diff --git a/public/app/plugins/datasource/elasticsearch/datasource.test.ts b/public/app/plugins/datasource/elasticsearch/datasource.test.ts index 4ee8c4dd5f0..408fd425dc3 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.test.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.test.ts @@ -1198,12 +1198,24 @@ describe('modifyQuery', () => { ); }); + it('should toggle the filter', () => { + query.query = 'foo:"bar"'; + expect(ds.modifyQuery(query, { type: 'ADD_FILTER', options: { key: 'foo', value: 'bar' } }).query).toBe(''); + }); + it('should add the negative filter', () => { expect(ds.modifyQuery(query, { type: 'ADD_FILTER_OUT', options: { key: 'foo', value: 'bar' } }).query).toBe( '-foo:"bar"' ); }); + it('should remove a positive filter to add a negative filter', () => { + query.query = 'foo:"bar"'; + expect(ds.modifyQuery(query, { type: 'ADD_FILTER_OUT', options: { key: 'foo', value: 'bar' } }).query).toBe( + '-foo:"bar"' + ); + }); + it('should do nothing on unknown type', () => { expect(ds.modifyQuery(query, { type: 'unknown', options: { key: 'foo', value: 'bar' } }).query).toBe(query.query); }); diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts index 2999a2f9a4c..61b38b49f4d 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.ts @@ -52,6 +52,7 @@ import { } from './components/QueryEditor/MetricAggregationsEditor/aggregations'; import { metricAggregationConfig } from './components/QueryEditor/MetricAggregationsEditor/utils'; import { isMetricAggregationWithMeta } from './guards'; +import { addFilterToQuery, queryHasFilter, removeFilterFromQuery } from './modifyQuery'; import { trackAnnotationQuery, trackQuery } from './tracking'; import { Logs, @@ -898,17 +899,22 @@ export class ElasticDatasource let expression = query.query ?? ''; switch (action.type) { case 'ADD_FILTER': { - if (expression.length > 0) { - expression += ' AND '; - } - expression += `${action.options.key}:"${action.options.value}"`; + // This gives the user the ability to toggle a filter on and off. + expression = queryHasFilter(expression, action.options.key, action.options.value) + ? removeFilterFromQuery(expression, action.options.key, action.options.value) + : addFilterToQuery(expression, action.options.key, action.options.value); break; } case 'ADD_FILTER_OUT': { - if (expression.length > 0) { - expression += ' AND '; + /** + * If there is a filter with the same key and value, remove it. + * This prevents the user from seeing no changes in the query when they apply + * this filter. + */ + if (queryHasFilter(expression, action.options.key, action.options.value)) { + expression = removeFilterFromQuery(expression, action.options.key, action.options.value); } - expression += `-${action.options.key}:"${action.options.value}"`; + expression = addFilterToQuery(expression, action.options.key, action.options.value, '-'); break; } } diff --git a/public/app/plugins/datasource/elasticsearch/modifyQuery.test.ts b/public/app/plugins/datasource/elasticsearch/modifyQuery.test.ts new file mode 100644 index 00000000000..5f785ced562 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/modifyQuery.test.ts @@ -0,0 +1,74 @@ +import { queryHasFilter, removeFilterFromQuery, addFilterToQuery } from './modifyQuery'; + +describe('queryHasFilter', () => { + it('should return true if the query contains the positive filter', () => { + expect(queryHasFilter('label:"value"', 'label', 'value')).toBe(true); + expect(queryHasFilter('label: "value"', 'label', 'value')).toBe(true); + expect(queryHasFilter('label : "value"', 'label', 'value')).toBe(true); + expect(queryHasFilter('label:value', 'label', 'value')).toBe(true); + expect(queryHasFilter('this:"that" AND label:value', 'label', 'value')).toBe(true); + expect( + queryHasFilter( + 'message:"Jun 20 17:19:47 Xtorm syslogd[348]: ASL Sender Statistics"', + 'message', + 'Jun 20 17:19:47 Xtorm syslogd[348]: ASL Sender Statistics' + ) + ).toBe(true); + }); + it('should return false if the query does not contain the positive filter', () => { + expect(queryHasFilter('label:"value"', 'label', 'otherValue')).toBe(false); + expect(queryHasFilter('-label:"value"', 'label', 'value')).toBe(false); + }); + it('should return true if the query contains the negative filter', () => { + expect(queryHasFilter('-label:"value"', 'label', 'value', '-')).toBe(true); + expect(queryHasFilter('-label: "value"', 'label', 'value', '-')).toBe(true); + expect(queryHasFilter('-label : "value"', 'label', 'value', '-')).toBe(true); + expect(queryHasFilter('-label:value', 'label', 'value', '-')).toBe(true); + expect(queryHasFilter('this:"that" AND -label:value', 'label', 'value', '-')).toBe(true); + }); + it('should return false if the query does not contain the negative filter', () => { + expect(queryHasFilter('label:"value"', 'label', 'otherValue', '-')).toBe(false); + expect(queryHasFilter('label:"value"', 'label', 'value', '-')).toBe(false); + }); +}); + +describe('addFilterToQuery', () => { + it('should add a positive filter to the query', () => { + expect(addFilterToQuery('', 'label', 'value')).toBe('label:"value"'); + }); + it('should add a positive filter to the query with other filters', () => { + expect(addFilterToQuery('label2:"value2"', 'label', 'value')).toBe('label2:"value2" AND label:"value"'); + }); + it('should add a negative filter to the query', () => { + expect(addFilterToQuery('', 'label', 'value', '-')).toBe('-label:"value"'); + }); + it('should add a negative filter to the query with other filters', () => { + expect(addFilterToQuery('label2:"value2"', 'label', 'value', '-')).toBe('label2:"value2" AND -label:"value"'); + }); +}); + +describe('removeFilterFromQuery', () => { + it('should remove filter from query', () => { + const query = 'label:"value"'; + expect(removeFilterFromQuery(query, 'label', 'value')).toBe(''); + }); + it('should remove filter from query with other filters', () => { + expect(removeFilterFromQuery('label:"value" AND label2:"value2"', 'label', 'value')).toBe('label2:"value2"'); + expect(removeFilterFromQuery('label:value AND label2:"value2"', 'label', 'value')).toBe('label2:"value2"'); + expect(removeFilterFromQuery('label : "value" OR label2:"value2"', 'label', 'value')).toBe('label2:"value2"'); + expect(removeFilterFromQuery('test="test" OR label:"value" AND label2:"value2"', 'label', 'value')).toBe( + 'test="test" AND label2:"value2"' + ); + }); + it('should not remove the wrong filter', () => { + expect(removeFilterFromQuery('-label:"value" AND label2:"value2"', 'label', 'value')).toBe( + '-label:"value" AND label2:"value2"' + ); + expect(removeFilterFromQuery('label2:"value2" OR -label:value', 'label', 'value')).toBe( + 'label2:"value2" OR -label:value' + ); + expect(removeFilterFromQuery('-label : "value" OR label2:"value2"', 'label', 'value')).toBe( + '-label : "value" OR label2:"value2"' + ); + }); +}); diff --git a/public/app/plugins/datasource/elasticsearch/modifyQuery.ts b/public/app/plugins/datasource/elasticsearch/modifyQuery.ts new file mode 100644 index 00000000000..cc2622f1d5b --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/modifyQuery.ts @@ -0,0 +1,57 @@ +import { escapeRegex } from '@grafana/data'; + +type ModifierType = '' | '-'; + +/** + * Checks for the presence of a given label:"value" filter in the query. + */ +export function queryHasFilter(query: string, key: string, value: string, modifier: ModifierType = ''): boolean { + const regex = getFilterRegex(key, value); + const matches = query.matchAll(regex); + for (const match of matches) { + if (modifier === '-' && match[0].startsWith(modifier)) { + return true; + } + if (modifier === '' && !match[0].startsWith('-')) { + return true; + } + } + return false; +} + +/** + * Adds a label:"value" expression to the query. + */ +export function addFilterToQuery(query: string, key: string, value: string, modifier: ModifierType = ''): string { + if (queryHasFilter(query, key, value, modifier)) { + return query; + } + + const filter = `${modifier}${key}:"${value}"`; + + return query === '' ? filter : `${query} AND ${filter}`; +} + +function getFilterRegex(key: string, value: string) { + return new RegExp(`[-]{0,1}\\s*${key}\\s*:\\s*["']{0,1}${escapeRegex(value)}["']{0,1}`, 'ig'); +} + +/** + * Removes a label:"value" expression from the query. + */ +export function removeFilterFromQuery(query: string, key: string, value: string, modifier: ModifierType = ''): string { + const regex = getFilterRegex(key, value); + const matches = query.matchAll(regex); + const opRegex = new RegExp(`\\s+(?:AND|OR)\\s*$|^\\s*(?:AND|OR)\\s+`, 'ig'); + for (const match of matches) { + if (modifier === '-' && match[0].startsWith(modifier)) { + query = query.replace(regex, '').replace(opRegex, ''); + } + if (modifier === '' && !match[0].startsWith('-')) { + query = query.replace(regex, '').replace(opRegex, ''); + } + } + query = query.replace(/AND\s+OR/gi, 'OR'); + query = query.replace(/OR\s+AND/gi, 'AND'); + return query; +} diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index 0affb9b22e5..e164893b4da 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -637,22 +637,55 @@ describe('LokiDatasource', () => { expect(result.refId).toEqual('A'); expect(result.expr).toEqual('rate({bar="baz", job="grafana"}[5m])'); }); - describe('and query has parser', () => { - it('then the correct label should be added for logs query', () => { - const query: LokiQuery = { refId: 'A', expr: '{bar="baz"} | logfmt' }; + + describe('and the filter is already present', () => { + it('then it should remove the filter', () => { + const query: LokiQuery = { refId: 'A', expr: '{bar="baz", job="grafana"}' }; const action = { options: { key: 'job', value: 'grafana' }, type: 'ADD_FILTER' }; const result = ds.modifyQuery(query, action); expect(result.refId).toEqual('A'); - expect(result.expr).toEqual('{bar="baz"} | logfmt | job=`grafana`'); + expect(result.expr).toEqual('{bar="baz"}'); }); - it('then the correct label should be added for metrics query', () => { - const query: LokiQuery = { refId: 'A', expr: 'rate({bar="baz"} | logfmt [5m])' }; + + it('then it should remove the filter with escaped value', () => { + const query: LokiQuery = { refId: 'A', expr: '{place="luna", job="\\"grafana/data\\""}' }; + const action = { options: { key: 'job', value: '"grafana/data"' }, type: 'ADD_FILTER' }; + const result = ds.modifyQuery(query, action); + + expect(result.refId).toEqual('A'); + expect(result.expr).toEqual('{place="luna"}'); + }); + }); + }); + + describe('and query has parser', () => { + it('then the correct label should be added for logs query', () => { + const query: LokiQuery = { refId: 'A', expr: '{bar="baz"} | logfmt' }; + const action = { options: { key: 'job', value: 'grafana' }, type: 'ADD_FILTER' }; + const result = ds.modifyQuery(query, action); + + expect(result.refId).toEqual('A'); + expect(result.expr).toEqual('{bar="baz"} | logfmt | job=`grafana`'); + }); + + it('then the correct label should be added for metrics query', () => { + const query: LokiQuery = { refId: 'A', expr: 'rate({bar="baz"} | logfmt [5m])' }; + const action = { options: { key: 'job', value: 'grafana' }, type: 'ADD_FILTER' }; + const result = ds.modifyQuery(query, action); + + expect(result.refId).toEqual('A'); + expect(result.expr).toEqual('rate({bar="baz"} | logfmt | job=`grafana` [5m])'); + }); + + describe('and the filter is already present', () => { + it('then it should remove the filter', () => { + const query: LokiQuery = { refId: 'A', expr: '{bar="baz"} | logfmt | job="grafana"' }; const action = { options: { key: 'job', value: 'grafana' }, type: 'ADD_FILTER' }; const result = ds.modifyQuery(query, action); expect(result.refId).toEqual('A'); - expect(result.expr).toEqual('rate({bar="baz"} | logfmt | job=`grafana` [5m])'); + expect(result.expr).toEqual('{bar="baz"} | logfmt'); }); }); }); @@ -691,27 +724,51 @@ describe('LokiDatasource', () => { expect(result.refId).toEqual('A'); expect(result.expr).toEqual('rate({bar="baz", job!="grafana"}[5m])'); }); - describe('and query has parser', () => { - let ds: LokiDatasource; - beforeEach(() => { - ds = createLokiDatasource(templateSrvStub); - }); - it('then the correct label should be added for logs query', () => { - const query: LokiQuery = { refId: 'A', expr: '{bar="baz"} | logfmt' }; + describe('and the opposite filter is present', () => { + it('then it should remove the filter', () => { + const query: LokiQuery = { refId: 'A', expr: '{bar="baz", job="grafana"}' }; const action = { options: { key: 'job', value: 'grafana' }, type: 'ADD_FILTER_OUT' }; const result = ds.modifyQuery(query, action); expect(result.refId).toEqual('A'); - expect(result.expr).toEqual('{bar="baz"} | logfmt | job!=`grafana`'); + expect(result.expr).toEqual('{bar="baz", job!="grafana"}'); }); - it('then the correct label should be added for metrics query', () => { - const query: LokiQuery = { refId: 'A', expr: 'rate({bar="baz"} | logfmt [5m])' }; + }); + }); + + describe('and query has parser', () => { + let ds: LokiDatasource; + beforeEach(() => { + ds = createLokiDatasource(templateSrvStub); + }); + + it('then the correct label should be added for logs query', () => { + const query: LokiQuery = { refId: 'A', expr: '{bar="baz"} | logfmt' }; + const action = { options: { key: 'job', value: 'grafana' }, type: 'ADD_FILTER_OUT' }; + const result = ds.modifyQuery(query, action); + + expect(result.refId).toEqual('A'); + expect(result.expr).toEqual('{bar="baz"} | logfmt | job!=`grafana`'); + }); + + it('then the correct label should be added for metrics query', () => { + const query: LokiQuery = { refId: 'A', expr: 'rate({bar="baz"} | logfmt [5m])' }; + const action = { options: { key: 'job', value: 'grafana' }, type: 'ADD_FILTER_OUT' }; + const result = ds.modifyQuery(query, action); + + expect(result.refId).toEqual('A'); + expect(result.expr).toEqual('rate({bar="baz"} | logfmt | job!=`grafana` [5m])'); + }); + + describe('and the filter is already present', () => { + it('then it should remove the filter', () => { + const query: LokiQuery = { refId: 'A', expr: '{bar="baz"} | logfmt | job="grafana"' }; const action = { options: { key: 'job', value: 'grafana' }, type: 'ADD_FILTER_OUT' }; const result = ds.modifyQuery(query, action); expect(result.refId).toEqual('A'); - expect(result.expr).toEqual('rate({bar="baz"} | logfmt | job!=`grafana` [5m])'); + expect(result.expr).toEqual('{bar="baz"} | logfmt | job!=`grafana`'); }); }); }); diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index c0da2484716..4c17e83a2d5 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -65,6 +65,8 @@ import { addLineFilter, findLastPosition, getLabelFilterPositions, + queryHasFilter, + removeLabelFromQuery, } from './modifyQuery'; import { getQueryHints } from './queryHints'; import { runSplitQuery } from './querySplitting'; @@ -614,13 +616,27 @@ export class LokiDatasource case 'ADD_FILTER': { if (action.options?.key && action.options?.value) { const value = escapeLabelValueInSelector(action.options.value); - expression = addLabelToQuery(expression, action.options.key, '=', value); + + // This gives the user the ability to toggle a filter on and off. + expression = queryHasFilter(expression, action.options.key, '=', value) + ? removeLabelFromQuery(expression, action.options.key, '=', value) + : addLabelToQuery(expression, action.options.key, '=', value); } break; } case 'ADD_FILTER_OUT': { if (action.options?.key && action.options?.value) { const value = escapeLabelValueInSelector(action.options.value); + + /** + * If there is a filter with the same key and value, remove it. + * This prevents the user from seeing no changes in the query when they apply + * this filter. + */ + if (queryHasFilter(expression, action.options.key, '=', value)) { + expression = removeLabelFromQuery(expression, action.options.key, '=', value); + } + expression = addLabelToQuery(expression, action.options.key, '!=', value); } break; diff --git a/public/app/plugins/datasource/loki/modifyQuery.test.ts b/public/app/plugins/datasource/loki/modifyQuery.test.ts index d484fe6f5fc..ae9d9ad8817 100644 --- a/public/app/plugins/datasource/loki/modifyQuery.test.ts +++ b/public/app/plugins/datasource/loki/modifyQuery.test.ts @@ -6,7 +6,9 @@ import { addNoPipelineErrorToQuery, addParserToQuery, NodePosition, + queryHasFilter, removeCommentsFromQuery, + removeLabelFromQuery, } from './modifyQuery'; describe('addLabelToQuery()', () => { @@ -246,3 +248,47 @@ describe('NodePosition', () => { }); }); }); + +describe('queryHasFilter', () => { + it.each([ + ['{job="grafana"}', 'grafana'], + ['{job="grafana", foo="bar"}', 'grafana'], + ['{foo="bar", job="grafana"}', 'grafana'], + ['{job="\\"grafana\\""}', '"grafana"'], + ['{foo="bar"} | logfmt | job=`grafana`', 'grafana'], + ])('should return true if query has a positive filter', (query: string, value: string) => { + expect(queryHasFilter(query, 'job', '=', value)).toBe(true); + }); + + it.each([ + ['{job!="grafana"}', 'grafana'], + ['{job!="grafana", foo="bar"}', 'grafana'], + ['{foo="bar", job!="grafana"}', 'grafana'], + ['{job!="\\"grafana\\""}', '"grafana"'], + ['{foo="bar"} | logfmt | job!=`grafana`', 'grafana'], + ])('should return true if query has a negative filter', (query: string, value: string) => { + expect(queryHasFilter(query, 'job', '!=', value)).toBe(true); + }); +}); + +describe('removeLabelFromQuery', () => { + it.each([ + ['{job="grafana"}', 'grafana', '{}'], + ['{job="grafana", foo="bar"}', 'grafana', '{foo="bar"}'], + ['{foo="bar", job="grafana"}', 'grafana', '{foo="bar"}'], + ['{job="\\"grafana\\""}', '"grafana"', '{}'], + ['{foo="bar"} | logfmt | job=`grafana`', 'grafana', '{foo="bar"} | logfmt'], + ])('should remove a positive label matcher from the query', (query: string, value: string, expected: string) => { + expect(removeLabelFromQuery(query, 'job', '=', value)).toBe(expected); + }); + + it.each([ + ['{job!="grafana"}', 'grafana', '{}'], + ['{job!="grafana", foo="bar"}', 'grafana', '{foo="bar"}'], + ['{foo="bar", job!="grafana"}', 'grafana', '{foo="bar"}'], + ['{job!="\\"grafana\\""}', '"grafana"', '{}'], + ['{foo="bar"} | logfmt | job!=`grafana`', 'grafana', '{foo="bar"} | logfmt'], + ])('should remove a negative label matcher from the query', (query: string, value: string, expected: string) => { + expect(removeLabelFromQuery(query, 'job', '!=', value)).toBe(expected); + }); +}); diff --git a/public/app/plugins/datasource/loki/modifyQuery.ts b/public/app/plugins/datasource/loki/modifyQuery.ts index aebc105d4a8..69804e9e5de 100644 --- a/public/app/plugins/datasource/loki/modifyQuery.ts +++ b/public/app/plugins/datasource/loki/modifyQuery.ts @@ -2,6 +2,7 @@ import { NodeType, SyntaxNode } from '@lezer/common'; import { sortBy } from 'lodash'; import { + Identifier, JsonExpressionParser, LabelFilter, LabelParser, @@ -14,13 +15,14 @@ import { PipelineExpr, Selector, UnwrapExpr, + String, } from '@grafana/lezer-logql'; import { QueryBuilderLabelFilter } from '../prometheus/querybuilder/shared/types'; import { unescapeLabelValue } from './languageUtils'; -import { LokiQueryModeller } from './querybuilder/LokiQueryModeller'; -import { buildVisualQueryFromString } from './querybuilder/parsing'; +import { lokiQueryModeller as modeller } from './querybuilder/LokiQueryModeller'; +import { buildVisualQueryFromString, handleQuotes } from './querybuilder/parsing'; export class NodePosition { from: number; @@ -45,6 +47,88 @@ export class NodePosition { return query.substring(this.from, this.to); } } + +/** + * Checks for the presence of a given label=value filter in any Matcher expression in the query. + */ +export function queryHasFilter(query: string, key: string, operator: string, value: string): boolean { + const matchers = getMatchersWithFilter(query, key, operator, value); + return matchers.length > 0; +} + +/** + * Removes a label=value Matcher expression from the query. + */ +export function removeLabelFromQuery(query: string, key: string, operator: string, value: string): string { + const matchers = getMatchersWithFilter(query, key, operator, value); + for (const matcher of matchers) { + query = + matcher.parent?.type.id === LabelFilter ? removeLabelFilter(query, matcher) : removeSelector(query, matcher); + } + return query; +} + +function removeLabelFilter(query: string, matcher: SyntaxNode): string { + const pipelineStage = matcher.parent?.parent; + if (!pipelineStage) { + return query; + } + return (query.substring(0, pipelineStage.from) + query.substring(pipelineStage.to)).trim(); +} + +function removeSelector(query: string, matcher: SyntaxNode): string { + let selector: SyntaxNode | null = matcher; + do { + selector = selector.parent; + } while (selector && selector.type.id !== Selector); + const label = matcher.getChild(Identifier); + if (!selector || !label) { + return query; + } + const labelName = query.substring(label.from, label.to); + + const prefix = query.substring(0, selector.from); + const suffix = query.substring(selector.to); + + const matchVisQuery = buildVisualQueryFromString(query.substring(selector.from, selector.to)); + matchVisQuery.query.labels = matchVisQuery.query.labels.filter((label) => label.label !== labelName); + + return prefix + modeller.renderQuery(matchVisQuery.query) + suffix; +} + +function getMatchersWithFilter(query: string, key: string, operator: string, value: string): SyntaxNode[] { + const tree = parser.parse(query); + const matchers: SyntaxNode[] = []; + tree.iterate({ + enter: ({ type, node }): void => { + if (type.id === Matcher) { + matchers.push(node); + } + }, + }); + return matchers.filter((matcher) => { + const labelNode = matcher.getChild(Identifier); + const opNode = labelNode?.nextSibling; + const valueNode = matcher.getChild(String); + if (!labelNode || !opNode || !valueNode) { + return false; + } + const labelName = query.substring(labelNode.from, labelNode.to); + if (labelName !== key) { + return false; + } + const labelValue = query.substring(valueNode.from, valueNode.to); + if (handleQuotes(labelValue) !== unescapeLabelValue(value)) { + return false; + } + const labelOperator = query.substring(opNode.from, opNode.to); + if (labelOperator !== operator) { + return false; + } + return true; + }); +} + /** * Adds label filter to existing query. Useful for query modification for example for ad hoc filters. * @@ -65,6 +149,10 @@ export function addLabelToQuery(query: string, key: string, operator: string, va } const streamSelectorPositions = getStreamSelectorPositions(query); + if (!streamSelectorPositions.length) { + return query; + } + const hasStreamSelectorMatchers = getMatcherInStreamPositions(query); const everyStreamSelectorHasMatcher = streamSelectorPositions.every((streamSelectorPosition) => hasStreamSelectorMatchers.some( @@ -74,9 +162,6 @@ export function addLabelToQuery(query: string, key: string, operator: string, va ); const parserPositions = getParserPositions(query); const labelFilterPositions = getLabelFilterPositions(query); - if (!streamSelectorPositions.length) { - return query; - } const filter = toLabelFilter(key, value, operator); // If we have non-empty stream selector and parser/label filter, we want to add a new label filter after the last one. @@ -103,6 +188,9 @@ export function addParserToQuery(query: string, parser: string): string { return addParser(query, lineFilterPositions, parser); } else { const streamSelectorPositions = getStreamSelectorPositions(query); + if (!streamSelectorPositions.length) { + return query; + } return addParser(query, streamSelectorPositions, parser); } } @@ -302,13 +390,11 @@ function addFilterToStreamSelector( vectorSelectorPositions: NodePosition[], filter: QueryBuilderLabelFilter ): string { - const modeller = new LokiQueryModeller(); let newQuery = ''; let prev = 0; for (let i = 0; i < vectorSelectorPositions.length; i++) { // This is basically just doing splice on a string for each matched vector selector. - const match = vectorSelectorPositions[i]; const isLast = i === vectorSelectorPositions.length - 1; @@ -421,6 +507,9 @@ function addLabelFormat( export function addLineFilter(query: string): string { const streamSelectorPositions = getStreamSelectorPositions(query); + if (!streamSelectorPositions.length) { + return query; + } const streamSelectorEnd = streamSelectorPositions[0].to; const newQueryExpr = query.slice(0, streamSelectorEnd) + ' |= ``' + query.slice(streamSelectorEnd); diff --git a/public/app/plugins/datasource/loki/querybuilder/parsing.ts b/public/app/plugins/datasource/loki/querybuilder/parsing.ts index cf2983a6d98..2bedcf3c227 100644 --- a/public/app/plugins/datasource/loki/querybuilder/parsing.ts +++ b/public/app/plugins/datasource/loki/querybuilder/parsing.ts @@ -597,7 +597,7 @@ function isIntervalVariableError(node: SyntaxNode) { return node?.parent?.type.id === Range; } -function handleQuotes(string: string) { +export function handleQuotes(string: string) { if (string[0] === `"` && string[string.length - 1] === `"`) { return string .substring(1, string.length - 1)