/** * MetricsPage — Prometheus metrics from the tns-csi controller pod. * Fetches metrics via API proxy and displays in structured cards. */ import { Loader, NameValueTable, SectionBox, SectionHeader, StatusLabel, } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; import React, { useCallback, useEffect, useState } from 'react'; import type { TnsCsiMetrics } from '../api/metrics'; import { fetchControllerMetrics, formatBytes, groupByLabel, sumSamples } from '../api/metrics'; import { useTnsCsiContext } from '../api/TnsCsiDataContext'; function formatAuditTime(iso: string): string { const date = new Date(iso); const diffMs = Date.now() - date.getTime(); const mins = Math.floor(diffMs / 60000); if (mins < 1) return 'just now'; if (mins < 60) return `${mins} minute${mins > 1 ? 's' : ''} ago`; const hours = Math.floor(mins / 60); return `${hours} hour${hours > 1 ? 's' : ''} ago`; } // --------------------------------------------------------------------------- // Metrics cards // --------------------------------------------------------------------------- function WebSocketCard({ metrics }: { metrics: TnsCsiMetrics }) { const connected = metrics.websocketConnected; const reconnects = metrics.websocketReconnectsTotal; return ( {connected === 1 ? 'Connected' : connected === 0 ? 'Disconnected' : 'Unknown'} ), }, { name: 'Total Reconnects', value: reconnects !== null ? String(reconnects) : '—', }, { name: 'Messages Total', value: String(Math.round(sumSamples(metrics.websocketMessagesTotal))), }, ]} /> ); } function VolumeOperationsCard({ metrics }: { metrics: TnsCsiMetrics }) { const byProtocol = groupByLabel(metrics.volumeOperationsTotal, 'protocol'); const totalOps = sumSamples(metrics.volumeOperationsTotal); const totalCapacityBytes = sumSamples(metrics.volumeCapacityBytes); const rows: Array<{ name: string; value: React.ReactNode }> = [ { name: 'Total Operations', value: String(Math.round(totalOps)) }, { name: 'Total Provisioned Capacity', value: formatBytes(totalCapacityBytes) }, ]; for (const [protocol, count] of byProtocol.entries()) { rows.push({ name: `Operations (${protocol})`, value: String(Math.round(count)) }); } return ( ); } function CsiOperationsCard({ metrics }: { metrics: TnsCsiMetrics }) { const byMethod = groupByLabel(metrics.csiOperationsTotal, 'method'); const totalOps = sumSamples(metrics.csiOperationsTotal); const rows: Array<{ name: string; value: React.ReactNode }> = [ { name: 'Total CSI Calls', value: String(Math.round(totalOps)) }, ]; for (const [method, count] of byMethod.entries()) { rows.push({ name: method, value: String(Math.round(count)) }); } return ( ); } // --------------------------------------------------------------------------- // Main component // --------------------------------------------------------------------------- export default function MetricsPage() { const { controllerPods, driverInstalled, loading } = useTnsCsiContext(); const [metrics, setMetrics] = useState(null); const [metricsLoading, setMetricsLoading] = useState(false); const [metricsError, setMetricsError] = useState(null); const [lastUpdated, setLastUpdated] = useState(null); const fetchMetrics = useCallback(async () => { if (controllerPods.length === 0) return; const pod = controllerPods[0]; if (!pod) return; setMetricsLoading(true); setMetricsError(null); try { const result = await fetchControllerMetrics(pod); setMetrics(result); setLastUpdated(new Date().toISOString()); } catch (err: unknown) { setMetricsError(err instanceof Error ? err.message : String(err)); } finally { setMetricsLoading(false); } }, [controllerPods]); useEffect(() => { void fetchMetrics(); }, [fetchMetrics]); if (loading) return ; return ( <> {lastUpdated && ( Updated: {formatAuditTime(lastUpdated)} )} void fetchMetrics()} disabled={metricsLoading} aria-label="Refresh metrics" style={{ padding: '6px 16px', backgroundColor: 'transparent', color: 'var(--mui-palette-primary-main, #1976d2)', border: '1px solid var(--mui-palette-primary-main, #1976d2)', borderRadius: '4px', cursor: metricsLoading ? 'not-allowed' : 'pointer', fontSize: '13px', fontWeight: 500, opacity: metricsLoading ? 0.6 : 1, }} > {metricsLoading ? 'Loading...' : 'Refresh'} {!driverInstalled && ( TNS-CSI driver not found on this cluster ), }, ]} /> )} {controllerPods.length === 0 && driverInstalled && ( No controller pod found, }, { name: 'Note', value: 'Ensure controller pod is running with metrics enabled on port 8080.', }, { name: 'Troubleshooting', value: 'kubectl logs -n kube-system -l app.kubernetes.io/name=tns-csi-driver,app.kubernetes.io/component=controller', }, ]} /> )} {metricsError && ( {metricsError} }, { name: 'Note', value: 'Metrics are fetched via Kubernetes API proxy to the controller pod port 8080.', }, ]} /> )} {metricsLoading && !metrics && } {metrics && ( <> > )} > ); }