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}
></PartialHighlighter>
) : (
<Highlighter textToHighlight={label} searchWords={[prefix ?? '']} highlightClassName={highlightClassName} />
<Highlighter
textToHighlight={label}
searchWords={[prefix ?? '']}
autoEscape={true}
highlightClassName={highlightClassName}
/>
)}
</li>
);

View File

@ -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])}"`);
}
}
}

View File

@ -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;
}

View File

@ -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' };

View File

@ -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);
}
}

View File

@ -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;
}

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 { 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<Props>(({ 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);
const forLabelInterpolated = datasource.interpolateString(forLabel.label);
return result[forLabelInterpolated] ?? [];
values = result[datasource.interpolateString(forLabel.label)];
}
return values ? values.map((v) => escapeLabelValueInSelector(v, forLabel.op)) : []; // Escape values in return
};
const labelFilterError: string | undefined = useMemo(() => {