From d363c368536d1d0da82392d4aa102953ceb0b25e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Thu, 30 Sep 2021 15:50:02 +0200 Subject: [PATCH] Prometheus: metrics browser: handle label values with special characters (#39713) * prometheus: handle label-values with special characters * added comment --- .../components/PrometheusMetricsBrowser.tsx | 5 +- .../datasource/prometheus/datasource.ts | 3 ++ .../prometheus/language_utils.test.ts | 50 ++++++++++++++++++- .../datasource/prometheus/language_utils.ts | 25 ++++++++++ 4 files changed, 80 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/components/PrometheusMetricsBrowser.tsx b/public/app/plugins/datasource/prometheus/components/PrometheusMetricsBrowser.tsx index 3f93d2ab99c..4ebf4fc9fc7 100644 --- a/public/app/plugins/datasource/prometheus/components/PrometheusMetricsBrowser.tsx +++ b/public/app/plugins/datasource/prometheus/components/PrometheusMetricsBrowser.tsx @@ -10,6 +10,7 @@ import { BrowserLabel as PromLabel, } from '@grafana/ui'; import PromQlLanguageProvider from '../language_provider'; +import { escapeLabelValueInExactSelector, escapeLabelValueInRegexSelector } from '../language_utils'; import { css, cx } from '@emotion/css'; import store from 'app/core/store'; import { FixedSizeList } from 'react-window'; @@ -65,12 +66,12 @@ export function buildSelector(labels: SelectableLabel[]): string { if ((label.name === METRIC_LABEL || 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) { if (label.name === METRIC_LABEL) { singleMetric = selectedValues[0]; } else { - selectedLabels.push(`${label.name}="${selectedValues[0]}"`); + selectedLabels.push(`${label.name}="${escapeLabelValueInExactSelector(selectedValues[0])}"`); } } } diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index b59347d454f..82d093c6fe7 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -900,6 +900,9 @@ export function extractRuleMappingFromGroups(groups: any[]) { ); } +// NOTE: these two functions are very similar to the escapeLabelValueIn* functions +// in language_utils.ts, but they are not exactly the same algorithm, and we found +// no way to reuse one in the another or vice versa. export function prometheusRegularEscape(value: any) { return typeof value === 'string' ? value.replace(/\\/g, '\\\\').replace(/'/g, "\\\\'") : value; } diff --git a/public/app/plugins/datasource/prometheus/language_utils.test.ts b/public/app/plugins/datasource/prometheus/language_utils.test.ts index b6069383a6c..144bffdb040 100644 --- a/public/app/plugins/datasource/prometheus/language_utils.test.ts +++ b/public/app/plugins/datasource/prometheus/language_utils.test.ts @@ -1,4 +1,10 @@ -import { expandRecordingRules, fixSummariesMetadata, parseSelector } from './language_utils'; +import { + escapeLabelValueInExactSelector, + escapeLabelValueInRegexSelector, + expandRecordingRules, + fixSummariesMetadata, + parseSelector, +} from './language_utils'; describe('parseSelector()', () => { let parsed; @@ -171,3 +177,45 @@ describe('expandRecordingRules()', () => { ).toBe('rate(fooA{label1="value1",label2="value2"}[])/ rate(fooB{label3="value3"}[])'); }); }); + +describe('escapeLabelValueInExactSelector()', () => { + it('handles newline characters', () => { + expect(escapeLabelValueInExactSelector('t\nes\nt')).toBe('t\\nes\\nt'); + }); + + it('handles backslash characters', () => { + expect(escapeLabelValueInExactSelector('t\\es\\t')).toBe('t\\\\es\\\\t'); + }); + + it('handles double-quote characters', () => { + expect(escapeLabelValueInExactSelector('t"es"t')).toBe('t\\"es\\"t'); + }); + + it('handles all together', () => { + expect(escapeLabelValueInExactSelector('t\\e"st\nl\nab"e\\l')).toBe('t\\\\e\\"st\\nl\\nab\\"e\\\\l'); + }); +}); + +describe('escapeLabelValueInRegexSelector()', () => { + it('handles newline characters', () => { + expect(escapeLabelValueInRegexSelector('t\nes\nt')).toBe('t\\nes\\nt'); + }); + + it('handles backslash characters', () => { + expect(escapeLabelValueInRegexSelector('t\\es\\t')).toBe('t\\\\\\\\es\\\\\\\\t'); + }); + + it('handles double-quote characters', () => { + expect(escapeLabelValueInRegexSelector('t"es"t')).toBe('t\\"es\\"t'); + }); + + it('handles regex-meaningful characters', () => { + expect(escapeLabelValueInRegexSelector('t+es$t')).toBe('t\\\\+es\\\\$t'); + }); + + it('handles all together', () => { + expect(escapeLabelValueInRegexSelector('t\\e"s+t\nl\n$ab"e\\l')).toBe( + 't\\\\\\\\e\\"s\\\\+t\\nl\\n\\\\$ab\\"e\\\\\\\\l' + ); + }); +}); diff --git a/public/app/plugins/datasource/prometheus/language_utils.ts b/public/app/plugins/datasource/prometheus/language_utils.ts index ba58c5d9673..e4859fdd15c 100644 --- a/public/app/plugins/datasource/prometheus/language_utils.ts +++ b/public/app/plugins/datasource/prometheus/language_utils.ts @@ -236,3 +236,28 @@ export function limitSuggestions(items: string[]) { export function addLimitInfo(items: any[] | undefined): string { return items && items.length >= SUGGESTIONS_LIMIT ? `, limited to the first ${SUGGESTIONS_LIMIT} received items` : ''; } + +// NOTE: the following 2 exported functions are very similar to the prometheus*Escape +// functions in datasource.ts, but they are not exactly the same algorithm, and we found +// no way to reuse one in the another or vice versa. + +// Prometheus 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 metacharacters is: *+?()|\.[]{}^$ +// we make a javascript regular expression that matches those characters: +const RE2_METACHARACTERS = /[*+?()|\\.\[\]{}^$]/g; +function escapePrometheusRegexp(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(escapePrometheusRegexp(labelValue)); +}