P1 (徹底長期修系列) — 治本所有 dedup 問題:把 incident_id 從 uuid4()[:6] 隨機改為 fingerprint hash 派生,open 期間同 fingerprint 強制復用同一 INC。 當前是 Proposed 狀態,等首席架構師審。Tier 3 紅區改動,不批不動 code。 包含: - 影響面盤點(1435 引用點,預計實際需改 ~10 檔 ~20 處) - 4 phase 漸進式遷移(~7 小時) - 跨日 reuse 行為決策 - 5 條風險與緩解 - 5 條驗收標準 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7.8 KiB
7.8 KiB
ADR-108: Incident ID 改為 fingerprint 派生(P1 徹底長期修)
| Status | Proposed (等首席架構師審) |
| Date | 2026-05-03 |
| Author | Claude Opus 4.7 + 統帥 ogt |
| Tier | 🔴 Tier 3 紅區(incident model + 1435 個引用點) |
| Related | ADR-073 (alert_analyzer fingerprint), ADR-068 (飛輪五根因), feedback_telegram_dedup.md |
1. 問題
鐵證
2026-05-02 統帥 Telegram 收到同症狀重複告警 24h 內 15+ 次(INC-20260501-6FE3BD HostBackupFailed)。 4 個 agent 真實數據查證,根因之一:
# apps/api/src/models/incident.py:362-364
incident_id: str = Field(
default_factory=lambda: f"INC-{datetime.now(timezone.utc).strftime('%Y%m%d')}-{str(uuid4())[:6].upper()}",
description="事件唯一識別碼",
)
incident_id 用 uuid4()[:6] 隨機派生,導致:
- 同 alertname + 同 target 過 180s debounce 就生全新 INC ID
- 所有以
incident_id為 key 的 dedup 邏輯(telegram_sent:{incident_id}/auto_repair:emergency_escalated:{incident_id}/decision:DEC-{HEX}等)永遠 miss - UI 顯示同問題不同 INC ID,難以追蹤
- KM 學習無法把同問題的多次發生視為同一個案例
已做的繞道修法(治標)
b3a0f0d7:decision card dedup key 改telegram_sent:fp:{alertname}:{target}繞過 incident_id47342dfb:escalation card dedup 同樣改
但這是繞道 — 系統內仍有 1435 個 incident_id 引用點,未來任何新增 dedup / 對應 / 追蹤邏輯只要使用 incident_id,就會再次踩同樣的雷。
2. 替代方案
A. 維持 uuid4,所有 dedup 都改用 fingerprint(已做的繞道方向)
- ✅ 改動小(單檔)
- ❌ 治標不治本,dedup 散在各處,未來必再漏
- ❌ UI 同問題仍多 INC ID
B. 改 incident_id 用 fingerprint hash 派生(推薦)
- ✅ 治本:incident_id 本身就帶語意(同症狀同 ID)
- ✅ 所有 incident_id-based 邏輯自動正確
- ✅ UI 同問題同 INC ID,可追蹤
- ❌ Tier 3 紅區,影響面大(1435 引用點,但 95% 是 string pass-through 不需改)
- ❌ 跨日同 fingerprint 怎麼算(用 date prefix 還是省略?)
C. 引入 incident_uuid 與 incident_fingerprint 雙欄位
- ✅ 向後相容
- ❌ 雙欄位語意混淆,使用者不知該用哪個
- ❌ 仍是治標
選 B。
3. 推薦方案:B 詳細設計
3.1 新 incident_id 格式
# apps/api/src/models/incident.py:362
def _derive_incident_id(fingerprint: str | None = None) -> str:
"""
fingerprint 給的話 → INC-{YYYYMMDD}-{fingerprint[:6].upper()}
fingerprint None → INC-{YYYYMMDD}-{uuid4()[:6].upper()}(保留向後相容)
"""
date = datetime.now(timezone.utc).strftime('%Y%m%d')
if fingerprint:
suffix = fingerprint[:6].upper()
else:
suffix = str(uuid4())[:6].upper()
return f"INC-{date}-{suffix}"
3.2 Open 期間強制 reuse
# apps/api/src/services/incident_service.py
async def get_or_create_incident_by_fingerprint(
self, fingerprint: str, ...
) -> Incident:
"""同 fingerprint 在 OPEN 狀態(INVESTIGATING/PENDING)→ 復用,否則新建"""
existing = await self._repo.find_open_by_fingerprint(fingerprint)
if existing:
return existing # reuse 同一 INC ID,新 signal 加進去
return self._build_new_incident(fingerprint=fingerprint, ...)
3.3 跨日決策
| 情境 | 行為 |
|---|---|
| 同 fingerprint,前次已 RESOLVED | 新建 INC(新 ID 含新日期) |
| 同 fingerprint,前次仍 INVESTIGATING/PENDING | 復用同 INC ID(不換日期) |
| 同 fingerprint,前次跨日 INVESTIGATING > 24h | 復用(不該換 ID 造成 dedup 混亂) |
rationale:fingerprint 代表「同症狀」,open 期間應該是同一事故。日期 prefix 只在新建時用,復用時保留原日期。
3.4 與既有 1435 引用點的相容性
絕大多數引用是 incident_id 當 string 傳遞(log / DB FK / Redis key 構造),不需改。
需要審視的引用模式:
- 直接 hardcode 比對:
if incident_id == "INC-20260501-6FE3BD":— grep 確認無 - uuid 格式假設:解析
incident_id拆 date / suffix — 需 audit - 長度假設:固定切片 / hash — 需 audit
預期實際需改動:< 5 處。
4. 影響面(真實數據盤點)
| 檔案 | 引用次數 | 預計需改 |
|---|---|---|
services/telegram_gateway.py |
139 | 0(純 string pass) |
services/decision_manager.py |
133 | 0(已用 fingerprint dedup) |
api/v1/webhooks.py |
64 | 2-3 (建立 incident 邏輯) |
services/incident_service.py |
60 | 5-10 (新增 get_or_create_by_fingerprint) |
services/learning_service.py |
58 | 0 |
services/openclaw.py |
52 | 0 |
api/v1/incidents.py |
49 | 0 |
services/approval_execution.py |
46 | 0 |
models/incident.py |
(model 本身) | 1 (default_factory) |
總改動點:~10 個檔案、~20 處實作(非全 1435 引用都要改)
5. 遷移步驟(漸進式,不停機)
Phase 1(單 commit, 低風險)
models/incident.py:362改_derive_incident_id接受 fingerprint kwarg, default uuid 保持向後相容- 加 unit test 驗證 fingerprint 給的話派生規則正確
- 不改任何 caller — 此 phase 純基礎建設
Phase 2(單 commit, 中風險)
incident_service.py新增get_or_create_by_fingerprintwebhooks.py:alertmanager_webhook改用新方法(取代當前隨機建 INC)- 加 integration test:同 fingerprint 24h 內 webhook 進來只生 1 個 INC
Phase 3(單 commit, 中風險)
incident_service.py:create_incident_from_signal也走 fingerprint reuse path- 加 regression test 含「跨日仍 INVESTIGATING」場景
Phase 4(清理 commit, 低風險)
6. 風險與緩解
| 風險 | 影響 | 緩解 |
|---|---|---|
| Phase 2 開始後新 incident 與舊 incident 共存 PG,UI 顯示混亂 | 中 | 分頁 by created_at, 不依賴 ID 排序 |
| fingerprint hash 撞號 | 極低 | SHA256[:6] = 16M 可能值, 同日撞號機率 < 1ppm |
| Open 期間 reuse 把不同 severity 的事件混淆 | 中 | reuse 時 severity = max(old, new),warning 升級為 critical 是合理 |
| 既有 fingerprint 為空的歷史 incident 無法套用 | 低 | _derive_incident_id 退回 uuid path |
| Codex 同時改 incident_service.py 衝突 | 中 | Phase 2 動手前先 git stash list 檢查 |
7. 驗收條件
- 同 fingerprint 24h 內進來 N 個 alertmanager webhook → 只 1 個 incident_id(PG 查詢驗證)
- incident_id 結尾後 6 字元 = fingerprint 前 6 字元(可直接從 INC ID 反推 fingerprint)
- 既有 4 個 agent 7d 漏斗統計復跑:DISTINCT incident_id 應顯著少於 DISTINCT fingerprint × 7(過去 24h 76 INC / 2 distinct fp → 改後應接近 2)
- 無 UI / API regression(incident detail page 仍可顯示)
- dedup 繞道(b3a0f0d7 / 47342dfb)+ 新 ID 派生雙重保險,效果不降低
8. 工程量估算
| Phase | 預計時間 | 工程性質 |
|---|---|---|
| Phase 1 | 1 小時 | 純機械改動 + 1 test |
| Phase 2 | 3 小時 | 含 webhook 邏輯重構 + integration test |
| Phase 3 | 2 小時 | incident_service 內部 |
| Phase 4 | 1 小時 | 文件 + 清理 |
| 總計 | ~7 小時 | 分 4 commits 漸進式 |
9. 等首席架構師批准
統帥 ogt — 此 ADR 為 Tier 3 紅區改動前置文件,不批准不動 code。
需確認:
- 路線正確(B 而非 A/C)
- 跨日 reuse 行為(3.3)合理
- 接受 ~7 小時 / 4 commits 漸進式遷移
- Codex 並行改
incident_service.py的協調機制(先 stash list 檢查)
批准後我從 Phase 1 開始。