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:
Alexander Zobnin
2020-12-22 15:33:14 +03:00
committed by GitHub
parent ad378a81e1
commit 83618178f0
18 changed files with 700 additions and 91 deletions

View File

@@ -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>
</>
);

View File

@@ -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 => {

View File

@@ -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">

View File

@@ -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 = {

View File

@@ -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 = {

View File

@@ -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,
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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';
}

View File

@@ -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
};
});

View File

@@ -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) {

View File

@@ -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 {