- 作者:老汪软件技巧
- 发表时间:2024-11-24 04:03
- 浏览量:
引言
在项目中记录请求信息并计算请求处理时间,是实现请求日志追踪的关键。然而,在 FastAPI 中直接通过 Middleware 读取请求体(body)会导致请求体无法被多次读取,进而在路由函数中引发异常。为了解决这个问题,FastAPI 推荐使用 APIRoute。通过 APIRoute,可以安全地读取请求体数据,并在读取后继续完成其他处理,而不会影响后续路由函数的正常执行。
基于 APIRoute 的特性,我们可以封装一个自定义的路由类,实现请求和响应的日志记录,并计算每个请求的处理时间。这种方式既能满足记录需求,又能确保应用的稳定性和性能。
Middleware 封装请求日志
首先想到的都是web中间件,每次请求都会先过中间件。封装如下
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 函数
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,然后重写 APIRoute 的 get_route_handler 方法即可。
官方文档:/zh/how-to/c…
代码如下
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
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 来进行敏捷开发,快速搭建项目。
具体的封装设计代码请看:/HuiDBK/Task…