Prometheus: metrics browser: handle label values with special characters (#39713) (#39858)

* prometheus: handle label-values with special characters

* added comment

(cherry picked from commit d363c36853)

Co-authored-by: Gábor Farkas <gabor.farkas@gmail.com>
This commit is contained in:
Grot (@grafanabot)
2021-10-01 16:11:03 +02:00
committed by GitHub
co-authored by Gábor Farkas
parent 5d2fb57dd8
commit ea030e8bbd
6 changed files with 249 additions and 226 deletions
+168 -216
View File
File diff suppressed because it is too large Load Diff
@@ -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])}"`);
}
}
}
@@ -864,6 +864,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;
}
@@ -1,4 +1,10 @@
import { expandRecordingRules, fixSummariesMetadata, parseSelector } from './language_utils';
import {
escapeLabelValueInExactSelector,
escapeLabelValueInRegexSelector,
expandRecordingRules,
fixSummariesMetadata,
parseSelector,
} from './language_utils';
describe('parseSelector()', () => {
let parsed;
@@ -168,3 +174,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'
);
});
});
@@ -239,3 +239,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));
}
+1 -7
View File
@@ -1,7 +1,7 @@
load('scripts/drone/vault.star', 'from_secret', 'github_token', 'pull_secret', 'drone_token')
grabpl_version = '2.4.6'
build_image = 'grafana/build-container:1.4.2'
build_image = 'grafana/build-container:1.4.3'
publish_image = 'grafana/grafana-ci-deploy:1.3.1'
grafana_docker_image = 'grafana/drone-grafana-docker:0.3.2'
deploy_docker_image = 'us.gcr.io/kubernetes-dev/drone/plugins/deploy-image'
@@ -63,8 +63,6 @@ def initialize_step(edition, platform, ver_mode, is_downstream=False, install_de
'curl -fLO https://github.com/jwilder/dockerize/releases/download/v$${DOCKERIZE_VERSION}/dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz',
'tar -C bin -xzvf dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz',
'rm dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz',
'mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30',
'mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list',
'yarn install --frozen-lockfile --no-progress',
])
if edition in ('enterprise', 'enterprise2'):
@@ -698,8 +696,6 @@ def postgres_integration_tests_step():
'POSTGRES_HOST': 'postgres',
},
'commands': [
'mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30',
'mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list',
'apt-get install -yq postgresql-client',
'./bin/dockerize -wait tcp://postgres:5432 -timeout 120s',
'psql -p 5432 -h postgres -U grafanatest -d grafanatest -f ' +
@@ -723,8 +719,6 @@ def mysql_integration_tests_step():
'MYSQL_HOST': 'mysql',
},
'commands': [
'mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30',
'mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list',
'apt-get install -yq default-mysql-client',
'./bin/dockerize -wait tcp://mysql:3306 -timeout 120s',
'cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root -prootpass',