""" Logging configuration for Code of Conquest. Sets up structured logging using structlog with JSON output and context-aware logging throughout the application. """ import logging import sys from pathlib import Path from typing import Optional import structlog from structlog.stdlib import LoggerFactory def setup_logging( log_level: str = "INFO", log_format: str = "json", log_file: Optional[str] = None ) -> None: """ Configure structured logging for the application. Args: log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) log_format: Output format ('json' or 'console') log_file: Optional path to log file Example: >>> from app.utils.logging import setup_logging >>> setup_logging(log_level="DEBUG", log_format="json") >>> logger = structlog.get_logger(__name__) >>> logger.info("Application started", version="0.1.0") """ # Convert log level string to logging constant numeric_level = getattr(logging, log_level.upper(), logging.INFO) # Configure standard library logging logging.basicConfig( format="%(message)s", stream=sys.stdout, level=numeric_level, ) # Create logs directory if logging to file if log_file: log_path = Path(log_file) log_path.parent.mkdir(parents=True, exist_ok=True) # Add file handler file_handler = logging.FileHandler(log_file) file_handler.setLevel(numeric_level) logging.root.addHandler(file_handler) # Configure structlog processors processors = [ # Add log level to event dict structlog.stdlib.add_log_level, # Add logger name to event dict structlog.stdlib.add_logger_name, # Add timestamp structlog.processors.TimeStamper(fmt="iso"), # Add stack info for exceptions structlog.processors.StackInfoRenderer(), # Format exceptions structlog.processors.format_exc_info, # Clean up _record and _from_structlog keys structlog.processors.UnicodeDecoder(), ] # Add format-specific processor if log_format == "json": # JSON output for production processors.append(structlog.processors.JSONRenderer()) else: # Console-friendly output for development processors.append( structlog.dev.ConsoleRenderer( colors=True, exception_formatter=structlog.dev.plain_traceback ) ) # Configure structlog structlog.configure( processors=processors, context_class=dict, logger_factory=LoggerFactory(), wrapper_class=structlog.stdlib.BoundLogger, cache_logger_on_first_use=True, ) def get_logger(name: str) -> structlog.stdlib.BoundLogger: """ Get a configured logger instance. Args: name: Logger name (typically __name__) Returns: BoundLogger: Configured structlog logger Example: >>> logger = get_logger(__name__) >>> logger.info("User logged in", user_id="123", email="user@example.com") """ return structlog.get_logger(name) class LoggerMixin: """ Mixin class to add logging capabilities to any class. Provides a `self.logger` attribute with context automatically bound to the class name. Example: >>> class MyService(LoggerMixin): ... def do_something(self, user_id): ... self.logger.info("Doing something", user_id=user_id) """ @property def logger(self) -> structlog.stdlib.BoundLogger: """Get logger for this class.""" if not hasattr(self, '_logger'): self._logger = get_logger(self.__class__.__name__) return self._logger # Common logging utilities def log_function_call(logger: structlog.stdlib.BoundLogger): """ Decorator to log function calls with arguments and return values. Args: logger: Logger instance to use Example: >>> logger = get_logger(__name__) >>> @log_function_call(logger) ... def process_data(data_id): ... return {"status": "processed"} """ def decorator(func): def wrapper(*args, **kwargs): logger.debug( f"Calling {func.__name__}", function=func.__name__, args=args, kwargs=kwargs ) try: result = func(*args, **kwargs) logger.debug( f"{func.__name__} completed", function=func.__name__, result=result ) return result except Exception as e: logger.error( f"{func.__name__} failed", function=func.__name__, error=str(e), exc_info=True ) raise return wrapper return decorator def log_ai_call( logger: structlog.stdlib.BoundLogger, user_id: str, model: str, tier: str, tokens_used: int, cost_estimate: float, context_type: str ) -> None: """ Log AI API call for cost tracking and analytics. Args: logger: Logger instance user_id: User making the request model: AI model used tier: Model tier (free, standard, premium) tokens_used: Number of tokens consumed cost_estimate: Estimated cost in USD context_type: Type of context (narrative, combat, etc.) """ logger.info( "AI call completed", event_type="ai_call", user_id=user_id, model=model, tier=tier, tokens_used=tokens_used, cost_estimate=cost_estimate, context_type=context_type ) def log_combat_action( logger: structlog.stdlib.BoundLogger, session_id: str, character_id: str, action_type: str, target_id: Optional[str] = None, damage: Optional[int] = None, effects: Optional[list] = None ) -> None: """ Log combat action for analytics and debugging. Args: logger: Logger instance session_id: Game session ID character_id: Acting character ID action_type: Type of action (attack, cast, item, defend) target_id: Target of action (if applicable) damage: Damage dealt (if applicable) effects: Effects applied (if applicable) """ logger.info( "Combat action executed", event_type="combat_action", session_id=session_id, character_id=character_id, action_type=action_type, target_id=target_id, damage=damage, effects=effects ) def log_marketplace_transaction( logger: structlog.stdlib.BoundLogger, transaction_id: str, buyer_id: str, seller_id: str, item_id: str, price: int, transaction_type: str ) -> None: """ Log marketplace transaction for analytics and auditing. Args: logger: Logger instance transaction_id: Transaction ID buyer_id: Buyer user ID seller_id: Seller user ID item_id: Item ID price: Transaction price transaction_type: Type of transaction """ logger.info( "Marketplace transaction", event_type="marketplace_transaction", transaction_id=transaction_id, buyer_id=buyer_id, seller_id=seller_id, item_id=item_id, price=price, transaction_type=transaction_type )