在您的 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
将我们的 response
的 background
属性设置为 BackgroundTask
对象可以将任务排队在后台运行。
就这样好了!进出你应用的每个请求和响应都将被记录。
我做自由职业编程和网络抓取的工作!如果你想联系我或者只是需要更多资源,下面这些都是可能对你有用的:
- 通过我的 LinkedIn 联系我,我们可以聊聊 FastAPI、AI、网页抓取或者其他任何你感兴趣的议题 :).
- 也可以在我的 网站 上看看我的其他项目。
- 如果你对网页抓取感兴趣,我写了一篇关于大规模抓取困难网站的 详细指南。
- 如果你需要用于抓取困难网站的高级代理,我推荐 Browserless.io 或者 Oxylabs。
共同学习,写下你的评论
评论加载中...
作者其他优质文章