Files
awoooi/apps/api/src/utils/incident_converter.py
OG T a94bb57d8b feat(types): ADR-046 IncidentConverter + IncidentEngineAdapter
實作 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>
2026-03-31 22:47:54 +08:00

206 lines
7.6 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.
"""
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 是 strLocalSignal.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