データ処理パイプラインの監視を強化するたびに通知が増え、気づいたら1日に数十件のアラートが飛んでくる状態になっていました。重要なアラートも「またか」とスルーする——典型的なアラート疲労です。
「通知を減らす」ではなく「通知を構造化する」というアプローチで、この問題に取り組んだ記録です。
問題:アラートが多すぎて、何も見ない
アラート疲労の進行
1Stage 1: 「通知が来た!すぐ確認しよう」2Stage 2: 「また同じやつか。後で見よう」3Stage 3: 「通知多すぎ。チャンネルをミュートしよう」4Stage 4: 「本当に重要なアラートも見逃す」定量的な現状把握
まず1週間分の通知を集計しました。
1総アラート数: 247件/週2 - 即時対応が必要: 3件(1.2%)3 - 当日中に確認: 12件(4.9%)4 - 情報として有用: 45件(18.2%)5 - 無意味(ノイズ): 187件(75.7%)75%がノイズです。このノイズの中に埋もれた1.2%の重要アラートを拾い上げるのは、人間の注意力では不可能に近いでしょう。
ノイズの内訳
| ノイズの種類 | 件数/週 | 原因 |
|---|---|---|
| 閾値が厳しすぎる | 68 | 「念のため」で設定した低い閾値 |
| 一時的な遅延 | 52 | ネットワーク遅延等で閾値超過→自動復旧 |
| 同一事象の重複 | 41 | 1つの障害で複数ジョブが連鎖的にアラート |
| 既知の事象 | 26 | メンテナンス中のアラート等 |
解決策:4層のアラート構造化
「減らす」のではなく「層に分ける」方針です。すべての通知は記録しますが、人間の注意を引くレベルを分離します。
Layer 1: P0(即座に対応)— 通知先: 電話/SMS
1p0_criteria:2 - データ処理パイプライン全停止3 - 直近24時間の出力がゼロ4 - ディスク使用率 > 95%週に0-1件です。この通知が来たら、何をしていても対応します。
Layer 2: P1(当日中に対応)— 通知先: Slack DM
1p1_criteria:2 - 特定ジョブの連続失敗(3回以上)3 - データ鮮度が閾値を超過(自動復旧なし)4 - 外部API連携のエラー率 > 10%週に5-15件です。Slack DMで通知し、当日中に確認します。
Layer 3: P2(週次レビューで確認)— 通知先: Slackチャンネル(ミュート可)
1p2_criteria:2 - 閾値の一時的な超過(自動復旧あり)3 - パフォーマンス低下(処理時間が通常の2倍以上)4 - 非クリティカルなジョブの失敗週に30-50件です。チャンネルに流しますが即時対応は不要です。週次レビューでトレンドを確認します。
Layer 4: ログのみ(通知しない)— 保存先: ログファイル
1log_only:2 - 正常実行の記録3 - 閾値内の変動4 - 自動復旧した一時的なエラー残りすべてです。通知はしませんが、調査時に参照できるよう記録は残します。
実装:アラートルーター
1from enum import Enum2from dataclasses import dataclass3
4class Severity(Enum):5 P0 = "p0" # 即座に対応6 P1 = "p1" # 当日中に対応7 P2 = "p2" # 週次レビュー8 LOG = "log" # ログのみ9
10@dataclass11class Alert:12 job_name: str13 message: str14 severity: Severity15 timestamp: str18 collapsed lines
16 details: dict17
18def route_alert(alert: Alert):19 """重要度に応じて通知先を振り分ける"""20 # 全アラートをログに記録21 log_alert(alert)22
23 match alert.severity:24 case Severity.P0:25 send_sms(alert)26 send_slack_dm(alert)27 log_alert(alert)28 case Severity.P1:29 send_slack_dm(alert)30 case Severity.P2:31 send_slack_channel(alert, channel="#alerts-low")32 case Severity.LOG:33 pass # ログのみ、通知なし重要度の自動判定
手動で重要度を設定するのは面倒で、設定ミスの温床になります。ルールベースで自動判定します。
1def classify_severity(job_name: str, error_type: str, context: dict) -> Severity:2 """エラーの種類とコンテキストから重要度を自動判定"""3
4 # P0: 全面停止系5 if context.get("all_jobs_affected"):6 return Severity.P07 if context.get("zero_output_last_24h"):8 return Severity.P09 if context.get("disk_usage_percent", 0) > 95:10 return Severity.P011
12 # P1: 連続失敗、長時間のデータ欠損13 if context.get("consecutive_failures", 0) >= 3:14 return Severity.P115 if context.get("data_age_hours", 0) > context.get("max_age_hours", 24) * 1.5:10 collapsed lines
16 return Severity.P117
18 # P2: 一時的な問題19 if context.get("auto_recovered"):20 return Severity.P221 if error_type in ("timeout", "rate_limit", "temporary_network"):22 return Severity.P223
24 # デフォルト: P225 return Severity.P2重複排除:連鎖アラートの抑制
1つの障害で10個のジョブがアラートを出すと、10件の通知が飛びます。これを抑制します。
時間窓による集約
1from collections import defaultdict2from datetime import datetime, timedelta3
4# 5分間のウィンドウでアラートを集約5AGGREGATION_WINDOW = timedelta(minutes=5)6recent_alerts: dict[str, list[Alert]] = defaultdict(list)7
8def should_send(alert: Alert) -> bool:9 """同一カテゴリのアラートが短期間に集中していたら集約する"""10 key = f"{alert.severity.value}:{alert.job_name}"11 now = datetime.now()12
13 # 古いアラートを削除14 recent_alerts[key] = [15 a for a in recent_alerts[key]12 collapsed lines
16 if (now - datetime.fromisoformat(a.timestamp)) < AGGREGATION_WINDOW17 ]18
19 if len(recent_alerts[key]) >= 3:20 # 3件目以降は集約通知(「他N件の同様のアラート」)21 if len(recent_alerts[key]) == 3:22 send_summary(key, recent_alerts[key])23 recent_alerts[key].append(alert)24 return False25
26 recent_alerts[key].append(alert)27 return True根本原因の特定支援
1def detect_cascade(alerts: list[Alert]) -> str | None:2 """5分以内に3つ以上のジョブがアラートを出したら連鎖と判定"""3 if len(alerts) < 3:4 return None5
6 job_names = {a.job_name for a in alerts}7 if len(job_names) >= 3:8 # 依存グラフから共通の上流を特定9 common_upstream = find_common_upstream(job_names)10 if common_upstream:11 return f"根本原因の候補: {common_upstream} の障害が下流に波及"12 return None週次レビューの仕組み化
Layer 3(P2)のアラートは即時対応しませんが、週次で傾向を確認します。
1def generate_weekly_report(alerts: list[Alert]) -> str:2 """週次アラートレポートを生成"""3 total = len(alerts)4 by_severity = Counter(a.severity.value for a in alerts)5 by_job = Counter(a.job_name for a in alerts).most_common(5)6
7 report = f"""8## 週次アラートレポート9
10**総数**: {total}件11**内訳**: P0={by_severity.get('p0', 0)}, P1={by_severity.get('p1', 0)}, P2={by_severity.get('p2', 0)}12
13### アラート頻度 Top 5(要改善候補)14"""15 for job, count in by_job:3 collapsed lines
16 report += f"- {job}: {count}件\n"17
18 return reportTop 5の活用: 頻度の高いアラートは、閾値の見直し or 根本原因の修正対象です。「ノイズを減らす」のではなく「ノイズの原因を直す」という考え方です。
効果測定
Before → After
| 指標 | Before | After |
|---|---|---|
| 総アラート/週 | 247 | 247(総数は変わらない) |
| 人間に通知/週 | 247 | 18(P0: 1, P1: 12, P2: 5件のサマリー) |
| 重要アラートの見逃し | 月2-3件 | 0件 |
| アラート対応時間 | 平均4時間 | P0: 15分以内, P1: 2時間以内 |
総数は減っていません。減らしたのは「人間の注意を引く回数」です。
閾値チューニングの継続
週次レポートのTop 5を毎週確認し、不要なアラートの閾値を調整し続けます。これにより、P2の件数は徐々に減少していきます。
まとめ
| ステップ | アクション | 効果 |
|---|---|---|
| 1. 定量把握 | 1週間分を集計 | 「75%がノイズ」を数字で確認 |
| 2. 層の設計 | P0/P1/P2/Logの4層 | 人間の注意資源を適切に配分 |
| 3. 自動判定 | ルールベースの重要度分類 | 設定ミスを排除 |
| 4. 重複排除 | 時間窓での集約 | 連鎖アラートの抑制 |
| 5. 週次レビュー | Top 5の傾向分析 | ノイズの根本原因を特定 |
アラート疲労の解決策は「通知を減らす」ではありません。通知を構造化し、重要度に応じて人間の注意を制御することです。
全部記録し、重要なものだけ通知する。この原則を守れば、「通知が多すぎて見ない」と「通知が少なすぎて見逃す」の両方を避けられます。