科技 · 实用指南
Python 日志每条都出现两次?先审计 handler 与传播图
给一条记录加唯一标记,沿父 logger 核对每个 handler,再区分重复配置、框架默认值和多进程采集,只删除一条重复路径。

重复行还不是诊断。一条 LogRecord 可能同时经过子 logger 和祖先 handler;请求路径也可能反复向同一个缓存 logger 添加 handler;框架可能另有输出流;多个进程或外层采集器也可能生成看似相同的事件。
安全修复要先给测试记录加标记,记录 PID、logger 和 handler 身份,画出发射点到所有接收端的路径。只改变一个所有权边界,再重放原命令。
先证明是事件重复还是仅输出重复
发送带唯一 token 的记录,并记录 PID、logger 名、level 和高精度时间。分别捕获控制台、文件与采集器输出。相同文字可能是两条事件,不同格式也可能是同一记录被处理两次。
必须通过真实命令、reloader、worker launcher 或服务单元重放;直接运行 python app.py 不能代表 uvicorn、gunicorn、cron 或容器路径。
导出 logger 到 handler 的完整图
记录发射 logger 的 name、disabled、effective level、propagate,以及每个直接 handler 的对象 id、类型、level、stream 或 filename 与 formatter,然后沿 parent 一直到 root。
若子与祖先存在同一 handler 或等价接收端且 propagation 开启,一条记录就有两条投递路径。保留选定最高责任方的 handler,或在明确边界停止传播,不能盲目同时改两处。
区分重复初始化与传播
在 import 前、应用启动后、一次请求后和 reload 后记录 handler 数量。图保持稳定但子与 root 均有 handler,指向传播;数量每次请求都增加,指向非幂等配置。
把配置移到唯一启动路径,或复用已知 handler。不要只按 handler 类型去重:两个 StreamHandler 可能有意写不同流,两个 FileHandler 也可能解析到同一文件。
尊重框架与进程所有权
调用 basicConfig 或 dictConfig 前先检查框架 logger。Uvicorn 有父、错误和访问分支,保留 access log 的决策可能不同于应用日志。应把实时图与当前文档比较。
探针必须包含 PID 和进程名。多 worker 应通过 QueueHandler 交给一个 listener 或外部采集器,不能各自轮转同一文件;还要核对 stdout 与 stderr 是否被外层服务重复采集。
删除一条边并运行验收矩阵
为每个接收端选择一个责任方:应用 root、框架分支、queue listener 或外部采集器。只删除证据确认的重复 handler、重复配置调用、传播边或采集路线,并保留回滚。
重放启动、一次成功请求、一次应用 warning、一次框架 error 和一次 access 事件。每个目标接收端只能出现一条探针,另一请求与 reload 后 handler 数量稳定,必要日志流必须仍在。
