FastAPI日志管理:优雅记录每一行代码
在FastAPI应用开发中,良好的日志管理是保证系统可观测性和可维护性的关键。本文将介绍如何在FastAPI中实现优雅的日志记录方案,包括中间件日志、装饰器日志以及异常捕获日志,让你的应用能够"呼吸"自如,记录每一个重要时刻。
正文
一、为什么需要专业的日志管理
日志是应用程序的"黑匣子",它能帮助我们:
- 追踪用户请求流程
- 定位和排查问题
- 分析系统性能瓶颈
- 满足审计需求
在FastAPI中,一个完整的日志系统应该包含:
- 请求/响应日志
- 业务操作日志
- 异常错误日志
- 系统运行日志
二、FastAPI日志系统架构解析
让我们先看一个完整的FastAPI日志管理实现方案:
import uvicorn
import traceback
from typing import Optional, TypeVar
from fastapi import FastAPI
from log_utils import record_log, log_decorator
from log_middleware import LoggerRecord, log_record
from pydantic import BaseModel, Field
app = FastAPI()
# 启动时初始化日志记录器
@app.on_event("startup")
async def startup():
LoggerRecord().start()
# 关闭时清理日志资源
@app.on_event("shutdown")
async def shutdown():
LoggerRecord.stop()
# 添加日志中间件
app.middleware("http")(log_record)
T = TypeVar("T")
class Result(BaseModel):
code: int = 200
msg: str = "success"
data: Optional[T] = None
class Params(BaseModel):
name: str = Field(default="xxxx")
age: int = Field(default=12)
# 使用日志装饰器的路由示例
@app.get("/demo1", response_model=Result)
@log_decorator(save_response=True)
def demo1():
return Result(data={"xxx": 11})
# 带异常处理的日志记录示例
@app.post("/demo2", response_model=Result)
@log_decorator()
def demo2(params: Params):
try:
1 / 0
except Exception as e:
record_log(params, msg=traceback.format_exc())
return Result(data="zxxxx")
if __name__ == '__main__':
uvicorn.run("app:app")
# 中间件
import json
import time
from enum import Enum
from threading import Thread
from queue import Queue, Empty
from typing import Union
from datetime import datetime
from contextvars import ContextVar
from starlette.types import Message
from fastapi import Request, Response
import pymysql
import constant
WHITE_MAP = {}
class LogType(Enum):
INTERFACE = "I"
FUNC = "F"
class LogRecordPo:
def __init__(self):
self.http_code: Union[int, None] = None
self.create_time: datetime = datetime.now()
self.module: str = ""
self.code: Union[int, None] = None
self.method: Union[str, None] = None
self.request: Union[str, None] = None
self.response: Union[str, None] = None
self.type: LogType = LogType.INTERFACE
self.cost_time: Union[int, None] = None
def to_dict(self):
return {
"http_code": self.http_code,
"create_time": self.create_time.strftime("%Y%m%d%H%M%S"),
"module": self.module,
"code": self.code,
"method": self.method,
"request": self.request,
"response": self.response,
"type": self.type,
"cost_time": self.cost_time,
}
def to_str(self):
return str(self.to_dict())
def get_record(log_config: dict, request: Request, response: Response, cost_time: int, req_body: bytes,
res_body: bytes) -> LogRecordPo:
"""
获取LogRecordPo对象
params:
log_config: 日志装饰器的配置字典
request: 请求对象
response:相应对象
cost_time:接口执行时间
req_body:请求体
res_body: 响应体
"""
record = LogRecordPo()
record.method = request.method
record.type = LogType.INTERFACE
if log_config.get(constant.SAVE_REQUEST):
record.request = req_body.decode()
if log_config.get(constant.SAVE_RESPONSE):
record.response = res_body.decode()
record.module = log_config.get(constant.MODULE)
record.url = request.url.path
record.http_code = response.status_code
record.cost_time = cost_time
if response.status_code != 200:
record.response = res_body.decode()
else:
try:
json_response = json.loads(res_body.decode())
record.code = json_response.get(constant.CODE)
if json_response.get(constant.CODE) != 200:
# 自定义code中规定200为正常
record.response = res_body.decode()
except:
pass
return record
class LoggerRecord(Thread):
"""线程异步写入日志到数据库"""
running = True
log_cache = Queue()
request_var: ContextVar[Request] = ContextVar("request")
def __init__(self):
super().__init__()
self.daemon = True
self.conn = pymysql.connect(host="192.168.3.201", user="de_Tx7S65", password="de_GAiE8D", db="de_85jaa2")
@classmethod
def record(cls, record: LogRecordPo):
cls.log_cache.put(record)
def insert_log_to_db(self, record: LogRecordPo):
sql = """
insert into log_record(http_code, create_time, module, code, method, request, response, type, cost_time)
values (%s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
params = (record.http_code,
record.create_time.strftime("%Y%m%d%H%M%S"),
record.module,
record.code,
record.method,
record.request,
record.response,
record.type.value,
record.cost_time)
cursor = self.conn.cursor()
ret = cursor.execute(sql, params)
self.conn.commit()
print(f"插入数据到数据库ret:{ret}")
def run(self):
print('异步线程启动')
while LoggerRecord.running:
try:
record: LogRecordPo = LoggerRecord.log_cache.get(timeout=1)
self.insert_log_to_db(record)
except Empty:
pass
except Exception as e:
print(f"异步日志线程异常:{e}")
print("异步线程执行完成")
@classmethod
def stop(cls):
cls.running = False
async def set_reqeust_body(request: Request, body: bytes):
async def receive() -> Message:
return {"type": "http.request", "body": body}
request._receive = receive
async def get_request_body(request: Request) -> bytes:
body = await request.body()
await set_reqeust_body(request, body)
return body
async def get_response_body(response: Response) -> bytes:
"""从响应中获取响应体"""
res_body = b""
async for chunk in response.body_iterator:
res_body += chunk
return res_body
async def log_record(request: Request, call_next):
"""
日志middleware
"""
LoggerRecord.request_var.set(request)
start_time = time.time()
req_body = await get_request_body(request)
response: Response = await call_next(request)
end_time = time.time()
cost_time = int((end_time - start_time) * 1000)
handler = request.scope.get("endpoint")
handler_name = getattr(handler, "__name__", None)
handler_module = getattr(handler, "__module__", None)
func_path = f"{handler_module}.{handler_name}"
# 检测是否在检测的名单中
if func_path in WHITE_MAP:
log_config = WHITE_MAP[func_path]
res_body = await get_response_body(response)
record = get_record(log_config=log_config, request=request, response=response, cost_time=cost_time,
req_body=req_body, res_body=res_body)
LoggerRecord.record(record)
# 从response中取出响应体之后需要重新写回去
return Response(
content=res_body,
status_code=response.status_code,
headers=dict(response.headers),
media_type=response.media_type
)
return response
import inspect
from functools import wraps
from pydantic import BaseModel
from fastapi import Request
from log_middleware import WHITE_MAP, LoggerRecord, LogRecordPo, LogType
import constant
def log_decorator(save_reqeust=True, save_response=False, module=None):
"""
日志记录装饰器
params:
save_reqeust: 是否保存请求
save_response: 是否保存响应
module:所属模块
"""
def inner(func):
# 试图层没有定义request时处理
# sig = inspect.signature(func)
# parameters = list(sig.parameters.values())
# has_request = any(param.annotation is Request for param in parameters)
# if not has_request:
# new_params = [inspect.Parameter("reqeust", inspect.Parameter.POSITIONAL_OR_KEYWORD,
# annotation=Request)] + parameters
# new_sig = sig.replace(parameters=new_params)
# func.__signature__ = new_sig
@wraps(func)
def wrapper(*args, **kwargs):
request = LoggerRecord.request_var.get()
handler = request.get("endpoint")
handler_name = getattr(handler, "__name__", None)
handler_module = getattr(handler, "__module__", None)
func_path = f"{handler_module}.{handler_name}"
if func_path not in WHITE_MAP:
WHITE_MAP[func_path] = {
constant.SAVE_REQUEST: save_reqeust,
constant.SAVE_RESPONSE: save_response,
constant.MODULE: module if module else func_path
}
return func(*args, **kwargs)
return wrapper
return inner
def record_log(params: BaseModel, msg, code=500):
"""
函数中的日志记录器
"""
try:
request: Request = LoggerRecord.request_var.get()
handler = request.get("endpoint")
handler_name = getattr(handler, "__name__", None)
handler_module = getattr(handler, "__module__", None)
func_path = f"{handler_module}.{handler_name}"
record = LogRecordPo()
log_config = WHITE_MAP.get(func_path)
if log_config:
record.module = log_config.get(constant.MODULE)
record.type = LogType.FUNC
record.url = request.url.path
record.method = request.method
record.response = msg
record.request = str(params.dict())
record.code = code
LoggerRecord.log_cache.put(record)
except Exception as e:
print(f"函数中记录日志异常:{e}")