Spaces:
Sleeping
Sleeping
| """JSON logging configured to carry per-request correlation IDs.""" | |
| from __future__ import annotations | |
| import logging | |
| from contextvars import ContextVar | |
| from opentelemetry import trace | |
| from pythonjsonlogger.json import JsonFormatter | |
| request_id_var: ContextVar[str | None] = ContextVar("request_id", default=None) | |
| user_id_var: ContextVar[str | None] = ContextVar("user_id", default=None) | |
| class CorrelationFilter(logging.Filter): | |
| """Attach correlation IDs from contextvars and the current span onto records.""" | |
| def filter(self, record: logging.LogRecord) -> bool: | |
| record.request_id = request_id_var.get() | |
| record.user_id = user_id_var.get() | |
| span = trace.get_current_span() | |
| ctx = span.get_span_context() if span else None | |
| if ctx and ctx.is_valid: | |
| # Format ids as in the OTel spec — zero-padded lowercase hex. | |
| record.trace_id = f"{ctx.trace_id:032x}" | |
| record.span_id = f"{ctx.span_id:016x}" | |
| else: | |
| record.trace_id = None | |
| record.span_id = None | |
| return True | |
| def configure_logging(level: str = "INFO") -> None: | |
| """Replace the root handler with a JSON one. Idempotent.""" | |
| formatter = JsonFormatter( | |
| "{asctime} {levelname} {name} {message} {request_id} {user_id} {trace_id} {span_id}", | |
| style="{", | |
| rename_fields={"asctime": "ts", "levelname": "level", "name": "logger"}, | |
| ) | |
| handler = logging.StreamHandler() | |
| handler.setFormatter(formatter) | |
| handler.addFilter(CorrelationFilter()) | |
| root = logging.getLogger() | |
| # Replace any handlers (uvicorn / pytest installs its own). | |
| root.handlers = [handler] | |
| root.setLevel(level.upper()) | |
| # uvicorn's named loggers shouldn't double-emit through their own handlers. | |
| for name in ("uvicorn", "uvicorn.access", "uvicorn.error"): | |
| logger = logging.getLogger(name) | |
| logger.handlers = [] | |
| logger.propagate = True | |