異常通知(Discord)の総点検 — テスト投稿と設計どおりの配信を誤警報にしない

  • #Discord
  • #監視
  • #CI
  • #障害対応
  • #自動化
異常通知(Discord)の総点検 — テスト投稿と設計どおりの配信を誤警報にしない

今回やったこと

複数の自動ジョブやCIから送られる Discord 通知の履歴を点検したところ、直近7日間の通知 143件のうち、およそ 15% にあたる 22件が「自動テストの実行」に起因して本番チャンネルへ誤って投稿されていたことが判明した。さらに、設計どおりのチャンネルへ正しく届いている通知に対して「重大度別の振り分けに失敗している」と自ら誤警告を重ねる判定バグも存在していた。

「異常通知が多い」という印象を受けたとき、システムの実態が壊れているのか、それとも観測側が自作自演でノイズを吐き出しているのかを客観的に切り分ける必要がある。本稿では、テスト環境からの通知漏出を完全に遮断した仕組みと、設計済みのチャンネル構成をコード上の正本として固定し直した改修の全容を記録する。

7日間の通知台帳を監査した結果

運用サーバ上で ~/.local/state/ntmedia/discord_notifications.jsonl に保存されている通知台帳を分析した。直近7日間(2026-08-21〜2026-08-28)における通知の内訳は以下のとおりであった。

区分件数割合判定
通知総数143件100.0%正常ログ・完了報告を含む
log(正常系ログ)124件86.7%正常終了・定期バッチ完了
alert(異常警報)19件13.3%調査が必要な警告
テスト由来の通知(総計)22件15.4%送信元ランキング第2位のノイズ
テスト由来の alert3件2.1%本番へ流すべきでない偽の異常
設計どおりのフォールバック警告4件2.8%判定スクリプト自身の誤警報

台帳の集計から明らかなように、本番 Discord 上で「連日アラートが出ている」ように見えていた現象の主因は、システムの不安定化ではなく観測経路自身の欠陥であった。

テストが本番へ書き込んでいた「意図と実装の乖離」

テスト由来の通知 22件の発生源は、プルリクエスト完了処理のテストスクリプト(scripts/test_finish_pr.sh)であった。

このスクリプトは正常系テストの中で本番の完了処理(scripts/lib/finish_pr.sh)を呼び出していたが、完了通知の抑制処理が正しく実装されていなかった。テストファイル内には「通知は飛ばさない」というコメントと、未使用の変数 notify_stub= が置かれていたものの、実際の送信関数はそのまま本番の Discord Webhook を叩き、本番台帳に書き込みを行っていた。

# 問題のテストコードにあった状態(コメントはあるが実装が無い)
# 通知は飛ばさない
notify_stub=""
# この後、普通に finish_pr.sh が実行されて本番 Discord へ投稿されていた

コメントが開発者の「意図」を示しているのに対し、ランタイムの「実装」が追いついていない典型的なケースである。テスト単体では正常終了するため、人手での動作確認でも気づきにくかった。

対策: NT_MEDIA_NOTIFY_SINK=off の導入

この問題に対し、scripts/notify_discord.py に厳密なテスト抑止環境変数 NT_MEDIA_NOTIFY_SINK=off を導入した。

# scripts/notify_discord.py の冒頭ガード
if os.environ.get("NT_MEDIA_NOTIFY_SINK") == "off":
    # 送信、ローカル台帳への書き込み、Issue作成要求のキューイングをすべて停止
    sys.exit(0)

テスト実行時はこの変数をエクスポートすることで、外部へのHTTPリクエストだけでなく、ローカル通知台帳へのログ追記や GitHub Issue 作成要求の蓄積もまとめて完全に抑止される。

さらに、回帰テスト(scripts/test_notify_discord.py)を追加し、NT_MEDIA_NOTIFY_SINK=off の条件下で通知スクリプトを実行しても、台帳ファイルの行数が1行も増加しないことをアサーションで担保した。

設計どおりの配信を「欠陥」とみなしていた誤警報

もう1つの問題は、異常通知が送られた際に末尾へ付与される「重大度別の振り分けが効いていません」というシステム警告であった。

プロジェクトでは、チャンネル構成として以下の3系統が正式に決定されていた。

  • #要対応 : DISCORD_WEBHOOK_ACTION(人間による介入が必須な障害)
  • #異常 : DISCORD_WEBHOOK_URL(従来の通知URLをそのまま異常用 catch-all 列として指定)
  • #ログ : DISCORD_WEBHOOK_LOG(定期ジョブ等の正常終了記録)

しかし、notify_discord.py 内の判定関数 webhook_fallback_warning() は、「DISCORD_WEBHOOK_ALERT が設定されておらず、第2候補の DISCORD_WEBHOOK_URL に落ちた」ことを一律に設定不備と判定していた。

実機(M1 / M5 サーバ)には決定どおり ACTION、LOG、URL の3本が正しく定義されており、未定義の ALERT にフォールバックするのは設計どおりの正常な振る舞いであった。しかし、この決定がコード内に正本として記述されておらず会話ログの中にしか残っていなかったため、監視コード自身が「設計どおりの動作」を「環境変数の設定漏れ」と誤認して警告を発していた。

# 改修: 設計されたチャンネル構成をコード上の正本として固定
DESIGNED_WEBHOOK_ENV = {
    "action": "DISCORD_WEBHOOK_ACTION",
    "alert": "DISCORD_WEBHOOK_URL",  # 設計上の catch-all 列
    "log": "DISCORD_WEBHOOK_LOG",
}

判定ロジックを改修し、警告を発するのは「他の重大度の列へ誤って落ちたとき(例: 要対応アラートが異常列に混入、または正常ログが異常列に混入)」という真の取り違え時のみに限定した。

やってみてわかったこと

通知基盤の保守において得られた知見は以下の3点である。

  1. コメントを信じず振る舞いを固定する: 「テストだから送信されないはず」という前提は脆い。環境変数レベルで完全にバイパスするゲートを設け、台帳行数が変化しないことをテストで直接検証しなければ漏出は防げない。
  2. 設計の決定をコードへ書き戻す: チャネル構成や環境変数のマッピングといった運用ルールは、口頭やドキュメントに留めず、コード内の定数(DESIGNED_WEBHOOK_ENV)として明記する。さもないと、未来の開発者やスクリプト自身が正常な設定を障害と誤解する。
  3. ノイズの犯人は外部ではなく観測者自身: アラート件数の増加に直面したときは、対象プロセスの不具合を疑う前に、通知送信元ランキングを集計し、テストや監視スクリプト自身の暴走がないかを確認することが復旧への最短ルートとなる。

更新履歴

  • 2026-09-03: 初稿。通知台帳の監査記録および PR #2882 の改修内容をもとに執筆。

訂正履歴

(なし)