FastAPI × Loguru:从“能跑”到“可运维”的日志实战

一句话总结

把散落在 Uvicorn / FastAPI / 业务代码里的所有日志,全部收敛到 Loguru 一个管道,统一格式、统一分级、统一落盘,并解决重复输出、异步安全、请求溯源三大痛点。


1. 为什么要"多此一举"?

痛点 原生 logging 的表现 Loguru 的改进
重复输出 Uvicorn 与 FastAPI 各用各的 Handler,一条请求在终端出现 2~3 次 拦截标准库,彻底干掉重复 Handler
格式不统一 access 日志无颜色,业务日志难加字段 一份模板控制所有目的地,支持彩色/JSON
异步安全 多 worker 下文件锁竞争,出现错乱 enqueue=True 内部队列 + 协程安全
可观测性 报错时只能看到框架栈,找不到业务参数 中间件注入 request_id,一次 grep 定位全链路

2. 最小可运行版本(MVP)

安装依赖

bash 复制代码
pip install fastapi uvicorn[standard] loguru

main.py

python 复制代码
from fastapi import FastAPI
from loguru import logger
import uvicorn, sys

# 1. 移除默认 handler,防止重复
logger.remove()
logger.add(sys.stdout,
           colorize=True,
           format="<green>{time:HH:mm:ss}</green> | <lvl>{level: <8}</lvl> | {message}")

app = FastAPI()

@app.get("/ping")
def ping():
    logger.info("pong")
    return {"msg": "pong"}

if __name__ == "__main__":
    # 2. 关键:禁用 uvicorn 的默认日志配置
    uvicorn.run("main:app", host="0.0.0.0", port=8000, log_config=None)

终端现在只有一行带颜色的 pong,世界安静了。


3. 生产级脚手架:七步统一日志

把下面代码放到 app/core/logging.py,项目启动时一次 setup_logging() 即可。

步骤 目的 关键参数
① 清空默认 防止重复 logger.remove()
② 彩色终端 开发调试 colorize=True
③ 文件轮转 避免打爆磁盘 rotation="100 MB", retention="30 days"
④ 错误分流 error.log 单独分析 level="ERROR"
⑤ 异步写入 高并发不阻塞 enqueue=True
⑥ 拦截标准库 收编 Uvicorn InterceptHandler
⑦ 注入 request_id 全链路追踪 bind(request_id=...)

完整代码(可直接复用)

python 复制代码
import logging, sys, os
from pathlib import Path
from loguru import logger
from contextvars import ContextVar

# 全链路 request_id
request_id: ContextVar[str] = ContextVar("request_id", default="-")

class InterceptHandler(logging.Handler):
    """把标准库日志无缝转到 loguru"""
    def emit(self, record: logging.LogRecord):
        try:
            level = logger.level(record.levelname).name
        except ValueError:
            level = record.levelno
        frame, depth = logging.currentframe(), 2
        while frame and frame.f_code.co_filename == logging.__file__:
            frame = frame.f_back
            depth += 1
        logger.opt(depth=depth, exception=record.exc_info).log(
            level, record.getMessage()
        )

def setup_logging():
    log_dir = Path("logs")
    log_dir.mkdir(exist_ok=True)

    # 统一格式
    fmt = ("<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
           "{extra[request_id]} | <lvl>{level: <8}</lvl> | "
           "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <lvl>{message}</lvl>")

    logger.remove()
    logger.add(sys.stdout, format=fmt, level="INFO", colorize=True)
    logger.add(log_dir / "app.log", rotation="100 MB", retention="30 days", enqueue=True, format=fmt)
    logger.add(log_dir / "error.log", level="ERROR", rotation="100 MB", retention="30 days", enqueue=True, format=fmt)

    # 收编三方库
    logging.basicConfig(handlers=[InterceptHandler()], level=logging.INFO)
    for name in ["uvicorn", "uvicorn.access", "uvicorn.error", "fastapi"]:
        _log = logging.getLogger(name)
        _log.handlers = [InterceptHandler()]
        _log.propagate = False

FastAPI 中间件:自动注入 request_id

python 复制代码
from fastapi import Request
import uuid

@app.middleware("http")
async def add_request_id(request: Request, call_next):
    rid = request.headers.get("X-Request-ID") or str(uuid.uuid4())
    request_id.set(rid)
    with logger.contextualize(request_id=rid):
        logger.info(f"{request.method} {request.url.path}")
        response = await call_next(request)
    response.headers["X-Request-ID"] = rid
    return response

一次 grep <request_id> 即可把 access + 业务 + 错误 日志全部拉出来。


4. 常见问题 FAQ

现象 根因 解法
终端仍然出现双份 旧 handler 未清干净 确保 logger.remove() 在新增 add 之前执行
多进程日志错乱 未开异步队列 所有 logger.add(..., enqueue=True)
422 报错无请求体 框架提前抛错 在中间件里 try/exceptlogger.bind(body=...)
日志文件中文乱码 Windows 默认 GBK 显式 encoding="utf-8"

5. 一键启动脚本

python 复制代码
# run.py
import uvicorn, asyncio
from app.core.logging import setup_logging

async def main():
    await setup_logging()
    config = uvicorn.Config("app.main:app", host="0.0.0.0", port=8000, log_config=None)
    server = uvicorn.Server(config)
    await server.serve()

if __name__ == "__main__":
    asyncio.run(main())

log_config=None 写死,彻底断绝 Uvicorn 自建 handler 的机会。


6. 结语

日志不是"打印字符串",而是可观测性的地基

用 Loguru 把格式、分级、轮转、追踪一次做到位,以后查问题只需带上 request_idgrep,再也不用面对满屏重复、格式乱糟糟的终端发呆。Happy logging!

相关推荐
爱勇宝20 小时前
第 1 章:别把“需求文档”当成真正的需求
前端·后端·程序员
IT_陈寒1 天前
闭包陷阱让我加了两天班,JavaScript你真行
前端·人工智能·后端
易协同低代码1 天前
通达OA核心类库TD类深度解析
后端
Gopher_HBo1 天前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志1 天前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..1 天前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects1 天前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰1 天前
一些基础和问题解决
后端
陈随易1 天前
前端项目部署只要30秒
前端·后端·程序员
YIAN1 天前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp