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:
@@ -35,6 +35,7 @@ export const ConfigEditor = (props: Props) => {
|
||||
trendsRange: '',
|
||||
cacheTTL: '',
|
||||
timeout: '',
|
||||
disableDataAlignment: false,
|
||||
...restJsonData,
|
||||
},
|
||||
});
|
||||
@@ -209,10 +210,20 @@ export const ConfigEditor = (props: Props) => {
|
||||
<h3 className="page-heading">Other</h3>
|
||||
<Switch
|
||||
label="Disable acknowledges for read-only users"
|
||||
labelClass="width-20"
|
||||
labelClass="width-16"
|
||||
checked={options.jsonData.disableReadOnlyUsersAck}
|
||||
onChange={jsonDataSwitchHandler('disableReadOnlyUsersAck', options, onOptionsChange)}
|
||||
/>
|
||||
<Switch
|
||||
label="Disable data alignment"
|
||||
labelClass="width-16"
|
||||
checked={!!options.jsonData.disableDataAlignment}
|
||||
onChange={jsonDataSwitchHandler('disableDataAlignment', options, onOptionsChange)}
|
||||
tooltip="Data alignment feature aligns points based on item update interval.
|
||||
For instance, if value collected once per minute, then timestamp of the each point will be set to the start of corresponding minute.
|
||||
This alignment required for proper work of the stacked graphs.
|
||||
If you don't need stacked graphs and want to get exactly the same timestamps as in Zabbix, then you can disable this feature."
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as utils from './utils';
|
||||
import * as migrations from './migrations';
|
||||
import * as metricFunctions from './metricFunctions';
|
||||
import * as c from './constants';
|
||||
import { align } from './timeseries';
|
||||
import dataProcessor from './dataProcessor';
|
||||
import responseHandler from './responseHandler';
|
||||
import problemsHandler from './problemsHandler';
|
||||
@@ -13,7 +14,7 @@ import { Zabbix } from './zabbix/zabbix';
|
||||
import { ZabbixAPIError } from './zabbix/connectors/zabbix_api/zabbixAPIConnector';
|
||||
import { ZabbixMetricsQuery, ZabbixDSOptions, VariableQueryTypes, ShowProblemTypes, ProblemDTO } from './types';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { DataSourceApi, DataSourceInstanceSettings } from '@grafana/data';
|
||||
import { DataFrame, DataQueryRequest, DataQueryResponse, DataSourceApi, DataSourceInstanceSettings, FieldType, isDataFrame, LoadingState } from '@grafana/data';
|
||||
|
||||
export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDSOptions> {
|
||||
name: string;
|
||||
@@ -25,6 +26,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
trendsRange: string;
|
||||
cacheTTL: any;
|
||||
disableReadOnlyUsersAck: boolean;
|
||||
disableDataAlignment: boolean;
|
||||
enableDirectDBConnection: boolean;
|
||||
dbConnectionDatasourceId: number;
|
||||
dbConnectionDatasourceName: string;
|
||||
@@ -64,6 +66,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
|
||||
// Other options
|
||||
this.disableReadOnlyUsersAck = jsonData.disableReadOnlyUsersAck;
|
||||
this.disableDataAlignment = jsonData.disableDataAlignment;
|
||||
|
||||
// Direct DB Connection options
|
||||
this.enableDirectDBConnection = jsonData.dbConnectionEnable || false;
|
||||
@@ -94,7 +97,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
* @param {Object} options Contains time range, targets and other info.
|
||||
* @return {Object} Grafana metrics object with timeseries data for each target.
|
||||
*/
|
||||
query(options) {
|
||||
query(options: DataQueryRequest<any>): Promise<DataQueryResponse> {
|
||||
// Create request for each target
|
||||
const promises = _.map(options.targets, t => {
|
||||
// Don't request for hidden targets
|
||||
@@ -164,7 +167,20 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
return Promise.all(_.flatten(promises))
|
||||
.then(_.flatten)
|
||||
.then(data => {
|
||||
return { data: data };
|
||||
if (data && data.length > 0 && isDataFrame(data[0]) && !utils.isProblemsDataFrame(data[0])) {
|
||||
data = responseHandler.alignFrames(data);
|
||||
if (responseHandler.isConvertibleToWide(data)) {
|
||||
console.log('Converting response to the wide format');
|
||||
data = responseHandler.convertToWide(data);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}).then(data => {
|
||||
return {
|
||||
data,
|
||||
state: LoadingState.Done,
|
||||
key: options.requestId,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -207,28 +223,31 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
/**
|
||||
* Query target data for Metrics
|
||||
*/
|
||||
queryNumericData(target, timeRange, useTrends, options) {
|
||||
let queryStart, queryEnd;
|
||||
async queryNumericData(target, timeRange, useTrends, options): Promise<DataFrame[]> {
|
||||
const getItemOptions = {
|
||||
itemtype: 'num'
|
||||
};
|
||||
return this.zabbix.getItemsFromTarget(target, getItemOptions)
|
||||
.then(items => {
|
||||
queryStart = new Date().getTime();
|
||||
return this.queryNumericDataForItems(items, target, timeRange, useTrends, options);
|
||||
}).then(result => {
|
||||
queryEnd = new Date().getTime();
|
||||
if (this.enableDebugLog) {
|
||||
console.log(`Datasource::Performance Query Time (${this.name}): ${queryEnd - queryStart}`);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
const items = await this.zabbix.getItemsFromTarget(target, getItemOptions);
|
||||
|
||||
const queryStart = new Date().getTime();
|
||||
const result = await this.queryNumericDataForItems(items, target, timeRange, useTrends, options);
|
||||
const queryEnd = new Date().getTime();
|
||||
|
||||
if (this.enableDebugLog) {
|
||||
console.log(`Datasource::Performance Query Time (${this.name}): ${queryEnd - queryStart}`);
|
||||
}
|
||||
|
||||
const valueMappings = await this.zabbix.getValueMappings();
|
||||
|
||||
const dataFrames = result.map(s => responseHandler.seriesToDataFrame(s, target, valueMappings));
|
||||
return dataFrames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query history for numeric items
|
||||
*/
|
||||
queryNumericDataForItems(items, target, timeRange, useTrends, options) {
|
||||
queryNumericDataForItems(items, target: ZabbixMetricsQuery, timeRange, useTrends, options) {
|
||||
let getHistoryPromise;
|
||||
options.valueType = this.getTrendValueType(target);
|
||||
options.consolidateBy = getConsolidateBy(target) || options.valueType;
|
||||
@@ -236,7 +255,11 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
if (useTrends) {
|
||||
getHistoryPromise = this.zabbix.getTrends(items, timeRange, options);
|
||||
} else {
|
||||
getHistoryPromise = this.zabbix.getHistoryTS(items, timeRange, options);
|
||||
getHistoryPromise = this.zabbix.getHistoryTS(items, timeRange, options)
|
||||
.then(timeseries => {
|
||||
const disableDataAlignment = this.disableDataAlignment || target.options?.disableDataAlignment;
|
||||
return !disableDataAlignment ? this.alignTimeSeriesData(timeseries) : timeseries;
|
||||
});
|
||||
}
|
||||
|
||||
return getHistoryPromise
|
||||
@@ -253,6 +276,14 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
return trendValueFunc ? trendValueFunc.params[0] : "avg";
|
||||
}
|
||||
|
||||
alignTimeSeriesData(timeseries: any[]) {
|
||||
for (const ts of timeseries) {
|
||||
const interval = utils.parseItemInterval(ts.scopedVars['__zbx_item_interval']?.value);
|
||||
ts.datapoints = align(ts.datapoints, interval);
|
||||
}
|
||||
return timeseries;
|
||||
}
|
||||
|
||||
applyDataProcessingFunctions(timeseries_data, target) {
|
||||
const transformFunctions = bindFunctionDefs(target.functions, 'Transform');
|
||||
const aggregationFunctions = bindFunctionDefs(target.functions, 'Aggregate');
|
||||
@@ -319,6 +350,12 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
return this.zabbix.getItemsFromTarget(target, options)
|
||||
.then(items => {
|
||||
return this.zabbix.getHistoryText(items, timeRange, target);
|
||||
})
|
||||
.then(result => {
|
||||
if (target.resultFormat !== 'table') {
|
||||
return result.map(s => responseHandler.seriesToDataFrame(s, target, [], FieldType.string));
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -337,6 +374,9 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
return this.zabbix.getItemsByIDs(itemids)
|
||||
.then(items => {
|
||||
return this.queryNumericDataForItems(items, target, timeRange, useTrends, options);
|
||||
})
|
||||
.then(result => {
|
||||
return result.map(s => responseHandler.seriesToDataFrame(s, target));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -367,7 +407,11 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
itservices = _.filter(itservices, {'serviceid': target.itservice?.serviceid});
|
||||
}
|
||||
return this.zabbix.getSLA(itservices, timeRange, target, options);})
|
||||
.then(itservicesdp => this.applyDataProcessingFunctions(itservicesdp, target));
|
||||
.then(itservicesdp => this.applyDataProcessingFunctions(itservicesdp, target))
|
||||
.then(result => {
|
||||
const dataFrames = result.map(s => responseHandler.seriesToDataFrame(s, target));
|
||||
return dataFrames;
|
||||
});
|
||||
}
|
||||
|
||||
queryTriggersData(target, timeRange) {
|
||||
@@ -665,7 +709,10 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
target[p].filter = this.replaceTemplateVars(target[p].filter, options.scopedVars);
|
||||
}
|
||||
});
|
||||
target.textFilter = this.replaceTemplateVars(target.textFilter, options.scopedVars);
|
||||
|
||||
if (target.textFilter) {
|
||||
target.textFilter = this.replaceTemplateVars(target.textFilter, options.scopedVars);
|
||||
}
|
||||
|
||||
_.forEach(target.functions, func => {
|
||||
func.params = _.map(func.params, param => {
|
||||
|
||||
@@ -282,19 +282,27 @@
|
||||
|
||||
<!-- Query options -->
|
||||
<div class="gf-form-group offset-width-7" ng-if="ctrl.showQueryOptions">
|
||||
<div class="gf-form" ng-hide="ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
|
||||
<gf-form-switch class="gf-form" label-class="width-10"
|
||||
label="Show disabled items"
|
||||
checked="ctrl.target.options.showDisabledItems"
|
||||
on-change="ctrl.onQueryOptionChange()">
|
||||
</gf-form-switch>
|
||||
<div ng-hide="ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
|
||||
<gf-form-switch class="gf-form" label-class="width-10"
|
||||
label="Show disabled items"
|
||||
checked="ctrl.target.options.showDisabledItems"
|
||||
on-change="ctrl.onQueryOptionChange()">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label-class="width-10"
|
||||
label="Disable data alignment"
|
||||
checked="ctrl.target.options.disableDataAlignment"
|
||||
on-change="ctrl.onQueryOptionChange()">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.target.queryType === editorMode.TEXT && ctrl.target.resultFormat === 'table'">
|
||||
<gf-form-switch class="gf-form" label-class="width-10"
|
||||
label="Skip empty values"
|
||||
checked="ctrl.target.options.skipEmptyValues"
|
||||
on-change="ctrl.onQueryOptionChange()">
|
||||
</gf-form-switch>
|
||||
|
||||
<div class="gf-form-group offset-width-7" ng-show="ctrl.target.queryType === editorMode.TEXT && ctrl.target.resultFormat === 'table'">
|
||||
<div class="gf-form">
|
||||
<gf-form-switch class="gf-form" label-class="width-10"
|
||||
label="Skip empty values"
|
||||
checked="ctrl.target.options.skipEmptyValues"
|
||||
on-change="ctrl.onQueryOptionChange()">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-show="ctrl.target.queryType == editorMode.PROBLEMS || ctrl.target.queryType == editorMode.TRIGGERS">
|
||||
|
||||
@@ -171,7 +171,11 @@ export function toDataFrame(problems: any[]): DataFrame {
|
||||
name: 'Problems',
|
||||
type: FieldType.other,
|
||||
values: new ArrayVector(problems),
|
||||
config: {},
|
||||
config: {
|
||||
custom: {
|
||||
type: 'problems',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response: DataFrame = {
|
||||
|
||||
@@ -27,6 +27,7 @@ function getTargetDefaults() {
|
||||
options: {
|
||||
showDisabledItems: false,
|
||||
skipEmptyValues: false,
|
||||
disableDataAlignment: false,
|
||||
},
|
||||
table: {
|
||||
'skipEmptyValues': false
|
||||
@@ -455,6 +456,7 @@ export class ZabbixQueryController extends QueryCtrl {
|
||||
renderQueryOptionsText() {
|
||||
const metricOptionsMap = {
|
||||
showDisabledItems: "Show disabled items",
|
||||
disableDataAlignment: "Disable data alignment",
|
||||
};
|
||||
|
||||
const problemsOptionsMap = {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import _ from 'lodash';
|
||||
import * as utils from './utils';
|
||||
import * as c from './constants';
|
||||
import { TimeSeriesPoints, TimeSeriesValue } from '@grafana/data';
|
||||
|
||||
const POINT_VALUE = 0;
|
||||
const POINT_TIMESTAMP = 1;
|
||||
@@ -62,6 +63,61 @@ function downsample(datapoints, time_to, ms_interval, func) {
|
||||
return downsampledSeries.reverse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects interval between data points and aligns time series. If there's no value in the interval, puts null as a value.
|
||||
*/
|
||||
export function align(datapoints: TimeSeriesPoints, interval?: number): TimeSeriesPoints {
|
||||
if (interval) {
|
||||
interval = detectSeriesInterval(datapoints);
|
||||
}
|
||||
|
||||
if (interval <= 0 || datapoints.length <= 1) {
|
||||
return datapoints;
|
||||
}
|
||||
|
||||
const aligned_ts: TimeSeriesPoints = [];
|
||||
let frame_ts = getPointTimeFrame(datapoints[0][POINT_TIMESTAMP], interval);
|
||||
let point_frame_ts = frame_ts;
|
||||
let point: TimeSeriesValue[];
|
||||
for (let i = 0; i < datapoints.length; i++) {
|
||||
point = datapoints[i];
|
||||
point_frame_ts = getPointTimeFrame(point[POINT_TIMESTAMP], interval);
|
||||
|
||||
if (point_frame_ts > frame_ts) {
|
||||
// Move frame window to next non-empty interval and fill empty by null
|
||||
while (frame_ts < point_frame_ts) {
|
||||
aligned_ts.push([null, frame_ts]);
|
||||
frame_ts += interval;
|
||||
}
|
||||
}
|
||||
|
||||
aligned_ts.push([point[POINT_VALUE], point_frame_ts]);
|
||||
frame_ts += interval;
|
||||
}
|
||||
return aligned_ts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects interval between data points in milliseconds.
|
||||
*/
|
||||
function detectSeriesInterval(datapoints: TimeSeriesPoints): number {
|
||||
if (datapoints.length < 2) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
let deltas = [];
|
||||
for (let i = 1; i < datapoints.length; i++) {
|
||||
// Get deltas (in seconds)
|
||||
const d = (datapoints[i][POINT_TIMESTAMP] - datapoints[i - 1][POINT_TIMESTAMP]) / 1000;
|
||||
deltas.push(Math.round(d));
|
||||
}
|
||||
|
||||
// Use 50th percentile (median) as an interval
|
||||
deltas = _.sortBy(deltas);
|
||||
const intervalSec = deltas[Math.floor(deltas.length * 0.5)];
|
||||
return intervalSec * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group points by given time interval
|
||||
* datapoints: [[<value>, <unixtime>], ...]
|
||||
@@ -255,7 +311,10 @@ function rate(datapoints) {
|
||||
return newSeries;
|
||||
}
|
||||
|
||||
function simpleMovingAverage(datapoints, n) {
|
||||
function simpleMovingAverage(datapoints: TimeSeriesPoints, n: number): TimeSeriesPoints {
|
||||
// It's not possible to calculate MA if n greater than number of points
|
||||
n = Math.min(n, datapoints.length);
|
||||
|
||||
const sma = [];
|
||||
let w_sum;
|
||||
let w_avg = null;
|
||||
@@ -298,7 +357,10 @@ function simpleMovingAverage(datapoints, n) {
|
||||
return sma;
|
||||
}
|
||||
|
||||
function expMovingAverage(datapoints, n) {
|
||||
function expMovingAverage(datapoints: TimeSeriesPoints, n: number): TimeSeriesPoints {
|
||||
// It's not possible to calculate MA if n greater than number of points
|
||||
n = Math.min(n, datapoints.length);
|
||||
|
||||
let ema = [datapoints[0]];
|
||||
let ema_prev = datapoints[0][POINT_VALUE];
|
||||
let ema_cur;
|
||||
@@ -526,6 +588,7 @@ const exportedFunctions = {
|
||||
PERCENTILE,
|
||||
sortByTime,
|
||||
flattenDatapoints,
|
||||
align,
|
||||
};
|
||||
|
||||
export default exportedFunctions;
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface ZabbixDSOptions extends DataSourceJsonData {
|
||||
dbConnectionDatasourceName?: string;
|
||||
dbConnectionRetentionPolicy?: string;
|
||||
disableReadOnlyUsersAck: boolean;
|
||||
disableDataAlignment: boolean;
|
||||
}
|
||||
|
||||
export interface ZabbixSecureJSONData {
|
||||
@@ -37,7 +38,7 @@ export interface ZabbixMetricsQuery extends DataQuery {
|
||||
queryType: string;
|
||||
datasourceId: number;
|
||||
functions: ZabbixMetricFunction[];
|
||||
options: any;
|
||||
options: ZabbixQueryOptions;
|
||||
textFilter: string;
|
||||
mode: number;
|
||||
itemids: number[];
|
||||
@@ -50,6 +51,19 @@ export interface ZabbixMetricsQuery extends DataQuery {
|
||||
itemFilter: string;
|
||||
}
|
||||
|
||||
export interface ZabbixQueryOptions {
|
||||
showDisabledItems?: boolean;
|
||||
skipEmptyValues?: boolean;
|
||||
disableDataAlignment?: boolean;
|
||||
// Problems options
|
||||
minSeverity?: number;
|
||||
sortProblems?: string;
|
||||
acknowledged?: number;
|
||||
hostsInMaintenance?: boolean;
|
||||
hostProxy?: boolean;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface ZabbixMetricFunction {
|
||||
name: string;
|
||||
params: any;
|
||||
|
||||
@@ -3,6 +3,7 @@ import moment from 'moment';
|
||||
import kbn from 'grafana/app/core/utils/kbn';
|
||||
import * as c from './constants';
|
||||
import { VariableQuery, VariableQueryTypes } from './types';
|
||||
import { arrowTableToDataFrame, isTableData, MappingType, ValueMap, ValueMapping, getValueFormats, DataFrame, FieldType } from '@grafana/data';
|
||||
|
||||
/*
|
||||
* This regex matches 3 types of variable reference with an optional format specifier
|
||||
@@ -235,6 +236,26 @@ export function escapeRegex(value) {
|
||||
return value.replace(/[\\^$*+?.()|[\]{}\/]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses Zabbix item update interval. Returns 0 in case of custom intervals.
|
||||
*/
|
||||
export function parseItemInterval(interval: string): number {
|
||||
const normalizedInterval = normalizeZabbixInterval(interval);
|
||||
if (normalizedInterval) {
|
||||
return parseInterval(normalizedInterval);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function normalizeZabbixInterval(interval: string): string {
|
||||
const intervalPattern = /(^[\d]+)(y|M|w|d|h|m|s)?/g;
|
||||
const parsedInterval = intervalPattern.exec(interval);
|
||||
if (!parsedInterval) {
|
||||
return '';
|
||||
}
|
||||
return parsedInterval[1] + (parsedInterval.length > 2 ? parsedInterval[2] : 's');
|
||||
}
|
||||
|
||||
export function parseInterval(interval: string): number {
|
||||
const intervalPattern = /(^[\d]+)(y|M|w|d|h|m|s)/g;
|
||||
const momentInterval: any[] = intervalPattern.exec(interval);
|
||||
@@ -387,3 +408,65 @@ export function parseTags(tagStr: string): any[] {
|
||||
export function mustArray(result: any): any[] {
|
||||
return result || [];
|
||||
}
|
||||
|
||||
const getUnitsMap = () => ({
|
||||
'%': 'percent',
|
||||
'b': 'decbits', // bits(SI)
|
||||
'bps': 'bps', // bits/sec(SI)
|
||||
'B': 'bytes', // bytes(IEC)
|
||||
'Bps': 'binBps', // bytes/sec(IEC)
|
||||
// 'unixtime': 'dateTimeAsSystem',
|
||||
'uptime': 'dtdhms',
|
||||
'qps': 'qps', // requests/sec (rps)
|
||||
'iops': 'iops', // I/O ops/sec (iops)
|
||||
'Hz': 'hertz', // Hertz (1/s)
|
||||
'V': 'volt', // Volt (V)
|
||||
'C': 'celsius', // Celsius (°C)
|
||||
'RPM': 'rotrpm', // Revolutions per minute (rpm)
|
||||
'dBm': 'dBm', // Decibel-milliwatt (dBm)
|
||||
});
|
||||
|
||||
const getKnownGrafanaUnits = () => {
|
||||
const units = {};
|
||||
const categories = getValueFormats();
|
||||
for (const category of categories) {
|
||||
for (const unitDesc of category.submenu) {
|
||||
const unit = unitDesc.value;
|
||||
units[unit] = unit;
|
||||
}
|
||||
}
|
||||
return units;
|
||||
};
|
||||
|
||||
const unitsMap = getUnitsMap();
|
||||
const knownGrafanaUnits = getKnownGrafanaUnits();
|
||||
|
||||
export function convertZabbixUnit(zabbixUnit: string): string {
|
||||
let unit = unitsMap[zabbixUnit];
|
||||
if (!unit) {
|
||||
unit = knownGrafanaUnits[zabbixUnit];
|
||||
}
|
||||
return unit;
|
||||
}
|
||||
|
||||
export function getValueMapping(item, valueMappings: any[]): ValueMapping[] | null {
|
||||
const { valuemapid } = item;
|
||||
const mapping = valueMappings.find(m => m.valuemapid === valuemapid);
|
||||
if (!mapping) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (mapping.mappings as any[]).map((m, i) => {
|
||||
const valueMapping: ValueMapping = {
|
||||
id: i,
|
||||
type: MappingType.ValueToText,
|
||||
value: m.value,
|
||||
text: m.newvalue,
|
||||
};
|
||||
return valueMapping;
|
||||
});
|
||||
}
|
||||
|
||||
export function isProblemsDataFrame(data: DataFrame): boolean {
|
||||
return data.fields.length && data.fields[0].type === FieldType.other && data.fields[0].config.custom['type'] === 'problems';
|
||||
}
|
||||
|
||||
@@ -134,6 +134,7 @@ function convertGrafanaTSResponse(time_series, items, addHostName) {
|
||||
'__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) {
|
||||
@@ -153,6 +154,7 @@ function convertGrafanaTSResponse(time_series, items, addHostName) {
|
||||
target: alias,
|
||||
datapoints,
|
||||
scopedVars,
|
||||
item
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -161,11 +161,15 @@ export class ZabbixAPIConnector {
|
||||
getItems(hostids, appids, itemtype) {
|
||||
const params: any = {
|
||||
output: [
|
||||
'name', 'key_',
|
||||
'name',
|
||||
'key_',
|
||||
'value_type',
|
||||
'hostid',
|
||||
'status',
|
||||
'state'
|
||||
'state',
|
||||
'units',
|
||||
'valuemapid',
|
||||
'delay'
|
||||
],
|
||||
sortfield: 'name',
|
||||
webitems: true,
|
||||
@@ -651,6 +655,15 @@ export class ZabbixAPIConnector {
|
||||
|
||||
return this.request('script.execute', params);
|
||||
}
|
||||
|
||||
getValueMappings() {
|
||||
const params = {
|
||||
output: 'extend',
|
||||
selectMappings: "extend",
|
||||
};
|
||||
|
||||
return this.request('valuemap.get', params);
|
||||
}
|
||||
}
|
||||
|
||||
function filterTriggersByAcknowledge(triggers, acknowledged) {
|
||||
|
||||
@@ -20,17 +20,17 @@ interface AppsResponse extends Array<any> {
|
||||
const REQUESTS_TO_PROXYFY = [
|
||||
'getHistory', 'getTrend', 'getGroups', 'getHosts', 'getApps', 'getItems', 'getMacros', 'getItemsByIDs',
|
||||
'getEvents', 'getAlerts', 'getHostAlerts', 'getAcknowledges', 'getITService', 'getSLA', 'getVersion', 'getProxies',
|
||||
'getEventAlerts', 'getExtendedEventData', 'getProblems', 'getEventsHistory', 'getTriggersByIds', 'getScripts'
|
||||
'getEventAlerts', 'getExtendedEventData', 'getProblems', 'getEventsHistory', 'getTriggersByIds', 'getScripts', 'getValueMappings'
|
||||
];
|
||||
|
||||
const REQUESTS_TO_CACHE = [
|
||||
'getGroups', 'getHosts', 'getApps', 'getItems', 'getMacros', 'getItemsByIDs', 'getITService', 'getProxies'
|
||||
'getGroups', 'getHosts', 'getApps', 'getItems', 'getMacros', 'getItemsByIDs', 'getITService', 'getProxies', 'getValueMappings'
|
||||
];
|
||||
|
||||
const REQUESTS_TO_BIND = [
|
||||
'getHistory', 'getTrend', 'getMacros', 'getItemsByIDs', 'getEvents', 'getAlerts', 'getHostAlerts',
|
||||
'getAcknowledges', 'getITService', 'getVersion', 'acknowledgeEvent', 'getProxies', 'getEventAlerts',
|
||||
'getExtendedEventData', 'getScripts', 'executeScript',
|
||||
'getExtendedEventData', 'getScripts', 'executeScript', 'getValueMappings'
|
||||
];
|
||||
|
||||
export class Zabbix implements ZabbixConnector {
|
||||
@@ -55,6 +55,7 @@ export class Zabbix implements ZabbixConnector {
|
||||
getExtendedEventData: (eventids) => Promise<any>;
|
||||
getMacros: (hostids: any[]) => Promise<any>;
|
||||
getVersion: () => Promise<string>;
|
||||
getValueMappings: () => Promise<any>;
|
||||
|
||||
constructor(options) {
|
||||
const {
|
||||
|
||||
Reference in New Issue
Block a user