Skip to content

Commit 004c154

Browse files
committed
feat: implement core bot infrastructure and monitoring system
- Add comprehensive task management system - Implement distributed tracing and monitoring - Refactor bot core with new sentry integration - Update database controllers with tracing support - Rename emoji.py to emoji_manager.py
1 parent 3e4f69e commit 004c154

File tree

14 files changed

+2887
-823
lines changed

14 files changed

+2887
-823
lines changed

tux/app.py

Lines changed: 90 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,156 +1,152 @@
1-
"""TuxApp: Orchestration and lifecycle management for the Tux Discord bot."""
1+
"""
2+
TuxApp: Main application entrypoint and lifecycle orchestrator.
3+
4+
This module contains the `TuxApp` class, which serves as the primary entrypoint
5+
for the Tux Discord bot. It is responsible for:
6+
7+
- **Environment Setup**: Validating configuration, initializing Sentry, and setting
8+
up OS-level signal handlers for graceful shutdown.
9+
- **Bot Instantiation**: Creating the instance of the `Tux` bot class with the
10+
appropriate intents, command prefix logic, and owner IDs.
11+
- **Lifecycle Management**: Starting the asyncio event loop and managing the
12+
bot's main `start` and `shutdown` sequence, including handling `KeyboardInterrupt`.
13+
"""
214

315
import asyncio
416
import signal
5-
from types import FrameType
617

718
import discord
8-
import sentry_sdk
919
from loguru import logger
1020

1121
from tux.bot import Tux
1222
from tux.help import TuxHelp
1323
from tux.utils.config import CONFIG
14-
from tux.utils.env import get_current_env
15-
16-
17-
async def get_prefix(bot: Tux, message: discord.Message) -> list[str]:
18-
"""Resolve the command prefix for a guild or use the default prefix."""
19-
prefix: str | None = None
20-
if message.guild:
21-
try:
22-
from tux.database.controllers import DatabaseController # noqa: PLC0415
23-
24-
prefix = await DatabaseController().guild_config.get_guild_prefix(message.guild.id)
25-
except Exception as e:
26-
logger.error(f"Error getting guild prefix: {e}")
27-
return [prefix or CONFIG.DEFAULT_PREFIX]
24+
from tux.utils.sentry_manager import SentryManager
2825

2926

3027
class TuxApp:
31-
"""Orchestrates the startup, shutdown, and environment for the Tux bot."""
32-
33-
def __init__(self):
34-
"""Initialize the TuxApp with no bot instance yet."""
35-
self.bot = None
36-
37-
def run(self) -> None:
38-
"""Run the Tux bot application (entrypoint for CLI)."""
39-
asyncio.run(self.start())
28+
"""
29+
Orchestrates the startup, shutdown, and environment for the Tux bot.
4030
41-
def setup_sentry(self) -> None:
42-
"""Initialize Sentry for error monitoring and tracing."""
43-
if not CONFIG.SENTRY_DSN:
44-
logger.warning("No Sentry DSN configured, skipping Sentry setup")
45-
return
46-
47-
logger.info("Setting up Sentry...")
48-
49-
try:
50-
sentry_sdk.init(
51-
dsn=CONFIG.SENTRY_DSN,
52-
release=CONFIG.BOT_VERSION,
53-
environment=get_current_env(),
54-
enable_tracing=True,
55-
attach_stacktrace=True,
56-
send_default_pii=False,
57-
traces_sample_rate=1.0,
58-
profiles_sample_rate=1.0,
59-
_experiments={
60-
"enable_logs": True, # https://docs.sentry.io/platforms/python/logs/
61-
},
62-
)
63-
64-
# Add additional global tags
65-
sentry_sdk.set_tag("discord_library_version", discord.__version__)
31+
This class is not a `discord.py` cog, but rather a top-level application
32+
runner that manages the bot's entire lifecycle from an OS perspective.
33+
"""
6634

67-
logger.info(f"Sentry initialized: {sentry_sdk.is_initialized()}")
35+
# --- Initialization ---
6836

69-
except Exception as e:
70-
logger.error(f"Failed to initialize Sentry: {e}")
71-
72-
def setup_signals(self) -> None:
73-
"""Set up signal handlers for graceful shutdown."""
74-
signal.signal(signal.SIGTERM, self.handle_sigterm)
75-
signal.signal(signal.SIGINT, self.handle_sigterm)
76-
77-
def handle_sigterm(self, signum: int, frame: FrameType | None) -> None:
78-
"""Handle SIGTERM/SIGINT by raising KeyboardInterrupt for graceful shutdown."""
79-
logger.info(f"Received signal {signum}")
37+
def __init__(self):
38+
"""Initializes the TuxApp, setting the bot instance to None initially."""
39+
self.bot: Tux | None = None
8040

81-
if sentry_sdk.is_initialized():
82-
with sentry_sdk.push_scope() as scope:
83-
scope.set_tag("signal.number", signum)
84-
scope.set_tag("lifecycle.event", "termination_signal")
41+
# --- Application Lifecycle ---
8542

86-
sentry_sdk.add_breadcrumb(
87-
category="lifecycle",
88-
message=f"Received termination signal {signum}",
89-
level="info",
90-
)
43+
def run(self) -> None:
44+
"""
45+
The main synchronous entrypoint for the application.
9146
92-
raise KeyboardInterrupt
47+
This method starts the asyncio event loop and runs the primary `start`
48+
coroutine, effectively launching the bot.
49+
"""
50+
asyncio.run(self.start())
9351

94-
def validate_config(self) -> bool:
95-
"""Validate that all required configuration is present."""
96-
if not CONFIG.BOT_TOKEN:
97-
logger.critical("No bot token provided. Set DEV_BOT_TOKEN or PROD_BOT_TOKEN in your .env file.")
98-
return False
52+
async def start(self) -> None:
53+
"""
54+
The main asynchronous entrypoint for the application.
9955
100-
return True
56+
This method orchestrates the entire bot startup sequence: setting up
57+
Sentry and signal handlers, validating config, creating the `Tux`
58+
instance, and connecting to Discord. It includes a robust
59+
try/except/finally block to ensure graceful shutdown.
60+
"""
10161

102-
async def start(self) -> None:
103-
"""Start the Tux bot, handling setup, errors, and shutdown."""
104-
self.setup_sentry()
62+
# Initialize Sentry
63+
SentryManager.setup()
10564

65+
# Set up signal handlers
10666
self.setup_signals()
10767

68+
# Validate config
10869
if not self.validate_config():
10970
return
11071

72+
# Configure owner IDs, dynamically adding sysadmins if configured.
73+
# This allows specified users to have access to sensitive commands like `eval`.
11174
owner_ids = {CONFIG.BOT_OWNER_ID}
112-
11375
if CONFIG.ALLOW_SYSADMINS_EVAL:
11476
logger.warning(
115-
"⚠️ Eval is enabled for sysadmins, this is potentially dangerous; see settings.yml.example for more info.",
77+
"⚠️ Eval is enabled for sysadmins, this is potentially dangerous; "
78+
"see settings.yml.example for more info.",
11679
)
11780
owner_ids.update(CONFIG.SYSADMIN_IDS)
118-
11981
else:
12082
logger.warning("🔒️ Eval is disabled for sysadmins; see settings.yml.example for more info.")
12183

84+
# Instantiate the main bot class with all necessary parameters.
12285
self.bot = Tux(
123-
command_prefix=get_prefix,
12486
strip_after_prefix=True,
12587
case_insensitive=True,
12688
intents=discord.Intents.all(),
127-
# owner_ids={CONFIG.BOT_OWNER_ID, *CONFIG.SYSADMIN_IDS},
12889
owner_ids=owner_ids,
12990
allowed_mentions=discord.AllowedMentions(everyone=False),
13091
help_command=TuxHelp(),
13192
activity=None,
13293
status=discord.Status.online,
13394
)
13495

96+
# Start the bot
13597
try:
98+
# This is the main blocking call that connects to Discord and runs the bot.
13699
await self.bot.start(CONFIG.BOT_TOKEN, reconnect=True)
137100

138101
except KeyboardInterrupt:
102+
# This is caught when the user presses Ctrl+C.
139103
logger.info("Shutdown requested (KeyboardInterrupt)")
140104
except Exception as e:
141-
logger.critical(f"Bot failed to start: {e}")
142-
await self.shutdown()
143-
105+
# Catch any other unexpected exception during bot runtime.
106+
logger.critical(f"Bot failed to start or run: {e}")
144107
finally:
108+
# Ensure that shutdown is always called to clean up resources.
145109
await self.shutdown()
146110

147111
async def shutdown(self) -> None:
148-
"""Gracefully shut down the bot and flush Sentry."""
112+
"""
113+
Gracefully shuts down the bot and its resources.
114+
115+
This involves calling the bot's internal shutdown sequence and then
116+
flushing any remaining Sentry events to ensure all data is sent.
117+
"""
149118
if self.bot and not self.bot.is_closed():
150119
await self.bot.shutdown()
151120

152-
if sentry_sdk.is_initialized():
153-
sentry_sdk.flush()
154-
await asyncio.sleep(0.1)
121+
SentryManager.flush()
122+
await asyncio.sleep(0.1) # Brief pause to allow buffers to flush
155123

156124
logger.info("Shutdown complete")
125+
126+
# --- Environment Setup ---
127+
128+
def setup_signals(self) -> None:
129+
"""
130+
Sets up OS-level signal handlers for graceful shutdown.
131+
132+
This ensures that when the bot process receives a SIGINT (Ctrl+C) or
133+
SIGTERM (from systemd or Docker), it is intercepted and handled
134+
cleanly instead of causing an abrupt exit.
135+
"""
136+
signal.signal(signal.SIGTERM, SentryManager.report_signal)
137+
signal.signal(signal.SIGINT, SentryManager.report_signal)
138+
139+
def validate_config(self) -> bool:
140+
"""
141+
Performs a pre-flight check for essential configuration.
142+
143+
Returns
144+
-------
145+
bool
146+
True if the configuration is valid, False otherwise.
147+
"""
148+
if not CONFIG.BOT_TOKEN:
149+
logger.critical("No bot token provided. Set DEV_BOT_TOKEN or PROD_BOT_TOKEN in your .env file.")
150+
return False
151+
152+
return True

0 commit comments

Comments
 (0)