mirror of
https://github.com/grafana/grafana.git
synced 2025-07-28 22:42:11 +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}
|
highlightParts={item.highlightParts}
|
||||||
></PartialHighlighter>
|
></PartialHighlighter>
|
||||||
) : (
|
) : (
|
||||||
<Highlighter textToHighlight={label} searchWords={[prefix ?? '']} highlightClassName={highlightClassName} />
|
<Highlighter
|
||||||
|
textToHighlight={label}
|
||||||
|
searchWords={[prefix ?? '']}
|
||||||
|
autoEscape={true}
|
||||||
|
highlightClassName={highlightClassName}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@ -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])}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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' };
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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 expr = lokiQueryModeller.renderLabels(labelsToConsider);
|
||||||
const result = await datasource.languageProvider.fetchSeriesLabels(expr);
|
const result = await datasource.languageProvider.fetchSeriesLabels(expr);
|
||||||
const forLabelInterpolated = datasource.interpolateString(forLabel.label);
|
values = result[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(() => {
|
const labelFilterError: string | undefined = useMemo(() => {
|
||||||
|
Reference in New Issue
Block a user