fix(api): scope stats report sources by project
All checks were successful
Code Review / ai-code-review (push) Successful in 17s
CD Pipeline / tests (push) Successful in 1m47s
CD Pipeline / build-and-deploy (push) Successful in 5m54s
CD Pipeline / post-deploy-checks (push) Successful in 1m51s

This commit is contained in:
Your Name
2026-06-27 15:51:23 +08:00
parent ee3fb5c005
commit 36951871ca
3 changed files with 157 additions and 30 deletions

View File

@@ -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,