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:
187
docs/adr/ADR-108-incident-id-fingerprint-derivation.md
Normal file
187
docs/adr/ADR-108-incident-id-fingerprint-derivation.md
Normal 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 共存 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 開始。
|
||||
Reference in New Issue
Block a user