fastapi: 第十七章:fastapi使用路由组中间件:记录请求和响应数据的日志

一,如何把中间件指定到路由组?

FastAPI 原生的 BaseHTTPMiddleware@app.middleware("http")无法直接绑定到特定路由或路由组的,
它们必然会拦截全站所有请求。
为了实现“只针对某个路由组(APIRouter)”记录参数、IP 和响应内容,最优雅、也是 FastAPI 官方推荐的方案:

使用自定义的路由类(Custom APIRoute) —— 适合路由组(APIRouter)
它拥有和中间件完全一样的“包裹”能力,能完美捕获响应体,且只对绑定了该类的路由组生效,不会污染全局。

二,代码

中间件代码:
# app/middleware/log_query_response.py
import time
import logging
from typing import Callable
from fastapi import FastAPI, APIRouter, Request, Response
from fastapi.routing import APIRoute
from app.core.logger import logger

# 2. 自定义路由类(局部中间件的核心)
class LogGroupRoute(APIRoute):
    def get_route_handler(self) -> Callable:
        original_route_handler = super().get_route_handler()

        async def custom_route_handler(request: Request) -> Response:
            start_time = time.time()
            client_ip = request.client.host if request.client else "Unknown"
            url = str(request.url)
            method = request.method

            # 获取 GET 参数
            query_params = dict(request.query_params)

            # 获取 POST/PUT 参数
            body_params = None
            if method in ["POST", "PUT", "PATCH"]:
                try:
                    # 自定义路由类中可以直接读取 body,不会像全局中间件那样导致后续读取阻塞
                    body_bytes = await request.body()
                    if body_bytes:
                        try:
                            body_params = await request.json()
                        except ValueError:
                            body_params = body_bytes.decode("utf-8")
                except Exception as e:
                    body_params = f"Read body error: {str(e)}"

            # 执行实际的业务路由逻辑,并获取响应对象
            response: Response = await original_route_handler(request)

            # 获取响应内容
            # 在 APIRoute 中,可以直接通过 response.body 获取内容,比全局中间件更安全
            response_content = "[Binary/Non-UTF8]"
            if hasattr(response, "body"):
                try:
                    response_content = response.body.decode("utf-8")
                except Exception:
                    pass

            # 计算耗时并记录日志
            process_time = round((time.time() - start_time) * 1000, 2)
            logger.bind(log_type="alipay").info(
                f"【路由组日志】IP: {client_ip} | Method: {method} | URL: {url} | "
                f"Query: {query_params} | Body: {body_params} | "
                f"Status: {response.status_code} | Duration: {process_time}ms | "
                f"Response: {response_content}"
            )
            return response

        return custom_route_handler
使用路由组的中间件:
 
# app/api/users.py
from fastapi import APIRouter, Depends

from app.core.Settings import get_settings
from app.core.logger import logger
from app.middleware.log_query_response import LogGroupRoute
from app.middleware.log_time import log_time_middleware

# 创建路由器实例
# prefix:所有路由的统一前缀,避免重复写"/users"
# tags:在Swagger文档中分组显示
router = APIRouter(prefix="/users", tags=["用户管理"], route_class=LogGroupRoute)

@router.get("/all")
def get_all_users():
    # 💡 直接在函数体内调用,不再作为参数传入
    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 20:18:10 | 【路由组日志】IP: 127.0.0.1 | Method: GET | URL: http://127.0.0.1:8000/users/all | Query: {} | Body: None | Status: 200 | Duration: 16.89ms | Response: {"app_name":"My FastAPI App","logs_dir":"/data/fastapi/test123/logs","items_per_page":20}
2026-05-28 20:18:45 | 【路由组日志】IP: 127.0.0.1 | Method: GET | URL: http://127.0.0.1:8000/users/all?category=admin | Query: {'category': 'admin'} | Body: None | Status: 200 | Duration: 0.61ms | Response: {"app_name":"My FastAPI App","logs_dir":"/data/fastapi/test123/logs","items_per_page":20}
2026-05-28 20:27:39 | 【路由组日志】IP: 127.0.0.1 | Method: GET | URL: http://127.0.0.1:8000/users/one?user_id=456&name=%E8%B0%A2%E5%B9%BF%E5%9D%A4 | Query: {'user_id': '456', 'name': '谢广坤'} | Body: None | Status: 200 | Duration: 3.59ms | Response: {"id":456,"name":"用户谢广坤"}
 
posted @ 2026-06-10 15:23  刘宏缔的架构森林  阅读(10)  评论(0)    收藏  举报