Skip to content

Commit bb3dfda

Browse files
bartovalstratoula
andauthored
[ES|QL] Support PromQL instant query (#271369)
## Summary Closes #270417 https://github.com/user-attachments/assets/6f164e44-9747-4b5a-a93d-a7950ebf13b0 - Added the `time `param both autocomplete e validation Co-authored-by: Stratou <efstratia.kalafateli@elastic.co>
1 parent 3790a2e commit bb3dfda

8 files changed

Lines changed: 123 additions & 9 deletions

File tree

src/platform/packages/shared/kbn-esql-language/src/commands/registry/promql/autocomplete.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,18 @@ describe('after PROMQL keyword', () => {
158158
);
159159
});
160160

161+
test('does not suggest time when range params are used', async () => {
162+
await expectPromqlSuggestions('PROMQL step=5m ', {
163+
textsNotContain: ['time = '],
164+
});
165+
});
166+
167+
test('does not suggest range params when time is used', async () => {
168+
await expectPromqlSuggestions('PROMQL time="2026-01-13T11:30:00.000Z" ', {
169+
textsNotContain: ['time = ', 'step = ', 'start = ', 'end = ', 'buckets = '],
170+
});
171+
});
172+
161173
test('handles quoted param values correctly', async () => {
162174
await expectPromqlSuggestions(
163175
'PROMQL index="metrics" step="5m" ',
@@ -897,6 +909,14 @@ describe('param value suggestions', () => {
897909
);
898910
});
899911

912+
test('suggests date literals for time=', async () => {
913+
await expectPromqlSuggestions(
914+
'PROMQL time=',
915+
{ textsContain: TIME_SYSTEM_PARAMS, labelsNotContain: promqlFunctionLabels },
916+
mockCallbacks
917+
);
918+
});
919+
900920
test('suggests date literals when query follows end=', async () => {
901921
const query =
902922
'PROMQL index = kibana_sample_data_logstsdb step = 1h start = "2026-01-08T16:00:00.000Z" end = bytes_counter';

src/platform/packages/shared/kbn-esql-language/src/commands/registry/promql/autocomplete.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
getUsedPromqlParamNames,
2929
isAtValidColumnSuggestionPosition,
3030
isParamValueComplete,
31+
isPromqlParamAvailable,
3132
} from './utils';
3233
import { findPipeOutsideQuotes } from '../../definitions/utils/shared';
3334
import { suggestForPromqlQuery } from '../../definitions/utils/autocomplete';
@@ -65,11 +66,8 @@ export async function autocomplete(
6566
switch (kind) {
6667
case 'after_command': {
6768
const usedParams = getUsedPromqlParamNames(commandText);
68-
const availableParamSuggestions = getPromqlParamKeySuggestions().filter(
69-
({ label }) =>
70-
!usedParams.has(label) &&
71-
!(label === PromqlParamName.Step && usedParams.has(PromqlParamName.Buckets)) &&
72-
!(label === PromqlParamName.Buckets && usedParams.has(PromqlParamName.Step))
69+
const availableParamSuggestions = getPromqlParamKeySuggestions().filter(({ label }) =>
70+
isPromqlParamAvailable(label, usedParams)
7371
);
7472

7573
const canSuggestQuery = isAtValidColumnSuggestionPosition(

src/platform/packages/shared/kbn-esql-language/src/commands/registry/promql/columns_after.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,26 @@ describe('PROMQL columnsAfter', () => {
8585
expect(result.map(({ name }) => name)).toEqual(['step', 'col0', 'job']);
8686
});
8787

88+
it('does not return step column when time param is used', async () => {
89+
const result = await columnsAfter(
90+
synth.cmd`PROMQL index=metrics time="2026-01-13T11:30:00.000Z" col0=(sum by (job) (http_requests_total{env="prod"}))`,
91+
[],
92+
'PROMQL index=metrics time="2026-01-13T11:30:00.000Z" col0=(sum by (job) (http_requests_total{env="prod"})) | KEEP job',
93+
{
94+
fromFrom: () => Promise.resolve([]),
95+
fromJoin: () => Promise.resolve([]),
96+
fromEnrich: () => Promise.resolve([]),
97+
fromPromql: () =>
98+
Promise.resolve([
99+
{ name: 'job', type: 'keyword', userDefined: false },
100+
{ name: 'http_requests_total', type: 'double', userDefined: false },
101+
]),
102+
}
103+
);
104+
105+
expect(result.map(({ name }) => name)).toEqual(['col0', 'job']);
106+
});
107+
88108
it('does not treat pipe inside label string as command delimiter', async () => {
89109
const sourceFields: ESQLFieldWithMetadata[] = [
90110
{ name: 'bytes', type: 'double', userDefined: false },

src/platform/packages/shared/kbn-esql-language/src/commands/registry/promql/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ export const promqlCommand = {
2929
preview: true,
3030
description: i18n.translate('kbn-esql-language.esql.definitions.promqlDoc', {
3131
defaultMessage:
32-
'Execute PromQL queries against time series data. Use step= or buckets= for time resolution. The index= parameter defaults to * if not specified.',
32+
'Execute PromQL queries against time series data. Use time= for instant queries, or step= or buckets= for range query time resolution. The index= parameter defaults to * if not specified.',
3333
}),
3434
declaration:
35-
'PROMQL [step=<duration>|buckets=<integer>] [start=<time>] [end=<time>] [index=<pattern>] [column=](<query>)',
35+
'PROMQL [time=<time>|[step=<duration>|buckets=<integer>] [start=<time>] [end=<time>]] [index=<pattern>] [column=](<query>)',
3636
examples: [
3737
'PROMQL index=metrics step=1m start=?_tstart end=?_tend (sum by (instance) (bytes))',
3838
'PROMQL index=metrics buckets=6 start=?_tstart end=?_tend (avg(cpu_usage))',

src/platform/packages/shared/kbn-esql-language/src/commands/registry/promql/summary.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ describe('PROMQL summary', () => {
3636
expectedNewColumns: ['step', 'col0'],
3737
});
3838
});
39+
it('does not return the step column when time param is used', () => {
40+
assertSummary('PROMQL index=metrics time="2026-01-13T11:30:00.000Z" col0=(sum(bytes))', {
41+
expectedNewColumns: ['col0'],
42+
});
43+
});
3944
it.todo('returns query text as column name when no label is provided');
4045
it.todo('collects columns derivated from grouping inside the query');
4146
});

src/platform/packages/shared/kbn-esql-language/src/commands/registry/promql/utils.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export enum PromqlParamValueType {
2020

2121
export enum PromqlParamName {
2222
Index = 'index',
23+
Time = 'time',
2324
Step = 'step',
2425
Start = 'start',
2526
End = 'end',
@@ -83,6 +84,11 @@ export const PROMQL_PARAMS: PromqlParamDefinition[] = [
8384
description: 'Index pattern to query',
8485
valueType: PromqlParamValueType.TimeseriesSources,
8586
},
87+
{
88+
name: PromqlParamName.Time,
89+
description: 'Instant query evaluation time',
90+
valueType: PromqlParamValueType.DateLiterals,
91+
},
8692
{
8793
name: PromqlParamName.Step,
8894
description: 'Query resolution step (e.g. 1m, 5m, 1h)',
@@ -112,6 +118,29 @@ export const PROMQL_PARAMS: PromqlParamDefinition[] = [
112118

113119
export const PROMQL_PARAM_NAMES: string[] = PROMQL_PARAMS.map(({ name }) => name);
114120

121+
export const PROMQL_RANGE_PARAM_NAMES = [
122+
PromqlParamName.Step,
123+
PromqlParamName.Buckets,
124+
PromqlParamName.Start,
125+
PromqlParamName.End,
126+
] as const;
127+
128+
export const PROMQL_PARAM_CONFLICTS: Readonly<Record<string, readonly string[]>> = {
129+
[PromqlParamName.Step]: [PromqlParamName.Buckets, PromqlParamName.Time],
130+
[PromqlParamName.Buckets]: [PromqlParamName.Step, PromqlParamName.Time],
131+
[PromqlParamName.Start]: [PromqlParamName.Time],
132+
[PromqlParamName.End]: [PromqlParamName.Time],
133+
[PromqlParamName.Time]: PROMQL_RANGE_PARAM_NAMES,
134+
};
135+
136+
export function isPromqlParamAvailable(name: string, usedParams: Set<string>): boolean {
137+
if (usedParams.has(name)) {
138+
return false;
139+
}
140+
141+
return !PROMQL_PARAM_CONFLICTS[name]?.some((param) => usedParams.has(param));
142+
}
143+
115144
const PARAM_ASSIGNMENT_PATTERNS = PROMQL_PARAM_NAMES.map((param) => ({
116145
param,
117146
pattern: new RegExp(`${param}\\s*=`, 'i'),

src/platform/packages/shared/kbn-esql-language/src/commands/registry/promql/validate.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ describe('PROMQL Validation', () => {
3939
['[PROMQL] Parameters "step" and "buckets" are mutually exclusive']
4040
);
4141
});
42+
43+
test('time cannot be used with range query params', () => {
44+
promqlExpectErrors(
45+
'PROMQL index=timeseries_index time="2026-01-13T11:30:00.000Z" step=5m (rate(counterIntegerField[5m]))',
46+
[
47+
'[PROMQL] Specify either [time] for instant query or any of [step], [buckets], [start], [end]',
48+
]
49+
);
50+
});
4251
});
4352

4453
describe('param values', () => {
@@ -82,6 +91,19 @@ describe('PROMQL Validation', () => {
8291
['[PROMQL] Invalid scrape_interval value']
8392
);
8493
});
94+
95+
test('valid time value', () => {
96+
promqlExpectErrors(
97+
'PROMQL time="2026-01-13T11:30:00.000Z" (rate(counterIntegerField[5m]))',
98+
[]
99+
);
100+
});
101+
102+
test('invalid time format', () => {
103+
promqlExpectErrors('PROMQL time=invalid (rate(counterIntegerField[5m]))', [
104+
'[PROMQL] Invalid time value. Use ISO 8601 with Z (e.g. 2024-01-15T10:00:00Z) or ?_tstart/?_tend',
105+
]);
106+
});
85107
});
86108

87109
describe('query presence', () => {

src/platform/packages/shared/kbn-esql-language/src/commands/registry/promql/validate.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
isPromqlParamName,
4040
looksLikePromqlParamAssignment,
4141
PromqlParamName,
42+
PROMQL_RANGE_PARAM_NAMES,
4243
} from './utils';
4344

4445
// ISO 8601 with Z, optional milliseconds (e.g. 2024-01-15T10:00:00Z or ...00.000Z).
@@ -80,6 +81,21 @@ export const validate = (
8081

8182
const hasStep = usedParams.has(PromqlParamName.Step);
8283
const hasBuckets = usedParams.has(PromqlParamName.Buckets);
84+
const hasTime = usedParams.has(PromqlParamName.Time);
85+
const hasRangeParam = PROMQL_RANGE_PARAM_NAMES.some((param) => usedParams.has(param));
86+
87+
if (hasTime && hasRangeParam) {
88+
messages.push({
89+
...getMessageFromId({
90+
messageId: 'promqlInvalidParam',
91+
values: {
92+
reason:
93+
'Specify either [time] for instant query or any of [step], [buckets], [start], [end]',
94+
},
95+
locations: command.location,
96+
}),
97+
});
98+
}
8399

84100
if (hasStep && hasBuckets) {
85101
messages.push({
@@ -94,7 +110,7 @@ export const validate = (
94110
const hasStart = usedParams.has(PromqlParamName.Start);
95111
const hasEnd = usedParams.has(PromqlParamName.End);
96112

97-
if (hasStart !== hasEnd) {
113+
if (!hasTime && hasStart !== hasEnd) {
98114
const param = hasStart ? PromqlParamName.End : PromqlParamName.Start;
99115
messages.push({
100116
...getMessageFromId({
@@ -123,7 +139,11 @@ export const validate = (
123139
continue;
124140
}
125141

126-
if (param === PromqlParamName.Start || param === PromqlParamName.End) {
142+
if (
143+
param === PromqlParamName.Start ||
144+
param === PromqlParamName.End ||
145+
param === PromqlParamName.Time
146+
) {
127147
const normalized = stripQuotes(value);
128148
const isPlaceholder = normalized === '?_tstart' || normalized === '?_tend';
129149

0 commit comments

Comments
 (0)