feat(incident): Phase 27 frequency_snapshot DB 持久化 — incidents 表新增 JSONB 欄位
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled

frequency_stats 原僅存 Redis(TTL 35天),Pod 重啟或超期即失
- incidents.frequency_snapshot JSONB:建立 incident 時寫入快照,永久保存
- incident_repository: _record_to_incident 還原 IncidentFrequencyStats
- _incident_to_record_data 序列化 frequency_stats 快照到 DB
- Migration: phase27_incident_frequency_snapshot.sql 已執行完成

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-10 01:05:41 +08:00
parent ae90c36cd7
commit 9846a6cc93
2 changed files with 25 additions and 1 deletions

View File

@@ -565,6 +565,15 @@ class IncidentRecord(Base):
comment="事件結果與人類回饋",
)
# === 頻率快照 (Phase 27, 2026-04-10 ogt) ===
# frequency_stats 原本只存記憶體/Redis(TTL=35天)Pod重啟或超期即失
# 此欄位在 incident 建立時寫入快照,永久保存當時的頻率統計
frequency_snapshot: Mapped[dict[str, Any] | None] = mapped_column(
JSON,
nullable=True,
comment="建立時刻的 AnomalyFrequency 快照,永久保存 (Phase 27)",
)
# === 時間軸 ===
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),

View File

@@ -19,7 +19,7 @@ from sqlalchemy import select
from src.db.base import get_db_context
from src.db.models import IncidentRecord
from src.models.incident import Incident, IncidentStatus, Severity
from src.models.incident import Incident, IncidentFrequencyStats, IncidentStatus, Severity
from src.repositories.interfaces import IIncidentRepository
logger = structlog.get_logger(__name__)
@@ -31,6 +31,14 @@ logger = structlog.get_logger(__name__)
def _record_to_incident(record: IncidentRecord) -> Incident:
"""Convert DB IncidentRecord to Pydantic Incident"""
# Phase 27: 從 DB 快照還原 frequency_stats
frequency_stats = None
if getattr(record, "frequency_snapshot", None):
try:
frequency_stats = IncidentFrequencyStats(**record.frequency_snapshot)
except Exception:
pass # 欄位結構不符時跳過,不影響主流程
return Incident(
incident_id=record.incident_id,
status=IncidentStatus(record.status),
@@ -42,11 +50,17 @@ def _record_to_incident(record: IncidentRecord) -> Incident:
updated_at=record.updated_at,
resolved_at=record.resolved_at,
closed_at=record.closed_at,
frequency_stats=frequency_stats,
)
def _incident_to_record_data(incident: Incident) -> dict[str, Any]:
"""Convert Pydantic Incident to dict for DB record"""
# Phase 27: 序列化 frequency_stats 快照到 DB
frequency_snapshot = None
if incident.frequency_stats:
frequency_snapshot = incident.frequency_stats.model_dump()
return {
"incident_id": incident.incident_id,
"status": incident.status.value,
@@ -58,6 +72,7 @@ def _incident_to_record_data(incident: Incident) -> dict[str, Any]:
"updated_at": incident.updated_at,
"resolved_at": incident.resolved_at,
"closed_at": incident.closed_at,
"frequency_snapshot": frequency_snapshot,
}