mirror of
https://github.com/grafana/grafana.git
synced 2025-07-28 16:32:13 +08:00
* 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:

committed by
GitHub

parent
b0f41b9772
commit
f00ffb190c
@ -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>
|
||||
);
|
||||
|
@ -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])}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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' };
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
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(() => {
|
||||
|
Reference in New Issue
Block a user