From f830d8772a5feeebaffdd1dd3b72d19b5d1c4d8e Mon Sep 17 00:00:00 2001 From: yenalsnavaj <10382039+yenalsnavaj@users.noreply.github.com> Date: Wed, 4 Nov 2020 08:31:38 +0000 Subject: [PATCH] Variables: Adds named capture groups to variable regex (#28625) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Dashboard: Add named capture groups to variable query regex Variable query regex are able to use 'text' and 'value' named capture groups to allow for separate display text to be extracted from the query result. e.g. Using a regex of /foo="(?[^"]+)|bar="(?[^"]+)/g on a query result of metric{foo="FOO", bar="BAR"} would result in the variable value being set to 'BAR' but display text being set to 'FOO' Resolves #21076 * Improve regex capture group documentation * Update docs/sources/variables/filter-variables-with-regex.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Hugo Häggmark * Use text capture if value capture does not match This is to keep the behaviour consistent with the current behavior. See discussion https://github.com/grafana/grafana/pull/28625/files#r516490942 * Improve regex field placeholder and tooltip message To make the feature more discoverable to users the place holder example now includes the named capture groups. The tool tip message also includes a reference and link to the documentation. Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> Co-authored-by: Hugo Häggmark --- .../variables/filter-variables-with-regex.md | 31 ++++++ .../variables/query/QueryVariableEditor.tsx | 16 ++- .../features/variables/query/reducer.test.ts | 97 +++++++++++++++++++ .../app/features/variables/query/reducer.ts | 32 +++++- 4 files changed, 169 insertions(+), 7 deletions(-) diff --git a/docs/sources/variables/filter-variables-with-regex.md b/docs/sources/variables/filter-variables-with-regex.md index 4a79dabdaeb..2f6c1c63fe7 100644 --- a/docs/sources/variables/filter-variables-with-regex.md +++ b/docs/sources/variables/filter-variables-with-regex.md @@ -79,4 +79,35 @@ demo.robustperception.io:9090 demo.robustperception.io:9093 demo.robustperception.io:9100 ``` + +## Filter and modify using named text and value capture groups + +Using named capture groups, you can capture separate 'text' and 'value' parts from the options returned by the variable query. This allows the variable drop-down list to contain a friendly name for each value that can be selected. + +For example, when querying the `node_hwmon_chip_names` Prometheus metric, the `chip_name` is a lot friendlier that the `chip` value. So the following variable query result: + +```text +node_hwmon_chip_names{chip="0000:d7:00_0_0000:d8:00_0",chip_name="enp216s0f0np0"} 1 +node_hwmon_chip_names{chip="0000:d7:00_0_0000:d8:00_1",chip_name="enp216s0f0np1"} 1 +node_hwmon_chip_names{chip="0000:d7:00_0_0000:d8:00_2",chip_name="enp216s0f0np2"} 1 +node_hwmon_chip_names{chip="0000:d7:00_0_0000:d8:00_3",chip_name="enp216s0f0np3"} 1 +``` + +Passed through the following Regex: + +```regex +/chip_name="(?[^"]+)|chip="(?[^"]+)/g ``` + +Would produce the following drop-down list: + +```text +Display Name Value +------------ ------------------------- +enp216s0f0np0 0000:d7:00_0_0000:d8:00_0 +enp216s0f0np1 0000:d7:00_0_0000:d8:00_1 +enp216s0f0np2 0000:d7:00_0_0000:d8:00_2 +enp216s0f0np3 0000:d7:00_0_0000:d8:00_3 +``` + +**Note:** Only `text` and `value` capture group names are supported. diff --git a/public/app/features/variables/query/QueryVariableEditor.tsx b/public/app/features/variables/query/QueryVariableEditor.tsx index f43c7456d2d..25853601cee 100644 --- a/public/app/features/variables/query/QueryVariableEditor.tsx +++ b/public/app/features/variables/query/QueryVariableEditor.tsx @@ -193,14 +193,26 @@ export class QueryVariableEditorUnConnected extends PureComponent
+ Optional, if you want to extract part of a series name or metric node segment. Named capture groups + can be used to separate the display text and value ( + + see examples + + ). +
+ } > Regex { }); }); + describe('when updateVariableOptions is dispatched and includeAll is false and regex is set and uses capture groups', () => { + it('normal regex should capture in order matches', () => { + const regex = '/somelabel="(?[^"]+).*somevalue="(?[^"]+)/i'; + const { initialState } = getVariableTestContext(adapter, { includeAll: false, regex }); + const metrics = [createMetric('A{somelabel="atext",somevalue="avalue"}'), createMetric('B')]; + const update = { results: metrics, templatedRegex: regex }; + const payload = toVariablePayload({ id: '0', type: 'query' }, update); + + reducerTester() + .givenReducer(queryVariableReducer, cloneDeep(initialState)) + .whenActionIsDispatched(updateVariableOptions(payload)) + .thenStateShouldEqual({ + ...initialState, + '0': ({ + ...initialState[0], + options: [{ text: 'atext', value: 'avalue', selected: false }], + } as unknown) as QueryVariableModel, + }); + }); + + it('global regex should capture out of order matches', () => { + const regex = '/somevalue="(?[^"]+)|somelabel="(?[^"]+)/gi'; + const { initialState } = getVariableTestContext(adapter, { includeAll: false, regex }); + const metrics = [createMetric('A{somelabel="atext",somevalue="avalue"}'), createMetric('B')]; + const update = { results: metrics, templatedRegex: regex }; + const payload = toVariablePayload({ id: '0', type: 'query' }, update); + + reducerTester() + .givenReducer(queryVariableReducer, cloneDeep(initialState)) + .whenActionIsDispatched(updateVariableOptions(payload)) + .thenStateShouldEqual({ + ...initialState, + '0': ({ + ...initialState[0], + options: [{ text: 'atext', value: 'avalue', selected: false }], + } as unknown) as QueryVariableModel, + }); + }); + + it('unmatched text capture will use value capture', () => { + const regex = '/somevalue="(?[^"]+)|somelabel="(?[^"]+)/gi'; + const { initialState } = getVariableTestContext(adapter, { includeAll: false, regex }); + const metrics = [createMetric('A{somename="atext",somevalue="avalue"}'), createMetric('B')]; + const update = { results: metrics, templatedRegex: regex }; + const payload = toVariablePayload({ id: '0', type: 'query' }, update); + + reducerTester() + .givenReducer(queryVariableReducer, cloneDeep(initialState)) + .whenActionIsDispatched(updateVariableOptions(payload)) + .thenStateShouldEqual({ + ...initialState, + '0': ({ + ...initialState[0], + options: [{ text: 'avalue', value: 'avalue', selected: false }], + } as unknown) as QueryVariableModel, + }); + }); + + it('unmatched value capture will use text capture', () => { + const regex = '/somevalue="(?[^"]+)|somelabel="(?[^"]+)/gi'; + const { initialState } = getVariableTestContext(adapter, { includeAll: false, regex }); + const metrics = [createMetric('A{somelabel="atext",somename="avalue"}'), createMetric('B')]; + const update = { results: metrics, templatedRegex: regex }; + const payload = toVariablePayload({ id: '0', type: 'query' }, update); + + reducerTester() + .givenReducer(queryVariableReducer, cloneDeep(initialState)) + .whenActionIsDispatched(updateVariableOptions(payload)) + .thenStateShouldEqual({ + ...initialState, + '0': ({ + ...initialState[0], + options: [{ text: 'atext', value: 'atext', selected: false }], + } as unknown) as QueryVariableModel, + }); + }); + + it('unmatched text capture and unmatched value capture returns empty state', () => { + const regex = '/somevalue="(?[^"]+)|somelabel="(?[^"]+)/gi'; + const { initialState } = getVariableTestContext(adapter, { includeAll: false, regex }); + const metrics = [createMetric('A{someother="atext",something="avalue"}'), createMetric('B')]; + const update = { results: metrics, templatedRegex: regex }; + const payload = toVariablePayload({ id: '0', type: 'query' }, update); + + reducerTester() + .givenReducer(queryVariableReducer, cloneDeep(initialState)) + .whenActionIsDispatched(updateVariableOptions(payload)) + .thenStateShouldEqual({ + ...initialState, + '0': ({ + ...initialState[0], + options: [{ text: 'None', value: '', selected: false, isNone: true }], + } as unknown) as QueryVariableModel, + }); + }); + }); + describe('when updateVariableTags is dispatched', () => { it('then state should be correct', () => { const { initialState } = getVariableTestContext(adapter); diff --git a/public/app/features/variables/query/reducer.ts b/public/app/features/variables/query/reducer.ts index ad77b03ce6b..d49120b6064 100644 --- a/public/app/features/variables/query/reducer.ts +++ b/public/app/features/variables/query/reducer.ts @@ -86,6 +86,18 @@ const sortVariableValues = (options: any[], sortOrder: VariableSort) => { return options; }; +const getAllMatches = (str: string, regex: RegExp): any => { + const results = {}; + let matches; + + do { + matches = regex.exec(str); + _.merge(results, matches); + } while (regex.global && matches); + + return results; +}; + const metricNamesToVariableValues = (variableRegEx: string, sort: VariableSort, metricNames: any[]) => { let regex, i, matches; let options: VariableOption[] = []; @@ -109,13 +121,23 @@ const metricNamesToVariableValues = (variableRegEx: string, sort: VariableSort, } if (regex) { - matches = regex.exec(value); - if (!matches) { + matches = getAllMatches(value, regex); + + if (_.isEmpty(matches)) { continue; } - if (matches.length > 1) { - value = matches[1]; - text = matches[1]; + + if (matches.groups && matches.groups.value && matches.groups.text) { + value = matches.groups.value; + text = matches.groups.text; + } else if (matches.groups && matches.groups.value) { + value = matches.groups.value; + text = value; + } else if (matches.groups && matches.groups.text) { + text = matches.groups.text; + value = text; + } else if (matches['1']) { + value = matches['1']; } }