ログ欠落は、防ぐものではなく「説明できるもの」として設計する
ログが欠けたとき、目に見える現象はだいたい同じだ。
ファイルに行がない。
でも、その裏にある原因は同じではない。
- ロガーに届く前に子プロセスが落ちた
- キューが詰まって取りこぼした
- 出力先が拒否した
- 一部の出力先には届いた
- ロガーが説明できない欠落がある
全部を「ログがない」で片付けると、調査の入口を間違える。
前回、ログ設定は運用契約だと書いた。その契約で一番重いのが、「何を失敗として扱うのか」だと思う。
なぜ調査は「ログがない」で止まるのか
Python の標準 logging は、ログレコードを handler に渡す仕組みを持っている。エラー時には handleError を通じて標準エラーへ出ることもある。
ただし、それだけでは「届いた」「拒否された」「取りこぼした」「説明できない」を同じ場所で数える構造にはならない。
その語彙がないと、調査は「ログがない」で止まる。止まった後、OS・キュー・終了処理・出力先・アプリケーション例外のどれを見ればいいのかを選びにくい。
欠落に名前を付ける
自分なら、ログ欠落を少なくとも次の 5 つに分けて見る。
| 状態 | 意味 |
|---|---|
| delivered | 必要な出力先へ配送できた |
| partial_delivered | 一部の出力先には届いた |
| known_rejected | 書き込み側 / 出力先が既知の理由で受け付けなかった |
| known_dropped | 上限付きキューや配送ルールにより既知の理由で取りこぼした |
| unexplained_lost | 届くはずだったがロガーが理由を説明できない |
unexplained_lost という名前は、できれば見たくない。
でも、隠すより残した方がいい。そこに異常があると分かるからだ。
分類は保証ではない
ログ設計でよくある危険は、「失敗しない」と言いたくなることだ。
でも、OS 障害、電源断、強制終了、子プロセスの異常終了まで含めて、ローカルのロガーが全部を保証するのは無理がある。
現実的には、次の線引きになる。
- ロガーが受け入れたものをどこまで追えるか
- 取りこぼしや拒否を既知のものとして数えられるか
- 説明できない欠落を説明できないまま残せるか
この方が、派手ではないが誠実だと思っている。
分類は、ログ欠落を消すためのものではない。保証できないものを保証したふりをしないためのものだ。
ケーススタディ: D-SafeLogger ではこう名付けた
D-SafeLogger のマルチプロセス構成では、この考え方を、親プロセス側の書き込み役に寄せて実装している。子プロセスは共有ログファイルを直接開かず、ログレコードを書き込み役へ送る。書き込み役が出力先ファイル、出力先の切り替え、終了処理、配送状態の集計を持つ。
子プロセス -> キュー -> 親プロセスの書き込み役 -> 出力先ファイル
所有権を親プロセス側へ寄せる理由は、Qiita #3 で実装寄りに書いた。ここで見たいのは、その構成の速さではなく、欠落をどう説明できる形にするかだ。
D-SafeLogger の mp.GetDeliveryStatus() は、実行中の集計を見る API だ。終了時の報告は、終了時点の最終集計を残すためのものだ。両方があることで、「いま何が起きているか」と「最後にどう終わったか」を切り分けて読める。
参考
- D-SafeLogger: https://github.com/nightmarewalker/D-SafeLogger
- Multiprocess Logging example: https://github.com/nightmarewalker/D-SafeLogger/blob/main/examples/12_multiprocess_logging.md
終わりに
ログ欠落に名前を付けるのは、気持ちのよい作業ではない。
でも、名前がない欠落は調査しにくい。
「ログがない」で止めずに、「なぜないのか」を分類する。キューや非同期バッファを挟むログ経路では、そのために少し複雑な設計を選ぶ価値がある。
関連記事
Zenn 連載:
- #1 ログファイルの切り替えは、rename ではなく「次の書き先の選択」として設計する
- #2 ログ設定は初期化コードではなく、運用契約として設計する
- #3 ログ欠落は、防ぐものではなく「説明できるもの」として設計する(本記事)
- #4 「ログは stdout に出しておけば OK」で済まない場合の話
Qiita の関連回:
Discussion