from __future__ import annotations import logging from logging.config import dictConfig from pathlib import Path from typing import cast import structlog from core.config.settings import PROJECT_ROOT, RuntimeSettings, Settings, config from core.logging.formatters import ( build_plain_formatter, build_processor_formatter, ensure_message_key, ) from core.logging.filters import build_sensitive_data_processor from core.logging.handlers import build_file_handler_config def _ensure_log_dirs(runtime: RuntimeSettings) -> None: _resolve_log_path(runtime.log_dir).mkdir(parents=True, exist_ok=True) _resolve_log_path(runtime.log_error_dir).mkdir(parents=True, exist_ok=True) def _resolve_log_path(path: str) -> Path: candidate = Path(path) if candidate.is_absolute(): return candidate return PROJECT_ROOT / candidate def build_logging_config(runtime: RuntimeSettings) -> dict[str, object]: log_dir = _resolve_log_path(runtime.log_dir) error_dir = _resolve_log_path(runtime.log_error_dir) formatter_name = "json" if runtime.log_json else "plain" file_handler = build_file_handler_config( runtime, file_path=log_dir / runtime.log_file_name, level=runtime.log_level, formatter=formatter_name, ) error_handler = build_file_handler_config( runtime, file_path=error_dir / runtime.log_error_file_name, level="ERROR", formatter=formatter_name, filters=["error_only"], ) return { "version": 1, "disable_existing_loggers": False, "filters": { "error_only": { "()": "core.logging.filters.ErrorLevelFilter", } }, "formatters": { "json": { "()": build_processor_formatter, "sensitive_fields": runtime.log_sensitive_fields, }, "plain": { "()": build_plain_formatter, "sensitive_fields": runtime.log_sensitive_fields, }, }, "handlers": { "file": file_handler, "error": error_handler, }, "root": { "handlers": ["file", "error"], "level": runtime.log_level, }, } def configure_logging(settings: Settings | None = None) -> None: active_settings = settings if settings is not None else cast(Settings, config) runtime = active_settings.runtime try: _ensure_log_dirs(runtime) dictConfig(build_logging_config(runtime)) except (OSError, ValueError) as exc: logging.basicConfig(level=runtime.log_level) logging.getLogger(__name__).error("Logging setup failed", exc_info=exc) structlog.configure( processors=[ structlog.contextvars.merge_contextvars, structlog.processors.add_log_level, structlog.processors.TimeStamper(fmt="iso", utc=True), structlog.processors.CallsiteParameterAdder( parameters=[ structlog.processors.CallsiteParameter.MODULE, structlog.processors.CallsiteParameter.FUNC_NAME, structlog.processors.CallsiteParameter.LINENO, ] ), build_sensitive_data_processor(runtime.log_sensitive_fields), ensure_message_key, structlog.processors.format_exc_info, structlog.processors.UnicodeDecoder(), structlog.stdlib.ProcessorFormatter.wrap_for_formatter, ], logger_factory=structlog.stdlib.LoggerFactory(), cache_logger_on_first_use=True, )