""" Incident Type Converter - 跨套件 Incident 型別轉換 ==================================================== ADR-046: BrainIncident (lewooogo-brain) ↔ LocalIncident (apps/api) 轉換層 設計原則: - BrainIncident: lewooogo-brain 套件的精簡型別 (無 AWOOOI 擴展欄位) - LocalIncident: apps/api 的完整型別 (含 decision_chain, outcome 等稽核欄位) - 兩者保持套件邊界,互不依賴,透過 Converter 顯式轉換 轉換方向: brain→local: 引擎輸出 → 本地服務消費 (IncidentMemoryAdapter, signal_worker) local→brain: 本地資料 → 引擎輸入 (儲存到 DualIncidentMemory) 版本: v1.0 建立: 2026-04-01 (台北時區) 建立者: Claude Code (ADR-046 Phase R-R3 Sprint) """ from typing import Any from uuid import uuid4 import structlog from src.models.incident import ( Incident, IncidentStatus, Severity, Signal, parse_signal_source, ) logger = structlog.get_logger(__name__) def brain_to_local(brain_incident: Any) -> Incident: """ BrainIncident → LocalIncident 轉換 ADR-046: 跨套件邊界時呼叫,將 lewooogo-brain 引擎輸出轉為本地型別。 欄位對應: - incident_id, status, severity, signals, affected_services, proposal_ids, created_at, updated_at, resolved_at, closed_at → 直接對應 - decision_chain, outcome, frequency_stats → None (BrainIncident 無此欄位) - ttl_days, persisted_to_pg, vectorized → 使用預設值 BrainIncidentStatus.ESCALATED 不存在 → 本地 ESCALATED 需 local_to_brain 時處理 """ try: from lewooogo_brain.interfaces.incident_processor import ( IncidentStatus as BrainIncidentStatus, Severity as BrainSeverity, ) # --- 狀態轉換 --- try: local_status = IncidentStatus(brain_incident.status.value) except ValueError: # BrainIncident 無 ESCALATED;理論上不應發生,記錄並降級 logger.warning( "brain_to_local_unknown_status", status=brain_incident.status, incident_id=brain_incident.incident_id, ) local_status = IncidentStatus.INVESTIGATING # --- 嚴重度轉換 --- try: local_severity = Severity(brain_incident.severity.value) except ValueError: logger.warning( "brain_to_local_unknown_severity", severity=brain_incident.severity, incident_id=brain_incident.incident_id, ) local_severity = Severity.P2 # --- Signal 轉換 --- local_signals = [] for brain_signal in brain_incident.signals: try: sig_severity = Severity(brain_signal.severity.value) except ValueError: sig_severity = Severity.P2 # Preserve repo-owned producer provenance across the package # boundary. Unknown values fail closed instead of being mislabeled # as Alertmanager and later poisoning the durable read model. source = parse_signal_source(getattr(brain_signal, "source", "")) local_signals.append( Signal( signal_id=str(uuid4())[:8], # BrainSignal 無 signal_id,自動生成 alert_name=brain_signal.alert_name, severity=sig_severity, source=source, fired_at=brain_signal.fired_at, resolved_at=getattr(brain_signal, "resolved_at", None), labels=getattr(brain_signal, "labels", {}), annotations=getattr(brain_signal, "annotations", {}), fingerprint=getattr(brain_signal, "fingerprint", None), ) ) return Incident( incident_id=brain_incident.incident_id, status=local_status, severity=local_severity, signals=local_signals, affected_services=brain_incident.affected_services or [], decision_chain=None, # BrainIncident 無此欄位 proposal_ids=list(brain_incident.proposal_ids or []), outcome=None, # BrainIncident 無此欄位 frequency_stats=None, # BrainIncident 無此欄位 created_at=brain_incident.created_at, updated_at=brain_incident.updated_at, resolved_at=brain_incident.resolved_at, closed_at=brain_incident.closed_at, ttl_days=7, persisted_to_pg=False, vectorized=False, ) except Exception as e: logger.error( "brain_to_local_conversion_failed", incident_id=getattr(brain_incident, "incident_id", "unknown"), error=str(e), ) raise def local_to_brain(local_incident: Incident) -> Any: """ LocalIncident → BrainIncident 轉換 ADR-046: 將本地完整 Incident 轉換為 lewooogo-brain 精簡格式, 供 DualIncidentMemory 儲存或 IncidentEngine 消費。 LocalIncidentStatus.ESCALATED 無對應 BrainStatus → 映射為 MITIGATING (升級中的事件在 brain 視角仍屬「處置中」) local-only 欄位 (decision_chain, outcome, frequency_stats 等) 在轉換後遺失, 請勿用 local_to_brain → brain_to_local 做 round-trip。 """ try: from lewooogo_brain.interfaces.incident_processor import ( Incident as BrainIncident, IncidentStatus as BrainIncidentStatus, Severity as BrainSeverity, Signal as BrainSignal, ) # --- 狀態轉換 --- _status_map = { "investigating": "investigating", "mitigating": "mitigating", "resolved": "resolved", "closed": "closed", "escalated": "mitigating", # ADR-046: ESCALATED → MITIGATING (brain 無 ESCALATED) } brain_status_value = _status_map.get(local_incident.status.value, "investigating") brain_status = BrainIncidentStatus(brain_status_value) # --- 嚴重度轉換 (1:1 對應) --- brain_severity = BrainSeverity(local_incident.severity.value) # --- Signal 轉換 --- brain_signals = [] for local_signal in local_incident.signals: brain_signals.append( BrainSignal( alert_name=local_signal.alert_name, severity=BrainSeverity(local_signal.severity.value), source=local_signal.source, # str 相容 fired_at=local_signal.fired_at, labels=local_signal.labels, annotations=local_signal.annotations, fingerprint=local_signal.fingerprint, # signal_id 和 resolved_at 是 LocalSignal 擴展欄位,BrainSignal 不存在 ) ) return BrainIncident( incident_id=local_incident.incident_id, status=brain_status, severity=brain_severity, signals=brain_signals, affected_services=local_incident.affected_services, proposal_ids=local_incident.proposal_ids, created_at=local_incident.created_at, updated_at=local_incident.updated_at, resolved_at=local_incident.resolved_at, closed_at=local_incident.closed_at, ) except Exception as e: logger.error( "local_to_brain_conversion_failed", incident_id=local_incident.incident_id, error=str(e), ) raise