- Add @logged_command decorator in utils/decorators.py to eliminate try/catch/finally boilerplate - Migrate all Discord commands to use new decorator pattern: * commands/league/info.py - /league command * commands/players/info.py - /player command * commands/teams/info.py - /team and /teams commands * commands/teams/roster.py - /roster command - Fix PyLance type issues by making model IDs required for database entities - Update Player and Team models to require id field since they come from database - Fix test cases to provide required id values - Add comprehensive test coverage for decorator functionality - Add migration guide for applying decorator to additional commands - Reduce codebase by ~100 lines of repetitive logging boilerplate 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
92 lines
3.9 KiB
Python
92 lines
3.9 KiB
Python
"""
|
|
Command decorators for Discord bot v2.0
|
|
|
|
This module provides decorators to reduce boilerplate code in Discord commands,
|
|
particularly for logging and error handling.
|
|
"""
|
|
|
|
import inspect
|
|
from functools import wraps
|
|
from typing import List, Optional
|
|
from utils.logging import set_discord_context, get_contextual_logger
|
|
|
|
|
|
def logged_command(
|
|
command_name: Optional[str] = None,
|
|
log_params: bool = True,
|
|
exclude_params: Optional[List[str]] = None
|
|
):
|
|
"""
|
|
Decorator for Discord commands that adds comprehensive logging.
|
|
|
|
This decorator automatically handles:
|
|
- Setting Discord context with interaction details
|
|
- Starting/ending operation timing
|
|
- Logging command start/completion/failure
|
|
- Preserving function metadata and signature
|
|
|
|
Args:
|
|
command_name: Override command name (defaults to function name with slashes)
|
|
log_params: Whether to log command parameters (default: True)
|
|
exclude_params: List of parameter names to exclude from logging
|
|
|
|
Example:
|
|
@logged_command("/roster", exclude_params=["sensitive_data"])
|
|
async def team_roster(self, interaction, team_name: str, season: int = None):
|
|
# Clean business logic only - no logging boilerplate needed
|
|
team = await team_service.find_team(team_name)
|
|
players = await team_service.get_roster(team.id, season)
|
|
embed = create_roster_embed(team, players)
|
|
await interaction.followup.send(embed=embed)
|
|
|
|
Side Effects:
|
|
- Automatically sets Discord context for all subsequent log entries
|
|
- Creates trace_id for request correlation
|
|
- Logs command execution timing and results
|
|
- Re-raises all exceptions after logging (preserves original behavior)
|
|
|
|
Requirements:
|
|
- The decorated class must have a 'logger' attribute, or one will be created
|
|
- Function must be an async method with (self, interaction, ...) signature
|
|
- Preserves Discord.py command registration compatibility
|
|
"""
|
|
def decorator(func):
|
|
@wraps(func)
|
|
async def wrapper(self, interaction, *args, **kwargs):
|
|
# Auto-detect command name if not provided
|
|
cmd_name = command_name or f"/{func.__name__.replace('_', '-')}"
|
|
|
|
# Build context with safe parameter logging
|
|
context = {"command": cmd_name}
|
|
if log_params:
|
|
sig = inspect.signature(func)
|
|
param_names = list(sig.parameters.keys())[2:] # Skip self, interaction
|
|
exclude_set = set(exclude_params or [])
|
|
|
|
for i, (name, value) in enumerate(zip(param_names, args)):
|
|
if name not in exclude_set:
|
|
context[f"param_{name}"] = value
|
|
|
|
set_discord_context(interaction=interaction, **context)
|
|
|
|
# Get logger from the class instance or create one
|
|
logger = getattr(self, 'logger', get_contextual_logger(f'{self.__class__.__module__}.{self.__class__.__name__}'))
|
|
trace_id = logger.start_operation(f"{func.__name__}_command")
|
|
|
|
try:
|
|
logger.info(f"{cmd_name} command started")
|
|
result = await func(self, interaction, *args, **kwargs)
|
|
logger.info(f"{cmd_name} command completed successfully")
|
|
logger.end_operation(trace_id, "completed")
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.error(f"{cmd_name} command failed", error=e)
|
|
logger.end_operation(trace_id, "failed")
|
|
# Re-raise to maintain original exception handling behavior
|
|
raise
|
|
|
|
# Preserve signature for Discord.py command registration
|
|
wrapper.__signature__ = inspect.signature(func)
|
|
return wrapper
|
|
return decorator |