FastAPI日志管理:优雅记录每一行代码

FastAPI日志管理:优雅记录每一行代码

de8e1eb0-0a66-4f7f-a453-c25a677fc362在FastAPI应用开发中,良好的日志管理是保证系统可观测性和可维护性的关键。本文将介绍如何在FastAPI中实现优雅的日志记录方案,包括中间件日志、装饰器日志以及异常捕获日志,让你的应用能够"呼吸"自如,记录每一个重要时刻。

正文

一、为什么需要专业的日志管理

日志是应用程序的"黑匣子",它能帮助我们:

  • 追踪用户请求流程
  • 定位和排查问题
  • 分析系统性能瓶颈
  • 满足审计需求

在FastAPI中,一个完整的日志系统应该包含:

  1. 请求/响应日志
  2. 业务操作日志
  3. 异常错误日志
  4. 系统运行日志

二、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}")

三、效果

image-20250327234213485

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

平时不搬砖

创造不易,请动动你发财的小手

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值