Transformations: Extract JSON Paths (#59400)

* Added new extractJSONPath transformer
Co-authored-by: Galen <galen.kistler@grafana.com>
This commit is contained in:
Niklas 2023-01-18 13:59:20 +01:00 committed by GitHub
parent b573b19ca3
commit 3debfd0ca7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 717 additions and 20 deletions

View File

@ -0,0 +1,316 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [
"gdev",
"transform",
"transformations",
"extract",
"json"
],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 8,
"links": [],
"liveNow": false,
"panels": [
{
"id": 2,
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 0
},
"type": "table",
"title": "Extracting individual values",
"transformations": [
{
"id": "extractFields",
"options": {
"format": "json",
"jsonPaths": [
{
"alias": "Temperature",
"path": "[8].testdata.source1.value1"
},
{
"alias": "Primes",
"path": "[8].testdata.source2[1][3]"
}
],
"keepTime": true,
"replace": true,
"source": "Value"
}
}
],
"datasource": {
"type": "testdata",
"uid": "gdev-testdata"
},
"pluginVersion": "9.4.0-pre",
"fieldConfig": {
"defaults": {
"custom": {
"align": "auto",
"displayMode": "auto",
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"color": {
"mode": "thresholds"
}
},
"overrides": []
},
"options": {
"showHeader": true,
"footer": {
"show": false,
"reducer": [
"sum"
],
"countRows": false,
"fields": ""
}
},
"targets": [
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata"
},
"rawFrameContent": "[{\"schema\":{\"refId\":\"A\",\"meta\":{\"channel\":\"ds/bHGPS1h4z/1s/test\",\"transformations\":[\"extractFields\",\"extractFields\",\"extractFields\"]},\"fields\":[{\"name\":\"Time\",\"type\":\"time\",\"config\":{\"custom\":{\"align\":\"auto\",\"displayMode\":\"auto\",\"inspect\":false},\"color\":{\"mode\":\"thresholds\"},\"thresholds\":{\"mode\":\"absolute\",\"steps\":[{\"color\":\"green\",\"value\":null},{\"color\":\"red\",\"value\":80}]}}},{\"name\":\"Value\",\"type\":\"other\",\"config\":{\"custom\":{\"align\":\"auto\",\"displayMode\":\"auto\",\"inspect\":false},\"color\":{\"mode\":\"thresholds\"},\"mappings\":[],\"thresholds\":{\"mode\":\"absolute\",\"steps\":[{\"color\":\"green\",\"value\":null},{\"color\":\"red\",\"value\":80}]}}}]},\"data\":{\"values\":[[1673543683471,1673543689063,1673543695050],[[\"2023-01-12T17:14:44.419Z\",62,141,79,29,79,-29,29,{\"testdata\":{\"source1\":{\"value1\":9,\"value2\":18},\"source2\":[[0,1,2,3,4,5,6,7,8,9],[7,11,13,17,19,23,27,29]]}}],[\"2023-01-12T17:14:50.050Z\",62,143,81,29,81,-29,29,{\"testdata\":{\"source1\":{\"value1\":10,\"value2\":20},\"source2\":[[1,2,3,4,5,6,7,8,9,10],[11,13,17,19,23,27,29,31]]}}],[\"2023-01-12T17:14:55.050Z\",61,146,80,22,85,-28,28,{\"testdata\":{\"source1\":{\"value1\":11,\"value2\":22},\"source2\":[[3,4,5,6,7,8,9,10,11,12],[13,17,19,23,27,29,31,37,41]]}}]]]}}]",
"refId": "A",
"scenarioId": "raw_frame"
}
],
"description": "Some data sources (for example MQTT) might be consuming incomparable metrics packaged in the same JSON payload. We can use this extract fields transformation's JSON option to select the specific fields we want, and alias the values to help classify unlabeled or unstructured data."
},
{
"id": 3,
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 0
},
"type": "timeseries",
"title": "Visualizing extracted JSON",
"transformations": [
{
"id": "extractFields",
"options": {
"format": "json",
"jsonPaths": [
{
"alias": "Temperature",
"path": "[8].testdata.source1.value1"
}
],
"keepTime": true,
"replace": true,
"source": "Value"
}
}
],
"datasource": {
"type": "testdata",
"uid": "gdev-testdata"
},
"pluginVersion": "9.4.0-pre",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"lineInterpolation": "linear",
"barAlignment": 0,
"lineWidth": 1,
"fillOpacity": 0,
"gradientMode": "none",
"spanNulls": false,
"showPoints": "auto",
"pointSize": 5,
"stacking": {
"mode": "none",
"group": "A"
},
"axisPlacement": "auto",
"axisLabel": "",
"axisColorMode": "text",
"scaleDistribution": {
"type": "linear"
},
"axisCenteredZero": false,
"hideFrom": {
"tooltip": false,
"viz": false,
"legend": false
},
"thresholdsStyle": {
"mode": "off"
}
},
"color": {
"mode": "palette-classic"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "celsius"
},
"overrides": []
},
"options": {
"tooltip": {
"mode": "single",
"sort": "none"
},
"legend": {
"showLegend": true,
"displayMode": "list",
"placement": "bottom",
"calcs": []
}
},
"targets": [
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata"
},
"rawFrameContent": "[{\"schema\":{\"refId\":\"A\",\"meta\":{\"channel\":\"ds/bHGPS1h4z/1s/test\",\"transformations\":[\"extractFields\",\"extractFields\",\"extractFields\"]},\"fields\":[{\"name\":\"Time\",\"type\":\"time\",\"config\":{\"custom\":{\"align\":\"auto\",\"displayMode\":\"auto\",\"inspect\":false},\"color\":{\"mode\":\"thresholds\"},\"thresholds\":{\"mode\":\"absolute\",\"steps\":[{\"color\":\"green\",\"value\":null},{\"color\":\"red\",\"value\":80}]}}},{\"name\":\"Value\",\"type\":\"other\",\"config\":{\"custom\":{\"align\":\"auto\",\"displayMode\":\"auto\",\"inspect\":false},\"color\":{\"mode\":\"thresholds\"},\"mappings\":[],\"thresholds\":{\"mode\":\"absolute\",\"steps\":[{\"color\":\"green\",\"value\":null},{\"color\":\"red\",\"value\":80}]}}}]},\"data\":{\"values\":[[1673543683471,1673543689063,1673543695050],[[\"2023-01-12T17:14:44.419Z\",62,141,79,29,79,-29,29,{\"testdata\":{\"source1\":{\"value1\":9,\"value2\":18},\"source2\":[[0,1,2,3,4,5,6,7,8,9],[7,11,13,17,19,23,27,29]]}}],[\"2023-01-12T17:14:50.050Z\",62,143,81,29,81,-29,29,{\"testdata\":{\"source1\":{\"value1\":10,\"value2\":20},\"source2\":[[1,2,3,4,5,6,7,8,9,10],[11,13,17,19,23,27,29,31]]}}],[\"2023-01-12T17:14:55.050Z\",61,146,80,22,85,-28,28,{\"testdata\":{\"source1\":{\"value1\":11,\"value2\":22},\"source2\":[[3,4,5,6,7,8,9,10,11,12],[13,17,19,23,27,29,31,37,41]]}}]]]}}]",
"refId": "A",
"scenarioId": "raw_frame"
}
],
"description": ""
},
{
"id": 4,
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 9
},
"type": "table",
"title": "Raw data",
"transformations": [],
"datasource": {
"type": "testdata",
"uid": "gdev-testdata"
},
"pluginVersion": "9.4.0-pre",
"fieldConfig": {
"defaults": {
"custom": {
"align": "auto",
"displayMode": "auto",
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"color": {
"mode": "thresholds"
}
},
"overrides": []
},
"options": {
"showHeader": true,
"footer": {
"show": false,
"reducer": [
"sum"
],
"countRows": false,
"fields": ""
}
},
"targets": [
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata"
},
"rawFrameContent": "[{\"schema\":{\"refId\":\"A\",\"meta\":{\"channel\":\"ds/bHGPS1h4z/1s/test\",\"transformations\":[\"extractFields\",\"extractFields\",\"extractFields\"]},\"fields\":[{\"name\":\"Time\",\"type\":\"time\",\"config\":{\"custom\":{\"align\":\"auto\",\"displayMode\":\"auto\",\"inspect\":false},\"color\":{\"mode\":\"thresholds\"},\"thresholds\":{\"mode\":\"absolute\",\"steps\":[{\"color\":\"green\",\"value\":null},{\"color\":\"red\",\"value\":80}]}}},{\"name\":\"Value\",\"type\":\"other\",\"config\":{\"custom\":{\"align\":\"auto\",\"displayMode\":\"auto\",\"inspect\":false},\"color\":{\"mode\":\"thresholds\"},\"mappings\":[],\"thresholds\":{\"mode\":\"absolute\",\"steps\":[{\"color\":\"green\",\"value\":null},{\"color\":\"red\",\"value\":80}]}}}]},\"data\":{\"values\":[[1673543683471,1673543689063,1673543695050],[[\"2023-01-12T17:14:44.419Z\",62,141,79,29,79,-29,29,{\"testdata\":{\"source1\":{\"value1\":9,\"value2\":18},\"source2\":[[0,1,2,3,4,5,6,7,8,9],[7,11,13,17,19,23,27,29]]}}],[\"2023-01-12T17:14:50.050Z\",62,143,81,29,81,-29,29,{\"testdata\":{\"source1\":{\"value1\":10,\"value2\":20},\"source2\":[[1,2,3,4,5,6,7,8,9,10],[11,13,17,19,23,27,29,31]]}}],[\"2023-01-12T17:14:55.050Z\",61,146,80,22,85,-28,28,{\"testdata\":{\"source1\":{\"value1\":11,\"value2\":22},\"source2\":[[3,4,5,6,7,8,9,10,11,12],[13,17,19,23,27,29,31,37,41]]}}]]]}}]",
"refId": "A",
"scenarioId": "raw_frame"
}
],
"description": ""
}
],
"revision": 1,
"schemaVersion": 37,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "2023-01-12T17:14:42.652Z",
"to": "2023-01-12T17:14:55.358Z"
},
"timepicker": {},
"timezone": "",
"title": "Test extractFields JSON",
"uid": "pD4vPYhVz",
"version": 3,
"weekStart": ""
}

View File

@ -177,6 +177,13 @@ local dashboard = grafana.dashboard;
id: 0,
}
},
dashboard.new('extract-json-paths', import '../dev-dashboards/transforms/extract-json-paths.json') +
resource.addMetadata('folder', 'dev-dashboards') +
{
spec+: {
id: 0,
}
},
dashboard.new('gauge-multi-series', import '../dev-dashboards/panel-gauge/gauge-multi-series.json') +
resource.addMetadata('folder', 'dev-dashboards') +
{

View File

@ -2,17 +2,19 @@ import React from 'react';
import {
DataTransformerID,
TransformerRegistryItem,
TransformerUIProps,
FieldNamePickerConfigSettings,
SelectableValue,
StandardEditorsRegistryItem,
TransformerRegistryItem,
TransformerUIProps,
} from '@grafana/data';
import { InlineField, InlineFieldRow, InlineSwitch, Select } from '@grafana/ui';
import { InlineField, InlineFieldRow, Select, InlineSwitch } from '@grafana/ui';
import { FieldNamePicker } from '@grafana/ui/src/components/MatchersUI/FieldNamePicker';
import { ExtractFieldsOptions, extractFieldsTransformer } from './extractFields';
import { FieldExtractorID, fieldExtractors } from './fieldExtractors';
import { JSONPathEditor } from './components/JSONPathEditor';
import { extractFieldsTransformer } from './extractFields';
import { fieldExtractors } from './fieldExtractors';
import { ExtractFieldsOptions, FieldExtractorID, JSONPath } from './types';
const fieldNamePickerSettings: StandardEditorsRegistryItem<string, FieldNamePickerConfigSettings> = {
settings: {
@ -43,13 +45,31 @@ export const extractFieldsTransformerEditor: React.FC<TransformerUIProps<Extract
});
};
const onJSONPathsChange = (jsonPaths: JSONPath[]) => {
onChange({
...options,
jsonPaths,
});
};
const onToggleReplace = () => {
if (options.replace) {
options.keepTime = false;
}
onChange({
...options,
replace: !options.replace,
});
};
const onToggleKeepTime = () => {
onChange({
...options,
keepTime: !options.keepTime,
});
};
const format = fieldExtractors.selectOptions(options.format ? [options.format] : undefined);
return (
@ -75,11 +95,19 @@ export const extractFieldsTransformerEditor: React.FC<TransformerUIProps<Extract
/>
</InlineField>
</InlineFieldRow>
{options.format === 'json' && <JSONPathEditor options={options.jsonPaths ?? []} onChange={onJSONPathsChange} />}
<InlineFieldRow>
<InlineField label={'Replace all fields'} labelWidth={16}>
<InlineSwitch value={options.replace ?? false} onChange={onToggleReplace} />
</InlineField>
</InlineFieldRow>
{options.replace && (
<InlineFieldRow>
<InlineField label={'Keep time'} labelWidth={16}>
<InlineSwitch value={options.keepTime ?? false} onChange={onToggleKeepTime} />
</InlineField>
</InlineFieldRow>
)}
</div>
);
};

View File

@ -0,0 +1,130 @@
import { css, cx } from '@emotion/css';
import React, { useState } from 'react';
import { Button, InlineField, InlineFieldRow, IconButton, Input } from '@grafana/ui';
import { JSONPath } from '../types';
interface Props {
options: JSONPath[];
onChange: (options: JSONPath[]) => void;
}
export function JSONPathEditor({ options, onChange }: Props) {
const [paths, setPaths] = useState<JSONPath[]>(options);
const tooltips = getTooltips();
const style = getStyle();
const addJSONPath = () => {
paths.push({ path: '' });
setPaths([...paths]);
onBlur();
};
const removeJSONPath = (keyPath: number) => {
if (paths) {
paths.splice(keyPath, 1);
setPaths([...paths]);
onBlur();
}
};
const onJSONPathChange = (event: React.SyntheticEvent<HTMLInputElement>, keyPath: number, type: 'alias' | 'path') => {
if (paths) {
if (type === 'alias') {
paths[keyPath].alias = event.currentTarget.value ?? '';
} else {
paths[keyPath].path = event.currentTarget.value ?? '';
}
setPaths([...paths]);
}
};
const onBlur = () => {
onChange(paths);
};
return (
<ol className={cx(style.list)}>
{paths &&
paths.map((path: JSONPath, key: number) => (
<li key={key}>
<InlineFieldRow>
<InlineField label="Field" tooltip={tooltips.field} grow>
<Input
onBlur={onBlur}
onChange={(event: React.SyntheticEvent<HTMLInputElement>) => onJSONPathChange(event, key, 'path')}
value={path.path}
placeholder='A valid json path, e.g. "object.value1" or "object.value2[0]"'
/>
</InlineField>
<InlineField label="Alias" tooltip={tooltips.alias}>
<Input
width={12}
value={path.alias}
onBlur={onBlur}
onChange={(event: React.SyntheticEvent<HTMLInputElement>) => onJSONPathChange(event, key, 'alias')}
/>
</InlineField>
<InlineField className={cx(style.removeIcon)}>
<IconButton onClick={() => removeJSONPath(key)} name={'trash-alt'} />
</InlineField>
</InlineFieldRow>
</li>
))}
<InlineField>
<Button icon={'plus'} onClick={() => addJSONPath()} variant={'secondary'}>
Add path
</Button>
</InlineField>
</ol>
);
}
const getTooltips = () => {
const mapValidPaths = [
{ path: 'object', description: '=> extract fields from object' },
{ path: 'object.value1', description: '=> extract value1' },
{ path: 'object.value2', description: '=> extract value2' },
{ path: 'object.value2[0]', description: '=> extract value2 first element' },
{ path: 'object.value2[1]', description: '=> extract value2 second element' },
];
return {
field: (
<div>
A valid path of an json object.
<div>
<strong>JSON Value:</strong>
</div>
<pre>
<code>
{['{', ' "object": {', ' "value1": "hello world"', ' "value2": [1, 2, 3, 4]', ' }', '}'].join('\n')}
</code>
</pre>
<strong>Valid Paths:</strong>
{mapValidPaths.map((value, key) => {
return (
<p key={key}>
<code>{value.path}</code>
<i>{value.description}</i>
</p>
);
})}
</div>
),
alias: 'An alias name for the variable in the dashboard. If left blank the given path will be used.',
};
};
function getStyle() {
return {
list: css`
margin-left: 20px;
`,
removeIcon: css`
margin: 0 0 0 4px;
align-items: center;
`,
};
}

View File

@ -1,6 +1,8 @@
import { ArrayVector, DataFrame, Field, FieldType } from '@grafana/data';
import { toDataFrame } from '@grafana/data/src/dataframe/processDataFrame';
import { ExtractFieldsOptions, extractFieldsTransformer } from './extractFields';
import { extractFieldsTransformer } from './extractFields';
import { ExtractFieldsOptions, FieldExtractorID } from './types';
describe('Fields from JSON', () => {
it('adds fields from JSON in string', async () => {
@ -40,8 +42,186 @@ describe('Fields from JSON', () => {
}
`);
});
it('Get nested path values', () => {
const cfg: ExtractFieldsOptions = {
replace: true,
source: 'JSON',
format: FieldExtractorID.JSON,
jsonPaths: [
{ path: 'object.nestedArray[0]' },
{ path: 'object.nestedArray[1]' },
{ path: 'object.nestedString' },
],
};
const ctx = { interpolate: (v: string) => v };
const frames = extractFieldsTransformer.transformer(cfg, ctx)([testDataFrame]);
expect(frames.length).toEqual(1);
expect(frames[0]).toMatchInlineSnapshot(`
{
"fields": [
{
"config": {},
"name": "object.nestedArray[0]",
"type": "number",
"values": [
1,
],
},
{
"config": {},
"name": "object.nestedArray[1]",
"type": "number",
"values": [
2,
],
},
{
"config": {},
"name": "object.nestedString",
"type": "string",
"values": [
"Hallo World",
],
},
],
"length": 1,
}
`);
});
it('Keep time field on replace', () => {
const cfg: ExtractFieldsOptions = {
replace: true,
keepTime: true,
source: 'JSON',
format: FieldExtractorID.JSON,
jsonPaths: [
{ path: 'object.nestedArray[2]' },
{ path: 'object.nestedArray[3]' },
{ path: 'object.nestedString' },
],
};
const ctx = { interpolate: (v: string) => v };
const frames = extractFieldsTransformer.transformer(cfg, ctx)([testDataFrame]);
expect(frames.length).toEqual(1);
expect(frames[0]).toMatchInlineSnapshot(`
{
"fields": [
{
"config": {},
"name": "Time",
"state": {
"displayName": "Time",
"multipleFrames": false,
},
"type": "time",
"values": [
1669638911691,
],
},
{
"config": {},
"name": "object.nestedArray[2]",
"type": "number",
"values": [
3,
],
},
{
"config": {},
"name": "object.nestedArray[3]",
"type": "number",
"values": [
4,
],
},
{
"config": {},
"name": "object.nestedString",
"type": "string",
"values": [
"Hallo World",
],
},
],
"length": 1,
}
`);
});
it('Path is invalid', () => {
const cfg: ExtractFieldsOptions = {
replace: true,
source: 'JSON',
format: FieldExtractorID.JSON,
jsonPaths: [{ path: 'object.nestedString' }, { path: 'invalid.path' }],
};
const ctx = { interpolate: (v: string) => v };
const frames = extractFieldsTransformer.transformer(cfg, ctx)([testDataFrame]);
expect(frames.length).toEqual(1);
expect(frames[0]).toMatchInlineSnapshot(`
{
"fields": [
{
"config": {},
"name": "object.nestedString",
"type": "string",
"values": [
"Hallo World",
],
},
{
"config": {},
"name": "invalid.path",
"type": "string",
"values": [
"Not Found",
],
},
],
"length": 1,
}
`);
});
});
const testFieldTime: Field = {
config: {},
name: 'Time',
type: FieldType.time,
values: new ArrayVector([1669638911691]),
};
const testFieldString: Field = {
config: {},
name: 'String',
type: FieldType.string,
values: new ArrayVector(['Hallo World']),
};
const testFieldJSON: Field = {
config: {},
name: 'JSON',
type: FieldType.string,
values: new ArrayVector([
JSON.stringify({
object: {
nestedArray: [1, 2, 3, 4],
nestedString: 'Hallo World',
},
}),
]),
};
const testDataFrame: DataFrame = {
fields: [testFieldTime, testFieldString, testFieldJSON],
length: 1,
};
const appl = [
[
'1636678740000000000',

View File

@ -1,4 +1,4 @@
import { isString } from 'lodash';
import { isString, get } from 'lodash';
import { map } from 'rxjs/operators';
import {
@ -12,13 +12,8 @@ import {
} from '@grafana/data';
import { findField } from 'app/features/dimensions';
import { FieldExtractorID, fieldExtractors } from './fieldExtractors';
export interface ExtractFieldsOptions {
source?: string;
format?: FieldExtractorID;
replace?: boolean;
}
import { fieldExtractors } from './fieldExtractors';
import { ExtractFieldsOptions, FieldExtractorID, JSONPath } from './types';
export const extractFieldsTransformer: SynchronousDataTransformerInfo<ExtractFieldsOptions> = {
id: DataTransformerID.extractFields,
@ -40,7 +35,9 @@ function addExtractedFields(frame: DataFrame, options: ExtractFieldsOptions): Da
if (!options.source) {
return frame;
}
const source = findField(frame, options.source);
if (!source) {
// this case can happen when there are multiple queries
return frame;
@ -57,6 +54,7 @@ function addExtractedFields(frame: DataFrame, options: ExtractFieldsOptions): Da
for (let i = 0; i < count; i++) {
let obj = source.values.get(i);
if (isString(obj)) {
try {
obj = ext.parse(obj);
@ -64,6 +62,22 @@ function addExtractedFields(frame: DataFrame, options: ExtractFieldsOptions): Da
obj = {}; // empty
}
}
if (options.format === FieldExtractorID.JSON && options.jsonPaths && options.jsonPaths?.length > 0) {
const newObj: { [k: string]: unknown } = {};
// filter out empty paths
const filteredPaths = options.jsonPaths.filter((path: JSONPath) => path.path);
if (filteredPaths.length > 0) {
filteredPaths.forEach((path: JSONPath) => {
const key = path.alias && path.alias.length > 0 ? path.alias : path.path;
newObj[key] = get(obj, path.path) ?? 'Not Found';
});
obj = newObj;
}
}
for (const [key, val] of Object.entries(obj)) {
let buffer = values.get(key);
if (buffer == null) {
@ -85,9 +99,17 @@ function addExtractedFields(frame: DataFrame, options: ExtractFieldsOptions): Da
} as Field;
});
if (options.keepTime) {
const sourceTime = findField(frame, 'Time') || findField(frame, 'time');
if (sourceTime) {
fields.unshift(sourceTime);
}
}
if (!options.replace) {
fields.unshift(...frame.fields);
}
return {
...frame,
fields,

View File

@ -1,4 +1,5 @@
import { fieldExtractors, FieldExtractorID } from './fieldExtractors';
import { fieldExtractors } from './fieldExtractors';
import { FieldExtractorID } from './types';
describe('Extract fields from text', () => {
it('JSON extractor', async () => {

View File

@ -1,10 +1,6 @@
import { Registry, RegistryItem } from '@grafana/data';
export enum FieldExtractorID {
JSON = 'json',
KeyValues = 'kvp',
Auto = 'auto',
}
import { FieldExtractorID } from './types';
export interface FieldExtractor extends RegistryItem {
parse: (v: string) => Record<string, any> | undefined;

View File

@ -0,0 +1,17 @@
export enum FieldExtractorID {
JSON = 'json',
KeyValues = 'kvp',
Auto = 'auto',
}
export interface JSONPath {
path: string;
alias?: string;
}
export interface ExtractFieldsOptions {
source?: string;
jsonPaths?: JSONPath[];
format?: FieldExtractorID;
replace?: boolean;
keepTime?: boolean;
}