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

11 KiB

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)

@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)

@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:

from utils.decorators import logged_command

Remove unused logging imports (if no longer needed):

# 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:

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:

@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:

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)

"""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)

"""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

@logged_command("/command-name")
async def my_command(self, interaction, param1: str):
    # Implementation

Auto-Detect Command Name

@logged_command()  # Will use "/my-command" based on function name
async def my_command(self, interaction, param1: str):
    # Implementation

Exclude Sensitive Parameters

@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

@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

# 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

# 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.