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/except 并 logger.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_id 和 grep,再也不用面对满屏重复、格式乱糟糟的终端发呆。Happy logging!

相关推荐
高频因子挖掘机11 分钟前
QuantDash 明明安装成功,为什么 Python 还是无法 import?
后端·github·api
Java爱好狂.8 小时前
Java初学者如何一周速通SpringBoot!
java·spring boot·后端·spring·高并发·架构师·java面试
马良神笔10 小时前
2026 年Rag最近技术前沿总结
后端·python·django
程序猿DD10 小时前
同样10美元,OpenDesign和Command Code的DeepSeek V4.1 Flash额度到底有多少?
后端
高频因子挖掘机10 小时前
A股量化从日线升级到5分钟线:数据量增加后最先暴露哪些工程问题?
后端·github·api
高频因子挖掘机10 小时前
历史行情返回顺序变了,为什么按行号计算指标很危险?
后端·github·api
团子哒哒12 小时前
数组、切片和Map
后端
专注仿真12 小时前
ThinkPHP6 API接口开发实例:从零搭建RESTful风格接口
后端·php·restful
开心猴爷12 小时前
应对iOS审核4.3:可以使用 IpaGuard 来给属性名混淆的技术细节与平衡之道
后端·ios
1m5555555512 小时前
RAG 工程实战(五):正确知识已经进入 Prompt,为什么大模型还是会答错?
后端·面试