自動ヘルスチェックを運用していると、**「本来は正常なのに ERROR を出す」**偽陽性に必ず悩まされます。月初の空ディレクトリ、/tmp 配下の symlink で find が0件を返す、累計ログが閾値を超え続ける——どれも障害ではないのに、毎朝アラートを見るたびに「これは本物か?偽陽性か?」と判断を迫られます。
これを繰り返すとアラート疲労になり、やがて本物の障害まで「どうせ偽陽性だろう」と流してしまいます。最悪の事故です。
本記事は、この問題への対処として「偽陽性を減らそうとするのではなく、既知の偽陽性パターンを専用ドキュメント(SoT: Source of Truth)に記録する」という運用の記録です。AI agent と一緒に運用する時代だからこそ効く、地味だが強力な手です。
事の発端:毎朝「これ偽陽性だっけ?」を繰り返していた
ジョブ監視スクリプト(仮に validate_job_health.sh とします)が、毎朝いくつかの ERROR を吐いていました。そのたびに私はログを追い、コードを読み、「あ、これは仕様か」と納得して閉じる——という作業を繰り返していました。
問題は、この納得が記録に残らないことです。3日後に同じ ERROR を見ると、また一から「これは何だっけ?」と調べ直します。判断のたびに5〜10分が溶けていきました。
さらに悪いのは、AI agent にログ確認を頼んだときです。agent は文脈(過去にこれは偽陽性と判定済み、という履歴)を持っていないので、毎回「ERROR が出ています、調査が必要です」と報告してきます。人間も AI も、同じ偽陽性を毎回ゼロから判定していたのです。
既知偽陽性を SoT 化する
そこで、KNOWN_FALSE_POSITIVES.md のような専用ドキュメントを1枚作りました。構造はシンプルで、**「エラー文字列 → これは偽陽性か → なぜか → どう確認するか」**を並べるだけです。
実際に記録した4パターンを、匿名化して紹介します。
パターンA:symlink で find -maxdepth 1 が0件を返す
1## Pattern A: 出力ディレクトリが空に見える(symlink 経由)2
3**症状**: "FAIL: no output files found in /data/out/"4**偽陽性か**: YES(条件付き)5**原因**: /data/out が /tmp 配下への symlink になっている環境で、6 `find /data/out -maxdepth 1 -name '*.parquet'` が7 symlink をたどらず0件を返すことがある。8 POSIX の find は -maxdepth と symlink follow の組み合わせで9 直感に反する挙動をする。10**確認方法**:11 ls -laL /data/out/ # -L で symlink をたどって実体を見る12 → 実体にファイルがあれば偽陽性確定これはツールの仕様起因の偽陽性です。find が POSIX の規約通りに動いているだけで、バグではありません。しかし知らないと「ファイルが消えた!」と慌てます。こういう仕様起因の罠こそ、言語化して記録する価値が高いものです。
パターンB:累計ログが閾値を超え続ける
1## Pattern B: エラー件数が閾値超過(累計集計の罠)2
3**症状**: "WARN: error count 152 exceeds threshold 100"4**偽陽性か**: YES5**原因**: チェックスクリプトがログファイル全体(起動以来の累計)を6 grep してエラー件数を数えている。日次の新規エラーではなく、7 運用開始以来の総数なので、時間が経てば必ず閾値を超える。8**確認方法**:9 当日分だけを抽出して数える:10 grep "$(date +%Y-%m-%d)" job.log | grep -c ERROR11 → 当日0件なら偽陽性確定これは集計スコープの設計ミス起因です。本来は日次で数えるべきところを累計で数えていた。SoT に書いておくことで「またこれか」と即判定でき、かつ「いつかスクリプト側を直す」という TODO の置き場にもなります。
パターンC:月初・年初の空ディレクトリ
1## Pattern C: 月初/年初にファイルが0件2
3**症状**: "FAIL: directory /data/2026-06/ is empty"4**偽陽性か**: YES(月初・年初の数日のみ)5**原因**: 月別ディレクトリは初回書き込み時に作られる。6 月初の最初のジョブが走る前は空 or 不在で正常。7**確認方法**: 実行日が月初3営業日以内なら偽陽性とみなす。これは時刻依存の偽陽性です。カレンダー境界でだけ起きるので、再現性が低く、記録しないと忘れます。
パターンD:下流チェックが上流より先に走る stale 判定
1## Pattern D: 下流の鮮度チェックが stale を報告2
3**症状**: "STALE: downstream input older than 2h"4**偽陽性か**: YES(実行順序による)5**原因**: 下流の鮮度チェックジョブが、上流の生成ジョブより6 先に発火するスケジュールになっている日がある。7 上流が走れば解消する一時的な stale。8**確認方法**:9 上流ジョブの最終実行時刻を確認し、10 下流チェックより後なら次回サイクルで解消 → 偽陽性これはジョブ間の実行順序起因です。スケジュール設計の歪みが偽陽性として現れるパターンです。
SoT 化がもたらす3つの効果
この1枚のドキュメントを作ったことで、運用が明確に変わりました。
効果1:判定が秒で終わる
ERROR を見たら、まず SoT のエラー文字列と照合します。一致すれば「パターンB、累計ログの罠」と即断でき、確認コマンド1本で裏が取れます。5〜10分かかっていた判定が30秒になりました。
効果2:本物の障害が「浮かび上がる」
既知偽陽性が全部 SoT に載っていると、SoT に載っていない ERROR = 要調査という単純なフィルタが成立します。ノイズが既知パターンとして畳まれることで、本物の異常がコントラストで目立つようになります。これがアラート疲労の本質的な解決です。
効果3:AI agent に文脈を渡せる
ログ確認を AI agent に頼むとき、この SoT を読ませてから依頼します。
1ログを確認する前に KNOWN_FALSE_POSITIVES.md を読むこと。2既知偽陽性パターンに一致する ERROR は「既知(パターンX)」とラベルし、3一致しないものだけを「要調査」として報告すること。すると agent は「ERROR が3件、うち2件は既知偽陽性(パターンA・B)、残り1件は未知のため要調査」と報告してきます。人間がやっていた一次トリアージを、SoT を介して agent に委譲できるのです。
なぜ「減らす」より「記録する」なのか
偽陽性を見つけたら、普通は「スクリプトを直して偽陽性をなくそう」と考えます。それも正しいのですが、全部を即座に直すのは現実的でないことが多いです。
- パターンA(symlink)は環境固有で、直すと別環境で壊れるリスクがある
- パターンB(累計ログ)はスクリプト改修が必要で、優先度が上がりにくい
- パターンC(月初)は年に12回しか起きず、修正コストが見合わない
つまり「修正」は時間がかかる一方、偽陽性は今日も明日も出ます。「記録」は5分で終わり、その瞬間から判定コストを下げられます。 記録してから、優先度の高いものを順に修正していけばいい。SoT は「修正待ちの偽陽性」の在庫管理表でもあるのです。
そして重要なのは、記録は組織知になることです。「あの ERROR は偽陽性」という知識が個人の頭の中にあると、その人がいないと判定できません。SoT に書けば、誰でも、AI agent でも、同じ判定ができます。
まとめ:偽陽性は「敵」ではなく「記録対象」
自動ヘルスチェックは、必ず偽陽性を持ちます。これは検知の網を広く張る以上、避けられないトレードオフです。問題は偽陽性そのものではなく、毎回ゼロから判定し直すことです。
だから、
- 偽陽性を見つけたら、エラー文字列・原因・確認方法を SoT に1エントリ追加する
- SoT に載っていない ERROR だけを「要調査」とする運用にする
- AI agent にもこの SoT を読ませ、一次トリアージを委譲する
- 時間ができたら、SoT を在庫表として偽陽性の根本修正を進める
特に find の symlink 挙動のようなツールの仕様起因の偽陽性は、知らないと永遠に慌て続けます。一度言語化して SoT に刻めば、二度と同じ時間を溶かさずに済みます。
AI 駆動開発で監視スクリプトを量産する時代、偽陽性は増えこそすれ減りません。それを敵とみなして潰し続けるより、記録して組織知に変えるほうが、人間にも AI にも優しい運用です。