feat(metrics): sparklines 串接真實數據 + TOOL_LINKS 移至 API (2026-04-04 ogt)
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 7m6s

前端 page.tsx:
- 今日事件 sparkline: 過去 6 小時每小時事件數 (從 incidents 計算)
- MTTR sparkline: 各已解決 incident 修復時間序列 (從 incidents 計算)
- 無數據時不顯示 sparkline (undefined 渲染 nothing)
- 移除硬碼 TOOL_LINKS,改讀 API 回傳的 tool.url

後端 monitoring.py:
- 每個 probe 函數回傳 dict 加入 "url" 欄位
- 前端工具連結由後端集中管理,解決多環境問題

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-04 11:09:04 +08:00
parent 5e836bde24
commit 200c382ca4
2 changed files with 45 additions and 31 deletions

View File

@@ -53,12 +53,13 @@ async def _probe_grafana(client: httpx.AsyncClient) -> dict:
"version": version,
"stats": f"面板 {dash_count}" if dash_count is not None else "監控面板",
"description": "指標視覺化 · Dashboard",
"url": base,
}
except Exception as e:
logger.warning("grafana_probe_failed", error=str(e))
return {
"name": "Grafana", "status": "down", "version": None,
"stats": None, "description": "指標視覺化 · Dashboard",
"stats": None, "description": "指標視覺化 · Dashboard", "url": base,
}
@@ -91,12 +92,13 @@ async def _probe_prometheus(client: httpx.AsyncClient) -> dict:
"stats": " · ".join(stats_parts),
"description": "時序資料庫 · 告警規則",
"firing_count": firing_count,
"url": base,
}
except Exception as e:
logger.warning("prometheus_probe_failed", error=str(e))
return {
"name": "Prometheus", "status": "down", "version": None,
"stats": None, "description": "時序資料庫 · 告警規則", "firing_count": 0,
"stats": None, "description": "時序資料庫 · 告警規則", "firing_count": 0, "url": base,
}
@@ -119,12 +121,13 @@ async def _probe_sentry(client: httpx.AsyncClient) -> dict:
"version": version,
"stats": "Error Tracking · Issue",
"description": "錯誤追蹤 · Issue 管理",
"url": base,
}
except Exception as e:
logger.warning("sentry_probe_failed", error=str(e))
return {
"name": "Sentry", "status": "down", "version": None,
"stats": None, "description": "錯誤追蹤 · Issue 管理",
"stats": None, "description": "錯誤追蹤 · Issue 管理", "url": base,
}
@@ -141,12 +144,13 @@ async def _probe_langfuse(client: httpx.AsyncClient) -> dict:
"version": version,
"stats": "LLM Tracing · AI 觀測",
"description": "LLM 追蹤 · AI 成本監控",
"url": base,
}
except Exception as e:
logger.warning("langfuse_probe_failed", error=str(e))
return {
"name": "Langfuse", "status": "down", "version": None,
"stats": None, "description": "LLM 追蹤 · AI 成本監控",
"stats": None, "description": "LLM 追蹤 · AI 成本監控", "url": base,
}
@@ -161,6 +165,7 @@ async def _probe_signoz(client: httpx.AsyncClient) -> dict:
"version": None,
"stats": "APM · Trace · Log",
"description": "可觀測性平台 · OTEL",
"url": base,
}
except Exception as e:
logger.warning("signoz_probe_failed", error=str(e))
@@ -169,13 +174,13 @@ async def _probe_signoz(client: httpx.AsyncClient) -> dict:
if r2.status_code in (200, 301, 302):
return {
"name": "SigNoz", "status": "up", "version": None,
"stats": "APM · Trace · Log", "description": "可觀測性平台 · OTEL",
"stats": "APM · Trace · Log", "description": "可觀測性平台 · OTEL", "url": base,
}
except Exception:
pass
return {
"name": "SigNoz", "status": "down", "version": None,
"stats": None, "description": "可觀測性平台 · OTEL",
"stats": None, "description": "可觀測性平台 · OTEL", "url": base,
}
@@ -192,12 +197,13 @@ async def _probe_gitea(client: httpx.AsyncClient) -> dict:
"version": version,
"stats": "CI/CD · Git · Mirror",
"description": "代碼倉庫 · Pipeline",
"url": base,
}
except Exception as e:
logger.warning("gitea_probe_failed", error=str(e))
return {
"name": "Gitea", "status": "down", "version": None,
"stats": None, "description": "代碼倉庫 · Pipeline",
"stats": None, "description": "代碼倉庫 · Pipeline", "url": base,
}

View File

@@ -76,16 +76,7 @@ interface MonitoringTool {
description: string
firing_count?: number
checked_at: string
}
// 各工具連結(內網,在同網段才能開啟)
const TOOL_LINKS: Record<string, string> = {
Grafana: 'http://192.168.0.110:3002',
Prometheus: 'http://192.168.0.110:9090',
Sentry: 'http://192.168.0.110:9000',
Langfuse: 'http://192.168.0.110:3100',
SigNoz: 'http://192.168.0.188:3301',
Gitea: 'http://192.168.0.110:3001',
url?: string
}
// figma-v2 左側彩色條顏色
@@ -142,7 +133,7 @@ function MonitoringTools() {
const statusText = isUp ? (hasFiring ? `${tool.firing_count} ${tDash('monitoringStatus.firing')}` : tDash('monitoringStatus.up')) : tDash('monitoringStatus.down')
const accentColor = TOOL_ACCENT_COLOR[tool.name] ?? '#b0ad9f'
const emoji = TOOL_EMOJI[tool.name] ?? '🔧'
const link = TOOL_LINKS[tool.name] ?? '#'
const link = tool.url ?? '#'
const timeStr = (() => {
try { return new Date(tool.checked_at).toLocaleTimeString('zh-TW', { timeZone: 'Asia/Taipei', hour: '2-digit', minute: '2-digit' }) }
catch { return '--' }
@@ -413,7 +404,30 @@ export default function Home({ params }: { params: { locale: string } }) {
return { mttrAvg: avgStr, mttrTrend: trend }
})()
// (pulseMetrics reserved for future sparklines)
// 今日事件 sparkline: 過去 6 小時每小時事件數 (真實數據)
const todaySparkValues = (() => {
if (!incidents?.length) return null
const now = Date.now()
const buckets = Array.from({ length: 6 }, (_, i) => {
const start = now - (6 - i) * 3600000
const end = start + 3600000
return incidents.filter(inc => {
const t = new Date(inc.created_at).getTime()
return t >= start && t < end
}).length
})
return buckets.some(v => v > 0) ? buckets : null
})()
// MTTR sparkline: 每筆已解決 incident 的修復時間 (分鐘) 序列 (真實數據)
const mttrSparkValues = (() => {
if (!incidents?.length) return null
const resolved = incidents
.filter(i => i.updated_at && (i.status === 'resolved' || i.status === 'closed'))
.map(i => Math.round((new Date(i.updated_at).getTime() - new Date(i.created_at).getTime()) / 60000))
.filter(m => m > 0)
return resolved.length >= 2 ? resolved.slice(-6) : null
})()
// POD health: healthy services / total
const podHealthStr = totalServices > 0 ? `${healthyServices}/${totalServices}` : '--'
@@ -475,12 +489,9 @@ export default function Home({ params }: { params: { locale: string } }) {
label: tDashboard('todayIncidents'),
value: todayIncidentCount,
trend: todayIncidentCount > 0 ? { text: `${todayIncidentCount > 0 ? Math.max(1, Math.round(todayIncidentCount * 0.2)) : 0}`, color: '#d97757' } : undefined,
extra: (
<svg width="60" height="12" viewBox="0 0 60 12" fill="none">
<polyline points="0,10 10,8 20,9 30,6 40,7 50,4 60,2" stroke="#d97757" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
<circle cx="60" cy="2" r="2.5" fill="#d97757"/>
</svg>
),
extra: todaySparkValues ? (
<MiniSparkline values={todaySparkValues} color="#d97757" />
) : undefined,
},
{
label: tDashboard('autoRemediationRate'),
@@ -499,12 +510,9 @@ export default function Home({ params }: { params: { locale: string } }) {
label: tDashboard('mttrAvg'),
value: mttrAvg,
trend: mttrTrend,
extra: (
<svg width="60" height="12" viewBox="0 0 60 12" fill="none">
<polyline points="0,2 10,3 20,2 30,5 40,4 50,7 60,9" stroke="#22C55E" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
<circle cx="60" cy="9" r="2.5" fill="#22C55E"/>
</svg>
),
extra: mttrSparkValues ? (
<MiniSparkline values={mttrSparkValues} color="#22C55E" />
) : undefined,
},
{
label: tDashboard('podHealth'),