From f00ffb190c758654adb46a157047823ddfaaa228 Mon Sep 17 00:00:00 2001 From: Alexander Kubyshkin Date: Wed, 4 May 2022 13:49:04 +0300 Subject: [PATCH] Escape backslashes in regexps in Loki label browser (#45809, #47039). (#47412) * Escape backslashes in regexps in Loki label browser (#45809, #47039). * Escape values in Loki Query Builder. * Escape more values in Loki Query Builder. --- .../components/Typeahead/TypeaheadItem.tsx | 7 +++- .../loki/components/LokiLabelBrowser.tsx | 5 +-- .../loki/components/LokiQueryField.tsx | 15 ++++++-- .../datasource/loki/datasource.test.ts | 22 +++++++++++- .../app/plugins/datasource/loki/datasource.ts | 11 +++--- .../plugins/datasource/loki/language_utils.ts | 34 +++++++++++++++++++ .../components/LokiQueryBuilder.tsx | 13 ++++--- 7 files changed, 89 insertions(+), 18 deletions(-) diff --git a/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx b/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx index d75685d866e..660af9683c0 100644 --- a/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx +++ b/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx @@ -92,7 +92,12 @@ export const TypeaheadItem: React.FC = (props: Props) => { highlightParts={item.highlightParts} > ) : ( - + )} ); diff --git a/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx b/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx index d5ff9246194..0357e78cc87 100644 --- a/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx +++ b/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx @@ -18,6 +18,7 @@ import { import PromQlLanguageProvider from '../../prometheus/language_provider'; import LokiLanguageProvider from '../language_provider'; +import { escapeLabelValueInExactSelector, escapeLabelValueInRegexSelector } from '../language_utils'; // Hard limit on labels to render const MAX_LABEL_COUNT = 1000; @@ -67,9 +68,9 @@ export function buildSelector(labels: SelectableLabel[]): string { if (label.selected && label.values && label.values.length > 0) { const selectedValues = label.values.filter((value) => value.selected).map((value) => value.name); if (selectedValues.length > 1) { - selectedLabels.push(`${label.name}=~"${selectedValues.join('|')}"`); + selectedLabels.push(`${label.name}=~"${selectedValues.map(escapeLabelValueInRegexSelector).join('|')}"`); } else if (selectedValues.length === 1) { - selectedLabels.push(`${label.name}="${selectedValues[0]}"`); + selectedLabels.push(`${label.name}="${escapeLabelValueInExactSelector(selectedValues[0])}"`); } } } diff --git a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx index 1be894153a0..c72286df18b 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx @@ -17,7 +17,7 @@ import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValue import { LokiDatasource } from '../datasource'; import LokiLanguageProvider from '../language_provider'; -import { shouldRefreshLabels } from '../language_utils'; +import { escapeLabelValueInSelector, shouldRefreshLabels } from '../language_utils'; import { LokiQuery, LokiOptions } from '../types'; import { LokiLabelBrowser } from './LokiLabelBrowser'; @@ -47,17 +47,26 @@ function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadTe case 'context-label-values': { // Always add quotes and remove existing ones instead + let suggestionModified = ''; + if (!typeaheadText.match(/^(!?=~?"|")/)) { - suggestion = `"${suggestion}`; + suggestionModified = '"'; } + + suggestionModified += escapeLabelValueInSelector(suggestion, typeaheadText); + if (DOMUtil.getNextCharacter() !== '"') { - suggestion = `${suggestion}"`; + suggestionModified += '"'; } + + suggestion = suggestionModified; + break; } default: } + return suggestion; } diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index e47abeecf95..b790bd86f42 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -453,7 +453,7 @@ describe('LokiDatasource', () => { ]); await lastValueFrom(ds.query(options as any)); expect(ds.runRangeQuery).toBeCalledWith( - { expr: 'rate({bar="baz",job="foo",k1=~"v.*",k2=~"v\\\\\'.*"} |= "bar" [5m])' }, + { expr: 'rate({bar="baz",job="foo",k1=~"v\\\\.\\\\*",k2=~"v\'\\\\.\\\\*"} |= "bar" [5m])' }, expect.anything() ); }); @@ -767,6 +767,16 @@ describe('LokiDatasource', () => { expect(result.expr).toEqual('{bar="baz",job="grafana"}'); }); + it('then the correctly escaped label should be added for logs query', () => { + const query: LokiQuery = { refId: 'A', expr: '{bar="baz"}' }; + const action = { key: 'job', value: '\\test', type: 'ADD_FILTER' }; + const ds = createLokiDSForTests(); + const result = ds.modifyQuery(query, action); + + expect(result.refId).toEqual('A'); + expect(result.expr).toEqual('{bar="baz",job="\\\\test"}'); + }); + it('then the correct label should be added for metrics query', () => { const query: LokiQuery = { refId: 'A', expr: 'rate({bar="baz"}[5m])' }; const action = { key: 'job', value: 'grafana', type: 'ADD_FILTER' }; @@ -811,6 +821,16 @@ describe('LokiDatasource', () => { expect(result.expr).toEqual('{bar="baz",job!="grafana"}'); }); + it('then the correctly escaped label should be added for logs query', () => { + const query: LokiQuery = { refId: 'A', expr: '{bar="baz"}' }; + const action = { key: 'job', value: '"test', type: 'ADD_FILTER_OUT' }; + const ds = createLokiDSForTests(); + const result = ds.modifyQuery(query, action); + + expect(result.refId).toEqual('A'); + expect(result.expr).toEqual('{bar="baz",job!="\\"test"}'); + }); + it('then the correct label should be added for metrics query', () => { const query: LokiQuery = { refId: 'A', expr: 'rate({bar="baz"}[5m])' }; const action = { key: 'job', value: 'grafana', type: 'ADD_FILTER_OUT' }; diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index cc0ec9cafb6..70f2b40d074 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -49,6 +49,7 @@ import { addLabelToQuery } from './add_label_to_query'; import { transformBackendResult } from './backendResultTransformer'; import { DEFAULT_RESOLUTION } from './components/LokiOptionFields'; import LanguageProvider from './language_provider'; +import { escapeLabelValueInSelector } from './language_utils'; import { LiveStreams, LokiLiveTarget } from './live_streams'; import { addParsedLabelToQuery, getNormalizedLokiQuery, queryHasPipeParser } from './query_utils'; import { lokiResultsToTableModel, lokiStreamsToDataFrames, processRangeQueryResponse } from './result_transformer'; @@ -825,10 +826,6 @@ export class LokiDatasource expr = adhocFilters.reduce((acc: string, filter: { key?: any; operator?: any; value?: any }) => { const { key, operator } = filter; let { value } = filter; - if (operator === '=~' || operator === '!~') { - value = lokiRegularEscape(value); - } - return this.addLabelToQuery(acc, key, value, operator, true); }, expr); @@ -843,11 +840,13 @@ export class LokiDatasource // Override to make sure that we use label as actual label and not parsed label notParsedLabelOverride?: boolean ) { + let escapedValue = escapeLabelValueInSelector(value.toString(), operator); + if (queryHasPipeParser(queryExpr) && !isMetricsQuery(queryExpr) && !notParsedLabelOverride) { // If query has parser, we treat all labels as parsed and use | key="value" syntax - return addParsedLabelToQuery(queryExpr, key, value, operator); + return addParsedLabelToQuery(queryExpr, key, escapedValue, operator); } else { - return addLabelToQuery(queryExpr, key, value, operator, true); + return addLabelToQuery(queryExpr, key, escapedValue, operator, true); } } diff --git a/public/app/plugins/datasource/loki/language_utils.ts b/public/app/plugins/datasource/loki/language_utils.ts index c7c140022fa..65bd5cde569 100644 --- a/public/app/plugins/datasource/loki/language_utils.ts +++ b/public/app/plugins/datasource/loki/language_utils.ts @@ -17,3 +17,37 @@ export function shouldRefreshLabels(range?: TimeRange, prevRange?: TimeRange): b } return false; } + +// Loki regular-expressions use the RE2 syntax (https://github.com/google/re2/wiki/Syntax), +// so every character that matches something in that list has to be escaped. +// the list of meta characters is: *+?()|\.[]{}^$ +// we make a javascript regular expression that matches those characters: +const RE2_METACHARACTERS = /[*+?()|\\.\[\]{}^$]/g; +function escapeLokiRegexp(value: string): string { + return value.replace(RE2_METACHARACTERS, '\\$&'); +} + +// based on the openmetrics-documentation, the 3 symbols we have to handle are: +// - \n ... the newline character +// - \ ... the backslash character +// - " ... the double-quote character +export function escapeLabelValueInExactSelector(labelValue: string): string { + return labelValue.replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/"/g, '\\"'); +} + +export function escapeLabelValueInRegexSelector(labelValue: string): string { + return escapeLabelValueInExactSelector(escapeLokiRegexp(labelValue)); +} + +export function escapeLabelValueInSelector(labelValue: string, selector?: string): string { + return isRegexSelector(selector) + ? escapeLabelValueInRegexSelector(labelValue) + : escapeLabelValueInExactSelector(labelValue); +} + +export function isRegexSelector(selector?: string) { + if (selector && (selector.includes('=~') || selector.includes('!~'))) { + return true; + } + return false; +} diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx index 4ea05365853..13fff38993b 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx @@ -8,6 +8,7 @@ import { OperationsEditorRow } from 'app/plugins/datasource/prometheus/querybuil import { QueryBuilderLabelFilter } from 'app/plugins/datasource/prometheus/querybuilder/shared/types'; import { LokiDatasource } from '../../datasource'; +import { escapeLabelValueInSelector } from '../../language_utils'; import { lokiQueryModeller } from '../LokiQueryModeller'; import { LokiOperationId, LokiVisualQuery } from '../types'; @@ -49,15 +50,17 @@ export const LokiQueryBuilder = React.memo(({ datasource, query, nested, return []; } + let values; const labelsToConsider = query.labels.filter((x) => x !== forLabel); if (labelsToConsider.length === 0) { - return await datasource.languageProvider.fetchLabelValues(forLabel.label); + values = await datasource.languageProvider.fetchLabelValues(forLabel.label); + } else { + const expr = lokiQueryModeller.renderLabels(labelsToConsider); + const result = await datasource.languageProvider.fetchSeriesLabels(expr); + values = result[datasource.interpolateString(forLabel.label)]; } - const expr = lokiQueryModeller.renderLabels(labelsToConsider); - const result = await datasource.languageProvider.fetchSeriesLabels(expr); - const forLabelInterpolated = datasource.interpolateString(forLabel.label); - return result[forLabelInterpolated] ?? []; + return values ? values.map((v) => escapeLabelValueInSelector(v, forLabel.op)) : []; // Escape values in return }; const labelFilterError: string | undefined = useMemo(() => {