Skip to content

Commit 7941dc1

Browse files
committed
implement promql wrapping pretty printer
1 parent 8c17a77 commit 7941dc1

3 files changed

Lines changed: 755 additions & 0 deletions

File tree

Lines changed: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { PromQLParser } from '../../parser';
9+
import {
10+
PromQLWrappingPrettyPrinter,
11+
type PromQLWrappingPrettyPrinterOptions,
12+
} from '../wrapping_pretty_printer';
13+
14+
const format = (src: string, opts?: PromQLWrappingPrettyPrinterOptions): string => {
15+
const result = PromQLParser.parse(src);
16+
if (result.errors.length > 0) {
17+
throw new Error(`Parse error: ${result.errors[0].message}`);
18+
}
19+
return PromQLWrappingPrettyPrinter.print(result.root, opts);
20+
};
21+
22+
/**
23+
* Assert that the source formats to {@link expected} (defaults to the source
24+
* itself when omitted — i.e. idempotent formatting).
25+
*/
26+
const assertFormat = (
27+
src: string,
28+
expected: string = src,
29+
opts?: PromQLWrappingPrettyPrinterOptions
30+
) => {
31+
const text = format(src, opts);
32+
expect(text).toBe(expected);
33+
};
34+
35+
/**
36+
* Assert formatting at a narrow width. The default width is 80 columns so we
37+
* use a smaller width to trigger wrapping in test cases.
38+
*/
39+
const assertNarrow = (src: string, expected: string, width: number = 30) => {
40+
assertFormat(src, expected, { printWidth: width });
41+
};
42+
43+
describe('PromQL WrappingPrettyPrinter', () => {
44+
describe('identifiers and literals', () => {
45+
test('simple metric name', () => {
46+
assertFormat('http_requests_total');
47+
});
48+
49+
test('integer literal', () => {
50+
assertFormat('42');
51+
});
52+
53+
test('decimal literal', () => {
54+
assertFormat('3.14');
55+
});
56+
57+
test('string literal (double-quoted)', () => {
58+
assertFormat('"hello world"');
59+
});
60+
61+
test('string literal (single-quoted)', () => {
62+
assertFormat("'hello world'");
63+
});
64+
65+
test('time literal', () => {
66+
assertFormat('5m', '5m');
67+
});
68+
69+
test('NaN', () => {
70+
assertFormat('NaN');
71+
});
72+
73+
test('Inf', () => {
74+
assertFormat('Inf');
75+
});
76+
});
77+
78+
describe('selectors', () => {
79+
test('simple metric', () => {
80+
assertFormat('http_requests_total');
81+
});
82+
83+
test('metric with single label', () => {
84+
assertFormat('http_requests_total{job="api"}');
85+
});
86+
87+
test('metric with multiple labels', () => {
88+
assertFormat('http_requests_total{job="api", status="200"}');
89+
});
90+
91+
test('label-only selector', () => {
92+
assertFormat('{job="api"}');
93+
});
94+
95+
test('range vector', () => {
96+
assertFormat('http_requests_total[5m]');
97+
});
98+
99+
test('range vector with labels', () => {
100+
assertFormat('http_requests_total{job="api"}[5m]');
101+
});
102+
103+
test('offset modifier', () => {
104+
assertFormat('http_requests_total offset 5m');
105+
});
106+
107+
test('negative offset', () => {
108+
assertFormat('http_requests_total offset - 5m');
109+
});
110+
111+
test('@ modifier', () => {
112+
assertFormat('http_requests_total @ 1609459200');
113+
});
114+
115+
test('@ with start()', () => {
116+
assertFormat('http_requests_total @ start()');
117+
});
118+
119+
test('combined offset and @', () => {
120+
assertFormat('http_requests_total offset 5m @ 1609459200');
121+
});
122+
123+
test('label map wraps when too wide', () => {
124+
assertNarrow(
125+
'http_requests_total{job="api-server", status="200", method="GET"}',
126+
[
127+
'http_requests_total{',
128+
' job="api-server",',
129+
' status="200",',
130+
' method="GET"',
131+
'}',
132+
].join('\n'),
133+
40
134+
);
135+
});
136+
});
137+
138+
describe('functions', () => {
139+
test('simple function', () => {
140+
assertFormat('rate(http_requests_total[5m])');
141+
});
142+
143+
test('multi-argument function', () => {
144+
assertFormat('clamp(metric_value, 0, 100)');
145+
});
146+
147+
test('no-argument function', () => {
148+
assertFormat('time()');
149+
});
150+
151+
test('aggregation with grouping after', () => {
152+
assertFormat('sum(rate(http_requests_total[5m])) by (job)');
153+
});
154+
155+
test('aggregation with grouping before', () => {
156+
assertFormat('sum by (job) (rate(http_requests_total[5m]))');
157+
});
158+
159+
test('aggregation with without', () => {
160+
assertFormat('avg(cpu_usage) without (instance)');
161+
});
162+
163+
test('function args wrap when too wide', () => {
164+
assertNarrow(
165+
'clamp(metric_value, 0, 100)',
166+
['clamp(', ' metric_value,', ' 0,', ' 100', ')'].join('\n'),
167+
20
168+
);
169+
});
170+
171+
test('nested function wraps', () => {
172+
assertNarrow(
173+
'sum(rate(http_requests_total[5m]))',
174+
['sum(', ' rate(', ' http_requests_total[5m]', ' )', ')'].join('\n'),
175+
25
176+
);
177+
});
178+
179+
test('aggregation with grouping before wraps', () => {
180+
assertNarrow(
181+
'sum by (job) (rate(http_requests_total[5m]))',
182+
['sum by (job) (', ' rate(', ' http_requests_total[5m]', ' )', ')'].join('\n'),
183+
25
184+
);
185+
});
186+
});
187+
188+
describe('binary expressions', () => {
189+
test('simple addition', () => {
190+
assertFormat('a + b');
191+
});
192+
193+
test('simple comparison', () => {
194+
assertFormat('a == b');
195+
});
196+
197+
test('bool modifier', () => {
198+
assertFormat('a == bool b');
199+
});
200+
201+
test('set operator', () => {
202+
assertFormat('a and b');
203+
});
204+
205+
test('binary with vector matching', () => {
206+
assertFormat('a + on(job) b');
207+
});
208+
209+
test('binary expression wraps when wide', () => {
210+
assertNarrow(
211+
'very_long_metric_name_left + very_long_metric_name_right',
212+
['very_long_metric_name_left', ' + very_long_metric_name_right'].join('\n'),
213+
40
214+
);
215+
});
216+
217+
test('chained addition', () => {
218+
assertFormat('a + b + c');
219+
});
220+
221+
test('binary with ignoring and group_left', () => {
222+
assertFormat('a + ignoring(instance) group_left(exported_job) b');
223+
});
224+
});
225+
226+
describe('unary expressions', () => {
227+
test('negation', () => {
228+
assertFormat('-metric');
229+
});
230+
231+
test('positive', () => {
232+
assertFormat('+metric');
233+
});
234+
});
235+
236+
describe('parenthesized expressions', () => {
237+
test('simple parens', () => {
238+
assertFormat('(a + b)');
239+
});
240+
241+
test('parens wrap when wide', () => {
242+
assertNarrow(
243+
'(very_long_metric_a + very_long_metric_b)',
244+
['(', ' very_long_metric_a', ' + very_long_metric_b', ')'].join('\n'),
245+
35
246+
);
247+
});
248+
});
249+
250+
describe('subqueries', () => {
251+
test('simple subquery', () => {
252+
assertFormat('rate(http_requests_total[5m])[30m:1m]');
253+
});
254+
255+
test('subquery without resolution', () => {
256+
assertFormat('rate(http_requests_total[5m])[30m:]');
257+
});
258+
259+
test('subquery with offset', () => {
260+
assertFormat('rate(http_requests_total[5m])[30m:1m] offset 5m');
261+
});
262+
});
263+
264+
describe('complex queries', () => {
265+
test('error rate query wraps at default width', () => {
266+
const result = format(
267+
'sum(rate(http_errors_total{job="api"}[5m])) / sum(rate(http_requests_total{job="api"}[5m]))'
268+
);
269+
270+
expect(result).toContain('\n');
271+
272+
const reparsed = PromQLParser.parse(result);
273+
274+
expect(reparsed.errors).toHaveLength(0);
275+
});
276+
277+
test('error rate query stays on one line when width allows', () => {
278+
assertFormat(
279+
'sum(rate(http_errors_total{job="api"}[5m])) / sum(rate(http_requests_total{job="api"}[5m]))',
280+
undefined,
281+
{ printWidth: 100 }
282+
);
283+
});
284+
285+
test('error rate query wraps at narrow width', () => {
286+
const result = format(
287+
'sum(rate(http_errors_total{job="api"}[5m])) / sum(rate(http_requests_total{job="api"}[5m]))',
288+
{ printWidth: 50 }
289+
);
290+
291+
expect(result).toContain('\n');
292+
293+
const reparsed = PromQLParser.parse(result);
294+
295+
expect(reparsed.errors).toHaveLength(0);
296+
});
297+
298+
test('histogram_quantile wraps nicely', () => {
299+
assertNarrow(
300+
'histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket{job="api"}[5m])))',
301+
[
302+
'histogram_quantile(',
303+
' 0.99,',
304+
' sum by (le) (',
305+
' rate(',
306+
' http_request_duration_seconds_bucket{',
307+
' job="api"',
308+
' }[5m]',
309+
' )',
310+
' )',
311+
')',
312+
].join('\n'),
313+
45
314+
);
315+
});
316+
});
317+
318+
describe('options', () => {
319+
test('lowercaseFunctions', () => {
320+
assertFormat('SUM(a)', 'sum(a)', { lowercaseFunctions: true });
321+
});
322+
323+
test('lowercaseKeywords', () => {
324+
assertFormat('sum BY (job) (a)', 'sum by (job) (a)', { lowercaseKeywords: true });
325+
});
326+
327+
test('lowercaseOperators', () => {
328+
assertFormat('a AND b', 'a and b', { lowercaseOperators: true });
329+
});
330+
331+
test('custom printWidth', () => {
332+
const result = format('rate(http_requests_total[5m])', { printWidth: 20 });
333+
334+
expect(result).toContain('\n');
335+
expect('\n' + result).toBe(`
336+
rate(
337+
http_requests_total[5m]
338+
)`);
339+
});
340+
});
341+
342+
describe('idempotency', () => {
343+
const queries = [
344+
'http_requests_total',
345+
'http_requests_total{job="api", status="200"}',
346+
'rate(http_requests_total[5m])',
347+
'sum by (job) (rate(http_requests_total[5m]))',
348+
'a + b * c',
349+
'a == bool b',
350+
'-metric',
351+
'(a + b)',
352+
'rate(http_requests_total[5m])[30m:1m]',
353+
'sum(rate(http_errors_total[5m])) / sum(rate(http_requests_total[5m]))',
354+
];
355+
356+
for (const query of queries) {
357+
test(`idempotent: ${query}`, () => {
358+
const first = format(query);
359+
const second = format(first);
360+
expect(second).toBe(first);
361+
});
362+
}
363+
});
364+
});

src/embedded_languages/promql/pretty_print/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@ export {
99
PromQLBasicPrettyPrinter,
1010
type PromQLBasicPrettyPrinterOptions,
1111
} from './basic_pretty_printer';
12+
13+
export {
14+
PromQLWrappingPrettyPrinter,
15+
type PromQLWrappingPrettyPrinterOptions,
16+
} from './wrapping_pretty_printer';

0 commit comments

Comments
 (0)