深入解析 FastAPI 日志中间件设计与实现

引言

在项目中记录请求信息并计算请求处理时间,是实现请求日志追踪的关键。然而,在 FastAPI 中直接通过 Middleware 读取请求体(body)会导致请求体无法被多次读取,进而在路由函数中引发异常。为了解决这个问题,FastAPI 推荐使用 APIRoute 。通过 APIRoute,可以安全地读取请求体数据,并在读取后继续完成其他处理,而不会影响后续路由函数的正常执行。

基于 APIRoute 的特性,我们可以封装一个自定义的路由类,实现请求和响应的日志记录,并计算每个请求的处理时间。这种方式既能满足记录需求,又能确保应用的稳定性和性能。

Middleware 封装请求日志

首先想到的都是web中间件,每次请求都会先过中间件。封装如下

python 复制代码
from py_tools.logging import logger
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.responses import JSONResponse


class LoggingMiddleware(BaseHTTPMiddleware):
    """
    日志中间件
    记录请求参数信息、计算响应时间
    """

    async def dispatch(self, request: Request, call_next) -> Response:
        start_time = time.perf_counter()

        # 打印请求信息
        logger.info(f"--> {request.method} {request.url.path} {request.client.host}")
        if request.query_params:
            logger.info(f"--> Query Params: {request.query_params}")

        if "application/json" in request.headers.get("Content-Type", ""):
            try:
                # starlette 中间件中不能读取请求数据,否则会进入循环等待 需要特殊处理或者换APIRoute实现
                body = await request.json()
                logger.info(f"--> Body: {body}")
            except Exception as e:
                logger.warning(f"Failed to parse JSON body: {e}")

        # 执行请求获取响应
        response = await call_next(request)

        # 计算响应时间
        process_time = time.perf_counter() - start_time
        response.headers["X-Response-Time"] = f"{process_time:.2f}s"
        logger.info(f"<-- {response.status_code} {request.url.path} (took: {process_time:.2f}s)\n")

        return response

这中间主要就是在请求处理前日志记录下请求路由信息、请求参数等,请求处理后记录下请求处理耗时。日志打印效果如下:

在中间件直接读取body出现问题了,注册用户请求读取body一直在等待,刷新下界面就400响应了

网上有一种解决方案,包装 receive 函数

python 复制代码
from py_tools.logging import logger
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.responses import JSONResponse


class LoggingMiddleware(BaseHTTPMiddleware):
    """
    日志中间件
    记录请求参数信息、计算响应时间
    """

    async def set_body(self, request: Request):
        receive_ = await request._receive()

        async def receive():
            return receive_

        request._receive = receive

    async def dispatch(self, request: Request, call_next) -> Response:
        start_time = time.perf_counter()

        # 打印请求信息
        logger.info(f"--> {request.method} {request.url.path} {request.client.host}")
        if request.query_params:
            logger.info(f"--> Query Params: {request.query_params}")

        if "application/json" in request.headers.get("Content-Type", ""):
            await self.set_body(request)
            try:
                # starlette 中间件中不能读取请求数据,否则会进入循环等待 需要特殊处理或者换APIRoute实现
                body = await request.json()
                logger.info(f"--> Body: {body}")
            except Exception as e:
                logger.warning(f"Failed to parse JSON body: {e}")

        # 执行请求获取响应
        response = await call_next(request)

        # 计算响应时间
        process_time = time.perf_counter() - start_time
        response.headers["X-Response-Time"] = f"{process_time:.2f}s"
        logger.info(f"<-- {response.status_code} {request.url.path} (took: {process_time:.2f}s)\n")

        return response

直接看看效果

ok,这样就解决,但这样不太好,官方也不推荐。

APIRoute 封装请求日志

官方也说明了不能在中间件直接读取 body 数据,可以使用 APIRoute 处理。在 APIRouter 中指定 route_class 参数来设置 APIRoute,然后重写 APIRouteget_route_handler 方法即可。

官方文档:fastapi.tiangolo.com/zh/how-to/c...

代码如下

python 复制代码
class LoggingAPIRoute(APIRoute):
    def get_route_handler(self) -> Callable:
        original_route_handler = super().get_route_handler()

        async def log_route_handler(request: Request) -> Response:
            """日志记录请求信息与处理耗时"""
            req_log_info = f"--> {request.method} {request.url.path} {request.client.host}:{request.client.port}"
            if request.query_params:
                req_log_info += f"\n--> Query Params: {request.query_params}"

            if "application/json" in request.headers.get("Content-Type", ""):
                try:
                    json_body = await request.json()
                    req_log_info += f"\n--> json_body: {json_body}"
                except Exception:
                    logger.exception("Failed to parse JSON body")

            logger.info(req_log_info)
            start_time = time.perf_counter()
            response: Response = await original_route_handler(request)
            process_time = time.perf_counter() - start_time
            response.headers["X-Response-Time"] = str(process_time)
            resp_log_info = f"<-- {response.status_code} {request.url.path} (took: {process_time:.5f}s)"
            logger.info(resp_log_info)  # 处理大量并发请求时,记录请求日志信息会影响服务性能,可以用nginx代替
            return response

        return log_route_handler

体验效果

APIRouter 需要指定下 route_class ,不然没有效果

改进代码,采用 BaseAPIRouter 继承 APIRouter 重写初始化方法,参数控制设置默认的 APIRoute,构建路由信息时改用 BaseAPIRouter 就不用为每一个 router 单独设置 APIRoute

python 复制代码
class BaseAPIRouter(fastapi.APIRouter):
    def __init__(self, *args, api_log=settings.server_access_log, **kwargs):
        super().__init__(*args, **kwargs)
        if api_log:
            # 开启api请求日志信息
            self.route_class = LoggingAPIRoute

这里多了一个 api_log 参数来控制是否开启api的请求日志信息。

直接使用 BaseAPIRouter 管理路由即可为每个 Router 添加日志处理类

再次体验效果如下

大功告成。

注意:最好在开发调试时开启,生产时请求访问、响应日志的信息最好不要用业务服务器记录,在高并发的情况下这会影响服务器的性能,可以借助 Nginx 来处理 access_log 这样性能会比较好,业务服务器一般记录业务的日志。

Github 源代码

TaskFlow 项目集成 py-tools 来进行敏捷开发,快速搭建项目。

具体的封装设计代码请看:github.com/HuiDBK/Task...

相关推荐
极智-9968 分钟前
GitHub 热榜项目 · 日榜精选(2026-01-06)
github·开源项目·技术趋势·开发者工具
吃茄子的猫16 分钟前
quecpython中&的具体含义和使用场景
开发语言·python
じ☆冷颜〃27 分钟前
黎曼几何驱动的算法与系统设计:理论、实践与跨领域应用
笔记·python·深度学习·网络协议·算法·机器学习
数据大魔方40 分钟前
【期货量化实战】日内动量策略:顺势而为的短线交易法(Python源码)
开发语言·数据库·python·mysql·算法·github·程序员创富
APIshop1 小时前
Python 爬虫获取 item_get_web —— 淘宝商品 SKU、详情图、券后价全流程解析
前端·爬虫·python
风送雨1 小时前
FastMCP 2.0 服务端开发教学文档(下)
服务器·前端·网络·人工智能·python·ai
效率客栈老秦1 小时前
Python Trae提示词开发实战(8):数据采集与清洗一体化方案让效率提升10倍
人工智能·python·ai·提示词·trae
哈里谢顿1 小时前
一条 Python 语句在 C 扩展里到底怎么跑
python
znhy_231 小时前
day46打卡
python
Edward.W2 小时前
Python uv:新一代Python包管理工具,彻底改变开发体验
开发语言·python·uv