为了账号安全,请及时绑定邮箱和手机立即绑定

如何在FastAPI中记录每个请求和响应的日志?

标签:
Python API

在您的 FastAPI 应用中高效记录每个请求和响应日志的指南

我最近一直在用FastAPI,感觉还不错。我目前正在构建Home Slice,这是最准确的抵押贷款计算引擎。我最近发布了Home Slice API的测试版(Beta版),它提供了一个简单的/calculate端点,让开发商可以使用我的算法来运行抵押贷款计算。我就是用FastAPI构建的。

我想更密切地跟踪用户是如何使用API的,比如他们使用API的频率有多高,哪些API端点被他们使用,哪些参数是最常用的等。

存储整齐的API日志,:)

为了完成这个任务,我需要一个这样的日志系统,它需要符合以下条件。

  • 读取每个传入的请求和每个传出的响应
  • 记录所有请求正文、响应正文、查询参数和路径参数
  • 记录元数据,如源 IP 地址、使用的 API 密钥、执行时长和请求时间
  • 将日志存储在 PostgreSQL 数据库中,以便后续分析
  • 异步写入数据库,以避免影响用户的响应延迟

为了达到这些要求,我们将使用FastAPI的中间件(middleware)后台任务(background tasks),和SQLModel

中间件(就是那种夹在应用软件和系统软件之间的软件)

“‘中间件’是一种函数,它会在任何特定的路径操作函数处理每个请求之前处理。也会在每个响应返回之前处理。” — FastAPI 文档——

这里是一个最简单的中间件函数例子。

    # main.py

    @app.middleware("http")
    # 中间件:处理请求和响应
    async def middleware(request: Request, call_next):
        # 调用下一个中间件处理请求
        response = await call_next(request)
        # 返回响应
        return response

在我们中间件函数里,我们可以访问每个通过我们应用程序的 API 调用的请求和响应对象。我们可以从这些对象中提取我们想要存储的数据。可以通过 request.json() 轻松访问请求体。然而,response 实际上是一个 Starlette StreamingResponse 对象,这意味着我们不能直接访问响应体(如果有的话)。为了获取响应体,我们可以使用响应对象的 body_iterator

    # main.py  

    from starlette.concurrency import iterate_in_threadpool  

    @app.middleware("http")  
    async def middleware(request: Request, call_next):  
        try:  
            req_body = await request.json()  
        except Exception:  
            req_body = None  

        start_time = time.perf_counter()  
        response = await call_next(request)  
        process_time = time.perf_counter() - start_time # 处理时间

        res_body = [section async for section in response.body_iterator]  
        response.body_iterator = iterate_in_threadpool(iter(res_body)) # 将响应体的迭代器转换为线程池中的迭代

        # 响应体的字符串形式  
        res_body = res_body[0].decode()  

        return response

现在我们已经有了记录大多数所需数据所需的条件。让我们定义模式结构,并用SQLModel来创建一个表。

SQLModel

这是我的PostgreSQL数据库的结构,如果需要改动,应该会非常简单。

    # models.py

    from sqlmodel import SQLModel, Field, JSON, Column
    from datetime import datetime
    import uuid

    class ApiLog(SQLModel, table=True):

        __tablename__ = 'api_logs'

        id: int | None = Field(default=None, primary_key=True)
        api_key: uuid.UUID | None
        ip_address: str
        path: str
        method: str
        status_code: int
        request_body: dict | list | None = Field(default=None, sa_column=Column(JSON))
        response_body: dict | list | None = Field(default=None, sa_column=Column(JSON))
        query_params: dict | None = Field(default=None, sa_column=Column(JSON))
        path_params: dict | None = Field(default=None, sa_column=Column(JSON))
        process_time: float
        created_at: datetime

        class 配置:
            allow_arbitrary_types = True

在这里遇到的主要问题是如何在SQLModel模式中使用JSON类型。我用的方法是,在Field中使用sa_column=Column(JSON)这一方式,并将allow_arbitrary_types设为true

记得,你需要先在你的db.py文件中配置好数据库连接,比如在db.py文件中添加连接信息。

后台任务

现在我们将使用FastAPI(实际上由Starlette提供)的后台任务功能来连接所有部分。这些后台任务很关键,因为它们允许我们队列一个在返回给用户的响应后后台执行的任务。我们不能因为数据库写入而延迟用户响应,这是很重要的。

首先,让我们编写一个将日志写入Postgres的函数:

    # 背景.py

    import json
    import uuid
    from datetime import datetime, UTC

    from fastapi import Request
    from starlette.responses import StreamingResponse

    from db import get_session
    from models.api_log import ApiLog

    def 写日志(req: Request, res: StreamingResponse, req_body: dict, res_body: str, process_time: float):

        db = next(get_session())

        try:
            # 尝试将响应体从字符串转换为字典
            res_body = json.loads(res_body)
        except Exception:
            # 如果转换失败,则将响应体设为 None
            res_body = None

        # 创建一个新的 ApiLog 记录
        log = ApiLog(
            api_key=uuid.UUID(req.headers.get("x-api-key")) if req.headers.get("x-api-key") else None,
            ip_address=req.client.host,
            path=req.url.path,
            method=req.method,
            status_code=res.status_code,
            request_body=req_body,
            response_body=res_body,
            query_params=dict(req.query_params),
            path_params=req.path_params,
            process_time=process_time,
            created_at=datetime.now(UTC)  # 记录当前时间
        )
        db.add(log)
        db.commit()

        db.close()

现在我们已经设置好了函数,我们将回到中间件部分,添加最后步骤,排队一个后台任务来处理日志。

这里是我们完成的中间件函数,在返回前刚刚添加的最后一行:

    # main.py  

    from fastapi import Request  
    from starlette.background import BackgroundTask  
    from starlette.concurrency import iterate_in_threadpool  
    from background import write_log  

    @app.middleware("http")  
    async def middleware(request: Request, call_next):  
        try:  
            req_body = await request.json()  
        except Exception:  
            req_body = None  

        start_time = time.perf_counter()  
        response = await call_next(request)  
        process_time = time.perf_counter() - start_time  

        res_body = [section async for section in response.body_iterator]  
        response.body_iterator = iterate_in_threadpool(iter(res_body))  
        res_body = res_body[0].decode()  

        # 将后台任务添加到响应对象中,以便排队处理  
        设置 response.background = BackgroundTask(write_log, request, response, req_body, res_body, process_time)  
        return response

将我们的 responsebackground 属性设置为 BackgroundTask 对象可以将任务排队在后台运行。

就这样好了!进出你应用的每个请求和响应都将被记录。

我做自由职业编程和网络抓取的工作!如果你想联系我或者只是需要更多资源,下面这些都是可能对你有用的:

  • 通过我的 LinkedIn 联系我,我们可以聊聊 FastAPI、AI、网页抓取或者其他任何你感兴趣的议题 :).
  • 也可以在我的 网站 上看看我的其他项目。
  • 如果你对网页抓取感兴趣,我写了一篇关于大规模抓取困难网站的 详细指南
  • 如果你需要用于抓取困难网站的高级代理,我推荐 Browserless.io 或者 Oxylabs
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消