"""Structured, JSON-compatible logging with per-request correlation IDs. Every request gets a request ID (from the `X-Request-ID` header if present, otherwise generated) bound into the log context for the lifetime of that request, via `RequestIDMiddleware` in `app.main`. """ from __future__ import annotations import logging import sys from contextvars import ContextVar import structlog from app.core.config import get_settings request_id_ctx: ContextVar[str | None] = ContextVar("request_id", default=None) def _add_request_id(_logger: object, _method_name: str, event_dict: dict) -> dict: request_id = request_id_ctx.get() if request_id is not None: event_dict["request_id"] = request_id return event_dict def configure_logging() -> None: settings = get_settings() level = getattr(logging, settings.AURORA_LOG_LEVEL.upper(), logging.INFO) logging.basicConfig( format="%(message)s", stream=sys.stdout, level=level, ) shared_processors: list = [ structlog.contextvars.merge_contextvars, _add_request_id, structlog.processors.add_log_level, structlog.processors.TimeStamper(fmt="iso"), structlog.processors.StackInfoRenderer(), ] renderer: structlog.types.Processor if settings.is_production or settings.AURORA_ENV != "development": renderer = structlog.processors.JSONRenderer() else: renderer = structlog.dev.ConsoleRenderer() structlog.configure( processors=[*shared_processors, structlog.processors.format_exc_info, renderer], wrapper_class=structlog.make_filtering_bound_logger(level), context_class=dict, logger_factory=structlog.PrintLoggerFactory(), cache_logger_on_first_use=True, ) def get_logger(name: str | None = None) -> structlog.stdlib.BoundLogger: return structlog.get_logger(name)