實作 ADR-046 Option B: IncidentConverter 轉換層,解決 BrainIncident (lewooogo-brain) 與 LocalIncident (apps/api) 型別邊界問題。 變更: - 新增 src/utils/incident_converter.py - brain_to_local(): BrainIncident → LocalIncident - local_to_brain(): LocalIncident → BrainIncident - ESCALATED → MITIGATING 映射 (brain 無 ESCALATED) - incident_engine.py: 新增 IncidentEngineAdapter 包裝層 - process_signal() / get_incident() 輸出轉換為 LocalIncident - get_incident_engine() 返回 IncidentEngineAdapter - incident_memory.py: 加入 brain_to_local import,更新 _record_to_incident 說明 - ADR-046: 標記三個轉換點全部完成 解鎖: #123 proposal_service.py 清理 (下一步) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
206 lines
7.6 KiB
Python
206 lines
7.6 KiB
Python
"""
|
||
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,
|
||
)
|
||
|
||
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
|
||
|
||
# BrainSignal.source 是 str,LocalSignal.source 是 Literal
|
||
# 使用 getattr 確保不因 BrainSignal 欄位增減而崩潰
|
||
source = getattr(brain_signal, "source", "alertmanager")
|
||
valid_sources = {"prometheus", "signoz", "alertmanager", "manual", "telegram"}
|
||
if source not in valid_sources:
|
||
source = "alertmanager"
|
||
|
||
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
|