# 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 問
