Files
awoooi/docs/adr/ADR-108-incident-id-fingerprint-derivation.md
Your Name 62698158b0 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>
2026-05-03 01:53:09 +08:00

7.8 KiB
Raw Blame History

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_iduuid4()[: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 學習無法把同問題的多次發生視為同一個案例

已做的繞道修法(治標)

  • b3a0f0d7decision card dedup key 改 telegram_sent:fp:{alertname}:{target} 繞過 incident_id
  • 47342dfbescalation 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_uuidincident_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 混亂)

rationalefingerprint 代表「同症狀」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_idPG 查詢驗證)
  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 regressionincident 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 開始。