from __future__ import annotations import logging from logging.handlers import RotatingFileHandler from pathlib import Path from typing import Any from iti.config import BaseConfig class SafeFormatter(logging.Formatter): def format(self, record: logging.LogRecord) -> str: for key in ("trace_id", "request_id", "actor_type", "actor_id", "response_code"): if not hasattr(record, key): setattr(record, key, "-") return super().format(record) def configure_logging(config: BaseConfig) -> None: level = getattr(logging, config.log_level.upper(), logging.INFO) formatter = SafeFormatter( "%(asctime)s %(levelname)s %(name)s " "trace=%(trace_id)s actor=%(actor_type)s:%(actor_id)s code=%(response_code)s - %(message)s" ) root_logger = logging.getLogger("iti") root_logger.setLevel(level) root_logger.handlers.clear() root_logger.propagate = False console_handler = logging.StreamHandler() console_handler.setLevel(level) console_handler.setFormatter(formatter) root_logger.addHandler(console_handler) error_logger = logging.getLogger("iti.error") error_logger.setLevel(logging.ERROR) error_logger.handlers.clear() error_logger.propagate = False if config.log_file_enabled: log_dir = Path(config.log_dir) log_dir.mkdir(parents=True, exist_ok=True) app_handler = RotatingFileHandler( log_dir / "app.log", encoding="utf-8", maxBytes=config.log_max_bytes, backupCount=config.log_backup_count, ) app_handler.setLevel(level) app_handler.setFormatter(formatter) root_logger.addHandler(app_handler) error_handler = RotatingFileHandler( log_dir / "error.log", encoding="utf-8", maxBytes=config.log_max_bytes, backupCount=config.log_backup_count, ) error_handler.setLevel(logging.ERROR) error_handler.setFormatter(formatter) root_logger.addHandler(error_handler) error_logger.addHandler(error_handler) def log_extra(request: Any | None = None) -> dict[str, Any]: if request is None: return { "trace_id": "-", "request_id": "-", "actor_type": "-", "actor_id": "-", "response_code": "-", } actor = getattr(request.state, "actor", None) principal = getattr(request.state, "principal", None) return { "trace_id": getattr(request.state, "trace_id", "-"), "request_id": getattr(request.state, "request_id", "-"), "actor_type": getattr(actor, "type", None) or ("user" if principal else "-"), "actor_id": getattr(actor, "id", None) or getattr(principal, "id", "-"), "response_code": getattr(request.state, "response_code", "-"), }