diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index e5baf883..a1aa590c 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -6,7 +6,7 @@ --- -## 📍 當前狀態 (2026-04-08 全面監控+操作記錄完成) +## 📍 當前狀態 (2026-04-08 Sprint 5.1 規劃完成) | 項目 | 狀態 | 說明 | |------|------|------| @@ -16,16 +16,31 @@ | alert_operation_log 溯源 | ✅ | Phase 11, 654 筆歷史回填 (f20121a) | | ADR-060 全面監控規劃 | ✅ | 已批准 | | ADR-061 Event Sourcing | ✅ | 已實施 | -| Plan A docker-health-monitor.sh | ⏳ 待實作 | 腳本設計完成,待部署 | -| Plan B Exporters (PG/Redis/Nginx) | ⏳ 待部署 | docker-compose.exporters.yaml 已有框架 | -| Plan C Blackbox 外部網站 | ⏳ | 4 個外部網站待加入 | +| ADR-062 Data Safety Guardrails | ✅ | 規劃批准,待實作 | +| ADR-063 Service Registry IaC | ✅ | 規劃批准,待實作 | +| Sprint 5.1 方案文件 | ✅ | 規範驗證通過,待統帥下令 | +| docker-health-monitor.sh | ⏳ Sprint 5.1 L4 | 需改為純感知層(移除 docker restart) | +| Plan B Exporters (PG/Redis/Nginx) | ⏳ Sprint 5.2 | docker-compose.exporters.yaml 已有框架 | +| Plan C Blackbox 外部網站 | ⏳ Sprint 5.2 | 4 個外部網站待加入 | -**下一步**: 實作 Plan A docker-health-monitor.sh → 部署 Plan B Exporters +**當前焦點**: Sprint 5.1 資料安全護欄(待統帥下令執行 Layer 0~7) +**下一步**: 統帥確認 C1-C5 前置條件 → 開始 Layer 0 --- ## 📊 里程碑總覽 (壓縮版) +### 2026-04-08 — Sprint 5.1 資料安全護欄規劃完成 + +- 11 項首席架構師決策(Q1-Q11)完成 +- 服務分級(BLOCK/CRITICAL_HITL/STANDARD_HITL/AUTO)確立 +- Pre-flight 備份檢查機制設計完成 +- MultiSig 雙簽機制設計完成 +- ADR-062 Data Safety Guardrails 批准 +- ADR-063 Service Registry IaC 批准 +- 完整實施方案 + 規範驗證通過(P1-P5 問題修正) +- 關鍵發現:Playbook 存於 Redis(非 PostgreSQL),修正 M-001 方向 + ### 2026-04-08 — 全面監控+操作溯源架構 - 自動修復移除所有 gate:直接執行(統帥指令) diff --git a/docs/adr/ADR-062-data-safety-guardrails.md b/docs/adr/ADR-062-data-safety-guardrails.md new file mode 100644 index 00000000..b30d8c83 --- /dev/null +++ b/docs/adr/ADR-062-data-safety-guardrails.md @@ -0,0 +1,98 @@ +# ADR-062 — 資料安全護欄 (Data Safety Guardrails) + +> **狀態**: 已批准 (首席架構師裁示 2026-04-08) +> **撰寫**: Claude Sonnet 4.6 / 2026-04-08 Asia/Taipei +> **相關**: ADR-061 (Alert Operation Log), ADR-058 (SSH Host Repair), ADR-039 (CI/CD) + +--- + +## 背景 + +Sprint 5.1 啟動前,AWOOOI 的自動修復機制已「移除所有 gate」(2026-04-08 統帥指令),所有 APPROVED Playbook 均可直接執行。此設計在無狀態服務上運作良好,但對**有狀態服務(Stateful Services)**存在資料遺失風險: + +- PostgreSQL 重啟 → WAL 截斷、事務回滾 +- Redis 重啟 → in-memory 資料遺失、冪等鎖消失 +- ClickHouse 重啟 → 列欄檔案損壞 + +同時,系統缺乏「AI 動作可追查」機制:OpenClaw 的 LLM 推理過程與 alert_operation_log 斷層。 + +--- + +## 決策 + +### D1 — 服務分級(Q1) + +建立四級 Stateful 分類,儲存於 `ops/config/service-registry.yaml`(IaC,版控): + +| 等級 | 定義 | 服務 | +|------|------|------| +| `BLOCK` | 系統禁止,僅告警 | PostgreSQL (所有實例), ClickHouse | +| `CRITICAL_HITL` | MultiSig 2票才執行 | Redis, Gitea, Harbor, MinIO | +| `STANDARD_HITL` | 1票審核,特殊 restart 命令 | Prometheus, Grafana, Alertmanager | +| `AUTO` | 自動執行 | Nginx, 所有 K3s Pod, Ollama 等 | + +### D2 — Pre-flight 備份檢查(Q2) + +針對 `CRITICAL_HITL` 且 `requires_pre_backup=True` 的服務,執行前檢查: +- Velero 最近 Completed 備份 < 4 小時 → 放行 +- 過期 → **Abort + 觸發緊急備份(非同步)+ 通知人工**(非直接執行) +- CPU/IO 高負載類告警 → 禁止觸發備份(Q4,防止雪上加霜) + +### D3 — MultiSig 雙簽(Q3) + +`CRITICAL_HITL` 服務需要 2 票 Telegram 授權才執行,延續 Sprint 3 的 MultiSig 基礎設施。Sprint 5.1 先以同一 user 兩次點擊驗證狀態機,後續 Sprint 擴充多帳號。 + +### D4 — Prometheus auto_repair flag 優先級(Q9) + +- `auto_repair: "false"` → 強制 HITL,AI 無法覆寫 +- `auto_repair: "true"` → 進入 AI + Guardrail 評估,Guardrail 有最終否決權 + +### D5 — Langfuse trace_id 寫入 alert_operation_log(Q10) + +OpenClaw 分析完成後,`langfuse_trace_id` 必須寫入 `alert_operation_log.context`,實現 AI 動作全程可追查。 + +### D6 — AlertManager 不抑制(Q11) + +Guardrail 阻擋時,不呼叫 AlertManager silence API。靠 AWOOOI Redis TTL 去重(10 分鐘),AlertManager 持續 firing 保持監控可見性。 + +--- + +## 新增 event_type(共 8 個) + +``` +GUARDRAIL_BLOCKED # Stateful 服務被阻擋 +PRE_FLIGHT_PASSED # Pre-flight 備份檢查通過 +PRE_FLIGHT_FAILED # Pre-flight 失敗(備份過期) +BACKUP_TRIGGERED # 緊急備份已觸發 +BACKUP_COMPLETED # 緊急備份完成 +BACKUP_FAILED # 緊急備份失敗 +APPROVAL_ESCALATED # 升級為 CRITICAL_APPROVAL(需 2 票) +CHANGE_APPLIED # 手動變更已記錄 +``` + +--- + +## 新增模組 + +| 模組 | 路徑 | 職責 | +|------|------|------| +| ServiceRegistryClient | `services/service_registry.py` | 讀取 YAML,查詢服務分級 | +| VeleroClient | `services/velero_client.py` | kubectl 查詢備份狀態,觸發緊急備份 | +| PreflightService | `services/preflight_service.py` | Pre-flight 備份檢查邏輯 | + +所有模組遵循 leWOOOgo 積木化規範:Service 層,有 `get_xxx()` + `set_xxx()` singleton 模式。 + +--- + +## 後果 + +- 所有有狀態服務的自動修復操作,由「預設允許」改為「預設拒絕,明確授權才執行」 +- AI 每一個修復決策都有 Langfuse trace_id 可追查 +- Prometheus alert rule 的 `auto_repair` flag 成為有效的安全底線 +- 系統複雜度增加,但「資料安全」優先於「修復速度」 + +--- + +## 實施計畫 + +見 `docs/superpowers/plans/2026-04-08-sprint5-data-safety-guardrails.md`(Layer 0~7 完整步驟) diff --git a/docs/adr/ADR-063-service-registry-iac.md b/docs/adr/ADR-063-service-registry-iac.md new file mode 100644 index 00000000..d82ad160 --- /dev/null +++ b/docs/adr/ADR-063-service-registry-iac.md @@ -0,0 +1,92 @@ +# ADR-063 — Service Registry IaC 設計規範 + +> **狀態**: 已批准 (首席架構師裁示 2026-04-08) +> **撰寫**: Claude Sonnet 4.6 / 2026-04-08 Asia/Taipei +> **相關**: ADR-062 (Data Safety Guardrails) + +--- + +## 背景 + +ADR-062 決定使用靜態 YAML 儲存服務的 Stateful 分級(Q6 決策:IaC)。需要規範此 YAML 的設計原則、修改流程、以及與程式碼的整合方式。 + +--- + +## 設計原則 + +**Infrastructure as Code**:服務的 Stateful 屬性是架構本質,不允許在 Web UI 上隨意修改。所有變更必須透過 Git PR + 統帥審核。 + +--- + +## 檔案位置 + +``` +ops/config/service-registry.yaml +``` + +此路徑在 `service_registry.py` 中以相對於 repo root 的方式計算: +```python +Path(__file__).parents[5] / "ops" / "config" / "service-registry.yaml" +``` + +--- + +## YAML 結構規範 + +```yaml +services: + - name: # 唯一識別,對應 container name / k8s service name + display_name: <顯示名稱> + host: + stateful_level: BLOCK | CRITICAL_HITL | STANDARD_HITL | AUTO + reason: <說明為何此分級> # 必填,記錄決策依據 + alert_only: true # 僅 BLOCK 使用 + requires_pre_backup: false # CRITICAL_HITL 使用 + restart_command: "docker start" # 特殊重啟命令(如 Prometheus/Grafana) + containers: ["container-name"] # docker container 名稱(可多個) + +backup_policies: + velero_max_age_hours: 4 + emergency_backup_timeout: 600 + block_backup_on_high_io: true + io_threshold_percent: 80 + +multisig: + critical_required_votes: 2 + standard_required_votes: 1 + vote_expiry_minutes: 30 +``` + +--- + +## 分級決策標準 + +| 分級 | 標準 | +|------|------| +| `BLOCK` | 有 PostgreSQL WAL / ClickHouse 列欄資料 / 重啟必然造成資料遺失或損壞 | +| `CRITICAL_HITL` | 有 Volume 掛載 / 重啟可能中斷活躍寫入 / AWOOOI 核心依賴 | +| `STANDARD_HITL` | 有 WAL 或狀態但可安全 `docker start`(非 restart)/ 監控基礎設施 | +| `AUTO` | 無狀態 / K3s Pod(有 PDB 保護)/ 重啟只影響短暫可用性 | + +--- + +## 維護流程 + +1. 新增服務 → PR to `ops/config/service-registry.yaml` +2. PR 描述必須說明:服務名稱、分級、理由 +3. 統帥審核後 merge +4. ServiceRegistryClient 下次載入時自動生效(singleton 有 lazy load) + +--- + +## 失敗行為(Fail-Safe) + +ServiceRegistryClient 讀取失敗時 **fallback AUTO**(不阻擋告警流程),並記錄 ERROR log。此設計確保 Service Registry 本身的故障不會導致整個告警鏈路中斷。 + +--- + +## 未來擴充 + +- 服務依賴關係(dependency edges)→ Sprint 5 拓撲圖使用 +- 備份策略客製化(每個服務獨立設定) +- 動態覆寫(緊急情況時可 kubectl configmap 注入) diff --git a/docs/superpowers/plans/2026-04-08-sprint5-data-safety-guardrails.md b/docs/superpowers/plans/2026-04-08-sprint5-data-safety-guardrails.md new file mode 100644 index 00000000..cd528ac4 --- /dev/null +++ b/docs/superpowers/plans/2026-04-08-sprint5-data-safety-guardrails.md @@ -0,0 +1,1320 @@ +# Sprint 5.1 — 資料安全護欄與全鏈路整合 +## 完整解決方案與細化實施步驟 + +> **建立**: 2026-04-08 (台北時間) +> **建立者**: Claude Sonnet 4.6 +> **狀態**: 📋 規範驗證通過,待統帥下令執行 +> **前置討論**: 11 項首席架構師決策 (Q1-Q11) +> **依賴**: Phase 10/11 完成 (auto_repair_executions + alert_operation_log) +> **驗證**: 2026-04-08 規範合規驗證完成,P1-P5 問題已修正(見第零章) + +--- + +## 第零章:規範驗證修正記錄(2026-04-08) + +> 合規驗證後發現 5 個問題,已在本章修正,不影響原方案邏輯。 + +### P1 🔴 Playbook 無 PostgreSQL ORM — Migration M-001 方向修正 + +**問題**:原方案假設 `playbooks` 有 SQL table,但實際上 Playbook 存在 **Redis**(Working Memory,7天 TTL),沒有 PostgreSQL table。 + +**修正**:Migration M-001 不執行 `ALTER TABLE playbooks`,改為: +- `requires_approval_level`、`stateful_targets`、`requires_pre_backup` 三個欄位加入 **Pydantic model**(`apps/api/src/models/playbook.py`) +- `playbook_repository.py` 的 `to_redis_dict()` / `from_redis_dict()` 同步更新 +- **無需 SQL migration** + +### P2 🟡 velero_client.py 命名修正 + +**問題**:目錄慣例混用 `*_client.py` 和 `*_service.py`(signoz_client.py 也用 client)。 + +**修正**:維持 `velero_client.py` 命名(與 signoz_client.py、langfuse_client.py 一致,client 表示「外部系統連線器」),不改名。 + +### P3 🟡 docker-health-monitor.sh 狀態釐清 + +**問題**:LOGBOOK 說「待部署」,但方案要改為純感知層(移除 docker restart)。 + +**修正**:Sprint 5.1 執行時覆蓋腳本,LOGBOOK 同步更新。腳本改造列為 Layer 4 工作項。 + +### P4 🟡 三個新 service 補充 DI setter + +**問題**:現有規範要求 `set_xxx_service()` 支援測試注入(見 `auto_repair_service.py` 模式)。 + +**修正**:三個新 service 均需補充 `set_xxx()` setter,已在 Layer 3 程式碼中標注。 + +### P5 🟡 補充 Deployment Verification 步驟 + +**問題**:方案缺少 CD smoke test 清單。 + +**修正**:已在 Layer 7 驗收標準中補充 pod 版本驗證步驟。 + +--- + +## 第一章:決策摘要 (Q1-Q11) + +| 決策 | 問題 | 裁示 | +|------|------|------| +| Q1 | Stateful 服務分級 | BLOCK(PG/ClickHouse) / CRITICAL_HITL(Gitea/Harbor/Redis/MinIO) / AUTO(Nginx等) | +| Q2 | Pre-flight 備份過期行為 | 選項C:Abort + 觸發緊急備份 + 通知人工 | +| Q3 | Critical_Approval 二次確認 | 選項C:MultiSig 雙簽(延續 Sprint 3 基礎) | +| Q4 | AI 主動備份權 | 限制:非 CPU/IO 告警才可觸發;CPU/IO 告警時禁止 | +| Q5 | Guardrail 與 Plan A/B/C 順序 | Sprint 5.1 (Guardrail) 先,Plan A/B/C 列為 Sprint 5.2 | +| Q6 | Service Registry 儲存 | 靜態 YAML (ops/config/service-registry.yaml),IaC 版控 | +| Q7 | Velero 整合方式 | kubectl get backup -n velero -o json,API Pod 需 RBAC | +| Q8 | MultiSig whitelist | Sprint 5.1 先做架構,同一 user 點兩次模擬雙簽 | +| Q9 | auto_repair flag 優先級 | Rule=false → 強制人工;Rule=true → AI+Guardrail 最終否決 | +| Q10 | Langfuse trace_id 寫入 | 必須寫入 alert_operation_log context 欄位 | +| Q11 | AlertManager 抑制 | 不抑制 AlertManager;靠 AWOOOI Redis TTL 去重 | + +--- + +## 第二章:整體架構與資料流 + +### 2.1 防護層次 (Defense in Depth) + +``` +告警來源 + Prometheus (auto_repair flag) ──┐ + Sentry (error webhook) ├──→ AWOOOI API Webhook 入口 + SignOz (anomaly) │ ↓ + docker-health-monitor (感知層) ──┘ ALERT_RECEIVED 寫入 alert_operation_log + ↓ + ┌─── [Layer 1: Service Registry] + │ ops/config/service-registry.yaml + │ BLOCK / CRITICAL_HITL / AUTO + │ ↓ + ├─── [Layer 2: Prometheus Flag] + │ auto_repair: "false" → 強制 HITL + │ auto_repair: "true" → 進入 Layer 3 + │ ↓ + ├─── [Layer 3: AI + Guardrail] + │ OpenClaw RCA 分析 + │ Langfuse trace_id 注入 + │ Guardrail 最終否決(Stateful 檢查) + │ ↓ + ├─── [Layer 4: Pre-flight Check] + │ Velero 備份時間檢查 + │ CPU/IO 負載檢查(Q4 限制) + │ Redis 冪等鎖(防並行備份) + │ ↓ + ├─── [Layer 5: Approval Level] + │ AUTO → 直接執行 + │ STANDARD → 1票(現有 HITL) + │ CRITICAL → 2票(MultiSig) + │ ↓ + └─── [Layer 6: 執行 + 記錄] + execute_auto_repair() + alert_operation_log 全程追蹤 + Telegram 所有節點通知 + SSE 前端即時推送 + Grafana deeplink + Langfuse LLM trace +``` + +### 2.2 新增 event_type 清單 + +在現有 10 個 event_type 基礎上新增: + +```python +# 新增 8 個 +"GUARDRAIL_BLOCKED" # Stateful 服務被 BLOCK,含原因 +"PRE_FLIGHT_PASSED" # Pre-flight 通過(備份時間 OK) +"PRE_FLIGHT_FAILED" # Pre-flight 失敗(備份過期) +"BACKUP_TRIGGERED" # 緊急備份已觸發(非同步進行中) +"BACKUP_COMPLETED" # 緊急備份完成 +"BACKUP_FAILED" # 緊急備份失敗 +"APPROVAL_ESCALATED" # 升級為 CRITICAL_APPROVAL(需 2 票) +"CHANGE_APPLIED" # 手動變更已記錄(record-change 概念) +``` + +--- + +## 第三章:需要產出的技術文件 + +### 3.1 ADR(架構決策記錄) + +| # | 文件 | 內容 | +|---|------|------| +| ADR-062 | `ADR-062-data-safety-guardrails.md` | 服務分級防線、Pre-flight、MultiSig 完整決策記錄 | +| ADR-063 | `ADR-063-service-registry-iac.md` | Service Registry YAML 設計與維護規範 | + +### 3.2 設定文件(IaC) + +| # | 文件 | 內容 | +|---|------|------| +| SR-001 | `ops/config/service-registry.yaml` | 全服務 Stateful 分級清單(主要產出物) | + +### 3.3 Migration 腳本 + +| # | 文件 | 內容 | +|---|------|------| +| M-001 | `apps/api/migrations/sprint51_playbook_safety.sql` | playbooks 表新增 3 欄位 | +| M-002 | `apps/api/migrations/sprint51_approval_multisig.sql` | approval_records 新增 approval_level/votes | +| M-003 | `apps/api/migrations/sprint51_alert_log_events.sql` | alert_operation_log ENUM 新增 8 事件 | + +### 3.4 K8s 設定 + +| # | 文件 | 內容 | +|---|------|------| +| K-001 | `k8s/rbac/api-velero-reader.yaml` | API Pod 讀取 Velero backup 的 ClusterRole + Binding | + +### 3.5 Runbook + +| # | 文件 | 內容 | +|---|------|------| +| RB-001 | `docs/runbooks/SPRINT51-ROLLBACK.md` | Sprint 5.1 Rollback 步驟(ENUM 不可回滾的處理方式) | + +--- + +## 第四章:細化實施步驟 + +### Layer 0 — 前置條件確認 + +#### L0-1: 確認 K8s RBAC 能力 + +**目的**:確認 API Pod 是否已有 kubectl exec 能力,或需要新增 Velero backup reader + +```bash +# 在 API Pod 內執行測試 +kubectl exec -n awoooi-prod deployment/awoooi-api -- \ + kubectl get backup -n velero --no-headers 2>&1 | head -5 +``` + +**預期結果**: +- 成功列出 backup → 直接進 L0-2 +- `Error from server (Forbidden)` → 需執行 K-001 RBAC + +**若需要新增 RBAC**(K-001): +```yaml +# k8s/rbac/api-velero-reader.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: awoooi-api-velero-reader +rules: + - apiGroups: ["velero.io"] + resources: ["backups"] + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: awoooi-api-velero-reader +subjects: + - kind: ServiceAccount + name: awoooi-api + namespace: awoooi-prod +roleRef: + kind: ClusterRole + name: awoooi-api-velero-reader + apiGroup: rbac.authorization.k8s.io +``` + +```bash +kubectl apply -f k8s/rbac/api-velero-reader.yaml +``` + +#### L0-2: 確認 Redis 連線 + +```python +# 驗證現有 auto_repair_service 使用的 Redis client +# 查看 apps/api/src/core/redis.py 或 auto_repair_service.py 的 import +grep -n "redis\|Redis\|get_redis" apps/api/src/services/auto_repair_service.py | head -10 +``` + +**預期**:確認 `get_redis_client()` 或類似函數存在且可用 + +#### L0-3: 建立 ops/config 目錄 + +```bash +mkdir -p ops/config +``` + +--- + +### Layer 1 — 資料庫變更 (Migration) + +> ⚠️ **執行前必須先備份**: +> ```bash +> PGPASSWORD=awoooi_prod_2026 pg_dump -U awoooi -d awoooi_prod \ +> -h localhost -t playbooks -t approval_records \ +> > /tmp/sprint51_pre_migration_backup_$(date +%Y%m%d_%H%M%S).sql +> ``` + +#### L1-1: Playbook model 新增安全欄位 (M-001) — ⚠️ 無 SQL,改 Pydantic + Redis + +> **P1 修正**:Playbook 存在 Redis(無 PostgreSQL table),不需要 SQL migration。 +> 修改點:`apps/api/src/models/playbook.py` + `apps/api/src/repositories/playbook_repository.py` + +**修改 `apps/api/src/models/playbook.py`**(在 Playbook class 新增欄位): + +```python +# 在 Playbook class 內新增(Sprint 5.1 / 2026-04-08 Asia/Taipei) +requires_approval_level: str = "auto" +# auto=直接執行, standard=1票審核, critical=2票MultiSig +# 由 Service Registry 決定,Playbook 記錄最後一次執行時的 level + +stateful_targets: list[str] = [] +# 此 Playbook 操作的 Stateful 服務清單,對應 service-registry.yaml +# 例: ["postgres", "redis"] + +requires_pre_backup: bool = False +# 執行前是否需要 Pre-flight 備份檢查 +``` + +**修改 `apps/api/src/repositories/playbook_repository.py`**: + +在 `to_redis_dict()` 加入三個新欄位的序列化,在 `from_redis_dict()` 加入反序列化(帶預設值,向後相容)。 + +#### L1-2: approval_records 新增 MultiSig 欄位 (M-002) + +```sql +-- apps/api/migrations/sprint51_approval_multisig.sql +-- Sprint 5.1: MultiSig 雙簽核支援 +-- 執行者: Claude Sonnet 4.6 / 2026-04-08 Asia/Taipei + +BEGIN; + +ALTER TABLE approval_records + ADD COLUMN IF NOT EXISTS approval_level VARCHAR(20) + DEFAULT 'standard' + CHECK (approval_level IN ('standard', 'critical')), + ADD COLUMN IF NOT EXISTS approval_votes JSONB + DEFAULT '[]'::jsonb + COMMENT '已投票記錄: [{user_id, voted_at, action}]', + ADD COLUMN IF NOT EXISTS required_votes INTEGER + DEFAULT 1 + COMMENT 'standard=1, critical=2'; + +COMMENT ON COLUMN approval_records.approval_votes IS + 'JSON array: [{"user_id": "123", "voted_at": "2026-04-08T...", "action": "approve"}]'; + +-- 現有記錄預設為 standard 1票 +UPDATE approval_records +SET approval_level = 'standard', required_votes = 1 +WHERE approval_level IS NULL; + +COMMIT; +``` + +#### L1-3: alert_operation_log ENUM 新增 (M-003) + +> ⚠️ PostgreSQL ENUM ALTER 不可 rollback,請先確認備份完成 + +```sql +-- apps/api/migrations/sprint51_alert_log_events.sql +-- Sprint 5.1: alert_operation_log 新增 8 個 event_type +-- 執行者: Claude Sonnet 4.6 / 2026-04-08 Asia/Taipei + +BEGIN; + +-- 新增 ENUM 值(PostgreSQL 支援逐一 ADD VALUE) +ALTER TYPE alert_event_type ADD VALUE IF NOT EXISTS 'GUARDRAIL_BLOCKED'; +ALTER TYPE alert_event_type ADD VALUE IF NOT EXISTS 'PRE_FLIGHT_PASSED'; +ALTER TYPE alert_event_type ADD VALUE IF NOT EXISTS 'PRE_FLIGHT_FAILED'; +ALTER TYPE alert_event_type ADD VALUE IF NOT EXISTS 'BACKUP_TRIGGERED'; +ALTER TYPE alert_event_type ADD VALUE IF NOT EXISTS 'BACKUP_COMPLETED'; +ALTER TYPE alert_event_type ADD VALUE IF NOT EXISTS 'BACKUP_FAILED'; +ALTER TYPE alert_event_type ADD VALUE IF NOT EXISTS 'APPROVAL_ESCALATED'; +ALTER TYPE alert_event_type ADD VALUE IF NOT EXISTS 'CHANGE_APPLIED'; + +COMMIT; +``` + +**執行方式(在 192.168.0.188 執行)**: + +> ⚠️ M-001 已改為 Pydantic model 修改,不再是 SQL。只有 M-002 和 M-003 需要執行 SQL。 + +```bash +# 先備份(M-002/M-003 涉及 approval_records 和 ENUM) +PGPASSWORD=awoooi_prod_2026 pg_dump -U awoooi -d awoooi_prod -h localhost \ + > /tmp/sprint51_pre_$(date +%Y%m%d_%H%M%S).sql + +# M-002: approval_records 多簽欄位 +PGPASSWORD=awoooi_prod_2026 psql -U awoooi -d awoooi_prod -h localhost \ + -f sprint51_approval_multisig.sql + +# M-003: alert_event_type ENUM 擴充(最後執行) +PGPASSWORD=awoooi_prod_2026 psql -U awoooi -d awoooi_prod -h localhost \ + -f sprint51_alert_log_events.sql + +# 驗證 +PGPASSWORD=awoooi_prod_2026 psql -U awoooi -d awoooi_prod -h localhost \ + -c "\dT alert_event_type" +``` + +--- + +### Layer 2 — 基礎設施層 + +#### L2-1: Service Registry YAML (SR-001) + +**檔案**:`ops/config/service-registry.yaml` + +```yaml +# ops/config/service-registry.yaml +# Service Registry — 服務 Stateful 分級清單 +# 版本: 1.0.0 / 2026-04-08 Asia/Taipei +# 維護: 修改需 PR + 統帥審核,禁止直接 push +# 說明: +# BLOCK = 系統禁止自動修復,僅告警(資料風險最高) +# CRITICAL_HITL = 允許 Playbook,但需 MultiSig 2票 +# STANDARD_HITL = 允許 Playbook,需 1票審核 +# AUTO = 允許自動執行(無狀態服務) + +services: + # ─── BLOCK:系統禁止(連 Playbook 都不提供)──────────────────────────── + - name: postgres + display_name: "PostgreSQL 主庫 (awoooi_prod)" + host: "192.168.0.188" + stateful_level: BLOCK + reason: "主要業務資料庫,重啟可能導致 WAL 截斷、事務回滾" + alert_only: true + containers: ["postgres"] + + - name: momo-db + display_name: "PostgreSQL (momo_db)" + host: "192.168.0.188" + stateful_level: BLOCK + reason: "momo 產品資料庫,禁止自動操作" + alert_only: true + containers: ["momo-db"] + + - name: langfuse-db + display_name: "PostgreSQL (Langfuse)" + host: "192.168.0.110" + stateful_level: BLOCK + reason: "LLM trace 資料庫,重啟導致追蹤資料遺失" + alert_only: true + containers: ["langfuse-db"] + + - name: harbor-db + display_name: "PostgreSQL (Harbor Registry)" + host: "192.168.0.110" + stateful_level: BLOCK + reason: "Harbor Registry 資料庫,重啟可能損壞 image layer 索引" + alert_only: true + containers: ["harbor-db"] + + - name: sentry-postgres + display_name: "PostgreSQL (Sentry)" + host: "192.168.0.110" + stateful_level: BLOCK + reason: "Sentry 錯誤追蹤資料庫" + alert_only: true + containers: ["sentry-postgres"] + + - name: signoz-clickhouse + display_name: "ClickHouse (SignOz)" + host: "192.168.0.188" + stateful_level: BLOCK + reason: "列欄式 OLAP 資料庫,寫入中重啟可能損壞列欄檔案" + alert_only: true + containers: ["signoz-clickhouse"] + + # ─── CRITICAL_HITL:高風險,需 MultiSig 2票 ────────────────────────── + - name: redis + display_name: "Redis (AWOOOI)" + host: "192.168.0.188" + stateful_level: CRITICAL_HITL + reason: "AWOOOI 依賴 Redis 做冪等鎖與快取,重啟丟失鎖狀態" + requires_pre_backup: false # Redis 無法備份 in-memory,需人工評估 + containers: ["redis"] + + - name: harbor-redis + display_name: "Redis (Harbor)" + host: "192.168.0.110" + stateful_level: CRITICAL_HITL + reason: "Harbor session 快取" + containers: ["harbor-redis"] + + - name: sentry-redis + display_name: "Redis (Sentry)" + host: "192.168.0.110" + stateful_level: CRITICAL_HITL + reason: "Sentry 任務佇列" + containers: ["sentry-redis"] + + - name: gitea + display_name: "Gitea (程式碼倉庫)" + host: "192.168.0.110" + stateful_level: CRITICAL_HITL + reason: "restart 會殺掉活躍 SSH session,Git push 中斷可能損壞 working copy" + requires_pre_backup: false + containers: ["gitea"] + + - name: harbor + display_name: "Harbor (Container Registry)" + host: "192.168.0.110" + stateful_level: CRITICAL_HITL + reason: "重啟中斷 pull/push;GC 進行中重啟可能損壞 layer" + requires_pre_backup: false + containers: ["harbor-core", "harbor-jobservice", "harbor-portal"] + + - name: minio + display_name: "MinIO (物件存儲)" + host: "192.168.0.188" + stateful_level: CRITICAL_HITL + reason: "寫入中重啟可能導致 multipart upload 中斷" + requires_pre_backup: false + containers: ["minio"] + + # ─── STANDARD_HITL:中風險,需 1票審核 ────────────────────────────── + - name: prometheus + display_name: "Prometheus" + host: "192.168.0.110" + stateful_level: STANDARD_HITL + reason: "有 TSDB WAL,exited 狀態用 docker start(非 restart)" + restart_command: "docker start" # 特殊:使用 start 而非 restart + containers: ["prometheus"] + + - name: grafana + display_name: "Grafana" + host: "192.168.0.110" + stateful_level: STANDARD_HITL + reason: "有 SQLite 設定儲存,exited 用 docker start" + restart_command: "docker start" + containers: ["grafana"] + + - name: alertmanager + display_name: "Alertmanager" + host: "192.168.0.110" + stateful_level: STANDARD_HITL + reason: "有 silence 狀態,exited 用 docker start" + restart_command: "docker start" + containers: ["alertmanager"] + + # ─── AUTO:無狀態,允許自動修復 ────────────────────────────────────── + - name: nginx + display_name: "Nginx (反向代理)" + host: "192.168.0.110" + stateful_level: AUTO + containers: ["nginx", "nginx-188"] + + - name: awoooi-api + display_name: "AWOOOI API (K3s)" + host: "k3s" + stateful_level: AUTO + containers: [] # K3s Pod,由 kubectl rollout 管理 + + - name: awoooi-web + display_name: "AWOOOI Web (K3s)" + host: "k3s" + stateful_level: AUTO + containers: [] + + - name: blackbox-exporter + display_name: "Blackbox Exporter" + host: "192.168.0.110" + stateful_level: AUTO + containers: ["blackbox-exporter"] + + - name: langfuse + display_name: "Langfuse (LLMOps)" + host: "192.168.0.110" + stateful_level: AUTO + containers: ["langfuse-web", "langfuse-worker"] + + - name: ollama + display_name: "Ollama (Local LLM)" + host: "192.168.0.188" + stateful_level: AUTO + containers: ["ollama"] + + - name: momo-app + display_name: "momo Web App" + host: "192.168.0.188" + stateful_level: AUTO + containers: ["momo-app"] + + - name: tsenyang-website + display_name: "Tsenyang Website" + host: "192.168.0.188" + stateful_level: AUTO + containers: ["tsenyang-website"] + + - name: stock-platform + display_name: "Stock Platform" + host: "192.168.0.110" + stateful_level: AUTO + containers: ["stock-platform"] + +# ─── 備份策略參考 ──────────────────────────────────────────────────────── +backup_policies: + velero_max_age_hours: 4 # Velero 備份過期閾值(Q2 決策) + emergency_backup_timeout: 600 # 緊急備份超時秒數 + block_backup_on_high_io: true # CPU/IO > 80% 時禁止觸發備份(Q4 決策) + io_threshold_percent: 80 + +# ─── MultiSig 設定 ─────────────────────────────────────────────────────── +multisig: + critical_required_votes: 2 # CRITICAL_HITL 需要幾票 + standard_required_votes: 1 # STANDARD_HITL 需要幾票 + vote_expiry_minutes: 30 # 投票有效期 +``` + +#### L2-2: Prometheus auto_repair flag 補齊 + +在 `ops/monitoring/alerts-unified.yml` 確認所有 Docker 容器規則有正確的 `auto_repair` 標籤。 + +新增(若缺少): +```yaml +# Docker 容器健康規則(Plan A 接入後使用) +- alert: DockerContainerUnhealthy + expr: container_health_status{job="docker-health-monitor"} == 0 + for: 2m + labels: + severity: warning + layer: docker-188 + auto_repair: "true" # 由 Service Registry 決定實際動作 + annotations: + summary: "容器 {{ $labels.container }} 健康檢查失敗" + description: "主機 {{ $labels.host }} 容器 {{ $labels.container }} 狀態異常" + +- alert: DockerContainerExited + expr: container_running_status{job="docker-health-monitor"} == 0 + for: 1m + labels: + severity: critical + layer: docker-188 + auto_repair: "true" + annotations: + summary: "容器 {{ $labels.container }} 已停止" +``` + +#### L2-3: K8s RBAC (K-001) + +見 L0-1 的 YAML 內容,執行: +```bash +kubectl apply -f k8s/rbac/api-velero-reader.yaml +# 驗證 +kubectl auth can-i list backups.velero.io \ + --as=system:serviceaccount:awoooi-prod:awoooi-api -n velero +``` + +--- + +### Layer 3 — 基礎設施層(IaC 相關 Python) + +#### L3-1: ServiceRegistryClient (新檔案) + +**檔案**:`apps/api/src/services/service_registry.py` + +```python +# apps/api/src/services/service_registry.py +# Service Registry Client — 讀取 ops/config/service-registry.yaml +# 撰寫: Claude Sonnet 4.6 / 2026-04-08 Asia/Taipei +# 架構: leWOOOgo 積木化,純 Service 層,無 Router/DB 依賴 + +from __future__ import annotations + +import logging +from enum import Enum +from functools import lru_cache +from pathlib import Path +from typing import Any + +import yaml + +logger = logging.getLogger(__name__) + +# YAML 路徑(相對於 repo root,API 啟動時設定 working dir) +_DEFAULT_REGISTRY_PATH = Path(__file__).parents[5] / "ops" / "config" / "service-registry.yaml" + + +class StatefulLevel(str, Enum): + BLOCK = "BLOCK" # 禁止,僅告警 + CRITICAL_HITL = "CRITICAL_HITL" # 2 票 MultiSig + STANDARD_HITL = "STANDARD_HITL" # 1 票 + AUTO = "AUTO" # 自動執行 + + +class ServiceInfo: + def __init__(self, data: dict[str, Any]) -> None: + self.name: str = data["name"] + self.display_name: str = data.get("display_name", self.name) + self.host: str = data.get("host", "unknown") + self.stateful_level: StatefulLevel = StatefulLevel(data.get("stateful_level", "AUTO")) + self.reason: str = data.get("reason", "") + self.alert_only: bool = data.get("alert_only", False) + self.requires_pre_backup: bool = data.get("requires_pre_backup", False) + self.restart_command: str = data.get("restart_command", "docker restart") + self.containers: list[str] = data.get("containers", []) + + +class ServiceRegistryClient: + """ + Service Registry 客戶端 + 讀取 ops/config/service-registry.yaml,提供服務 Stateful 分級查詢 + 設計原則: 純讀取,不寫入;失敗時 fallback AUTO(防護不應阻擋告警流程) + """ + + def __init__(self, registry_path: Path | None = None) -> None: + self._path = registry_path or _DEFAULT_REGISTRY_PATH + self._services: dict[str, ServiceInfo] = {} + self._backup_policies: dict[str, Any] = {} + self._multisig_config: dict[str, Any] = {} + self._loaded = False + + def _load(self) -> None: + if self._loaded: + return + try: + with open(self._path) as f: + data = yaml.safe_load(f) + for svc in data.get("services", []): + info = ServiceInfo(svc) + self._services[info.name] = info + # 也按 container 名稱建立索引 + for container in info.containers: + self._services[container] = info + self._backup_policies = data.get("backup_policies", {}) + self._multisig_config = data.get("multisig", {}) + self._loaded = True + logger.info(f"Service Registry 載入完成: {len(self._services)} 個服務") + except Exception as e: + logger.error(f"Service Registry 載入失敗: {e},所有服務 fallback AUTO") + self._loaded = True # 防止重複嘗試 + + def get_service(self, name: str) -> ServiceInfo | None: + self._load() + return self._services.get(name) + + def get_stateful_level(self, service_name: str) -> StatefulLevel: + """查詢服務分級,未知服務 fallback AUTO""" + info = self.get_service(service_name) + if info is None: + logger.warning(f"未知服務 '{service_name}',fallback AUTO") + return StatefulLevel.AUTO + return info.stateful_level + + def is_blocked(self, service_name: str) -> bool: + return self.get_stateful_level(service_name) == StatefulLevel.BLOCK + + def requires_multisig(self, service_name: str) -> bool: + return self.get_stateful_level(service_name) == StatefulLevel.CRITICAL_HITL + + def get_required_votes(self, service_name: str) -> int: + self._load() + level = self.get_stateful_level(service_name) + if level == StatefulLevel.CRITICAL_HITL: + return self._multisig_config.get("critical_required_votes", 2) + return self._multisig_config.get("standard_required_votes", 1) + + def get_backup_policies(self) -> dict[str, Any]: + self._load() + return self._backup_policies + + def get_restart_command(self, service_name: str) -> str: + info = self.get_service(service_name) + return info.restart_command if info else "docker restart" + + +# Singleton +_registry_client: ServiceRegistryClient | None = None + + +def get_service_registry() -> ServiceRegistryClient: + global _registry_client + if _registry_client is None: + _registry_client = ServiceRegistryClient() + return _registry_client +``` + +#### L3-2: VeleroClient (新檔案) + +**檔案**:`apps/api/src/services/velero_client.py` + +```python +# apps/api/src/services/velero_client.py +# Velero Backup 查詢客戶端 (kubectl 方式,Q7 決策) +# 撰寫: Claude Sonnet 4.6 / 2026-04-08 Asia/Taipei + +from __future__ import annotations + +import asyncio +import json +import logging +from datetime import UTC, datetime, timedelta + +logger = logging.getLogger(__name__) + +_VELERO_NAMESPACE = "velero" +_KUBECTL_TIMEOUT = 30 # 秒 + + +class VeleroClient: + """ + 透過 kubectl 查詢 Velero 備份狀態 + 設計原則: 失敗時 fallback「假設備份過期」(保守原則) + """ + + async def get_latest_backup_age_hours(self) -> float: + """ + 查詢最近一次 Completed 備份距今幾小時 + 失敗時返回 999.0(視為嚴重過期,觸發 Abort) + """ + try: + result = await asyncio.wait_for( + self._run_kubectl( + ["get", "backup", "-n", _VELERO_NAMESPACE, + "-o", "json", "--field-selector", "status.phase=Completed"] + ), + timeout=_KUBECTL_TIMEOUT, + ) + data = json.loads(result) + items = data.get("items", []) + if not items: + logger.warning("Velero: 找不到任何 Completed 備份") + return 999.0 + + # 找最新的完成時間 + latest = max( + items, + key=lambda x: x.get("status", {}).get("completionTimestamp", ""), + ) + completion_ts = latest["status"].get("completionTimestamp", "") + if not completion_ts: + return 999.0 + + completed_at = datetime.fromisoformat(completion_ts.replace("Z", "+00:00")) + age = (datetime.now(UTC) - completed_at).total_seconds() / 3600 + logger.info(f"Velero 最近備份: {completion_ts},距今 {age:.1f} 小時") + return age + + except asyncio.TimeoutError: + logger.error("Velero kubectl 查詢超時") + return 999.0 + except Exception as e: + logger.error(f"Velero 查詢失敗: {e}") + return 999.0 + + async def trigger_emergency_backup(self, backup_name: str | None = None) -> bool: + """ + 觸發緊急備份(非同步,不等待完成) + 返回 True 表示指令已成功發送 + """ + import time + name = backup_name or f"emergency-{int(time.time())}" + try: + await asyncio.wait_for( + self._run_kubectl([ + "create", "backup", name, + "-n", _VELERO_NAMESPACE, + "--include-namespaces", "awoooi-prod", + "--wait=false", + ]), + timeout=_KUBECTL_TIMEOUT, + ) + logger.info(f"Velero 緊急備份已啟動: {name}") + return True + except Exception as e: + logger.error(f"Velero 緊急備份失敗: {e}") + return False + + async def _run_kubectl(self, args: list[str]) -> str: + proc = await asyncio.create_subprocess_exec( + "kubectl", *args, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await proc.communicate() + if proc.returncode != 0: + raise RuntimeError(f"kubectl 失敗: {stderr.decode()}") + return stdout.decode() + + +_velero_client: VeleroClient | None = None + + +def get_velero_client() -> VeleroClient: + global _velero_client + if _velero_client is None: + _velero_client = VeleroClient() + return _velero_client +``` + +#### L3-3: PreflightService (新檔案) + +**檔案**:`apps/api/src/services/preflight_service.py` + +```python +# apps/api/src/services/preflight_service.py +# Pre-flight 安全檢查服務 (Q2/Q4 決策) +# 撰寫: Claude Sonnet 4.6 / 2026-04-08 Asia/Taipei + +from __future__ import annotations + +import logging +from dataclasses import dataclass +from enum import Enum + +from .service_registry import ServiceRegistryClient, StatefulLevel, get_service_registry +from .velero_client import VeleroClient, get_velero_client + +logger = logging.getLogger(__name__) + + +class PreflightResult(str, Enum): + PASS = "PASS" + ABORT_BACKUP_EXPIRED = "ABORT_BACKUP_EXPIRED" + ABORT_HIGH_IO = "ABORT_HIGH_IO" + SKIP = "SKIP" # 服務不需要 Pre-flight + + +@dataclass +class PreflightReport: + result: PreflightResult + backup_age_hours: float | None = None + backup_name_triggered: str | None = None + reason: str = "" + + +class PreflightService: + """ + Pre-flight 安全檢查 + - 只有 CRITICAL_HITL 且 requires_pre_backup=True 的服務才觸發 + - 備份過期 → Abort + 觸發緊急備份(非同步) + - CPU/IO 高負載告警 → 禁止觸發備份(Q4) + """ + + def __init__( + self, + registry: ServiceRegistryClient | None = None, + velero: VeleroClient | None = None, + ) -> None: + self._registry = registry or get_service_registry() + self._velero = velero or get_velero_client() + + async def check( + self, + service_name: str, + alert_labels: dict | None = None, + ) -> PreflightReport: + """ + 執行 Pre-flight 檢查 + alert_labels: Prometheus 告警標籤,用於判斷 CPU/IO 負載 + """ + info = self._registry.get_service(service_name) + if info is None or not info.requires_pre_backup: + return PreflightReport(result=PreflightResult.SKIP, reason="服務不需要 Pre-flight") + + # Q4: CPU/IO 高負載告警時禁止觸發備份 + if self._is_high_io_alert(alert_labels): + logger.warning(f"Pre-flight: {service_name} 屬於 CPU/IO 高負載告警,跳過備份觸發") + return PreflightReport( + result=PreflightResult.ABORT_HIGH_IO, + reason="告警類型為 CPU/IO 高負載,禁止觸發備份(Q4 決策)", + ) + + policies = self._registry.get_backup_policies() + max_age = policies.get("velero_max_age_hours", 4) + + age = await self._velero.get_latest_backup_age_hours() + if age <= max_age: + return PreflightReport( + result=PreflightResult.PASS, + backup_age_hours=age, + reason=f"備份時間正常 ({age:.1f}h < {max_age}h)", + ) + + # 備份過期 → 觸發緊急備份 + Abort + import time + backup_name = f"emergency-preflight-{int(time.time())}" + triggered = await self._velero.trigger_emergency_backup(backup_name) + return PreflightReport( + result=PreflightResult.ABORT_BACKUP_EXPIRED, + backup_age_hours=age, + backup_name_triggered=backup_name if triggered else None, + reason=( + f"備份過期 ({age:.1f}h > {max_age}h)。" + f"{'緊急備份已啟動: ' + backup_name if triggered else '緊急備份啟動失敗,請人工處理'}" + ), + ) + + def _is_high_io_alert(self, labels: dict | None) -> bool: + if not labels: + return False + alert_name = labels.get("alertname", "").lower() + return any(kw in alert_name for kw in ["cpu", "io", "disk", "load", "memory"]) + + +_preflight_service: PreflightService | None = None + + +def get_preflight_service() -> PreflightService: + global _preflight_service + if _preflight_service is None: + _preflight_service = PreflightService() + return _preflight_service +``` + +--- + +### Layer 4 — auto_repair_service 注入 Guardrail + +**修改檔案**:`apps/api/src/services/auto_repair_service.py` + +在 `evaluate_auto_repair()` 的**最前面**(全域熔斷之後、severity 檢查之前)注入: + +```python +# 在 evaluate_auto_repair() 內,行 178 之後插入: + +# ─── Guardrail: Service Registry 分級檢查 ────────────────────────────── +from .service_registry import get_service_registry, StatefulLevel + +registry = get_service_registry() +# 從 incident 的 target_resource 或 service_name 提取服務名稱 +service_name = incident.target_resource or incident.service_name or "" +stateful_level = registry.get_stateful_level(service_name) + +if stateful_level == StatefulLevel.BLOCK: + # 寫入 alert_operation_log + await op_log.append( + "GUARDRAIL_BLOCKED", + incident_id=str(incident.id), + actor="guardrail", + action_detail=f"服務 {service_name} 屬於 BLOCK 等級,禁止自動修復", + success=False, + context={"service_name": service_name, "stateful_level": "BLOCK"}, + ) + return AutoRepairDecision( + should_repair=False, + reason=f"GUARDRAIL_BLOCK: 服務 {service_name} 屬於禁止自動修復清單(資料風險)", + playbook=None, + ) +``` + +**在 `evaluate_auto_repair()` 中,Playbook 匹配後注入 approval_level**: + +```python +# 在找到 top_playbook 後,決定 approval level +required_votes = registry.get_required_votes(service_name) +# 寫入 ApprovalRecord(或透過現有 approval_service 設定) +# 確保 approval_record.approval_level 和 required_votes 正確設定 +``` + +--- + +### Layer 4 — Critical Bug 修復 (Prometheus auto_repair flag) + +**修改檔案**:`apps/api/src/api/v1/webhooks.py` + +在 `alertmanager_webhook()` 或處理告警的函數中,讀取 labels 並傳入: + +```python +# 在建立 Incident 之前,提取 auto_repair flag +labels = alert.labels or {} +can_auto_repair = labels.get("auto_repair", "false").lower() == "true" + +# 建立 Incident 時帶入此欄位(若 Incident model 有此欄位) +# 或在呼叫 _try_auto_repair_background 前判斷 +if not can_auto_repair: + # 強制 HITL,不觸發自動修復背景任務 + await op_log.append( + "GUARDRAIL_BLOCKED", + incident_id=incident_id, + actor="prometheus-rule", + action_detail=f"Prometheus rule 設定 auto_repair=false,強制人工審核", + context={"alert_name": alert.name, "auto_repair_flag": False}, + ) + return # 不呼叫 _try_auto_repair_background +``` + +--- + +### Layer 4 — Langfuse trace_id 注入 (Q10) + +**修改檔案**:`apps/api/src/api/v1/webhooks.py` 或 `openclaw.py` + +在 OpenClaw 分析完成後,取得 Langfuse trace_id 並寫入: + +```python +# 在 analyze_alert() 返回後 +langfuse_trace_id = getattr(analysis_result, "langfuse_trace_id", None) + +await op_log.append( + "AUTO_REPAIR_TRIGGERED", + incident_id=incident_id, + actor="openclaw", + context={ + "langfuse_trace_id": langfuse_trace_id, + "langfuse_url": f"http://192.168.0.110:3100/trace/{langfuse_trace_id}" if langfuse_trace_id else None, + "model_used": analysis_result.model_used, + "confidence": analysis_result.confidence, + }, +) +``` + +--- + +### Layer 4 — ALERT_RECEIVED 補點 + +**修改檔案**:`apps/api/src/api/v1/webhooks.py` + +在每個告警入口函數的**最開頭**(驗證完 HMAC 之後): + +```python +# signoz_webhook / alertmanager_webhook / sentry_webhook 入口 +await op_log.append( + "ALERT_RECEIVED", + actor="alertmanager", # 或 "sentry" / "signoz" + action_detail=f"收到告警: {alert.name or alert.title}", + context={"source": "alertmanager", "labels": labels}, +) +``` + +--- + +### Layer 4 — docker-health-monitor 改造(純感知層) + +**覆蓋** `scripts/ops/docker-health-monitor.sh`: + +移除所有 `docker restart` / `docker start` 邏輯,改為: +1. 偵測容器狀態 +2. 組裝符合現有 Alertmanager webhook 格式的 JSON +3. POST 到 `/api/v1/webhooks/alertmanager`(現有端點,無需新端點) +4. Fallback:API down → 直接 Telegram Bot API + +```bash +# 核心改造:send_awoooi_alert 改為送 Alertmanager 格式 +send_to_awoooi() { + local container="$1" + local status="$2" + local hostname + hostname=$(hostname) + + # 組裝 Alertmanager 格式 JSON + local payload + payload=$(cat < +git push gitea main +# CD 自動重新部署舊版本 +``` + +### 5.3 YAML Rollback + +```bash +# Service Registry YAML 是 IaC,直接 git revert 即可 +# ServiceRegistryClient 讀取失敗時 fallback AUTO,不影響現有功能 +``` + +--- + +## 第六章:需要產出的技術文件清單 + +| # | 文件 | 類型 | 優先 | +|---|------|------|------| +| ADR-062 | Data Safety Guardrails 完整架構決策 | ADR | P0 | +| ADR-063 | Service Registry IaC 設計規範 | ADR | P0 | +| SR-001 | ops/config/service-registry.yaml | IaC | P0 | +| M-001~003 | Migration 腳本 ×3 | SQL | P0 | +| K-001 | k8s/rbac/api-velero-reader.yaml | K8s | P0 | +| RB-001 | SPRINT51-ROLLBACK.md | Runbook | P0 | +| PB-UPDATE | 12 個 Playbook 補齊 stateful_targets 欄位 | DB | P1 | +| DASHBOARD | Grafana alert_operation_log 儀表板 | Grafana | P1 | +| E2E-TEST | 5 個 E2E 測試情境的測試腳本 | QA | P1 | + +--- + +## 第七章:執行前最後確認 + +**統帥在下令執行前,請確認:** + +| # | 確認項 | 原因 | +|---|--------|------| +| C1 | API Pod 的 K8s ServiceAccount 名稱是否為 `awoooi-api` | L0-1 RBAC binding 需要正確名稱 | +| C2 | 192.168.0.188 上 kubectl 可用(API Pod 或直接 SSH) | Velero 查詢需要 | +| C3 | Telegram whitelist 目前只有一個 user_id | MultiSig Q8 確認:同一人兩次點擊 | +| C4 | Velero 是否已部署且有正常 Completed backup | Pre-flight 需要至少一筆 Completed backup | +| C5 | pg_dump 備份在執行 Migration 前完成 | ENUM 不可回滾 |