loguru重复写日志的根本原因是多次调用add()注册了多个handler,尤其在fastapi/flask中因模块重复导入或请求中误调用导致;应全局仅初始化一次,用remove()清理默认handler后再add(),避免在路由、中间件或__init__中调用。

Loguru 的 add() 方法为什么总在重复写日志?
根本原因是每次调用 add() 都会注册一个新 handler,而 Web 应用(如 FastAPI/Flask)常在模块导入时或应用启动时反复执行初始化逻辑。如果你把 logger.add(...) 放在模块顶层、或每次请求都调用它,就会累积多个相同输出目标的 handler。
实操建议:
- 全局 logger 初始化只做一次:用
logger.remove()清掉默认 handler,再add()你想要的;推荐放在应用入口(如main.py最上方)或通过单例封装 - 避免在路由函数、中间件、类的
__init__中调用add() - 若需动态增删 handler(如按用户切换日志路径),务必记录返回的 handler ID,并用
logger.remove(handler_id)精确移除
如何让 Loguru 输出结构化 JSON 日志并兼容 ELK?
Loguru 默认输出是纯文本,ELK 栈(Elasticsearch + Logstash + Kibana)依赖字段可解析的 JSON。直接用 serialize=True 不够——它只是把整个 log record 打包成 JSON 字符串,字段名不标准、嵌套深、缺少 trace_id 等关键上下文。
实操建议:
- 用
format参数自定义输出结构,配合logger.patch()注入请求 ID 或用户 ID - 禁用
serialize=True,改用json.dumps()手动构造字典,确保字段名对齐团队约定(如"timestamp","level","trace_id","service") - 示例片段:
import json from loguru import logger <p>def json_formatter(record): subset = { "timestamp": record["time"].isoformat(), "level": record["level"].name, "message": record["message"], "service": "my-api", "trace_id": record["extra"].get("trace_id", ""), } return json.dumps(subset, ensure_ascii=False) + "\n"</p><p>logger.add("logs/app.json", format=json_formatter)</p>
在 FastAPI 中如何为每个请求注入唯一 trace_id 并贯穿日志?
Loguru 本身不感知 HTTP 请求生命周期,extra 字段是线程/协程局部的,但 FastAPI 默认用异步协程,且中间件与路由间不自动传递 extra。直接在中间件里 logger.bind(trace_id=...) 不会生效到后续日志中。
实操建议:
- 用
contextvars创建协程本地变量trace_id_var,在中间件中set(),再通过logger.patch()每次格式化前读取 - 不要依赖
logger.bind()后续自动继承——它只影响当前日志调用,不持久化到 logger 实例 - 关键代码模式:
import contextvars from loguru import logger <p>trace_id_var = contextvars.ContextVar("trace_id", default="")</p><p>logger = logger.patch(lambda r: r["extra"].update(trace_id=trace_id_var.get()))</p><h1>在 FastAPI 中间件里:</h1><p>@app.middleware("http") async def add_trace_id(request: Request, call_next): trace_id = request.headers.get("X-Trace-ID") or str(uuid4()) trace_id_var.set(trace_id) response = await call_next(request) return response</p>
Loguru 和 Gunicorn/Uvicorn 部署时为什么日志丢失或错乱?
Gunicorn 的多 worker 模式下,多个进程共享同一个文件 handler 会导致竞态写入(日志混行、截断);Uvicorn 的 reload 模式则可能触发多次 add(),造成重复 handler。更隐蔽的问题是:子进程不会自动继承父进程的 logger 配置。
实操建议:
- 禁用文件 handler 的多进程并发写入:改用
rotation+enqueue=True,让 Loguru 内部用队列转发日志到主进程写入 - Gunicorn 启动时加
--preload,确保 logger 初始化只在主进程发生;或把logger.add()移到post_fork钩子里(每个 worker 单独 add) - Uvicorn 开发时用
--reload必须配合logger.remove()开头,否则热重载会不断叠加 handler
Loguru 的简洁性容易让人忽略它的运行时模型——它不是“配置即生效”的静态系统,而是依赖 Python 运行时状态(handler 列表、contextvar、协程上下文)。最常出问题的地方,永远是初始化时机和作用域范围,而不是语法写错。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











