9日間、誰も気づかなかった沈黙 — 通知が死ぬと、障害も静かに消える
今回の登場人物
Ringo(リンゴ)
AI パートナー / 解析・運用支援
WebManagements システムを担当。サイトの健康状態を常時監視し、SEO・パフォーマンス・ログの異変を最初に嗅ぎつける解析の専門家。静かな障害ほど早く気づく。
チームツクルン全プロジェクトの Webマーケティング管理システム。Google Search Console・Analytics・PSI を統合した社内インフラ。
ある朝、レポートが来なかった
毎朝 8 時を少し過ぎた頃、Slack にツクルン HP の朝レポートが届く。サイトのクリック数、順位変動、インデックス状況が一行ずつまとめられた、小さな定点観測だ。
それが来なくなっても、すぐには気づかなかった。
他のプロジェクト(album-sweet や membo)のレポートは毎朝届いていた。ツクルンだけ静かになっていた。でも「ツクルンは今のところ課題が少ないから、レポートが短くなったのかな」と、そのまま流してしまった。ひとつのサイトが沈黙していても、全体が動いていると問題に見えない。
9日が経ってから、リンゴがログを開いた。
エラーは毎朝届いていた。どこにも届かない形で
ログには、整然と並んだ記録があった。
[2026-05-26] daily-analytics-report.js ... FAILED
[2026-05-27] daily-analytics-report.js ... FAILED
[2026-05-28] daily-analytics-report.js ... FAILED
...
[2026-06-03] daily-analytics-report.js ... FAILED
毎朝、スクリプトは走っていた。そして毎朝、静かに失敗していた。
真因は単純だった。設定ファイル(.report-config.json)の webhook_url が未設定のまま保存されていた。Slack への送信先がなかった。
しかし、ここで問題の本質が現れる。
エラー通知の経路自体が死んでいたから、エラーが通知されなかった。
障害を知らせるはずの仕組みが壊れていると、障害が存在しないように見える。「通知が来ない=異常なし」という解釈が自然に生まれてしまう。これは入れ子構造の罠だ——監視の死角を監視するものがいない。
リンゴが設計した「不在検知」
真因を特定したリンゴは、webhook_url を設定ファイルに正しく保存して即日修正した。翌朝からレポートが届き始めた。
ただ、直すだけでは終わらせなかった。リンゴが提案したのは「不在検知」という設計概念だ。
「通知システムの障害は通知されない。"来ないこと"を検知する仕組みが要る」
再発防止として設計したのは、診断フローだ:
- ログを確認する(スクリプトが走ったか)
- cron を確認する(定期実行が生きているか)
- webhook を確認する(送信先が設定されているか)
- 設定ファイルを確認する(必須キーが全部あるか)
「なぜ届かないか」ではなく「どこから届かなくなったか」を上から順に追う。これを SKILL として文書化し、次に沈黙が起きた時の診断コストをゼロに近づけた。
9日間の学び
修正後、ツクルンの朝レポートは毎朝届いている。あの沈黙の間も、サイト自体は動いていた。順位も落ちていなかった。だから被害はなかった——ただ、9日間ブラインドだった。
リンゴはそれを「気づかなかった事実」として残した。直ったことより、気づけなかった構造を問題とした。その姿勢が、今のチームの監視設計を一段階上げた。
【技術コラム】「不在検知」を自分のシステムに組み込む
今回の9日間障害は、エンジニアなら一度は経験するタイプの罠だ。「エラーが出ない=正常」という前提が崩れる瞬間——それは、エラーの出口が閉じた時に訪れる。
パターン1: watchdog(番犬)方式
定期ジョブが「動いた証拠」を残し、別プロセスがそれを見張る。
# cron ジョブ側(毎朝8時)
0 8 * * * /path/to/daily-report.sh && date > /var/run/daily-report.last_success
# watchdog 側(毎朝9時に確認)
0 9 * * * python3 /path/to/watchdog.py
# watchdog.py の核心部分
import os, time, requests
HEARTBEAT_FILE = "/var/run/daily-report.last_success"
SLACK_URL = "[WEBHOOK_URL]"
if not os.path.exists(HEARTBEAT_FILE):
requests.post(SLACK_URL, json={"text": "⚠️ 朝レポート: ハートビートファイルがありません"})
else:
mtime = os.path.getmtime(HEARTBEAT_FILE)
age_hours = (time.time() - mtime) / 3600
if age_hours > 25: # 25時間以上更新されていない
requests.post(SLACK_URL, json={"text": f"⚠️ 朝レポート: {age_hours:.1f}時間 未更新(最終成功: {time.ctime(mtime)})"})
パターン2: 設定ファイル起動時バリデーション
スクリプト起動の冒頭で、必須キーを全件確認する。
// Node.js の例
function validateConfig(config) {
const required = ['webhook_url', 'api_key', 'target_site'];
const missing = required.filter(key => !config[key]);
if (missing.length > 0) {
throw new Error(`設定不足: ${missing.join(', ')} が未設定です。処理を中止します。`);
}
}
// main の冒頭で必ず呼ぶ
validateConfig(config); // ここで止まれば、エラーがコンソールに残る
このパターンの強みは、「走り始めた瞬間に落ちる」こと。9日間黙って失敗し続けるより、初日に明確なエラーを出す方がはるかに早く気づける。
パターン3: ログ監視で「来なかった」を検出する
Slack ではなくログだけが証拠として残る場合、ログの最終更新日時を監視する。
# シェルスクリプト例(毎朝チェック)
LOG_FILE="/var/log/daily-report.log"
LAST_LINE=$(tail -1 "$LOG_FILE" 2>/dev/null)
if ! echo "$LAST_LINE" | grep -q "$(date +%Y-%m-%d)"; then
echo "今日のログエントリが見つかりません。日次レポートが未実行の可能性があります。" | \
curl -X POST "[WEBHOOK_URL]" -H 'Content-type: application/json' \
-d "{\"text\": \"⚠️ daily-report: 今日のエントリがありません\"}"
fi
どのパターンを選ぶにしても、核心は一つだ——「来ないこと」を積極的に観察する場所を作る。待つ監視ではなく、問いかける監視へ。
リンゴが9日後に発見したのは、設定の欠落だけでなく、この「問いかける構造」が欠けていたという事実だった。