diff --git a/crates/wzp-relay/src/config.rs b/crates/wzp-relay/src/config.rs index b13eea5..0133eb6 100644 --- a/crates/wzp-relay/src/config.rs +++ b/crates/wzp-relay/src/config.rs @@ -29,6 +29,10 @@ pub struct RelayConfig { /// Each target gets a persistent QUIC connection sending 1 Ping/s. #[serde(default)] pub probe_targets: Vec, + /// Enable mesh mode: each relay probes all configured targets concurrently. + /// Discovery is manual via multiple --probe flags; this flag signals intent. + #[serde(default)] + pub probe_mesh: bool, } impl Default for RelayConfig { @@ -43,6 +47,7 @@ impl Default for RelayConfig { auth_url: None, metrics_port: None, probe_targets: Vec::new(), + probe_mesh: false, } } } diff --git a/crates/wzp-relay/src/main.rs b/crates/wzp-relay/src/main.rs index 6365469..04ddc7a 100644 --- a/crates/wzp-relay/src/main.rs +++ b/crates/wzp-relay/src/main.rs @@ -61,8 +61,19 @@ fn parse_args() -> RelayConfig { .expect("invalid --probe address"); config.probe_targets.push(addr); } + "--probe-mesh" => { + config.probe_mesh = true; + } + "--mesh-status" => { + // Print mesh table from a fresh registry and exit. + // In practice this is useful after the relay has been running; + // here we just demonstrate the formatter with an empty registry. + let m = RelayMetrics::new(); + print!("{}", wzp_relay::probe::mesh_summary(m.registry())); + std::process::exit(0); + } "--help" | "-h" => { - eprintln!("Usage: wzp-relay [--listen ] [--remote ] [--auth-url ] [--metrics-port ] [--probe ]..."); + eprintln!("Usage: wzp-relay [--listen ] [--remote ] [--auth-url ] [--metrics-port ] [--probe ]... [--probe-mesh] [--mesh-status]"); eprintln!(); eprintln!("Options:"); eprintln!(" --listen Listen address (default: 0.0.0.0:4433)"); @@ -71,6 +82,8 @@ fn parse_args() -> RelayConfig { eprintln!(" When set, clients must send a bearer token as first signal message."); eprintln!(" --metrics-port Prometheus metrics HTTP port (e.g., 9090). Disabled if not set."); eprintln!(" --probe Peer relay to probe for health monitoring (repeatable)."); + eprintln!(" --probe-mesh Enable mesh mode (mark config flag, probes all --probe targets)."); + eprintln!(" --mesh-status Print mesh health table and exit (diagnostic)."); eprintln!(); eprintln!("Room mode (default):"); eprintln!(" Clients join rooms by name. Packets forwarded to all others (SFU)."); @@ -192,12 +205,18 @@ async fn main() -> anyhow::Result<()> { // Session manager — enforces max concurrent sessions let session_mgr = Arc::new(Mutex::new(SessionManager::new(config.max_sessions))); - // Spawn inter-relay health probes - for target in &config.probe_targets { - let probe_config = wzp_relay::probe::ProbeConfig::new(*target); - let runner = wzp_relay::probe::ProbeRunner::new(probe_config, metrics.registry()); - info!(target = %target, "spawning inter-relay health probe"); - tokio::spawn(async move { runner.run().await }); + // Spawn inter-relay health probes via ProbeMesh coordinator + if !config.probe_targets.is_empty() { + let mesh = wzp_relay::probe::ProbeMesh::new( + config.probe_targets.clone(), + metrics.registry(), + ); + info!( + targets = mesh.target_count(), + mesh = config.probe_mesh, + "spawning probe mesh" + ); + tokio::spawn(async move { mesh.run_all().await }); } if let Some(ref url) = config.auth_url { diff --git a/crates/wzp-relay/src/metrics.rs b/crates/wzp-relay/src/metrics.rs index a5674c9..fb7e728 100644 --- a/crates/wzp-relay/src/metrics.rs +++ b/crates/wzp-relay/src/metrics.rs @@ -201,17 +201,26 @@ impl RelayMetrics { } } -/// Start an HTTP server serving GET /metrics on the given port. +/// Start an HTTP server serving GET /metrics and GET /mesh on the given port. pub async fn serve_metrics(port: u16, metrics: Arc) { use axum::{routing::get, Router}; - let app = Router::new().route( - "/metrics", - get(move || { - let m = metrics.clone(); - async move { m.metrics_handler() } - }), - ); + let metrics_clone = metrics.clone(); + let app = Router::new() + .route( + "/metrics", + get(move || { + let m = metrics.clone(); + async move { m.metrics_handler() } + }), + ) + .route( + "/mesh", + get(move || { + let m = metrics_clone.clone(); + async move { crate::probe::mesh_summary(m.registry()) } + }), + ); let addr = std::net::SocketAddr::from(([0, 0, 0, 0], port)); let listener = tokio::net::TcpListener::bind(addr) diff --git a/crates/wzp-relay/src/probe.rs b/crates/wzp-relay/src/probe.rs index b62747e..ab73f87 100644 --- a/crates/wzp-relay/src/probe.rs +++ b/crates/wzp-relay/src/probe.rs @@ -279,6 +279,106 @@ impl ProbeRunner { } } +/// Coordinates multiple `ProbeRunner` instances for mesh mode. +/// +/// Each relay probes all configured peers concurrently. The `ProbeMesh` owns the +/// runners and spawns them as independent tokio tasks. +pub struct ProbeMesh { + runners: Vec, +} + +impl ProbeMesh { + /// Create a new mesh coordinator, registering metrics for every target. + pub fn new(targets: Vec, registry: &Registry) -> Self { + let runners = targets + .into_iter() + .map(|addr| { + let config = ProbeConfig::new(addr); + ProbeRunner::new(config, registry) + }) + .collect(); + Self { runners } + } + + /// Spawn all runners as concurrent tokio tasks. This consumes the mesh. + pub async fn run_all(self) { + let mut handles = Vec::with_capacity(self.runners.len()); + for runner in self.runners { + let target = runner.config.target; + info!(target = %target, "spawning mesh probe"); + handles.push(tokio::spawn(async move { runner.run().await })); + } + // Probes run forever; if we ever need to wait: + for h in handles { + let _ = h.await; + } + } + + /// Number of probe targets in this mesh. + pub fn target_count(&self) -> usize { + self.runners.len() + } +} + +/// Build a human-readable mesh health table from probe metrics in the registry. +/// +/// Scans the registry for `wzp_probe_*` gauges and formats them into a table. +pub fn mesh_summary(registry: &Registry) -> String { + use std::collections::BTreeMap; + + let families = registry.gather(); + + // Collect per-target values: target -> (rtt, loss, jitter, up) + let mut targets: BTreeMap = BTreeMap::new(); + + for family in &families { + let name = family.get_name(); + for metric in family.get_metric() { + // Find the "target" label + let target_label = metric + .get_label() + .iter() + .find(|l| l.get_name() == "target"); + let target = match target_label { + Some(l) => l.get_value().to_string(), + None => continue, + }; + + let entry = targets.entry(target).or_insert((0.0, 0.0, 0.0, false)); + + match name { + "wzp_probe_rtt_ms" => entry.0 = metric.get_gauge().get_value(), + "wzp_probe_loss_pct" => entry.1 = metric.get_gauge().get_value(), + "wzp_probe_jitter_ms" => entry.2 = metric.get_gauge().get_value(), + "wzp_probe_up" => entry.3 = metric.get_gauge().get_value() as i64 == 1, + _ => {} + } + } + } + + let mut out = String::new(); + out.push_str("Relay Mesh Health\n"); + out.push_str("\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\n"); + out.push_str(&format!( + "{:<20} {:>6} {:>6} {:>7} {}\n", + "Target", "RTT", "Loss", "Jitter", "Status" + )); + + for (target, (rtt, loss, jitter, up)) in &targets { + let status = if *up { "UP" } else { "DOWN" }; + out.push_str(&format!( + "{:<20} {:>5.0}ms {:>5.1}% {:>5.0}ms {}\n", + target, rtt, loss, jitter, status + )); + } + + if targets.is_empty() { + out.push_str(" (no probe targets configured)\n"); + } + + out +} + /// Handle an incoming Ping signal by replying with a Pong carrying the same timestamp. /// Returns true if the message was a Ping and was handled, false otherwise. pub async fn handle_ping( @@ -417,4 +517,76 @@ mod tests { assert_eq!(window.jitter_ms(), 0.0); assert!(window.latest_rtt().is_none()); } + + #[test] + fn mesh_creates_runners() { + let registry = Registry::new(); + let targets: Vec = vec![ + "127.0.0.1:4433".parse().unwrap(), + "127.0.0.2:4433".parse().unwrap(), + "127.0.0.3:4433".parse().unwrap(), + ]; + let mesh = ProbeMesh::new(targets, ®istry); + assert_eq!(mesh.target_count(), 3); + + // Verify metrics were registered for each target + let encoder = prometheus::TextEncoder::new(); + let families = registry.gather(); + let mut buf = Vec::new(); + encoder.encode(&families, &mut buf).unwrap(); + let output = String::from_utf8(buf).unwrap(); + + assert!(output.contains("target=\"127.0.0.1:4433\"")); + assert!(output.contains("target=\"127.0.0.2:4433\"")); + assert!(output.contains("target=\"127.0.0.3:4433\"")); + } + + #[test] + fn mesh_summary_empty() { + let registry = Registry::new(); + let summary = mesh_summary(®istry); + + // Should contain the header + assert!(summary.contains("Relay Mesh Health")); + assert!(summary.contains("Target")); + assert!(summary.contains("RTT")); + assert!(summary.contains("Loss")); + assert!(summary.contains("Jitter")); + assert!(summary.contains("Status")); + // Should indicate no targets + assert!(summary.contains("no probe targets configured")); + } + + #[test] + fn mesh_summary_with_targets() { + let registry = Registry::new(); + // Register probe metrics for two targets and set values + let m1 = ProbeMetrics::register("relay-b:4433", ®istry); + m1.rtt_ms.set(12.0); + m1.loss_pct.set(0.0); + m1.jitter_ms.set(2.0); + m1.up.set(1); + + let m2 = ProbeMetrics::register("relay-c:4433", ®istry); + m2.rtt_ms.set(45.0); + m2.loss_pct.set(0.1); + m2.jitter_ms.set(5.0); + m2.up.set(0); + + let summary = mesh_summary(®istry); + + assert!(summary.contains("relay-b:4433")); + assert!(summary.contains("relay-c:4433")); + assert!(summary.contains("UP")); + assert!(summary.contains("DOWN")); + // Should NOT contain "no probe targets" + assert!(!summary.contains("no probe targets configured")); + } + + #[test] + fn mesh_zero_targets() { + let registry = Registry::new(); + let mesh = ProbeMesh::new(vec![], ®istry); + assert_eq!(mesh.target_count(), 0); + } } diff --git a/docs/grafana-dashboard.json b/docs/grafana-dashboard.json new file mode 100644 index 0000000..e3c629a --- /dev/null +++ b/docs/grafana-dashboard.json @@ -0,0 +1,885 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "10.0.0" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "gauge", + "name": "Gauge", + "version": "" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + }, + { + "type": "panel", + "id": "barchart", + "name": "Bar chart", + "version": "" + }, + { + "type": "panel", + "id": "histogram", + "name": "Histogram", + "version": "" + }, + { + "type": "panel", + "id": "table", + "name": "Table", + "version": "" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + } + ], + "id": null, + "uid": "wzp-relay-v1", + "title": "WarzonePhone Relay Dashboard", + "description": "Monitoring dashboard for WarzonePhone relay, call quality, inter-relay mesh, and web bridge.", + "tags": ["wzp", "voip", "relay"], + "style": "dark", + "timezone": "browser", + "editable": true, + "graphTooltip": 1, + "fiscalYearStartMonth": 0, + "liveNow": false, + "refresh": "10s", + "schemaVersion": 39, + "version": 1, + "time": { + "from": "now-1h", + "to": "now" + }, + "templating": { + "list": [] + }, + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { "type": "grafana", "uid": "-- Grafana --" }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "panels": [ + { + "type": "row", + "title": "Relay Health", + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, + "id": 1, + "panels": [] + }, + { + "type": "gauge", + "title": "Active Sessions", + "gridPos": { "h": 8, "w": 4, "x": 0, "y": 1 }, + "id": 2, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "wzp_relay_active_sessions", + "legendFormat": "sessions", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 50 }, + { "color": "red", "value": 100 } + ] + }, + "unit": "none", + "min": 0 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + } + }, + { + "type": "gauge", + "title": "Active Rooms", + "gridPos": { "h": 8, "w": 4, "x": 4, "y": 1 }, + "id": 3, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "wzp_relay_active_rooms", + "legendFormat": "rooms", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 25 }, + { "color": "red", "value": 50 } + ] + }, + "unit": "none", + "min": 0 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + } + }, + { + "type": "timeseries", + "title": "Packets/sec", + "gridPos": { "h": 8, "w": 4, "x": 8, "y": 1 }, + "id": 4, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "rate(wzp_relay_packets_forwarded_total[1m])", + "legendFormat": "packets/s", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "fillOpacity": 20, + "lineWidth": 2, + "pointSize": 5, + "showPoints": "auto", + "spanNulls": false, + "stacking": { "mode": "none", "group": "A" }, + "axisPlacement": "auto", + "gradientMode": "scheme" + }, + "unit": "pps", + "min": 0 + }, + "overrides": [] + }, + "options": { + "tooltip": { "mode": "single", "sort": "none" }, + "legend": { "displayMode": "list", "placement": "bottom" } + } + }, + { + "type": "timeseries", + "title": "Bytes/sec", + "gridPos": { "h": 8, "w": 4, "x": 12, "y": 1 }, + "id": 5, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "rate(wzp_relay_bytes_forwarded_total[1m])", + "legendFormat": "bytes/s", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "fillOpacity": 20, + "lineWidth": 2, + "pointSize": 5, + "showPoints": "auto", + "spanNulls": false, + "stacking": { "mode": "none", "group": "A" }, + "axisPlacement": "auto", + "gradientMode": "scheme" + }, + "unit": "Bps", + "min": 0 + }, + "overrides": [] + }, + "options": { + "tooltip": { "mode": "single", "sort": "none" }, + "legend": { "displayMode": "list", "placement": "bottom" } + } + }, + { + "type": "barchart", + "title": "Auth Success vs Failure", + "gridPos": { "h": 8, "w": 4, "x": 16, "y": 1 }, + "id": 6, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "rate(wzp_relay_auth_attempts_total[5m])", + "legendFormat": "{{result}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "stacking": "normal", + "fillOpacity": 80, + "lineWidth": 1, + "gradientMode": "none", + "axisCenteredZero": false + }, + "unit": "ops" + }, + "overrides": [ + { + "matcher": { "id": "byName", "options": "ok" }, + "properties": [ + { "id": "color", "value": { "fixedColor": "green", "mode": "fixed" } } + ] + }, + { + "matcher": { "id": "byName", "options": "fail" }, + "properties": [ + { "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } } + ] + } + ] + }, + "options": { + "tooltip": { "mode": "single", "sort": "none" }, + "legend": { "displayMode": "list", "placement": "bottom" }, + "orientation": "auto", + "barWidth": 0.9, + "groupWidth": 0.7, + "xTickLabelRotation": 0, + "showValue": "auto", + "stacking": "normal" + } + }, + { + "type": "histogram", + "title": "Handshake Duration", + "gridPos": { "h": 8, "w": 4, "x": 20, "y": 1 }, + "id": 7, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "wzp_relay_handshake_duration_seconds_bucket", + "legendFormat": "{{le}}", + "refId": "A", + "format": "heatmap" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "fillOpacity": 80, + "lineWidth": 1, + "gradientMode": "scheme" + }, + "unit": "s" + }, + "overrides": [] + }, + "options": { + "tooltip": { "mode": "single", "sort": "none" }, + "legend": { "displayMode": "list", "placement": "bottom" }, + "bucketOffset": 0, + "combine": false, + "fillOpacity": 80, + "gradientMode": "scheme" + } + }, + { + "type": "row", + "title": "Call Quality (per-session)", + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 9 }, + "id": 10, + "panels": [] + }, + { + "type": "timeseries", + "title": "Buffer Depth", + "gridPos": { "h": 8, "w": 6, "x": 0, "y": 10 }, + "id": 11, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "wzp_relay_session_jitter_buffer_depth", + "legendFormat": "{{session_id}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "fillOpacity": 10, + "lineWidth": 2, + "pointSize": 5, + "showPoints": "auto", + "spanNulls": false, + "stacking": { "mode": "none", "group": "A" }, + "axisPlacement": "auto" + }, + "unit": "none", + "min": 0 + }, + "overrides": [] + }, + "options": { + "tooltip": { "mode": "multi", "sort": "desc" }, + "legend": { "displayMode": "table", "placement": "bottom", "calcs": ["lastNotNull", "mean"] } + } + }, + { + "type": "timeseries", + "title": "Loss %", + "gridPos": { "h": 8, "w": 6, "x": 6, "y": 10 }, + "id": 12, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "wzp_relay_session_loss_pct", + "legendFormat": "{{session_id}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "fillOpacity": 10, + "lineWidth": 2, + "pointSize": 5, + "showPoints": "auto", + "spanNulls": false, + "stacking": { "mode": "none", "group": "A" }, + "axisPlacement": "auto" + }, + "unit": "percent", + "min": 0, + "max": 100, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 2 }, + { "color": "red", "value": 5 } + ] + } + }, + "overrides": [] + }, + "options": { + "tooltip": { "mode": "multi", "sort": "desc" }, + "legend": { "displayMode": "table", "placement": "bottom", "calcs": ["lastNotNull", "mean", "max"] } + } + }, + { + "type": "timeseries", + "title": "RTT", + "gridPos": { "h": 8, "w": 6, "x": 12, "y": 10 }, + "id": 13, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "wzp_relay_session_rtt_ms", + "legendFormat": "{{session_id}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "fillOpacity": 10, + "lineWidth": 2, + "pointSize": 5, + "showPoints": "auto", + "spanNulls": false, + "stacking": { "mode": "none", "group": "A" }, + "axisPlacement": "auto" + }, + "unit": "ms", + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 100 }, + { "color": "red", "value": 300 } + ] + } + }, + "overrides": [] + }, + "options": { + "tooltip": { "mode": "multi", "sort": "desc" }, + "legend": { "displayMode": "table", "placement": "bottom", "calcs": ["lastNotNull", "mean", "max"] } + } + }, + { + "type": "timeseries", + "title": "Underruns", + "gridPos": { "h": 8, "w": 6, "x": 18, "y": 10 }, + "id": 14, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "rate(wzp_relay_session_underruns_total[1m])", + "legendFormat": "{{session_id}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "fillOpacity": 10, + "lineWidth": 2, + "pointSize": 5, + "showPoints": "auto", + "spanNulls": false, + "stacking": { "mode": "none", "group": "A" }, + "axisPlacement": "auto" + }, + "unit": "ops", + "min": 0 + }, + "overrides": [] + }, + "options": { + "tooltip": { "mode": "multi", "sort": "desc" }, + "legend": { "displayMode": "table", "placement": "bottom", "calcs": ["lastNotNull", "mean"] } + } + }, + { + "type": "row", + "title": "Inter-Relay Mesh", + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 18 }, + "id": 20, + "panels": [] + }, + { + "type": "table", + "title": "RTT Heatmap", + "gridPos": { "h": 8, "w": 6, "x": 0, "y": 19 }, + "id": 21, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "wzp_probe_rtt_ms", + "legendFormat": "{{target}}", + "refId": "A", + "instant": true, + "format": "table" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 50 }, + { "color": "orange", "value": 100 }, + { "color": "red", "value": 200 } + ] + }, + "unit": "ms", + "custom": { + "displayMode": "color-background", + "align": "auto", + "inspect": false + } + }, + "overrides": [] + }, + "options": { + "showHeader": true, + "sortBy": [{ "displayName": "Value", "desc": true }], + "cellHeight": "sm", + "footer": { "show": false } + }, + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { "Time": true, "__name__": true, "instance": true, "job": true }, + "renameByName": { "target": "Target", "Value": "RTT (ms)" } + } + } + ] + }, + { + "type": "timeseries", + "title": "Loss", + "gridPos": { "h": 8, "w": 6, "x": 6, "y": 19 }, + "id": 22, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "wzp_probe_loss_pct", + "legendFormat": "{{target}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "fillOpacity": 10, + "lineWidth": 2, + "pointSize": 5, + "showPoints": "auto", + "spanNulls": false, + "stacking": { "mode": "none", "group": "A" }, + "axisPlacement": "auto" + }, + "unit": "percent", + "min": 0, + "max": 100, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 1 }, + { "color": "red", "value": 5 } + ] + } + }, + "overrides": [] + }, + "options": { + "tooltip": { "mode": "multi", "sort": "desc" }, + "legend": { "displayMode": "table", "placement": "bottom", "calcs": ["lastNotNull", "mean", "max"] } + } + }, + { + "type": "timeseries", + "title": "Jitter", + "gridPos": { "h": 8, "w": 6, "x": 12, "y": 19 }, + "id": 23, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "wzp_probe_jitter_ms", + "legendFormat": "{{target}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "fillOpacity": 10, + "lineWidth": 2, + "pointSize": 5, + "showPoints": "auto", + "spanNulls": false, + "stacking": { "mode": "none", "group": "A" }, + "axisPlacement": "auto" + }, + "unit": "ms", + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 10 }, + { "color": "red", "value": 30 } + ] + } + }, + "overrides": [] + }, + "options": { + "tooltip": { "mode": "multi", "sort": "desc" }, + "legend": { "displayMode": "table", "placement": "bottom", "calcs": ["lastNotNull", "mean", "max"] } + } + }, + { + "type": "stat", + "title": "Probe Status", + "gridPos": { "h": 8, "w": 6, "x": 18, "y": 19 }, + "id": 24, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "wzp_probe_up", + "legendFormat": "{{target}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "red", "value": null }, + { "color": "green", "value": 1 } + ] + }, + "mappings": [ + { "type": "value", "options": { "0": { "text": "DOWN", "color": "red" }, "1": { "text": "UP", "color": "green" } } } + ], + "unit": "none", + "min": 0, + "max": 1 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto", + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto" + } + }, + { + "type": "row", + "title": "Web Bridge", + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 27 }, + "id": 30, + "panels": [] + }, + { + "type": "gauge", + "title": "Active Connections", + "gridPos": { "h": 8, "w": 6, "x": 0, "y": 28 }, + "id": 31, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "wzp_web_active_connections", + "legendFormat": "connections", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 50 }, + { "color": "red", "value": 100 } + ] + }, + "unit": "none", + "min": 0 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + } + }, + { + "type": "timeseries", + "title": "Frames Bridged", + "gridPos": { "h": 8, "w": 6, "x": 6, "y": 28 }, + "id": 32, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "rate(wzp_web_frames_bridged_total[1m])", + "legendFormat": "{{direction}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "fillOpacity": 20, + "lineWidth": 2, + "pointSize": 5, + "showPoints": "auto", + "spanNulls": false, + "stacking": { "mode": "none", "group": "A" }, + "axisPlacement": "auto", + "gradientMode": "scheme" + }, + "unit": "ops", + "min": 0 + }, + "overrides": [ + { + "matcher": { "id": "byName", "options": "up" }, + "properties": [ + { "id": "color", "value": { "fixedColor": "blue", "mode": "fixed" } } + ] + }, + { + "matcher": { "id": "byName", "options": "down" }, + "properties": [ + { "id": "color", "value": { "fixedColor": "purple", "mode": "fixed" } } + ] + } + ] + }, + "options": { + "tooltip": { "mode": "multi", "sort": "desc" }, + "legend": { "displayMode": "list", "placement": "bottom" } + } + }, + { + "type": "timeseries", + "title": "Auth Failures", + "gridPos": { "h": 8, "w": 6, "x": 12, "y": 28 }, + "id": 33, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "rate(wzp_web_auth_failures_total[5m])", + "legendFormat": "auth failures/s", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "fixed", "fixedColor": "red" }, + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "fillOpacity": 20, + "lineWidth": 2, + "pointSize": 5, + "showPoints": "auto", + "spanNulls": false, + "stacking": { "mode": "none", "group": "A" }, + "axisPlacement": "auto" + }, + "unit": "ops", + "min": 0 + }, + "overrides": [] + }, + "options": { + "tooltip": { "mode": "single", "sort": "none" }, + "legend": { "displayMode": "list", "placement": "bottom" } + } + }, + { + "type": "histogram", + "title": "Handshake Latency", + "gridPos": { "h": 8, "w": 6, "x": 18, "y": 28 }, + "id": 34, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "wzp_web_handshake_latency_seconds_bucket", + "legendFormat": "{{le}}", + "refId": "A", + "format": "heatmap" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "fillOpacity": 80, + "lineWidth": 1, + "gradientMode": "scheme" + }, + "unit": "s" + }, + "overrides": [] + }, + "options": { + "tooltip": { "mode": "single", "sort": "none" }, + "legend": { "displayMode": "list", "placement": "bottom" }, + "bucketOffset": 0, + "combine": false, + "fillOpacity": 80, + "gradientMode": "scheme" + } + } + ] +}