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.
This commit is contained in:
Alexander Kubyshkin
2022-05-04 13:49:04 +03:00
committed by GitHub
parent b0f41b9772
commit f00ffb190c
7 changed files with 89 additions and 18 deletions

View File

@ -92,7 +92,12 @@ export const TypeaheadItem: React.FC<Props> = (props: Props) => {
highlightParts={item.highlightParts} highlightParts={item.highlightParts}
></PartialHighlighter> ></PartialHighlighter>
) : ( ) : (
<Highlighter textToHighlight={label} searchWords={[prefix ?? '']} highlightClassName={highlightClassName} /> <Highlighter
textToHighlight={label}
searchWords={[prefix ?? '']}
autoEscape={true}
highlightClassName={highlightClassName}
/>
)} )}
</li> </li>
); );

View File

@ -18,6 +18,7 @@ import {
import PromQlLanguageProvider from '../../prometheus/language_provider'; import PromQlLanguageProvider from '../../prometheus/language_provider';
import LokiLanguageProvider from '../language_provider'; import LokiLanguageProvider from '../language_provider';
import { escapeLabelValueInExactSelector, escapeLabelValueInRegexSelector } from '../language_utils';
// Hard limit on labels to render // Hard limit on labels to render
const MAX_LABEL_COUNT = 1000; const MAX_LABEL_COUNT = 1000;
@ -67,9 +68,9 @@ export function buildSelector(labels: SelectableLabel[]): string {
if (label.selected && label.values && label.values.length > 0) { if (label.selected && label.values && label.values.length > 0) {
const selectedValues = label.values.filter((value) => value.selected).map((value) => value.name); const selectedValues = label.values.filter((value) => value.selected).map((value) => value.name);
if (selectedValues.length > 1) { if (selectedValues.length > 1) {
selectedLabels.push(`${label.name}=~"${selectedValues.join('|')}"`); selectedLabels.push(`${label.name}=~"${selectedValues.map(escapeLabelValueInRegexSelector).join('|')}"`);
} else if (selectedValues.length === 1) { } else if (selectedValues.length === 1) {
selectedLabels.push(`${label.name}="${selectedValues[0]}"`); selectedLabels.push(`${label.name}="${escapeLabelValueInExactSelector(selectedValues[0])}"`);
} }
} }
} }

View File

@ -17,7 +17,7 @@ import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValue
import { LokiDatasource } from '../datasource'; import { LokiDatasource } from '../datasource';
import LokiLanguageProvider from '../language_provider'; import LokiLanguageProvider from '../language_provider';
import { shouldRefreshLabels } from '../language_utils'; import { escapeLabelValueInSelector, shouldRefreshLabels } from '../language_utils';
import { LokiQuery, LokiOptions } from '../types'; import { LokiQuery, LokiOptions } from '../types';
import { LokiLabelBrowser } from './LokiLabelBrowser'; import { LokiLabelBrowser } from './LokiLabelBrowser';
@ -47,17 +47,26 @@ function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadTe
case 'context-label-values': { case 'context-label-values': {
// Always add quotes and remove existing ones instead // Always add quotes and remove existing ones instead
let suggestionModified = '';
if (!typeaheadText.match(/^(!?=~?"|")/)) { if (!typeaheadText.match(/^(!?=~?"|")/)) {
suggestion = `"${suggestion}`; suggestionModified = '"';
} }
suggestionModified += escapeLabelValueInSelector(suggestion, typeaheadText);
if (DOMUtil.getNextCharacter() !== '"') { if (DOMUtil.getNextCharacter() !== '"') {
suggestion = `${suggestion}"`; suggestionModified += '"';
} }
suggestion = suggestionModified;
break; break;
} }
default: default:
} }
return suggestion; return suggestion;
} }

View File

@ -453,7 +453,7 @@ describe('LokiDatasource', () => {
]); ]);
await lastValueFrom(ds.query(options as any)); await lastValueFrom(ds.query(options as any));
expect(ds.runRangeQuery).toBeCalledWith( 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() expect.anything()
); );
}); });
@ -767,6 +767,16 @@ describe('LokiDatasource', () => {
expect(result.expr).toEqual('{bar="baz",job="grafana"}'); 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', () => { it('then the correct label should be added for metrics query', () => {
const query: LokiQuery = { refId: 'A', expr: 'rate({bar="baz"}[5m])' }; const query: LokiQuery = { refId: 'A', expr: 'rate({bar="baz"}[5m])' };
const action = { key: 'job', value: 'grafana', type: 'ADD_FILTER' }; const action = { key: 'job', value: 'grafana', type: 'ADD_FILTER' };
@ -811,6 +821,16 @@ describe('LokiDatasource', () => {
expect(result.expr).toEqual('{bar="baz",job!="grafana"}'); 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', () => { it('then the correct label should be added for metrics query', () => {
const query: LokiQuery = { refId: 'A', expr: 'rate({bar="baz"}[5m])' }; const query: LokiQuery = { refId: 'A', expr: 'rate({bar="baz"}[5m])' };
const action = { key: 'job', value: 'grafana', type: 'ADD_FILTER_OUT' }; const action = { key: 'job', value: 'grafana', type: 'ADD_FILTER_OUT' };

View File

@ -49,6 +49,7 @@ import { addLabelToQuery } from './add_label_to_query';
import { transformBackendResult } from './backendResultTransformer'; import { transformBackendResult } from './backendResultTransformer';
import { DEFAULT_RESOLUTION } from './components/LokiOptionFields'; import { DEFAULT_RESOLUTION } from './components/LokiOptionFields';
import LanguageProvider from './language_provider'; import LanguageProvider from './language_provider';
import { escapeLabelValueInSelector } from './language_utils';
import { LiveStreams, LokiLiveTarget } from './live_streams'; import { LiveStreams, LokiLiveTarget } from './live_streams';
import { addParsedLabelToQuery, getNormalizedLokiQuery, queryHasPipeParser } from './query_utils'; import { addParsedLabelToQuery, getNormalizedLokiQuery, queryHasPipeParser } from './query_utils';
import { lokiResultsToTableModel, lokiStreamsToDataFrames, processRangeQueryResponse } from './result_transformer'; 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 }) => { expr = adhocFilters.reduce((acc: string, filter: { key?: any; operator?: any; value?: any }) => {
const { key, operator } = filter; const { key, operator } = filter;
let { value } = filter; let { value } = filter;
if (operator === '=~' || operator === '!~') {
value = lokiRegularEscape(value);
}
return this.addLabelToQuery(acc, key, value, operator, true); return this.addLabelToQuery(acc, key, value, operator, true);
}, expr); }, expr);
@ -843,11 +840,13 @@ export class LokiDatasource
// Override to make sure that we use label as actual label and not parsed label // Override to make sure that we use label as actual label and not parsed label
notParsedLabelOverride?: boolean notParsedLabelOverride?: boolean
) { ) {
let escapedValue = escapeLabelValueInSelector(value.toString(), operator);
if (queryHasPipeParser(queryExpr) && !isMetricsQuery(queryExpr) && !notParsedLabelOverride) { if (queryHasPipeParser(queryExpr) && !isMetricsQuery(queryExpr) && !notParsedLabelOverride) {
// If query has parser, we treat all labels as parsed and use | key="value" syntax // 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 { } else {
return addLabelToQuery(queryExpr, key, value, operator, true); return addLabelToQuery(queryExpr, key, escapedValue, operator, true);
} }
} }

View File

@ -17,3 +17,37 @@ export function shouldRefreshLabels(range?: TimeRange, prevRange?: TimeRange): b
} }
return false; 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;
}

View File

@ -8,6 +8,7 @@ import { OperationsEditorRow } from 'app/plugins/datasource/prometheus/querybuil
import { QueryBuilderLabelFilter } from 'app/plugins/datasource/prometheus/querybuilder/shared/types'; import { QueryBuilderLabelFilter } from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
import { LokiDatasource } from '../../datasource'; import { LokiDatasource } from '../../datasource';
import { escapeLabelValueInSelector } from '../../language_utils';
import { lokiQueryModeller } from '../LokiQueryModeller'; import { lokiQueryModeller } from '../LokiQueryModeller';
import { LokiOperationId, LokiVisualQuery } from '../types'; import { LokiOperationId, LokiVisualQuery } from '../types';
@ -49,15 +50,17 @@ export const LokiQueryBuilder = React.memo<Props>(({ datasource, query, nested,
return []; return [];
} }
let values;
const labelsToConsider = query.labels.filter((x) => x !== forLabel); const labelsToConsider = query.labels.filter((x) => x !== forLabel);
if (labelsToConsider.length === 0) { 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); return values ? values.map((v) => escapeLabelValueInSelector(v, forLabel.op)) : []; // Escape values in return
const result = await datasource.languageProvider.fetchSeriesLabels(expr);
const forLabelInterpolated = datasource.interpolateString(forLabel.label);
return result[forLabelInterpolated] ?? [];
}; };
const labelFilterError: string | undefined = useMemo(() => { const labelFilterError: string | undefined = useMemo(() => {