([])
+const {
+ latencyAndQualityCharts,
+ loadingLatencyAndQualityReport,
+ errorLatencyAndQualityReport,
+ errorLatencyAndQualityReportDetails
+} = useLatencyAndQualityReport()
+
let loading = ref({
listWans: false,
getMwanReport: false,
@@ -269,6 +282,19 @@ async function getMwanPolicies() {
{{ errorNetworkConfigDetails }}
+
+
+
+ {{ errorLatencyAndQualityReportDetails }}
+
+
@@ -320,6 +346,69 @@ async function getMwanPolicies() {
:device="wan.device"
class="sm:col-span-12 xl:col-span-6 3xl:col-span-4 7xl:col-span-3"
/>
+
+
+
+
+ {
+ router.push(`${getStandaloneRoutePrefix()}/monitoring/ping-latency-monitor`)
+ }
+ "
+ >
+
+
+
+ {{ t('common.go_to_page', { page: t('standalone.ping_latency_monitor.title') }) }}
+
+
+
+
+
+
+
+
+
diff --git a/src/composables/useLatencyAndQualityReport.ts b/src/composables/useLatencyAndQualityReport.ts
new file mode 100644
index 000000000..5fa613f85
--- /dev/null
+++ b/src/composables/useLatencyAndQualityReport.ts
@@ -0,0 +1,174 @@
+// Copyright (C) 2024 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import { onMounted, onUnmounted, ref } from 'vue'
+import { ubusCall } from '@/lib/standalone/ubus'
+import { useI18n } from 'vue-i18n'
+import { getAxiosErrorMessage } from '@nethesis/vue-components'
+import { useThemeStore } from '@/stores/theme'
+import { AMBER_500, AMBER_600, CYAN_500, CYAN_600, EMERALD_500, EMERALD_600 } from '@/lib/color'
+
+export type LatencyOrQualityChart = {
+ pingHost: string
+ type: 'latency' | 'quality'
+ labels: number[] // timestamps
+ datasets: LatencyOrQualityDataset[]
+}
+
+type LatencyAndQualityData = {
+ latency: ChartLabelsAndData
+ quality: ChartLabelsAndData
+}
+
+type ChartLabelsAndData = {
+ labels: string[]
+ data: number[][]
+}
+
+interface LatencyOrQualityDataset {
+ label: string
+ borderColor: string
+ backgroundColor: string
+ data: number[]
+ borderWidth: number
+ radius: number
+}
+
+export function useLatencyAndQualityReport() {
+ // random refresh interval between 20 and 30 seconds
+ const REFRESH_INTERVAL = 20000 + Math.random() * 10 * 1000
+ const { t } = useI18n()
+ const themeStore = useThemeStore()
+ const intervalId = ref(0)
+ const latencyAndQualityCharts = ref([])
+ const loadingLatencyAndQualityReport = ref(true)
+ const errorLatencyAndQualityReport = ref('')
+ const errorLatencyAndQualityReportDetails = ref('')
+
+ onMounted(() => {
+ fetchLatencyAndQualityReport()
+
+ // periodically reload data
+ intervalId.value = setInterval(fetchLatencyAndQualityReport, REFRESH_INTERVAL)
+ })
+
+ onUnmounted(() => {
+ if (intervalId.value) {
+ clearInterval(intervalId.value)
+ }
+ })
+
+ function buildQualityChart(latencyAndQualityData: LatencyAndQualityData, pingHost: string) {
+ // convert timestamp to milliseconds
+ const chartLabels = latencyAndQualityData.quality.data.map((d: number[]) => d[0] * 1000)
+ const qualityData = latencyAndQualityData.quality.data.map((d: number[]) => d[1])
+
+ const chartDatasets = [
+ {
+ label: t('standalone.real_time_monitor.packet_delivery_rate'),
+ borderColor: themeStore.isLight ? CYAN_600 : CYAN_500,
+ backgroundColor: themeStore.isLight ? CYAN_600 : CYAN_500,
+ data: qualityData,
+ borderWidth: 1,
+ radius: 0
+ }
+ ]
+
+ const qualityChart = {
+ pingHost,
+ type: 'quality',
+ labels: chartLabels,
+ datasets: chartDatasets
+ } as LatencyOrQualityChart
+ return qualityChart
+ }
+
+ function buildLatencyChart(latencyAndQualityData: LatencyAndQualityData, pingHost: string) {
+ // convert timestamp to milliseconds
+ const chartLabels = latencyAndQualityData.latency.data.map((d: number[]) => d[0] * 1000)
+ // show latency in milliseconds with one decimal
+ const minLatencyData = latencyAndQualityData.latency.data.map((d: number[]) =>
+ d[1] ? parseFloat(d[1].toFixed(1)) : null
+ )
+ const maxLatencyData = latencyAndQualityData.latency.data.map((d: number[]) =>
+ d[2] ? parseFloat(d[2].toFixed(1)) : null
+ )
+ const avgLatencyData = latencyAndQualityData.latency.data.map((d: number[]) =>
+ d[3] ? parseFloat(d[3].toFixed(1)) : null
+ )
+
+ const chartDatasets = [
+ {
+ label: t('standalone.real_time_monitor.min_latency'),
+ borderColor: themeStore.isLight ? EMERALD_600 : EMERALD_500,
+ backgroundColor: themeStore.isLight ? EMERALD_600 : EMERALD_500,
+ data: minLatencyData,
+ borderWidth: 1,
+ radius: 0
+ },
+ {
+ label: t('standalone.real_time_monitor.max_latency'),
+ borderColor: themeStore.isLight ? AMBER_600 : AMBER_500,
+ backgroundColor: themeStore.isLight ? AMBER_600 : AMBER_500,
+ data: maxLatencyData,
+ borderWidth: 1,
+ radius: 0
+ },
+ {
+ label: t('standalone.real_time_monitor.avg_latency'),
+ borderColor: themeStore.isLight ? CYAN_600 : CYAN_500,
+ backgroundColor: themeStore.isLight ? CYAN_600 : CYAN_500,
+ data: avgLatencyData,
+ borderWidth: 1,
+ radius: 0
+ }
+ ]
+
+ const latencyChart = {
+ pingHost,
+ type: 'latency',
+ labels: chartLabels,
+ datasets: chartDatasets
+ } as LatencyOrQualityChart
+ return latencyChart
+ }
+
+ async function fetchLatencyAndQualityReport() {
+ errorLatencyAndQualityReport.value = ''
+ errorLatencyAndQualityReportDetails.value = ''
+
+ try {
+ const res = await ubusCall('ns.report', 'latency-and-quality-report')
+ latencyAndQualityCharts.value = []
+
+ for (const [pingHost, latencyAndQualityData] of Object.entries(res.data) as [
+ string,
+ LatencyAndQualityData
+ ][]) {
+ if (latencyAndQualityData.latency.data.length > 0) {
+ const latencyChart = buildLatencyChart(latencyAndQualityData, pingHost)
+ latencyAndQualityCharts.value.push(latencyChart)
+ }
+
+ if (latencyAndQualityData.quality.data.length > 0) {
+ const qualityChart = buildQualityChart(latencyAndQualityData, pingHost)
+ latencyAndQualityCharts.value.push(qualityChart)
+ }
+ }
+ } catch (err: any) {
+ console.error(err)
+ errorLatencyAndQualityReport.value = t(getAxiosErrorMessage(err))
+ errorLatencyAndQualityReportDetails.value = err.toString()
+ } finally {
+ loadingLatencyAndQualityReport.value = false
+ }
+ }
+
+ return {
+ latencyAndQualityCharts,
+ fetchLatencyAndQualityReport,
+ loadingLatencyAndQualityReport,
+ errorLatencyAndQualityReport,
+ errorLatencyAndQualityReportDetails
+ }
+}
diff --git a/src/i18n/en/translation.json b/src/i18n/en/translation.json
index 2845535e3..75b0264a0 100644
--- a/src/i18n/en/translation.json
+++ b/src/i18n/en/translation.json
@@ -385,7 +385,8 @@
"cannot_retrieve_client_traffic_by_hour": "Cannot retrieve client traffic by hour",
"cannot_retrieve_talkers_list": "Cannot retrieve talkers list",
"invalid_number": "Invalid number",
- "network_conflict": "This name is already in use by another network interface"
+ "network_conflict": "This name is already in use by another network interface",
+ "cannot_retrieve_latency_and_quality_report": "Cannot retrieve latency and quality report"
},
"ne_text_input": {
"show_password": "Show password",
@@ -1953,11 +1954,19 @@
"no_events_message": "No events, all good",
"view_all_on_grafana": "View all on Grafana",
"no_vpn_network_configured": "No VPN network configured",
- "blocklist": "Blocklist"
+ "blocklist": "Blocklist",
+ "min_latency": "Min latency",
+ "max_latency": "Max latency",
+ "avg_latency": "Avg latency",
+ "packet_delivery_rate": "Packet delivery rate",
+ "ping_host_latency": "Latency to {pingHost}",
+ "ping_host_packet_delivery_rate": "Packet delivery rate to {pingHost}",
+ "latency_and_packet_delivery_rate": "Latency and packet delivery rate",
+ "no_hosts_configured_for_monitoring": "No hosts configured for monitoring"
},
"ping_latency_monitor": {
"title": "Ping latency monitor",
- "description": "Set up the monitoring tool to assess round-trip time and packet loss by sending ping messages to network hosts. This tool is used for monitoring network connectivity quality. You can add one or more hosts to monitor. It's also possible to add IP addresses in a VPN to assess tunnel quality.",
+ "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 > Connectivity.",
"add_host": "Add host",
"host_to_monitor": "Hosts to monitor"
},