diff --git a/docker/telemetry/grafana/dashboards/ledger-data-sync.json b/docker/telemetry/grafana/dashboards/ledger-data-sync.json index 230a487e96..640b5b464c 100644 --- a/docker/telemetry/grafana/dashboards/ledger-data-sync.json +++ b/docker/telemetry/grafana/dashboards/ledger-data-sync.json @@ -1545,11 +1545,11 @@ "h": 1, "w": 24, "x": 0, - "y": 115 + "y": 121 }, "collapsed": false, "panels": [], - "id": 27 + "id": 38 }, { "title": "Job Queue Backlog and Deferred by Type", @@ -1559,7 +1559,7 @@ "h": 8, "w": 12, "x": 0, - "y": 116 + "y": 122 }, "options": { "tooltip": { @@ -1608,7 +1608,7 @@ "h": 8, "w": 12, "x": 12, - "y": 116 + "y": 122 }, "options": { "tooltip": { @@ -1649,7 +1649,7 @@ "h": 1, "w": 24, "x": 0, - "y": 124 + "y": 130 }, "collapsed": false, "panels": [], @@ -1663,7 +1663,7 @@ "h": 8, "w": 12, "x": 0, - "y": 125 + "y": 131 }, "options": { "tooltip": { @@ -1735,7 +1735,7 @@ "h": 8, "w": 12, "x": 12, - "y": 125 + "y": 131 }, "options": { "tooltip": { @@ -1784,7 +1784,7 @@ "h": 8, "w": 12, "x": 0, - "y": 133 + "y": 139 }, "options": { "tooltip": { @@ -1844,7 +1844,7 @@ "h": 8, "w": 12, "x": 12, - "y": 133 + "y": 139 }, "options": { "tooltip": { @@ -1893,7 +1893,7 @@ "h": 8, "w": 12, "x": 0, - "y": 141 + "y": 147 }, "options": { "tooltip": { @@ -1949,7 +1949,7 @@ "h": 8, "w": 12, "x": 12, - "y": 141 + "y": 147 }, "options": { "tooltip": { @@ -1998,7 +1998,7 @@ "h": 8, "w": 12, "x": 0, - "y": 149 + "y": 155 }, "options": { "tooltip": { diff --git a/docker/telemetry/grafana/dashboards/validate_dashboards.py b/docker/telemetry/grafana/dashboards/validate_dashboards.py index e921d02c4f..d1b17dbbf6 100755 --- a/docker/telemetry/grafana/dashboards/validate_dashboards.py +++ b/docker/telemetry/grafana/dashboards/validate_dashboards.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 -"""Dashboard lint: cumulative metrics must be rate()-wrapped; tier filters present.""" +"""Dashboard lint: cumulative metrics rate()-wrapped; tier filters; sane panel grid.""" import json, re, sys +GRID_COLUMNS = 24 + # Prometheus gauges that hold a CUMULATIVE total -> must be rate()/increase()-wrapped. CUMULATIVE_PREFIXES = ( "total_bytes_", @@ -55,6 +57,55 @@ def iter_panels(dash): yield sub +def check_layout(path, dash): + """Duplicate panel ids and grid collisions -- both break Grafana's loader. + + Grafana keys panels by id when it builds the dashboard model, so two panels + sharing an id make the load non-deterministic. Overlapping gridPos rectangles + have no valid layout. Only top-level panels are checked: panels nested inside + a collapsed row do not occupy the outer grid. + """ + errs = [] + top = dash.get("panels", []) or [] + + seen_ids = {} + for p in top: + pid = p.get("id") + if pid is None: + continue + if pid in seen_ids: + errs.append( + f"{path}: duplicate panel id {pid}: " + f"[{seen_ids[pid]}] and [{p.get('title', '')}]" + ) + else: + seen_ids[pid] = p.get("title", "") + + # Mark every grid cell each panel covers; a second claim on a cell is a collision. + owner = {} + for p in top: + g = p.get("gridPos") or {} + x, y = g.get("x", 0), g.get("y", 0) + w, h = g.get("w", 0), g.get("h", 0) + title = p.get("title", "") + if x + w > GRID_COLUMNS: + errs.append( + f"{path} [{title}]: spans past the {GRID_COLUMNS}-column grid (x={x}, w={w})" + ) + clashed = set() + for cy in range(y, y + h): + for cx in range(x, min(x + w, GRID_COLUMNS)): + prev = owner.get((cy, cx)) + if prev is None: + owner[(cy, cx)] = title + elif prev not in clashed: + clashed.add(prev) + errs.append( + f"{path} [{title}]: grid overlap with [{prev}] at y={cy} x={cx}" + ) + return errs + + def expr_is_wrapped(expr): return ( "rate(" in expr @@ -70,6 +121,7 @@ def check(path, forbid_5m): dash = json.load(open(path)) except Exception as e: return [f"{path}: INVALID JSON: {e}"] + errs += check_layout(path, dash) for p in iter_panels(dash): title = p.get("title", "") for tg in p.get("targets", []) or []: