"""Structured JSON logging. Every log record is a single JSON line. Anything related to a job carries the job_id so the whole conversion can be reconstructed from the log. """ import json import logging import sys from contextvars import ContextVar current_job_id: ContextVar[str | None] = ContextVar("current_job_id", default=None) _RESERVED = { "args", "asctime", "created", "exc_info", "exc_text", "filename", "funcName", "levelname", "levelno", "lineno", "module", "msecs", "message", "msg", "name", "pathname", "process", "processName", "relativeCreated", "stack_info", "thread", "threadName", "taskName", } class JsonFormatter(logging.Formatter): def format(self, record: logging.LogRecord) -> str: payload: dict[str, object] = { "ts": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"), "level": record.levelname, "logger": record.name, "message": record.getMessage(), } job_id = getattr(record, "job_id", None) or current_job_id.get() if job_id: payload["job_id"] = job_id for key, value in record.__dict__.items(): if key not in _RESERVED and not key.startswith("_") and key != "job_id": payload[key] = value if record.exc_info: payload["exception"] = self.formatException(record.exc_info) return json.dumps(payload, ensure_ascii=False, default=str) def setup_logging(level: str) -> None: handler = logging.StreamHandler(sys.stdout) handler.setFormatter(JsonFormatter()) root = logging.getLogger() root.handlers.clear() root.addHandler(handler) root.setLevel(getattr(logging, level, logging.INFO)) # uvicorn keeps its own handlers, route them through ours as well for name in ("uvicorn", "uvicorn.access", "uvicorn.error"): logger = logging.getLogger(name) logger.handlers.clear() logger.propagate = True