"""请求上下文中间件与结构化日志。 - `Hunjing-Request-ID`:合法则回显,缺失由服务端生成(UUIDv4,request_ 前缀)。 - `traceparent`:透传 W3C Trace Context;缺失生成新 trace-id(不与业务请求 ID 混用)。 - JSON 结构化日志:Authorization/Cookie/Set-Cookie 等敏感头脱敏。 """ import json import logging import re import uuid from typing import Any from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from app.core.ids import new_id REDACTED_HEADERS = {"authorization", "cookie", "set-cookie", "hunjing-csrf-token", "x-api-key"} _TRACEPARENT_RE = re.compile(r"^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$") def _sanitize_headers(headers: Any) -> dict[str, str]: return { k: ("[REDACTED]" if k.lower() in REDACTED_HEADERS else v) for k, v in headers.items() } class JsonFormatter(logging.Formatter): def format(self, record: logging.LogRecord) -> str: payload: dict[str, Any] = { "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", "trace_id", "method", "path", "status"): 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(level: str = "info") -> None: handler = logging.StreamHandler() handler.setFormatter(JsonFormatter()) root = logging.getLogger() root.handlers = [handler] root.setLevel(level.upper()) class RequestContextMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next: Any) -> Any: request_id = request.headers.get("Hunjing-Request-ID") or new_id("request_", version=4) traceparent = request.headers.get("traceparent") if traceparent is None or not _TRACEPARENT_RE.match(traceparent): traceparent = f"00-{uuid.uuid4().hex}-{uuid.uuid4().hex[:16]}-01" trace_id = traceparent.split("-")[1] request.state.request_id = request_id request.state.trace_id = trace_id response = await call_next(request) response.headers["Hunjing-Request-ID"] = request_id response.headers["traceparent"] = traceparent logging.getLogger("hunjing.access").info( "request", extra={ "request_id": request_id, "trace_id": trace_id, "method": request.method, "path": request.url.path, "status": response.status_code, }, ) return response __all__ = ["RequestContextMiddleware", "configure_logging", "_sanitize_headers"]