From 77421907b77d92bcb5d43b42304c218da952cbb2 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Mon, 25 Jul 2022 15:16:04 +0200 Subject: [PATCH] Loki: Add support for IP label and line filter in query builder (#52658) * Add support for IP line filter * Add support for IP label filter * Updates for Explain mode * Update test * Remove invalid options in LabelFilterIpMatches --- .../querybuilder/LokiQueryModeller.test.ts | 2 +- .../loki/querybuilder/operations.ts | 40 ++++++++++++++- .../loki/querybuilder/parsing.test.ts | 50 ++++++++++++------- .../datasource/loki/querybuilder/parsing.ts | 43 ++++++++++------ .../datasource/loki/querybuilder/types.ts | 2 + 5 files changed, 101 insertions(+), 36 deletions(-) diff --git a/public/app/plugins/datasource/loki/querybuilder/LokiQueryModeller.test.ts b/public/app/plugins/datasource/loki/querybuilder/LokiQueryModeller.test.ts index a7ef55e61e4..86160cba660 100644 --- a/public/app/plugins/datasource/loki/querybuilder/LokiQueryModeller.test.ts +++ b/public/app/plugins/datasource/loki/querybuilder/LokiQueryModeller.test.ts @@ -127,7 +127,7 @@ describe('LokiQueryModeller', () => { labels: [{ label: 'app', op: '=', value: 'grafana' }], operations: [{ id: LokiOperationId.LabelFilter, params: ['__error__', '=', 'value'] }], }) - ).toBe('{app="grafana"} | __error__=`value`'); + ).toBe('{app="grafana"} | __error__ = `value`'); }); it('Can query with label filter expression using greater than operator', () => { diff --git a/public/app/plugins/datasource/loki/querybuilder/operations.ts b/public/app/plugins/datasource/loki/querybuilder/operations.ts index a250b9520e7..824dba68535 100644 --- a/public/app/plugins/datasource/loki/querybuilder/operations.ts +++ b/public/app/plugins/datasource/loki/querybuilder/operations.ts @@ -292,6 +292,27 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] { addOperationHandler: addLokiOperation, explainHandler: (op) => `Return log lines that does not match regex \`${op.params[0]}\`.`, }, + { + id: LokiOperationId.LineFilterIpMatches, + name: 'IP line filter expression', + params: [ + { name: 'Operator', type: 'string', options: ['|=', '!='] }, + { + name: 'Pattern', + type: 'string', + placeholder: '', + minWidth: 16, + runQueryOnEnter: true, + }, + ], + defaultParams: ['|=', ''], + alternativesKey: 'line filter', + category: LokiVisualQueryOperationCategory.LineFilters, + orderRank: LokiOperationOrder.LineFilters, + renderer: (op, def, innerExpr) => `${innerExpr} ${op.params[0]} ip(\`${op.params[1]}\`)`, + addOperationHandler: addLokiOperation, + explainHandler: (op) => `Return log lines using IP matching of \`${op.params[1]}\``, + }, { id: LokiOperationId.LabelFilter, name: 'Label filter expression', @@ -308,6 +329,23 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] { addOperationHandler: addLokiOperation, explainHandler: () => `Label expression filter allows filtering using original and extracted labels.`, }, + { + id: LokiOperationId.LabelFilterIpMatches, + name: 'IP label filter expression', + params: [ + { name: 'Label', type: 'string' }, + { name: 'Operator', type: 'string', options: ['=', '!='] }, + { name: 'Value', type: 'string' }, + ], + defaultParams: ['', '=', ''], + alternativesKey: 'label filter', + category: LokiVisualQueryOperationCategory.LabelFilters, + orderRank: LokiOperationOrder.LabelFilters, + renderer: (model, def, innerExpr) => + `${innerExpr} | ${model.params[0]} ${model.params[1]} ip(\`${model.params[2]}\`)`, + addOperationHandler: addLokiOperation, + explainHandler: (op) => `Return log lines using IP matching of \`${op.params[2]}\` for \`${op.params[0]}\` label`, + }, { id: LokiOperationId.LabelFilterNoErrors, name: 'No pipeline errors', @@ -443,7 +481,7 @@ function labelFilterRenderer(model: QueryBuilderOperation, def: QueryBuilderOper return `${innerExpr} | ${model.params[0]} ${model.params[1]} ${model.params[2]}`; } - return `${innerExpr} | ${model.params[0]}${model.params[1]}\`${model.params[2]}\``; + return `${innerExpr} | ${model.params[0]} ${model.params[1]} \`${model.params[2]}\``; } function pipelineRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) { diff --git a/public/app/plugins/datasource/loki/querybuilder/parsing.test.ts b/public/app/plugins/datasource/loki/querybuilder/parsing.test.ts index 48511efc9f0..219fd075e2a 100644 --- a/public/app/plugins/datasource/loki/querybuilder/parsing.test.ts +++ b/public/app/plugins/datasource/loki/querybuilder/parsing.test.ts @@ -1,5 +1,5 @@ import { buildVisualQueryFromString } from './parsing'; -import { LokiVisualQuery } from './types'; +import { LokiOperationId, LokiVisualQuery } from './types'; describe('buildVisualQueryFromString', () => { it('creates no errors for empty query', () => { @@ -77,15 +77,22 @@ describe('buildVisualQueryFromString', () => { }); it('returns error for query with ip matching line filter', () => { - const context = buildVisualQueryFromString('{app="frontend"} |= ip("192.168.4.5/16")'); - expect(context.errors).toEqual([ - { - text: 'Matching ip addresses not supported in query builder: |= ip("192.168.4.5/16")', - from: 17, - to: 40, - parentType: 'LineFilters', - }, - ]); + const context = buildVisualQueryFromString('{app="frontend"} |= ip("192.168.4.5/16") | logfmt'); + expect(context).toEqual( + noErrors({ + labels: [ + { + op: '=', + value: 'frontend', + label: 'app', + }, + ], + operations: [ + { id: '__line_filter_ip_matches', params: ['|=', '192.168.4.5/16'] }, + { id: 'logfmt', params: [] }, + ], + }) + ); }); it('parses query with matcher label filter', () => { @@ -162,14 +169,21 @@ describe('buildVisualQueryFromString', () => { it('returns error for query with ip label filter', () => { const context = buildVisualQueryFromString('{app="frontend"} | logfmt | address=ip("192.168.4.5/16")'); - expect(context.errors).toEqual([ - { - text: 'IpLabelFilter not supported in query builder: address=ip("192.168.4.5/16")', - from: 28, - to: 56, - parentType: 'PipelineStage', - }, - ]); + expect(context).toEqual( + noErrors({ + labels: [ + { + op: '=', + value: 'frontend', + label: 'app', + }, + ], + operations: [ + { id: 'logfmt', params: [] }, + { id: LokiOperationId.LabelFilterIpMatches, params: ['address', '=', '192.168.4.5/16'] }, + ], + }) + ); }); it('parses query with with parser', () => { diff --git a/public/app/plugins/datasource/loki/querybuilder/parsing.ts b/public/app/plugins/datasource/loki/querybuilder/parsing.ts index 9b1057dfcb2..28a5bf284cf 100644 --- a/public/app/plugins/datasource/loki/querybuilder/parsing.ts +++ b/public/app/plugins/datasource/loki/querybuilder/parsing.ts @@ -14,7 +14,7 @@ import { import { QueryBuilderLabelFilter, QueryBuilderOperation } from '../../prometheus/querybuilder/shared/types'; import { binaryScalarDefs } from './binaryScalarOperations'; -import { LokiVisualQuery, LokiVisualQueryBinary } from './types'; +import { LokiOperationId, LokiVisualQuery, LokiVisualQueryBinary } from './types'; interface Context { query: LokiVisualQuery; @@ -182,22 +182,24 @@ function getLabel(expr: string, node: SyntaxNode): QueryBuilderLabelFilter { } function getLineFilter(expr: string, node: SyntaxNode): { operation?: QueryBuilderOperation; error?: string } { - // Check for nodes not supported in visual builder and return error - const ipLineFilter = getAllByType(expr, node, 'Ip'); - if (ipLineFilter.length > 0) { - return { - error: 'Matching ip addresses not supported in query builder', - }; - } - - const mapFilter: any = { - '|=': '__line_contains', - '!=': '__line_contains_not', - '|~': '__line_matches_regex', - '!~': '"__line_matches_regex"_not', - }; const filter = getString(expr, node.getChild('Filter')); const filterExpr = handleQuotes(getString(expr, node.getChild('String'))); + const ipLineFilter = node.getChild('FilterOp')?.getChild('Ip'); + + if (ipLineFilter) { + return { + operation: { + id: LokiOperationId.LineFilterIpMatches, + params: [filter, filterExpr], + }, + }; + } + const mapFilter: any = { + '|=': LokiOperationId.LineContains, + '!=': LokiOperationId.LineContainsNot, + '|~': LokiOperationId.LineMatchesRegex, + '!~': LokiOperationId.LineMatchesRegexNot, + }; return { operation: { @@ -238,8 +240,17 @@ function getLabelFilter(expr: string, node: SyntaxNode): { operation?: QueryBuil }; } if (node.firstChild!.name === 'IpLabelFilter') { + const ipLabelFilter = node.firstChild; + const label = ipLabelFilter?.getChild('Identifier'); + const op = label?.nextSibling; + const value = ipLabelFilter?.getChild('String'); + const valueString = handleQuotes(getString(expr, value)); + return { - error: 'IpLabelFilter not supported in query builder', + operation: { + id: LokiOperationId.LabelFilterIpMatches, + params: [getString(expr, label), getString(expr, op), valueString], + }, }; } diff --git a/public/app/plugins/datasource/loki/querybuilder/types.ts b/public/app/plugins/datasource/loki/querybuilder/types.ts index 63aaa7f7481..71bbb52ed16 100644 --- a/public/app/plugins/datasource/loki/querybuilder/types.ts +++ b/public/app/plugins/datasource/loki/querybuilder/types.ts @@ -62,8 +62,10 @@ export enum LokiOperationId { LineContainsNot = '__line_contains_not', LineMatchesRegex = '__line_matches_regex', LineMatchesRegexNot = '__line_matches_regex_not', + LineFilterIpMatches = '__line_filter_ip_matches', LabelFilter = '__label_filter', LabelFilterNoErrors = '__label_filter_no_errors', + LabelFilterIpMatches = '__label_filter_ip_marches', Unwrap = 'unwrap', // Binary ops Addition = '__addition',