fastapi:第十八章:为单路由记录请求和响应数据
一,单个路由如何捕捉到响应体?
针对单个指定路由,要百分之百完美捕获到响应体,答案就是使用自定义路由装饰器
我们不使用中间件或依赖项,而是自己写一个装饰器。
它能像中间件一样实现前置和后置拦截,且能百分之百拿到最终的响应体
它能像中间件一样实现前置和后置拦截,且能百分之百拿到最终的响应体
一,代码:
装饰器
import time
import logging
from functools import wraps
from fastapi import FastAPI, Request, Response
from app.core.logger import logger
# 2. 编写专用的路由日志装饰器
def log_this_route():
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
# 关键修复点:从 kwargs 中安全获取 request,绝对不要动 *args 的结构
request: Request = kwargs.get("request")
if not request:
raise RuntimeError(
f"路由函数 '{func.__name__}' 必须显式声明 'request: Request' 参数,"
f"且客户端请求时不能缺失。"
)
# 【1. 请求进入前:记录请求信息】
start_time = time.time()
client_ip = request.client.host if request.client else "Unknown"
url = str(request.url)
method = request.method
query_params = dict(request.query_params)
# 安全读取 POST/PUT Body
body_params = None
if method in ["POST", "PUT", "PATCH"]:
try:
body_bytes = await request.body()
if body_bytes:
try:
body_params = await request.json()
except ValueError:
body_params = body_bytes.decode("utf-8")
# 关键:重新包装 receive 流,确保 FastAPI 后续能正常解析 body 传给业务参数
async def receive():
return {"type": "http.request", "body": body_bytes, "more_body": False}
request._receive = receive
except Exception as e:
body_params = f"Read body error: {str(e)}"
# 【2. 执行真正的业务路由函数】
# 原封不动透传 args 和 kwargs,FastAPI 的参数解析器就能完美工作了
response_data = await func(*args, **kwargs)
# 【3. 响应返回前:记录耗时和返回值】
process_time = round((time.time() - start_time) * 1000, 2)
response_content = str(response_data)
logger.bind(log_type="alipay").info(
f"【单路由日志】IP: {client_ip} | Method: {method} | URL: {url} | "
f"Query: {query_params} | Body: {body_params} | "
f"Duration: {process_time}ms | "
f"Response: {response_content}"
)
return response_data
return wrapper
return decorator
使用:
要注意的两点:
1. @log_this_route() 必须写在 @router.get() 的【下面】(离函数最近)
2. 路由函数的参数里【必须】加上 request: Request
# app/api/users.py
from fastapi import APIRouter, Depends
from starlette.requests import Request
from app.core.Settings import get_settings
from app.middleware.log_single_route import log_this_route
# 创建路由器实例
# prefix:所有路由的统一前缀,避免重复写"/users"
# tags:在Swagger文档中分组显示
router = APIRouter(prefix="/users", tags=["用户管理"])
@router.get("/all")
@log_this_route()
async def get_all_users(request: Request,name: str = None):
# 💡 直接在函数体内调用,不再作为参数传入
settings = get_settings()
return {
"app_name": settings.app_name,
"logs_dir": settings.logs_dir,
"items_per_page": settings.items_per_page
}
@router.get("/one")
def get_user(user_id: int = 1,name: str = None):
"""根据ID获取单个用户"""
return {"id": user_id, "name": f"用户{name}"}
二,测试 效果 :
查看日志:
2026-05-28 21:02:33 | 【单路由日志】IP: 127.0.0.1 | Method: GET | URL: http://127.0.0.1:8000/users/all?category=admin | Query: {'category': 'admin'} | Body: None | Duration: 0.02ms | Response: {'app_name': 'My FastAPI App', 'logs_dir': '/data/fastapi/test123/logs', 'items_per_page': 20}
2026-05-28 21:02:51 | 【单路由日志】IP: 127.0.0.1 | Method: GET | URL: http://127.0.0.1:8000/users/all?category=admin&name=liu | Query: {'category': 'admin', 'name': 'liu'} | Body: None | Duration: 0.02ms | Response: {'app_name': 'My FastAPI App', 'logs_dir': '/data/fastapi/test123/logs', 'items_per_page': 20}
浙公网安备 33010602011771号