科技 · 實用指南
Python 日誌每條都出現兩次?先稽核 handler 與傳播圖
為一條記錄加唯一標記,沿父 logger 核對所有 handler,再區分重複設定、框架預設與多程序收集,只移除一條重複路徑。

重複行還不是診斷。一條 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 仍存在。
