深入解析 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...

相关推荐
久绊A17 分钟前
Python 基本语法的详细解释
开发语言·windows·python
Hylan_J4 小时前
【VSCode】MicroPython环境配置
ide·vscode·python·编辑器
莫忘初心丶4 小时前
在 Ubuntu 22 上使用 Gunicorn 启动 Flask 应用程序
python·ubuntu·flask·gunicorn
闲猫4 小时前
go orm GORM
开发语言·后端·golang
丁卯4044 小时前
Go语言中使用viper绑定结构体和yaml文件信息时,标签的使用
服务器·后端·golang
失败尽常态5237 小时前
用Python实现Excel数据同步到飞书文档
python·excel·飞书
2501_904447747 小时前
OPPO发布新型折叠屏手机 起售价8999
python·智能手机·django·virtualenv·pygame
青龙小码农7 小时前
yum报错:bash: /usr/bin/yum: /usr/bin/python: 坏的解释器:没有那个文件或目录
开发语言·python·bash·liunx
大数据追光猿7 小时前
Python应用算法之贪心算法理解和实践
大数据·开发语言·人工智能·python·深度学习·算法·贪心算法
Leuanghing7 小时前
【Leetcode】11. 盛最多水的容器
python·算法·leetcode