From b4098050759d2e53007ee7e8cb1d2caaeffcd449 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Tue, 5 May 2026 12:27:53 +0200 Subject: [PATCH 1/4] feat: expose metrics and alerts from Victoria Assisted-by: Copilot:Sonnet4.6 --- src/components/charts/TimeLineChart.vue | 3 +- src/components/standalone/SideMenu.vue | 4 + .../monitoring/metrics/MetricChartCard.vue | 45 +++ .../metrics/MetricsAlertsSection.vue | 225 +++++++++++++ .../metrics/NetworkMetricsSection.vue | 92 ++++++ .../metrics/SystemMetricsSection.vue | 93 ++++++ src/composables/useMetricsCharts.ts | 310 ++++++++++++++++++ src/i18n/en.json | 54 +++ src/i18n/it.json | 54 +++ src/i18n/ta.json | 1 + src/router/index.ts | 5 + .../standalone/monitoring/MetricsView.vue | 285 ++++++++++++++++ .../monitoring/PingLatencyMonitorView.vue | 6 +- 13 files changed, 1173 insertions(+), 4 deletions(-) create mode 100644 src/components/standalone/monitoring/metrics/MetricChartCard.vue create mode 100644 src/components/standalone/monitoring/metrics/MetricsAlertsSection.vue create mode 100644 src/components/standalone/monitoring/metrics/NetworkMetricsSection.vue create mode 100644 src/components/standalone/monitoring/metrics/SystemMetricsSection.vue create mode 100644 src/composables/useMetricsCharts.ts create mode 100644 src/views/standalone/monitoring/MetricsView.vue diff --git a/src/components/charts/TimeLineChart.vue b/src/components/charts/TimeLineChart.vue index 3a7fefd54..94be244ad 100644 --- a/src/components/charts/TimeLineChart.vue +++ b/src/components/charts/TimeLineChart.vue @@ -125,7 +125,8 @@ const defaultOptions: any = { } const allOptions = computed(() => { - return merge(typeof props.options === 'object' ? props.options : {}, defaultOptions) + const customOptions = props.options && typeof props.options === 'object' ? props.options : {} + return merge({}, defaultOptions, customOptions) }) const chartData: any = computed(() => { diff --git a/src/components/standalone/SideMenu.vue b/src/components/standalone/SideMenu.vue index 98584f1b7..0f05a14fb 100644 --- a/src/components/standalone/SideMenu.vue +++ b/src/components/standalone/SideMenu.vue @@ -51,6 +51,10 @@ const navigation: Ref = ref([ { name: 'standalone.ping_latency_monitor.title', to: 'monitoring/ping-latency-monitor' + }, + { + name: 'standalone.metrics.title', + to: 'monitoring/metrics' } ] }, diff --git a/src/components/standalone/monitoring/metrics/MetricChartCard.vue b/src/components/standalone/monitoring/metrics/MetricChartCard.vue new file mode 100644 index 000000000..545f225d5 --- /dev/null +++ b/src/components/standalone/monitoring/metrics/MetricChartCard.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/src/components/standalone/monitoring/metrics/MetricsAlertsSection.vue b/src/components/standalone/monitoring/metrics/MetricsAlertsSection.vue new file mode 100644 index 000000000..9b6810db2 --- /dev/null +++ b/src/components/standalone/monitoring/metrics/MetricsAlertsSection.vue @@ -0,0 +1,225 @@ + + + + + diff --git a/src/components/standalone/monitoring/metrics/NetworkMetricsSection.vue b/src/components/standalone/monitoring/metrics/NetworkMetricsSection.vue new file mode 100644 index 000000000..79f9cb3c2 --- /dev/null +++ b/src/components/standalone/monitoring/metrics/NetworkMetricsSection.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/src/components/standalone/monitoring/metrics/SystemMetricsSection.vue b/src/components/standalone/monitoring/metrics/SystemMetricsSection.vue new file mode 100644 index 000000000..2233de12d --- /dev/null +++ b/src/components/standalone/monitoring/metrics/SystemMetricsSection.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/src/composables/useMetricsCharts.ts b/src/composables/useMetricsCharts.ts new file mode 100644 index 000000000..345a72239 --- /dev/null +++ b/src/composables/useMetricsCharts.ts @@ -0,0 +1,310 @@ +// Copyright (C) 2026 Nethesis S.r.l. +// SPDX-License-Identifier: GPL-3.0-or-later + +import { computed, type Ref } from 'vue' +import { useThemeStore } from '@/stores/theme' +import { byteFormat1024 } from '@nethesis/vue-components' +import { + AMBER_500, + AMBER_600, + CYAN_500, + CYAN_600, + EMERALD_500, + EMERALD_600, + FUCHSIA_500, + FUCHSIA_600, + INDIGO_500, + INDIGO_600, + ROSE_500, + ROSE_600, + VIOLET_500, + VIOLET_600 +} from '@/lib/color' + +// ── Types ──────────────────────────────────────────────────────────────────── + +export interface MetricDataset { + label: string + data: number[] +} + +export interface MetricSeries { + labels: number[] + datasets: MetricDataset[] +} + +export interface TrafficInterface extends MetricSeries { + zone?: string + device?: string +} + +export interface PingHost { + latency: MetricSeries + quality: MetricSeries +} + +export interface MetricsData { + connections: MetricSeries + traffic: Record + cpu: MetricSeries + load: MetricSeries + diskio: MetricSeries + disk: MetricSeries + processes: MetricSeries + memory: MetricSeries + packets: MetricSeries + latency_quality?: Record +} + +export interface ChartData { + labels: number[] + datasets: object[] +} + +// ── Color palette ───────────────────────────────────────────────────────────── + +const PALETTE_LIGHT = [ + CYAN_600, + INDIGO_600, + EMERALD_600, + ROSE_600, + AMBER_600, + VIOLET_600, + FUCHSIA_600 +] +const PALETTE_DARK = [ + CYAN_500, + INDIGO_500, + EMERALD_500, + ROSE_500, + AMBER_500, + VIOLET_500, + FUCHSIA_500 +] + +// ── Composable ──────────────────────────────────────────────────────────────── + +export function useMetricsCharts(metrics: Ref) { + const themeStore = useThemeStore() + + function toMs(labels: number[]): number[] { + return labels.map((s) => s * 1000) + } + + function makeDataset(ds: MetricDataset, color: string) { + return { + label: ds.label, + data: ds.data, + borderColor: color, + backgroundColor: color, + borderWidth: 1.5, + radius: 0, + tension: 0 + } + } + + function buildDatasets( + series: MetricSeries, + colorsLight: string[], + colorsDark: string[] + ): object[] { + const palette = themeStore.isLight ? colorsLight : colorsDark + return series.datasets.map((ds, i) => makeDataset(ds, palette[i % palette.length]!)) + } + + function buildTrafficDatasets(series: MetricSeries): object[] { + return buildDatasets(series, [CYAN_600, INDIGO_600], [CYAN_500, INDIGO_500]) + } + + function buildLatencyDatasets(series: MetricSeries): object[] { + return buildDatasets( + series, + [EMERALD_600, AMBER_600, ROSE_600], + [EMERALD_500, AMBER_500, ROSE_500] + ) + } + + function buildQualityDatasets(series: MetricSeries): object[] { + return buildDatasets(series, [INDIGO_600], [INDIGO_500]) + } + + // ── Chart options (plain objects – they never change) ───────────────────── + + const byteAxisOptions = { + scales: { + y: { ticks: { callback: (value: number) => byteFormat1024(Math.abs(value)) } } + }, + plugins: { + tooltip: { + callbacks: { + label: (ctx: { dataset: { label?: string }; parsed: { y: number } }) => { + const label = ctx.dataset.label ? `${ctx.dataset.label}: ` : '' + return label + byteFormat1024(Math.abs(ctx.parsed.y)) + } + } + } + } + } + + const latencyOptions = { + scales: { + y: { ticks: { callback: (value: number) => `${Math.round(value)} ms` } } + }, + plugins: { + tooltip: { + callbacks: { + label: (ctx: { dataset: { label?: string }; parsed: { y: number } }) => { + const label = ctx.dataset.label ? `${ctx.dataset.label}: ` : '' + return label + `${Math.round(ctx.parsed.y)} ms` + } + } + } + } + } + + const qualityOptions = { + scales: { + y: { + min: 0, + max: 100, + ticks: { callback: (value: number) => `${Math.round(value)}%` } + } + }, + plugins: { + tooltip: { + callbacks: { + label: (ctx: { dataset: { label?: string }; parsed: { y: number } }) => { + const label = ctx.dataset.label ? `${ctx.dataset.label}: ` : '' + return label + `${Math.round(ctx.parsed.y)}%` + } + } + } + } + } + + // ── Computed chart data ─────────────────────────────────────────────────── + + const cpuChart = computed(() => { + if (!metrics.value) return null + const s = metrics.value.cpu + return { labels: toMs(s.labels), datasets: buildDatasets(s, [AMBER_600], [AMBER_500]) } + }) + + const loadChart = computed(() => { + if (!metrics.value) return null + const s = metrics.value.load + return { + labels: toMs(s.labels), + datasets: buildDatasets( + s, + [EMERALD_600, CYAN_600, VIOLET_600], + [EMERALD_500, CYAN_500, VIOLET_500] + ) + } + }) + + const diskioChart = computed(() => { + if (!metrics.value) return null + const s = metrics.value.diskio + return { + labels: toMs(s.labels), + datasets: buildDatasets(s, [CYAN_600, ROSE_600], [CYAN_500, ROSE_500]) + } + }) + + const diskChart = computed(() => { + if (!metrics.value) return null + const s = metrics.value.disk + return { labels: toMs(s.labels), datasets: buildDatasets(s, PALETTE_LIGHT, PALETTE_DARK) } + }) + + const processesChart = computed(() => { + if (!metrics.value) return null + const s = metrics.value.processes + return { labels: toMs(s.labels), datasets: buildDatasets(s, [FUCHSIA_600], [FUCHSIA_500]) } + }) + + const memoryChart = computed(() => { + if (!metrics.value) return null + const s = metrics.value.memory + return { + labels: toMs(s.labels), + datasets: buildDatasets(s, [INDIGO_600, CYAN_600], [INDIGO_500, CYAN_500]) + } + }) + + const trafficInterfaces = computed(() => { + if (!metrics.value) return [] + return Object.keys(metrics.value.traffic).sort() + }) + + function getInterfaceDisplayName(iface: string): string { + if (!metrics.value?.traffic[iface]) return iface + const data = metrics.value.traffic[iface] + if (data.zone && data.device) { + return `${data.zone} (${data.device})` + } + return iface + } + + function getInterfaceTrafficChart(iface: string): ChartData | null { + if (!metrics.value?.traffic[iface]) return null + const s = metrics.value.traffic[iface] + return { labels: toMs(s.labels), datasets: buildTrafficDatasets(s) } + } + + const packetsChart = computed(() => { + if (!metrics.value) return null + const s = metrics.value.packets + return { + labels: toMs(s.labels), + datasets: buildDatasets(s, [CYAN_600, INDIGO_600], [CYAN_500, INDIGO_500]) + } + }) + + const connectionsChart = computed(() => { + if (!metrics.value) return null + const s = metrics.value.connections + return { labels: toMs(s.labels), datasets: buildDatasets(s, [EMERALD_600], [EMERALD_500]) } + }) + + const latencyQuality = computed>(() => { + return metrics.value?.latency_quality ?? {} + }) + + function getLatencyChart(hostData: PingHost): ChartData | null { + if (!hostData.latency?.labels?.length) return null + return { + labels: toMs(hostData.latency.labels), + datasets: buildLatencyDatasets(hostData.latency) + } + } + + function getQualityChart(hostData: PingHost): ChartData | null { + if (!hostData.quality?.labels?.length) return null + return { + labels: toMs(hostData.quality.labels), + datasets: buildQualityDatasets(hostData.quality) + } + } + + return { + cpuChart, + loadChart, + diskioChart, + diskChart, + processesChart, + memoryChart, + trafficInterfaces, + getInterfaceDisplayName, + getInterfaceTrafficChart, + packetsChart, + connectionsChart, + latencyQuality, + getLatencyChart, + getQualityChart, + byteAxisOptions, + latencyOptions, + qualityOptions + } +} diff --git a/src/i18n/en.json b/src/i18n/en.json index b6684ceda..5a616314a 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -212,6 +212,7 @@ "cannot_retrieve_backup_info": "Cannot retrieve backup information", "cannot_retrieve_flashstart_configuration": "Cannot retrieve configuration", "cannot_retrieve_netdata_configuration": "Cannot retrieve configuration", + "cannot_retrieve_telegraf_configuration": "Cannot retrieve Telegraf configuration", "cannot_retrieve_dpi_rules": "Cannot retrieve DPI rules", "cannot_retrieve_dpi_exceptions": "Cannot retrieve DPI exceptions", "cannot_create_dpi_exception": "Cannot create DPI exception", @@ -2448,6 +2449,59 @@ "raw_data": "Raw data", "table_aria_label": "Table of active flows" }, + "metrics": { + "title": "Metrics", + "time_range": "Time range", + "time_range_minutes": "Last {num} minute | Last {num} minutes", + "time_range_hours": "Last {num} hour | Last {num} hours", + "time_range_days": "Last {num} day | Last {num} days", + "refresh_interval": "Auto-refresh", + "refresh_off": "Off", + "refresh_30s": "Every 30 seconds", + "refresh_60s": "Every 60 seconds", + "system_section": "System", + "network_section": "Network", + "connections": "Connections (conntrack)", + "traffic": "Network interface traffic", + "latency": "Latency", + "packet_delivery": "Packet delivery", + "cpu": "CPU usage", + "load": "System load", + "diskio": "Disk I/O", + "disk": "Disk usage (%)", + "processes": "Total processes", + "memory": "RAM usage", + "packets": "Network packets", + "no_data": "No data available for the selected time range", + "cannot_retrieve_metrics": "Cannot retrieve metrics", + "tabs": { + "charts": "Charts", + "alerts": "Alerts" + }, + "alerts_description": "Current pending and firing alerts evaluated by vmalert.", + "alerts_table_aria_label": "Table of current monitoring alerts", + "alert": "Alert", + "state": "State", + "severity": "Severity", + "summary": "Summary", + "active_since": "Active since", + "service": "Service", + "group": "Group", + "no_alerts": "No alerts", + "no_alerts_description": "There are no pending or firing alerts.", + "cannot_retrieve_alerts": "Cannot retrieve alerts", + "firing": "Firing", + "pending": "Pending", + "critical": "Critical", + "warning": "Warning", + "info": "Info", + "unknown": "Unknown" + }, + "metrics_monitor": { + "description": "Historical view of key system metrics collected by Telegraf and stored in VictoriaMetrics. Select a time range to adjust the chart window.", + "refresh": "Refresh", + "vm_unreachable": "VictoriaMetrics is not reachable. Make sure the service is running." + }, "ping_latency_monitor": { "title": "Ping latency monitor", "description": "Measure round-trip time and packet delivery rates by pinging network hosts. Add one or more hosts, including VPN IPs to monitor tunnel quality. Latency and packet delivery charts are available under Monitoring > Real-time monitor > WAN uplinks.", diff --git a/src/i18n/it.json b/src/i18n/it.json index 8a0c11412..90962b1ff 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -189,6 +189,7 @@ "invalid_future_date": "La data non può essere impostata nel passato", "cannot_set_passphrase": "Impossibile impostare passphrase", "cannot_retrieve_netdata_configuration": "Impossibile recuperare la configurazione Netdata", + "cannot_retrieve_telegraf_configuration": "Impossibile recuperare la configurazione Telegraf", "cannot_retrieve_tunnel_options": "Impossibile recuperare le opzioni del tunnel", "invalid_negative_integer": "Inserire un numero intero maggiore o uguale a 0", "cannot_cancel_update_schedule": "Impossibile annullare la programmazione dell'aggiornamento", @@ -2461,6 +2462,59 @@ "monitoring": { "title": "Monitoraggio" }, + "metrics": { + "title": "Metriche", + "time_range": "Intervallo di tempo", + "time_range_minutes": "Ultimo {num} minuto | Ultimi {num} minuti", + "time_range_hours": "Ultima {num} ora | Ultime {num} ore", + "time_range_days": "Ultimo {num} giorno | Ultimi {num} giorni", + "refresh_interval": "Aggiornamento automatico", + "refresh_off": "Disattivato", + "refresh_30s": "Ogni 30 secondi", + "refresh_60s": "Ogni 60 secondi", + "system_section": "Sistema", + "network_section": "Rete", + "connections": "Connessioni (conntrack)", + "traffic": "Traffico delle interfacce di rete", + "latency": "Latenza", + "packet_delivery": "Consegna pacchetti", + "cpu": "Utilizzo CPU", + "load": "Carico di sistema", + "diskio": "I/O disco", + "disk": "Utilizzo disco (%)", + "processes": "Processi totali", + "memory": "Utilizzo RAM", + "packets": "Pacchetti di rete", + "no_data": "Nessun dato disponibile per l'intervallo selezionato", + "cannot_retrieve_metrics": "Impossibile recuperare le metriche", + "tabs": { + "charts": "Grafici", + "alerts": "Avvisi" + }, + "alerts_description": "Elenco corrente degli avvisi in stato pending o firing valutati da vmalert.", + "alerts_table_aria_label": "Tabella degli avvisi di monitoraggio correnti", + "alert": "Avviso", + "state": "Stato", + "severity": "Gravita", + "summary": "Riepilogo", + "active_since": "Attivo da", + "service": "Servizio", + "group": "Gruppo", + "no_alerts": "Nessun avviso", + "no_alerts_description": "Non ci sono avvisi in stato pending o firing.", + "cannot_retrieve_alerts": "Impossibile recuperare gli avvisi", + "firing": "Attivo", + "pending": "In attesa", + "critical": "Critico", + "warning": "Avviso", + "info": "Info", + "unknown": "Sconosciuto" + }, + "metrics_monitor": { + "description": "Vista storica delle metriche principali del sistema raccolte da Telegraf e archiviate in VictoriaMetrics. Seleziona un intervallo di tempo per regolare la finestra dei grafici.", + "refresh": "Aggiorna", + "vm_unreachable": "VictoriaMetrics non e raggiungibile. Verifica che il servizio sia in esecuzione." + }, "netify_informatics": { "title": "Netify Informatics", "netify_informatics_disable_description": "L'invio di metadati è stato disattivato", diff --git a/src/i18n/ta.json b/src/i18n/ta.json index 881499d06..643e85140 100644 --- a/src/i18n/ta.json +++ b/src/i18n/ta.json @@ -133,6 +133,7 @@ "cannot_retrieve_backup_info": "காப்புப் பிரதி தகவலை மீட்டெடுக்க முடியாது", "cannot_retrieve_flashstart_configuration": "உள்ளமைவை மீட்டெடுக்க முடியவில்லை", "cannot_retrieve_netdata_configuration": "உள்ளமைவை மீட்டெடுக்க முடியவில்லை", + "cannot_retrieve_telegraf_configuration": "Telegraf உள்ளமைவை மீட்டெடுக்க முடியவில்லை", "cannot_retrieve_dpi_rules": "DPI விதிகளை மீட்டெடுக்க முடியாது", "cannot_retrieve_dpi_exceptions": "DPI விதிவிலக்குகளை மீட்டெடுக்க முடியாது", "cannot_create_dpi_exception": "DPI விதிவிலக்கை உருவாக்க முடியாது", diff --git a/src/router/index.ts b/src/router/index.ts index d774594ef..6587d01dc 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -196,6 +196,11 @@ const standaloneRoutes = [ name: 'PingLatencyMonitor', component: () => import('../views/standalone/monitoring/PingLatencyMonitorView.vue') }, + { + path: 'monitoring/metrics', + name: 'MetricsMonitor', + component: () => import('../views/standalone/monitoring/MetricsView.vue') + }, { path: 'account', name: 'Account', diff --git a/src/views/standalone/monitoring/MetricsView.vue b/src/views/standalone/monitoring/MetricsView.vue new file mode 100644 index 000000000..e7b9866d8 --- /dev/null +++ b/src/views/standalone/monitoring/MetricsView.vue @@ -0,0 +1,285 @@ + + + + + diff --git a/src/views/standalone/monitoring/PingLatencyMonitorView.vue b/src/views/standalone/monitoring/PingLatencyMonitorView.vue index 00810df3f..ad2d2d85f 100644 --- a/src/views/standalone/monitoring/PingLatencyMonitorView.vue +++ b/src/views/standalone/monitoring/PingLatencyMonitorView.vue @@ -47,12 +47,12 @@ onMounted(() => { async function getConfiguration() { loading.value = true try { - const res = await ubusCall('ns.netdata', 'get-configuration', {}) + const res = await ubusCall('ns.telegraf', 'get-configuration', {}) if (res?.data?.hosts) { formPing.value.hostList = res.data.hosts } } catch (exception: any) { - errorConfiguration.value.notificationTitle = t('error.cannot_retrieve_netdata_configuration') + errorConfiguration.value.notificationTitle = t('error.cannot_retrieve_telegraf_configuration') errorConfiguration.value.notificationDescription = t(getAxiosErrorMessage(exception)) errorConfiguration.value.notificationDetails = exception.toString() } finally { @@ -88,7 +88,7 @@ function save() { hosts: formPing.value.hostList.filter((item) => item) } - ubusCall('ns.netdata', 'set-hosts', payload) + ubusCall('ns.telegraf', 'set-hosts', payload) .then((response) => { if (response?.data?.success && response.data.success) { getConfiguration() From 4c5e147412130e980712bf3d547bf8ec107605de Mon Sep 17 00:00:00 2001 From: Tommaso Bailetti Date: Thu, 7 May 2026 16:17:57 +0200 Subject: [PATCH 2/4] refactor(ping): using new api to fetch and set IPs --- .../monitoring/PingLatencyMonitorView.vue | 175 +++++++----------- 1 file changed, 65 insertions(+), 110 deletions(-) diff --git a/src/views/standalone/monitoring/PingLatencyMonitorView.vue b/src/views/standalone/monitoring/PingLatencyMonitorView.vue index ad2d2d85f..0f42325d7 100644 --- a/src/views/standalone/monitoring/PingLatencyMonitorView.vue +++ b/src/views/standalone/monitoring/PingLatencyMonitorView.vue @@ -3,10 +3,10 @@ SPDX-License-Identifier: GPL-3.0-or-later --> -