mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
3f2b28975c
commit
82b21fe35e
@ -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',
|
||||
|
@ -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,
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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 };
|
||||
};
|
@ -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.',
|
||||
};
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user