"""
Logger initialization and configuration for Unipress games.
Provides centralized logging setup using Loguru with TOML configuration.
"""
import sys
from pathlib import Path
from loguru import logger
from unipress.core.settings import get_setting, load_settings
[docs]
def init_logger(game_name: str = None, **overrides) -> None:
"""
Initialize the logger with settings from TOML configuration.
Args:
game_name: Name of the game for per-game settings (optional)
**overrides: Direct setting overrides for logging configuration
"""
# Load settings with game-specific overrides if provided
if game_name:
settings = load_settings(game_name, **overrides)
else:
# Load global settings for system-wide logging
from unipress.core import settings as settings_module
settings = settings_module.get_default_settings()
# Apply any direct overrides
for key, value in overrides.items():
if "." in key:
# Handle nested keys like "logging.level"
parts = key.split(".")
current = settings
for part in parts[:-1]:
if part not in current:
current[part] = {}
current = current[part]
current[parts[-1]] = value
else:
settings[key] = value
# Remove default handler to avoid duplicate logs
logger.remove()
# Get logging configuration
log_level = get_setting(settings, "logging.level", "INFO")
console_enabled = get_setting(settings, "logging.console_enabled", True)
file_enabled = get_setting(settings, "logging.file_enabled", True)
# Use process-specific log files to avoid multi-process I/O conflicts
base_file_path = get_setting(
settings, "logging.file_path", "logs/unipress-{time:YYYY-MM-DD}.log"
)
# Add process name to log file path
if game_name:
# Replace the file extension with process name + extension
file_path = base_file_path.replace(".log", f"-{game_name}.log")
else:
file_path = base_file_path
rotation = get_setting(settings, "logging.rotation", "10 MB")
retention = get_setting(settings, "logging.retention", "30 days")
compression = get_setting(settings, "logging.compression", "gz")
log_format = get_setting(settings, "logging.format", "json")
# Console handler (human-readable for development)
# WARNING: If True can cause container freezing when high-frequency logging
if console_enabled:
if log_format == "human":
console_format = (
"<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
"<level>{level: <8}</level> | "
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | "
"<level>{message}</level>"
)
else:
# Structured format for console too
console_format = (
'{{"timestamp": "{time:YYYY-MM-DD HH:mm:ss.SSS}", '
'"level": "{level}", '
'"logger": "{name}", '
'"function": "{function}", '
'"line": {line}, '
'"message": "{message}"}}'
)
logger.add(
sys.stderr,
format=console_format,
level=log_level,
colorize=(log_format == "human"),
)
# File handler (always JSON for structured logging)
if file_enabled:
# Ensure logs directory exists
log_file_path = Path(file_path)
log_file_path.parent.mkdir(parents=True, exist_ok=True)
file_format = (
'{{"timestamp": "{time:YYYY-MM-DD HH:mm:ss.SSS}", '
'"level": "{level}", '
'"logger": "{name}", '
'"function": "{function}", '
'"line": {line}, '
'"process": {process}, '
'"thread": {thread}, '
'"message": "{message}"}}'
)
logger.add(
file_path,
format=file_format,
level=log_level,
rotation=rotation,
retention=retention,
compression=compression,
serialize=False, # We're already formatting as JSON
)
# Log the initialization
logger.info(
"Logger initialized",
extra={
"game_name": game_name,
"log_level": log_level,
"console_enabled": console_enabled,
"file_enabled": file_enabled,
"file_path": file_path if file_enabled else None,
},
)
[docs]
def get_logger(name: str = None):
"""
Get a logger instance with optional name binding.
Args:
name: Optional name to bind to the logger (e.g., game name, module name)
Returns:
Configured logger instance
"""
if name:
return logger.bind(logger_name=name)
return logger
# Convenience functions for common logging patterns
[docs]
def log_game_event(event: str, **context):
"""Log a game event with structured context."""
logger.info(f"Game event: {event}", extra=context)
[docs]
def log_player_action(action: str, **context):
"""Log a player action with structured context."""
logger.info(f"Player action: {action}", extra=context)
[docs]
def log_error(error: Exception, context: str = "", **extra_context):
"""Log an error with full context and traceback."""
logger.error(
f"Error: {context}",
extra={
"error_type": type(error).__name__,
"error_message": str(error),
**extra_context,
},
)
logger.exception("Full traceback:")