Spaces:
Running
Running
| """Structured JSON logging with request context.""" | |
| 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): | |
| """Format log records as single-line JSON objects.""" | |
| 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) | |
| # Quiet noisy third-party loggers | |
| 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: | |
| """Log every request with a correlation id and duration.""" | |
| 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 | |