docs(adr): ADR-108 incident_id fingerprint derivation (P1 design doc)

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>
This commit is contained in:
Your Name
2026-05-03 01:53:09 +08:00
parent 8fb0c5df33
commit 62698158b0

View File

@@ -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 共存 PGUI 顯示混亂 | 中 | 分頁 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 開始。