Skip to content

Commit 544cc2b

Browse files
authored
Merge branch 'main' into 185866-hide-log-rate-no-data-alerts
2 parents 109b655 + b663900 commit 544cc2b

19 files changed

Lines changed: 1141 additions & 142 deletions

File tree

src/platform/packages/shared/kbn-esql-utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export {
5252
getESQLStatsQueryMeta,
5353
constructCascadeQuery,
5454
mutateQueryStatsGrouping,
55+
appendFilteringWhereClauseForCascadeLayout,
5556
} from './src';
5657

5758
export { ENABLE_ESQL, FEEDBACK_LINK } from './constants';

src/platform/packages/shared/kbn-esql-utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,5 @@ export {
5959
getESQLStatsQueryMeta,
6060
constructCascadeQuery,
6161
mutateQueryStatsGrouping,
62+
appendFilteringWhereClauseForCascadeLayout,
6263
} from './utils/cascaded_documents_helpers';

src/platform/packages/shared/kbn-esql-utils/src/utils/append_to_query.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ describe('appendToQuery', () => {
3737
appendWhereClauseToESQLQuery('from logstash-* // meow', 'dest', 'tada!', '+', 'string')
3838
).toBe(
3939
`from logstash-* // meow
40-
| WHERE \`dest\`=="tada!"`
40+
| WHERE \`dest\` == "tada!"`
4141
);
4242
});
4343
it('appends a filter out where clause in an existing query', () => {
4444
expect(
4545
appendWhereClauseToESQLQuery('from logstash-* // meow', 'dest', 'tada!', '-', 'string')
4646
).toBe(
4747
`from logstash-* // meow
48-
| WHERE \`dest\`!="tada!"`
48+
| WHERE \`dest\` != "tada!"`
4949
);
5050
});
5151

@@ -54,14 +54,14 @@ describe('appendToQuery', () => {
5454
appendWhereClauseToESQLQuery('from logstash-* // meow', 'ip_field', 'tada!', '-', 'ip')
5555
).toBe(
5656
`from logstash-* // meow
57-
| WHERE \`ip_field\`!="tada!"`
57+
| WHERE \`ip_field\` != "tada!"`
5858
);
5959
});
6060

6161
it('appends a where clause in an existing query with casting to string when the type is not given', () => {
6262
expect(appendWhereClauseToESQLQuery('from logstash-* // meow', 'dest', 'tada!', '-')).toBe(
6363
`from logstash-* // meow
64-
| WHERE \`dest\`::string!="tada!"`
64+
| WHERE \`dest\`::string != "tada!"`
6565
);
6666
});
6767

@@ -106,7 +106,7 @@ describe('appendToQuery', () => {
106106
)
107107
).toBe(
108108
`from logstash-* | where country == "GR"
109-
AND \`dest\`=="Crete"`
109+
AND \`dest\` == "Crete"`
110110
);
111111
});
112112

@@ -169,7 +169,7 @@ AND \`dest\`=="Crete"`
169169
)
170170
).toBe(
171171
`from logstash-* | where CIDR_MATCH(ip1, "127.0.0.2/32", "127.0.0.3/32")
172-
and \`ip\`!="127.0.0.2/32"`
172+
and \`ip\` != "127.0.0.2/32"`
173173
);
174174
});
175175

src/platform/packages/shared/kbn-esql-utils/src/utils/append_to_query.ts

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
99

10-
import { parse, mutate, BasicPrettyPrinter } from '@kbn/esql-ast';
10+
import { BasicPrettyPrinter, EsqlQuery, parse, mutate } from '@kbn/esql-ast';
11+
import type { BinaryExpressionComparisonOperator } from '@kbn/esql-ast/src/types';
1112
import { sanitazeESQLInput } from './sanitaze_input';
1213

13-
const PARAM_TYPES_NO_NEED_IMPLICIT_STRING_CASTING = [
14+
export const PARAM_TYPES_NO_NEED_IMPLICIT_STRING_CASTING = [
1415
'date',
1516
'date_nanos',
1617
'version',
@@ -20,12 +21,47 @@ const PARAM_TYPES_NO_NEED_IMPLICIT_STRING_CASTING = [
2021
'string',
2122
];
2223

24+
export type SupportedOperators =
25+
| Extract<BinaryExpressionComparisonOperator, '==' | '!='>
26+
| 'is not null'
27+
| 'is null';
28+
2329
// Append in a new line the appended text to take care of the case where the user adds a comment at the end of the query
2430
// in these cases a base query such as "from index // comment" will result in errors or wrong data if we don't append in a new line
2531
export function appendToESQLQuery(baseESQLQuery: string, appendedText: string): string {
2632
return `${baseESQLQuery}\n${appendedText}`;
2733
}
2834

35+
/**
36+
* Gets the operator and expression type for the given operation
37+
*/
38+
export const getOperator = (
39+
operation: '+' | '-' | 'is_not_null' | 'is_null'
40+
): { operator: SupportedOperators; expressionType: 'postfix-unary' | 'binary' } => {
41+
switch (operation) {
42+
case 'is_not_null':
43+
return {
44+
operator: 'is not null',
45+
expressionType: 'postfix-unary',
46+
};
47+
case 'is_null':
48+
return {
49+
operator: 'is null',
50+
expressionType: 'postfix-unary',
51+
};
52+
case '-':
53+
return {
54+
operator: '!=',
55+
expressionType: 'binary',
56+
};
57+
default:
58+
return {
59+
operator: '==',
60+
expressionType: 'binary',
61+
};
62+
}
63+
};
64+
2965
export function appendWhereClauseToESQLQuery(
3066
baseESQLQuery: string,
3167
field: string,
@@ -37,20 +73,13 @@ export function appendWhereClauseToESQLQuery(
3773
if (Array.isArray(value)) {
3874
return undefined;
3975
}
40-
let operator;
41-
switch (operation) {
42-
case 'is_not_null':
43-
operator = ' is not null';
44-
break;
45-
case 'is_null':
46-
operator = ' is null';
47-
break;
48-
case '-':
49-
operator = '!=';
50-
break;
51-
default:
52-
operator = '==';
53-
}
76+
77+
const ESQLQuery = EsqlQuery.fromSrc(baseESQLQuery);
78+
79+
const whereCommands = Array.from(mutate.commands.where.list(ESQLQuery.ast));
80+
81+
const { operator } = getOperator(operation);
82+
5483
let filterValue =
5584
typeof value === 'string' ? `"${value.replace(/\\/g, '\\\\').replace(/\"/g, '\\"')}"` : value;
5685
// Adding the backticks here are they are needed for special char fields
@@ -69,16 +98,14 @@ export function appendWhereClauseToESQLQuery(
6998
filterValue = '';
7099
}
71100

72-
const { ast } = parse(baseESQLQuery);
73-
74-
const lastCommandIsWhere = ast[ast.length - 1].name === 'where';
75101
// if where command already exists in the end of the query:
76-
// - we need to append with and if the filter doesnt't exist
102+
// - we need to append with and if the filter doesn't exist
77103
// - we need to change the filter operator if the filter exists with different operator
78104
// - we do nothing if the filter exists with the same operator
79-
if (lastCommandIsWhere) {
80-
const whereCommand = ast[ast.length - 1];
81-
const whereAstText = whereCommand.text;
105+
if (Boolean(whereCommands.length)) {
106+
const lastWhereCommand = whereCommands[whereCommands.length - 1];
107+
108+
const whereAstText = lastWhereCommand.text;
82109
// the filter already exists in the where clause
83110
if (whereAstText.includes(field) && whereAstText.includes(String(filterValue))) {
84111
const pipesArray = baseESQLQuery.split('|');
@@ -88,7 +115,10 @@ export function appendWhereClauseToESQLQuery(
88115
if (matches) {
89116
const existingOperator = matches[1]?.trim().replace('`', '').toLowerCase();
90117
if (!['==', '!=', 'is not null', 'is null'].includes(existingOperator.trim())) {
91-
return appendToESQLQuery(baseESQLQuery, `and ${fieldName}${operator}${filterValue}`);
118+
return appendToESQLQuery(
119+
baseESQLQuery,
120+
`and ${fieldName} ${operator} ${filterValue}`.trim()
121+
);
92122
}
93123
// the filter is the same
94124
if (existingOperator === operator.trim()) {
@@ -101,11 +131,13 @@ export function appendWhereClauseToESQLQuery(
101131
}
102132
}
103133
}
134+
104135
// filter does not exist in the where clause
105-
const whereClause = `AND ${fieldName}${operator}${filterValue}`;
136+
const whereClause = `AND ${fieldName} ${operator} ${filterValue}`.trim();
106137
return appendToESQLQuery(baseESQLQuery, whereClause);
107138
}
108-
const whereClause = `| WHERE ${fieldName}${operator}${filterValue}`;
139+
140+
const whereClause = `| WHERE ${fieldName} ${operator} ${filterValue}`.trim();
109141
return appendToESQLQuery(baseESQLQuery, whereClause);
110142
}
111143

src/platform/packages/shared/kbn-esql-utils/src/utils/cascaded_documents_helpers.test.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
getESQLStatsQueryMeta,
1313
constructCascadeQuery,
1414
mutateQueryStatsGrouping,
15+
appendFilteringWhereClauseForCascadeLayout,
1516
} from './cascaded_documents_helpers';
1617

1718
describe('cascaded documents helpers utils', () => {
@@ -350,4 +351,112 @@ describe('cascaded documents helpers utils', () => {
350351
);
351352
});
352353
});
354+
355+
describe('appendFilteringWhereClauseForCascadeLayout', () => {
356+
it('appends a filter in where clause in an existing query containing a stats command without a where command', () => {
357+
expect(
358+
appendFilteringWhereClauseForCascadeLayout(
359+
'from logstash-* | stats var = avg(woof)',
360+
'dest',
361+
'tada!',
362+
'+',
363+
'string'
364+
)
365+
).toBe('FROM logstash-* | WHERE dest == "tada!" | STATS var = AVG(woof)');
366+
});
367+
368+
it('appends a filter in where clause in an existing query containing a stats command with a where command', () => {
369+
expect(
370+
appendFilteringWhereClauseForCascadeLayout(
371+
'from logstash-* | where country == "GR" | stats var = avg(woof) ',
372+
'dest',
373+
'tada!',
374+
'+',
375+
'string'
376+
)
377+
).toBe('FROM logstash-* | WHERE dest == "tada!" AND country == "GR" | STATS var = AVG(woof)');
378+
});
379+
380+
it('negates a filter in where clause in an existing query containing a stats command without a where command', () => {
381+
expect(
382+
appendFilteringWhereClauseForCascadeLayout(
383+
`FROM logstash-* | WHERE \`geo.dest\` == "BT" | SORT @timestamp DESC | LIMIT 10000 | STATS countB = COUNT(bytes) BY geo.dest | SORT countB`,
384+
'geo.dest',
385+
'BT',
386+
'-',
387+
'string'
388+
)
389+
).toBe(
390+
'FROM logstash-* | WHERE `geo.dest` != "BT" | SORT @timestamp DESC | LIMIT 10000 | STATS countB = COUNT(bytes) BY geo.dest | SORT countB'
391+
);
392+
});
393+
394+
it("accounts for case where there's a pre-existing where command that references a runtime field created in a previous stats command ", () => {
395+
expect(
396+
appendFilteringWhereClauseForCascadeLayout(
397+
'from logstash-* | sort @timestamp desc | limit 10000 | stats countB = count(bytes) by geo.dest | sort countB | where countB > 0',
398+
'dest',
399+
'tada!',
400+
'+',
401+
'string'
402+
)
403+
).toBe(
404+
'FROM logstash-* | WHERE dest == "tada!" | SORT @timestamp DESC | LIMIT 10000 | STATS countB = COUNT(bytes) BY geo.dest | SORT countB | WHERE countB > 0'
405+
);
406+
});
407+
408+
it('handles the case where the field being filtered on is a runtime field created by a stats command', () => {
409+
expect(
410+
appendFilteringWhereClauseForCascadeLayout(
411+
'FROM kibana_sample_data_logs | STATS count = COUNT(bytes), average = AVG(memory) BY Pattern = CATEGORIZE(message), agent.keyword, clientip',
412+
'Pattern',
413+
'tada!',
414+
'+',
415+
'string'
416+
)
417+
).toBe(
418+
'FROM kibana_sample_data_logs | STATS count = COUNT(bytes), average = AVG(memory) BY Pattern = CATEGORIZE(message), agent.keyword, clientip | WHERE Pattern == "tada!"'
419+
);
420+
});
421+
422+
it('handles the case where the field being filtered on is a runtime field created by a stats command, followed by other commands', () => {
423+
expect(
424+
appendFilteringWhereClauseForCascadeLayout(
425+
'FROM kibana_sample_data_logs | STATS count = COUNT(bytes), average = AVG(memory) BY Pattern = CATEGORIZE(message), agent.keyword, clientip | SORT count DESC',
426+
'Pattern',
427+
'tada!',
428+
'+',
429+
'string'
430+
)
431+
).toBe(
432+
'FROM kibana_sample_data_logs | STATS count = COUNT(bytes), average = AVG(memory) BY Pattern = CATEGORIZE(message), agent.keyword, clientip | WHERE Pattern == "tada!" | SORT count DESC'
433+
);
434+
});
435+
436+
it('handles the case where the field being filtered on is a runtime field created by a stats command, and with another filter applied there after', () => {
437+
const initialFilterResult = appendFilteringWhereClauseForCascadeLayout(
438+
'FROM kibana_sample_data_logs | STATS count = COUNT(bytes), average = AVG(memory) BY Pattern = CATEGORIZE(message), agent.keyword, clientip | SORT count DESC',
439+
'Pattern',
440+
'tada!',
441+
'+',
442+
'string'
443+
);
444+
445+
expect(initialFilterResult).toBe(
446+
'FROM kibana_sample_data_logs | STATS count = COUNT(bytes), average = AVG(memory) BY Pattern = CATEGORIZE(message), agent.keyword, clientip | WHERE Pattern == "tada!" | SORT count DESC'
447+
);
448+
449+
expect(
450+
appendFilteringWhereClauseForCascadeLayout(
451+
initialFilterResult,
452+
'clientip',
453+
'192.168.1.1',
454+
'+',
455+
'string'
456+
)
457+
).toBe(
458+
'FROM kibana_sample_data_logs | WHERE clientip == "192.168.1.1" | STATS count = COUNT(bytes), average = AVG(memory) BY Pattern = CATEGORIZE(message), agent.keyword, clientip | WHERE Pattern == "tada!" | SORT count DESC'
459+
);
460+
});
461+
});
353462
});

0 commit comments

Comments
 (0)