From 9846a6cc93f5f6627ee7fccc26c4d974132bab6e Mon Sep 17 00:00:00 2001 From: OG T Date: Fri, 10 Apr 2026 01:05:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(incident):=20Phase=2027=20frequency=5Fsnap?= =?UTF-8?q?shot=20DB=20=E6=8C=81=E4=B9=85=E5=8C=96=20=E2=80=94=20incidents?= =?UTF-8?q?=20=E8=A1=A8=E6=96=B0=E5=A2=9E=20JSONB=20=E6=AC=84=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps/api/src/db/models.py | 9 +++++++++ .../api/src/repositories/incident_repository.py | 17 ++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/apps/api/src/db/models.py b/apps/api/src/db/models.py index 1b3c5b2c..6faabe5d 100644 --- a/apps/api/src/db/models.py +++ b/apps/api/src/db/models.py @@ -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), diff --git a/apps/api/src/repositories/incident_repository.py b/apps/api/src/repositories/incident_repository.py index 52239063..85620339 100644 --- a/apps/api/src/repositories/incident_repository.py +++ b/apps/api/src/repositories/incident_repository.py @@ -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, }