Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ repos:
language: system
files: ^(\.mise\.toml|\.devcontainer/devcontainer\.json|services/frontend/\.nvmrc|services/.*/Dockerfile|loadgen/Dockerfile|services/backend/pyproject\.toml|.*/gradle-wrapper\.properties)$
pass_filenames: false
# Dashboard identity gate: a provisioned dashboard without a pinned uid has
# no durable identity, so a UI save detaches it and the next restart
# provisions a duplicate beside it
- repo: local
hooks:
- id: dashboard-uids
name: Grafana dashboards have stable uids
entry: bash scripts/check-dashboard-uids.sh
language: system
files: ^observability/grafana/dashboards/.*\.json$
pass_filenames: false
# Secret scanning (ADR-0011)
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
Expand Down
1 change: 1 addition & 0 deletions observability/grafana/dashboards/analytics.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"uid": "analytics-ingest",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"annotations": {
"list": [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"uid": "devops-demo",
"annotations": { "list": [{ "builtIn": 1, "datasource": "-- Grafana --", "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "type": "dashboard" }] },
"editable": true,
"fiscalYearStartMonth": 1,
Expand Down
1 change: 1 addition & 0 deletions observability/grafana/dashboards/history.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"uid": "analytics-history",
"annotations": {
"list": [
{
Expand Down
1 change: 1 addition & 0 deletions observability/grafana/dashboards/load.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"uid": "load-k6",
"annotations": { "list": [{ "builtIn": 1, "datasource": "-- Grafana --", "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "type": "dashboard" }] },
"editable": true,
"fiscalYearStartMonth": 1,
Expand Down
1 change: 1 addition & 0 deletions observability/grafana/dashboards/monitoring-layers.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"uid": "monitoring-layers",
"annotations": { "list": [{ "builtIn": 1, "datasource": "-- Grafana --", "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "type": "dashboard" }] },
"editable": true,
"fiscalYearStartMonth": 1,
Expand Down
1 change: 1 addition & 0 deletions observability/grafana/dashboards/reports-ui.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"uid": "reports-ui",
"annotations": {
"list": [
{
Expand Down
1 change: 1 addition & 0 deletions observability/grafana/dashboards/reports.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"uid": "reports-jvm",
"annotations": {
"list": [
{
Expand Down
63 changes: 63 additions & 0 deletions scripts/check-dashboard-uids.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# Dashboard identity gate: every provisioned dashboard carries a stable uid.
#
# Grafana's file provider identifies a dashboard by uid. A file without one is
# assigned a random uid at provision time, so the dashboard has no durable
# identity: a dashboard saved from the UI detaches from its file, and the next
# restart provisions a second dashboard with the same title beside it. Pinning
# the uid in the file is what keeps one file to one dashboard, and what keeps
# /d/<uid>/ links stable across a rebuild.
# Runs as a prek hook locally and in CI (same config).

set -euo pipefail

python3 <<'PY'
import collections
import glob
import json
import os
import sys

DASHBOARD_DIR = "observability/grafana/dashboards"
UID_MAX = 40 # Grafana rejects anything longer

by_uid = collections.defaultdict(list)
failures = []

for path in sorted(glob.glob(os.path.join(DASHBOARD_DIR, "*.json"))):
try:
with open(path, encoding="utf-8") as handle:
dashboard = json.load(handle)
except (OSError, json.JSONDecodeError) as exc:
failures.append(f"{path}: cannot read as JSON: {exc}")
continue

if not isinstance(dashboard, dict):
failures.append(
f"{path}: top level is {type(dashboard).__name__}, expected a JSON object"
)
continue

uid = dashboard.get("uid")
if uid is None or uid == "":
failures.append(
f'{path}: no "uid" -- add a stable one so this file owns exactly '
"one dashboard (without it a UI save detaches and the next restart "
"provisions a duplicate)"
)
elif not isinstance(uid, str):
failures.append(f'{path}: "uid" must be a string, got {type(uid).__name__}')
elif len(uid) > UID_MAX:
failures.append(f'{path}: uid "{uid}" is longer than {UID_MAX} characters')
else:
by_uid[uid].append(path)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

for uid, paths in sorted(by_uid.items()):
if len(paths) > 1:
failures.append(f'uid "{uid}" is used by more than one file: {", ".join(paths)}')

if failures:
for failure in failures:
print(failure, file=sys.stderr)
sys.exit(1)
PY