# ADR-064: Alert Rule Engine — YAML 驅動規則匹配 + AI 自動學習 **狀態**: 已批准 **日期**: 2026-04-09 **作者**: ogt + Claude Sonnet 4.6 **審查**: 首席架構師(2026-04-09) --- ## 背景 `openclaw.py` 中的 `_generate_mock_response` 用硬編碼 if/elif 實作規則匹配,每次新增告警類型都要改 Python 代碼並重新部署。隨著監控目標增加,此模式不可持續。 ## 決策 ### D1: 規則外化為 YAML(`apps/api/alert_rules.yaml`) 規則引擎從 YAML 檔載入,Pod 重啟即生效,不需要改代碼。 **規則結構**: ```yaml rules: - id: docker_container_unhealthy priority: 10 match: alertname: [DockerContainerUnhealthy] message: [unhealthy] response: kubectl_command: "ssh {host} 'docker inspect {container}...'" suggested_action: RESTART_DEPLOYMENT risk: medium responsibility: INFRA ``` **Priority 體系**: | 範圍 | 用途 | |------|------| | 1–499 | 手寫規則(高優先,不被 AI 覆蓋) | | 500–890 | AI 自動生成規則 | | 999 | `generic_fallback` 通用兜底 | ### D2: AI 自動規則學習機制 當告警命中 `generic_fallback` 時,觸發 `auto_generate_rule()`: 1. 呼叫 Ollama (deepseek-r1:14b) 生成 YAML 規則片段 2. Ollama 失敗 → Gemini 2.0 Flash 備援 3. 驗證格式(id/match/response/kubectl_command 必填) 4. `textwrap.dedent()` 正規化縮排(防 LLM 輸出前置空格) 5. append 到 `alert_rules.yaml` 6. `lru_cache.cache_clear()` — 同 Pod 立即生效 ### D3: 模組邊界 - `alert_rule_engine.py` = Service 層,只讀 YAML,不直接存取 Redis/DB - `auto_generate_rule()` 採用 DI 參數注入(`ollama_url`, `model`, `gemini_api_key`),不 import settings 全域單例 - asyncio 觸發在上層 async `_call_with_fallback()` 執行,不在 sync `_generate_mock_response()` 中操作 event loop ### D4: 匹配邏輯 優先順序:alertname 完全匹配 > alert_type 部分匹配 > message 關鍵字 `generic_fallback`(`alertname: ["*"]`)在 `_matches()` 中永遠回傳 False,由 `match_rule()` 的第二輪迴圈單獨選取,防止其 alert_type/message 關鍵字意外命中。 ## 已知限制 ### L1: 多 Pod 環境下規則可能重複生成 `_generating` set 是進程記憶體級去重,多 Pod 各自維護。同一告警可能在不同 Pod 同時觸發生成,產生重複規則 append。 **緩解**: `_rule_id_exists()` 提供二次去重,但有 lru_cache 的時間窗口 race condition。 **計劃**: 若未來 Pod 數 > 2,需 Redis 分散式鎖。目前 prod 為 2 Pod,可接受。 ### L2: `lru_cache` 跨 Pod 不同步 新規則寫入後,只有寫入的 Pod 清除了 cache,其他 Pod 需重啟才能載入新規則。這是已知行為,下次告警觸發時仍會走 `generic_fallback`,但不會再次生成(`_rule_id_exists` 讀 YAML 直接確認)。 ## 測試策略 `auto_generate_rule()` 採 DI,可在不啟動 FastAPI 的情況下單獨測試: ```python await auto_generate_rule( alert_context={"labels": {"alertname": "TestAlert"}}, ollama_url="http://mock", model="test-model", ) ``` ## I1 整合記錄 (2026-04-11, commit d77b2ad) `get_incident_type(alertname)` 已整合,實作三層降級: ``` YAML rule.incident_type(Layer 1) → ALERTNAME_TO_TYPE 靜態 dict(Layer 2, src/constants/alert_types.py) → "custom"(Layer 3 兜底) ``` **關鍵決策**: - YAML `rule.id` 不等於 `incident_type`(命名空間不同,禁止混用) - YAML 無 `incident_type` 欄位時一律 fall through,不用 rule.id 代替 - `webhooks.py` 已改用 `get_incident_type()` 取代靜態 dict **M3 常量遷移**:`ALERTNAME_TO_TYPE`(56 筆)從 webhooks.py 內聯遷移至 `src/constants/alert_types.py` ## 相關檔案 - `apps/api/alert_rules.yaml` — 規則定義 - `apps/api/src/services/alert_rule_engine.py` — 規則引擎 + `get_incident_type()` - `apps/api/src/constants/alert_types.py` — ALERTNAME_TO_TYPE 靜態 dict(Layer 2 fallback) - `apps/api/src/api/v1/webhooks.py` — 呼叫 `get_incident_type()` - `apps/api/tests/test_get_incident_type.py` — 11 個整合測試 - `apps/api/src/services/openclaw.py` — `_generate_mock_response` + `_call_with_fallback` 整合點 - `apps/api/Dockerfile` — COPY alert_rules.yaml ## 參考 - ADR-006: AI 模型路由配置 - ADR-052: Phase 24 AIRouter - `feedback_lewooogo_modular_enforcement.md`: 積木化 5 問