""" Logging setup — structured JSON in production, human-readable in dev. Switch with env var LOG_FORMAT=json|text (default: text for local dev). Every /chat request gets a `request_id` UUID; pass it via `extra={...}` on every log call inside the request, and downstream tooling can trace the question through retrieve → LLM → response. """ from __future__ import annotations import json import logging import os import sys from datetime import datetime, timezone # These keys are part of the default LogRecord and are not user payload — # everything else found on the record was added via `extra=` and should ship. _STANDARD_ATTRS = { "name", "msg", "args", "levelname", "levelno", "pathname", "filename", "module", "exc_info", "exc_text", "stack_info", "lineno", "funcName", "created", "msecs", "relativeCreated", "thread", "threadName", "processName", "process", "message", "taskName", } class JsonFormatter(logging.Formatter): def format(self, record: logging.LogRecord) -> str: payload: dict = { "ts": datetime.now(timezone.utc).isoformat(), "level": record.levelname, "logger": record.name, "msg": record.getMessage(), } for k, v in record.__dict__.items(): if k not in _STANDARD_ATTRS and not k.startswith("_"): payload[k] = v if record.exc_info: payload["exc"] = self.formatException(record.exc_info) return json.dumps(payload, ensure_ascii=False, default=str) def configure_logging() -> None: fmt = os.getenv("LOG_FORMAT", "text").lower() handler = logging.StreamHandler(sys.stdout) if fmt == "json": handler.setFormatter(JsonFormatter()) else: handler.setFormatter(logging.Formatter( "%(asctime)s %(levelname)s %(name)s %(message)s" )) root = logging.getLogger() # Replace any pre-existing handlers so uvicorn's default doesn't double-log. for h in list(root.handlers): root.removeHandler(h) root.addHandler(handler) root.setLevel(os.getenv("LOG_LEVEL", "INFO").upper())