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!

相关推荐
Craaaayon2 小时前
如何选择两种缓存更新策略(写缓存+异步写库;写数据库+异步更新缓存)
java·数据库·redis·后端·缓存·mybatis
唐僧洗头爱飘柔95273 小时前
【GORM(3)】Go的跨时代ORM框架!—— 数据库连接、配置参数;本文从0开始教会如何配置GORM的数据库
开发语言·数据库·后端·golang·gorm·orm框架·dsn
Jonathan Star3 小时前
在 Go 语言中,模板字符串
开发语言·后端·golang
盘古开天16664 小时前
从零开始:如何搭建你的第一个简单的Flask网站
后端·python·flask
用户21411832636024 小时前
Claude Skills 从零到一:手把手打造专属公众号文风生成器,10 分钟搞定 AI 技能定制
后端
追逐时光者4 小时前
C#/.NET/.NET Core技术前沿周刊 | 第 60 期(2025年11.1-11.9)
后端·.net
码上成长5 小时前
GraphQL:让前端自己决定要什么数据
前端·后端·graphql
码事漫谈5 小时前
C++双向链表删除操作:由浅入深完全指南
后端
码事漫谈5 小时前
软件生产的“高速公路网”:深入浅出理解CI/CD的核心流程
后端