一句话总结
把散落在 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
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!