From b7a3c530a724949e10147256a53c7b979edfb5e7 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Mon, 8 Jun 2026 01:07:10 +0700 Subject: [PATCH] refactor(web): replace custom telemetry API with direct Prometheus queries Rewrite the telemetry page logic to bypass the custom proxy API in favor of direct Prometheus queries. - Implement `queryRange` and `queryInstant` helpers for Prometheus API - Replace `TelemetryAPI` client with direct fetch calls to Prometheus - Update data fetching logic to use standard Prometheus query parameters - Simplify state management and data transformation for charts --- apps/web/src/pages/telemetry-page.tsx | 585 +++++++------------------- 1 file changed, 142 insertions(+), 443 deletions(-) diff --git a/apps/web/src/pages/telemetry-page.tsx b/apps/web/src/pages/telemetry-page.tsx index df6bf3b..381b5fd 100644 --- a/apps/web/src/pages/telemetry-page.tsx +++ b/apps/web/src/pages/telemetry-page.tsx @@ -1,141 +1,64 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { - BarChart3, - Activity, - Cpu, - HardDrive, - Database, - Layers, - RefreshCw, - AlertTriangle, + BarChart3, Activity, Cpu, HardDrive, Database, + RefreshCw, Server, } from "lucide-react"; import { - AreaChart, - Area, - XAxis, - YAxis, - CartesianGrid, - Tooltip, + AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, - BarChart, - Bar, } from "recharts"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -// ─── Types ────────────────────────────────────────────────────────── -const API_BASE = "https://telemetry.imrnes.team/proxy/dashboard"; +// ─── Prometheus API ───────────────────────────────────────────────── +const PROM = "https://telemetry.imrnes.team/prometheus/api/v1"; +const INSTANCE = '100.96.248.86:9100'; -interface DashboardStats { - cpu_usage: number; - disk_usage: number; - total_metrics: number; - active_services: number; - uptime_seconds: number; - health: { disk_readonly: boolean; errors: number }; -} - -interface DiscoveredMetric { - metric_name: string; - service: string; - sample_count: number; - latest_value: number; -} - -interface ChartPoint { - time: string; +interface PromValue { + time: number; value: number; } +async function queryRange(query: string, steps = 60): Promise { + const now = Math.floor(Date.now() / 1000); + const start = now - 3600; + const q = `query=${encodeURIComponent(query)}&start=${start}&end=${now}&step=${steps}`; + const res = await fetch(`${PROM}/query_range?${q}`); + if (!res.ok) throw new Error(`Prometheus: ${res.status}`); + const data = await res.json(); + const results = data?.data?.result ?? []; + if (results.length === 0) return []; + return results[0].values.map((v: [number, string]) => ({ + time: new Date(v[0] * 1000).toISOString(), + value: parseFloat(v[1]), + })); +} + +async function queryInstant(query: string): Promise { + const res = await fetch(`${PROM}/query?query=${encodeURIComponent(query)}`); + if (!res.ok) return null; + const data = await res.json(); + const results = data?.data?.result ?? []; + if (results.length === 0) return null; + return parseFloat(results[0].value[1]); +} + // ─── Helpers ───────────────────────────────────────────────────────── -function fmt(n: number): string { - if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M"; - if (n >= 1_000) return (n / 1_000).toFixed(1) + "K"; - return n.toFixed(1); -} - -function fmtDuration(s: number): string { - const d = Math.floor(s / 86400); - const h = Math.floor((s % 86400) / 3600); - if (d > 0) return `${d}d ${h}h`; - const m = Math.floor((s % 3600) / 60); - if (h > 0) return `${h}h ${m}m`; - return `${m}m`; -} - function fmtPct(v: number): string { - return (v * 100).toFixed(1) + "%"; + return v.toFixed(1) + "%"; } -// ─── API Client ────────────────────────────────────────────────────── -class TelemetryAPI { - private base: string; - constructor(base: string) { - this.base = base; - } - - async stats(): Promise { - const res = await fetch(`${this.base}/stats`); - if (!res.ok) throw new Error(`Stats API: ${res.status}`); - return res.json(); - } - - async discover(): Promise { - const res = await fetch(`${this.base}/discover`); - if (!res.ok) throw new Error(`Discover API: ${res.status}`); - const data = await res.json(); - return data.metrics ?? []; - } - - async charts( - panels: { key: string; metric: string; aggregation?: string }[], - ): Promise> { - const now = Math.floor(Date.now() / 1000); - const oneHourAgo = now - 3600; - const res = await fetch(`${this.base}/charts`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - panels: panels.map((p) => ({ - key: p.key, - metric: p.metric, - start: oneHourAgo, - end: now, - aggregation: p.aggregation ?? "avg", - })), - }), - }); - if (!res.ok) throw new Error(`Charts API: ${res.status}`); - const data = await res.json(); - const map = new Map(); - for (const r of data.results ?? []) { - map.set(r.key, r.data ?? []); - } - return map; - } +function shortMetric(name: string): string { + return name.replace(/^zeavis_api_/, "").replace(/^zeavis_ml_/, ""); } -const api = new TelemetryAPI(API_BASE); - -// ─── Stat Card ─────────────────────────────────────────────────────── -function StatCard({ - icon: Icon, - label, - value, - sub, - color, -}: { - icon: typeof BarChart3; - label: string; - value: string; - sub?: string; - color: string; +// ─── StatCard ──────────────────────────────────────────────────────── +function StatCard({ icon: Icon, label, value, sub, color }: { + icon: typeof BarChart3; label: string; value: string; sub?: string; color: string; }) { return ( - - {label} - + {label} @@ -146,33 +69,10 @@ function StatCard({ ); } -// ─── Metric Card ───────────────────────────────────────────────────── -function MetricChart({ - title, - data, - loading, - color, -}: { - title: string; - data: ChartPoint[]; - loading: boolean; - color: string; +// ─── Chart ────────────────────────────────────────────────────────── +function ChartCard({ title, data, color }: { + title: string; data: PromValue[]; color: string; }) { - if (loading) { - return ( - - - {title} - - -
- Loading... -
-
-
- ); - } - if (!data || data.length === 0) { return ( @@ -180,50 +80,38 @@ function MetricChart({ {title} -
- No data available -
+
No data
); } - return ( {title} - + - + - { - const d = new Date(v); - return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}`; - }} + new Date(v).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} /> - + new Date(v).toLocaleTimeString()} - // eslint-disable-next-line @typescript-eslint/no-explicit-any - formatter={(val: any) => [typeof val === "number" ? val.toFixed(2) : String(val ?? ""), title]} - /> - [ + typeof val === "number" ? val.toFixed(1) + "%" : String(val ?? ""), title + ]} /> + @@ -233,74 +121,54 @@ function MetricChart({ // ─── Main Page ─────────────────────────────────────────────────────── export function TelemetryPage() { - const [stats, setStats] = useState(null); - const [metrics, setMetrics] = useState([]); - const [chartMap, setChartMap] = useState>(new Map()); - const [memChartData, setMemChartData] = useState(null); + const [cpuData, setCpuData] = useState([]); + const [memData, setMemData] = useState([]); + const [diskData, setDiskData] = useState([]); + const [cpuNow, setCpuNow] = useState(null); + const [memNow, setMemNow] = useState(null); + const [diskNow, setDiskNow] = useState(null); + const [zeavisMetrics, setZeavisMetrics] = useState<{ name: string; value: string }[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [refreshing, setRefreshing] = useState(false); - const intervalRef = useRef(undefined); + const intRef = useRef(0); - const fetchData = useCallback(async (isRefresh = false) => { + const fetchAll = useCallback(async (isRefresh = false) => { try { - if (isRefresh) setRefreshing(true); - else setLoading(true); + if (isRefresh) setRefreshing(true); else setLoading(true); setError(null); - const [statsData, discoverData] = await Promise.all([ - api.stats(), - api.discover(), + // Run all queries in parallel + const [cpu, mem, disk, cpuNowVal, memNowVal, diskNowVal, upData] = await Promise.all([ + queryRange(`100 - (avg(rate(node_cpu_seconds_total{mode="idle",instance="${INSTANCE}"}[5m])) * 100)`, 60), + queryRange(`(1 - node_memory_MemAvailable_bytes{instance="${INSTANCE}"} / node_memory_MemTotal_bytes{instance="${INSTANCE}"}) * 100`, 60), + queryRange(`(1 - node_filesystem_avail_bytes{instance="${INSTANCE}",mountpoint="/"} / node_filesystem_size_bytes{instance="${INSTANCE}",mountpoint="/"}) * 100`, 60), + queryInstant(`100 - (avg(rate(node_cpu_seconds_total{mode="idle",instance="${INSTANCE}"}[5m])) * 100)`), + queryInstant(`(1 - node_memory_MemAvailable_bytes{instance="${INSTANCE}"} / node_memory_MemTotal_bytes{instance="${INSTANCE}"}) * 100`), + queryInstant(`(1 - node_filesystem_avail_bytes{instance="${INSTANCE}",mountpoint="/"} / node_filesystem_size_bytes{instance="${INSTANCE}",mountpoint="/"}) * 100`), + // ZeaVis app metrics + (async () => { + const names = [ + "zeavis_api_http_requests_total", + "zeavis_api_http_requests_active", + "zeavis_ml_zeavis_ml_model_load_status", + ]; + const results: { name: string; value: string }[] = []; + for (const n of names) { + const val = await queryInstant(n); + if (val !== null) results.push({ name: n, value: val.toFixed(2) }); + } + return results; + })(), ]); - setStats(statsData); - setMetrics(discoverData); - - // Fetch charts for top metrics (excluding prometheus noise) - const topMetrics = discoverData - .filter((m) => !m.metric_name.startsWith("prometheus_") && m.service !== "prometheus") - .slice(0, 4); - - const chartPanels = topMetrics.map((m) => ({ - key: m.metric_name, - metric: m.metric_name, - })); - - // Built-in computations + app metrics - chartPanels.unshift({ key: "_cpu_usage_pct", metric: "_cpu_usage_pct" }); - chartPanels.unshift({ key: "_disk_usage_pct", metric: "_disk_usage_pct" }); - - // Memory charts (raw values, % computed client-side) - chartPanels.push({ key: "node_memory_MemTotal_bytes", metric: "node_memory_MemTotal_bytes" }); - chartPanels.push({ key: "node_memory_MemAvailable_bytes", metric: "node_memory_MemAvailable_bytes" }); - - // App-specific metrics - for (const appMetric of ["zeavis_api_http_requests_total", "zeavis_api_http_requests_active"]) { - if (discoverData.find((m) => m.metric_name === appMetric)) { - chartPanels.push({ key: appMetric, metric: appMetric }); - } - } - - const charts = await api.charts(chartPanels); - setChartMap(charts); - - // Compute memory usage % = (total - available) / total - const memTotalData = charts.get("node_memory_MemTotal_bytes"); - const memAvailData = charts.get("node_memory_MemAvailable_bytes"); - if (memTotalData && memAvailData && memTotalData.length > 0 && memAvailData.length > 0) { - const merged: ChartPoint[] = []; - for (let i = 0; i < Math.min(memTotalData.length, memAvailData.length); i++) { - const total = memTotalData[i].value; - const avail = memAvailData[i].value; - if (total > 0) { - merged.push({ time: memTotalData[i].time, value: 1 - avail / total }); - } - } - setMemChartData(merged); - } else { - setMemChartData([]); - } - setChartMap(charts); + setCpuData(cpu); + setMemData(mem); + setDiskData(disk); + setCpuNow(cpuNowVal); + setMemNow(memNowVal); + setDiskNow(diskNowVal); + setZeavisMetrics(upData); } catch (e) { setError(e instanceof Error ? e.message : "Unknown error"); } finally { @@ -310,56 +178,18 @@ export function TelemetryPage() { }, []); useEffect(() => { - fetchData(); - intervalRef.current = window.setInterval(() => fetchData(true), 30_000); - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - }; - }, [fetchData]); + fetchAll(); + intRef.current = window.setInterval(() => fetchAll(true), 30_000); + return () => clearInterval(intRef.current); + }, [fetchAll]); - const serviceCounts = useMemo(() => { - const counts = new Map(); - for (const m of metrics) { - counts.set(m.service, (counts.get(m.service) ?? 0) + 1); - } - return Array.from(counts.entries()).sort((a, b) => b[1] - a[1]); - }, [metrics]); - - const topMetrics = useMemo(() => { - return metrics.slice(0, 10); - }, [metrics]); - - if (loading && !stats) { + if (loading && cpuData.length === 0 && memData.length === 0) { return (
-

- Loading telemetry data... -

-
-
-
- ); - } - - if (error && !stats) { - return ( -
-
-
- -

- Failed to load telemetry data -

-

{error}

- +

Loading telemetry data...

@@ -376,195 +206,64 @@ export function TelemetryPage() { Telemetry Dashboard

- System metrics from Prometheus pipeline via ClickHouse. - {error && ( - - (partial data — {error}) - - )} + Metrics from Prometheus — orange VPS ({INSTANCE}) + {error && ({error})}

- {/* Stat Cards */} - {stats && ( -
- - - - 0 - ? `${stats.health.errors} errors` - : "All healthy" - } - color={stats.health.errors > 0 ? "text-red-600" : "text-green-600"} - /> -
- )} - - {/* CPU, Memory, Disk charts */} -
- - - +
+ + + +
- {/* Application Metrics */} - {(chartMap.has("zeavis_api_http_requests_total") || chartMap.has("zeavis_api_http_requests_active")) && ( + {/* System Charts */} +
+ + + +
+ + {/* ZeaVis Application Metrics */} + {zeavisMetrics.length > 0 && (
-

Application Metrics

-
- {chartMap.has("zeavis_api_http_requests_total") && ( - - )} - {chartMap.has("zeavis_api_http_requests_active") && ( - - )} +

+ ZeaVis Application Metrics +

+
+ {zeavisMetrics.map((m) => ( + + + + {shortMetric(m.name)} + + + +
{m.value}
+

{m.name}

+
+
+ ))}
)} - {/* Top Metrics */} - - - - - Top Metrics - - - - {topMetrics.length === 0 ? ( -
- No metrics discovered yet -
- ) : ( -
- - - - - - - - - - - {topMetrics.map((m) => ( - - - - - - - ))} - -
- Metric - - Service - - Samples - - Latest Value -
- {m.metric_name} - - - {m.service} - - - {fmt(m.sample_count)} - - {typeof m.latest_value === "number" - ? m.latest_value.toFixed(4) - : String(m.latest_value)} -
-
- )} -
-
- - {/* Service Distribution */} - - - - - Services - - - - {serviceCounts.length === 0 ? ( -
- No services discovered -
- ) : ( - - ({ name, count }))}> - - - - - - - - )} -
-
+ {/* Raw metric names in Prometheus */} + {zeavisMetrics.length === 0 && ( + + + No application metrics available. Prometheus results shown above. + + + )}
); }