Transformer: Rename metrics based on regex (#29281)

* Grafana: Rename By Regex Transformer

* Removing unused deps

* Add scrollIntoView() to TranformTab.content()

* Exporting RenameByRegexTransformerOptions

* Add doc block to renameByRegex transformer

* Adding doc block for RenameByRegexTransformerOptions

* removing scrollIntoView() for transform tab

* Adding back in scrollIntoView() for transform panel

* Tests: fixes e2e tests

* Apply to displayName instead of just the name of the frame

* Rewrite docblock to match new functionality

* Adding documentation

* Changing TLD to domain name

* Fixing typo

* Update docs/sources/panels/transformations/types-options.md

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* Update docs/sources/panels/transformations/types-options.md

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* Update docs/sources/panels/transformations/types-options.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
This commit is contained in:
Chris Cowan
2020-12-09 03:31:13 -07:00
committed by GitHub
parent 3f2b28975c
commit 82b21fe35e
8 changed files with 394 additions and 0 deletions

View File

@@ -19,6 +19,7 @@ Grafana comes with the following transformations:
- [Concatenate fields](#concatenate-fields) - [Concatenate fields](#concatenate-fields)
- [Group by](#group-by) - [Group by](#group-by)
- [Merge](#merge) - [Merge](#merge)
- [Rename by regex](#rename-by-regex)
Keep reading for detailed descriptions of each type of transformation and the options available for each, as well as suggestions on how to use them. Keep reading for detailed descriptions of each type of transformation and the options available for each, as well as suggestions on how to use them.
@@ -398,3 +399,17 @@ When you have more than one condition, you can choose if you want the action (in
In the example above we chose **Match all** because we wanted to include the rows that have a temperature lower than 30 _AND_ an altitude higher than 100. If we wanted to include the rows that have a temperature lower than 30 _OR_ an altitude higher than 100 instead, then we would select **Match any**. This would include the first row in the original data, which has a temperature of 32°C (does not match the first condition) but an altitude of 101 (which matches the second condition), so it is included. In the example above we chose **Match all** because we wanted to include the rows that have a temperature lower than 30 _AND_ an altitude higher than 100. If we wanted to include the rows that have a temperature lower than 30 _OR_ an altitude higher than 100 instead, then we would select **Match any**. This would include the first row in the original data, which has a temperature of 32°C (does not match the first condition) but an altitude of 101 (which matches the second condition), so it is included.
Conditions that are invalid or incompletely configured are ignored. Conditions that are invalid or incompletely configured are ignored.
## Rename by regex
Use this transformation to rename parts of the query results using a regular expression and replacement pattern.
You can specify a regular expression, which is only applied to matches, along with a replacement pattern that support back references. For example, let's imagine you're visualizing CPU usage per host and you want to remove the domain name. You could set the regex to `([^\.]+)\..+` and the replacement pattern to `$1`, `web-01.example.com` would become `web-01`.
In the following example, we are stripping the prefix from event types. In the before image, you can see everything is prefixed with `system.`
{{< docs-imagebox img="/img/docs/transformations/rename-by-regex-before-7-3.png" class="docs-image--no-shadow" max-width= "1100px" >}}
With the transformation applied, you can see we are left with just the remainder of the string.
{{< docs-imagebox img="/img/docs/transformations/rename-by-regex-after-7-3.png" class="docs-image--no-shadow" max-width= "1100px" >}}

View File

@@ -10,4 +10,5 @@ export {
standardTransformersRegistry, standardTransformersRegistry,
} from './standardTransformersRegistry'; } from './standardTransformersRegistry';
export { RegexpOrNamesMatcherOptions } from './matchers/nameMatcher'; export { RegexpOrNamesMatcherOptions } from './matchers/nameMatcher';
export { RenameByRegexTransformerOptions } from './transformers/renameByRegex';
export { outerJoinDataFrames } from './transformers/seriesToColumns'; export { outerJoinDataFrames } from './transformers/seriesToColumns';

View File

@@ -14,6 +14,7 @@ import { labelsToFieldsTransformer } from './transformers/labelsToFields';
import { ensureColumnsTransformer } from './transformers/ensureColumns'; import { ensureColumnsTransformer } from './transformers/ensureColumns';
import { groupByTransformer } from './transformers/groupBy'; import { groupByTransformer } from './transformers/groupBy';
import { mergeTransformer } from './transformers/merge'; import { mergeTransformer } from './transformers/merge';
import { renameByRegexTransformer } from './transformers/renameByRegex';
import { filterByValueTransformer } from './transformers/filterByValue'; import { filterByValueTransformer } from './transformers/filterByValue';
export const standardTransformers = { export const standardTransformers = {
@@ -35,4 +36,5 @@ export const standardTransformers = {
ensureColumnsTransformer, ensureColumnsTransformer,
groupByTransformer, groupByTransformer,
mergeTransformer, mergeTransformer,
renameByRegexTransformer,
}; };

View File

@@ -16,6 +16,7 @@ export enum DataTransformerID {
filterFieldsByName = 'filterFieldsByName', filterFieldsByName = 'filterFieldsByName',
filterFrames = 'filterFrames', filterFrames = 'filterFrames',
filterByRefId = 'filterByRefId', filterByRefId = 'filterByRefId',
renameByRegex = 'renameByRegex',
filterByValue = 'filterByValue', filterByValue = 'filterByValue',
noop = 'noop', noop = 'noop',
ensureColumns = 'ensureColumns', ensureColumns = 'ensureColumns',

View File

@@ -0,0 +1,180 @@
import { DataTransformerConfig, DataTransformerID, FieldType, toDataFrame, transformDataFrame } from '@grafana/data';
import { renameByRegexTransformer, RenameByRegexTransformerOptions } from './renameByRegex';
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
describe('Rename By Regex Transformer', () => {
beforeAll(() => {
mockTransformationsRegistry([renameByRegexTransformer]);
});
describe('when regex and replacement pattern', () => {
const data = toDataFrame({
name: 'web-01.example.com',
fields: [
{
name: 'Time',
type: FieldType.time,
config: { name: 'Time' },
values: [3000, 4000, 5000, 6000],
},
{
name: 'Value',
type: FieldType.number,
config: { displayName: 'web-01.example.com' },
values: [10000.3, 10000.4, 10000.5, 10000.6],
},
],
});
it('should rename matches using references', async () => {
const cfg: DataTransformerConfig<RenameByRegexTransformerOptions> = {
id: DataTransformerID.renameByRegex,
options: {
regex: '([^.]+).example.com',
renamePattern: '$1',
},
};
await expect(transformDataFrame([cfg], [data])).toEmitValuesWith(received => {
const data = received[0];
const frame = data[0];
expect(frame.fields).toMatchInlineSnapshot(`
Array [
Object {
"config": Object {
"name": "Time",
},
"name": "Time",
"state": Object {
"displayName": "Time",
},
"type": "time",
"values": Array [
3000,
4000,
5000,
6000,
],
},
Object {
"config": Object {
"displayName": "web-01",
},
"name": "Value",
"state": Object {
"displayName": "web-01",
},
"type": "number",
"values": Array [
10000.3,
10000.4,
10000.5,
10000.6,
],
},
]
`);
});
});
it('should not rename misses', async () => {
const cfg: DataTransformerConfig<RenameByRegexTransformerOptions> = {
id: DataTransformerID.renameByRegex,
options: {
regex: '([^.]+).bad-domain.com',
renamePattern: '$1',
},
};
await expect(transformDataFrame([cfg], [data])).toEmitValuesWith(received => {
const data = received[0];
const frame = data[0];
expect(frame.fields).toMatchInlineSnapshot(`
Array [
Object {
"config": Object {
"name": "Time",
},
"name": "Time",
"state": Object {
"displayName": "Time",
},
"type": "time",
"values": Array [
3000,
4000,
5000,
6000,
],
},
Object {
"config": Object {
"displayName": "web-01.example.com",
},
"name": "Value",
"state": Object {
"displayName": "web-01.example.com",
},
"type": "number",
"values": Array [
10000.3,
10000.4,
10000.5,
10000.6,
],
},
]
`);
});
});
it('should not rename with empty regex and repacement pattern', async () => {
const cfg: DataTransformerConfig<RenameByRegexTransformerOptions> = {
id: DataTransformerID.renameByRegex,
options: {
regex: '',
renamePattern: '',
},
};
await expect(transformDataFrame([cfg], [data])).toEmitValuesWith(received => {
const data = received[0];
const frame = data[0];
expect(frame.fields).toMatchInlineSnapshot(`
Array [
Object {
"config": Object {
"displayName": "Time",
"name": "Time",
},
"name": "Time",
"state": Object {
"displayName": "Time",
},
"type": "time",
"values": Array [
3000,
4000,
5000,
6000,
],
},
Object {
"config": Object {
"displayName": "web-01.example.com",
},
"name": "Value",
"state": Object {
"displayName": "web-01.example.com",
},
"type": "number",
"values": Array [
10000.3,
10000.4,
10000.5,
10000.6,
],
},
]
`);
});
});
});
});

View File

@@ -0,0 +1,62 @@
import { DataTransformerID } from './ids';
import { DataTransformerInfo } from '../../types/transformations';
import { map } from 'rxjs/operators';
import { DataFrame } from '../../types/dataFrame';
import { getFieldDisplayName } from '../../field/fieldState';
/**
* Options for renameByRegexTransformer
*
* @public
*/
export interface RenameByRegexTransformerOptions {
regex: string;
renamePattern: string;
}
/**
* Replaces the displayName of a field by applying a regular expression
* to match the name and a pattern for the replacement.
*
* @public
*/
export const renameByRegexTransformer: DataTransformerInfo<RenameByRegexTransformerOptions> = {
id: DataTransformerID.renameByRegex,
name: 'Rename fields by regex',
description: 'Rename fields based on regular expression by users.',
defaultOptions: {
regex: '(.*)',
renamePattern: '$1',
},
/**
* Return a modified copy of the series. If the transform is not or should not
* be applied, just return the input series
*/
operator: options => source =>
source.pipe(
map(data => {
if (!Array.isArray(data) || data.length === 0) {
return data;
}
return data.map(renameFieldsByRegex(options));
})
),
};
const renameFieldsByRegex = (options: RenameByRegexTransformerOptions) => (frame: DataFrame) => {
const regex = new RegExp(options.regex);
const fields = frame.fields.map(field => {
const displayName = getFieldDisplayName(field, frame);
if (!regex.test(displayName)) {
return field;
}
const newDisplayName = displayName.replace(regex, options.renamePattern);
return {
...field,
config: { ...field.config, displayName: newDisplayName },
state: { ...field.state, displayName: newDisplayName },
};
});
return { ...frame, fields };
};

View File

@@ -0,0 +1,131 @@
import React from 'react';
import {
DataTransformerID,
standardTransformers,
TransformerRegistyItem,
TransformerUIProps,
stringToJsRegex,
} from '@grafana/data';
import { Field, Input } from '@grafana/ui';
import { css } from 'emotion';
import { RenameByRegexTransformerOptions } from '@grafana/data/src/transformations/transformers/renameByRegex';
interface RenameByRegexTransformerEditorProps extends TransformerUIProps<RenameByRegexTransformerOptions> {}
interface RenameByRegexTransformerEditorState {
regex?: string;
renamePattern?: string;
isRegexValid?: boolean;
}
export class RenameByRegexTransformerEditor extends React.PureComponent<
RenameByRegexTransformerEditorProps,
RenameByRegexTransformerEditorState
> {
constructor(props: RenameByRegexTransformerEditorProps) {
super(props);
this.state = {
regex: props.options.regex,
renamePattern: props.options.renamePattern,
isRegexValid: true,
};
}
handleRegexChange = (e: React.FormEvent<HTMLInputElement>) => {
const regex = e.currentTarget.value;
let isRegexValid = true;
if (regex) {
try {
if (regex) {
stringToJsRegex(regex);
}
} catch (e) {
isRegexValid = false;
}
}
this.setState(previous => ({ ...previous, regex, isRegexValid }));
};
handleRenameChange = (e: React.FormEvent<HTMLInputElement>) => {
const renamePattern = e.currentTarget.value;
this.setState(previous => ({ ...previous, renamePattern }));
};
handleRegexBlur = (e: React.FocusEvent<HTMLInputElement>) => {
const regex = e.currentTarget.value;
let isRegexValid = true;
try {
if (regex) {
stringToJsRegex(regex);
}
} catch (e) {
isRegexValid = false;
}
this.setState({ isRegexValid }, () => {
if (isRegexValid) {
this.props.onChange({ ...this.props.options, regex });
}
});
};
handleRenameBlur = (e: React.FocusEvent<HTMLInputElement>) => {
const renamePattern = e.currentTarget.value;
this.setState({ renamePattern }, () => this.props.onChange({ ...this.props.options, renamePattern }));
};
render() {
const { regex, renamePattern, isRegexValid } = this.state;
return (
<>
<div className="gf-form-inline">
<div className="gf-form gf-form--grow">
<div className="gf-form-label width-8">Match</div>
<Field
invalid={!isRegexValid}
error={!isRegexValid ? 'Invalid pattern' : undefined}
className={css`
margin-bottom: 0;
`}
>
<Input
placeholder="Regular expression pattern"
value={regex || ''}
onChange={this.handleRegexChange}
onBlur={this.handleRegexBlur}
width={25}
/>
</Field>
</div>
</div>
<div className="gf-form-inline">
<div className="gf-form gf-form--grow">
<div className="gf-form-label width-8">Replace</div>
<Field
className={css`
margin-bottom: 0;
`}
>
<Input
placeholder="Replacement pattern"
value={renamePattern || ''}
onChange={this.handleRenameChange}
onBlur={this.handleRenameBlur}
width={25}
/>
</Field>
</div>
</div>
</>
);
}
}
export const renameByRegexTransformRegistryItem: TransformerRegistyItem<RenameByRegexTransformerOptions> = {
id: DataTransformerID.renameByRegex,
editor: RenameByRegexTransformerEditor,
transformation: standardTransformers.renameByRegexTransformer,
name: 'Rename by regex',
description: 'Renames part of the query result by using regular expression with placeholders.',
};

View File

@@ -11,11 +11,13 @@ import { groupByTransformRegistryItem } from '../components/TransformersUI/Group
import { mergeTransformerRegistryItem } from '../components/TransformersUI/MergeTransformerEditor'; import { mergeTransformerRegistryItem } from '../components/TransformersUI/MergeTransformerEditor';
import { seriesToRowsTransformerRegistryItem } from '../components/TransformersUI/SeriesToRowsTransformerEditor'; import { seriesToRowsTransformerRegistryItem } from '../components/TransformersUI/SeriesToRowsTransformerEditor';
import { concatenateTransformRegistryItem } from '../components/TransformersUI/ConcatenateTransformerEditor'; import { concatenateTransformRegistryItem } from '../components/TransformersUI/ConcatenateTransformerEditor';
import { renameByRegexTransformRegistryItem } from '../components/TransformersUI/RenameByRegexTransformer';
export const getStandardTransformers = (): Array<TransformerRegistyItem<any>> => { export const getStandardTransformers = (): Array<TransformerRegistyItem<any>> => {
return [ return [
reduceTransformRegistryItem, reduceTransformRegistryItem,
filterFieldsByNameTransformRegistryItem, filterFieldsByNameTransformRegistryItem,
renameByRegexTransformRegistryItem,
filterFramesByRefIdTransformRegistryItem, filterFramesByRefIdTransformRegistryItem,
filterByValueTransformRegistryItem, filterByValueTransformRegistryItem,
organizeFieldsTransformRegistryItem, organizeFieldsTransformRegistryItem,