fix(api): 方案 C - Incident 解析相容舊格式 Enum
問題: Redis 存有舊 Enum 值 (status='open', severity='critical')
導致 Pydantic 驗證失敗
解法:
- normalize_status(): 'open' → 'investigating'
- normalize_severity(): 'critical' → 'P0' 等
- 應用於 get_from_working_memory, get_active_incidents, _record_to_incident
優點:
- 零資料風險 (不動 Redis)
- 回滾 = git revert (秒級)
- 新舊格式都能讀
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,52 @@ from src.models.incident import (
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Legacy Value Normalization (方案 C - 代碼相容舊格式)
|
||||
# =============================================================================
|
||||
# 問題: Redis 有舊 Enum 值 (status='open', severity='critical')
|
||||
# 解法: 解析時正規化,不動 Redis 資料
|
||||
# 回滾: git revert (秒級恢復)
|
||||
# =============================================================================
|
||||
|
||||
def normalize_status(value: str) -> str:
|
||||
"""
|
||||
正規化 IncidentStatus 舊格式值
|
||||
|
||||
舊值 → 新值:
|
||||
- 'open' → 'investigating'
|
||||
"""
|
||||
legacy_map = {
|
||||
"open": "investigating",
|
||||
}
|
||||
return legacy_map.get(value, value)
|
||||
|
||||
|
||||
def normalize_severity(value: str) -> str:
|
||||
"""
|
||||
正規化 Severity 舊格式值
|
||||
|
||||
舊值 → 新值:
|
||||
- 'critical' → 'P0'
|
||||
- 'high' → 'P1'
|
||||
- 'warning' → 'P2'
|
||||
- 'medium' → 'P2'
|
||||
- 'info' → 'P3'
|
||||
- 'low' → 'P3'
|
||||
- 'none' → 'P3'
|
||||
"""
|
||||
legacy_map = {
|
||||
"critical": "P0",
|
||||
"high": "P1",
|
||||
"warning": "P2",
|
||||
"medium": "P2",
|
||||
"info": "P3",
|
||||
"low": "P3",
|
||||
"none": "P3",
|
||||
}
|
||||
return legacy_map.get(value, value)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Constants
|
||||
# =============================================================================
|
||||
@@ -110,6 +156,8 @@ class IncidentService:
|
||||
"""
|
||||
從 Working Memory 讀取 Incident
|
||||
|
||||
方案 C: 解析時正規化舊格式 Enum 值
|
||||
|
||||
Returns:
|
||||
Incident | None: 事件資料,若不存在則返回 None
|
||||
"""
|
||||
@@ -121,7 +169,19 @@ class IncidentService:
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
return Incident.model_validate_json(data)
|
||||
# 方案 C: 正規化舊格式 Enum 值
|
||||
incident_dict = json.loads(data)
|
||||
if "status" in incident_dict:
|
||||
incident_dict["status"] = normalize_status(incident_dict["status"])
|
||||
if "severity" in incident_dict:
|
||||
incident_dict["severity"] = normalize_severity(incident_dict["severity"])
|
||||
|
||||
# 同時正規化 signals 內的 severity
|
||||
for signal in incident_dict.get("signals", []):
|
||||
if "severity" in signal:
|
||||
signal["severity"] = normalize_severity(signal["severity"])
|
||||
|
||||
return Incident.model_validate(incident_dict)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
@@ -131,6 +191,74 @@ class IncidentService:
|
||||
)
|
||||
return None
|
||||
|
||||
async def get_active_incidents(self) -> list[Incident]:
|
||||
"""
|
||||
列出所有活躍的 Incidents (從 Working Memory)
|
||||
|
||||
方案 C: 解析時正規化舊格式 Enum 值
|
||||
|
||||
Returns:
|
||||
list[Incident]: 活躍事件列表 (investigating 或 mitigating)
|
||||
"""
|
||||
redis_client = get_redis()
|
||||
incidents: list[Incident] = []
|
||||
|
||||
try:
|
||||
# SCAN 所有 incident:* keys
|
||||
async for key in redis_client.scan_iter(
|
||||
match=f"{INCIDENT_KEY_PREFIX}*",
|
||||
count=100,
|
||||
):
|
||||
# 排除索引 keys
|
||||
if ":idx:" in key:
|
||||
continue
|
||||
|
||||
data = await redis_client.get(key)
|
||||
if data is None:
|
||||
continue
|
||||
|
||||
try:
|
||||
# 方案 C: 正規化舊格式 Enum 值
|
||||
incident_dict = json.loads(data)
|
||||
if "status" in incident_dict:
|
||||
incident_dict["status"] = normalize_status(incident_dict["status"])
|
||||
if "severity" in incident_dict:
|
||||
incident_dict["severity"] = normalize_severity(incident_dict["severity"])
|
||||
|
||||
# 正規化 signals 內的 severity
|
||||
for signal in incident_dict.get("signals", []):
|
||||
if "severity" in signal:
|
||||
signal["severity"] = normalize_severity(signal["severity"])
|
||||
|
||||
incident = Incident.model_validate(incident_dict)
|
||||
|
||||
# 只返回活躍狀態的 Incident
|
||||
if incident.status in (
|
||||
IncidentStatus.INVESTIGATING,
|
||||
IncidentStatus.MITIGATING,
|
||||
):
|
||||
incidents.append(incident)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"incident_parse_error",
|
||||
key=key,
|
||||
error=str(e),
|
||||
)
|
||||
continue
|
||||
|
||||
logger.info(
|
||||
"get_active_incidents",
|
||||
count=len(incidents),
|
||||
)
|
||||
return incidents
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
"get_active_incidents_error",
|
||||
error=str(e),
|
||||
)
|
||||
return []
|
||||
|
||||
# =========================================================================
|
||||
# Episodic Memory (PostgreSQL)
|
||||
# =========================================================================
|
||||
@@ -225,10 +353,21 @@ class IncidentService:
|
||||
return None
|
||||
|
||||
def _record_to_incident(self, record: IncidentRecord) -> Incident:
|
||||
"""將 SQLAlchemy record 轉換為 Pydantic Incident"""
|
||||
"""
|
||||
將 SQLAlchemy record 轉換為 Pydantic Incident
|
||||
|
||||
方案 C: 解析時正規化舊格式 Enum 值
|
||||
"""
|
||||
from src.models.incident import AIDecisionChain, IncidentOutcome
|
||||
|
||||
signals = [Signal(**s) for s in (record.signals or [])]
|
||||
# 方案 C: 正規化 signals 內的舊格式 severity
|
||||
signals = []
|
||||
for s in (record.signals or []):
|
||||
signal_data = s.copy()
|
||||
if "severity" in signal_data:
|
||||
signal_data["severity"] = normalize_severity(signal_data["severity"])
|
||||
signals.append(Signal(**signal_data))
|
||||
|
||||
decision_chain = (
|
||||
AIDecisionChain(**record.decision_chain)
|
||||
if record.decision_chain
|
||||
@@ -240,10 +379,14 @@ class IncidentService:
|
||||
else None
|
||||
)
|
||||
|
||||
# 方案 C: 正規化舊格式 Enum 值
|
||||
normalized_status = normalize_status(record.status)
|
||||
normalized_severity = normalize_severity(record.severity)
|
||||
|
||||
return Incident(
|
||||
incident_id=record.incident_id,
|
||||
status=IncidentStatus(record.status),
|
||||
severity=Severity(record.severity),
|
||||
status=IncidentStatus(normalized_status),
|
||||
severity=Severity(normalized_severity),
|
||||
signals=signals,
|
||||
affected_services=record.affected_services or [],
|
||||
decision_chain=decision_chain,
|
||||
|
||||
Reference in New Issue
Block a user