From c09521a1c6d8f7d06d942e6bba53f135ddc24cc8 Mon Sep 17 00:00:00 2001 From: OG T Date: Sun, 12 Apr 2026 14:51:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(cr):=20Code=20Review=20P0/P1=20=E5=85=A8?= =?UTF-8?q?=E4=BF=AE=E8=A3=9C=20=E2=80=94=20=E7=A9=8D=E6=9C=A8=E5=8C=96+SS?= =?UTF-8?q?H=E8=B7=AF=E7=94=B1+=E5=AE=89=E5=85=A8=E5=AE=88=E8=A1=9B?= =?UTF-8?q?=E9=A0=86=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P0-1: classify_alert_early 移至 incident_service (Service層),webhooks.py import 修正 P0-2: _ssh_execute() 改用 self._ssh,移除冗餘 SSHProvider() 實例化 P1-1: infrastructure SSH routing 移至 kubectl safety guard 之前,docker指令不再被攔截 P1-2: alert_rule_engine 新增 get_risk_for_alertname() public API P1-3: classify_notification() docstring 修正 ORM→Pydantic Co-Authored-By: Claude Sonnet 4.6 --- apps/api/src/api/v1/webhooks.py | 25 +----------- apps/api/src/services/alert_rule_engine.py | 14 +++++++ apps/api/src/services/decision_manager.py | 44 ++++++++++------------ apps/api/src/services/incident_service.py | 36 ++++++++++++++++++ apps/api/src/services/telegram_gateway.py | 2 +- 5 files changed, 72 insertions(+), 49 deletions(-) diff --git a/apps/api/src/api/v1/webhooks.py b/apps/api/src/api/v1/webhooks.py index 01e0c6b54..419d5beac 100644 --- a/apps/api/src/api/v1/webhooks.py +++ b/apps/api/src/api/v1/webhooks.py @@ -55,6 +55,7 @@ from src.services.approval_db import get_approval_service # Phase 17 P0: Service 層 (消除 Router 直接存取 Redis) # C2 修正 (首席架構師審查 2026-04-10): create_incident_for_approval + extract_affected_services 已移入 Service 層 from src.services.incident_service import ( + classify_alert_early, create_incident_for_approval, extract_affected_services, get_incident_service, @@ -970,30 +971,6 @@ class AlertmanagerPayload(BaseModel): alerts: list[AlertmanagerAlert] -def classify_alert_early(alertname: str, severity: str, _labels: dict) -> tuple[str, str]: - """ - ADR-073 Phase 2-2: 早期分診,在 LLM 分析前決定 alert_category + notification_type。 - 2026-04-12 ogt: 防止 HostBackupFailed 等被誤路由到 K8s executor。 - - Returns: - tuple[str, str]: (alert_category, notification_type) - """ - alertname_lower = alertname.lower() - if alertname in ("ConfigurationDrift", "KubeConfigDrift"): - return "config_drift", "TYPE-4D" - if severity in ("info", "none"): - return "info", "TYPE-1" - if any(kw in alertname_lower for kw in ("backup", "heartbeat")): - return "backup", "TYPE-1" - if alertname.startswith(("Docker", "Host")): - return "infrastructure", "TYPE-3" - if alertname.startswith(("Kube", "Pod", "Deploy", "Node", "Velero", "ArgoCD")): - return "kubernetes", "TYPE-3" - if alertname.startswith(("Postgres", "Redis")): - return "database", "TYPE-3" - return "general", "TYPE-3" - - def is_internal_ip(client_ip: str) -> bool: """檢查是否為內網 IP""" import ipaddress diff --git a/apps/api/src/services/alert_rule_engine.py b/apps/api/src/services/alert_rule_engine.py index 8699df0c9..b2a02c22d 100644 --- a/apps/api/src/services/alert_rule_engine.py +++ b/apps/api/src/services/alert_rule_engine.py @@ -145,6 +145,20 @@ def _is_generic(rule: dict) -> bool: # ── 公開 API ──────────────────────────────────────────────── +def get_risk_for_alertname(alertname: str) -> str | None: + """ + 根據 alertname 從 alert_rules.yaml 取得 risk 等級。 + ADR-073 Phase 3-6: 供 decision_manager 做 YAML risk override。 + + Returns: + risk 字串 ("low"/"medium"/"critical") 或 None(無匹配規則) + """ + for rule in _load_rules(): + if _matches(rule, alertname, "", "", ""): + return rule.get("response", {}).get("risk") + return None + + def match_rule(alert_context: dict) -> dict[str, Any] | None: """ 根據 alert_context 匹配規則,回傳填充後的 response dict。 diff --git a/apps/api/src/services/decision_manager.py b/apps/api/src/services/decision_manager.py index fdc6a613d..df1085d11 100644 --- a/apps/api/src/services/decision_manager.py +++ b/apps/api/src/services/decision_manager.py @@ -1246,6 +1246,15 @@ class DecisionManager: action = _re.sub(r"", _target, action) action = _re.sub(r"<[^>]+>", _target, action) + # ADR-073 Phase 3-2: infrastructure 告警 (Docker/Host) → SSH MCP routing (2026-04-12 ogt) + # alert_category = "infrastructure" 表示 Docker/Host 告警,不走 K8s executor + # action 格式應為 "docker restart " 或 "systemctl restart " + # P1-1 fix 2026-04-12: 必須在 kubectl safety guard 之前 routing,否則 docker 指令被 _action_safe=False 攔截 + _alert_category = getattr(incident, "alert_category", None) or "" + if _alert_category == "infrastructure" and action and not action.startswith("kubectl"): + await self._ssh_execute(incident, token, action, _target) + return + # 安全守衛: 替換後仍含 "unknown" 或未替換的 <...>/{...} → 拒絕執行 # 另外:若 target 等於 alertname,代表 LLM 把告警名稱填入 deployment_name,也拒絕 _alertname = incident.signals[0].labels.get("alertname", "") if incident.signals else "" @@ -1274,14 +1283,6 @@ class DecisionManager: ) return - # ADR-073 Phase 3-2: infrastructure 告警 (Docker/Host) → SSH MCP routing (2026-04-12 ogt) - # alert_category = "infrastructure" 表示 Docker/Host 告警,不走 K8s executor - # action 格式應為 "docker restart " 或 "systemctl restart " - _alert_category = getattr(incident, "alert_category", None) or "" - if _alert_category == "infrastructure" and action and not action.startswith("kubectl"): - await self._ssh_execute(incident, token, action, _target) - return - # BUG-003 修復 2026-04-11: 加入 K8s deployment 存在性驗證, # 避免 LLM 產生的無效 deployment name(/alertname/unknown)通過 safety guard # 但仍對 K8s 發出錯誤指令 @@ -1596,24 +1597,21 @@ class DecisionManager: # ADR-073 Phase 3-6: YAML rule risk_level 優先於 LLM 輸出 (2026-04-12 ogt) # LLM 有時把 critical 告警估為 medium,YAML 規則是由人工審閱過的,優先採用 try: - from src.services.alert_rule_engine import _load_rules, _matches + from src.services.alert_rule_engine import get_risk_for_alertname _alertname_for_risk = ( incident.signals[0].labels.get("alertname", "") if incident.signals else "" ) if _alertname_for_risk: - for _rule in _load_rules(): - if _matches(_rule, _alertname_for_risk, "", "", ""): - _yaml_risk = _rule.get("response", {}).get("risk") - if _yaml_risk and _yaml_risk != result.get("risk_level"): - logger.info( - "risk_level_yaml_override", - incident_id=incident.incident_id, - llm_risk=result.get("risk_level"), - yaml_risk=_yaml_risk, - ) - result["risk_level"] = _yaml_risk - break + _yaml_risk = get_risk_for_alertname(_alertname_for_risk) + if _yaml_risk and _yaml_risk != result.get("risk_level"): + logger.info( + "risk_level_yaml_override", + incident_id=incident.incident_id, + llm_risk=result.get("risk_level"), + yaml_risk=_yaml_risk, + ) + result["risk_level"] = _yaml_risk except Exception as _re: logger.debug("risk_level_yaml_override_failed", error=str(_re)) @@ -2030,7 +2028,6 @@ class DecisionManager: - systemctl restart - docker rm -f (含 docker start) """ - from src.plugins.mcp.providers.ssh_provider import SSHProvider import os as _os # 取得主機 — 從 instance label 或 SSH_MCP_ALLOWED_HOSTS 第一台 @@ -2076,7 +2073,6 @@ class DecisionManager: _fire_and_forget(_push_decision_to_telegram(incident, token.proposal_data)) return - ssh = SSHProvider() params: dict = {"host": _host} if _tool == "docker_restart": params["container"] = _container @@ -2084,7 +2080,7 @@ class DecisionManager: params["service"] = _service try: - result = await ssh.execute(tool_name=_tool, parameters=params) + result = await self._ssh.execute(tool_name=_tool, parameters=params) success = result.success logger.info( diff --git a/apps/api/src/services/incident_service.py b/apps/api/src/services/incident_service.py index 378a5fdb3..d3686f1ed 100644 --- a/apps/api/src/services/incident_service.py +++ b/apps/api/src/services/incident_service.py @@ -101,6 +101,42 @@ def extract_affected_services(labels: dict, target_resource: str) -> list[str]: return [] +def classify_alert_early(alertname: str, severity: str, _labels: dict) -> tuple[str, str]: + """ + ADR-073 Phase 2-2: 早期分診,在 LLM 分析前決定 alert_category + notification_type。 + 防止 HostBackupFailed 等告警被誤路由到 K8s executor。 + + 規則優先順序(由高到低): + 1. ConfigurationDrift / KubeConfigDrift → TYPE-4D (Config Drift 卡片) + 2. severity=info/none → TYPE-1 (純資訊,無按鈕) + 3. backup/heartbeat 關鍵字 → TYPE-1 + 4. Docker/Host 前綴 → infrastructure TYPE-3 + 5. Kube/Pod/Deploy/Node/Velero/ArgoCD 前綴 → kubernetes TYPE-3 + 6. Postgres/Redis 前綴 → database TYPE-3 + 7. 預設 → general TYPE-3 + + C3 修正 (首席架構師 CR 2026-04-13): 從 Router 層 (webhooks.py) 移入 Service 層 + 原違規: 業務邏輯函數定義在 api/v1/webhooks.py + + Returns: + tuple[str, str]: (alert_category, notification_type) + """ + alertname_lower = alertname.lower() + if alertname in ("ConfigurationDrift", "KubeConfigDrift"): + return "config_drift", "TYPE-4D" + if severity in ("info", "none"): + return "info", "TYPE-1" + if any(kw in alertname_lower for kw in ("backup", "heartbeat")): + return "backup", "TYPE-1" + if alertname.startswith(("Docker", "Host")): + return "infrastructure", "TYPE-3" + if alertname.startswith(("Kube", "Pod", "Deploy", "Node", "Velero", "ArgoCD")): + return "kubernetes", "TYPE-3" + if alertname.startswith(("Postgres", "Redis")): + return "database", "TYPE-3" + return "general", "TYPE-3" + + async def create_incident_for_approval( approval_id: str, risk_level: str, diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py index 07c794d27..711a5e16e 100644 --- a/apps/api/src/services/telegram_gateway.py +++ b/apps/api/src/services/telegram_gateway.py @@ -1127,7 +1127,7 @@ def classify_notification( TYPE-4D > TYPE-1 > TYPE-2 > TYPE-4 > TYPE-3(預設) Args: - incident: Incident ORM 物件(需要 signals[].labels + title) + incident: Incident Pydantic 模型(需要 signals[].labels + title) confidence: AI 決策信心值 (0.0~1.0) auto_executed: 是否已自動修復執行完成 mcp_all_failed: 所有 MCP provider 是否全失敗