Use Data frames response format (#1099)
* Use data frames for numeric data * Use data frames for text data * Use data frames for IT services * fix multiple series * Convert to the wide format if possible * Fix table format for text data * Add refId to the data frames * Align time series from Zabbix API * Fill gaps with nulls * Fix moving average functions * Option for disabling data alignment * remove unused logging * Add labels to data frames * Detect units * Set min and max for if percent unit used * Use value mapping from Zabbix * Rename unitConverter -> convertZabbixUnit * More units * Add missing points in front of each series * Fix handling table data * fix db connector data frames handling * fix it services data frames handling * Detect all known grafana units * Chore: remove unused logging * Fix problems format * Debug logging: show original units * Add global option for disabling data alignment * Add tooltip for the disableDataAlignment feature * Add note about query options * Functions for aligning timeseries on the backend
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import _ from 'lodash';
|
||||
import TableModel from 'grafana/app/core/table_model';
|
||||
import * as c from './constants';
|
||||
import * as utils from './utils';
|
||||
import { ArrayVector, DataFrame, DataQuery, Field, FieldType, MutableDataFrame, TIME_SERIES_TIME_FIELD_NAME, TIME_SERIES_VALUE_FIELD_NAME } from '@grafana/data';
|
||||
|
||||
/**
|
||||
* Convert Zabbix API history.get response to Grafana format
|
||||
@@ -35,6 +37,7 @@ function convertHistory(history, items, addHostName, convertPointCallback) {
|
||||
'__zbx_item': { value: item.name },
|
||||
'__zbx_item_name': { value: item.name },
|
||||
'__zbx_item_key': { value: item.key_ },
|
||||
'__zbx_item_interval': { value: item.delay },
|
||||
};
|
||||
|
||||
if (_.keys(hosts).length > 0) {
|
||||
@@ -52,10 +55,184 @@ function convertHistory(history, items, addHostName, convertPointCallback) {
|
||||
target: alias,
|
||||
datapoints: _.map(hist, convertPointCallback),
|
||||
scopedVars,
|
||||
item
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function seriesToDataFrame(timeseries, target: DataQuery, valueMappings?: any[], fieldType?: FieldType): DataFrame {
|
||||
const { datapoints, scopedVars, target: seriesName, item } = timeseries;
|
||||
|
||||
const timeFiled: Field = {
|
||||
name: TIME_SERIES_TIME_FIELD_NAME,
|
||||
type: FieldType.time,
|
||||
config: {
|
||||
custom: {}
|
||||
},
|
||||
values: new ArrayVector<number>(datapoints.map(p => p[c.DATAPOINT_TS])),
|
||||
};
|
||||
|
||||
let values: ArrayVector<number> | ArrayVector<string>;
|
||||
if (fieldType === FieldType.string) {
|
||||
values = new ArrayVector<string>(datapoints.map(p => p[c.DATAPOINT_VALUE]));
|
||||
} else {
|
||||
values = new ArrayVector<number>(datapoints.map(p => p[c.DATAPOINT_VALUE]));
|
||||
}
|
||||
|
||||
const valueFiled: Field = {
|
||||
name: TIME_SERIES_VALUE_FIELD_NAME,
|
||||
type: fieldType ?? FieldType.number,
|
||||
labels: {},
|
||||
config: {
|
||||
displayName: seriesName,
|
||||
displayNameFromDS: seriesName,
|
||||
custom: {}
|
||||
},
|
||||
values,
|
||||
};
|
||||
|
||||
if (scopedVars) {
|
||||
timeFiled.config.custom = {
|
||||
itemInterval: scopedVars['__zbx_item_interval']?.value,
|
||||
};
|
||||
|
||||
valueFiled.labels = {
|
||||
host: scopedVars['__zbx_host_name']?.value,
|
||||
item: scopedVars['__zbx_item']?.value,
|
||||
item_key: scopedVars['__zbx_item_key']?.value,
|
||||
};
|
||||
|
||||
valueFiled.config.custom = {
|
||||
itemInterval: scopedVars['__zbx_item_interval']?.value,
|
||||
};
|
||||
}
|
||||
|
||||
if (item) {
|
||||
// Try to use unit configured in Zabbix
|
||||
const unit = utils.convertZabbixUnit(item.units);
|
||||
if (unit) {
|
||||
console.log(`Datasource: unit detected: ${unit} (${item.units})`);
|
||||
valueFiled.config.unit = unit;
|
||||
|
||||
if (unit === 'percent') {
|
||||
valueFiled.config.min = 0;
|
||||
valueFiled.config.max = 100;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to use value mapping from Zabbix
|
||||
const mappings = utils.getValueMapping(item, valueMappings);
|
||||
if (mappings) {
|
||||
console.log(`Datasource: value mapping detected`);
|
||||
valueFiled.config.mappings = mappings;
|
||||
}
|
||||
}
|
||||
|
||||
const fields: Field[] = [ timeFiled, valueFiled ];
|
||||
|
||||
const frame: DataFrame = {
|
||||
name: seriesName,
|
||||
refId: target.refId,
|
||||
fields,
|
||||
length: datapoints.length,
|
||||
};
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
export function isConvertibleToWide(data: DataFrame[]): boolean {
|
||||
if (!data || data.length < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const first = data[0].fields.find(f => f.type === FieldType.time);
|
||||
if (!first) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
const timeField = data[i].fields.find(f => f.type === FieldType.time);
|
||||
|
||||
for (let j = 0; j < Math.min(data.length, 2); j++) {
|
||||
if (timeField.values.get(j) !== first.values.get(j)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function alignFrames(data: DataFrame[]): DataFrame[] {
|
||||
if (!data || data.length === 0) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// Get oldest time stamp for all frames
|
||||
let minTimestamp = data[0].fields.find(f => f.name === TIME_SERIES_TIME_FIELD_NAME).values.get(0);
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const timeField = data[i].fields.find(f => f.name === TIME_SERIES_TIME_FIELD_NAME);
|
||||
const firstTs = timeField.values.get(0);
|
||||
if (firstTs < minTimestamp) {
|
||||
minTimestamp = firstTs;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const frame = data[i];
|
||||
const timeField = frame.fields.find(f => f.name === TIME_SERIES_TIME_FIELD_NAME);
|
||||
const valueField = frame.fields.find(f => f.name === TIME_SERIES_VALUE_FIELD_NAME);
|
||||
const firstTs = timeField.values.get(0);
|
||||
|
||||
if (firstTs > minTimestamp) {
|
||||
console.log('Data frames: adding missing points');
|
||||
let timestamps = timeField.values.toArray();
|
||||
let values = valueField.values.toArray();
|
||||
const missingTimestamps = [];
|
||||
const missingValues = [];
|
||||
const frameInterval: number = timeField.config.custom?.itemInterval;
|
||||
for (let j = minTimestamp; j < firstTs; j+=frameInterval) {
|
||||
missingTimestamps.push(j);
|
||||
missingValues.push(null);
|
||||
}
|
||||
|
||||
timestamps = missingTimestamps.concat(timestamps);
|
||||
values = missingValues.concat(values);
|
||||
timeField.values = new ArrayVector(timestamps);
|
||||
valueField.values = new ArrayVector(values);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export function convertToWide(data: DataFrame[]): DataFrame[] {
|
||||
const timeField = data[0].fields.find(f => f.type === FieldType.time);
|
||||
if (!timeField) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const fields: Field[] = [ timeField ];
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const valueField = data[i].fields.find(f => f.name === TIME_SERIES_VALUE_FIELD_NAME);
|
||||
if (!valueField) {
|
||||
continue;
|
||||
}
|
||||
|
||||
valueField.name = data[i].name;
|
||||
fields.push(valueField);
|
||||
}
|
||||
|
||||
const frame: DataFrame = {
|
||||
name: "wide",
|
||||
fields,
|
||||
length: timeField.values.length,
|
||||
};
|
||||
|
||||
return [frame];
|
||||
}
|
||||
|
||||
function sortTimeseries(timeseries) {
|
||||
// Sort trend data, issue #202
|
||||
_.forEach(timeseries, series => {
|
||||
@@ -256,5 +433,9 @@ export default {
|
||||
handleHistoryAsTable,
|
||||
handleSLAResponse,
|
||||
handleTriggersResponse,
|
||||
sortTimeseries
|
||||
sortTimeseries,
|
||||
seriesToDataFrame,
|
||||
isConvertibleToWide,
|
||||
convertToWide,
|
||||
alignFrames,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user