From c45f771f24e1b66294e4524e77b71949a481bb8d Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Mon, 8 Jun 2026 01:20:42 +0700 Subject: [PATCH] feat(telemetry): add comprehensive Prometheus metrics and expand dashboard with system, API, ML, and Node.js runtime sections --- apps/api/src/lib/telemetry.ts | 45 ++++ apps/web/src/pages/telemetry-page.tsx | 319 +++++++++++++++++--------- 2 files changed, 251 insertions(+), 113 deletions(-) diff --git a/apps/api/src/lib/telemetry.ts b/apps/api/src/lib/telemetry.ts index cd55a90..95f5dea 100644 --- a/apps/api/src/lib/telemetry.ts +++ b/apps/api/src/lib/telemetry.ts @@ -44,6 +44,14 @@ export const diagnosisCounter = new Counter({ registers: [registry], }); +export const diagnosisUploadSize = new Histogram({ + name: 'zeavis_api_diagnosis_upload_bytes', + help: 'Distribution of uploaded diagnosis image sizes in bytes', + labelNames: ['status'] as const, + buckets: [1024, 51200, 102400, 204800, 512000, 1048576, 2097152, 5242880], + registers: [registry], +}); + export const authCounter = new Counter({ name: 'zeavis_api_auth_operations_total', help: 'Total authentication operations (login, register, refresh)', @@ -51,6 +59,43 @@ export const authCounter = new Counter({ registers: [registry], }); +export const diseaseCatalogRequests = new Counter({ + name: 'zeavis_api_disease_catalog_requests_total', + help: 'Total disease catalog page views', + labelNames: ['type'] as const, // 'list' | 'detail' + registers: [registry], +}); + +export const expertReviewCounter = new Counter({ + name: 'zeavis_api_expert_reviews_total', + help: 'Total expert reviews submitted', + labelNames: ['verdict'] as const, // 'verified' | 'corrected' + registers: [registry], +}); + +export const dbQueryDuration = new Histogram({ + name: 'zeavis_api_db_query_duration_seconds', + help: 'Histogram of database query durations', + labelNames: ['operation'] as const, + buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.25, 0.5, 1], + registers: [registry], +}); + +export const imageModelInferenceDuration = new Histogram({ + name: 'zeavis_api_ml_inference_duration_seconds', + help: 'Histogram of ML model inference durations (via API->ML service)', + labelNames: ['result'] as const, // 'success' | 'failure' + buckets: [0.05, 0.1, 0.25, 0.5, 0.75, 1, 2, 5, 10], + registers: [registry], +}); + +export const mlServiceRequests = new Counter({ + name: 'zeavis_api_ml_service_requests_total', + help: 'Total requests forwarded to ML service', + labelNames: ['status'] as const, // 'success' | 'error' + registers: [registry], +}); + // ── Export ────────────────────────────────────────────── export function getMetricsContentType(): string { diff --git a/apps/web/src/pages/telemetry-page.tsx b/apps/web/src/pages/telemetry-page.tsx index 381b5fd..e146b33 100644 --- a/apps/web/src/pages/telemetry-page.tsx +++ b/apps/web/src/pages/telemetry-page.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { BarChart3, Activity, Cpu, HardDrive, Database, - RefreshCw, Server, + RefreshCw, Server, Wifi, Layers, } from "lucide-react"; import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, @@ -11,10 +11,10 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; // ─── Prometheus API ───────────────────────────────────────────────── const PROM = "https://telemetry.imrnes.team/prometheus/api/v1"; -const INSTANCE = '100.96.248.86:9100'; +const INST = "100.96.248.86"; // orange VPS interface PromValue { - time: number; + time: string; value: number; } @@ -47,71 +47,69 @@ function fmtPct(v: number): string { return v.toFixed(1) + "%"; } -function shortMetric(name: string): string { - return name.replace(/^zeavis_api_/, "").replace(/^zeavis_ml_/, ""); +function fmtBytes(v: number): string { + if (v >= 1 << 30) return (v / (1 << 30)).toFixed(1) + " GiB"; + if (v >= 1 << 20) return (v / (1 << 20)).toFixed(1) + " MiB"; + if (v >= 1 << 10) return (v / (1 << 10)).toFixed(1) + " KiB"; + return v.toFixed(0) + " B"; } -// ─── StatCard ──────────────────────────────────────────────────────── +// ─── UI Components ────────────────────────────────────────────────── function StatCard({ icon: Icon, label, value, sub, color }: { icon: typeof BarChart3; label: string; value: string; sub?: string; color: string; }) { return ( - + {label} - +
{value}
- {sub &&

{sub}

} + {sub &&

{sub}

}
); } -// ─── Chart ────────────────────────────────────────────────────────── -function ChartCard({ title, data, color }: { - title: string; data: PromValue[]; color: string; +function ChartCard({ title, data, color, unit = "", domain }: { + title: string; data: PromValue[]; color: string; unit?: string; domain?: [number, number]; }) { if (!data || data.length === 0) { return ( - + {title} - -
No data
+ +
No data
); } return ( - + {title} - - + + - + - new Date(v).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} - /> - + + new Date(v).toLocaleTimeString()} - formatter={(val: unknown) => [ - typeof val === "number" ? val.toFixed(1) + "%" : String(val ?? ""), title - ]} + formatter={(val: unknown) => [typeof val === "number" ? val.toFixed(2) : String(val ?? ""), title]} /> + fill={`url(#g-${title.replace(/\s+/g, "")})`} strokeWidth={2} /> @@ -119,15 +117,76 @@ function ChartCard({ title, data, color }: { ); } +function GaugeCard({ label, value, max, unit, color }: { + label: string; value: number; max: number; unit: string; color: string; +}) { + const pct = max > 0 ? Math.min((value / max) * 100, 100) : 0; + return ( + + + {label} + + +
+ {typeof value === "number" ? value.toFixed(1) : "?"} + {unit} +
+
+
+
+ + + ); +} + +// ─── Section Header ───────────────────────────────────────────────── +function SectionTitle({ icon: Icon, title, color }: { icon: typeof Layers; title: string; color?: string }) { + return ( +

+ {title} +

+ ); +} + // ─── Main Page ─────────────────────────────────────────────────────── export function TelemetryPage() { + // System 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 [loadNow, setLoadNow] = useState(null); + const [netRx, setNetRx] = useState(null); + const [netTx, setNetTx] = useState(null); + const [netRxData, setNetRxData] = useState([]); + const [netTxData, setNetTxData] = useState([]); + + // ZeaVis API + const [apiReqsTotal, setApiReqsTotal] = useState(null); + const [apiReqsActive, setApiReqsActive] = useState(null); + const [apiLatency, setApiLatency] = useState(null); + const [apiReqsData, setApiReqsData] = useState([]); + const [apiLatencyData, setApiLatencyData] = useState([]); + + // ML + const [mlModelLoaded, setMlModelLoaded] = useState(null); + + // NodeJS + const [heapUsed, setHeapUsed] = useState(null); + const [heapTotal, setHeapTotal] = useState(null); + const [eventLoopLag, setEventLoopLag] = useState(null); + const [activeHandles, setActiveHandles] = useState(null); + const [activeRequests, setActiveRequests] = useState(null); + const [heapData, setHeapData] = useState([]); + const [elLagData, setElLagData] = useState([]); + + // Process + const [procCpu, setProcCpu] = useState(null); + const [procMem, setProcMem] = useState(null); + const [procFds, setProcFds] = useState(null); + const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [refreshing, setRefreshing] = useState(false); @@ -138,37 +197,61 @@ export function TelemetryPage() { if (isRefresh) setRefreshing(true); else setLoading(true); setError(null); - // 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; - })(), + const [ + cpuR, memR, diskR, + cpuN, memN, diskN, loadN, + netRxR, netTxR, netRxN, netTxN, + apiReqsN, apiActiveN, apiLatN, apiReqsR, apiLatR, + mlN, + heapN, heapTN, elN, ahN, arN, heapR, elR, + procCpuN, procMemN, procFdsN, + ] = await Promise.all([ + // Range queries + queryRange(`100 - (avg(rate(node_cpu_seconds_total{mode="idle",instance="${INST}:9100"}[5m])) * 100)`, 60), + queryRange(`(1 - node_memory_MemAvailable_bytes{instance="${INST}:9100"} / node_memory_MemTotal_bytes{instance="${INST}:9100"}) * 100`, 60), + queryRange(`(1 - node_filesystem_avail_bytes{instance="${INST}:9100",mountpoint="/"} / node_filesystem_size_bytes{instance="${INST}:9100",mountpoint="/"}) * 100`, 60), + // Instant queries - system + queryInstant(`100 - (avg(rate(node_cpu_seconds_total{mode="idle",instance="${INST}:9100"}[5m])) * 100)`), + queryInstant(`(1 - node_memory_MemAvailable_bytes{instance="${INST}:9100"} / node_memory_MemTotal_bytes{instance="${INST}:9100"}) * 100`), + queryInstant(`(1 - node_filesystem_avail_bytes{instance="${INST}:9100",mountpoint="/"} / node_filesystem_size_bytes{instance="${INST}:9100",mountpoint="/"}) * 100`), + queryInstant(`node_load15{instance="${INST}:9100"}`), + // Network + queryRange(`rate(node_network_receive_bytes_total{instance="${INST}:9100",device="eth0"}[5m])`, 60), + queryRange(`rate(node_network_transmit_bytes_total{instance="${INST}:9100",device="eth0"}[5m])`, 60), + queryInstant(`rate(node_network_receive_bytes_total{instance="${INST}:9100",device="eth0"}[5m])`), + queryInstant(`rate(node_network_transmit_bytes_total{instance="${INST}:9100",device="eth0"}[5m])`), + // API + queryInstant(`zeavis_api_http_requests_total{instance="${INST}:3000"}`), + queryInstant(`zeavis_api_http_requests_active{instance="${INST}:3000"}`), + queryInstant(`zeavis_api_http_request_duration_seconds_sum{instance="${INST}:3000"} / zeavis_api_http_request_duration_seconds_count{instance="${INST}:3000"}`), + queryRange(`zeavis_api_http_requests_total{instance="${INST}:3000"}`, 60), + queryRange(`zeavis_api_http_request_duration_seconds_sum{instance="${INST}:3000"} / zeavis_api_http_request_duration_seconds_count{instance="${INST}:3000"}`, 60), + // ML + queryInstant(`zeavis_ml_zeavis_ml_model_load_status{instance="${INST}:8000"}`), + // NodeJS + queryInstant(`nodejs_heap_size_used_bytes{instance="${INST}:3000"}`), + queryInstant(`nodejs_heap_size_total_bytes{instance="${INST}:3000"}`), + queryInstant(`nodejs_eventloop_lag_seconds{instance="${INST}:3000"}`), + queryInstant(`nodejs_active_handles_total{instance="${INST}:3000"}`), + queryInstant(`nodejs_active_requests_total{instance="${INST}:3000"}`), + queryRange(`nodejs_heap_size_used_bytes{instance="${INST}:3000"}`, 60), + queryRange(`nodejs_eventloop_lag_seconds{instance="${INST}:3000"}`, 60), + // Process + queryInstant(`rate(process_cpu_seconds_total{instance="${INST}:3000"}[5m])`), + queryInstant(`process_resident_memory_bytes{instance="${INST}:3000"}`), + queryInstant(`process_open_fds{instance="${INST}:3000"}`), ]); - setCpuData(cpu); - setMemData(mem); - setDiskData(disk); - setCpuNow(cpuNowVal); - setMemNow(memNowVal); - setDiskNow(diskNowVal); - setZeavisMetrics(upData); + setCpuData(cpuR); setMemData(memR); setDiskData(diskR); + setCpuNow(cpuN); setMemNow(memN); setDiskNow(diskN); setLoadNow(loadN); + setNetRx(netRxN); setNetTx(netTxN); setNetRxData(netRxR); setNetTxData(netTxR); + setApiReqsTotal(apiReqsN); setApiReqsActive(apiActiveN); setApiLatency(apiLatN); + setApiReqsData(apiReqsR); setApiLatencyData(apiLatR); + setMlModelLoaded(mlN); + setHeapUsed(heapN); setHeapTotal(heapTN); setEventLoopLag(elN); + setActiveHandles(ahN); setActiveRequests(arN); + setHeapData(heapR); setElLagData(elR); + setProcCpu(procCpuN); setProcMem(procMemN); setProcFds(procFdsN); } catch (e) { setError(e instanceof Error ? e.message : "Unknown error"); } finally { @@ -183,14 +266,12 @@ export function TelemetryPage() { return () => clearInterval(intRef.current); }, [fetchAll]); - if (loading && cpuData.length === 0 && memData.length === 0) { + if (loading && cpuNow === null) { return ( -
-
-
-
-

Loading telemetry data...

-
+
+
+
+

Loading telemetry data...

); @@ -202,68 +283,80 @@ export function TelemetryPage() {

- + Telemetry Dashboard

-

- Metrics from Prometheus — orange VPS ({INSTANCE}) - {error && ({error})} +

+ Real-time metrics from Prometheus + {error && (partial — {error})}

- {/* Stat Cards */} -
- - - - + {/* ===== SYSTEM ===== */} + +
+ + + + + + +
+
+ + + + +
+
+ +
- {/* System Charts */} -
- - - + {/* ===== APPLICATION ===== */} + +
+ + + + + +
+
+ + +
- {/* ZeaVis Application Metrics */} - {zeavisMetrics.length > 0 && ( -
-

- ZeaVis Application Metrics -

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

{m.name}

-
-
- ))} -
-
- )} + {/* ===== ML SERVICE ===== */} + +
+ + + + +
- {/* Raw metric names in Prometheus */} - {zeavisMetrics.length === 0 && ( - - - No application metrics available. Prometheus results shown above. - - - )} + {/* ===== NODEJS DETAIL ===== */} + +
+ + + + + + +
+
+ + +
); }