Files
Code_of_Conquest/api/app/utils/logging.py
2025-11-24 23:10:55 -06:00

273 lines
7.4 KiB
Python

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