Merge branch 'master' into backend
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
@@ -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;}
|
||||
1
src/datasource-zabbix/img/icn-zabbix-datasource.svg
Normal file
1
src/datasource-zabbix/img/icn-zabbix-datasource.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.4 KiB |
@@ -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 |
@@ -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;
|
||||
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
201
src/datasource-zabbix/problemsHandler.ts
Normal file
201
src/datasource-zabbix/problemsHandler.ts
Normal 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;
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;}
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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'],
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 || [];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
42
src/datasource-zabbix/zabbix/connectors/zabbix_api/types.ts
Normal file
42
src/datasource-zabbix/zabbix/connectors/zabbix_api/types.ts
Normal 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;
|
||||
@@ -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)
|
||||
});
|
||||
|
||||
}
|
||||
@@ -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 || "";
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
23
src/datasource-zabbix/zabbix/types.ts
Normal file
23
src/datasource-zabbix/zabbix/types.ts
Normal 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;
|
||||
}
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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));
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user