Watch at Ease看見世界,讀懂生活。

科技 · 實用指南

Python 日誌每條都出現兩次?先稽核 handler 與傳播圖

為一條記錄加唯一標記,沿父 logger 核對所有 handler,再區分重複設定、框架預設與多程序收集,只移除一條重複路徑。

Original diagram tracing one LogRecord through child, ancestor and root handlers.
原創示意圖:改 propagation 前追蹤記錄經過的每個 handler。 Watch at Ease

重複行還不是診斷。一條 LogRecord 可能同時經過子 logger 與祖先 handler;request path 也可能反覆向同一 logger 加 handler;框架可能另有 stream;不同 process 或 collector 也可能產生看似相同的事件。

安全修正要先標記測試記錄,記下 PID、logger 與 handler identity,畫出 emitter 到所有 sink 的路徑。只改一個 ownership boundary,再重播原命令。

先證明是事件重複還是輸出重複

送出唯一 token 並記錄 PID、logger name、level 與高精度時間;console、file 與 collector 分開捕捉。相同文字可能是兩個事件,不同格式也可能是同一記錄兩次處理。

要透過真實 command、reloader、worker launcher 或 service unit 重播;直接 python app.py 不能代表 uvicorn、gunicorn、cron 或 container。

匯出 logger 到 handler 的完整圖

記錄 emitter logger 的 name、disabled、effective level、propagate,以及每個 direct handler 的 object id、class、level、stream/filename 與 formatter,再一路走到 root。

若 child 與 ancestor 有相同 handler 或等價 sink 且 propagation 開啟,一條記錄就有兩條 delivery path。保留選定 owner 或在明確 boundary 停止傳播。

分開重複初始化與 propagation

在 import 前、startup 後、一次 request 後和 reload 後記錄 handler count。圖穩定但 child 與 root 都有 handler 代表 propagation;數量持續增加代表非 idempotent 設定。

把設定移到單一 startup path 或重用已知 handler。不要只按 class 去重,兩個 StreamHandler 可能有意寫不同 stream。

尊重 framework 與 process ownership

basicConfig 或 dictConfig 前先檢查 framework logger。Uvicorn 的 parent、error 與 access branch 需分別保留;比較 live graph 與 current docs。

probe 要包含 PID 與 process name。多 worker 應使用 QueueHandler 到單一 listener 或 external collector,並檢查 stdout/stderr 是否被重複收集。

移除一條 edge 並執行驗收矩陣

每個 sink 只指定一個 owner:application root、framework branch、queue listener 或 external collector。只移除證據確認的重複 edge,並保留 rollback。

重播 startup、成功 request、application warning、framework error 與 access event。每個預期 sink 只有一條 probe,reload 後 count 穩定且必要 stream 仍存在。

Original decision diagram separating ancestor propagation, repeated setup, framework and process duplication.
原創示意圖:四種重複分支需要不同修正。 Watch at Ease