# -*- coding: utf-8 -*- """ 全局审计中间件 拦截所有入站 HTTP 请求,记录:方法、URL、客户端 IP、耗时、响应状态码。 日志输出到标准 logging,生产环境可对接 ELK / Loki 等日志收集系统。 """ import logging import time from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint from starlette.requests import Request from starlette.responses import Response # 配置审计专用 logger,与业务日志分离 audit_logger = logging.getLogger("audit") audit_logger.setLevel(logging.INFO) class AuditMiddleware(BaseHTTPMiddleware): """ 审计中间件 - 记录每个请求的关键信息。 日志格式: [AUDIT] <客户端IP> <方法> <状态码> <耗时ms> """ async def dispatch( self, request: Request, call_next: RequestResponseEndpoint ) -> Response: # 提取客户端真实 IP (优先取反向代理传递的 X-Forwarded-For) client_ip = request.headers.get( "X-Forwarded-For", request.client.host if request.client else "unknown" ) method = request.method url = str(request.url) start_time = time.perf_counter() try: response = await call_next(request) except Exception: # 未捕获异常也要记录审计日志 elapsed_ms = (time.perf_counter() - start_time) * 1000 audit_logger.error( "[AUDIT] %s %s %s 500 %.1fms (unhandled exception)", client_ip, method, url, elapsed_ms, ) raise elapsed_ms = (time.perf_counter() - start_time) * 1000 audit_logger.info( "[AUDIT] %s %s %s %d %.1fms", client_ip, method, url, response.status_code, elapsed_ms, ) # 将审计信息注入响应头 (方便调试,生产环境可移除) response.headers["X-Request-Duration-Ms"] = f"{elapsed_ms:.1f}" return response