Files
awoooi/apps/api/src/services/playbook_seed_service.py
Your Name 45dbe07188
Some checks failed
run-migration / migrate (push) Failing after 22s
Deploy Alert Rules / Deploy Prometheus Alert Rules (push) Successful in 53s
Type Sync Check / check-type-sync (push) Successful in 2m54s
CD Pipeline / build-and-deploy (push) Has been cancelled
Ansible Lint / lint (push) Has been cancelled
fix(flywheel): 自動化飛輪六大能力修復(ADR-092 B3)
【根因鏈修復】
MCP Provider bugs → PreDecisionInvestigator 失敗 → Agent Debate 無上下文
→ LLM 逾時 → description="待分析" → ADR-091 鐵閘攔截 → tg_sent 未設
→ W-2 Watchdog 誤報「靜默故障」

【六大修復】
1. MCP Provider 三蟲修復
   - ssh_provider: asyncssh.run() → conn.run()
   - prometheus_provider: KeyError 'query' → .get() 容錯
   - k8s_provider: 空 pod_name → 早返回錯誤字典

2. Agent Debate / 決策品質
   - decision_manager: 逾時降級文字改為明確描述(繞過 ADR-091 鐵閘)
   - intent_classifier: LLM 逾時降級至關鍵字分類(非 None)

3. Watchdog 誤報修復(ADR-092 B3)
   - W-2: tg_sent Redis TTL → telegram_message_id IS NULL(DB 真值)
   - W-5 新增: suggested_action IN 空/待分析/NO_ACTION + tg_id IS NULL
   - approval_timeout_resolver: 60min → 15min,batch 50 → 200

4. Config Drift 自動化
   - drift_adopt_service: auto_adopt_if_safe() 六條件安全閘
   - drift.py: 背景任務先嘗試自動採納再發人工 Telegram 卡片

5. Playbook 飛輪穩定
   - playbook_seed_service: 修復幂等性(deprecated 不視為缺失)
   - playbook_evolver: 只載 DRAFT+APPROVED(非全部 294 筆)

6. 可觀測性
   - alert_rule_engine: auto_rule 結構化日誌 + Redis 計數器(pipeline)
   - auto_approve: reject 原因 Redis 計數器
   - heartbeat_report_service: 新增「⚙️ 自動化統計(今日)」區塊

【待人工執行】
psql $DATABASE_URL -f apps/api/migrations/cleanup_duplicate_deprecated_playbooks.sql

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 10:55:50 +08:00

118 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Playbook Seed Service — 從 alert_rules.yaml 初始化 Playbook 資料
=================================================================
職責:
- 啟動時讀取 alert_rules.yaml
- 將每條規則轉換為 APPROVED Playbook 寫入 DB冪等已存在則跳過
- 確保自動修復鏈路有資料可用
呼叫方: main.py lifespan (asyncio.create_task — 非阻塞)
2026-04-10 Claude Sonnet 4.6 Asia/Taipei
"""
from __future__ import annotations
from pathlib import Path
import structlog
import yaml
logger = structlog.get_logger(__name__)
_RULES_PATH = Path(__file__).parent.parent.parent / "alert_rules.yaml"
async def seed_playbooks_from_rules() -> None:
"""從 alert_rules.yaml 匯入 APPROVED Playbook冪等"""
try:
if not _RULES_PATH.exists():
logger.warning("playbook_seed_rules_not_found", path=str(_RULES_PATH))
return
data = yaml.safe_load(_RULES_PATH.read_text())
rules = data.get("rules", [])
if not rules:
return
from src.models.playbook import (
ActionType, Playbook, PlaybookSource, PlaybookStatus,
RepairStep, RiskLevel, SymptomPattern,
)
from src.repositories.playbook_repository import get_playbook_repository
repo = get_playbook_repository()
# 取得現有 YAML_RULE playbook依 name 去重避免重複建立
# 2026-04-15 ogt: 不再用 list_playbooks舊格式 repair_steps 會 validation error
# 改用 raw SQL 只撈 name 欄位,更穩健
# 2026-04-24 ogt + Claude Sonnet 4.6: 修復重複建立/封存迴圈
# 舊邏輯 status != 'deprecated' 導致deprecated 歷史記錄永遠存在 →
# 每次啟動都重建同名 PlaybookC1 保護後不再封存,但 deprecated 不清除)
# 修:不管 status同名 yaml_rule 任何狀態存在即視為已存在,不重建
from src.db.base import get_db_context
from sqlalchemy import text as sa_text
async with get_db_context() as db:
rows = await db.execute(
sa_text(
"SELECT name FROM playbooks WHERE source = 'yaml_rule'"
)
)
existing_names = {r[0] for r in rows.fetchall()}
seeded = 0
for rule in rules:
rule_id = rule.get("id", "")
rule_name = rule.get("description", rule_id)
if rule_name in existing_names:
continue
resp = rule.get("response", {})
kubectl_cmd = resp.get("kubectl_command", "").strip()
if not kubectl_cmd:
continue
risk_str = resp.get("risk", "medium").lower()
risk_map = {"low": RiskLevel.LOW, "medium": RiskLevel.MEDIUM, "critical": RiskLevel.HIGH}
risk = risk_map.get(risk_str, RiskLevel.MEDIUM)
alertnames = rule.get("match", {}).get("alertname", [])
action_type = ActionType.KUBECTL
if kubectl_cmd.startswith("ssh"):
action_type = ActionType.SSH_COMMAND
playbook = Playbook(
name=rule.get("description", rule_id),
description=resp.get("description", rule.get("description", "")),
status=PlaybookStatus.APPROVED,
source=PlaybookSource.YAML_RULE,
symptom_pattern=SymptomPattern(
alert_names=alertnames,
affected_services=[],
severity_range=["P2", "P3"],
),
repair_steps=[
RepairStep(
step_number=1,
action_type=action_type,
command=kubectl_cmd,
expected_result=resp.get("action_title", ""),
risk_level=risk,
requires_approval=False,
)
],
ai_confidence=1.0,
approved_by="alert_rules_yaml",
)
try:
await repo.create(playbook)
seeded += 1
logger.info("playbook_seeded", rule_id=rule_id, name=playbook.name)
except Exception as e:
logger.warning("playbook_seed_failed", rule_id=rule_id, error=str(e))
logger.info("playbook_seed_complete", seeded=seeded, total=len(rules))
except Exception as e:
logger.error("playbook_seed_error", error=str(e))