Loguru需手动构建生产日志体系,关键在初始化时机、handler管理与结构化输出设计;FastAPI/Flask中重复日志源于多次调用logger.add(),须全局仅初始化一次并配合contextvars传递trace_id。

Loguru 本身不自动构建生产日志体系,它只提供灵活的工具链;真正规范化的关键在于初始化时机、handler 管理方式和结构化输出设计——这三者出错,再好的配置也会导致重复日志、丢失上下文或 ELK 解析失败。
为什么 logger.add() 在 FastAPI/Flask 中总重复写日志
根本原因是每次调用 logger.add() 都会注册一个新 handler,而 Web 框架常因模块重载、热更新或中间件多次执行导致该语句被反复触发。比如在 FastAPI 的 @app.middleware 或 Flask 的 @app.before_request 里调用 logger.add(),每处理一个请求就新增一个文件写入器,最终日志被复制 N 次。
- 必须全局只做一次初始化:推荐放在
main.py或app/__init__.py顶部,且第一行执行logger.remove() - 禁止在路由函数、中间件、类
__init__或依赖注入函数中调用logger.add() - 若需动态增删(如按租户切日志路径),务必用
handler_id = logger.add(...)记录返回值,并通过logger.remove(handler_id)精确清理
如何输出真正可用的 JSON 结构化日志(非 serialize=True)
serialize=True 生成的是嵌套过深、字段名不统一、缺少 trace_id 的“伪 JSON”,ELK 无法直接提取 timestamp 或 service 字段。实际工程中应放弃该参数,改用自定义 formatter 手动构造字典。
- 用
json.dumps()显式控制字段名、类型和顺序,确保与团队日志规范对齐(如固定字段:timestamp、level、message、trace_id、service) - 所有业务上下文通过
logger.bind()注入extra,再在 formatter 中用record["extra"].get("xxx")提取 - 避免在 formatter 内做耗时操作(如网络请求、复杂计算),否则阻塞日志线程
示例:
立即学习“Python免费学习笔记(深入)”;
import json
from loguru import logger
def json_formatter(record):
return json.dumps({
"timestamp": record["time"].isoformat(),
"level": record["level"].name,
"message": record["message"],
"trace_id": record["extra"].get("trace_id", ""),
"service": "order-api",
"path": record["file"].path,
"line": record["line"]
}, ensure_ascii=False) + "\n"
logger.add("logs/app.json", format=json_formatter)
如何在 FastAPI 中为每个请求绑定唯一 trace_id 并贯穿全程
Loguru 的 extra 是协程局部的,但 FastAPI 默认不自动跨中间件/路由传递。直接在中间件里 logger.bind(trace_id=...) 只影响当次调用,后续日志仍无 trace_id。
- 必须用
contextvars.ContextVar创建协程变量(如trace_id_var = ContextVar("trace_id", default="")) - 在 ASGI 中间件中调用
trace_id_var.set(...),并在 formatter 或logger.patch()中读取该值 - 推荐用
logger.patch(lambda r: r["extra"].update(trace_id=trace_id_var.get())),比每次logger.bind()更轻量 - 注意:不要在同步函数(如数据库钩子)中读取该 contextvar,除非显式切换到当前协程上下文
最易被忽略的点是:Loguru 的 enqueue=True 虽能缓解 I/O 压力,但它不解决上下文丢失问题——协程变量不会自动跨队列传递,必须在日志格式化阶段重新捕获。


















