"""Structlog initialization. Single entry point :func:`configure_logging` sets up structlog with an ``APP_ENV``-driven renderer: a pretty console renderer during development and JSON-lines output in production so logs plug straight into any log aggregator. Must be called exactly once at app startup, before any module obtains a logger via ``structlog.get_logger()``. """ from __future__ import annotations import logging import sys from typing import Any import structlog def configure_logging(app_env: str) -> None: """Configure structlog + stdlib logging for the application. Parameters ---------- app_env: The resolved ``APP_ENV`` value. ``"development"`` selects a colorized console renderer; anything else (including ``"production"``) selects the JSON renderer so logs are machine-parseable. Notes ----- This function is idempotent within a process: structlog accepts repeated calls to :func:`structlog.configure`. It deliberately keeps the setup small for Phase 0 — a proper ``ProcessorFormatter`` bridge between stdlib and structlog can be layered in later without touching call sites. """ # Route stdlib logging through a root handler writing to stdout. Our # own loggers go through structlog's pipeline; third-party libraries # that use the stdlib logger will at least surface at INFO. logging.basicConfig( level=logging.INFO, stream=sys.stdout, format="%(message)s", ) # Processors shared across environments. Order matters: contextvars # first so bound context is present for every later step; exception # info extraction before rendering so tracebacks serialize cleanly. shared_processors: list[Any] = [ structlog.contextvars.merge_contextvars, structlog.processors.add_log_level, structlog.processors.TimeStamper(fmt="iso", utc=True), structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, ] # Environment-specific final renderer. Dev gets human-friendly # colorized output; everywhere else emits JSON lines. if app_env == "development": final_renderer: Any = structlog.dev.ConsoleRenderer(colors=True) else: final_renderer = structlog.processors.JSONRenderer() structlog.configure( processors=[*shared_processors, final_renderer], # PrintLoggerFactory writes to stdout without requiring a stdlib # logger bridge; sufficient for Phase 0. logger_factory=structlog.PrintLoggerFactory(), wrapper_class=structlog.make_filtering_bound_logger(logging.INFO), cache_logger_on_first_use=True, )