import json import logging import sys import time import uuid from collections.abc import Awaitable, Callable from fastapi import Request, Response class JsonFormatter(logging.Formatter): def format(self, record: logging.LogRecord) -> str: payload = { "ts": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"), "level": record.levelname, "logger": record.name, "message": record.getMessage(), } for key in ("request_id", "method", "path", "status", "duration_ms", "user_id"): value = getattr(record, key, None) if value is not None: payload[key] = value if record.exc_info: payload["exception"] = self.formatException(record.exc_info) return json.dumps(payload, ensure_ascii=False) def configure_logging(debug: bool = False) -> None: handler = logging.StreamHandler(sys.stdout) handler.setFormatter(JsonFormatter()) root = logging.getLogger() root.handlers = [handler] root.setLevel(logging.DEBUG if debug else logging.INFO) for name in ("httpx", "httpcore", "chromadb", "uvicorn.access"): logging.getLogger(name).setLevel(logging.WARNING) logger = logging.getLogger("synapse") async def request_logging_middleware( request: Request, call_next: Callable[[Request], Awaitable[Response]] ) -> Response: request_id = uuid.uuid4().hex[:12] request.state.request_id = request_id start = time.perf_counter() try: response = await call_next(request) except Exception: duration_ms = round((time.perf_counter() - start) * 1000) logger.exception( "request failed", extra={ "request_id": request_id, "method": request.method, "path": request.url.path, "duration_ms": duration_ms, }, ) raise duration_ms = round((time.perf_counter() - start) * 1000) logger.info( "request", extra={ "request_id": request_id, "method": request.method, "path": request.url.path, "status": response.status_code, "duration_ms": duration_ms, }, ) response.headers["X-Request-ID"] = request_id return response