major-domo-v2/DECORATOR_MIGRATION_GUIDE.md
Cal Corum 8897b7fa5e CLAUDE: Add logged_command decorator and migrate Discord commands to reduce boilerplate
- 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>
2025-08-15 14:56:42 -05:00

326 lines
11 KiB
Markdown

# Discord Bot v2.0 - Logging Decorator Migration Guide
This guide documents the process for migrating existing Discord commands to use the new `@logged_command` decorator, which eliminates boilerplate logging code and standardizes command logging patterns.
## Overview
The `@logged_command` decorator automatically handles:
- Discord context setting with interaction details
- Operation timing and trace ID generation
- Command start/completion/failure logging
- Exception handling and logging
- Parameter logging with exclusion options
## What Was Changed
### Before (Manual Logging Pattern)
```python
@discord.app_commands.command(name="roster", description="Display team roster")
async def team_roster(self, interaction: discord.Interaction, abbrev: str):
set_discord_context(interaction=interaction, command="/roster")
trace_id = logger.start_operation("team_roster_command")
try:
logger.info("Team roster command started")
# Business logic here
logger.info("Team roster command completed successfully")
except Exception as e:
logger.error("Team roster command failed", error=e)
# Error handling
finally:
logger.end_operation(trace_id)
```
### After (With Decorator)
```python
@discord.app_commands.command(name="roster", description="Display team roster")
@logged_command("/roster")
async def team_roster(self, interaction: discord.Interaction, abbrev: str):
# Business logic only - no logging boilerplate needed
# All try/catch/finally logging is handled automatically
```
## Step-by-Step Migration Process
### 1. Update Imports
**Add the decorator import:**
```python
from utils.decorators import logged_command
```
**Remove unused logging imports (if no longer needed):**
```python
# Remove if not used elsewhere in the file:
from utils.logging import set_discord_context # Usually can be removed
```
### 2. Ensure Class Has Logger
**Before migration, ensure the command class has a logger:**
```python
class YourCommandCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.logger = get_contextual_logger(f'{__name__}.YourCommandCog') # Add this line
```
### 3. Apply the Decorator
**Add the decorator above the command method:**
```python
@discord.app_commands.command(name="your-command", description="...")
@logged_command("/your-command") # Add this line
async def your_command_method(self, interaction, ...):
```
### 4. Remove Manual Logging Boilerplate
**Remove these patterns:**
- `set_discord_context(interaction=interaction, command="...")`
- `trace_id = logger.start_operation("...")`
- `try:` / `except:` / `finally:` blocks used only for logging
- `logger.info("Command started")` and `logger.info("Command completed")`
- `logger.error("Command failed", error=e)` in catch blocks
- `logger.end_operation(trace_id)`
**Keep these:**
- Business logic logging (e.g., `logger.info("Team found", team_id=123)`)
- Specific error handling (user-facing error messages)
- All business logic and Discord interaction code
### 5. Test the Migration
**Run the tests to ensure the migration works:**
```bash
python -m pytest tests/test_utils_decorators.py -v
python -m pytest # Run all tests to ensure no regressions
```
## Example: Complete Migration
### commands/teams/roster.py (BEFORE)
```python
"""Team roster commands for Discord Bot v2.0"""
import logging
from typing import Optional, Dict, Any, List
import discord
from discord.ext import commands
from utils.logging import get_contextual_logger, set_discord_context
class TeamRosterCommands(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
# Missing: self.logger = get_contextual_logger(...)
@discord.app_commands.command(name="roster", description="Display team roster")
async def team_roster(self, interaction: discord.Interaction, abbrev: str):
set_discord_context(interaction=interaction, command="/roster")
trace_id = logger.start_operation("team_roster_command")
try:
await interaction.response.defer()
logger.info("Team roster command requested", team_abbrev=abbrev)
# Business logic
team = await team_service.get_team_by_abbrev(abbrev)
# ... more business logic ...
logger.info("Team roster displayed successfully")
except BotException as e:
logger.error("Bot error in team roster command", error=str(e))
# Error handling
except Exception as e:
logger.error("Unexpected error in team roster command", error=str(e))
# Error handling
finally:
logger.end_operation(trace_id)
```
### commands/teams/roster.py (AFTER)
```python
"""Team roster commands for Discord Bot v2.0"""
import logging
from typing import Optional, Dict, Any, List
import discord
from discord.ext import commands
from utils.logging import get_contextual_logger
from utils.decorators import logged_command # Added
class TeamRosterCommands(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.logger = get_contextual_logger(f'{__name__}.TeamRosterCommands') # Added
@discord.app_commands.command(name="roster", description="Display team roster")
@logged_command("/roster") # Added
async def team_roster(self, interaction: discord.Interaction, abbrev: str):
await interaction.response.defer()
# Business logic only - all boilerplate logging removed
team = await team_service.get_team_by_abbrev(abbrev)
if team is None:
self.logger.info("Team not found", team_abbrev=abbrev) # Business logic logging
# ... handle not found ...
return
# ... rest of business logic ...
self.logger.info("Team roster displayed successfully", # Business logic logging
team_id=team.id, team_abbrev=team.abbrev)
```
## Migration Checklist for Each Command
- [ ] Add `from utils.decorators import logged_command` import
- [ ] Ensure class has `self.logger = get_contextual_logger(...)` in `__init__`
- [ ] Add `@logged_command("/command-name")` decorator
- [ ] Remove `set_discord_context()` call
- [ ] Remove `trace_id = logger.start_operation()` call
- [ ] Remove `try:` block (if only used for logging)
- [ ] Remove `logger.info("Command started")` and `logger.info("Command completed")`
- [ ] Remove generic `except Exception as e:` blocks (if only used for logging)
- [ ] Remove `logger.error("Command failed")` calls
- [ ] Remove `finally:` block and `logger.end_operation()` call
- [ ] Keep business logic logging (specific info/debug/warning messages)
- [ ] Keep error handling that sends user-facing messages
- [ ] Test the command works correctly
## Decorator Options
### Basic Usage
```python
@logged_command("/command-name")
async def my_command(self, interaction, param1: str):
# Implementation
```
### Auto-Detect Command Name
```python
@logged_command() # Will use "/my-command" based on function name
async def my_command(self, interaction, param1: str):
# Implementation
```
### Exclude Sensitive Parameters
```python
@logged_command("/login", exclude_params=["password", "token"])
async def login_command(self, interaction, username: str, password: str):
# password won't appear in logs
```
### Disable Parameter Logging
```python
@logged_command("/sensitive-command", log_params=False)
async def sensitive_command(self, interaction, sensitive_data: str):
# No parameters will be logged
```
## Expected Benefits
### Lines of Code Reduction
- **Before**: ~25-35 lines per command (including try/catch/finally)
- **After**: ~10-15 lines per command
- **Reduction**: ~15-20 lines of boilerplate per command
### Consistency Improvements
- Standardized logging format across all commands
- Consistent error handling patterns
- Automatic trace ID generation and correlation
- Reduced chance of logging bugs (forgotten `end_operation`, etc.)
### Maintainability
- Single point of change for logging behavior
- Easier to add new logging features (e.g., performance metrics)
- Less code duplication
- Clearer separation of business logic and infrastructure
## Files to Migrate
Based on the current codebase structure, these files likely need migration:
```
commands/
├── league/
│ └── info.py
├── players/
│ └── info.py
└── teams/
├── info.py
└── roster.py # ✅ Already migrated (example)
```
## Testing Migration
### 1. Unit Tests
```bash
# Test the decorator itself
python -m pytest tests/test_utils_decorators.py -v
# Test migrated commands still work
python -m pytest tests/ -v
```
### 2. Integration Testing
```bash
# Verify command registration still works
python -c "
import discord
from commands.teams.roster import TeamRosterCommands
from discord.ext import commands
intents = discord.Intents.default()
bot = commands.Bot(command_prefix='!', intents=intents)
cog = TeamRosterCommands(bot)
print('✅ Command loads successfully')
"
```
### 3. Log Output Verification
After migration, verify that log entries still contain:
- Correct trace IDs for request correlation
- Command start/completion messages
- Error logging with exceptions
- Business logic messages
- Discord context (user_id, guild_id, etc.)
## Troubleshooting
### Common Issues
**Issue**: `AttributeError: 'YourCog' object has no attribute 'logger'`
**Solution**: Add `self.logger = get_contextual_logger(...)` to the cog's `__init__` method
**Issue**: Parameters not appearing in logs
**Solution**: Check if parameters are in the `exclude_params` list or if `log_params=False`
**Issue**: Command not registering with Discord
**Solution**: Ensure `@logged_command()` is placed AFTER `@discord.app_commands.command()`
**Issue**: Signature errors during command registration
**Solution**: The decorator preserves signatures automatically; if issues persist, check Discord.py version compatibility
### Debugging Steps
1. Check that all imports are correct
2. Verify logger exists on the cog instance
3. Run unit tests to ensure decorator functionality
4. Check log files for expected trace IDs and messages
5. Test command execution in a development environment
## Migration Timeline
**Recommended approach**: Migrate one command at a time and test thoroughly before moving to the next.
1. **Phase 1**: Migrate simple commands (no complex error handling)
2. **Phase 2**: Migrate commands with custom error handling
3. **Phase 3**: Migrate complex commands with multiple operations
4. **Phase 4**: Update documentation and add any additional decorator features
This approach ensures that any issues can be isolated and resolved before affecting multiple commands.