"""Structured logging with correlation IDs.""" from __future__ import annotations import logging import sys import uuid from contextvars import ContextVar import structlog correlation_id_var: ContextVar[str] = ContextVar("correlation_id", default="") def get_correlation_id() -> str: cid = correlation_id_var.get() if not cid: cid = uuid.uuid4().hex[:16] correlation_id_var.set(cid) return cid def add_correlation_id( logger: structlog.types.WrappedLogger, method_name: str, event_dict: dict, ) -> dict: event_dict["correlation_id"] = get_correlation_id() return event_dict def setup_logging(log_level: str = "INFO", log_format: str = "json") -> None: shared_processors: list = [ structlog.contextvars.merge_contextvars, add_correlation_id, structlog.stdlib.add_log_level, structlog.stdlib.add_logger_name, structlog.processors.TimeStamper(fmt="iso"), structlog.processors.StackInfoRenderer(), structlog.processors.UnicodeDecoder(), ] if log_format == "json": renderer = structlog.processors.JSONRenderer() else: renderer = structlog.dev.ConsoleRenderer() structlog.configure( processors=[ *shared_processors, structlog.stdlib.ProcessorFormatter.wrap_for_formatter, ], logger_factory=structlog.stdlib.LoggerFactory(), wrapper_class=structlog.stdlib.BoundLogger, cache_logger_on_first_use=True, ) formatter = structlog.stdlib.ProcessorFormatter( processors=[ structlog.stdlib.ProcessorFormatter.remove_processors_meta, renderer, ], foreign_pre_chain=shared_processors, ) handler = logging.StreamHandler(sys.stdout) handler.setFormatter(formatter) root = logging.getLogger() root.handlers.clear() root.addHandler(handler) root.setLevel(getattr(logging, log_level.upper(), logging.INFO)) for name in ("uvicorn", "uvicorn.access", "uvicorn.error"): lg = logging.getLogger(name) lg.handlers.clear() lg.propagate = True def get_logger(name: str | None = None) -> structlog.stdlib.BoundLogger: return structlog.get_logger(name)