"""请求日志中间件 — 记录最近 MAX_ENTRIES 条请求到内存""" import time import asyncio from collections import deque from typing import Optional from fastapi import Request, Response import logging logger = logging.getLogger(__name__) MAX_ENTRIES = 500 _request_log: deque = deque(maxlen=MAX_ENTRIES) def get_request_log() -> list: """返回请求日志副本(最近在前)""" return list(_request_log)[::-1] def clear_request_log() -> int: """清空日志,返回清空条数""" n = len(_request_log) _request_log.clear() return n async def request_log_middleware(request: Request, call_next) -> Response: start_time = time.perf_counter() client_ip = request.client.host if request.client else "-" # 读取请求体(不重复消费) body = None if request.method in ("POST", "PUT", "PATCH"): try: raw = await request.body() if raw: try: body = raw.decode("utf-8") if len(body) > 2000: body = body[:2000] + "...(truncated)" except Exception: body = f"" except Exception: pass try: response = await call_next(request) except Exception as exc: elapsed = time.perf_counter() - start_time status_code = 500 response_body = "" logger.error("REQUEST EXCEPTION: %s %s %.2fs error=%r", request.method, request.url.path, elapsed, exc) _request_log.appendleft({ "ts": time.strftime("%H:%M:%S"), "method": request.method, "path": request.url.path, "status": 500, "duration_ms": round(elapsed * 1000), "client_ip": client_ip, "body": body, "error": str(exc)[:500], }) # 构造错误响应 from fastapi.responses import JSONResponse return JSONResponse(status_code=500, content={"detail": str(exc)}) elapsed = time.perf_counter() - start_time status_code = response.status_code # 读取响应体(不可逆,只取内容长度) response_body = "" if hasattr(response, "body") and response.body: try: response_body = response.body.decode("utf-8")[:1000] except Exception: response_body = f"" # 写入日志 entry = { "ts": time.strftime("%H:%M:%S"), "method": request.method, "path": request.url.path, "status": status_code, "duration_ms": round(elapsed * 1000), "client_ip": client_ip, "body": body, "response": response_body, } _request_log.appendleft(entry) # 超5秒标记 warning if elapsed > 5: logger.warning("SLOW_REQUEST: %s %s %.2fs", request.method, request.url.path, elapsed) else: logger.debug("REQUEST: %s %s %.2fs", request.method, request.url.path, elapsed) response.headers["X-Response-Time"] = f"{elapsed:.3f}s" return response