diff --git a/docs/adr/ADR-108-incident-id-fingerprint-derivation.md b/docs/adr/ADR-108-incident-id-fingerprint-derivation.md new file mode 100644 index 00000000..09b2005d --- /dev/null +++ b/docs/adr/ADR-108-incident-id-fingerprint-derivation.md @@ -0,0 +1,187 @@ +# 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 真實數據查證,**根因之一**: + +```python +# 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]` **隨機**派生,導致: + +1. 同 alertname + 同 target 過 180s debounce 就生**全新 INC ID** +2. 所有以 `incident_id` 為 key 的 dedup 邏輯(`telegram_sent:{incident_id}` / `auto_repair:emergency_escalated:{incident_id}` / `decision:DEC-{HEX}` 等)**永遠 miss** +3. UI 顯示同問題不同 INC ID,難以追蹤 +4. KM 學習無法把同問題的多次發生視為同一個案例 + +### 已做的繞道修法(治標) +- `b3a0f0d7`:decision card dedup key 改 `telegram_sent:fp:{alertname}:{target}` 繞過 incident_id +- `47342dfb`: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 格式 + +```python +# 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 + +```python +# 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 構造),**不需改**。 + +需要審視的引用模式: +1. **直接 hardcode 比對**:`if incident_id == "INC-20260501-6FE3BD":` — grep 確認無 +2. **uuid 格式假設**:解析 `incident_id` 拆 date / suffix — 需 audit +3. **長度假設**:固定切片 / 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, 低風險) +1. `models/incident.py:362` 改 `_derive_incident_id` 接受 fingerprint kwarg, default uuid 保持向後相容 +2. 加 unit test 驗證 fingerprint 給的話派生規則正確 +3. 不改任何 caller — 此 phase 純基礎建設 + +### Phase 2(單 commit, 中風險) +1. `incident_service.py` 新增 `get_or_create_by_fingerprint` +2. `webhooks.py:alertmanager_webhook` 改用新方法(取代當前隨機建 INC) +3. 加 integration test:同 fingerprint 24h 內 webhook 進來只生 1 個 INC + +### Phase 3(單 commit, 中風險) +1. `incident_service.py:create_incident_from_signal` 也走 fingerprint reuse path +2. 加 regression test 含「跨日仍 INVESTIGATING」場景 + +### Phase 4(清理 commit, 低風險) +1. 既有 b3a0f0d7 / 47342dfb 的繞道 dedup 可保留(防呆雙重保險) +2. 文件更新 + LOGBOOK 收尾 + +## 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. 驗收條件 + +1. **同 fingerprint 24h 內進來 N 個 alertmanager webhook → 只 1 個 incident_id**(PG 查詢驗證) +2. **incident_id 結尾後 6 字元 = fingerprint 前 6 字元**(可直接從 INC ID 反推 fingerprint) +3. **既有 4 個 agent 7d 漏斗統計復跑**:DISTINCT incident_id 應顯著少於 DISTINCT fingerprint × 7(過去 24h 76 INC / 2 distinct fp → 改後應接近 2) +4. **無 UI / API regression**(incident detail page 仍可顯示) +5. **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 開始。