Merge branch 'master' into backend

This commit is contained in:
Alexander Zobnin
2020-05-28 12:02:36 +03:00
100 changed files with 4537 additions and 3689 deletions

View File

@@ -39,7 +39,6 @@ class FunctionEditor extends React.PureComponent<FunctionEditorProps, FunctionEd
<div style={{ overflow: 'auto', maxHeight: '30rem', textAlign: 'left', fontWeight: 'normal' }}>
<h4 style={{ color: 'white' }}> {name} </h4>
<div>{description}</div>
/>
</div>
);
}

View File

@@ -1,16 +1,22 @@
import React, { PureComponent } from 'react';
import { parseLegacyVariableQuery } from '../utils';
import { Select, Input, AsyncSelect, FormLabel } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { VariableQuery, VariableQueryTypes, VariableQueryProps, VariableQueryData } from '../types';
import { ZabbixInput } from './ZabbixInput';
// FormLabel was renamed to InlineFormLabel in Grafana 7.0
import * as grafanaUi from '@grafana/ui';
const FormLabel = grafanaUi.FormLabel || (grafanaUi as any).InlineFormLabel;
const Select = (grafanaUi as any).LegacyForms?.Select || (grafanaUi as any).Select;
const Input = (grafanaUi as any).LegacyForms?.Input || (grafanaUi as any).Input;
export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps, VariableQueryData> {
queryTypes: Array<SelectableValue<VariableQueryTypes>> = [
{ value: VariableQueryTypes.Group, label: 'Group'},
{ value: VariableQueryTypes.Host, label: 'Host' },
{ value: VariableQueryTypes.Application, label: 'Application' },
{ value: VariableQueryTypes.Item, label: 'Item' },
{ value: VariableQueryTypes.ItemValues, label: 'Item values' },
];
defaults: VariableQueryData = {
@@ -119,7 +125,8 @@ export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps,
}
</div>
{(selectedQueryType.value === VariableQueryTypes.Application ||
selectedQueryType.value === VariableQueryTypes.Item) &&
selectedQueryType.value === VariableQueryTypes.Item ||
selectedQueryType.value === VariableQueryTypes.ItemValues) &&
<div className="gf-form-inline">
<div className="gf-form max-width-30">
<FormLabel width={10}>Application</FormLabel>
@@ -129,7 +136,8 @@ export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps,
onBlur={this.handleQueryChange}
/>
</div>
{selectedQueryType.value === VariableQueryTypes.Item &&
{(selectedQueryType.value === VariableQueryTypes.Item ||
selectedQueryType.value === VariableQueryTypes.ItemValues) &&
<div className="gf-form max-width-30">
<FormLabel width={10}>Item</FormLabel>
<ZabbixInput

View File

@@ -1,17 +1,20 @@
import React, { FC } from 'react';
import { css, cx } from 'emotion';
import { withTheme, Input, EventsWithValidation, ValidationEvents, Themeable } from '@grafana/ui';
import { EventsWithValidation, ValidationEvents, useTheme } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data';
import { isRegex, variableRegex } from '../utils';
import * as grafanaUi from '@grafana/ui';
const Input = (grafanaUi as any).LegacyForms?.Input || (grafanaUi as any).Input;
const variablePattern = RegExp(`^${variableRegex.source}`);
const getStyles = (theme: GrafanaTheme) => ({
inputRegex: css`
color: ${theme.colors.orange}
color: ${theme.colors.orange || (theme as any).palette.orange}
`,
inputVariable: css`
color: ${theme.colors.variable}
color: ${theme.colors.variable || (theme as any).palette.variable}
`,
});
@@ -43,17 +46,14 @@ const zabbixInputValidationEvents: ValidationEvents = {
],
};
interface Props extends React.ComponentProps<typeof Input>, Themeable {
}
const UnthemedZabbixInput: FC<Props> = ({ theme, value, ref, validationEvents, ...restProps }) => {
export const ZabbixInput: FC<any> = ({ value, ref, validationEvents, ...restProps }) => {
const theme = useTheme();
const styles = getStyles(theme);
let inputClass;
let inputClass = styles.inputRegex;
if (variablePattern.test(value as string)) {
inputClass = styles.inputVariable;
}
if (isRegex(value)) {
} else if (isRegex(value)) {
inputClass = styles.inputRegex;
}
@@ -66,5 +66,3 @@ const UnthemedZabbixInput: FC<Props> = ({ theme, value, ref, validationEvents, .
/>
);
};
export const ZabbixInput = withTheme(UnthemedZabbixInput);

View File

@@ -1,14 +1,9 @@
import _ from 'lodash';
import { getDataSourceSrv } from '@grafana/runtime';
import { migrateDSConfig } from './migrations';
const SUPPORTED_SQL_DS = ['mysql', 'postgres', 'influxdb'];
const zabbixVersions = [
{ name: '2.x', value: 2 },
{ name: '3.x', value: 3 },
{ name: '4.x', value: 4 },
];
const defaultConfig = {
trends: false,
dbConnectionEnable: false,
@@ -17,29 +12,24 @@ const defaultConfig = {
addThresholds: false,
alertingMinSeverity: 3,
disableReadOnlyUsersAck: false,
zabbixVersion: 3,
};
export class ZabbixDSConfigController {
/** @ngInject */
constructor($scope, $injector, datasourceSrv) {
this.datasourceSrv = datasourceSrv;
constructor() {
this.current.jsonData = migrateDSConfig(this.current.jsonData);
_.defaults(this.current.jsonData, defaultConfig);
this.dbConnectionDatasourceId = this.current.jsonData.dbConnectionDatasourceId;
this.dbDataSources = this.getSupportedDBDataSources();
this.zabbixVersions = _.cloneDeep(zabbixVersions);
this.autoDetectZabbixVersion();
if (!this.dbConnectionDatasourceId) {
this.loadCurrentDBDatasource();
}
}
getSupportedDBDataSources() {
let datasources = this.datasourceSrv.getAll();
let datasources = getDataSourceSrv().getAll();
return _.filter(datasources, ds => {
return _.includes(SUPPORTED_SQL_DS, ds.type);
});
@@ -53,7 +43,7 @@ export class ZabbixDSConfigController {
loadCurrentDBDatasource() {
const dsName= this.current.jsonData.dbConnectionDatasourceName;
this.datasourceSrv.loadDatasource(dsName)
getDataSourceSrv().loadDatasource(dsName)
.then(ds => {
if (ds) {
this.dbConnectionDatasourceId = ds.id;
@@ -61,25 +51,6 @@ export class ZabbixDSConfigController {
});
}
autoDetectZabbixVersion() {
if (!this.current.id) {
return;
}
this.datasourceSrv.loadDatasource(this.current.name)
.then(ds => {
return ds.getVersion();
})
.then(version => {
if (version) {
if (!_.find(zabbixVersions, ['value', version])) {
this.zabbixVersions.push({ name: version + '.x', value: version });
}
this.current.jsonData.zabbixVersion = version;
}
});
}
onDBConnectionDatasourceChange() {
this.current.jsonData.dbConnectionDatasourceId = this.dbConnectionDatasourceId;
}

View File

@@ -1,3 +1,7 @@
// Plugin IDs
export const ZABBIX_PROBLEMS_PANEL_ID = 'alexanderzobnin-zabbix-triggers-panel';
export const ZABBIX_DS_ID = 'alexanderzobnin-zabbix-datasource';
// Data point
export const DATAPOINT_VALUE = 0;
export const DATAPOINT_TS = 1;
@@ -8,6 +12,7 @@ export const MODE_ITSERVICE = 1;
export const MODE_TEXT = 2;
export const MODE_ITEMID = 3;
export const MODE_TRIGGERS = 4;
export const MODE_PROBLEMS = 5;
// Triggers severity
export const SEV_NOT_CLASSIFIED = 0;
@@ -23,8 +28,10 @@ export const SHOW_OK_EVENTS = 1;
// Acknowledge
export const ZBX_ACK_ACTION_NONE = 0;
export const ZBX_ACK_ACTION_CLOSE = 1;
export const ZBX_ACK_ACTION_ACK = 2;
export const ZBX_ACK_ACTION_ADD_MESSAGE = 4;
export const ZBX_ACK_ACTION_CHANGE_SEVERITY = 8;
export const TRIGGER_SEVERITY = [
{val: 0, text: 'Not classified'},
@@ -39,3 +46,5 @@ export const TRIGGER_SEVERITY = [
export const MIN_SLA_INTERVAL = 3600;
export const RANGE_VARIABLE_VALUE = 'range_series';
export const DEFAULT_ZABBIX_PROBLEMS_LIMIT = 1001;

View File

@@ -1,35 +1,37 @@
import _ from 'lodash';
// Available in 7.0
// import { getTemplateSrv } from '@grafana/runtime';
import * as utils from './utils';
import ts, { groupBy_perf as groupBy } from './timeseries';
let SUM = ts.SUM;
let COUNT = ts.COUNT;
let AVERAGE = ts.AVERAGE;
let MIN = ts.MIN;
let MAX = ts.MAX;
let MEDIAN = ts.MEDIAN;
let PERCENTILE = ts.PERCENTILE;
const SUM = ts.SUM;
const COUNT = ts.COUNT;
const AVERAGE = ts.AVERAGE;
const MIN = ts.MIN;
const MAX = ts.MAX;
const MEDIAN = ts.MEDIAN;
const PERCENTILE = ts.PERCENTILE;
let downsampleSeries = ts.downsample;
let groupBy_exported = (interval, groupFunc, datapoints) => groupBy(datapoints, interval, groupFunc);
let sumSeries = ts.sumSeries;
let delta = ts.delta;
let rate = ts.rate;
let scale = (factor, datapoints) => ts.scale_perf(datapoints, factor);
let offset = (delta, datapoints) => ts.offset(datapoints, delta);
let simpleMovingAverage = (n, datapoints) => ts.simpleMovingAverage(datapoints, n);
let expMovingAverage = (a, datapoints) => ts.expMovingAverage(datapoints, a);
let percentile = (interval, n, datapoints) => groupBy(datapoints, interval, _.partial(PERCENTILE, n));
const downsampleSeries = ts.downsample;
const groupBy_exported = (interval, groupFunc, datapoints) => groupBy(datapoints, interval, groupFunc);
const sumSeries = ts.sumSeries;
const delta = ts.delta;
const rate = ts.rate;
const scale = (factor, datapoints) => ts.scale_perf(datapoints, factor);
const offset = (delta, datapoints) => ts.offset(datapoints, delta);
const simpleMovingAverage = (n, datapoints) => ts.simpleMovingAverage(datapoints, n);
const expMovingAverage = (a, datapoints) => ts.expMovingAverage(datapoints, a);
const percentile = (interval, n, datapoints) => groupBy(datapoints, interval, _.partial(PERCENTILE, n));
function limit(order, n, orderByFunc, timeseries) {
let orderByCallback = aggregationFunctions[orderByFunc];
let sortByIteratee = (ts) => {
let values = _.map(ts.datapoints, (point) => {
const orderByCallback = aggregationFunctions[orderByFunc];
const sortByIteratee = (ts) => {
const values = _.map(ts.datapoints, (point) => {
return point[0];
});
return orderByCallback(values);
};
let sortedTimeseries = _.sortBy(timeseries, sortByIteratee);
const sortedTimeseries = _.sortBy(timeseries, sortByIteratee);
if (order === 'bottom') {
return sortedTimeseries.slice(0, n);
} else {
@@ -64,13 +66,17 @@ function transformNull(n, datapoints) {
});
}
function sortSeries(direction, timeseries) {
return _.orderBy(timeseries, [function (ts) {
function sortSeries(direction, timeseries: any[]) {
return _.orderBy(timeseries, [ts => {
return ts.target.toLowerCase();
}], direction);
}
function setAlias(alias, timeseries) {
// TODO: use getTemplateSrv() when available (since 7.0)
if (this.templateSrv && timeseries && timeseries.scopedVars) {
alias = this.templateSrv.replace(alias, timeseries.scopedVars);
}
timeseries.target = alias;
return timeseries;
}
@@ -84,6 +90,10 @@ function replaceAlias(regexp, newAlias, timeseries) {
}
let alias = timeseries.target.replace(pattern, newAlias);
// TODO: use getTemplateSrv() when available (since 7.0)
if (this.templateSrv && timeseries && timeseries.scopedVars) {
alias = this.templateSrv.replace(alias, timeseries.scopedVars);
}
timeseries.target = alias;
return timeseries;
}
@@ -94,14 +104,13 @@ function setAliasByRegex(alias, timeseries) {
}
function extractText(str, pattern) {
var extractPattern = new RegExp(pattern);
var extractedValue = extractPattern.exec(str);
extractedValue = extractedValue[0];
return extractedValue;
const extractPattern = new RegExp(pattern);
const extractedValue = extractPattern.exec(str);
return extractedValue[0];
}
function groupByWrapper(interval, groupFunc, datapoints) {
var groupByCallback = aggregationFunctions[groupFunc];
const groupByCallback = aggregationFunctions[groupFunc];
return groupBy(datapoints, interval, groupByCallback);
}
@@ -110,12 +119,12 @@ function aggregateByWrapper(interval, aggregateFunc, datapoints) {
const flattenedPoints = ts.flattenDatapoints(datapoints);
// groupBy_perf works with sorted series only
const sortedPoints = ts.sortByTime(flattenedPoints);
let groupByCallback = aggregationFunctions[aggregateFunc];
const groupByCallback = aggregationFunctions[aggregateFunc];
return groupBy(sortedPoints, interval, groupByCallback);
}
function aggregateWrapper(groupByCallback, interval, datapoints) {
var flattenedPoints = ts.flattenDatapoints(datapoints);
const flattenedPoints = ts.flattenDatapoints(datapoints);
// groupBy_perf works with sorted series only
const sortedPoints = ts.sortByTime(flattenedPoints);
return groupBy(sortedPoints, interval, groupByCallback);
@@ -125,19 +134,19 @@ function percentileAgg(interval, n, datapoints) {
const flattenedPoints = ts.flattenDatapoints(datapoints);
// groupBy_perf works with sorted series only
const sortedPoints = ts.sortByTime(flattenedPoints);
let groupByCallback = _.partial(PERCENTILE, n);
const groupByCallback = _.partial(PERCENTILE, n);
return groupBy(sortedPoints, interval, groupByCallback);
}
function timeShift(interval, range) {
let shift = utils.parseTimeShiftInterval(interval) / 1000;
const shift = utils.parseTimeShiftInterval(interval) / 1000;
return _.map(range, time => {
return time - shift;
});
}
function unShiftTimeSeries(interval, datapoints) {
let unshift = utils.parseTimeShiftInterval(interval);
const unshift = utils.parseTimeShiftInterval(interval);
return _.map(datapoints, dp => {
return [
dp[0],
@@ -146,7 +155,7 @@ function unShiftTimeSeries(interval, datapoints) {
});
}
let metricFunctions = {
const metricFunctions = {
groupBy: groupByWrapper,
scale: scale,
offset: offset,
@@ -177,7 +186,7 @@ let metricFunctions = {
replaceAlias: replaceAlias
};
let aggregationFunctions = {
const aggregationFunctions = {
avg: AVERAGE,
min: MIN,
max: MAX,

View File

@@ -1,5 +1,6 @@
import _ from 'lodash';
import config from 'grafana/app/core/config';
import { contextSrv } from 'grafana/app/core/core';
import * as dateMath from 'grafana/app/core/utils/datemath';
import * as utils from './utils';
import * as migrations from './migrations';
@@ -7,34 +8,44 @@ import * as metricFunctions from './metricFunctions';
import * as c from './constants';
import dataProcessor from './dataProcessor';
import responseHandler from './responseHandler';
import problemsHandler from './problemsHandler';
import { Zabbix } from './zabbix/zabbix';
import { ZabbixAPIError } from './zabbix/connectors/zabbix_api/zabbixAPICore';
import {
DataSourceApi,
// DataSourceInstanceSettings,
} from '@grafana/data';
// import { BackendSrv, DataSourceSrv } from '@grafana/runtime';
// import { ZabbixAlertingService } from './zabbixAlerting.service';
// import { ZabbixConnectionTestQuery, ZabbixConnectionInfo, TemplateSrv, TSDBResponse } from './types';
import { VariableQueryTypes } from './types';
const DEFAULT_ZABBIX_VERSION = 3;
import { VariableQueryTypes, ShowProblemTypes } from './types';
import { getBackendSrv } from '@grafana/runtime';
import { DataSourceApi, DataSourceInstanceSettings } from '@grafana/data';
export class ZabbixDatasource extends DataSourceApi {
name: string;
url: string;
basicAuth: any;
withCredentials: any;
/**
* @ngInject
* @param {DataSourceInstanceSettings} instanceSettings
* @param {TemplateSrv} templateSrv
* @param {BackendSrv} backendSrv
* @param {DataSourceSrv} datasourceSrv
* @param {ZabbixAlertingService} zabbixAlertingSrv
*/
constructor(instanceSettings, templateSrv, backendSrv, datasourceSrv, zabbixAlertingSrv) {
username: string;
password: string;
trends: boolean;
trendsFrom: string;
trendsRange: string;
cacheTTL: any;
alertingEnabled: boolean;
addThresholds: boolean;
alertingMinSeverity: string;
disableReadOnlyUsersAck: boolean;
enableDirectDBConnection: boolean;
dbConnectionDatasourceId: number;
dbConnectionDatasourceName: string;
dbConnectionRetentionPolicy: string;
enableDebugLog: boolean;
datasourceId: number;
zabbix: Zabbix;
replaceTemplateVars: (target: any, scopedVars?: any) => any;
/** @ngInject */
constructor(instanceSettings: DataSourceInstanceSettings, private templateSrv, private zabbixAlertingSrv) {
super(instanceSettings);
this.type = 'zabbix';
this.templateSrv = templateSrv;
this.backendSrv = backendSrv;
this.zabbixAlertingSrv = zabbixAlertingSrv;
this.enableDebugLog = config.buildInfo.env === 'development';
@@ -61,7 +72,7 @@ export class ZabbixDatasource extends DataSourceApi {
this.trendsRange = jsonData.trendsRange || '4d';
// Set cache update interval
var ttl = jsonData.cacheTTL || '1h';
const ttl = jsonData.cacheTTL || '1h';
this.cacheTTL = utils.parseInterval(ttl);
// Alerting options
@@ -71,7 +82,6 @@ export class ZabbixDatasource extends DataSourceApi {
// Other options
this.disableReadOnlyUsersAck = jsonData.disableReadOnlyUsersAck;
this.zabbixVersion = jsonData.zabbixVersion || DEFAULT_ZABBIX_VERSION;
// Direct DB Connection options
this.enableDirectDBConnection = jsonData.dbConnectionEnable || false;
@@ -79,21 +89,21 @@ export class ZabbixDatasource extends DataSourceApi {
this.dbConnectionDatasourceName = jsonData.dbConnectionDatasourceName;
this.dbConnectionRetentionPolicy = jsonData.dbConnectionRetentionPolicy;
let zabbixOptions = {
const zabbixOptions = {
url: this.url,
username: this.username,
password: this.password,
basicAuth: this.basicAuth,
withCredentials: this.withCredentials,
zabbixVersion: this.zabbixVersion,
cacheTTL: this.cacheTTL,
enableDirectDBConnection: this.enableDirectDBConnection,
dbConnectionDatasourceId: this.dbConnectionDatasourceId,
dbConnectionDatasourceName: this.dbConnectionDatasourceName,
dbConnectionRetentionPolicy: this.dbConnectionRetentionPolicy,
datasourceId: this.datasourceId,
};
this.zabbix = new Zabbix(zabbixOptions, datasourceSrv, backendSrv, this.datasourceId);
this.zabbix = new Zabbix(zabbixOptions);
}
////////////////////////
@@ -124,7 +134,7 @@ export class ZabbixDatasource extends DataSourceApi {
}
// Create request for each target
let promises = _.map(options.targets, t => {
const promises = _.map(options.targets, t => {
// Don't request for hidden targets
if (t.hide) {
return [];
@@ -144,40 +154,45 @@ export class ZabbixDatasource extends DataSourceApi {
this.replaceTargetVariables(target, options);
// Apply Time-related functions (timeShift(), etc)
let timeFunctions = bindFunctionDefs(target.functions, 'Time');
const timeFunctions = bindFunctionDefs(target.functions, 'Time');
if (timeFunctions.length) {
const [time_from, time_to] = utils.sequence(timeFunctions)([timeFrom, timeTo]);
timeFrom = time_from;
timeTo = time_to;
}
let timeRange = [timeFrom, timeTo];
const timeRange = [timeFrom, timeTo];
let useTrends = this.isUseTrends(timeRange);
const useTrends = this.isUseTrends(timeRange);
// Metrics or Text query mode
if (!target.mode || target.mode === c.MODE_METRICS || target.mode === c.MODE_TEXT) {
// Metrics or Text query
if (!target.queryType || target.queryType === c.MODE_METRICS || target.queryType === c.MODE_TEXT) {
// Don't request undefined targets
if (!target.group || !target.host || !target.item) {
return [];
}
if (!target.mode || target.mode === c.MODE_METRICS) {
if (!target.queryType || target.queryType === c.MODE_METRICS) {
return this.queryNumericData(target, timeRange, useTrends, options);
} else if (target.mode === c.MODE_TEXT) {
} else if (target.queryType === c.MODE_TEXT) {
return this.queryTextData(target, timeRange);
} else {
return [];
}
} else if (target.mode === c.MODE_ITEMID) {
// Item ID mode
} else if (target.queryType === c.MODE_ITEMID) {
// Item ID query
if (!target.itemids) {
return [];
}
return this.queryItemIdData(target, timeRange, useTrends, options);
} else if (target.mode === c.MODE_ITSERVICE) {
// IT services mode
} else if (target.queryType === c.MODE_ITSERVICE) {
// IT services query
return this.queryITServiceData(target, timeRange, options);
} else if (target.mode === c.MODE_TRIGGERS) {
// Triggers mode
} else if (target.queryType === c.MODE_TRIGGERS) {
// Triggers query
return this.queryTriggersData(target, timeRange);
} else if (target.queryType === c.MODE_PROBLEMS) {
// Problems query
return this.queryProblems(target, timeRange, options);
} else {
return [];
}
@@ -192,7 +207,7 @@ export class ZabbixDatasource extends DataSourceApi {
}
doTsdbRequest(options) {
const tsdbRequestData = {
const tsdbRequestData: any = {
queries: options.targets.map(target => {
target.datasourceId = this.datasourceId;
target.queryType = 'zabbixAPI';
@@ -205,7 +220,7 @@ export class ZabbixDatasource extends DataSourceApi {
tsdbRequestData.to = options.range.to.valueOf().toString();
}
return this.backendSrv.post('/api/tsdb/query', tsdbRequestData);
return getBackendSrv().post('/api/tsdb/query', tsdbRequestData);
}
/**
@@ -224,15 +239,15 @@ export class ZabbixDatasource extends DataSourceApi {
]
};
return this.backendSrv.post('/api/tsdb/query', tsdbRequestData);
return getBackendSrv().post('/api/tsdb/query', tsdbRequestData);
}
/**
* Query target data for Metrics mode
* Query target data for Metrics
*/
queryNumericData(target, timeRange, useTrends, options) {
let queryStart, queryEnd;
let getItemOptions = {
const getItemOptions = {
itemtype: 'num'
};
return this.zabbix.getItemsFromTarget(target, getItemOptions)
@@ -242,7 +257,7 @@ export class ZabbixDatasource extends DataSourceApi {
}).then(result => {
queryEnd = new Date().getTime();
if (this.enableDebugLog) {
console.debug(`Datasource::Performance Query Time (${this.name}): ${queryEnd - queryStart}`);
console.log(`Datasource::Performance Query Time (${this.name}): ${queryEnd - queryStart}`);
}
return result;
});
@@ -269,18 +284,18 @@ export class ZabbixDatasource extends DataSourceApi {
getTrendValueType(target) {
// Find trendValue() function and get specified trend value
var trendFunctions = _.map(metricFunctions.getCategories()['Trends'], 'name');
var trendValueFunc = _.find(target.functions, func => {
const trendFunctions = _.map(metricFunctions.getCategories()['Trends'], 'name');
const trendValueFunc = _.find(target.functions, func => {
return _.includes(trendFunctions, func.def.name);
});
return trendValueFunc ? trendValueFunc.params[0] : "avg";
}
applyDataProcessingFunctions(timeseries_data, target) {
let transformFunctions = bindFunctionDefs(target.functions, 'Transform');
let aggregationFunctions = bindFunctionDefs(target.functions, 'Aggregate');
let filterFunctions = bindFunctionDefs(target.functions, 'Filter');
let aliasFunctions = bindFunctionDefs(target.functions, 'Alias');
const transformFunctions = bindFunctionDefs(target.functions, 'Transform');
const aggregationFunctions = bindFunctionDefs(target.functions, 'Aggregate');
const filterFunctions = bindFunctionDefs(target.functions, 'Filter');
const aliasFunctions = bindFunctionDefs(target.functions, 'Alias');
// Apply transformation functions
timeseries_data = _.cloneDeep(_.map(timeseries_data, timeseries => {
@@ -298,8 +313,8 @@ export class ZabbixDatasource extends DataSourceApi {
let dp = _.map(timeseries_data, 'datapoints');
dp = utils.sequence(aggregationFunctions)(dp);
let aggFuncNames = _.map(metricFunctions.getCategories()['Aggregate'], 'name');
let lastAgg = _.findLast(target.functions, func => {
const aggFuncNames = _.map(metricFunctions.getCategories()['Aggregate'], 'name');
const lastAgg = _.findLast(target.functions, func => {
return _.includes(aggFuncNames, func.def.name);
});
@@ -310,7 +325,7 @@ export class ZabbixDatasource extends DataSourceApi {
}
// Apply alias functions
_.forEach(timeseries_data, utils.sequence(aliasFunctions));
_.forEach(timeseries_data, utils.sequence(aliasFunctions).bind(this));
// Apply Time-related functions (timeShift(), etc)
// Find timeShift() function and get specified trend value
@@ -321,11 +336,11 @@ export class ZabbixDatasource extends DataSourceApi {
applyTimeShiftFunction(timeseries_data, target) {
// Find timeShift() function and get specified interval
let timeShiftFunc = _.find(target.functions, (func) => {
const timeShiftFunc = _.find(target.functions, (func) => {
return func.def.name === 'timeShift';
});
if (timeShiftFunc) {
let shift = timeShiftFunc.params[0];
const shift = timeShiftFunc.params[0];
_.forEach(timeseries_data, (series) => {
series.datapoints = dataProcessor.unShiftTimeSeries(shift, series.datapoints);
});
@@ -333,10 +348,10 @@ export class ZabbixDatasource extends DataSourceApi {
}
/**
* Query target data for Text mode
* Query target data for Text
*/
queryTextData(target, timeRange) {
let options = {
const options = {
itemtype: 'text'
};
return this.zabbix.getItemsFromTarget(target, options)
@@ -346,7 +361,7 @@ export class ZabbixDatasource extends DataSourceApi {
}
/**
* Query target data for Item ID mode
* Query target data for Item ID
*/
queryItemIdData(target, timeRange, useTrends, options) {
let itemids = target.itemids;
@@ -364,7 +379,7 @@ export class ZabbixDatasource extends DataSourceApi {
}
/**
* Query target data for IT Services mode
* Query target data for IT Services
*/
queryITServiceData(target, timeRange, options) {
// Don't show undefined and hidden targets
@@ -382,21 +397,26 @@ export class ZabbixDatasource extends DataSourceApi {
itServiceFilter = this.replaceTemplateVars(target.itServiceFilter, options.scopedVars);
}
options.slaInterval = target.slaInterval;
return this.zabbix.getITServices(itServiceFilter)
.then(itservices => {
if (options.isOldVersion) {
itservices = _.filter(itservices, {'serviceid': target.itservice?.serviceid});
}
return this.zabbix.getSLA(itservices, timeRange, target, options);})
.then(itservicesdp => this.applyDataProcessingFunctions(itservicesdp, target));
}
queryTriggersData(target, timeRange) {
let [timeFrom, timeTo] = timeRange;
const [timeFrom, timeTo] = timeRange;
return this.zabbix.getHostsFromTarget(target)
.then(results => {
let [hosts, apps] = results;
const [hosts, apps] = results;
if (hosts.length) {
let hostids = _.map(hosts, 'hostid');
let appids = _.map(apps, 'applicationid');
let options = {
const hostids = _.map(hosts, 'hostid');
const appids = _.map(apps, 'applicationid');
const options = {
minSeverity: target.triggers.minSeverity,
acknowledged: target.triggers.acknowledged,
count: target.triggers.count,
@@ -417,6 +437,78 @@ export class ZabbixDatasource extends DataSourceApi {
});
}
queryProblems(target, timeRange, options) {
const [timeFrom, timeTo] = timeRange;
const userIsEditor = contextSrv.isEditor || contextSrv.isGrafanaAdmin;
let proxies;
let showAckButton = true;
const showProblems = target.showProblems || ShowProblemTypes.Problems;
const showProxy = target.options.hostProxy;
const getProxiesPromise = showProxy ? this.zabbix.getProxies() : () => [];
showAckButton = !this.disableReadOnlyUsersAck || userIsEditor;
// Replace template variables
const groupFilter = this.replaceTemplateVars(target.group?.filter, options.scopedVars);
const hostFilter = this.replaceTemplateVars(target.host?.filter, options.scopedVars);
const appFilter = this.replaceTemplateVars(target.application?.filter, options.scopedVars);
const proxyFilter = this.replaceTemplateVars(target.proxy?.filter, options.scopedVars);
const triggerFilter = this.replaceTemplateVars(target.trigger?.filter, options.scopedVars);
const tagsFilter = this.replaceTemplateVars(target.tags?.filter, options.scopedVars);
const replacedTarget = {
...target,
trigger: { filter: triggerFilter },
tags: { filter: tagsFilter },
};
const problemsOptions: any = {
recent: showProblems === ShowProblemTypes.Recent,
minSeverity: target.options?.minSeverity,
limit: target.options?.limit,
};
if (target.options?.acknowledged === 0 || target.options?.acknowledged === 1) {
problemsOptions.acknowledged = target.options?.acknowledged ? true : false;
}
if (target.options?.minSeverity) {
const severities = [0, 1, 2, 3, 4, 5].filter(v => v >= target.options?.minSeverity);
problemsOptions.severities = severities;
}
if (showProblems === ShowProblemTypes.History) {
problemsOptions.timeFrom = timeFrom;
problemsOptions.timeTo = timeTo;
}
const getProblemsPromise = showProblems === ShowProblemTypes.History ?
this.zabbix.getProblemsHistory(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions) :
this.zabbix.getProblems(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions);
const problemsPromises = Promise.all([
getProblemsPromise,
getProxiesPromise
])
.then(([problems, sourceProxies]) => {
proxies = _.keyBy(sourceProxies, 'proxyid');
return problems;
})
.then(problems => problemsHandler.setMaintenanceStatus(problems))
.then(problems => problemsHandler.setAckButtonStatus(problems, showAckButton))
.then(problems => problemsHandler.filterTriggersPre(problems, replacedTarget))
.then(problems => problemsHandler.addTriggerDataSource(problems, target))
.then(problems => problemsHandler.addTriggerHostProxy(problems, proxies));
return problemsPromises.then(problems => {
const problemsDataFrame = problemsHandler.toDataFrame(problems);
return problemsDataFrame;
});
}
/**
* Test connection to Zabbix API and external history DB.
*/
@@ -436,30 +528,26 @@ export class ZabbixDatasource extends DataSourceApi {
title: "Success",
message: message
};
}
catch (error) {
} catch (error) {
if (error instanceof ZabbixAPIError) {
return {
status: "error",
title: error.message,
message: error.message
};
}
else if (error.data && error.data.message) {
} else if (error.data && error.data.message) {
return {
status: "error",
title: "Zabbix Client Error",
message: error.data.message
};
}
else if (typeof (error) === 'string') {
} else if (typeof (error) === 'string') {
return {
status: "error",
title: "Unknown Error",
message: error
};
}
else {
} else {
console.log(error);
return {
status: "error",
@@ -470,20 +558,6 @@ export class ZabbixDatasource extends DataSourceApi {
}
}
/**
* Get Zabbix version
*/
getVersion() {
return this.zabbix.getVersion()
.then(version => {
const zabbixVersion = utils.parseVersion(version);
if (!zabbixVersion) {
return null;
}
return zabbixVersion.major;
});
}
////////////////
// Templating //
////////////////
@@ -495,7 +569,7 @@ export class ZabbixDatasource extends DataSourceApi {
* @return {string} Metric name - group, host, app or item or list
* of metrics in "{metric1,metcic2,...,metricN}" format.
*/
metricFindQuery(query) {
metricFindQuery(query, options) {
let resultPromise;
let queryModel = _.cloneDeep(query);
@@ -512,6 +586,8 @@ export class ZabbixDatasource extends DataSourceApi {
queryModel[prop] = this.replaceTemplateVars(queryModel[prop], {});
}
const { group, host, application, item } = queryModel;
switch (queryModel.queryType) {
case VariableQueryTypes.Group:
resultPromise = this.zabbix.getGroups(queryModel.group);
@@ -525,6 +601,10 @@ export class ZabbixDatasource extends DataSourceApi {
case VariableQueryTypes.Item:
resultPromise = this.zabbix.getItems(queryModel.group, queryModel.host, queryModel.application, queryModel.item);
break;
case VariableQueryTypes.ItemValues:
const range = options?.range;
resultPromise = this.zabbix.getItemValues(group, host, application, item, { range });
break;
default:
resultPromise = Promise.resolve([]);
break;
@@ -543,71 +623,63 @@ export class ZabbixDatasource extends DataSourceApi {
const timeRange = options.range || options.rangeRaw;
const timeFrom = Math.ceil(dateMath.parse(timeRange.from) / 1000);
const timeTo = Math.ceil(dateMath.parse(timeRange.to) / 1000);
var annotation = options.annotation;
var showOkEvents = annotation.showOkEvents ? c.SHOW_ALL_EVENTS : c.SHOW_OK_EVENTS;
const annotation = options.annotation;
// Show all triggers
let triggersOptions = {
showTriggers: c.SHOW_ALL_TRIGGERS,
hideHostsInMaintenance: false
const problemsOptions: any = {
value: annotation.showOkEvents ? ['0', '1'] : '1',
valueFromEvent: true,
timeFrom,
timeTo,
};
var getTriggers = this.zabbix.getTriggers(this.replaceTemplateVars(annotation.group, {}),
this.replaceTemplateVars(annotation.host, {}),
this.replaceTemplateVars(annotation.application, {}),
triggersOptions);
if (annotation.minseverity) {
const severities = [0, 1, 2, 3, 4, 5].filter(v => v >= Number(annotation.minseverity));
problemsOptions.severities = severities;
}
return getTriggers.then(triggers => {
const groupFilter = this.replaceTemplateVars(annotation.group, {});
const hostFilter = this.replaceTemplateVars(annotation.host, {});
const appFilter = this.replaceTemplateVars(annotation.application, {});
const proxyFilter = undefined;
return this.zabbix.getProblemsHistory(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions)
.then(problems => {
// Filter triggers by description
let triggerName = this.replaceTemplateVars(annotation.trigger, {});
if (utils.isRegex(triggerName)) {
triggers = _.filter(triggers, trigger => {
return utils.buildRegex(triggerName).test(trigger.description);
const problemName = this.replaceTemplateVars(annotation.trigger, {});
if (utils.isRegex(problemName)) {
problems = _.filter(problems, p => {
return utils.buildRegex(problemName).test(p.description);
});
} else if (triggerName) {
triggers = _.filter(triggers, trigger => {
return trigger.description === triggerName;
} else if (problemName) {
problems = _.filter(problems, p => {
return p.description === problemName;
});
}
// Remove events below the chose severity
triggers = _.filter(triggers, trigger => {
return Number(trigger.priority) >= Number(annotation.minseverity);
});
var objectids = _.map(triggers, 'triggerid');
return this.zabbix
.getEvents(objectids, timeFrom, timeTo, showOkEvents)
.then(events => {
var indexedTriggers = _.keyBy(triggers, 'triggerid');
// Hide acknowledged events if option enabled
if (annotation.hideAcknowledged) {
events = _.filter(events, event => {
return !event.acknowledges.length;
});
}
return _.map(events, event => {
let tags;
if (annotation.showHostname) {
tags = _.map(event.hosts, 'name');
}
// Show event type (OK or Problem)
let title = Number(event.value) ? 'Problem' : 'OK';
let formatted_acknowledges = utils.formatAcknowledges(event.acknowledges);
return {
annotation: annotation,
time: event.clock * 1000,
title: title,
tags: tags,
text: indexedTriggers[event.objectid].description + formatted_acknowledges
};
});
// Hide acknowledged events if option enabled
if (annotation.hideAcknowledged) {
problems = _.filter(problems, p => {
return !p.acknowledges?.length;
});
}
return _.map(problems, p => {
const formattedAcknowledges = utils.formatAcknowledges(p.acknowledges);
let annotationTags: string[] = [];
if (annotation.showHostname) {
annotationTags = _.map(p.hosts, 'name');
}
return {
title: p.value === '1' ? 'Problem' : 'OK',
time: p.timestamp * 1000,
annotation: annotation,
text: p.name + formattedAcknowledges,
tags: annotationTags,
};
});
});
}
@@ -617,8 +689,8 @@ export class ZabbixDatasource extends DataSourceApi {
* or empty object if no related triggers are finded.
*/
alertQuery(options) {
let enabled_targets = filterEnabledTargets(options.targets);
let getPanelItems = _.map(enabled_targets, t => {
const enabled_targets = filterEnabledTargets(options.targets);
const getPanelItems = _.map(enabled_targets, t => {
let target = _.cloneDeep(t);
target = migrations.migrate(target);
this.replaceTargetVariables(target, options);
@@ -627,8 +699,8 @@ export class ZabbixDatasource extends DataSourceApi {
return Promise.all(getPanelItems)
.then(results => {
let items = _.flatten(results);
let itemids = _.map(items, 'itemid');
const items = _.flatten(results);
const itemids = _.map(items, 'itemid');
if (itemids.length === 0) {
return [];
@@ -646,12 +718,12 @@ export class ZabbixDatasource extends DataSourceApi {
let state = 'ok';
let firedTriggers = _.filter(triggers, {value: '1'});
const firedTriggers = _.filter(triggers, {value: '1'});
if (firedTriggers.length) {
state = 'alerting';
}
let thresholds = _.map(triggers, trigger => {
const thresholds = _.map(triggers, trigger => {
return getTriggerThreshold(trigger.expression);
});
@@ -665,7 +737,7 @@ export class ZabbixDatasource extends DataSourceApi {
// Replace template variables
replaceTargetVariables(target, options) {
let parts = ['group', 'host', 'application', 'item'];
const parts = ['group', 'host', 'application', 'item'];
_.forEach(parts, p => {
if (target[p] && target[p].filter) {
target[p].filter = this.replaceTemplateVars(target[p].filter, options.scopedVars);
@@ -685,10 +757,10 @@ export class ZabbixDatasource extends DataSourceApi {
}
isUseTrends(timeRange) {
let [timeFrom, timeTo] = timeRange;
let useTrendsFrom = Math.ceil(dateMath.parse('now-' + this.trendsFrom) / 1000);
let useTrendsRange = Math.ceil(utils.parseInterval(this.trendsRange) / 1000);
let useTrends = this.trends && (
const [timeFrom, timeTo] = timeRange;
const useTrendsFrom = Math.ceil(dateMath.parse('now-' + this.trendsFrom) / 1000);
const useTrendsRange = Math.ceil(utils.parseInterval(this.trendsRange) / 1000);
const useTrends = this.trends && (
(timeFrom < useTrendsFrom) ||
(timeTo - timeFrom > useTrendsRange)
);
@@ -697,20 +769,20 @@ export class ZabbixDatasource extends DataSourceApi {
}
function bindFunctionDefs(functionDefs, category) {
var aggregationFunctions = _.map(metricFunctions.getCategories()[category], 'name');
var aggFuncDefs = _.filter(functionDefs, function(func) {
const aggregationFunctions = _.map(metricFunctions.getCategories()[category], 'name');
const aggFuncDefs = _.filter(functionDefs, func => {
return _.includes(aggregationFunctions, func.def.name);
});
return _.map(aggFuncDefs, function(func) {
var funcInstance = metricFunctions.createFuncInstance(func.def, func.params);
return _.map(aggFuncDefs, func => {
const funcInstance = metricFunctions.createFuncInstance(func.def, func.params);
return funcInstance.bindFunction(dataProcessor.metricFunctions);
});
}
function getConsolidateBy(target) {
let consolidateBy;
let funcDef = _.find(target.functions, func => {
const funcDef = _.find(target.functions, func => {
return func.def.name === 'consolidateBy';
});
if (funcDef && funcDef.params && funcDef.params.length) {
@@ -720,8 +792,8 @@ function getConsolidateBy(target) {
}
function downsampleSeries(timeseries_data, options) {
let defaultAgg = dataProcessor.aggregationFunctions['avg'];
let consolidateByFunc = dataProcessor.aggregationFunctions[options.consolidateBy] || defaultAgg;
const defaultAgg = dataProcessor.aggregationFunctions['avg'];
const consolidateByFunc = dataProcessor.aggregationFunctions[options.consolidateBy] || defaultAgg;
return _.map(timeseries_data, timeseries => {
if (timeseries.datapoints.length > options.maxDataPoints) {
timeseries.datapoints = dataProcessor
@@ -753,7 +825,7 @@ export function zabbixTemplateFormat(value) {
return utils.escapeRegex(value);
}
var escapedValues = _.map(value, utils.escapeRegex);
const escapedValues = _.map(value, utils.escapeRegex);
return '(' + escapedValues.join('|') + ')';
}
@@ -773,7 +845,7 @@ function zabbixItemIdsTemplateFormat(value) {
* /$variable/ -> /a|b|c/ -> /a|b|c/
*/
function replaceTemplateVars(templateSrv, target, scopedVars) {
var replacedTarget = templateSrv.replace(target, scopedVars, zabbixTemplateFormat);
let replacedTarget = templateSrv.replace(target, scopedVars, zabbixTemplateFormat);
if (target !== replacedTarget && !utils.isRegex(replacedTarget)) {
replacedTarget = '/^' + replacedTarget + '$/';
}
@@ -787,8 +859,8 @@ function filterEnabledTargets(targets) {
}
function getTriggerThreshold(expression) {
let thresholdPattern = /.*[<>=]{1,2}([\d\.]+)/;
let finded_thresholds = expression.match(thresholdPattern);
const thresholdPattern = /.*[<>=]{1,2}([\d\.]+)/;
const finded_thresholds = expression.match(thresholdPattern);
if (finded_thresholds && finded_thresholds.length >= 2) {
let threshold = finded_thresholds[1];
threshold = Number(threshold);
@@ -797,7 +869,3 @@ function getTriggerThreshold(expression) {
return null;
}
}
// Fix for backward compatibility with lodash 2.4
if (!_.includes) {_.includes = _.contains;}
if (!_.keyBy) {_.keyBy = _.indexBy;}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -1,107 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.0"
id="Layer_1"
x="0px"
y="0px"
width="100px"
height="100px"
viewBox="692 0 100 100"
style="enable-background:new 692 0 100 100;"
xml:space="preserve"
inkscape:version="0.91 r"
sodipodi:docname="zabbix_app_logo.svg"
enable-background="new"><metadata
id="metadata13"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs11"><linearGradient
id="SVGID_1_"
gradientUnits="userSpaceOnUse"
x1="2.6005001"
y1="65.475197"
x2="94.377701"
y2="30.245199"><stop
id="stop34"
style="stop-color:#58595B"
offset="0.2583" /><stop
id="stop32"
style="stop-color:#646C70"
offset="0.2917" /><stop
id="stop30"
style="stop-color:#6C8087"
offset="0.3398" /><stop
id="stop28"
style="stop-color:#6D8F9B"
offset="0.3927" /><stop
id="stop26"
style="stop-color:#689BAA"
offset="0.4499" /><stop
id="stop24"
style="stop-color:#5FA3B5"
offset="0.5128" /><stop
id="stop22"
style="stop-color:#53A8BD"
offset="0.5837" /><stop
id="stop20"
style="stop-color:#47ABC2"
offset="0.6674" /><stop
id="stop18"
style="stop-color:#3FAEC5"
offset="0.7759" /><stop
id="stop16"
style="stop-color:#3CAFC7"
offset="1" /><stop
id="stop14"
style="stop-color:#3BB0C9"
offset="1" /></linearGradient></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1615"
inkscape:window-height="1026"
id="namedview9"
showgrid="false"
inkscape:zoom="4.285"
inkscape:cx="50.424685"
inkscape:cy="23.581186"
inkscape:window-x="65"
inkscape:window-y="24"
inkscape:window-maximized="1"
inkscape:current-layer="g5194" /><style
type="text/css"
id="style3">
.st0{fill:#787878;}
</style><g
inkscape:groupmode="layer"
id="g5194"
inkscape:label="Zabbix BG Original"
style="display:inline"><rect
style="fill:#d40000;fill-opacity:1"
id="rect5196"
width="100"
height="100"
x="692"
y="0" /></g><g
inkscape:groupmode="layer"
id="layer6"
inkscape:label="Zabbix Original Z"
style="display:inline"><path
d="m 715.54426,16.689227 52.91147,0 0,6.87033 -42.58255,52.167008 43.62047,0 0,7.584207 -54.9873,0 0,-6.871516 42.58255,-52.166552 -41.54464,0 0,-7.583477 z"
style="display:inline;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path4169-6"
inkscape:connector-curvature="0" /></g></svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -1,8 +1,8 @@
import _ from 'lodash';
import $ from 'jquery';
import { isNumeric } from './utils';
var index = [];
var categories = {
const index = [];
const categories = {
Transform: [],
Aggregate: [],
Filter: [],
@@ -298,11 +298,15 @@ addFuncDef({
defaultParams: ['avg'],
});
_.each(categories, function(funcList, catName) {
_.each(categories, (funcList, catName) => {
categories[catName] = _.sortBy(funcList, 'name');
});
class FuncInstance {
def: any;
params: any;
text: string;
constructor(funcDef, params) {
this.def = funcDef;
@@ -318,13 +322,13 @@ class FuncInstance {
}
bindFunction(metricFunctions) {
var func = metricFunctions[this.def.name];
const func = metricFunctions[this.def.name];
if (func) {
// Bind function arguments
var bindedFunc = func;
var param;
for (var i = 0; i < this.params.length; i++) {
let bindedFunc = func;
let param;
for (let i = 0; i < this.params.length; i++) {
param = this.params[i];
// Convert numeric params
@@ -341,23 +345,21 @@ class FuncInstance {
}
render(metricExp) {
var str = this.def.name + '(';
var parameters = _.map(this.params, function(value, index) {
var paramType = this.def.params[index].type;
const str = this.def.name + '(';
const parameters = _.map(this.params, (value, index) => {
const paramType = this.def.params[index].type;
if (paramType === 'int' ||
paramType === 'float' ||
paramType === 'value_or_series' ||
paramType === 'boolean') {
return value;
}
else if (paramType === 'int_or_interval' && $.isNumeric(value)) {
} else if (paramType === 'int_or_interval' && isNumeric(value)) {
return value;
}
return "'" + value + "'";
}, this);
});
if (metricExp) {
parameters.unshift(metricExp);
@@ -378,16 +380,15 @@ class FuncInstance {
// handle optional parameters
// if string contains ',' and next param is optional, split and update both
if (this._hasMultipleParamsInString(strValue, index)) {
_.each(strValue.split(','), function(partVal, idx) {
_.each(strValue.split(','), (partVal, idx) => {
this.updateParam(partVal.trim(), idx);
}, this);
});
return;
}
if (strValue === '' && this.def.params[index].optional) {
this.params.splice(index, 1);
}
else {
}else {
this.params[index] = strValue;
}
@@ -400,7 +401,7 @@ class FuncInstance {
return;
}
var text = this.def.name + '(';
let text = this.def.name + '(';
text += this.params.join(', ');
text += ')';
this.text = text;

View File

@@ -1,5 +1,6 @@
import _ from 'lodash';
import { ZabbixMetricsQuery } from './types';
import * as c from './constants';
/**
* Query format migration.
@@ -28,6 +29,34 @@ export function migrateFrom2To3version(target: ZabbixMetricsQuery) {
return target;
}
function migratePercentileAgg(target) {
if (target.functions) {
for (const f of target.functions) {
if (f.def && f.def.name === 'percentil') {
f.def.name = 'percentile';
}
}
}
}
function migrateQueryType(target) {
if (target.queryType === undefined) {
if (target.mode === 'Metrics') {
// Explore mode
target.queryType = c.MODE_METRICS;
} else if (target.mode !== undefined) {
target.queryType = target.mode;
delete target.mode;
}
}
}
function migrateSLA(target) {
if (target.queryType === c.MODE_ITSERVICE && !target.slaInterval) {
target.slaInterval = 'none';
}
}
export function migrate(target) {
target.resultFormat = target.resultFormat || 'time_series';
target = fixTargetGroup(target);
@@ -35,6 +64,8 @@ export function migrate(target) {
return migrateFrom2To3version(target);
}
migratePercentileAgg(target);
migrateQueryType(target);
migrateSLA(target);
return target;
}
@@ -53,16 +84,6 @@ function convertToRegex(str) {
}
}
function migratePercentileAgg(target) {
if (target.functions) {
for (const f of target.functions) {
if (f.def && f.def.name === 'percentil') {
f.def.name = 'percentile';
}
}
}
}
export const DS_CONFIG_SCHEMA = 2;
export function migrateDSConfig(jsonData) {
if (!jsonData) {

View File

@@ -73,15 +73,6 @@
placeholder="1h">
</input>
</div>
<div class="gf-form max-width-20">
<span class="gf-form-label width-12">Zabbix version</span>
<div class="gf-form-select-wrapper max-width-7">
<select class="gf-form-input" ng-model="ctrl.current.jsonData.zabbixVersion"
ng-options="s.value as s.name for s in ctrl.zabbixVersions">
</select>
</div>
</div>
</div>
<div class="gf-form-group">

View File

@@ -5,14 +5,14 @@
<label class="gf-form-label width-7">Query Mode</label>
<div class="gf-form-select-wrapper max-width-20">
<select class="gf-form-input"
ng-change="ctrl.switchEditorMode(ctrl.target.mode)"
ng-model="ctrl.target.mode"
ng-options="m.mode as m.text for m in ctrl.editorModes">
ng-change="ctrl.switchEditorMode(ctrl.target.queryType)"
ng-model="ctrl.target.queryType"
ng-options="m.queryType as m.text for m in ctrl.editorModes">
</select>
</div>
</div>
<div class="gf-form" ng-show="ctrl.target.mode == editorMode.TEXT">
<label class="gf-form-label query-keyword width-8">Format As</label>
<div class="gf-form" ng-show="ctrl.target.queryType == editorMode.TEXT">
<label class="gf-form-label query-keyword width-7">Format As</label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input gf-size-auto" ng-model="ctrl.target.resultFormat" ng-options="f.value as f.text for f in ctrl.resultFormats" ng-change="ctrl.refresh()"></select>
</div>
@@ -23,7 +23,7 @@
</div>
<!-- IT Service editor -->
<div class="gf-form-inline" ng-show="ctrl.target.mode == editorMode.ITSERVICE">
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.ITSERVICE">
<div class="gf-form max-width-20">
<label class="gf-form-label query-keyword width-7">IT Service</label>
<input type="text"
@@ -40,7 +40,7 @@
</input>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword">Property</label>
<label class="gf-form-label query-keyword width-7">Property</label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input"
ng-change="ctrl.onTargetBlur()"
@@ -49,12 +49,22 @@
</select>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword width-7">Interval</label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input"
ng-change="ctrl.onTargetBlur()"
ng-model="ctrl.target.slaInterval"
ng-options="i.value as i.text for i in ctrl.slaIntervals">
</select>
</div>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline" ng-show="ctrl.target.mode == editorMode.METRICS || ctrl.target.mode == editorMode.TEXT || ctrl.target.mode == editorMode.TRIGGERS">
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.TEXT || ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
<!-- Select Group -->
<div class="gf-form max-width-20">
<label class="gf-form-label query-keyword width-7">Group</label>
@@ -71,8 +81,8 @@
}"></input>
</div>
<!-- Select Host -->
<div class="gf-form">
<label class="gf-form-label query-keyword width-8">Host</label>
<div class="gf-form max-width-20">
<label class="gf-form-label query-keyword width-7">Host</label>
<input type="text"
ng-model="ctrl.target.host.filter"
bs-typeahead="ctrl.getHostNames"
@@ -86,12 +96,27 @@
}">
</div>
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
<label class="gf-form-label query-keyword width-7">Proxy</label>
<input type="text"
ng-model="ctrl.target.proxy.filter"
bs-typeahead="ctrl.getProxyNames"
ng-blur="ctrl.onTargetBlur()"
data-min-length=0
data-items=100
class="gf-form-input width-14"
ng-class="{
'zbx-variable': ctrl.isVariable(ctrl.target.proxy.filter),
'zbx-regex': ctrl.isRegex(ctrl.target.proxy.filter)
}">
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline" ng-show="ctrl.target.mode == editorMode.METRICS || ctrl.target.mode == editorMode.TEXT || ctrl.target.mode == editorMode.TRIGGERS">
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.TEXT || ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
<!-- Select Application -->
<div class="gf-form max-width-20">
<label class="gf-form-label query-keyword width-7">Application</label>
@@ -109,8 +134,8 @@
</div>
<!-- Select Item -->
<div class="gf-form" ng-show="ctrl.target.mode == editorMode.METRICS || ctrl.target.mode == editorMode.TEXT">
<label class="gf-form-label query-keyword width-8">Item</label>
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.TEXT">
<label class="gf-form-label query-keyword width-7">Item</label>
<input type="text"
ng-model="ctrl.target.item.filter"
bs-typeahead="ctrl.getItemNames"
@@ -124,62 +149,93 @@
}">
</div>
<div class="gf-form max-width-23" ng-show="ctrl.target.mode == editorMode.TRIGGERS">
<label class="gf-form-label query-keyword width-8">Min Severity</label>
<div class="gf-form-select-wrapper width-16">
<select class="gf-form-input"
ng-change="ctrl.onTargetBlur()"
ng-model="ctrl.target.triggers.minSeverity"
ng-options="s.val as s.text for s in ctrl.triggerSeverity">
</select>
</div>
</div>
<div class="gf-form max-width-20" ng-show="ctrl.target.mode == editorMode.TRIGGERS">
<label class="gf-form-label query-keyword width-8">Acknowledged</label>
<div class="gf-form-select-wrapper width-12">
<select class="gf-form-input"
ng-change="ctrl.onTargetBlur()"
ng-model="ctrl.target.triggers.acknowledged"
ng-options="a.value as a.text for a in ctrl.ackFilters">
</select>
</div>
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
<label class="gf-form-label query-keyword width-7">Problem</label>
<input type="text"
ng-model="ctrl.target.trigger.filter"
ng-blur="ctrl.onTargetBlur()"
placeholder="Problem name"
class="gf-form-input"
ng-style="ctrl.target.trigger.style"
ng-class="{
'zbx-variable': ctrl.isVariable(ctrl.target.trigger.filter),
'zbx-regex': ctrl.isRegex(ctrl.target.trigger.filter)
}"
empty-to-null>
</div>
<gf-form-switch class="gf-form" label="Count" ng-show="ctrl.target.mode == editorMode.TRIGGERS"
checked="ctrl.target.triggers.count" on-change="ctrl.onTargetBlur()">
</gf-form-switch>
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
<label class="gf-form-label query-keyword width-7">Tags</label>
<input type="text" class="gf-form-input width-14"
ng-model="ctrl.target.tags.filter"
ng-blur="ctrl.onTargetBlur()"
placeholder="tag1:value1, tag2:value2">
</div>
<div class="gf-form gf-form--grow">
<label class="gf-form-label gf-form-label--grow">
<a ng-click="ctrl.toggleQueryOptions()" ng-hide="ctrl.target.mode == editorMode.TRIGGERS">
<i class="fa fa-caret-down" ng-show="ctrl.showQueryOptions"></i>
<i class="fa fa-caret-right" ng-hide="ctrl.showQueryOptions"></i>
{{ctrl.queryOptionsText}}
</a>
</label>
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<!-- Query options -->
<div class="gf-form-group" ng-if="ctrl.showQueryOptions">
<div class="gf-form offset-width-7" ng-hide="ctrl.target.mode == editorMode.TRIGGERS">
<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 class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
<label class="gf-form-label query-keyword width-7">Show</label>
<div class="gf-form-select-wrapper max-width-20">
<select class="gf-form-input"
ng-model="ctrl.target.showProblems"
ng-options="v.value as v.text for v in ctrl.showProblemsOptions"
ng-change="ctrl.onTargetBlur()">
</select>
</div>
</div>
<div class="gf-form offset-width-7" ng-show="ctrl.target.mode === 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 max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
<label class="gf-form-label query-keyword width-7">Min severity</label>
<div class="gf-form-select-wrapper max-width-20">
<select class="gf-form-input"
ng-model="ctrl.target.options.minSeverity"
ng-options="v.val as v.text for v in ctrl.severityOptions"
ng-change="ctrl.onTargetBlur()">
</select>
</div>
</div>
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.TRIGGERS">
<label class="gf-form-label query-keyword width-7">Min Severity</label>
<div class="gf-form-select-wrapper width-14">
<select class="gf-form-input"
ng-change="ctrl.onTargetBlur()"
ng-model="ctrl.target.triggers.minSeverity"
ng-options="s.val as s.text for s in ctrl.severityOptions">
</select>
</div>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<!-- Text mode options -->
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.TEXT">
<!-- Text metric regex -->
<div class="gf-form max-width-20">
<label class="gf-form-label query-keyword width-7">Text filter</label>
<input type="text"
class="gf-form-input"
ng-model="ctrl.target.textFilter"
spellcheck='false'
placeholder="Text filter (regex)"
ng-blur="ctrl.onTargetBlur()">
</div>
<gf-form-switch class="gf-form" label="Use capture groups" checked="ctrl.target.useCaptureGroups" on-change="ctrl.onTargetBlur()">
</gf-form-switch>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<!-- Item IDs editor mode -->
<div class="gf-form-inline" ng-show="ctrl.target.mode == editorMode.ITEMID">
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.ITEMID">
<div class="gf-form max-width-20">
<label class="gf-form-label query-keyword width-7">Item IDs</label>
<input type="text"
@@ -201,7 +257,7 @@
</div>
<!-- Metric processing functions -->
<div class="gf-form-inline" ng-show="ctrl.target.mode == editorMode.METRICS || ctrl.target.mode == editorMode.ITEMID || ctrl.target.mode == editorMode.ITSERVICE">
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.ITEMID || ctrl.target.queryType == editorMode.ITSERVICE">
<div class="gf-form">
<label class="gf-form-label query-keyword width-7">Functions</label>
</div>
@@ -215,23 +271,82 @@
</div>
</div>
<!-- Text mode options -->
<div class="gf-form-inline" ng-show="ctrl.target.mode == editorMode.TEXT">
<!-- Text metric regex -->
<div class="gf-form max-width-20">
<label class="gf-form-label query-keyword width-7">Text filter</label>
<input type="text"
class="gf-form-input"
ng-model="ctrl.target.textFilter"
spellcheck='false'
placeholder="Text filter (regex)"
ng-blur="ctrl.onTargetBlur()">
<div class="gf-form gf-form--grow">
<label class="gf-form-label gf-form-label--grow">
<a ng-click="ctrl.toggleQueryOptions()">
<i class="fa fa-caret-down" ng-show="ctrl.showQueryOptions"></i>
<i class="fa fa-caret-right" ng-hide="ctrl.showQueryOptions"></i>
{{ctrl.queryOptionsText}}
</a>
</label>
</div>
<!-- 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>
<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>
<gf-form-switch class="gf-form" label="Use capture groups" checked="ctrl.target.useCaptureGroups" on-change="ctrl.onTargetBlur()">
</gf-form-switch>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
<div class="gf-form-group" ng-show="ctrl.target.queryType == editorMode.PROBLEMS || ctrl.target.queryType == editorMode.TRIGGERS">
<gf-form-switch class="gf-form" ng-show="ctrl.target.queryType == editorMode.TRIGGERS"
label-class="width-9"
label="Count"
checked="ctrl.target.triggers.count"
on-change="ctrl.onTargetBlur()">
</gf-form-switch>
<div class="gf-form" ng-show="ctrl.target.queryType == editorMode.PROBLEMS || ctrl.target.queryType == editorMode.TRIGGERS">
<label class="gf-form-label width-9">Acknowledged</label>
<div class="gf-form-select-wrapper width-12">
<select class="gf-form-input"
ng-change="ctrl.onQueryOptionChange()"
ng-model="ctrl.target.options.acknowledged"
ng-options="a.value as a.text for a in ctrl.ackFilters">
</select>
</div>
</div>
<div ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
<div class="gf-form">
<label class="gf-form-label width-9">Sort by</label>
<div class="gf-form-select-wrapper width-12">
<select class="gf-form-input"
ng-model="ctrl.target.options.sortProblems"
ng-options="f.value as f.text for f in ctrl.sortByFields"
ng-change="ctrl.onQueryOptionChange()">
</select>
</div>
</div>
<gf-form-switch class="gf-form"
label-class="width-9"
label="Hosts in maintenance"
checked="ctrl.target.options.hostsInMaintenance"
on-change="ctrl.onQueryOptionChange()">
</gf-form-switch>
<gf-form-switch class="gf-form"
label-class="width-9"
label="Host proxy"
checked="ctrl.target.options.hostProxy"
on-change="ctrl.onQueryOptionChange()">
</gf-form-switch>
<div class="gf-form">
<label class="gf-form-label width-9">Limit triggers</label>
<input class="gf-form-input width-5"
type="number" placeholder="100"
ng-model="ctrl.target.options.limit"
ng-model-onblur ng-change="ctrl.onQueryOptionChange()">
</div>
</div>
</div>
</div>
</query-editor-row>

View File

@@ -38,8 +38,8 @@
"url": "https://github.com/alexanderzobnin/grafana-zabbix"
},
"logos": {
"small": "img/zabbix_app_logo.svg",
"large": "img/zabbix_app_logo.svg"
"small": "img/icn-zabbix-datasource.svg",
"large": "img/icn-zabbix-datasource.svg"
}
}
}

View File

@@ -0,0 +1,201 @@
import _ from 'lodash';
import * as utils from '../datasource-zabbix/utils';
import { DataFrame, Field, FieldType, ArrayVector } from '@grafana/data';
import { ZBXProblem, ZBXTrigger, ProblemDTO, ZBXEvent } from './types';
export function joinTriggersWithProblems(problems: ZBXProblem[], triggers: ZBXTrigger[]): ProblemDTO[] {
const problemDTOList: ProblemDTO[] = [];
for (let i = 0; i < problems.length; i++) {
const p = problems[i];
const triggerId = Number(p.objectid);
const t = triggers[triggerId];
if (t) {
const problemDTO: ProblemDTO = {
timestamp: Number(p.clock),
triggerid: p.objectid,
eventid: p.eventid,
name: p.name,
severity: p.severity,
acknowledged: p.acknowledged,
acknowledges: p.acknowledges,
tags: p.tags,
suppressed: p.suppressed,
suppression_data: p.suppression_data,
description: t.description,
comments: t.comments,
value: t.value,
groups: t.groups,
hosts: t.hosts,
items: t.items,
alerts: t.alerts,
url: t.url,
expression: t.expression,
correlation_mode: t.correlation_mode,
correlation_tag: t.correlation_tag,
manual_close: t.manual_close,
state: t.state,
error: t.error,
};
problemDTOList.push(problemDTO);
}
}
return problemDTOList;
}
interface JoinOptions {
valueFromEvent?: boolean;
}
export function joinTriggersWithEvents(events: ZBXEvent[], triggers: ZBXTrigger[], options?: JoinOptions): ProblemDTO[] {
const { valueFromEvent } = options;
const problemDTOList: ProblemDTO[] = [];
for (let i = 0; i < events.length; i++) {
const e = events[i];
const triggerId = Number(e.objectid);
const t = triggers[triggerId];
if (t) {
const problemDTO: ProblemDTO = {
value: valueFromEvent ? e.value : t.value,
timestamp: Number(e.clock),
triggerid: e.objectid,
eventid: e.eventid,
name: e.name,
severity: e.severity,
acknowledged: e.acknowledged,
acknowledges: e.acknowledges,
tags: e.tags,
suppressed: e.suppressed,
description: t.description,
comments: t.comments,
groups: t.groups,
hosts: t.hosts,
items: t.items,
alerts: t.alerts,
url: t.url,
expression: t.expression,
correlation_mode: t.correlation_mode,
correlation_tag: t.correlation_tag,
manual_close: t.manual_close,
state: t.state,
error: t.error,
};
problemDTOList.push(problemDTO);
}
}
return problemDTOList;
}
export function setMaintenanceStatus(triggers) {
_.each(triggers, (trigger) => {
const maintenance_status = _.some(trigger.hosts, (host) => host.maintenance_status === '1');
trigger.maintenance = maintenance_status;
});
return triggers;
}
export function setAckButtonStatus(triggers, showAckButton) {
_.each(triggers, (trigger) => {
trigger.showAckButton = showAckButton;
});
return triggers;
}
export function addTriggerDataSource(triggers, target) {
_.each(triggers, (trigger) => {
trigger.datasource = target.datasource;
});
return triggers;
}
export function addTriggerHostProxy(triggers, proxies) {
triggers.forEach(trigger => {
if (trigger.hosts && trigger.hosts.length) {
const host = trigger.hosts[0];
if (host.proxy_hostid !== '0') {
const hostProxy = proxies[host.proxy_hostid];
host.proxy = hostProxy ? hostProxy.host : '';
}
}
});
return triggers;
}
export function filterTriggersPre(triggerList, replacedTarget) {
// Filter triggers by description
const triggerFilter = replacedTarget.trigger.filter;
if (triggerFilter) {
triggerList = filterTriggers(triggerList, triggerFilter);
}
// Filter by tags
if (replacedTarget.tags.filter) {
let tagsFilter = replacedTarget.tags.filter;
// replaceTemplateVars() builds regex-like string, so we should trim it.
tagsFilter = tagsFilter.replace('/^', '').replace('$/', '');
const tags = utils.parseTags(tagsFilter);
triggerList = _.filter(triggerList, trigger => {
return _.every(tags, tag => {
return _.find(trigger.tags, t => t.tag === tag.tag && (!tag.value || t.value === tag.value));
});
});
}
// Filter by maintenance status
if (!replacedTarget.options.hostsInMaintenance) {
triggerList = _.filter(triggerList, (trigger) => !trigger.maintenance);
}
return triggerList;
}
function filterTriggers(triggers, triggerFilter) {
if (utils.isRegex(triggerFilter)) {
return _.filter(triggers, trigger => {
return utils.buildRegex(triggerFilter).test(trigger.description);
});
} else {
return _.filter(triggers, trigger => {
return trigger.description === triggerFilter;
});
}
}
export function toDataFrame(problems: any[]): DataFrame {
const problemsField: Field<any> = {
name: 'Problems',
type: FieldType.other,
values: new ArrayVector(problems),
config: {},
};
const response: DataFrame = {
name: 'problems',
fields: [problemsField],
length: problems.length,
};
return response;
}
const problemsHandler = {
addTriggerDataSource,
addTriggerHostProxy,
setMaintenanceStatus,
setAckButtonStatus,
filterTriggersPre,
toDataFrame,
joinTriggersWithProblems,
joinTriggersWithEvents,
};
export default problemsHandler;

View File

@@ -4,6 +4,58 @@ import * as c from './constants';
import * as utils from './utils';
import * as metricFunctions from './metricFunctions';
import * as migrations from './migrations';
import { ShowProblemTypes } from './types';
function getTargetDefaults() {
return {
queryType: c.MODE_METRICS,
group: { 'filter': "" },
host: { 'filter': "" },
application: { 'filter': "" },
item: { 'filter': "" },
functions: [],
triggers: {
'count': true,
'minSeverity': 3,
'acknowledged': 2
},
trigger: {filter: ""},
tags: {filter: ""},
proxy: {filter: ""},
options: {
showDisabledItems: false,
skipEmptyValues: false,
},
table: {
'skipEmptyValues': false
},
};
}
function getSLATargetDefaults() {
return {
slaProperty: { name: "SLA", property: "sla" },
slaInterval: 'none',
};
}
function getProblemsTargetDefaults() {
return {
showProblems: ShowProblemTypes.Problems,
options: {
minSeverity: 0,
sortProblems: 'default',
acknowledged: 2,
hostsInMaintenance: false,
hostProxy: false,
limit: c.DEFAULT_ZABBIX_PROBLEMS_LIMIT,
},
};
}
function getSeverityOptions() {
return c.TRIGGER_SEVERITY;
}
export class ZabbixQueryController extends QueryCtrl {
@@ -17,11 +69,12 @@ export class ZabbixQueryController extends QueryCtrl {
this.templateSrv = templateSrv;
this.editorModes = [
{value: 'num', text: 'Metrics', mode: c.MODE_METRICS},
{value: 'text', text: 'Text', mode: c.MODE_TEXT},
{value: 'itservice', text: 'IT Services', mode: c.MODE_ITSERVICE},
{value: 'itemid', text: 'Item ID', mode: c.MODE_ITEMID},
{value: 'triggers', text: 'Triggers', mode: c.MODE_TRIGGERS}
{value: 'num', text: 'Metrics', queryType: c.MODE_METRICS},
{value: 'text', text: 'Text', queryType: c.MODE_TEXT},
{value: 'itservice', text: 'IT Services', queryType: c.MODE_ITSERVICE},
{value: 'itemid', text: 'Item ID', queryType: c.MODE_ITEMID},
{value: 'triggers', text: 'Triggers', queryType: c.MODE_TRIGGERS},
{value: 'problems', text: 'Problems', queryType: c.MODE_PROBLEMS},
];
this.$scope.editorMode = {
@@ -29,7 +82,8 @@ export class ZabbixQueryController extends QueryCtrl {
TEXT: c.MODE_TEXT,
ITSERVICE: c.MODE_ITSERVICE,
ITEMID: c.MODE_ITEMID,
TRIGGERS: c.MODE_TRIGGERS
TRIGGERS: c.MODE_TRIGGERS,
PROBLEMS: c.MODE_PROBLEMS,
};
this.slaPropertyList = [
@@ -40,15 +94,49 @@ export class ZabbixQueryController extends QueryCtrl {
{name: "Down time", property: "downtimeTime"}
];
this.slaIntervals = [
{ text: 'No interval', value: 'none' },
{ text: 'Auto', value: 'auto' },
{ text: '1 hour', value: '1h' },
{ text: '12 hours', value: '12h' },
{ text: '24 hours', value: '1d' },
{ text: '1 week', value: '1w' },
{ text: '1 month', value: '1M' },
];
this.ackFilters = [
{text: 'all triggers', value: 2},
{text: 'unacknowledged', value: 0},
{text: 'acknowledged', value: 1},
];
this.problemAckFilters = [
'all triggers',
'unacknowledged',
'acknowledged'
];
this.sortByFields = [
{ text: 'Default', value: 'default' },
{ text: 'Last change', value: 'lastchange' },
{ text: 'Severity', value: 'priority' },
];
this.showEventsFields = [
{ text: 'All', value: [0,1] },
{ text: 'OK', value: [0] },
{ text: 'Problems', value: 1 }
];
this.showProblemsOptions = [
{ text: 'Problems', value: 'problems' },
{ text: 'Recent problems', value: 'recent' },
{ text: 'History', value: 'history' },
];
this.resultFormats = [{ text: 'Time series', value: 'time_series' }, { text: 'Table', value: 'table' }];
this.triggerSeverity = c.TRIGGER_SEVERITY;
this.severityOptions = getSeverityOptions();
// Map functions for bs-typeahead
this.getGroupNames = _.bind(this.getMetricNames, this, 'groupList');
@@ -56,6 +144,7 @@ export class ZabbixQueryController extends QueryCtrl {
this.getApplicationNames = _.bind(this.getMetricNames, this, 'appList');
this.getItemNames = _.bind(this.getMetricNames, this, 'itemList');
this.getITServices = _.bind(this.getMetricNames, this, 'itServiceList');
this.getProxyNames = _.bind(this.getMetricNames, this, 'proxyList');
this.getVariables = _.bind(this.getTemplateVariables, this);
// Update metric suggestion when template variable was changed
@@ -80,40 +169,32 @@ export class ZabbixQueryController extends QueryCtrl {
_.defaults(this, scopeDefaults);
// Load default values
var targetDefaults = {
'mode': c.MODE_METRICS,
'group': { 'filter': "" },
'host': { 'filter': "" },
'application': { 'filter': "" },
'item': { 'filter': "" },
'functions': [],
'triggers': {
'count': true,
'minSeverity': 3,
'acknowledged': 2
},
'options': {
'showDisabledItems': false,
'skipEmptyValues': false
},
'table': {
'skipEmptyValues': false
}
};
_.defaults(target, targetDefaults);
const targetDefaults = getTargetDefaults();
_.defaultsDeep(target, targetDefaults);
if (this.panel.type === c.ZABBIX_PROBLEMS_PANEL_ID) {
target.queryType = c.MODE_PROBLEMS;
}
// Create function instances from saved JSON
target.functions = _.map(target.functions, function(func) {
return metricFunctions.createFuncInstance(func.def, func.params);
});
if (target.mode === c.MODE_METRICS ||
target.mode === c.MODE_TEXT ||
target.mode === c.MODE_TRIGGERS) {
this.initFilters();
if (target.queryType === c.MODE_ITSERVICE) {
_.defaultsDeep(target, getSLATargetDefaults());
}
else if (target.mode === c.MODE_ITSERVICE) {
_.defaults(target, {slaProperty: {name: "SLA", property: "sla"}});
if (target.queryType === c.MODE_PROBLEMS) {
_.defaultsDeep(target, getProblemsTargetDefaults());
}
if (target.queryType === c.MODE_METRICS ||
target.queryType === c.MODE_TEXT ||
target.queryType === c.MODE_TRIGGERS ||
target.queryType === c.MODE_PROBLEMS) {
this.initFilters();
} else if (target.queryType === c.MODE_ITSERVICE) {
this.suggestITServices();
}
};
@@ -123,14 +204,20 @@ export class ZabbixQueryController extends QueryCtrl {
}
initFilters() {
let itemtype = _.find(this.editorModes, {'mode': this.target.mode});
let itemtype = _.find(this.editorModes, {'queryType': this.target.queryType});
itemtype = itemtype ? itemtype.value : null;
return Promise.all([
const promises = [
this.suggestGroups(),
this.suggestHosts(),
this.suggestApps(),
this.suggestItems(itemtype)
]);
this.suggestItems(itemtype),
];
if (this.target.queryType === c.MODE_PROBLEMS) {
promises.push(this.suggestProxies());
}
return Promise.all(promises);
}
// Get list of metric names for bs-typeahead directive
@@ -207,6 +294,15 @@ export class ZabbixQueryController extends QueryCtrl {
});
}
suggestProxies() {
return this.zabbix.getProxies()
.then(response => {
const proxies = _.map(response, 'host');
this.metric.proxyList = proxies;
return proxies;
});
}
isRegex(str) {
return utils.isRegex(str);
}
@@ -302,19 +398,42 @@ export class ZabbixQueryController extends QueryCtrl {
}
renderQueryOptionsText() {
var optionsMap = {
const metricOptionsMap = {
showDisabledItems: "Show disabled items",
skipEmptyValues: "Skip empty values"
};
var options = [];
const problemsOptionsMap = {
sortProblems: "Sort problems",
acknowledged: "Acknowledged",
skipEmptyValues: "Skip empty values",
hostsInMaintenance: "Show hosts in maintenance",
limit: "Limit problems",
hostProxy: "Show proxy",
};
let optionsMap = {};
if (this.target.queryType === c.MODE_METRICS) {
optionsMap = metricOptionsMap;
} else if (this.target.queryType === c.MODE_PROBLEMS || this.target.queryType === c.MODE_TRIGGERS) {
optionsMap = problemsOptionsMap;
}
const options = [];
_.forOwn(this.target.options, (value, key) => {
if (value) {
if (value && optionsMap[key]) {
if (value === true) {
// Show only option name (if enabled) for boolean options
options.push(optionsMap[key]);
} else {
// Show "option = value" for another options
options.push(optionsMap[key] + " = " + value);
let optionValue = value;
if (value && value.text) {
optionValue = value.text;
} else if (value && value.value) {
optionValue = value.value;
}
options.push(optionsMap[key] + " = " + optionValue);
}
}
});
@@ -329,7 +448,8 @@ export class ZabbixQueryController extends QueryCtrl {
* 2 - Text metrics
*/
switchEditorMode(mode) {
this.target.mode = mode;
this.target.queryType = mode;
this.queryOptionsText = this.renderQueryOptionsText();
this.init();
this.targetChanged();
}

View File

@@ -29,3 +29,7 @@ This mode is suitable for rendering charts in grafana by passing itemids as url
##### Triggers
Active triggers count for selected hosts or table data like Zabbix _System status_ panel on the main dashboard.
#### Documentation links:
[Grafana-Zabbix Documentation](https://alexanderzobnin.github.io/grafana-zabbix)

View File

@@ -23,19 +23,35 @@ function convertHistory(history, items, addHostName, convertPointCallback) {
*/
// Group history by itemid
var grouped_history = _.groupBy(history, 'itemid');
var hosts = _.uniqBy(_.flatten(_.map(items, 'hosts')), 'hostid'); //uniqBy is needed to deduplicate
const grouped_history = _.groupBy(history, 'itemid');
const hosts = _.uniqBy(_.flatten(_.map(items, 'hosts')), 'hostid'); //uniqBy is needed to deduplicate
return _.map(grouped_history, function(historyPoint, itemid) {
var item = _.find(items, {'itemid': itemid});
var alias = item.name;
if (_.keys(hosts).length > 1 && addHostName) { //only when actual multi hosts selected
var host = _.find(hosts, {'hostid': item.hostid});
alias = host.name + ": " + alias;
return _.map(grouped_history, (hist, itemid) => {
const item = _.find(items, {'itemid': itemid}) as any;
let alias = item.name;
// Add scopedVars for using in alias functions
const scopedVars: any = {
'__zbx_item': { value: item.name },
'__zbx_item_name': { value: item.name },
'__zbx_item_key': { value: item.key_ },
};
if (_.keys(hosts).length > 0) {
const host = _.find(hosts, {'hostid': item.hostid});
scopedVars['__zbx_host'] = { value: host.host };
scopedVars['__zbx_host_name'] = { value: host.name };
// Only add host when multiple hosts selected
if (_.keys(hosts).length > 1 && addHostName) {
alias = host.name + ": " + alias;
}
}
return {
target: alias,
datapoints: _.map(historyPoint, convertPointCallback)
datapoints: _.map(hist, convertPointCallback),
scopedVars,
};
});
}
@@ -53,29 +69,29 @@ function handleHistory(history, items, addHostName = true) {
}
function handleTrends(history, items, valueType, addHostName = true) {
var convertPointCallback = _.partial(convertTrendPoint, valueType);
const convertPointCallback = _.partial(convertTrendPoint, valueType);
return convertHistory(history, items, addHostName, convertPointCallback);
}
function handleText(history, items, target, addHostName = true) {
let convertTextCallback = _.partial(convertText, target);
const convertTextCallback = _.partial(convertText, target);
return convertHistory(history, items, addHostName, convertTextCallback);
}
function handleHistoryAsTable(history, items, target) {
let table = new TableModel();
const table: any = new TableModel();
table.addColumn({text: 'Host'});
table.addColumn({text: 'Item'});
table.addColumn({text: 'Key'});
table.addColumn({text: 'Last value'});
let grouped_history = _.groupBy(history, 'itemid');
const grouped_history = _.groupBy(history, 'itemid');
_.each(items, (item) => {
let itemHistory = grouped_history[item.itemid] || [];
let lastPoint = _.last(itemHistory);
const itemHistory = grouped_history[item.itemid] || [];
const lastPoint = _.last(itemHistory);
let lastValue = lastPoint ? lastPoint.value : null;
if(target.options.skipEmptyValues && (!lastValue || lastValue === '')) {
if (target.options.skipEmptyValues && (!lastValue || lastValue === '')) {
return;
}
@@ -84,7 +100,7 @@ function handleHistoryAsTable(history, items, target) {
lastValue = extractText(lastValue, target.textFilter, target.useCaptureGroups);
}
let host = _.first(item.hosts);
let host: any = _.first(item.hosts);
host = host ? host.name : "";
table.rows.push([
@@ -110,22 +126,22 @@ function convertText(target, point) {
}
function extractText(str, pattern, useCaptureGroups) {
let extractPattern = new RegExp(pattern);
let extractedValue = extractPattern.exec(str);
const extractPattern = new RegExp(pattern);
const extractedValue = extractPattern.exec(str);
if (extractedValue) {
if (useCaptureGroups) {
extractedValue = extractedValue[1];
return extractedValue[1];
} else {
extractedValue = extractedValue[0];
return extractedValue[0];
}
}
return extractedValue;
return "";
}
function handleSLAResponse(itservice, slaProperty, slaObject) {
var targetSLA = slaObject[itservice.serviceid].sla;
const targetSLA = slaObject[itservice.serviceid].sla;
if (slaProperty.property === 'status') {
var targetStatus = parseInt(slaObject[itservice.serviceid].status);
const targetStatus = parseInt(slaObject[itservice.serviceid].status, 10);
return {
target: itservice.name + ' ' + slaProperty.name,
datapoints: [
@@ -134,7 +150,7 @@ function handleSLAResponse(itservice, slaProperty, slaObject) {
};
} else {
let i;
let slaArr = [];
const slaArr = [];
for (i = 0; i < targetSLA.length; i++) {
if (i === 0) {
slaArr.push([targetSLA[i][slaProperty.property], targetSLA[i].from * 1000]);
@@ -165,7 +181,7 @@ function handleTriggersResponse(triggers, groups, timeRange) {
} else {
const stats = getTriggerStats(triggers);
const groupNames = _.map(groups, 'name');
let table = new TableModel();
const table: any = new TableModel();
table.addColumn({text: 'Host group'});
_.each(_.orderBy(c.TRIGGER_SEVERITY, ['val'], ['desc']), (severity) => {
table.addColumn({text: severity.text});
@@ -182,11 +198,11 @@ function handleTriggersResponse(triggers, groups, timeRange) {
}
function getTriggerStats(triggers) {
let groups = _.uniq(_.flattenDeep(_.map(triggers, (trigger) => _.map(trigger.groups, 'name'))));
const groups = _.uniq(_.flattenDeep(_.map(triggers, (trigger) => _.map(trigger.groups, 'name'))));
// let severity = _.map(c.TRIGGER_SEVERITY, 'text');
let stats = {};
const stats = {};
_.each(groups, (group) => {
stats[group] = {0:0, 1:0, 2:0, 3:0, 4:0, 5:0}; // severity:count
stats[group] = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0}; // severity:count
});
_.each(triggers, (trigger) => {
_.each(trigger.groups, (group) => {
@@ -205,7 +221,7 @@ function convertHistoryPoint(point) {
}
function convertTrendPoint(valueType, point) {
var value;
let value;
switch (valueType) {
case "min":
value = point.value_min;
@@ -242,6 +258,3 @@ export default {
handleTriggersResponse,
sortTimeseries
};
// Fix for backward compatibility with lodash 2.4
if (!_.uniqBy) {_.uniqBy = _.uniq;}

View File

@@ -4,6 +4,12 @@ import { Datasource } from "../module";
import { zabbixTemplateFormat } from "../datasource";
import { dateMath } from '@grafana/data';
jest.mock('@grafana/runtime', () => ({
getBackendSrv: () => ({
datasourceRequest: jest.fn().mockResolvedValue({data: {result: ''}}),
}),
}), {virtual: true});
describe('ZabbixDatasource', () => {
let ctx = {};
@@ -21,11 +27,11 @@ describe('ZabbixDatasource', () => {
};
ctx.templateSrv = mocks.templateSrvMock;
ctx.backendSrv = mocks.backendSrvMock;
// ctx.backendSrv = mocks.backendSrvMock;
ctx.datasourceSrv = mocks.datasourceSrvMock;
ctx.zabbixAlertingSrv = mocks.zabbixAlertingSrvMock;
ctx.ds = new Datasource(ctx.instanceSettings, ctx.templateSrv, ctx.backendSrv, ctx.datasourceSrv, ctx.zabbixAlertingSrv);
ctx.ds = new Datasource(ctx.instanceSettings, ctx.templateSrv, ctx.zabbixAlertingSrv);
});
describe('When querying data', () => {
@@ -119,7 +125,7 @@ describe('ZabbixDatasource', () => {
item: {filter: "System information"},
textFilter: "",
useCaptureGroups: true,
mode: 2,
queryType: 2,
resultFormat: "table",
options: {
skipEmptyValues: false

View File

@@ -1,11 +1,17 @@
import mocks from '../../test-setup/mocks';
import { DBConnector } from '../zabbix/connectors/dbConnector';
const loadDatasourceMock = jest.fn().mockResolvedValue({ id: 42, name: 'foo', meta: {} });
const getAllMock = jest.fn().mockReturnValue([{ id: 42, name: 'foo', meta: {} }]);
jest.mock('@grafana/runtime', () => ({
getDataSourceSrv: () => ({
loadDatasource: loadDatasourceMock,
getAll: getAllMock
}),
}));
describe('DBConnector', () => {
let ctx = {};
const datasourceSrv = mocks.datasourceSrvMock;
datasourceSrv.loadDatasource.mockResolvedValue({ id: 42, name: 'foo', meta: {} });
datasourceSrv.getAll.mockReturnValue([{ id: 42, name: 'foo' }]);
const ctx: any = {};
describe('When init DB connector', () => {
beforeEach(() => {
@@ -13,34 +19,34 @@ describe('DBConnector', () => {
datasourceId: 42,
datasourceName: undefined
};
loadDatasourceMock.mockClear();
getAllMock.mockClear();
});
it('should try to load datasource by name first', () => {
ctx.options = {
datasourceName: 'bar'
};
const dbConnector = new DBConnector(ctx.options, datasourceSrv);
const dbConnector = new DBConnector({ datasourceName: 'bar' });
dbConnector.loadDBDataSource();
expect(datasourceSrv.getAll).not.toHaveBeenCalled();
expect(datasourceSrv.loadDatasource).toHaveBeenCalledWith('bar');
expect(getAllMock).not.toHaveBeenCalled();
expect(loadDatasourceMock).toHaveBeenCalledWith('bar');
});
it('should load datasource by id if name not present', () => {
const dbConnector = new DBConnector(ctx.options, datasourceSrv);
const dbConnector = new DBConnector({ datasourceId: 42 });
dbConnector.loadDBDataSource();
expect(datasourceSrv.getAll).toHaveBeenCalled();
expect(datasourceSrv.loadDatasource).toHaveBeenCalledWith('foo');
expect(getAllMock).toHaveBeenCalled();
expect(loadDatasourceMock).toHaveBeenCalledWith('foo');
});
it('should throw error if no name and id specified', () => {
ctx.options = {};
const dbConnector = new DBConnector(ctx.options, datasourceSrv);
const dbConnector = new DBConnector(ctx.options);
return expect(dbConnector.loadDBDataSource()).rejects.toBe('Data Source name should be specified');
});
it('should throw error if datasource with given id is not found', () => {
ctx.options.datasourceId = 45;
const dbConnector = new DBConnector(ctx.options, datasourceSrv);
const dbConnector = new DBConnector(ctx.options);
return expect(dbConnector.loadDBDataSource()).rejects.toBe('Data Source with ID 45 not found');
});
});

View File

@@ -1,17 +1,20 @@
import { InfluxDBConnector } from '../zabbix/connectors/influxdb/influxdbConnector';
import { compactQuery } from '../utils';
jest.mock('@grafana/runtime', () => ({
getDataSourceSrv: jest.fn(() => ({
loadDatasource: jest.fn().mockResolvedValue(
{ id: 42, name: 'InfluxDB DS', meta: {} }
),
})),
}));
describe('InfluxDBConnector', () => {
let ctx = {};
beforeEach(() => {
ctx.options = { datasourceName: 'InfluxDB DS', retentionPolicy: 'longterm' };
ctx.datasourceSrvMock = {
loadDatasource: jest.fn().mockResolvedValue(
{ id: 42, name: 'InfluxDB DS', meta: {} }
),
};
ctx.influxDBConnector = new InfluxDBConnector(ctx.options, ctx.datasourceSrvMock);
ctx.influxDBConnector = new InfluxDBConnector(ctx.options);
ctx.influxDBConnector.invokeInfluxDBQuery = jest.fn().mockResolvedValue([]);
ctx.defaultQueryParams = {
itemids: ['123', '234'],

View File

@@ -20,35 +20,30 @@ const POINT_TIMESTAMP = 1;
* Downsample time series by using given function (avg, min, max).
*/
function downsample(datapoints, time_to, ms_interval, func) {
var downsampledSeries = [];
var timeWindow = {
const downsampledSeries = [];
const timeWindow = {
from: time_to * 1000 - ms_interval,
to: time_to * 1000
};
var points_sum = 0;
var points_num = 0;
var value_avg = 0;
var frame = [];
let points_sum = 0;
let points_num = 0;
let value_avg = 0;
let frame = [];
for (var i = datapoints.length - 1; i >= 0; i -= 1) {
for (let i = datapoints.length - 1; i >= 0; i -= 1) {
if (timeWindow.from < datapoints[i][1] && datapoints[i][1] <= timeWindow.to) {
points_sum += datapoints[i][0];
points_num++;
frame.push(datapoints[i][0]);
}
else {
} else {
value_avg = points_num ? points_sum / points_num : 0;
if (func === "max") {
downsampledSeries.push([_.max(frame), timeWindow.to]);
}
else if (func === "min") {
} else if (func === "min") {
downsampledSeries.push([_.min(frame), timeWindow.to]);
}
// avg by default
else {
} else {
downsampledSeries.push([value_avg, timeWindow.to]);
}
@@ -72,25 +67,25 @@ function downsample(datapoints, time_to, ms_interval, func) {
* datapoints: [[<value>, <unixtime>], ...]
*/
function groupBy(datapoints, interval, groupByCallback) {
var ms_interval = utils.parseInterval(interval);
const ms_interval = utils.parseInterval(interval);
// Calculate frame timestamps
var frames = _.groupBy(datapoints, function (point) {
const frames = _.groupBy(datapoints, point => {
// Calculate time for group of points
return Math.floor(point[1] / ms_interval) * ms_interval;
});
// frame: { '<unixtime>': [[<value>, <unixtime>], ...] }
// return [{ '<unixtime>': <value> }, { '<unixtime>': <value> }, ...]
var grouped = _.mapValues(frames, function (frame) {
var points = _.map(frame, function (point) {
const grouped = _.mapValues(frames, frame => {
const points = _.map(frame, point => {
return point[0];
});
return groupByCallback(points);
});
// Convert points to Grafana format
return sortByTime(_.map(grouped, function (value, timestamp) {
return sortByTime(_.map(grouped, (value, timestamp) => {
return [Number(value), Number(timestamp)];
}));
}
@@ -104,15 +99,15 @@ export function groupBy_perf(datapoints, interval, groupByCallback) {
return groupByRange(datapoints, groupByCallback);
}
let ms_interval = utils.parseInterval(interval);
let grouped_series = [];
const ms_interval = utils.parseInterval(interval);
const grouped_series = [];
let frame_values = [];
let frame_value;
let frame_ts = datapoints.length ? getPointTimeFrame(datapoints[0][POINT_TIMESTAMP], ms_interval) : 0;
let point_frame_ts = frame_ts;
let point;
for (let i=0; i < datapoints.length; i++) {
for (let i = 0; i < datapoints.length; i++) {
point = datapoints[i];
point_frame_ts = getPointTimeFrame(point[POINT_TIMESTAMP], ms_interval);
if (point_frame_ts === frame_ts) {
@@ -142,7 +137,7 @@ export function groupByRange(datapoints, groupByCallback) {
const frame_start = datapoints[0][POINT_TIMESTAMP];
const frame_end = datapoints[datapoints.length - 1][POINT_TIMESTAMP];
let point;
for (let i=0; i < datapoints.length; i++) {
for (let i = 0; i < datapoints.length; i++) {
point = datapoints[i];
frame_values.push(point[POINT_VALUE]);
}
@@ -157,30 +152,30 @@ export function groupByRange(datapoints, groupByCallback) {
function sumSeries(timeseries) {
// Calculate new points for interpolation
var new_timestamps = _.uniq(_.map(_.flatten(timeseries, true), function (point) {
let new_timestamps = _.uniq(_.map(_.flatten(timeseries), point => {
return point[1];
}));
new_timestamps = _.sortBy(new_timestamps);
var interpolated_timeseries = _.map(timeseries, function (series) {
const interpolated_timeseries = _.map(timeseries, series => {
series = fillZeroes(series, new_timestamps);
var timestamps = _.map(series, function (point) {
const timestamps = _.map(series, point => {
return point[1];
});
var new_points = _.map(_.difference(new_timestamps, timestamps), function (timestamp) {
const new_points = _.map(_.difference(new_timestamps, timestamps), timestamp => {
return [null, timestamp];
});
var new_series = series.concat(new_points);
const new_series = series.concat(new_points);
return sortByTime(new_series);
});
_.each(interpolated_timeseries, interpolateSeries);
var new_timeseries = [];
var sum;
for (var i = new_timestamps.length - 1; i >= 0; i--) {
const new_timeseries = [];
let sum;
for (let i = new_timestamps.length - 1; i >= 0; i--) {
sum = 0;
for (var j = interpolated_timeseries.length - 1; j >= 0; j--) {
for (let j = interpolated_timeseries.length - 1; j >= 0; j--) {
sum += interpolated_timeseries[j][i][0];
}
new_timeseries.push([sum, new_timestamps[i]]);
@@ -225,9 +220,9 @@ function offset(datapoints, delta) {
* @param {*} datapoints
*/
function delta(datapoints) {
let newSeries = [];
const newSeries = [];
let deltaValue;
for (var i = 1; i < datapoints.length; i++) {
for (let i = 1; i < datapoints.length; i++) {
deltaValue = datapoints[i][0] - datapoints[i - 1][0];
newSeries.push([deltaValue, datapoints[i][1]]);
}
@@ -239,7 +234,7 @@ function delta(datapoints) {
* @param {*} datapoints
*/
function rate(datapoints) {
let newSeries = [];
const newSeries = [];
let point, point_prev;
let valueDelta = 0;
let timeDelta = 0;
@@ -261,7 +256,7 @@ function rate(datapoints) {
}
function simpleMovingAverage(datapoints, n) {
let sma = [];
const sma = [];
let w_sum;
let w_avg = null;
let w_count = 0;
@@ -352,7 +347,7 @@ function expMovingAverage(datapoints, n) {
}
function PERCENTILE(n, values) {
var sorted = _.sortBy(values);
const sorted = _.sortBy(values);
return sorted[Math.floor(sorted.length * n / 100)];
}
@@ -361,7 +356,7 @@ function COUNT(values) {
}
function SUM(values) {
var sum = null;
let sum = null;
for (let i = 0; i < values.length; i++) {
if (values[i] !== null) {
sum += values[i];
@@ -371,7 +366,7 @@ function SUM(values) {
}
function AVERAGE(values) {
let values_non_null = getNonNullValues(values);
const values_non_null = getNonNullValues(values);
if (values_non_null.length === 0) {
return null;
}
@@ -379,7 +374,7 @@ function AVERAGE(values) {
}
function getNonNullValues(values) {
let values_non_null = [];
const values_non_null = [];
for (let i = 0; i < values.length; i++) {
if (values[i] !== null) {
values_non_null.push(values[i]);
@@ -397,7 +392,7 @@ function MAX(values) {
}
function MEDIAN(values) {
var sorted = _.sortBy(values);
const sorted = _.sortBy(values);
return sorted[Math.floor(sorted.length / 2)];
}
@@ -418,7 +413,7 @@ function getPointTimeFrame(timestamp, ms_interval) {
}
function sortByTime(series) {
return _.sortBy(series, function (point) {
return _.sortBy(series, point => {
return point[1];
});
}
@@ -432,8 +427,8 @@ function sortByTime(series) {
* @param {*} timestamps
*/
function fillZeroes(series, timestamps) {
let prepend = [];
let append = [];
const prepend = [];
const append = [];
let new_point;
for (let i = 0; i < timestamps.length; i++) {
if (timestamps[i] < series[0][POINT_TIMESTAMP]) {
@@ -451,10 +446,10 @@ function fillZeroes(series, timestamps) {
* Interpolate series with gaps
*/
function interpolateSeries(series) {
var left, right;
let left, right;
// Interpolate series
for (var i = series.length - 1; i >= 0; i--) {
for (let i = series.length - 1; i >= 0; i--) {
if (!series[i][0]) {
left = findNearestLeft(series, i);
right = findNearestRight(series, i);
@@ -479,7 +474,7 @@ function linearInterpolation(timestamp, left, right) {
}
function findNearestRight(series, pointIndex) {
for (var i = pointIndex; i < series.length; i++) {
for (let i = pointIndex; i < series.length; i++) {
if (series[i][0] !== null) {
return series[i];
}
@@ -488,7 +483,7 @@ function findNearestRight(series, pointIndex) {
}
function findNearestLeft(series, pointIndex) {
for (var i = pointIndex; i > 0; i--) {
for (let i = pointIndex; i > 0; i--) {
if (series[i][0] !== null) {
return series[i];
}

View File

@@ -111,4 +111,183 @@ export enum VariableQueryTypes {
Host = 'host',
Application = 'application',
Item = 'item',
ItemValues = 'itemValues',
}
export enum ShowProblemTypes {
Problems = 'problems',
Recent = 'recent',
History = 'history',
}
export interface ProblemDTO {
triggerid?: string;
eventid?: string;
timestamp: number;
/** Name of the trigger. */
name?: string;
/** Same as a name. */
description?: string;
/** Whether the trigger is in OK or problem state. */
value?: string;
datasource?: string;
comments?: string;
host?: string;
hostTechName?: string;
proxy?: string;
severity?: string;
acknowledged?: '1' | '0';
acknowledges?: ZBXAcknowledge[];
groups?: ZBXGroup[];
hosts?: ZBXHost[];
items?: ZBXItem[];
alerts?: ZBXAlert[];
tags?: ZBXTag[];
url?: string;
expression?: string;
correlation_mode?: string;
correlation_tag?: string;
suppressed?: string;
suppression_data?: any[];
state?: string;
maintenance?: boolean;
manual_close?: string;
error?: string;
showAckButton?: boolean;
}
export interface ZBXProblem {
acknowledged?: '1' | '0';
acknowledges?: ZBXAcknowledge[];
clock: string;
ns: string;
correlationid?: string;
datasource?: string;
name?: string;
eventid?: string;
maintenance?: boolean;
object?: string;
objectid?: string;
opdata?: any;
r_eventid?: string;
r_clock?: string;
r_ns?: string;
severity?: string;
showAckButton?: boolean;
source?: string;
suppressed?: string;
suppression_data?: any[];
tags?: ZBXTag[];
userid?: string;
}
export interface ZBXTrigger {
acknowledges?: ZBXAcknowledge[];
showAckButton?: boolean;
alerts?: ZBXAlert[];
age?: string;
color?: string;
comments?: string;
correlation_mode?: string;
correlation_tag?: string;
datasource?: string;
description?: string;
error?: string;
expression?: string;
flags?: string;
groups?: ZBXGroup[];
host?: string;
hostTechName?: string;
hosts?: ZBXHost[];
items?: ZBXItem[];
lastEvent?: ZBXEvent;
lastchange?: string;
lastchangeUnix?: number;
maintenance?: boolean;
manual_close?: string;
priority?: string;
proxy?: string;
recovery_expression?: string;
recovery_mode?: string;
severity?: string;
state?: string;
status?: string;
tags?: ZBXTag[];
templateid?: string;
triggerid?: string;
/** Whether the trigger can generate multiple problem events. */
type?: string;
url?: string;
value?: string;
}
export interface ZBXGroup {
groupid: string;
name: string;
}
export interface ZBXHost {
hostid: string;
name: string;
host: string;
maintenance_status?: string;
proxy_hostid?: string;
}
export interface ZBXItem {
itemid: string;
name: string;
key_: string;
lastvalue?: string;
}
export interface ZBXEvent {
eventid: string;
clock: string;
ns?: string;
value?: string;
name?: string;
source?: string;
object?: string;
objectid?: string;
severity?: string;
hosts?: ZBXHost[];
acknowledged?: '1' | '0';
acknowledges?: ZBXAcknowledge[];
tags?: ZBXTag[];
suppressed?: string;
}
export interface ZBXTag {
tag: string;
value?: string;
}
export interface ZBXAcknowledge {
acknowledgeid: string;
eventid: string;
userid: string;
action: string;
clock: string;
time: string;
message?: string;
user: string;
alias: string;
name: string;
surname: string;
}
export interface ZBXAlert {
eventid: string;
clock: string;
message: string;
error: string;
}

View File

@@ -19,7 +19,7 @@ export const variableRegex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?:\
* @param {string} key item key, ie system.cpu.util[,system,avg1]
* @return {string} expanded name, ie "CPU system time"
*/
export function expandItemName(name, key) {
export function expandItemName(name: string, key: string): string {
// extract params from key:
// "system.cpu.util[,system,avg1]" --> ["", "system", "avg1"]
@@ -78,13 +78,26 @@ export function containsMacro(itemName) {
return MACRO_PATTERN.test(itemName);
}
export function replaceMacro(item, macros) {
let itemName = item.name;
export function replaceMacro(item, macros, isTriggerItem?) {
let itemName = isTriggerItem ? item.url : item.name;
const item_macros = itemName.match(MACRO_PATTERN);
_.forEach(item_macros, macro => {
const host_macros = _.filter(macros, m => {
if (m.hostid) {
return m.hostid === item.hostid;
if (isTriggerItem) {
// Trigger item can have multiple hosts
// Check all trigger host ids against macro host id
let hostIdFound = false;
_.forEach(item.hosts, h => {
if (h.hostid === m.hostid) {
hostIdFound = true;
}
});
return hostIdFound;
} else {
// Check app host id against macro host id
return m.hostid === item.hostid;
}
} else {
// Add global macros
return true;
@@ -222,10 +235,11 @@ export function escapeRegex(value) {
return value.replace(/[\\^$*+?.()|[\]{}\/]/g, '\\$&');
}
export function parseInterval(interval) {
export function parseInterval(interval: string): number {
const intervalPattern = /(^[\d]+)(y|M|w|d|h|m|s)/g;
const momentInterval: any[] = intervalPattern.exec(interval);
return moment.duration(Number(momentInterval[1]), momentInterval[2]).valueOf();
const duration = moment.duration(Number(momentInterval[1]), momentInterval[2]);
return (duration.valueOf() as number);
}
export function parseTimeShiftInterval(interval) {
@@ -314,7 +328,7 @@ export function isValidVersion(version) {
return versionPattern.exec(version);
}
export function parseVersion(version) {
export function parseVersion(version: string) {
const match = versionPattern.exec(version);
if (!match) {
return null;
@@ -344,7 +358,29 @@ export function getArrayDepth(a, level = 0) {
return level + 1;
}
// Fix for backward compatibility with lodash 2.4
if (!_.includes) {
_.includes = (_ as any).contains;
/**
* Checks whether its argument represents a numeric value.
*/
export function isNumeric(n: any): boolean {
return !isNaN(parseFloat(n)) && isFinite(n);
}
/**
* Parses tags string into array of {tag: value} objects
*/
export function parseTags(tagStr: string): any[] {
if (!tagStr) {
return [];
}
let tags: any[] = _.map(tagStr.split(','), (tag) => tag.trim());
tags = _.map(tags, (tag) => {
const tagParts = tag.split(':');
return {tag: tagParts[0].trim(), value: tagParts[1].trim()};
});
return tags;
}
export function mustArray(result: any): any[] {
return result || [];
}

View File

@@ -1,4 +1,5 @@
import _ from 'lodash';
import { getDataSourceSrv } from '@grafana/runtime';
export const DEFAULT_QUERY_LIMIT = 10000;
export const HISTORY_TO_TABLE_MAP = {
@@ -34,31 +35,30 @@ export const consolidateByTrendColumns = {
* `testDataSource()` methods, which describe how to fetch data from source other than Zabbix API.
*/
export class DBConnector {
constructor(options, datasourceSrv) {
this.datasourceSrv = datasourceSrv;
constructor(options) {
this.datasourceId = options.datasourceId;
this.datasourceName = options.datasourceName;
this.datasourceTypeId = null;
this.datasourceTypeName = null;
}
static loadDatasource(dsId, dsName, datasourceSrv) {
static loadDatasource(dsId, dsName) {
if (!dsName && dsId !== undefined) {
let ds = _.find(datasourceSrv.getAll(), {'id': dsId});
let ds = _.find(getDataSourceSrv().getAll(), {'id': dsId});
if (!ds) {
return Promise.reject(`Data Source with ID ${dsId} not found`);
}
dsName = ds.name;
}
if (dsName) {
return datasourceSrv.loadDatasource(dsName);
return getDataSourceSrv().loadDatasource(dsName);
} else {
return Promise.reject(`Data Source name should be specified`);
}
}
loadDBDataSource() {
return DBConnector.loadDatasource(this.datasourceId, this.datasourceName, this.datasourceSrv)
return DBConnector.loadDatasource(this.datasourceId, this.datasourceName)
.then(ds => {
this.datasourceTypeId = ds.meta.id;
this.datasourceTypeName = ds.meta.name;
@@ -123,22 +123,36 @@ export class ZabbixNotImplemented {
*/
function convertGrafanaTSResponse(time_series, items, addHostName) {
//uniqBy is needed to deduplicate
var hosts = _.uniqBy(_.flatten(_.map(items, 'hosts')), 'hostid');
const hosts = _.uniqBy(_.flatten(_.map(items, 'hosts')), 'hostid');
let grafanaSeries = _.map(_.compact(time_series), series => {
let itemid = series.name;
var item = _.find(items, {'itemid': itemid});
var alias = item.name;
//only when actual multi hosts selected
if (_.keys(hosts).length > 1 && addHostName) {
var host = _.find(hosts, {'hostid': item.hostid});
alias = host.name + ": " + alias;
const itemid = series.name;
const item = _.find(items, {'itemid': itemid});
let alias = item.name;
// Add scopedVars for using in alias functions
const scopedVars = {
'__zbx_item': { value: item.name },
'__zbx_item_name': { value: item.name },
'__zbx_item_key': { value: item.key_ },
};
if (_.keys(hosts).length > 0) {
const host = _.find(hosts, {'hostid': item.hostid});
scopedVars['__zbx_host'] = { value: host.host };
scopedVars['__zbx_host_name'] = { value: host.name };
// Only add host when multiple hosts selected
if (_.keys(hosts).length > 1 && addHostName) {
alias = host.name + ": " + alias;
}
}
// CachingProxy deduplicates requests and returns one time series for equal queries.
// Clone is needed to prevent changing of series object shared between all targets.
let datapoints = _.cloneDeep(series.points);
const datapoints = _.cloneDeep(series.points);
return {
target: alias,
datapoints: datapoints
datapoints,
scopedVars,
};
});

View File

@@ -11,8 +11,8 @@ const consolidateByFunc = {
};
export class InfluxDBConnector extends DBConnector {
constructor(options, datasourceSrv) {
super(options, datasourceSrv);
constructor(options) {
super(options);
this.retentionPolicy = options.retentionPolicy;
super.loadDBDataSource().then(ds => {
this.influxDS = ds;
@@ -24,7 +24,14 @@ export class InfluxDBConnector extends DBConnector {
* Try to invoke test query for one of Zabbix database tables.
*/
testDataSource() {
return this.influxDS.testDatasource();
return this.influxDS.testDatasource().then(result => {
if (result.status && result.status === 'error') {
return Promise.reject({ data: {
message: `InfluxDB connection error: ${result.message}`
}});
}
return result;
});
}
getHistory(items, timeFrom, timeTill, options) {

View File

@@ -3,26 +3,24 @@
*/
function historyQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction) {
let time_expression = `clock DIV ${intervalSec} * ${intervalSec}`;
let query = `
SELECT CAST(itemid AS CHAR) AS metric, ${time_expression} AS time_sec, ${aggFunction}(value) AS value
SELECT CAST(itemid AS CHAR) AS metric, MIN(clock) AS time_sec, ${aggFunction}(value) AS value
FROM ${table}
WHERE itemid IN (${itemids})
AND clock > ${timeFrom} AND clock < ${timeTill}
GROUP BY ${time_expression}, metric
GROUP BY (clock-${timeFrom}) DIV ${intervalSec}, metric
ORDER BY time_sec ASC
`;
return query;
}
function trendsQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction, valueColumn) {
let time_expression = `clock DIV ${intervalSec} * ${intervalSec}`;
let query = `
SELECT CAST(itemid AS CHAR) AS metric, ${time_expression} AS time_sec, ${aggFunction}(${valueColumn}) AS value
SELECT CAST(itemid AS CHAR) AS metric, MIN(clock) AS time_sec, ${aggFunction}(${valueColumn}) AS value
FROM ${table}
WHERE itemid IN (${itemids})
AND clock > ${timeFrom} AND clock < ${timeTill}
GROUP BY ${time_expression}, metric
GROUP BY (clock-${timeFrom}) DIV ${intervalSec}, metric
ORDER BY time_sec ASC
`;
return query;

View File

@@ -1,4 +1,5 @@
import _ from 'lodash';
import { getBackendSrv } from '@grafana/runtime';
import { compactQuery } from '../../../utils';
import mysql from './mysql';
import postgres from './postgres';
@@ -10,15 +11,14 @@ const supportedDatabases = {
};
export class SQLConnector extends DBConnector {
constructor(options, datasourceSrv) {
super(options, datasourceSrv);
constructor(options) {
super(options);
this.limit = options.limit || DEFAULT_QUERY_LIMIT;
this.sqlDialect = null;
super.loadDBDataSource()
.then(ds => {
this.backendSrv = ds.backendSrv;
.then(() => {
this.loadSQLDialect();
});
}
@@ -43,6 +43,12 @@ export class SQLConnector extends DBConnector {
let {intervalMs, consolidateBy} = options;
let intervalSec = Math.ceil(intervalMs / 1000);
// The interval must match the time range exactly n times, otherwise
// the resulting first and last data points will yield invalid values in the
// calculated average value in downsampleSeries - when using consolidateBy(avg)
let numOfIntervals = Math.ceil((timeTill - timeFrom) / intervalSec);
intervalSec = (timeTill - timeFrom) / numOfIntervals;
consolidateBy = consolidateBy || 'avg';
let aggFunction = dbConnector.consolidateByFunc[consolidateBy];
@@ -66,6 +72,12 @@ export class SQLConnector extends DBConnector {
let { intervalMs, consolidateBy } = options;
let intervalSec = Math.ceil(intervalMs / 1000);
// The interval must match the time range exactly n times, otherwise
// the resulting first and last data points will yield invalid values in the
// calculated average value in downsampleSeries - when using consolidateBy(avg)
let numOfIntervals = Math.ceil((timeTill - timeFrom) / intervalSec);
intervalSec = (timeTill - timeFrom) / numOfIntervals;
consolidateBy = consolidateBy || 'avg';
let aggFunction = dbConnector.consolidateByFunc[consolidateBy];
@@ -96,7 +108,7 @@ export class SQLConnector extends DBConnector {
maxDataPoints: this.limit
};
return this.backendSrv.datasourceRequest({
return getBackendSrv().datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
data: {

View File

@@ -0,0 +1,42 @@
export interface JSONRPCRequest {
jsonrpc: '2.0' | string;
method: string;
id: number;
auth?: string | null;
params?: JSONRPCRequestParams;
}
export interface JSONRPCResponse<T> {
jsonrpc: '2.0' | string;
id: number;
result?: T;
error?: JSONRPCError;
}
export interface JSONRPCError {
code?: number;
message?: string;
data?: string;
}
export interface GFHTTPRequest {
method: HTTPMethod;
url: string;
data?: any;
headers?: {[key: string]: string};
withCredentials?: boolean;
}
export type JSONRPCRequestParams = {[key: string]: any};
export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'CONNECT' | 'OPTIONS' | 'TRACE';
export type GFRequestOptions = {[key: string]: any};
export interface ZabbixRequestResponse {
data?: JSONRPCResponse<any>;
}
export type ZabbixAPIResponse<T> = T;
export type APILoginResponse = string;

View File

@@ -1,8 +1,14 @@
import _ from 'lodash';
import semver from 'semver';
import kbn from 'grafana/app/core/utils/kbn';
import * as utils from '../../../utils';
import { ZabbixAPICore } from './zabbixAPICore';
import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_ADD_MESSAGE, MIN_SLA_INTERVAL } from '../../../constants';
import { ShowProblemTypes, ZBXProblem } from '../../../types';
import { JSONRPCRequestParams } from './types';
import { getBackendSrv } from '@grafana/runtime';
const DEFAULT_ZABBIX_VERSION = '3.0.0';
/**
* Zabbix API Wrapper.
@@ -10,12 +16,25 @@ import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_ADD_MESSAGE, MI
* Wraps API calls and provides high-level methods.
*/
export class ZabbixAPIConnector {
constructor(api_url, username, password, version, basicAuth, withCredentials, backendSrv, datasourceId) {
url: string;
username: string;
password: string;
auth: string;
requestOptions: { basicAuth: any; withCredentials: boolean; };
loginPromise: Promise<string>;
loginErrorCount: number;
maxLoginAttempts: number;
zabbixAPICore: ZabbixAPICore;
getTrend: (items: any, timeFrom: any, timeTill: any) => Promise<any[]>;
version: string;
getVersionPromise: Promise<string>;
datasourceId: number;
constructor(api_url: string, username: string, password: string, basicAuth: any, withCredentials: boolean, datasourceId: number) {
this.url = api_url;
this.username = username;
this.password = password;
this.auth = '';
this.version = version;
this.requestOptions = {
basicAuth: basicAuth,
@@ -23,16 +42,17 @@ export class ZabbixAPIConnector {
};
this.datasourceId = datasourceId;
this.backendSrv = backendSrv;
this.loginPromise = null;
this.loginErrorCount = 0;
this.maxLoginAttempts = 3;
this.zabbixAPICore = new ZabbixAPICore(backendSrv);
this.zabbixAPICore = new ZabbixAPICore();
this.getTrend = this.getTrend_ZBXNEXT1193;
//getTrend = getTrend_30;
this.initVersion();
}
//////////////////////////
@@ -59,13 +79,40 @@ export class ZabbixAPIConnector {
}],
};
return this.backendSrv.datasourceRequest({
return getBackendSrv().datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
data: tsdbRequestData
});
}
_request(method: string, params: JSONRPCRequestParams): Promise<any> {
if (!this.version) {
return this.initVersion().then(() => this.request(method, params));
}
return this.zabbixAPICore.request(this.url, method, params, this.requestOptions, this.auth)
.catch(error => {
if (isNotInitialized(error.data)) {
// If API not initialized yet (auth is empty), login first
return this.loginOnce()
.then(() => this.request(method, params));
} else if (isNotAuthorized(error.data)) {
// Handle auth errors
this.loginErrorCount++;
if (this.loginErrorCount > this.maxLoginAttempts) {
this.loginErrorCount = 0;
return Promise.resolve();
} else {
return this.loginOnce()
.then(() => this.request(method, params));
}
} else {
return Promise.reject(error);
}
});
}
handleTsdbResponse(response) {
if (!response || !response.data || !response.data.results) {
return [];
@@ -78,9 +125,8 @@ export class ZabbixAPIConnector {
* When API unauthenticated or auth token expired each request produce login()
* call. But auth token is common to all requests. This function wraps login() method
* and call it once. If login() already called just wait for it (return its promise).
* @return login promise
*/
loginOnce() {
loginOnce(): Promise<string> {
if (!this.loginPromise) {
this.loginPromise = Promise.resolve(
this.login().then(auth => {
@@ -96,7 +142,7 @@ export class ZabbixAPIConnector {
/**
* Get authentication token.
*/
login() {
login(): Promise<string> {
return this.zabbixAPICore.login(this.url, this.username, this.password, this.requestOptions);
}
@@ -107,23 +153,49 @@ export class ZabbixAPIConnector {
return this.zabbixAPICore.getVersion(this.url, this.requestOptions);
}
initVersion(): Promise<string> {
if (!this.getVersionPromise) {
this.getVersionPromise = Promise.resolve(
this.getVersion().then(version => {
if (version) {
console.log(`Zabbix version detected: ${version}`);
} else {
console.log(`Failed to detect Zabbix version, use default ${DEFAULT_ZABBIX_VERSION}`);
}
this.version = version || DEFAULT_ZABBIX_VERSION;
this.getVersionPromise = null;
return version;
})
);
}
return this.getVersionPromise;
}
////////////////////////////////
// Zabbix API method wrappers //
////////////////////////////////
acknowledgeEvent(eventid, message) {
const action = this.version >= 4 ? ZBX_ACK_ACTION_ACK + ZBX_ACK_ACTION_ADD_MESSAGE : ZBX_ACK_ACTION_NONE;
const params = {
acknowledgeEvent(eventid: string, message: string, action?: number, severity?: number) {
if (!action) {
action = semver.gte(this.version, '4.0.0') ? ZBX_ACK_ACTION_ADD_MESSAGE : ZBX_ACK_ACTION_NONE;
}
const params: any = {
eventids: eventid,
message: message,
action: action
};
if (severity) {
params.severity = severity;
}
return this.request('event.acknowledge', params);
}
getGroups() {
var params = {
const params = {
output: ['name'],
sortfield: 'name',
real_hosts: true
@@ -133,7 +205,7 @@ export class ZabbixAPIConnector {
}
getHosts(groupids) {
var params = {
const params: any = {
output: ['name', 'host'],
sortfield: 'name'
};
@@ -144,8 +216,8 @@ export class ZabbixAPIConnector {
return this.request('host.get', params);
}
getApps(hostids) {
var params = {
getApps(hostids): Promise<any[]> {
const params = {
output: 'extend',
hostids: hostids
};
@@ -161,7 +233,7 @@ export class ZabbixAPIConnector {
* @return {[type]} array of items
*/
getItems(hostids, appids, itemtype) {
var params = {
const params: any = {
output: [
'name', 'key_',
'value_type',
@@ -172,7 +244,7 @@ export class ZabbixAPIConnector {
sortfield: 'name',
webitems: true,
filter: {},
selectHosts: ['hostid', 'name']
selectHosts: ['hostid', 'name', 'host']
};
if (hostids) {
params.hostids = hostids;
@@ -194,7 +266,7 @@ export class ZabbixAPIConnector {
}
getItemsByIDs(itemids) {
var params = {
const params = {
itemids: itemids,
output: [
'name', 'key_',
@@ -208,11 +280,11 @@ export class ZabbixAPIConnector {
};
return this.request('item.get', params)
.then(utils.expandItems);
.then(items => utils.expandItems(items));
}
getMacros(hostids) {
var params = {
const params = {
output: 'extend',
hostids: hostids
};
@@ -221,7 +293,7 @@ export class ZabbixAPIConnector {
}
getGlobalMacros() {
var params = {
const params = {
output: 'extend',
globalmacro: true
};
@@ -230,7 +302,7 @@ export class ZabbixAPIConnector {
}
getLastValue(itemid) {
var params = {
const params = {
output: ['lastvalue'],
itemids: itemid
};
@@ -249,10 +321,10 @@ export class ZabbixAPIConnector {
getHistory(items, timeFrom, timeTill) {
// Group items by value type and perform request for each value type
let grouped_items = _.groupBy(items, 'value_type');
let promises = _.map(grouped_items, (items, value_type) => {
let itemids = _.map(items, 'itemid');
let params = {
const grouped_items = _.groupBy(items, 'value_type');
const promises = _.map(grouped_items, (items, value_type) => {
const itemids = _.map(items, 'itemid');
const params: any = {
output: 'extend',
history: value_type,
itemids: itemids,
@@ -284,10 +356,10 @@ export class ZabbixAPIConnector {
getTrend_ZBXNEXT1193(items, timeFrom, timeTill) {
// Group items by value type and perform request for each value type
let grouped_items = _.groupBy(items, 'value_type');
let promises = _.map(grouped_items, (items, value_type) => {
let itemids = _.map(items, 'itemid');
let params = {
const grouped_items = _.groupBy(items, 'value_type');
const promises = _.map(grouped_items, (items, value_type) => {
const itemids = _.map(items, 'itemid');
const params: any = {
output: 'extend',
trend: value_type,
itemids: itemids,
@@ -308,10 +380,10 @@ export class ZabbixAPIConnector {
}
getTrend_30(items, time_from, time_till, value_type) {
var self = this;
var itemids = _.map(items, 'itemid');
const self = this;
const itemids = _.map(items, 'itemid');
var params = {
const params: any = {
output: ["itemid",
"clock",
value_type
@@ -328,8 +400,8 @@ export class ZabbixAPIConnector {
return self.request('trend.get', params);
}
getITService(serviceids) {
var params = {
getITService(serviceids?) {
const params = {
output: 'extend',
serviceids: serviceids
};
@@ -337,18 +409,88 @@ export class ZabbixAPIConnector {
}
getSLA(serviceids, timeRange, options) {
const intervals = buildSLAIntervals(timeRange, options.intervalMs);
const params = {
const [timeFrom, timeTo] = timeRange;
let intervals = [{ from: timeFrom, to: timeTo }];
if (options.slaInterval === 'auto') {
const interval = getSLAInterval(options.intervalMs);
intervals = buildSLAIntervals(timeRange, interval);
} else if (options.slaInterval !== 'none') {
const interval = utils.parseInterval(options.slaInterval) / 1000;
intervals = buildSLAIntervals(timeRange, interval);
}
const params: any = {
serviceids,
intervals
};
return this.request('service.getsla', params);
}
getTriggers(groupids, hostids, applicationids, options) {
let {showTriggers, maintenance, timeFrom, timeTo} = options;
getProblems(groupids, hostids, applicationids, options): Promise<ZBXProblem[]> {
const { timeFrom, timeTo, recent, severities, limit, acknowledged } = options;
let params = {
const params: any = {
output: 'extend',
selectAcknowledges: 'extend',
selectSuppressionData: 'extend',
selectTags: 'extend',
source: '0',
object: '0',
sortfield: ['eventid'],
sortorder: 'ASC',
evaltype: '0',
// preservekeys: '1',
groupids,
hostids,
applicationids,
recent,
};
if (severities) {
params.severities = severities;
}
if (acknowledged !== undefined) {
params.acknowledged = acknowledged;
}
if (limit) {
params.limit = limit;
}
if (timeFrom || timeTo) {
params.time_from = timeFrom;
params.time_till = timeTo;
}
return this.request('problem.get', params).then(utils.mustArray);
}
getTriggersByIds(triggerids: string[]) {
const params: any = {
output: 'extend',
triggerids: triggerids,
expandDescription: true,
expandData: true,
expandComment: true,
monitored: true,
skipDependent: true,
selectGroups: ['name'],
selectHosts: ['name', 'host', 'maintenance_status', 'proxy_hostid'],
selectItems: ['name', 'key_', 'lastvalue'],
// selectLastEvent: 'extend',
// selectTags: 'extend',
preservekeys: '1',
};
return this.request('trigger.get', params).then(utils.mustArray);
}
getTriggers(groupids, hostids, applicationids, options) {
const {showTriggers, maintenance, timeFrom, timeTo} = options;
const params: any = {
output: 'extend',
groupids: groupids,
hostids: hostids,
@@ -369,8 +511,10 @@ export class ZabbixAPIConnector {
selectTags: 'extend'
};
if (showTriggers) {
params.filter.value = showTriggers;
if (showTriggers === ShowProblemTypes.Problems) {
params.filter.value = 1;
} else if (showTriggers === ShowProblemTypes.Recent || showTriggers === ShowProblemTypes.History) {
params.filter.value = [0, 1];
}
if (maintenance) {
@@ -386,7 +530,7 @@ export class ZabbixAPIConnector {
}
getEvents(objectids, timeFrom, timeTo, showEvents, limit) {
var params = {
const params: any = {
output: 'extend',
time_from: timeFrom,
time_till: timeTo,
@@ -402,27 +546,47 @@ export class ZabbixAPIConnector {
params.sortorder = 'DESC';
}
return this.request('event.get', params);
return this.request('event.get', params).then(utils.mustArray);
}
getAcknowledges(eventids) {
var params = {
getEventsHistory(groupids, hostids, applicationids, options) {
const { timeFrom, timeTo, severities, limit, value } = options;
const params: any = {
output: 'extend',
eventids: eventids,
preservekeys: true,
time_from: timeFrom,
time_till: timeTo,
value: '1',
source: '0',
object: '0',
evaltype: '0',
sortfield: ['eventid'],
sortorder: 'ASC',
select_acknowledges: 'extend',
sortfield: 'clock',
sortorder: 'DESC'
selectTags: 'extend',
selectSuppressionData: ['maintenanceid', 'suppress_until'],
groupids,
hostids,
applicationids,
};
return this.request('event.get', params)
.then(events => {
return _.filter(events, (event) => event.acknowledges.length);
});
if (limit) {
params.limit = limit;
}
if (severities) {
params.severities = severities;
}
if (value) {
params.value = value;
}
return this.request('event.get', params).then(utils.mustArray);
}
getExtendedEventData(eventids) {
var params = {
const params = {
output: 'extend',
eventids: eventids,
preservekeys: true,
@@ -450,8 +614,24 @@ export class ZabbixAPIConnector {
return this.request('alert.get', params);
}
getAcknowledges(eventids) {
const params = {
output: 'extend',
eventids: eventids,
preservekeys: true,
select_acknowledges: 'extend',
sortfield: 'clock',
sortorder: 'DESC'
};
return this.request('event.get', params)
.then(events => {
return _.filter(events, (event) => event.acknowledges.length);
});
}
getAlerts(itemids, timeFrom, timeTo) {
var params = {
const params: any = {
output: 'extend',
itemids: itemids,
expandDescription: true,
@@ -475,8 +655,8 @@ export class ZabbixAPIConnector {
}
getHostAlerts(hostids, applicationids, options) {
let {minSeverity, acknowledged, count, timeFrom, timeTo} = options;
let params = {
const {minSeverity, acknowledged, count, timeFrom, timeTo} = options;
const params: any = {
output: 'extend',
hostids: hostids,
min_severity: minSeverity,
@@ -517,7 +697,7 @@ export class ZabbixAPIConnector {
}
getProxies() {
var params = {
const params = {
output: ['proxyid', 'host'],
};
@@ -535,13 +715,17 @@ function filterTriggersByAcknowledge(triggers, acknowledged) {
}
}
// function isNotAuthorized(message) {
// return (
// message === "Session terminated, re-login, please." ||
// message === "Not authorised." ||
// message === "Not authorized."
// );
// }
function isNotAuthorized(message) {
return (
message === "Session terminated, re-login, please." ||
message === "Not authorised." ||
message === "Not authorized."
);
}
function isNotInitialized(message) {
return message === "Not initialized";
}
function getSLAInterval(intervalMs) {
// Too many intervals may cause significant load on the database, so decrease number of resulting points
@@ -550,19 +734,18 @@ function getSLAInterval(intervalMs) {
return Math.max(interval, MIN_SLA_INTERVAL);
}
function buildSLAIntervals(timeRange, intervalMs) {
function buildSLAIntervals(timeRange, interval) {
let [timeFrom, timeTo] = timeRange;
const slaInterval = getSLAInterval(intervalMs);
const intervals = [];
// Align time range with calculated interval
timeFrom = Math.floor(timeFrom / slaInterval) * slaInterval;
timeTo = Math.ceil(timeTo / slaInterval) * slaInterval;
timeFrom = Math.floor(timeFrom / interval) * interval;
timeTo = Math.ceil(timeTo / interval) * interval;
for (let i = timeFrom; i <= timeTo - slaInterval; i += slaInterval) {
for (let i = timeFrom; i <= timeTo - interval; i += interval) {
intervals.push({
from : i,
to : (i + slaInterval)
to : (i + interval)
});
}

View File

@@ -1,20 +1,16 @@
/**
* General Zabbix API methods
*/
import { getBackendSrv } from '@grafana/runtime';
import { JSONRPCRequest, ZabbixRequestResponse, JSONRPCError, APILoginResponse, GFHTTPRequest, GFRequestOptions } from './types';
export class ZabbixAPICore {
/** @ngInject */
constructor(backendSrv) {
this.backendSrv = backendSrv;
}
/**
* Request data from Zabbix API
* @return {object} response.result
*/
request(api_url, method, params, options, auth) {
let requestData = {
request(api_url: string, method: string, params: any, options: GFRequestOptions, auth?: string) {
const requestData: JSONRPCRequest = {
jsonrpc: '2.0',
method: method,
params: params,
@@ -23,13 +19,13 @@ export class ZabbixAPICore {
if (auth === "") {
// Reject immediately if not authenticated
return Promise.reject(new ZabbixAPIError({data: "Not authorised."}));
return Promise.reject(new ZabbixAPIError({data: "Not initialized"}));
} else if (auth) {
// Set auth parameter only if it needed
requestData.auth = auth;
}
let requestOptions = {
const requestOptions: GFHTTPRequest = {
method: 'POST',
url: api_url,
data: requestData,
@@ -50,18 +46,18 @@ export class ZabbixAPICore {
}
datasourceRequest(requestOptions) {
return this.backendSrv.datasourceRequest(requestOptions)
.then((response) => {
if (!response.data) {
return getBackendSrv().datasourceRequest(requestOptions)
.then((response: ZabbixRequestResponse) => {
if (!response?.data) {
return Promise.reject(new ZabbixAPIError({data: "General Error, no data"}));
} else if (response.data.error) {
} else if (response?.data.error) {
// Handle Zabbix API errors
return Promise.reject(new ZabbixAPIError(response.data.error));
}
// Success
return response.data.result;
return response?.data.result;
});
}
@@ -69,8 +65,8 @@ export class ZabbixAPICore {
* Get authentication token.
* @return {string} auth token
*/
login(api_url, username, password, options) {
let params = {
login(api_url: string, username: string, password: string, options: GFRequestOptions): Promise<APILoginResponse> {
const params = {
user: username,
password: password
};
@@ -81,14 +77,22 @@ export class ZabbixAPICore {
* Get Zabbix API version
* Matches the version of Zabbix starting from Zabbix 2.0.4
*/
getVersion(api_url, options) {
return this.request(api_url, 'apiinfo.version', [], options);
getVersion(api_url: string, options: GFRequestOptions): Promise<string> {
return this.request(api_url, 'apiinfo.version', [], options).catch(err => {
console.error(err);
return undefined;
});
}
}
// Define zabbix API exception type
export class ZabbixAPIError {
constructor(error) {
code: number;
name: string;
data: string;
message: string;
constructor(error: JSONRPCError) {
this.code = error.code || null;
this.name = error.message || "";
this.data = error.data || "";

View File

@@ -4,6 +4,10 @@
*/
export class CachingProxy {
cacheEnabled: boolean;
ttl: number;
cache: any;
promises: any;
constructor(cacheOptions) {
this.cacheEnabled = cacheOptions.enabled;
@@ -33,13 +37,13 @@ export class CachingProxy {
}
proxyfyWithCache(func, funcName, funcScope) {
let proxyfied = this.proxyfy(func, funcName, funcScope);
const proxyfied = this.proxyfy(func, funcName, funcScope);
return this.cacheRequest(proxyfied, funcName, funcScope);
}
_isExpired(cacheObject) {
if (cacheObject) {
let object_age = Date.now() - cacheObject.timestamp;
const object_age = Date.now() - cacheObject.timestamp;
return !(cacheObject.timestamp && object_age < this.ttl);
} else {
return true;
@@ -52,8 +56,9 @@ export class CachingProxy {
* with same params when waiting for result.
*/
function callOnce(func, promiseKeeper, funcScope) {
// tslint:disable-next-line: only-arrow-functions
return function() {
var hash = getRequestHash(arguments);
const hash = getRequestHash(arguments);
if (!promiseKeeper[hash]) {
promiseKeeper[hash] = Promise.resolve(
func.apply(funcScope, arguments)
@@ -68,22 +73,25 @@ function callOnce(func, promiseKeeper, funcScope) {
}
function cacheRequest(func, funcName, funcScope, self) {
// tslint:disable-next-line: only-arrow-functions
return function() {
if (!self.cache[funcName]) {
self.cache[funcName] = {};
}
let cacheObject = self.cache[funcName];
let hash = getRequestHash(arguments);
const cacheObject = self.cache[funcName];
const hash = getRequestHash(arguments);
if (self.cacheEnabled && !self._isExpired(cacheObject[hash])) {
return Promise.resolve(cacheObject[hash].value);
} else {
return func.apply(funcScope, arguments)
.then(result => {
cacheObject[hash] = {
value: result,
timestamp: Date.now()
};
if (result !== undefined) {
cacheObject[hash] = {
value: result,
timestamp: Date.now()
};
}
return result;
});
}
@@ -92,17 +100,17 @@ function cacheRequest(func, funcName, funcScope, self) {
function getRequestHash(args) {
const argsJson = JSON.stringify(args);
return argsJson.getHash();
return getHash(argsJson);
}
String.prototype.getHash = function() {
var hash = 0, i, chr, len;
if (this.length !== 0) {
for (i = 0, len = this.length; i < len; i++) {
chr = this.charCodeAt(i);
function getHash(str: string): number {
let hash = 0, i, chr, len;
if (str.length !== 0) {
for (i = 0, len = str.length; i < len; i++) {
chr = str.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
}
return hash;
};
}

View File

@@ -0,0 +1,23 @@
export interface ZabbixConnector {
getHistory: (items, timeFrom, timeTill) => Promise<any>;
getTrend: (items, timeFrom, timeTill) => Promise<any>;
getItemsByIDs: (itemids) => Promise<any>;
getEvents: (objectids, timeFrom, timeTo, showEvents, limit?) => Promise<any>;
getAlerts: (itemids, timeFrom?, timeTo?) => Promise<any>;
getHostAlerts: (hostids, applicationids, options?) => Promise<any>;
getAcknowledges: (eventids) => Promise<any>;
getITService: (serviceids?) => Promise<any>;
acknowledgeEvent: (eventid, message) => Promise<any>;
getProxies: () => Promise<any>;
getEventAlerts: (eventids) => Promise<any>;
getExtendedEventData: (eventids) => Promise<any>;
getMacros: (hostids: any[]) => Promise<any>;
getVersion: () => Promise<string>;
login: () => Promise<any>;
getGroups: (groupFilter?) => any;
getHosts: (groupFilter?, hostFilter?) => any;
getApps: (groupFilter?, hostFilter?, appFilter?) => any;
getItems: (groupFilter?, hostFilter?, appFilter?, itemFilter?, options?) => any;
getSLA: (itservices, timeRange, target, options?) => any;
}

View File

@@ -1,6 +1,11 @@
import mocks from '../../test-setup/mocks';
import { Zabbix } from './zabbix';
jest.mock('@grafana/runtime', () => ({
getBackendSrv: () => ({
datasourceRequest: jest.fn().mockResolvedValue({data: {result: ''}}),
}),
}), {virtual: true});
describe('Zabbix', () => {
let ctx = {};
let zabbix;
@@ -8,14 +13,13 @@ describe('Zabbix', () => {
url: 'http://localhost',
username: 'zabbix',
password: 'zabbix',
zabbixVersion: 4,
};
beforeEach(() => {
ctx.options = options;
ctx.backendSrv = mocks.backendSrvMock;
ctx.datasourceSrv = mocks.datasourceSrvMock;
zabbix = new Zabbix(ctx.options, ctx.backendSrvMock, ctx.datasourceSrvMock);
// ctx.backendSrv = mocks.backendSrvMock;
// ctx.datasourceSrv = mocks.datasourceSrvMock;
zabbix = new Zabbix(ctx.options);
});
describe('When querying proxies', () => {

View File

@@ -1,4 +1,5 @@
import _ from 'lodash';
import moment from 'moment';
import * as utils from '../utils';
import responseHandler from '../responseHandler';
import { CachingProxy } from './proxy/cachingProxy';
@@ -7,11 +8,19 @@ import { DBConnector } from './connectors/dbConnector';
import { ZabbixAPIConnector } from './connectors/zabbix_api/zabbixAPIConnector';
import { SQLConnector } from './connectors/sql/sqlConnector';
import { InfluxDBConnector } from './connectors/influxdb/influxdbConnector';
import { ZabbixConnector } from './types';
import { joinTriggersWithProblems, joinTriggersWithEvents } from '../problemsHandler';
import { ProblemDTO } from '../types';
interface AppsResponse extends Array<any> {
appFilterEmpty?: boolean;
hostids?: any[];
}
const REQUESTS_TO_PROXYFY = [
'getHistory', 'getTrend', 'getGroups', 'getHosts', 'getApps', 'getItems', 'getMacros', 'getItemsByIDs',
'getEvents', 'getAlerts', 'getHostAlerts', 'getAcknowledges', 'getITService', 'getSLA', 'getVersion', 'getProxies',
'getEventAlerts', 'getExtendedEventData'
'getEventAlerts', 'getExtendedEventData', 'getProblems', 'getEventsHistory', 'getTriggersByIds'
];
const REQUESTS_TO_CACHE = [
@@ -24,40 +33,63 @@ const REQUESTS_TO_BIND = [
'getExtendedEventData'
];
export class Zabbix {
constructor(options, datasourceSrv, backendSrv, datasourceId) {
let {
export class Zabbix implements ZabbixConnector {
enableDirectDBConnection: boolean;
cachingProxy: CachingProxy;
zabbixAPI: ZabbixAPIConnector;
getHistoryDB: any;
dbConnector: any;
getTrendsDB: any;
getHistory: (items, timeFrom, timeTill) => Promise<any>;
getTrend: (items, timeFrom, timeTill) => Promise<any>;
getItemsByIDs: (itemids) => Promise<any>;
getEvents: (objectids, timeFrom, timeTo, showEvents, limit?) => Promise<any>;
getAlerts: (itemids, timeFrom?, timeTo?) => Promise<any>;
getHostAlerts: (hostids, applicationids, options?) => Promise<any>;
getAcknowledges: (eventids) => Promise<any>;
getITService: (serviceids?) => Promise<any>;
acknowledgeEvent: (eventid, message) => Promise<any>;
getProxies: () => Promise<any>;
getEventAlerts: (eventids) => Promise<any>;
getExtendedEventData: (eventids) => Promise<any>;
getMacros: (hostids: any[]) => Promise<any>;
getVersion: () => Promise<string>;
login: () => Promise<any>;
constructor(options) {
const {
url,
username,
password,
basicAuth,
withCredentials,
zabbixVersion,
cacheTTL,
enableDirectDBConnection,
dbConnectionDatasourceId,
dbConnectionDatasourceName,
dbConnectionRetentionPolicy,
datasourceId,
} = options;
this.enableDirectDBConnection = enableDirectDBConnection;
// Initialize caching proxy for requests
let cacheOptions = {
const cacheOptions = {
enabled: true,
ttl: cacheTTL
};
this.cachingProxy = new CachingProxy(cacheOptions);
this.zabbixAPI = new ZabbixAPIConnector(url, username, password, zabbixVersion, basicAuth, withCredentials, backendSrv, datasourceId);
this.zabbixAPI = new ZabbixAPIConnector(url, username, password, basicAuth, withCredentials, datasourceId);
this.proxyfyRequests();
this.cacheRequests();
this.bindRequests();
if (enableDirectDBConnection) {
const connectorOptions = { dbConnectionRetentionPolicy };
this.initDBConnector(dbConnectionDatasourceId, dbConnectionDatasourceName, datasourceSrv, connectorOptions)
const connectorOptions: any = { dbConnectionRetentionPolicy };
this.initDBConnector(dbConnectionDatasourceId, dbConnectionDatasourceName, connectorOptions)
.then(() => {
this.getHistoryDB = this.cachingProxy.proxyfyWithCache(this.dbConnector.getHistory, 'getHistory', this.dbConnector);
this.getTrendsDB = this.cachingProxy.proxyfyWithCache(this.dbConnector.getTrends, 'getTrends', this.dbConnector);
@@ -65,34 +97,34 @@ export class Zabbix {
}
}
initDBConnector(datasourceId, datasourceName, datasourceSrv, options) {
return DBConnector.loadDatasource(datasourceId, datasourceName, datasourceSrv)
initDBConnector(datasourceId, datasourceName, options) {
return DBConnector.loadDatasource(datasourceId, datasourceName)
.then(ds => {
let connectorOptions = { datasourceId, datasourceName };
const connectorOptions: any = { datasourceId, datasourceName };
if (ds.type === 'influxdb') {
connectorOptions.retentionPolicy = options.dbConnectionRetentionPolicy;
this.dbConnector = new InfluxDBConnector(connectorOptions, datasourceSrv);
this.dbConnector = new InfluxDBConnector(connectorOptions);
} else {
this.dbConnector = new SQLConnector(connectorOptions, datasourceSrv);
this.dbConnector = new SQLConnector(connectorOptions);
}
return this.dbConnector;
});
}
proxyfyRequests() {
for (let request of REQUESTS_TO_PROXYFY) {
for (const request of REQUESTS_TO_PROXYFY) {
this.zabbixAPI[request] = this.cachingProxy.proxyfy(this.zabbixAPI[request], request, this.zabbixAPI);
}
}
cacheRequests() {
for (let request of REQUESTS_TO_CACHE) {
for (const request of REQUESTS_TO_CACHE) {
this.zabbixAPI[request] = this.cachingProxy.cacheRequest(this.zabbixAPI[request], request, this.zabbixAPI);
}
}
bindRequests() {
for (let request of REQUESTS_TO_BIND) {
for (const request of REQUESTS_TO_BIND) {
this[request] = this.zabbixAPI[request].bind(this.zabbixAPI);
}
}
@@ -101,14 +133,14 @@ export class Zabbix {
* Perform test query for Zabbix API and external history DB.
* @return {object} test result object:
* ```
{
zabbixVersion,
dbConnectorStatus: {
dsType,
dsName
}
}
```
* {
* zabbixVersion,
* dbConnectorStatus: {
* dsType,
* dsName
* }
* }
* ```
*/
// testDataSource() {
// let zabbixVersion;
@@ -143,19 +175,20 @@ export class Zabbix {
// }
getItemsFromTarget(target, options) {
let parts = ['group', 'host', 'application', 'item'];
let filters = _.map(parts, p => target[p].filter);
const parts = ['group', 'host', 'application', 'item'];
const filters = _.map(parts, p => target[p].filter);
return this.getItems(...filters, options);
}
getHostsFromTarget(target) {
let parts = ['group', 'host', 'application'];
let filters = _.map(parts, p => target[p].filter);
const parts = ['group', 'host', 'application'];
const filters = _.map(parts, p => target[p].filter);
return Promise.all([
this.getHosts(...filters),
this.getApps(...filters),
]).then((results) => {
let [hosts, apps] = results;
]).then(results => {
const hosts = results[0];
let apps: AppsResponse = results[1];
if (apps.appFilterEmpty) {
apps = [];
}
@@ -178,12 +211,12 @@ export class Zabbix {
getAllHosts(groupFilter) {
return this.getGroups(groupFilter)
.then(groups => {
let groupids = _.map(groups, 'groupid');
const groupids = _.map(groups, 'groupid');
return this.zabbixAPI.getHosts(groupids);
});
}
getHosts(groupFilter, hostFilter) {
getHosts(groupFilter?, hostFilter?) {
return this.getAllHosts(groupFilter)
.then(hosts => findByFilter(hosts, hostFilter));
}
@@ -194,34 +227,34 @@ export class Zabbix {
getAllApps(groupFilter, hostFilter) {
return this.getHosts(groupFilter, hostFilter)
.then(hosts => {
let hostids = _.map(hosts, 'hostid');
const hostids = _.map(hosts, 'hostid');
return this.zabbixAPI.getApps(hostids);
});
}
getApps(groupFilter, hostFilter, appFilter) {
getApps(groupFilter?, hostFilter?, appFilter?): Promise<AppsResponse> {
return this.getHosts(groupFilter, hostFilter)
.then(hosts => {
let hostids = _.map(hosts, 'hostid');
const hostids = _.map(hosts, 'hostid');
if (appFilter) {
return this.zabbixAPI.getApps(hostids)
.then(apps => filterByQuery(apps, appFilter));
} else {
return {
appFilterEmpty: true,
hostids: hostids
};
const appsResponse: AppsResponse = hostids;
appsResponse.hostids = hostids;
appsResponse.appFilterEmpty = true;
return Promise.resolve(appsResponse);
}
});
}
getAllItems(groupFilter, hostFilter, appFilter, options = {}) {
getAllItems(groupFilter, hostFilter, appFilter, options: any = {}) {
return this.getApps(groupFilter, hostFilter, appFilter)
.then(apps => {
if (apps.appFilterEmpty) {
return this.zabbixAPI.getItems(apps.hostids, undefined, options.itemtype);
} else {
let appids = _.map(apps, 'applicationid');
const appids = _.map(apps, 'applicationid');
return this.zabbixAPI.getItems(undefined, appids, options.itemtype);
}
})
@@ -235,34 +268,54 @@ export class Zabbix {
.then(this.expandUserMacro.bind(this));
}
expandUserMacro(items) {
let hostids = getHostIds(items);
expandUserMacro(items, isTriggerItem) {
const hostids = getHostIds(items);
return this.getMacros(hostids)
.then(macros => {
_.forEach(items, item => {
if (utils.containsMacro(item.name)) {
item.name = utils.replaceMacro(item, macros);
if (utils.containsMacro(isTriggerItem ? item.url : item.name)) {
if (isTriggerItem) {
item.url = utils.replaceMacro(item, macros, isTriggerItem);
} else {
item.name = utils.replaceMacro(item, macros);
}
}
});
return items;
});
}
getItems(groupFilter, hostFilter, appFilter, itemFilter, options = {}) {
getItems(groupFilter?, hostFilter?, appFilter?, itemFilter?, options = {}) {
return this.getAllItems(groupFilter, hostFilter, appFilter, options)
.then(items => filterByQuery(items, itemFilter));
}
getItemValues(groupFilter?, hostFilter?, appFilter?, itemFilter?, options: any = {}) {
return this.getItems(groupFilter, hostFilter, appFilter, itemFilter, options).then(items => {
let timeRange = [moment().subtract(2, 'h').unix(), moment().unix()];
if (options.range) {
timeRange = [options.range.from.unix(), options.range.to.unix()];
}
const [timeFrom, timeTo] = timeRange;
return this.zabbixAPI.getHistory(items, timeFrom, timeTo).then(history => {
if (history) {
const values = _.uniq(history.map(v => v.value));
return values.map(value => ({ name: value }));
} else {
return [];
}
});
});
}
getITServices(itServiceFilter) {
return this.zabbixAPI.getITService()
.then(itServices => findByFilter(itServices, itServiceFilter));
}
/**
* Build query - convert target filters to array of Zabbix items
*/
getTriggers(groupFilter, hostFilter, appFilter, options, proxyFilter) {
let promises = [
getProblems(groupFilter, hostFilter, appFilter, proxyFilter?, options?) {
const promises = [
this.getGroups(groupFilter),
this.getHosts(groupFilter, hostFilter),
this.getApps(groupFilter, hostFilter, appFilter)
@@ -270,8 +323,8 @@ export class Zabbix {
return Promise.all(promises)
.then(results => {
let [filteredGroups, filteredHosts, filteredApps] = results;
let query = {};
const [filteredGroups, filteredHosts, filteredApps] = results;
const query: any = {};
if (appFilter) {
query.applicationids = _.flatten(_.map(filteredApps, 'applicationid'));
@@ -285,8 +338,53 @@ export class Zabbix {
return query;
})
.then(query => this.zabbixAPI.getTriggers(query.groupids, query.hostids, query.applicationids, options))
.then(triggers => this.filterTriggersByProxy(triggers, proxyFilter));
.then(query => this.zabbixAPI.getProblems(query.groupids, query.hostids, query.applicationids, options))
.then(problems => {
const triggerids = problems?.map(problem => problem.objectid);
return Promise.all([
Promise.resolve(problems),
this.zabbixAPI.getTriggersByIds(triggerids)
]);
})
.then(([problems, triggers]) => joinTriggersWithProblems(problems, triggers))
.then(triggers => this.filterTriggersByProxy(triggers, proxyFilter))
.then(triggers => this.expandUserMacro.bind(this)(triggers, true));
}
getProblemsHistory(groupFilter, hostFilter, appFilter, proxyFilter?, options?): Promise<ProblemDTO[]> {
const { valueFromEvent } = options;
const promises = [
this.getGroups(groupFilter),
this.getHosts(groupFilter, hostFilter),
this.getApps(groupFilter, hostFilter, appFilter)
];
return Promise.all(promises)
.then(results => {
const [filteredGroups, filteredHosts, filteredApps] = results;
const query: any = {};
if (appFilter) {
query.applicationids = _.flatten(_.map(filteredApps, 'applicationid'));
}
if (hostFilter) {
query.hostids = _.map(filteredHosts, 'hostid');
}
if (groupFilter) {
query.groupids = _.map(filteredGroups, 'groupid');
}
return query;
})
.then(query => this.zabbixAPI.getEventsHistory(query.groupids, query.hostids, query.applicationids, options))
.then(problems => {
const triggerids = problems?.map(problem => problem.objectid);
return Promise.all([Promise.resolve(problems), this.zabbixAPI.getTriggersByIds(triggerids)]);
})
.then(([problems, triggers]) => joinTriggersWithEvents(problems, triggers, { valueFromEvent }))
.then(triggers => this.filterTriggersByProxy(triggers, proxyFilter))
.then(triggers => this.expandUserMacro.bind(this)(triggers, true));
}
filterTriggersByProxy(triggers, proxyFilter) {
@@ -295,14 +393,13 @@ export class Zabbix {
if (proxyFilter && proxyFilter !== '/.*/' && triggers) {
const proxy_ids = proxies.map(proxy => proxy.proxyid);
triggers = triggers.filter(trigger => {
let filtered = false;
for(let i = 0; i < trigger.hosts.length; i++) {
for (let i = 0; i < trigger.hosts.length; i++) {
const host = trigger.hosts[i];
if (proxy_ids.includes(host.proxy_hostid)) {
filtered = true;
return true;
}
}
return filtered;
return false;
});
}
return triggers;
@@ -318,7 +415,7 @@ export class Zabbix {
}
getHistoryTS(items, timeRange, options) {
let [timeFrom, timeTo] = timeRange;
const [timeFrom, timeTo] = timeRange;
if (this.enableDirectDBConnection) {
return this.getHistoryDB(items, timeFrom, timeTo, options)
.then(history => this.dbConnector.handleGrafanaTSResponse(history, items));
@@ -329,12 +426,12 @@ export class Zabbix {
}
getTrends(items, timeRange, options) {
let [timeFrom, timeTo] = timeRange;
const [timeFrom, timeTo] = timeRange;
if (this.enableDirectDBConnection) {
return this.getTrendsDB(items, timeFrom, timeTo, options)
.then(history => this.dbConnector.handleGrafanaTSResponse(history, items));
} else {
let valueType = options.consolidateBy || options.valueType;
const valueType = options.consolidateBy || options.valueType;
return this.zabbixAPI.getTrend(items, timeFrom, timeTo)
.then(history => responseHandler.handleTrends(history, items, valueType))
.then(responseHandler.sortTimeseries); // Sort trend data, issue #202
@@ -342,7 +439,7 @@ export class Zabbix {
}
getHistoryText(items, timeRange, target) {
let [timeFrom, timeTo] = timeRange;
const [timeFrom, timeTo] = timeRange;
if (items.length) {
return this.zabbixAPI.getHistory(items, timeFrom, timeTo)
.then(history => {
@@ -358,15 +455,11 @@ export class Zabbix {
}
getSLA(itservices, timeRange, target, options) {
let itServices = itservices;
if (options.isOldVersion) {
itServices = _.filter(itServices, {'serviceid': target.itservice.serviceid});
}
let itServiceIds = _.map(itServices, 'serviceid');
const itServiceIds = _.map(itservices, 'serviceid');
return this.zabbixAPI.getSLA(itServiceIds, timeRange, options)
.then(slaResponse => {
return _.map(itServiceIds, serviceid => {
let itservice = _.find(itServices, {'serviceid': serviceid});
const itservice = _.find(itservices, {'serviceid': serviceid});
return responseHandler.handleSLAResponse(itservice, target.slaProperty, slaResponse);
});
});
@@ -382,7 +475,7 @@ export class Zabbix {
* @return array with finded element or empty array
*/
function findByName(list, name) {
var finded = _.find(list, {'name': name});
const finded = _.find(list, {'name': name});
if (finded) {
return [finded];
} else {
@@ -399,7 +492,7 @@ function findByName(list, name) {
* @return {[type]} array with finded element or empty array
*/
function filterByName(list, name) {
var finded = _.filter(list, {'name': name});
const finded = _.filter(list, {'name': name});
if (finded) {
return finded;
} else {
@@ -408,8 +501,8 @@ function filterByName(list, name) {
}
function filterByRegex(list, regex) {
var filterPattern = utils.buildRegex(regex);
return _.filter(list, function (zbx_obj) {
const filterPattern = utils.buildRegex(regex);
return _.filter(list, (zbx_obj) => {
return filterPattern.test(zbx_obj.name);
});
}
@@ -431,7 +524,7 @@ function filterByQuery(list, filter) {
}
function getHostIds(items) {
let hostIds = _.map(items, item => {
const hostIds = _.map(items, item => {
return _.map(item.hosts, 'hostid');
});
return _.uniq(_.flatten(hostIds));

View File

@@ -16,8 +16,11 @@ export class ZabbixAlertingService {
}
setPanelAlertState(panelId, alertState) {
let panelIndex;
if (!alertState) {
return;
}
let panelIndex;
let panelContainers = _.filter($('.panel-container'), elem => {
return elem.clientHeight && elem.clientWidth;
});
@@ -32,8 +35,7 @@ export class ZabbixAlertingService {
});
}
// Don't apply alert styles to .panel-container--absolute (it rewrites position from absolute to relative)
if (panelIndex >= 0 && !panelContainers[panelIndex].className.includes('panel-container--absolute')) {
if (panelIndex >= 0) {
let alertClass = "panel-has-alert panel-alert-state--ok panel-alert-state--alerting";
$(panelContainers[panelIndex]).removeClass(alertClass);