"""Structured JSON logging with request-ID correlation.""" from __future__ import annotations import contextvars import logging import sys import uuid from pythonjsonlogger import jsonlogger from starlette.middleware.base import BaseHTTPMiddleware from .settings import settings _request_id_var: contextvars.ContextVar[str] = contextvars.ContextVar( "request_id", default="-" ) def get_request_id() -> str: return _request_id_var.get() class RequestIdFilter(logging.Filter): def filter(self, record: logging.LogRecord) -> bool: record.request_id = _request_id_var.get() return True def configure_logging() -> None: """Configure root logger to emit JSON with request_id.""" root = logging.getLogger() # Remove any pre-existing handlers (uvicorn installs its own) for h in list(root.handlers): root.removeHandler(h) handler = logging.StreamHandler(sys.stdout) formatter = jsonlogger.JsonFormatter( fmt="%(asctime)s %(levelname)s %(name)s %(request_id)s %(message)s", rename_fields={"asctime": "ts", "levelname": "level", "name": "logger"}, ) handler.setFormatter(formatter) handler.addFilter(RequestIdFilter()) root.addHandler(handler) root.setLevel(getattr(logging, settings.log_level.upper(), logging.INFO)) # Calm down noisy loggers logging.getLogger("uvicorn.access").setLevel(logging.WARNING) class RequestIdMiddleware(BaseHTTPMiddleware): async def dispatch(self, request, call_next): rid = request.headers.get("X-Request-ID") or uuid.uuid4().hex[:12] token = _request_id_var.set(rid) try: response = await call_next(request) response.headers["X-Request-ID"] = rid return response finally: _request_id_var.reset(token)