Commit 82b21fe3 by Chris Cowan Committed by GitHub

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>
parent 3f2b2897
......@@ -19,6 +19,7 @@ Grafana comes with the following transformations:
- [Concatenate fields](#concatenate-fields)
- [Group by](#group-by)
- [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.
......@@ -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.
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" >}}
......@@ -10,4 +10,5 @@ export {
standardTransformersRegistry,
} from './standardTransformersRegistry';
export { RegexpOrNamesMatcherOptions } from './matchers/nameMatcher';
export { RenameByRegexTransformerOptions } from './transformers/renameByRegex';
export { outerJoinDataFrames } from './transformers/seriesToColumns';
......@@ -14,6 +14,7 @@ import { labelsToFieldsTransformer } from './transformers/labelsToFields';
import { ensureColumnsTransformer } from './transformers/ensureColumns';
import { groupByTransformer } from './transformers/groupBy';
import { mergeTransformer } from './transformers/merge';
import { renameByRegexTransformer } from './transformers/renameByRegex';
import { filterByValueTransformer } from './transformers/filterByValue';
export const standardTransformers = {
......@@ -35,4 +36,5 @@ export const standardTransformers = {
ensureColumnsTransformer,
groupByTransformer,
mergeTransformer,
renameByRegexTransformer,
};
......@@ -16,6 +16,7 @@ export enum DataTransformerID {
filterFieldsByName = 'filterFieldsByName',
filterFrames = 'filterFrames',
filterByRefId = 'filterByRefId',
renameByRegex = 'renameByRegex',
filterByValue = 'filterByValue',
noop = 'noop',
ensureColumns = 'ensureColumns',
......
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,
],
},
]
`);
});
});
});
});
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 };
};
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.',
};
......@@ -11,11 +11,13 @@ import { groupByTransformRegistryItem } from '../components/TransformersUI/Group
import { mergeTransformerRegistryItem } from '../components/TransformersUI/MergeTransformerEditor';
import { seriesToRowsTransformerRegistryItem } from '../components/TransformersUI/SeriesToRowsTransformerEditor';
import { concatenateTransformRegistryItem } from '../components/TransformersUI/ConcatenateTransformerEditor';
import { renameByRegexTransformRegistryItem } from '../components/TransformersUI/RenameByRegexTransformer';
export const getStandardTransformers = (): Array<TransformerRegistyItem<any>> => {
return [
reduceTransformRegistryItem,
filterFieldsByNameTransformRegistryItem,
renameByRegexTransformRegistryItem,
filterFramesByRefIdTransformRegistryItem,
filterByValueTransformRegistryItem,
organizeFieldsTransformRegistryItem,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment