first commit
This commit is contained in:
272
api/app/utils/logging.py
Normal file
272
api/app/utils/logging.py
Normal file
@@ -0,0 +1,272 @@
|
||||
"""
|
||||
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
|
||||
)
|
||||
Reference in New Issue
Block a user