"""Per-user rotating audit logger for tracking events.""" import logging import os from logging.handlers import RotatingFileHandler from config.paths import get_logs_dir from models.tracking_event import TrackingEvent # Store handlers per user_id to avoid recreating _user_loggers = {} def get_tracking_logger(user_id: str) -> logging.Logger: """ Get or create a per-user rotating file logger for tracking events. Args: user_id: User ID for the log file Returns: Logger instance configured for the user """ if user_id in _user_loggers: return _user_loggers[user_id] logs_dir = get_logs_dir() os.makedirs(logs_dir, exist_ok=True) log_file = os.path.join(logs_dir, f'tracking_user_{user_id}.log') logger = logging.getLogger(f'tracking.user.{user_id}') logger.setLevel(logging.INFO) logger.propagate = False # Don't propagate to root logger # Rotating file handler: 10MB max, keep 5 backups handler = RotatingFileHandler( log_file, maxBytes=10 * 1024 * 1024, # 10MB backupCount=5, encoding='utf-8' ) formatter = logging.Formatter( '%(asctime)s | %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) handler.setFormatter(formatter) logger.addHandler(handler) _user_loggers[user_id] = logger return logger def log_tracking_event(event: TrackingEvent) -> None: """ Log a tracking event to the user's audit log file. Args: event: TrackingEvent to log """ if not event.user_id: # If user was deleted (anonymized), skip logging return logger = get_tracking_logger(event.user_id) log_msg = ( f"user_id={event.user_id} | " f"child_id={event.child_id} | " f"entity_type={event.entity_type} | " f"entity_id={event.entity_id} | " f"action={event.action} | " f"points_before={event.points_before} | " f"points_after={event.points_after} | " f"delta={event.delta:+d} | " f"occurred_at={event.occurred_at}" ) if event.metadata: metadata_str = ' | '.join(f"{k}={v}" for k, v in event.metadata.items()) log_msg += f" | {metadata_str}" logger.info(log_msg)