fix(api): scope stats report sources by project
This commit is contained in:
@@ -34,6 +34,7 @@ logger = structlog.get_logger(__name__)
|
||||
|
||||
# 快取 TTL (秒)
|
||||
STATS_CACHE_TTL = 300 # 5 分鐘
|
||||
DEFAULT_STATS_PROJECT_ID = "awoooi"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -50,37 +51,37 @@ class IStatsService(Protocol):
|
||||
"""
|
||||
|
||||
async def get_incident_summary(
|
||||
self, days: int = 30
|
||||
self, days: int = 30, project_id: str | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""取得事件總覽統計"""
|
||||
...
|
||||
|
||||
async def get_resolution_stats(
|
||||
self, days: int = 30
|
||||
self, days: int = 30, project_id: str | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""取得解決時間統計"""
|
||||
...
|
||||
|
||||
async def get_ai_performance(
|
||||
self, days: int = 30
|
||||
self, days: int = 30, project_id: str | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""取得 AI 效能統計"""
|
||||
...
|
||||
|
||||
async def get_affected_services(
|
||||
self, days: int = 30, limit: int = 10
|
||||
self, days: int = 30, limit: int = 10, project_id: str | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
"""取得受影響服務排名"""
|
||||
...
|
||||
|
||||
async def get_incident_trends(
|
||||
self, days: int = 30, period: str = "daily"
|
||||
self, days: int = 30, period: str = "daily", project_id: str | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""取得事件趨勢"""
|
||||
...
|
||||
|
||||
async def get_feedback_summary(
|
||||
self, days: int = 30
|
||||
self, days: int = 30, project_id: str | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""取得人類回饋摘要"""
|
||||
...
|
||||
@@ -98,6 +99,19 @@ class StatsService:
|
||||
封裝統計 API 的快取邏輯與資料庫查詢
|
||||
"""
|
||||
|
||||
def __init__(self, project_id: str = DEFAULT_STATS_PROJECT_ID) -> None:
|
||||
normalized = str(project_id or DEFAULT_STATS_PROJECT_ID).strip()
|
||||
self.project_id = normalized or DEFAULT_STATS_PROJECT_ID
|
||||
|
||||
def _project_id(self, project_id: str | None = None) -> str:
|
||||
normalized = str(project_id or self.project_id or DEFAULT_STATS_PROJECT_ID).strip()
|
||||
return normalized or DEFAULT_STATS_PROJECT_ID
|
||||
|
||||
def _cache_key(self, name: str, *parts: Any, project_id: str | None = None) -> str:
|
||||
suffix = ":".join(str(part) for part in parts if part is not None)
|
||||
base = f"stats:{self._project_id(project_id)}:{name}"
|
||||
return f"{base}:{suffix}" if suffix else base
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 快取相關
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -151,16 +165,21 @@ class StatsService:
|
||||
# 統計查詢 (Phase 17 P1: 從 Router 層遷移)
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
async def get_incident_summary(self, days: int = 30) -> dict[str, Any]:
|
||||
async def get_incident_summary(
|
||||
self,
|
||||
days: int = 30,
|
||||
project_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
取得事件總覽統計
|
||||
|
||||
包含: 總事件數、狀態分佈、嚴重度分佈、解決率
|
||||
"""
|
||||
cache_key = f"stats:incident_summary:{days}"
|
||||
effective_project_id = self._project_id(project_id)
|
||||
cache_key = self._cache_key("incident_summary", days, project_id=effective_project_id)
|
||||
|
||||
async def compute() -> dict[str, Any]:
|
||||
async with get_db_context() as db:
|
||||
async with get_db_context(effective_project_id) as db:
|
||||
since = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
# 總數
|
||||
@@ -215,6 +234,7 @@ class StatsService:
|
||||
|
||||
logger.info(
|
||||
"stats_incident_summary",
|
||||
project_id=effective_project_id,
|
||||
total=total,
|
||||
resolved_rate=resolved_rate,
|
||||
days=days,
|
||||
@@ -230,16 +250,21 @@ class StatsService:
|
||||
|
||||
return await self.get_cached_or_compute(cache_key, compute)
|
||||
|
||||
async def get_resolution_stats(self, days: int = 30) -> dict[str, Any]:
|
||||
async def get_resolution_stats(
|
||||
self,
|
||||
days: int = 30,
|
||||
project_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
取得解決時間統計
|
||||
|
||||
計算: 平均、P50、P95、最快、最慢解決時間
|
||||
"""
|
||||
cache_key = f"stats:resolution:{days}"
|
||||
effective_project_id = self._project_id(project_id)
|
||||
cache_key = self._cache_key("resolution", days, project_id=effective_project_id)
|
||||
|
||||
async def compute() -> dict[str, Any]:
|
||||
async with get_db_context() as db:
|
||||
async with get_db_context(effective_project_id) as db:
|
||||
since = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
result = await db.execute(
|
||||
@@ -293,16 +318,21 @@ class StatsService:
|
||||
|
||||
return await self.get_cached_or_compute(cache_key, compute)
|
||||
|
||||
async def get_ai_performance(self, days: int = 30) -> dict[str, Any]:
|
||||
async def get_ai_performance(
|
||||
self,
|
||||
days: int = 30,
|
||||
project_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
取得 AI 提案效能統計
|
||||
|
||||
評估: 提案執行率、成功率、有效性評分
|
||||
"""
|
||||
cache_key = f"stats:ai_performance:{days}"
|
||||
effective_project_id = self._project_id(project_id)
|
||||
cache_key = self._cache_key("ai_performance", days, project_id=effective_project_id)
|
||||
|
||||
async def compute() -> dict[str, Any]:
|
||||
async with get_db_context() as db:
|
||||
async with get_db_context(effective_project_id) as db:
|
||||
since = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
result = await db.execute(
|
||||
@@ -342,15 +372,24 @@ class StatsService:
|
||||
return await self.get_cached_or_compute(cache_key, compute)
|
||||
|
||||
async def get_affected_services(
|
||||
self, days: int = 30, limit: int = 10
|
||||
self,
|
||||
days: int = 30,
|
||||
limit: int = 10,
|
||||
project_id: str | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
取得最常受影響的服務排名
|
||||
"""
|
||||
cache_key = f"stats:affected_services:{days}:{limit}"
|
||||
effective_project_id = self._project_id(project_id)
|
||||
cache_key = self._cache_key(
|
||||
"affected_services",
|
||||
days,
|
||||
limit,
|
||||
project_id=effective_project_id,
|
||||
)
|
||||
|
||||
async def compute() -> dict[str, Any]:
|
||||
async with get_db_context() as db:
|
||||
async with get_db_context(effective_project_id) as db:
|
||||
since = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
result = await db.execute(
|
||||
@@ -391,15 +430,19 @@ class StatsService:
|
||||
return result.get("services", [])
|
||||
|
||||
async def get_incident_trends(
|
||||
self, days: int = 30, period: str = "daily"
|
||||
self,
|
||||
days: int = 30,
|
||||
period: str = "daily",
|
||||
project_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
取得事件趨勢數據 (SQL GROUP BY 優化版)
|
||||
"""
|
||||
cache_key = f"stats:incident_trends:{days}:{period}"
|
||||
effective_project_id = self._project_id(project_id)
|
||||
cache_key = self._cache_key("incident_trends", days, period, project_id=effective_project_id)
|
||||
|
||||
async def compute() -> dict[str, Any]:
|
||||
async with get_db_context() as db:
|
||||
async with get_db_context(effective_project_id) as db:
|
||||
since = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
trunc_unit = {"daily": "day", "weekly": "week", "monthly": "month"}.get(
|
||||
@@ -430,6 +473,7 @@ class StatsService:
|
||||
|
||||
logger.info(
|
||||
"stats_incident_trends",
|
||||
project_id=effective_project_id,
|
||||
period=period,
|
||||
days=days,
|
||||
data_points=len(trend_data),
|
||||
@@ -439,14 +483,19 @@ class StatsService:
|
||||
|
||||
return await self.get_cached_or_compute(cache_key, compute)
|
||||
|
||||
async def get_feedback_summary(self, days: int = 30) -> dict[str, Any]:
|
||||
async def get_feedback_summary(
|
||||
self,
|
||||
days: int = 30,
|
||||
project_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
取得人類回饋統計
|
||||
"""
|
||||
cache_key = f"stats:feedback_summary:{days}"
|
||||
effective_project_id = self._project_id(project_id)
|
||||
cache_key = self._cache_key("feedback_summary", days, project_id=effective_project_id)
|
||||
|
||||
async def compute() -> dict[str, Any]:
|
||||
async with get_db_context() as db:
|
||||
async with get_db_context(effective_project_id) as db:
|
||||
since = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
result = await db.execute(
|
||||
@@ -500,6 +549,7 @@ class StatsService:
|
||||
|
||||
logger.info(
|
||||
"stats_feedback_summary",
|
||||
project_id=effective_project_id,
|
||||
total=total,
|
||||
positive=positive,
|
||||
negative=negative,
|
||||
|
||||
Reference in New Issue
Block a user