From b03464d0df1e5cdb332fafe7ab11661c0ae13f15 Mon Sep 17 00:00:00 2001 From: Damien Arnodo Date: Fri, 10 Jul 2026 09:45:40 +0000 Subject: [PATCH] Extend weathermap dashboard with BGP/ports tables, per-site throughput, fix node status query Fix node.statusQuery colliding legends: multi-neighbor devices previously sourced from raw per-neighbor BGP session-state series that collide under one legend, so an arbitrary neighbor won. Replace with a combined device-up * worst-BGP-session query per device, with an explicit fallback so zero-BGP devices (access switches) read as neutral/OK. Add site label via Prometheus relabel (derived from device naming convention) instead of per-query label_replace(). Add site/device template variables, BGP sessions table, ports/interfaces table (Grafana merge transform, not PromQL join), and per-site throughput graphs. Refs #49 --- README.md | 5 + configs/grafana/weathermap-dashboard.json | 385 ++++++++++++++++++++-- configs/prometheus/prometheus.yml | 10 + scripts/generate_weathermap.py | 230 ++++++++++++- 4 files changed, 588 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 73b782b..c9ae1ad 100644 --- a/README.md +++ b/README.md @@ -367,6 +367,11 @@ only appears as the sample *value* of `(device, interface, vlan)`. Per-VNI MAC counts require a PromQL join on `vlan` rather than a native `vni` label. +`site` (`dc`/`campus`/`core`) is also not gnmic-exported. It's added as a +Prometheus `metric_relabel_configs` rule deriving it from `device` via the +`-` naming convention regex (see #49), rather than repeating +`label_replace()` in every dashboard query. + - Config: `configs/prometheus/prometheus.yml` - This is separate from, and does not replace, the existing external Prometheus instance — no cutover yet, both run in parallel pending validation diff --git a/configs/grafana/weathermap-dashboard.json b/configs/grafana/weathermap-dashboard.json index 9a848ee..61e3f76 100644 --- a/configs/grafana/weathermap-dashboard.json +++ b/configs/grafana/weathermap-dashboard.json @@ -11,6 +11,50 @@ "timezone": "browser", "schemaVersion": 39, "version": 0, + "templating": { + "list": [ + { + "name": "site", + "type": "query", + "datasource": { + "type": "prometheus", + "uid": "cfrlusac89se8a" + }, + "query": { + "query": "label_values(interfaces_interface_state_oper_status, site)", + "refId": "site" + }, + "refresh": 2, + "sort": 1, + "multi": true, + "includeAll": true, + "current": { + "text": "All", + "value": "$__all" + } + }, + { + "name": "device", + "type": "query", + "datasource": { + "type": "prometheus", + "uid": "cfrlusac89se8a" + }, + "query": { + "query": "label_values(interfaces_interface_state_oper_status{site=~\"$site\"}, device)", + "refId": "device" + }, + "refresh": 2, + "sort": 1, + "multi": true, + "includeAll": true, + "current": { + "text": "All", + "value": "$__all" + } + } + ] + }, "panels": [ { "id": 1, @@ -29,8 +73,8 @@ "targets": [ { "refId": "A", - "expr": "min by (device) (network_instances_network_instance_protocols_protocol_bgp_neighbors_neighbor_state_session_state{network_instance_name=\"default\", device=~\"campus-spine1|dc-spine1|dc-spine2|campus-spine2|campus-border-leaf1|dc-leaf1|campus-border-leaf2|dc-leaf4|dc-leaf3|dc-leaf5|dc-border-leaf1|dc-leaf6|dc-access1|dc-access2|dc-leaf8|dc-leaf7|dc-access4|dc-access3|dc-border-leaf2|dc-leaf2|campus-leaf1|campus-access1|campus-leaf2|campus-leaf3|campus-access2|campus-leaf4|core1|core2\"})", - "legendFormat": "BGP {{device}}", + "expr": "min by (device) (interfaces_interface_state_oper_status{device=~\"campus-spine1|dc-spine1|dc-spine2|campus-spine2|campus-border-leaf1|dc-leaf1|campus-border-leaf2|dc-leaf4|dc-leaf3|dc-leaf5|dc-border-leaf1|dc-leaf6|dc-access1|dc-access2|dc-leaf8|dc-leaf7|dc-access4|dc-access3|dc-border-leaf2|dc-leaf2|campus-leaf1|campus-access1|campus-leaf2|campus-leaf3|campus-access2|campus-leaf4|core1|core2\"}) * (min by (device) (network_instances_network_instance_protocols_protocol_bgp_neighbors_neighbor_state_session_state{network_instance_name=\"default\", device=~\"campus-spine1|dc-spine1|dc-spine2|campus-spine2|campus-border-leaf1|dc-leaf1|campus-border-leaf2|dc-leaf4|dc-leaf3|dc-leaf5|dc-border-leaf1|dc-leaf6|dc-access1|dc-access2|dc-leaf8|dc-leaf7|dc-access4|dc-access3|dc-border-leaf2|dc-leaf2|campus-leaf1|campus-access1|campus-leaf2|campus-leaf3|campus-access2|campus-leaf4|core1|core2\"}) or (min by (device) (interfaces_interface_state_oper_status{device=~\"campus-spine1|dc-spine1|dc-spine2|campus-spine2|campus-border-leaf1|dc-leaf1|campus-border-leaf2|dc-leaf4|dc-leaf3|dc-leaf5|dc-border-leaf1|dc-leaf6|dc-access1|dc-access2|dc-leaf8|dc-leaf7|dc-access4|dc-access3|dc-border-leaf2|dc-leaf2|campus-leaf1|campus-access1|campus-leaf2|campus-leaf3|campus-access2|campus-leaf4|core1|core2\"}) * 0 + 1))", + "legendFormat": "STATUS {{device}}", "datasource": { "type": "prometheus", "uid": "cfrlusac89se8a" @@ -90,7 +134,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP campus-spine1", + "statusQuery": "STATUS campus-spine1", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -146,7 +190,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP dc-spine1", + "statusQuery": "STATUS dc-spine1", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -202,7 +246,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP dc-spine2", + "statusQuery": "STATUS dc-spine2", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -258,7 +302,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP campus-spine2", + "statusQuery": "STATUS campus-spine2", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -314,7 +358,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP campus-border-leaf1", + "statusQuery": "STATUS campus-border-leaf1", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -370,7 +414,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP dc-leaf1", + "statusQuery": "STATUS dc-leaf1", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -433,7 +477,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP campus-border-leaf2", + "statusQuery": "STATUS campus-border-leaf2", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -489,7 +533,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP dc-leaf4", + "statusQuery": "STATUS dc-leaf4", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -545,7 +589,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP dc-leaf3", + "statusQuery": "STATUS dc-leaf3", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -601,7 +645,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP dc-leaf5", + "statusQuery": "STATUS dc-leaf5", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -664,7 +708,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP dc-border-leaf1", + "statusQuery": "STATUS dc-border-leaf1", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -720,7 +764,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP dc-leaf6", + "statusQuery": "STATUS dc-leaf6", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -783,7 +827,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP dc-access1", + "statusQuery": "STATUS dc-access1", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -839,7 +883,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP dc-access2", + "statusQuery": "STATUS dc-access2", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -895,7 +939,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP dc-leaf8", + "statusQuery": "STATUS dc-leaf8", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -951,7 +995,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP dc-leaf7", + "statusQuery": "STATUS dc-leaf7", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -1007,7 +1051,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP dc-access4", + "statusQuery": "STATUS dc-access4", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -1063,7 +1107,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP dc-access3", + "statusQuery": "STATUS dc-access3", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -1119,7 +1163,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP dc-border-leaf2", + "statusQuery": "STATUS dc-border-leaf2", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -1175,7 +1219,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP dc-leaf2", + "statusQuery": "STATUS dc-leaf2", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -1238,7 +1282,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP campus-leaf1", + "statusQuery": "STATUS campus-leaf1", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -1301,7 +1345,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP campus-access1", + "statusQuery": "STATUS campus-access1", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -1357,7 +1401,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP campus-leaf2", + "statusQuery": "STATUS campus-leaf2", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -1420,7 +1464,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP campus-leaf3", + "statusQuery": "STATUS campus-leaf3", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -1483,7 +1527,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP campus-access2", + "statusQuery": "STATUS campus-access2", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -1539,7 +1583,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP campus-leaf4", + "statusQuery": "STATUS campus-leaf4", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -1602,7 +1646,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP core1", + "statusQuery": "STATUS core1", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -1658,7 +1702,7 @@ "statusDown": "#F2495C" }, "nodeIcon": null, - "statusQuery": "BGP core2", + "statusQuery": "STATUS core2", "nodeStatusColorTarget": "border", "statusValueMappings": [ { @@ -3910,6 +3954,287 @@ } } } + }, + { + "id": 2, + "type": "table", + "title": "BGP Sessions", + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 24 + }, + "datasource": { + "type": "prometheus", + "uid": "cfrlusac89se8a" + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "cfrlusac89se8a" + }, + "instant": true, + "format": "table", + "expr": "network_instances_network_instance_protocols_protocol_bgp_neighbors_neighbor_state_session_state{device=~\"$device\"}" + } + ], + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "__name__": true, + "instance": true, + "job": true, + "protocol_identifier": true, + "protocol_name": true, + "subscription_name": true + }, + "renameByName": { + "device": "Device", + "neighbor_address": "Neighbor Address", + "network_instance_name": "VRF", + "Value": "State" + } + } + } + ], + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "Down", + "color": "red" + }, + "1": { + "text": "Up", + "color": "green" + } + } + } + ], + "custom": { + "cellOptions": { + "type": "color-background" + } + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "State" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "color-background" + } + }, + { + "id": "mappings", + "value": [ + { + "type": "value", + "options": { + "0": { + "text": "Down", + "color": "red" + }, + "1": { + "text": "Up", + "color": "green" + } + } + } + ] + } + ] + } + ] + } + }, + { + "id": 3, + "type": "table", + "title": "Ports / Interfaces", + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 24 + }, + "datasource": { + "type": "prometheus", + "uid": "cfrlusac89se8a" + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "cfrlusac89se8a" + }, + "instant": true, + "format": "table", + "expr": "interfaces_interface_state_oper_status{device=~\"$device\"}" + }, + { + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "cfrlusac89se8a" + }, + "instant": true, + "format": "table", + "expr": "rate(interfaces_interface_state_counters_in_octets{device=~\"$device\"}[5m]) * 8" + }, + { + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "cfrlusac89se8a" + }, + "instant": true, + "format": "table", + "expr": "rate(interfaces_interface_state_counters_out_octets{device=~\"$device\"}[5m]) * 8" + } + ], + "transformations": [ + { + "id": "merge", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "__name__": true, + "instance": true, + "job": true, + "subscription_name": true + }, + "renameByName": { + "device": "Device", + "interface": "Interface", + "Value #A": "Status", + "Value #B": "In (bps)", + "Value #C": "Out (bps)" + } + } + } + ], + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Status" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "color-background" + } + }, + { + "id": "mappings", + "value": [ + { + "type": "value", + "options": { + "0": { + "text": "Down", + "color": "red" + }, + "1": { + "text": "Up", + "color": "green" + } + } + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "In (bps)" + }, + "properties": [ + { + "id": "unit", + "value": "bps" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Out (bps)" + }, + "properties": [ + { + "id": "unit", + "value": "bps" + } + ] + } + ] + } + }, + { + "id": 4, + "type": "timeseries", + "title": "Throughput per Site", + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 34 + }, + "datasource": { + "type": "prometheus", + "uid": "cfrlusac89se8a" + }, + "fieldConfig": { + "defaults": { + "unit": "bps" + }, + "overrides": [] + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "cfrlusac89se8a" + }, + "expr": "sum by (site) (rate(interfaces_interface_state_counters_in_octets{site=~\"$site\"}[5m]) * 8)", + "legendFormat": "{{site}} in" + }, + { + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "cfrlusac89se8a" + }, + "expr": "-sum by (site) (rate(interfaces_interface_state_counters_out_octets{site=~\"$site\"}[5m]) * 8)", + "legendFormat": "{{site}} out" + } + ] } ] }, diff --git a/configs/prometheus/prometheus.yml b/configs/prometheus/prometheus.yml index 9adf8e0..837ef16 100644 --- a/configs/prometheus/prometheus.yml +++ b/configs/prometheus/prometheus.yml @@ -16,6 +16,16 @@ scrape_configs: - action: labeldrop regex: source + # site: not exported by gnmic -- derived from the hostname naming + # convention (see CLAUDE.md: dc-*/campus-*/core* regex used for + # IPFabric site separation) here instead of repeating label_replace() + # in every dashboard query, see issue #49 + - source_labels: [device] + target_label: site + regex: (campus|core|dc)-?.* + replacement: $1 + action: replace + # interface: raw label is already "interface_name" (OpenConfig key on the # "interface" path element) — normalize to "interface" for consistency - source_labels: [interface_name] diff --git a/scripts/generate_weathermap.py b/scripts/generate_weathermap.py index 5540da1..994c3a6 100644 --- a/scripts/generate_weathermap.py +++ b/scripts/generate_weathermap.py @@ -265,7 +265,7 @@ def build_weathermap(devices, links, interface_speeds, positions, prometheus_url "padding": {"horizontal": 12, "vertical": 6}, "colors": dict(NODE_COLORS), "nodeIcon": None, - "statusQuery": f"BGP {host}", + "statusQuery": f"STATUS {host}", "nodeStatusColorTarget": "border", "statusValueMappings": [dict(m) for m in STATUS_VALUE_MAPPINGS], "anchors": {a: {"numLinks": anchor_counts[host][a], "numFilledLinks": 0} for a in ANCHOR.values()}, @@ -317,11 +317,26 @@ def build_weathermap(devices, links, interface_speeds, positions, prometheus_url interface_regex = "|".join(sorted(interface_regex_parts)) # -- targets (one query per metric family, per task spec) -- + # + # Node status (refId A): a device is "up" only if at least one interface + # is up AND every BGP session is up, if it has any -- see #49. The old + # "BGP {{device}}" query sourced raw per-neighbor session-state series, + # so a device with multiple neighbors collided on one legend string and + # only one arbitrarily won. `min by (device)` picks the *worst* session + # (0 beats 1), and devices with zero BGP sessions (access switches) are + # not penalized: the `or` fallback substitutes a constant 1 for any + # device present in device-up but absent from the BGP series entirely. + device_up = f'min by (device) (interfaces_interface_state_oper_status{{device=~"{host_regex}"}})' + bgp_worst = ( + f'min by (device) (network_instances_network_instance_protocols_protocol_bgp_neighbors_neighbor_state_session_state' + f'{{network_instance_name="default", device=~"{host_regex}"}})' + ) + bgp_ok_or_not_applicable = f'({bgp_worst} or ({device_up} * 0 + 1))' targets = [ { "refId": "A", - "expr": f'min by (device) (network_instances_network_instance_protocols_protocol_bgp_neighbors_neighbor_state_session_state{{network_instance_name="default", device=~"{host_regex}"}})', - "legendFormat": "BGP {{device}}", + "expr": f'{device_up} * {bgp_ok_or_not_applicable}', + "legendFormat": "STATUS {{device}}", }, { "refId": "B", @@ -378,7 +393,202 @@ def build_weathermap(devices, links, interface_speeds, positions, prometheus_url # Grafana provisioning # -------------------------------------------------------------------------- +def build_templating(datasource_uid): + ds = {"type": "prometheus", "uid": datasource_uid} + return { + "list": [ + { + "name": "site", + "type": "query", + "datasource": ds, + "query": {"query": "label_values(interfaces_interface_state_oper_status, site)", "refId": "site"}, + "refresh": 2, + "sort": 1, + "multi": True, + "includeAll": True, + "current": {"text": "All", "value": "$__all"}, + }, + { + "name": "device", + "type": "query", + "datasource": ds, + # chained: only devices belonging to the selected site(s) + "query": { + "query": 'label_values(interfaces_interface_state_oper_status{site=~"$site"}, device)', + "refId": "device", + }, + "refresh": 2, + "sort": 1, + "multi": True, + "includeAll": True, + "current": {"text": "All", "value": "$__all"}, + }, + ] + } + + +def build_bgp_table_panel(panel_id, datasource_uid, grid_y): + ds = {"type": "prometheus", "uid": datasource_uid} + return { + "id": panel_id, + "type": "table", + "title": "BGP Sessions", + "gridPos": {"h": 10, "w": 12, "x": 0, "y": grid_y}, + "datasource": ds, + "targets": [ + { + "refId": "A", + "datasource": ds, + "instant": True, + "format": "table", + "expr": ( + 'network_instances_network_instance_protocols_protocol_bgp_neighbors_neighbor_state_session_state' + '{device=~"$device"}' + ), + } + ], + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": True, "__name__": True, "instance": True, "job": True, + "protocol_identifier": True, "protocol_name": True, "subscription_name": True, + }, + "renameByName": { + "device": "Device", + "neighbor_address": "Neighbor Address", + "network_instance_name": "VRF", + "Value": "State", + }, + }, + } + ], + "fieldConfig": { + "defaults": { + "mappings": [ + {"type": "value", "options": {"0": {"text": "Down", "color": "red"}, "1": {"text": "Up", "color": "green"}}} + ], + "custom": {"cellOptions": {"type": "color-background"}}, + }, + "overrides": [ + {"matcher": {"id": "byName", "options": "State"}, "properties": [ + {"id": "custom.cellOptions", "value": {"type": "color-background"}}, + {"id": "mappings", "value": [ + {"type": "value", "options": {"0": {"text": "Down", "color": "red"}, "1": {"text": "Up", "color": "green"}}} + ]}, + ]} + ], + }, + } + + +def build_ports_table_panel(panel_id, datasource_uid, grid_y): + ds = {"type": "prometheus", "uid": datasource_uid} + return { + "id": panel_id, + "type": "table", + "title": "Ports / Interfaces", + "gridPos": {"h": 10, "w": 12, "x": 12, "y": grid_y}, + "datasource": ds, + "targets": [ + { + "refId": "A", + "datasource": ds, + "instant": True, + "format": "table", + "expr": 'interfaces_interface_state_oper_status{device=~"$device"}', + }, + { + "refId": "B", + "datasource": ds, + "instant": True, + "format": "table", + "expr": 'rate(interfaces_interface_state_counters_in_octets{device=~"$device"}[5m]) * 8', + }, + { + "refId": "C", + "datasource": ds, + "instant": True, + "format": "table", + "expr": 'rate(interfaces_interface_state_counters_out_octets{device=~"$device"}[5m]) * 8', + }, + ], + # Grafana-side merge (not a PromQL join) -- see #44/#48 VXLAN join + # failure; joining on (device, interface) in PromQL across these + # three metric families is fragile, merging the three table results + # in Grafana on shared field values is not. + "transformations": [ + {"id": "merge", "options": {}}, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": True, "__name__": True, "instance": True, "job": True, "subscription_name": True, + }, + "renameByName": { + "device": "Device", + "interface": "Interface", + "Value #A": "Status", + "Value #B": "In (bps)", + "Value #C": "Out (bps)", + }, + }, + }, + ], + "fieldConfig": { + "defaults": {}, + "overrides": [ + {"matcher": {"id": "byName", "options": "Status"}, "properties": [ + {"id": "custom.cellOptions", "value": {"type": "color-background"}}, + {"id": "mappings", "value": [ + {"type": "value", "options": {"0": {"text": "Down", "color": "red"}, "1": {"text": "Up", "color": "green"}}} + ]}, + ]}, + {"matcher": {"id": "byName", "options": "In (bps)"}, "properties": [{"id": "unit", "value": "bps"}]}, + {"matcher": {"id": "byName", "options": "Out (bps)"}, "properties": [{"id": "unit", "value": "bps"}]}, + ], + }, + } + + +def build_throughput_panel(panel_id, datasource_uid, grid_y): + ds = {"type": "prometheus", "uid": datasource_uid} + return { + "id": panel_id, + "type": "timeseries", + "title": "Throughput per Site", + "gridPos": {"h": 10, "w": 24, "x": 0, "y": grid_y}, + "datasource": ds, + "fieldConfig": {"defaults": {"unit": "bps"}, "overrides": []}, + "targets": [ + { + "refId": "A", + "datasource": ds, + "expr": 'sum by (site) (rate(interfaces_interface_state_counters_in_octets{site=~"$site"}[5m]) * 8)', + "legendFormat": "{{site}} in", + }, + { + "refId": "B", + "datasource": ds, + # negated so in/out render as a signed graph (in above zero, out below) + "expr": '-sum by (site) (rate(interfaces_interface_state_counters_out_octets{site=~"$site"}[5m]) * 8)', + "legendFormat": "{{site}} out", + }, + ], + } + + def build_dashboard(weathermap, targets, dashboard_uid, datasource_uid, plugin_id): + weathermap_panel = { + "id": 1, + "type": plugin_id, + "title": "Fabric Weathermap", + "gridPos": {"h": 24, "w": 24, "x": 0, "y": 0}, + "datasource": {"type": "prometheus", "uid": datasource_uid}, + "targets": [dict(t, datasource={"type": "prometheus", "uid": datasource_uid}) for t in targets], + "options": {"weathermap": weathermap}, + } return { "dashboard": { "uid": dashboard_uid, @@ -387,16 +597,12 @@ def build_dashboard(weathermap, targets, dashboard_uid, datasource_uid, plugin_i "timezone": "browser", "schemaVersion": 39, "version": 0, + "templating": build_templating(datasource_uid), "panels": [ - { - "id": 1, - "type": plugin_id, - "title": "Fabric Weathermap", - "gridPos": {"h": 24, "w": 24, "x": 0, "y": 0}, - "datasource": {"type": "prometheus", "uid": datasource_uid}, - "targets": [dict(t, datasource={"type": "prometheus", "uid": datasource_uid}) for t in targets], - "options": {"weathermap": weathermap}, - } + weathermap_panel, + build_bgp_table_panel(2, datasource_uid, 24), + build_ports_table_panel(3, datasource_uid, 24), + build_throughput_panel(4, datasource_uid, 34), ], }, "overwrite": True,