Transformations: Extract JSON Paths (#59400)
* Added new extractJSONPath transformer Co-authored-by: Galen <galen.kistler@grafana.com>
This commit is contained in:
@@ -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": ""
|
||||||
|
}
|
||||||
@@ -177,6 +177,13 @@ local dashboard = grafana.dashboard;
|
|||||||
id: 0,
|
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') +
|
dashboard.new('gauge-multi-series', import '../dev-dashboards/panel-gauge/gauge-multi-series.json') +
|
||||||
resource.addMetadata('folder', 'dev-dashboards') +
|
resource.addMetadata('folder', 'dev-dashboards') +
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,17 +2,19 @@ import React from 'react';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
DataTransformerID,
|
DataTransformerID,
|
||||||
|
TransformerRegistryItem,
|
||||||
|
TransformerUIProps,
|
||||||
FieldNamePickerConfigSettings,
|
FieldNamePickerConfigSettings,
|
||||||
SelectableValue,
|
SelectableValue,
|
||||||
StandardEditorsRegistryItem,
|
StandardEditorsRegistryItem,
|
||||||
TransformerRegistryItem,
|
|
||||||
TransformerUIProps,
|
|
||||||
} from '@grafana/data';
|
} 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 { FieldNamePicker } from '@grafana/ui/src/components/MatchersUI/FieldNamePicker';
|
||||||
|
|
||||||
import { ExtractFieldsOptions, extractFieldsTransformer } from './extractFields';
|
import { JSONPathEditor } from './components/JSONPathEditor';
|
||||||
import { FieldExtractorID, fieldExtractors } from './fieldExtractors';
|
import { extractFieldsTransformer } from './extractFields';
|
||||||
|
import { fieldExtractors } from './fieldExtractors';
|
||||||
|
import { ExtractFieldsOptions, FieldExtractorID, JSONPath } from './types';
|
||||||
|
|
||||||
const fieldNamePickerSettings: StandardEditorsRegistryItem<string, FieldNamePickerConfigSettings> = {
|
const fieldNamePickerSettings: StandardEditorsRegistryItem<string, FieldNamePickerConfigSettings> = {
|
||||||
settings: {
|
settings: {
|
||||||
@@ -43,13 +45,31 @@ export const extractFieldsTransformerEditor: React.FC<TransformerUIProps<Extract
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onJSONPathsChange = (jsonPaths: JSONPath[]) => {
|
||||||
|
onChange({
|
||||||
|
...options,
|
||||||
|
jsonPaths,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const onToggleReplace = () => {
|
const onToggleReplace = () => {
|
||||||
|
if (options.replace) {
|
||||||
|
options.keepTime = false;
|
||||||
|
}
|
||||||
|
|
||||||
onChange({
|
onChange({
|
||||||
...options,
|
...options,
|
||||||
replace: !options.replace,
|
replace: !options.replace,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onToggleKeepTime = () => {
|
||||||
|
onChange({
|
||||||
|
...options,
|
||||||
|
keepTime: !options.keepTime,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const format = fieldExtractors.selectOptions(options.format ? [options.format] : undefined);
|
const format = fieldExtractors.selectOptions(options.format ? [options.format] : undefined);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -75,11 +95,19 @@ export const extractFieldsTransformerEditor: React.FC<TransformerUIProps<Extract
|
|||||||
/>
|
/>
|
||||||
</InlineField>
|
</InlineField>
|
||||||
</InlineFieldRow>
|
</InlineFieldRow>
|
||||||
|
{options.format === 'json' && <JSONPathEditor options={options.jsonPaths ?? []} onChange={onJSONPathsChange} />}
|
||||||
<InlineFieldRow>
|
<InlineFieldRow>
|
||||||
<InlineField label={'Replace all fields'} labelWidth={16}>
|
<InlineField label={'Replace all fields'} labelWidth={16}>
|
||||||
<InlineSwitch value={options.replace ?? false} onChange={onToggleReplace} />
|
<InlineSwitch value={options.replace ?? false} onChange={onToggleReplace} />
|
||||||
</InlineField>
|
</InlineField>
|
||||||
</InlineFieldRow>
|
</InlineFieldRow>
|
||||||
|
{options.replace && (
|
||||||
|
<InlineFieldRow>
|
||||||
|
<InlineField label={'Keep time'} labelWidth={16}>
|
||||||
|
<InlineSwitch value={options.keepTime ?? false} onChange={onToggleKeepTime} />
|
||||||
|
</InlineField>
|
||||||
|
</InlineFieldRow>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { ArrayVector, DataFrame, Field, FieldType } from '@grafana/data';
|
||||||
import { toDataFrame } from '@grafana/data/src/dataframe/processDataFrame';
|
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', () => {
|
describe('Fields from JSON', () => {
|
||||||
it('adds fields from JSON in string', async () => {
|
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 = [
|
const appl = [
|
||||||
[
|
[
|
||||||
'1636678740000000000',
|
'1636678740000000000',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isString } from 'lodash';
|
import { isString, get } from 'lodash';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -12,13 +12,8 @@ import {
|
|||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { findField } from 'app/features/dimensions';
|
import { findField } from 'app/features/dimensions';
|
||||||
|
|
||||||
import { FieldExtractorID, fieldExtractors } from './fieldExtractors';
|
import { fieldExtractors } from './fieldExtractors';
|
||||||
|
import { ExtractFieldsOptions, FieldExtractorID, JSONPath } from './types';
|
||||||
export interface ExtractFieldsOptions {
|
|
||||||
source?: string;
|
|
||||||
format?: FieldExtractorID;
|
|
||||||
replace?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const extractFieldsTransformer: SynchronousDataTransformerInfo<ExtractFieldsOptions> = {
|
export const extractFieldsTransformer: SynchronousDataTransformerInfo<ExtractFieldsOptions> = {
|
||||||
id: DataTransformerID.extractFields,
|
id: DataTransformerID.extractFields,
|
||||||
@@ -40,7 +35,9 @@ function addExtractedFields(frame: DataFrame, options: ExtractFieldsOptions): Da
|
|||||||
if (!options.source) {
|
if (!options.source) {
|
||||||
return frame;
|
return frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
const source = findField(frame, options.source);
|
const source = findField(frame, options.source);
|
||||||
|
|
||||||
if (!source) {
|
if (!source) {
|
||||||
// this case can happen when there are multiple queries
|
// this case can happen when there are multiple queries
|
||||||
return frame;
|
return frame;
|
||||||
@@ -57,6 +54,7 @@ function addExtractedFields(frame: DataFrame, options: ExtractFieldsOptions): Da
|
|||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
let obj = source.values.get(i);
|
let obj = source.values.get(i);
|
||||||
|
|
||||||
if (isString(obj)) {
|
if (isString(obj)) {
|
||||||
try {
|
try {
|
||||||
obj = ext.parse(obj);
|
obj = ext.parse(obj);
|
||||||
@@ -64,6 +62,22 @@ function addExtractedFields(frame: DataFrame, options: ExtractFieldsOptions): Da
|
|||||||
obj = {}; // empty
|
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)) {
|
for (const [key, val] of Object.entries(obj)) {
|
||||||
let buffer = values.get(key);
|
let buffer = values.get(key);
|
||||||
if (buffer == null) {
|
if (buffer == null) {
|
||||||
@@ -85,9 +99,17 @@ function addExtractedFields(frame: DataFrame, options: ExtractFieldsOptions): Da
|
|||||||
} as Field;
|
} as Field;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (options.keepTime) {
|
||||||
|
const sourceTime = findField(frame, 'Time') || findField(frame, 'time');
|
||||||
|
if (sourceTime) {
|
||||||
|
fields.unshift(sourceTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!options.replace) {
|
if (!options.replace) {
|
||||||
fields.unshift(...frame.fields);
|
fields.unshift(...frame.fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...frame,
|
...frame,
|
||||||
fields,
|
fields,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { fieldExtractors, FieldExtractorID } from './fieldExtractors';
|
import { fieldExtractors } from './fieldExtractors';
|
||||||
|
import { FieldExtractorID } from './types';
|
||||||
|
|
||||||
describe('Extract fields from text', () => {
|
describe('Extract fields from text', () => {
|
||||||
it('JSON extractor', async () => {
|
it('JSON extractor', async () => {
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { Registry, RegistryItem } from '@grafana/data';
|
import { Registry, RegistryItem } from '@grafana/data';
|
||||||
|
|
||||||
export enum FieldExtractorID {
|
import { FieldExtractorID } from './types';
|
||||||
JSON = 'json',
|
|
||||||
KeyValues = 'kvp',
|
|
||||||
Auto = 'auto',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FieldExtractor extends RegistryItem {
|
export interface FieldExtractor extends RegistryItem {
|
||||||
parse: (v: string) => Record<string, any> | undefined;
|
parse: (v: string) => Record<string, any> | undefined;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user