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}

 

posted @ 2026-06-10 15:23  刘宏缔的架构森林  阅读(13)  评论(0)    收藏  举报