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
【根因鏈修復】 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>
118 lines
4.5 KiB
Python
118 lines
4.5 KiB
Python
"""
|
||
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 歷史記錄永遠存在 →
|
||
# 每次啟動都重建同名 Playbook(C1 保護後不再封存,但 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))
|