Skip to content

Commit dd3e325

Browse files
committed
feat: add PromQL to Composer
1 parent 60c4be0 commit dd3e325

13 files changed

Lines changed: 1006 additions & 11 deletions

File tree

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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 { esql } from '../esql';
9+
import { pqlFunc, pqlSel, pqlNum } from '../synth';
10+
11+
test('simple instant vector', () => {
12+
const query = esql.promql(pqlSel('up'));
13+
14+
expect(query.print()).toBe('PROMQL (up)');
15+
});
16+
17+
test('range vector selector', () => {
18+
const query = esql.promql(pqlSel('http_requests_total', '5m'));
19+
20+
expect(query.print()).toBe('PROMQL (http_requests_total[5m])');
21+
});
22+
23+
test('from PromQL synth-created node', () => {
24+
const query = esql.promql(esql.pql`http_requests_total[5m]`);
25+
26+
expect(query.print()).toBe('PROMQL (http_requests_total[5m])');
27+
});
28+
29+
test('from PromQL synth-created node - 2', () => {
30+
const query = esql.promql(esql.pql`http_requests_total[5m]`, { params: { a: 'b' } });
31+
32+
expect(query.print()).toBe('PROMQL a = b (http_requests_total[5m])');
33+
});
34+
35+
test('from PromQL synth-created node - 3', () => {
36+
const query = esql.promql(esql.pql`http_requests_total[5m]`, {
37+
params: { a: 'b' },
38+
outputName: 'result',
39+
});
40+
41+
expect(query.print()).toBe('PROMQL a = b result = (http_requests_total[5m])');
42+
});
43+
44+
test('function expression', () => {
45+
const query = esql.promql(pqlFunc('rate', pqlSel('http_requests_total', '5m')));
46+
47+
expect(query.print()).toBe('PROMQL (rate(http_requests_total[5m]))');
48+
});
49+
50+
test('aggregation with grouping', () => {
51+
const expr = pqlFunc('sum', pqlFunc('rate', pqlSel('metric', '5m')), { by: ['job'] });
52+
const query = esql.promql(expr);
53+
54+
expect(query.print()).toBe('PROMQL (sum(rate(metric[5m])) by (job))');
55+
});
56+
57+
test('accepts a raw PromQL string', () => {
58+
const query = esql.promql('rate(http_requests_total[5m])');
59+
60+
expect(query.print()).toBe('PROMQL (rate(http_requests_total[5m]))');
61+
});
62+
63+
test('accepts a simple metric string', () => {
64+
const query = esql.promql('up');
65+
66+
expect(query.print()).toBe('PROMQL (up)');
67+
});
68+
69+
test('accepts a pql-tagged expression', () => {
70+
const expr = esql.pql`sum(rate(metric[5m])) by (job)`;
71+
const query = esql.promql(expr);
72+
73+
expect(query.print()).toBe('PROMQL (sum(rate(metric[5m])) by (job))');
74+
});
75+
76+
test('single command-level param', () => {
77+
const query = esql.promql(pqlSel('up'), { params: { index: 'k8s' } });
78+
79+
expect(query.print()).toBe('PROMQL index = k8s (up)');
80+
});
81+
82+
test('multiple command-level params', () => {
83+
const query = esql.promql(pqlSel('up'), { params: { index: 'k8s', timeout: '10s' } });
84+
85+
expect(query.print()).toBe('PROMQL index = k8s timeout = 10s (up)');
86+
});
87+
88+
test('named output column', () => {
89+
const query = esql.promql(pqlFunc('sum', pqlSel('metric', '5m')), { outputName: 'result' });
90+
91+
expect(query.print()).toBe('PROMQL result = (sum(metric[5m]))');
92+
});
93+
94+
test('params and output name together', () => {
95+
const query = esql.promql(pqlSel('up'), {
96+
params: { index: 'k8s' },
97+
outputName: 'health',
98+
});
99+
100+
expect(query.print()).toBe('PROMQL index = k8s health = (up)');
101+
});
102+
103+
test('result is a ComposerQuery that can be piped', () => {
104+
const query = esql.promql(pqlSel('up')).limit(100);
105+
106+
expect(query.print()).toBe('PROMQL (up) | LIMIT 100');
107+
});
108+
109+
test('result can be sorted', () => {
110+
const query = esql.promql(pqlFunc('rate', pqlSel('metric', '5m')))
111+
.where`value > ${pqlNum(0)}`.limit(50);
112+
113+
expect(query.print()).toBe('PROMQL (rate(metric[5m])) | WHERE value > 0 | LIMIT 50');
114+
});
115+
116+
test('first command is a PROMQL command', () => {
117+
const query = esql.promql(pqlSel('up'));
118+
const [cmd] = query.ast.commands;
119+
120+
expect(cmd.type).toBe('command');
121+
expect(cmd.name).toBe('promql');
122+
});

src/composer/esql.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import type {
1919
FromSourcesAndMetadataQueryStarter,
2020
FromSourcesQueryStarter,
2121
ParametrizedComposerQueryTag,
22+
PromqlQueryStarter,
23+
PromqlStarterOpts,
2224
} from './types';
2325
import type { ESQLSource } from '../types';
2426
import { isSource } from '../ast/is';
@@ -177,6 +179,28 @@ const createFromSourceAndMetadataCommandStarter =
177179
: esql`${synth.kwd(cmd)} ${sourceNodes}`;
178180
};
179181

182+
const createPromqlStarter = (): PromqlQueryStarter => (expression, opts?: PromqlStarterOpts) => {
183+
const expr = typeof expression === 'string' ? synth.pql(expression) : expression;
184+
185+
const paramsPart = opts?.params
186+
? Object.entries(opts.params)
187+
.map(([k, v]) => `${k}=${v}`)
188+
.join(' ')
189+
: '';
190+
191+
const outputName = opts?.outputName;
192+
193+
if (paramsPart && outputName) {
194+
return esql`PROMQL ${synth.kwd(paramsPart)} ${synth.col(outputName)} = (${expr as synth.SynthTemplateHole})`;
195+
} else if (paramsPart) {
196+
return esql`PROMQL ${synth.kwd(paramsPart)} (${expr as synth.SynthTemplateHole})`;
197+
} else if (outputName) {
198+
return esql`PROMQL ${synth.col(outputName)} = (${expr as synth.SynthTemplateHole})`;
199+
} else {
200+
return esql`PROMQL (${expr as synth.SynthTemplateHole})`;
201+
}
202+
};
203+
180204
/**
181205
* ESQL query composer tag function.
182206
*
@@ -229,6 +253,8 @@ export const esql: ComposerQueryTag &
229253

230254
ts: createFromLikeStarter('TS'),
231255

256+
promql: createPromqlStarter(),
257+
232258
get nop() {
233259
return synth.cmd`WHERE TRUE`;
234260
},

src/composer/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ export { ComposerQuery } from './composer_query';
1111
export { ParameterHole } from './parameter_hole';
1212

1313
export * as synth from './synth';
14-
export { qry, cmd, exp } from './synth';
14+
export { qry, cmd, exp, pql } from './synth';
1515

1616
export { EsqlQuery } from './query';
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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 { BasicPrettyPrinter } from '../../../pretty_print';
9+
import { PromQLBasicPrettyPrinter } from '../../../embedded_languages/promql/pretty_print';
10+
import { pql, promqlExpression } from '../promql';
11+
import { cmd } from '../command';
12+
import { qry } from '../query';
13+
14+
test('creates a PromQL expression node from a tagged template', () => {
15+
const node = pql`rate(http_requests_total[5m])`;
16+
17+
expect(node).toBeDefined();
18+
expect(node.type).toBe('query');
19+
expect(node.dialect).toBe('promql');
20+
});
21+
22+
test('pql is an alias for promqlExpression', () => {
23+
expect(pql).toBe(promqlExpression);
24+
});
25+
26+
test('serializes a PromQL expression back to text', () => {
27+
const node = pql`rate(http_requests_total[5m])`;
28+
const text = PromQLBasicPrettyPrinter.print(node);
29+
30+
expect(text).toBe('rate(http_requests_total[5m])');
31+
});
32+
33+
test('serializes a metric selector', () => {
34+
const node = pql`http_requests_total{job="api",status="200"}`;
35+
const text = PromQLBasicPrettyPrinter.print(node);
36+
37+
expect(text).toBe('http_requests_total{job="api", status="200"}');
38+
});
39+
40+
test('can be used as a function call', () => {
41+
const node = pql('up == 1');
42+
const text = PromQLBasicPrettyPrinter.print(node);
43+
44+
expect(text).toBe('up == 1');
45+
});
46+
47+
test('can compose PromQL expressions', () => {
48+
const time = pql`5m`;
49+
const selector = pql`some_metric{job="api"}[${time}]`;
50+
const number = pql`456`;
51+
const func = pql`func(${selector}, 123, ${number})`;
52+
const text = func + '';
53+
54+
expect(text).toBe('func(some_metric{job="api"}[5m], 123, 456)');
55+
});
56+
57+
test('can be interpolated as a hole in a PROMQL command', () => {
58+
const expr = pql`bytes_in{job="prometheus"}`;
59+
const node = cmd`PROMQL (${expr})`;
60+
const text = BasicPrettyPrinter.command(node);
61+
62+
expect(text).toBe('PROMQL (bytes_in{job="prometheus"})');
63+
});
64+
65+
test('can build a full PROMQL query via qry', () => {
66+
const expr = pql`rate(http_requests_total[5m])`;
67+
const node = qry`PROMQL (${expr})`;
68+
const text = BasicPrettyPrinter.print(node);
69+
70+
expect(text).toBe('PROMQL (rate(http_requests_total[5m]))');
71+
});
72+
73+
test('trims whitespace from source', () => {
74+
const node = pql` up `;
75+
const text = PromQLBasicPrettyPrinter.print(node);
76+
77+
expect(text).toBe('up');
78+
});

0 commit comments

Comments
 (0)