"""Centralized runtime logging bootstrap.""" import json import logging import sys from collections.abc import Iterable from typing import Any, TextIO import structlog _UVICORN_LOGGER_NAMES = ("uvicorn", "uvicorn.error", "uvicorn.access") def configure_logging( *, log_level: int = logging.INFO, stream: TextIO | None = None, ) -> None: resolved_stream = sys.stdout if stream is None else stream formatter = structlog.stdlib.ProcessorFormatter( foreign_pre_chain=list(_shared_processors()), processors=[ structlog.stdlib.ProcessorFormatter.remove_processors_meta, structlog.processors.JSONRenderer(sort_keys=True, ensure_ascii=False), ], ) handler = logging.StreamHandler(resolved_stream) handler.setLevel(log_level) handler.setFormatter(formatter) structlog.reset_defaults() structlog.configure( processors=[ structlog.stdlib.PositionalArgumentsFormatter(), _render_context_in_event, *_shared_processors(), structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, structlog.stdlib.ProcessorFormatter.wrap_for_formatter, ], logger_factory=structlog.stdlib.LoggerFactory(), wrapper_class=structlog.stdlib.BoundLogger, cache_logger_on_first_use=True, ) _configure_logger(logging.getLogger(), handler=handler, log_level=log_level) for logger_name in _UVICORN_LOGGER_NAMES: _configure_logger( logging.getLogger(logger_name), handler=handler, log_level=log_level, propagate=False, ) def _shared_processors() -> tuple[structlog.types.Processor, ...]: return ( structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.processors.TimeStamper(fmt="iso", utc=True), ) def _render_context_in_event( _logger: Any, _method_name: str, event_dict: structlog.types.EventDict, ) -> structlog.types.EventDict: event = str(event_dict.get("event", "")) context = " ".join( f"{key}={_serialize_log_value(value)}" for key, value in sorted(event_dict.items()) if key not in {"event", "exc_info", "stack_info"} ) event_dict["event"] = f"{event} {context}" if context else event return event_dict def _serialize_log_value(value: object) -> str: return json.dumps( value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str, ) def _configure_logger( logger: logging.Logger, *, handler: logging.Handler, log_level: int, propagate: bool = True, ) -> None: _clear_handlers(logger.handlers) logger.setLevel(log_level) logger.handlers = [handler] logger.propagate = propagate def _clear_handlers(handlers: Iterable[logging.Handler]) -> None: for existing_handler in list(handlers): existing_handler.close()