feat(metrics): sparklines 串接真實數據 + TOOL_LINKS 移至 API (2026-04-04 ogt)
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 7m6s
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:
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user