Build plugin with grafana toolkit (#1539)

* Use grafana toolkit template for building plugin

* Fix linter and type errors

* Update styles building

* Fix sass deprecation warning

* Remove empty js files produced by webpack building sass

* Fix signing script

* Replace classnames with cx

* Fix data source config page

* Use custom webpack config instead of overriding original one

* Use gpx_ prefix for plugin executable

* Remove unused configs

* Roll back react hooks dependencies usage

* Move plugin-specific ts config to root config file

* Temporary do not use rst2html for function description tooltip

* Remove unused code

* remove unused dependencies

* update react table dependency

* Migrate tests to typescript

* remove unused dependencies

* Remove old webpack configs

* Add sign target to makefile

* Add magefile

* Update CI test job

* Update go packages

* Update build instructions

* Downgrade go version to 1.18

* Fix go version in ci

* Fix metric picker

* Add comment to webpack config

* remove angular mocks

* update bra config

* Rename datasource-zabbix to datasource (fix mage build)

* Add instructions for building backend with mage

* Fix webpack targets

* Fix ci backend tests

* Add initial e2e tests

* Fix e2e ci tests

* Update docker compose for cypress tests

* build grafana docker image

* Fix docker stop task

* CI: add Grafana compatibility check
This commit is contained in:
Alexander Zobnin
2022-12-09 14:14:34 +03:00
committed by GitHub
parent 26ed740945
commit e3e896742b
136 changed files with 5765 additions and 4636 deletions

View File

@@ -0,0 +1,88 @@
import _ from 'lodash';
import { getDataSourceSrv } from '@grafana/runtime';
export const DEFAULT_QUERY_LIMIT = 10000;
export const HISTORY_TO_TABLE_MAP = {
'0': 'history',
'1': 'history_str',
'2': 'history_log',
'3': 'history_uint',
'4': 'history_text',
};
export const TREND_TO_TABLE_MAP = {
'0': 'trends',
'3': 'trends_uint',
};
export const consolidateByFunc = {
avg: 'AVG',
min: 'MIN',
max: 'MAX',
sum: 'SUM',
count: 'COUNT',
};
export const consolidateByTrendColumns = {
avg: 'value_avg',
min: 'value_min',
max: 'value_max',
sum: 'num*value_avg', // sum of sums inside the one-hour trend period
};
/**
* Base class for external history database connectors. Subclasses should implement `getHistory()`, `getTrends()` and
* `testDataSource()` methods, which describe how to fetch data from source other than Zabbix API.
*/
export class DBConnector {
protected datasourceId: any;
private datasourceName: any;
protected datasourceTypeId: any;
// private datasourceTypeName: any;
constructor(options) {
this.datasourceId = options.datasourceId;
this.datasourceName = options.datasourceName;
this.datasourceTypeId = null;
// this.datasourceTypeName = null;
}
static loadDatasource(dsId, dsName) {
if (!dsName && dsId !== undefined) {
const ds = _.find(getDataSourceSrv().getList(), { id: dsId });
if (!ds) {
return Promise.reject(`Data Source with ID ${dsId} not found`);
}
dsName = ds.name;
}
if (dsName) {
return getDataSourceSrv().get(dsName);
} else {
return Promise.reject(`Data Source name should be specified`);
}
}
loadDBDataSource() {
return DBConnector.loadDatasource(this.datasourceId, this.datasourceName).then((ds) => {
this.datasourceTypeId = ds.meta.id;
// this.datasourceTypeName = ds.meta.name;
if (!this.datasourceName) {
this.datasourceName = ds.name;
}
if (!this.datasourceId) {
this.datasourceId = ds.id;
}
return ds;
});
}
}
export default {
DBConnector,
DEFAULT_QUERY_LIMIT,
HISTORY_TO_TABLE_MAP,
TREND_TO_TABLE_MAP,
consolidateByFunc,
consolidateByTrendColumns,
};

View File

@@ -0,0 +1,165 @@
import {
ArrayVector,
DataFrame,
dataFrameToJSON,
Field,
FieldType,
MutableDataFrame,
TIME_SERIES_TIME_FIELD_NAME,
} from '@grafana/data';
import _ from 'lodash';
import { compactQuery } from '../../../utils';
import { consolidateByTrendColumns, DBConnector, HISTORY_TO_TABLE_MAP } from '../dbConnector';
const consolidateByFunc = {
avg: 'MEAN',
min: 'MIN',
max: 'MAX',
sum: 'SUM',
count: 'COUNT',
};
export class InfluxDBConnector extends DBConnector {
private retentionPolicy: any;
private influxDS: any;
constructor(options) {
super(options);
this.retentionPolicy = options.retentionPolicy;
super.loadDBDataSource().then((ds) => {
this.influxDS = ds;
return ds;
});
}
/**
* Try to invoke test query for one of Zabbix database tables.
*/
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) {
const { intervalMs, retentionPolicy } = options;
let { consolidateBy } = options;
const intervalSec = Math.ceil(intervalMs / 1000);
const range = { timeFrom, timeTill };
consolidateBy = consolidateBy || 'avg';
// Group items by value type and perform request for each value type
const grouped_items = _.groupBy(items, 'value_type');
const promises = _.map(grouped_items, (items, value_type) => {
const itemids = _.map(items, 'itemid');
const table = HISTORY_TO_TABLE_MAP[value_type];
const query = this.buildHistoryQuery(itemids, table, range, intervalSec, consolidateBy, retentionPolicy);
return this.invokeInfluxDBQuery(query);
});
return Promise.all(promises)
.then(_.flatten)
.then((results) => {
return handleInfluxHistoryResponse(results);
});
}
getTrends(items, timeFrom, timeTill, options) {
options.retentionPolicy = this.retentionPolicy;
return this.getHistory(items, timeFrom, timeTill, options);
}
buildHistoryQuery(itemids, table, range, intervalSec, aggFunction, retentionPolicy) {
const { timeFrom, timeTill } = range;
const measurement = retentionPolicy ? `"${retentionPolicy}"."${table}"` : `"${table}"`;
let value = 'value';
if (retentionPolicy) {
value = consolidateByTrendColumns[aggFunction] || 'value_avg';
}
const aggregation = consolidateByFunc[aggFunction] || aggFunction;
const where_clause = this.buildWhereClause(itemids);
const query = `SELECT ${aggregation}("${value}")
FROM ${measurement}
WHERE ${where_clause}
AND "time" >= ${timeFrom}s
AND "time" <= ${timeTill}s
GROUP BY time(${intervalSec}s), "itemid" fill(none)`;
return compactQuery(query);
}
buildWhereClause(itemids) {
const itemidsWhere = itemids.map((itemid) => `"itemid" = '${itemid}'`).join(' OR ');
return `(${itemidsWhere})`;
}
async invokeInfluxDBQuery(query) {
const data = await this.influxDS._seriesQuery(query).toPromise();
return data?.results || [];
}
}
///////////////////////////////////////////////////////////////////////////////
function handleInfluxHistoryResponse(results) {
if (!results) {
return [];
}
const frames: DataFrame[] = [];
for (let i = 0; i < results.length; i++) {
const result = results[i];
if (result.error) {
const error = `InfluxDB error: ${result.error}`;
return Promise.reject(new Error(error));
}
if (!result || !result.series) {
continue;
}
const influxSeriesList = results[i].series;
for (let y = 0; y < influxSeriesList.length; y++) {
const influxSeries = influxSeriesList[y];
const tsBuffer = [];
const valuesBuffer = [];
if (influxSeries.values) {
for (i = 0; i < influxSeries.values.length; i++) {
tsBuffer.push(influxSeries.values[i][0]);
valuesBuffer.push(influxSeries.values[i][1]);
}
}
const timeFiled: Field<number> = {
name: TIME_SERIES_TIME_FIELD_NAME,
type: FieldType.time,
config: {},
values: new ArrayVector(tsBuffer),
};
const valueFiled: Field<number | null> = {
name: influxSeries?.tags?.itemid,
type: FieldType.number,
config: {},
values: new ArrayVector(valuesBuffer),
};
frames.push(
new MutableDataFrame({
name: influxSeries?.tags?.itemid,
fields: [timeFiled, valueFiled],
})
);
}
}
return frames.map((f) => dataFrameToJSON(f));
}

View File

@@ -0,0 +1,46 @@
/**
* MySQL queries
*/
function historyQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction) {
const time_expression = `clock DIV ${intervalSec} * ${intervalSec}`;
return `
SELECT CAST(itemid AS CHAR) AS metric, ${time_expression} AS time_sec, ${aggFunction}(value) AS value
FROM ${table}
WHERE itemid IN (${itemids})
AND clock
> ${timeFrom}
AND clock
< ${timeTill}
GROUP BY ${time_expression}, metric
ORDER BY time_sec ASC
`;
}
function trendsQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction, valueColumn) {
const time_expression = `clock DIV ${intervalSec} * ${intervalSec}`;
return `
SELECT CAST(itemid AS CHAR) AS metric, ${time_expression} AS time_sec, ${aggFunction}(${valueColumn}) AS value
FROM ${table}
WHERE itemid IN (${itemids})
AND clock
> ${timeFrom}
AND clock
< ${timeTill}
GROUP BY ${time_expression}, metric
ORDER BY time_sec ASC
`;
}
function testQuery() {
return `SELECT CAST(itemid AS CHAR) AS metric, clock AS time_sec, value_avg AS value
FROM trends_uint LIMIT 1`;
}
const mysql = {
historyQuery,
trendsQuery,
testQuery,
};
export default mysql;

View File

@@ -0,0 +1,52 @@
/**
* Postgres queries
*/
const ITEMID_FORMAT = 'FM99999999999999999999';
function historyQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction) {
const time_expression = `clock / ${intervalSec} * ${intervalSec}`;
return `
SELECT to_char(itemid, '${ITEMID_FORMAT}') AS metric, ${time_expression} AS time, ${aggFunction}(value) AS value
FROM ${table}
WHERE itemid IN (${itemids})
AND clock
> ${timeFrom}
AND clock
< ${timeTill}
GROUP BY 1, 2
ORDER BY time ASC
`;
}
function trendsQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction, valueColumn) {
const time_expression = `clock / ${intervalSec} * ${intervalSec}`;
return `
SELECT to_char(itemid, '${ITEMID_FORMAT}') AS metric, ${time_expression} AS time, ${aggFunction}(${valueColumn}) AS value
FROM ${table}
WHERE itemid IN (${itemids})
AND clock
> ${timeFrom}
AND clock
< ${timeTill}
GROUP BY 1, 2
ORDER BY time ASC
`;
}
const TEST_QUERY = `
SELECT to_char(itemid, '${ITEMID_FORMAT}') AS metric, clock AS time, value_avg AS value
FROM trends_uint LIMIT 1
`;
function testQuery() {
return TEST_QUERY;
}
const postgres = {
historyQuery,
trendsQuery,
testQuery,
};
export default postgres;

View File

@@ -0,0 +1,140 @@
import _ from 'lodash';
import { getBackendSrv } from '@grafana/runtime';
import { compactQuery } from '../../../utils';
import mysql from './mysql';
import postgres from './postgres';
import dbConnector, {
DBConnector,
DEFAULT_QUERY_LIMIT,
HISTORY_TO_TABLE_MAP,
TREND_TO_TABLE_MAP,
} from '../dbConnector';
const supportedDatabases = {
mysql: 'mysql',
postgres: 'postgres',
};
export class SQLConnector extends DBConnector {
private limit: number;
private sqlDialect: any;
constructor(options) {
super(options);
this.limit = options.limit || DEFAULT_QUERY_LIMIT;
this.sqlDialect = null;
super.loadDBDataSource().then(() => {
this.loadSQLDialect();
});
}
loadSQLDialect() {
if (this.datasourceTypeId === supportedDatabases.postgres) {
this.sqlDialect = postgres;
} else {
this.sqlDialect = mysql;
}
}
/**
* Try to invoke test query for one of Zabbix database tables.
*/
testDataSource() {
const testQuery = this.sqlDialect.testQuery();
return this.invokeSQLQuery(testQuery);
}
getHistory(items, timeFrom, timeTill, options) {
const { aggFunction, intervalSec } = getAggFunc(timeFrom, timeTill, options);
// Group items by value type and perform request for each value type
const grouped_items = _.groupBy(items, 'value_type');
const promises = _.map(grouped_items, (items, value_type) => {
const itemids = _.map(items, 'itemid').join(', ');
const table = HISTORY_TO_TABLE_MAP[value_type];
let query = this.sqlDialect.historyQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction);
query = compactQuery(query);
return this.invokeSQLQuery(query);
});
return Promise.all(promises).then((results) => {
return _.flatten(results);
});
}
getTrends(items, timeFrom, timeTill, options) {
const { consolidateBy } = options;
const { aggFunction, intervalSec } = getAggFunc(timeFrom, timeTill, options);
// Group items by value type and perform request for each value type
const grouped_items = _.groupBy(items, 'value_type');
const promises = _.map(grouped_items, (items, value_type) => {
const itemids = _.map(items, 'itemid').join(', ');
const table = TREND_TO_TABLE_MAP[value_type];
let valueColumn = _.includes(['avg', 'min', 'max', 'sum'], consolidateBy) ? consolidateBy : 'avg';
valueColumn = dbConnector.consolidateByTrendColumns[valueColumn];
let query = this.sqlDialect.trendsQuery(
itemids,
table,
timeFrom,
timeTill,
intervalSec,
aggFunction,
valueColumn
);
query = compactQuery(query);
return this.invokeSQLQuery(query);
});
return Promise.all(promises).then((results) => {
return _.flatten(results);
});
}
invokeSQLQuery(query) {
const queryDef = {
refId: 'A',
format: 'time_series',
datasourceId: this.datasourceId,
rawSql: query,
maxDataPoints: this.limit,
};
return getBackendSrv()
.datasourceRequest({
url: '/api/ds/query',
method: 'POST',
data: {
queries: [queryDef],
},
})
.then((response) => {
const results = response.data.results;
if (results['A']) {
return results['A'].frames;
} else {
return null;
}
});
}
}
function getAggFunc(timeFrom, timeTill, options) {
const { intervalMs } = options;
let { 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)
const numOfIntervals = Math.ceil((timeTill - timeFrom) / intervalSec);
intervalSec = Math.ceil((timeTill - timeFrom) / numOfIntervals);
consolidateBy = consolidateBy || 'avg';
const aggFunction = dbConnector.consolidateByFunc[consolidateBy];
return { aggFunction, intervalSec };
}

View File

@@ -0,0 +1,52 @@
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 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> = Promise<T>;
export type APILoginResponse = string;
export interface ZBXScript {
scriptid: string;
name?: string;
command?: string;
host_access?: string;
usrgrpid?: string;
groupid?: string;
description?: string;
confirmation?: string;
type?: string;
execute_on?: string;
}
export interface APIExecuteScriptResponse {
response: 'success' | 'failed';
value?: string;
}

View File

@@ -0,0 +1,773 @@
import _ from 'lodash';
import semver from 'semver';
import kbn from 'grafana/app/core/utils/kbn';
import * as utils from '../../../utils';
import { MIN_SLA_INTERVAL, ZBX_ACK_ACTION_ADD_MESSAGE, ZBX_ACK_ACTION_NONE } from '../../../constants';
import { ShowProblemTypes, ZBXProblem } from '../../../types';
import { APIExecuteScriptResponse, JSONRPCError, ZBXScript } from './types';
import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime';
import { rangeUtil } from '@grafana/data';
const DEFAULT_ZABBIX_VERSION = '3.0.0';
// Backward compatibility. Since Grafana 7.2 roundInterval() func was moved to @grafana/data package
const roundInterval: (interval: number) => number = rangeUtil?.roundInterval || kbn.roundInterval || kbn.round_interval;
/**
* Zabbix API Wrapper.
* Creates Zabbix API instance with given parameters (url, credentials and other).
* Wraps API calls and provides high-level methods.
*/
export class ZabbixAPIConnector {
backendAPIUrl: string;
requestOptions: { basicAuth: any; withCredentials: boolean };
getTrend: (items: any, timeFrom: any, timeTill: any) => Promise<any[]>;
version: string;
getVersionPromise: Promise<string>;
datasourceId: number;
constructor(basicAuth: any, withCredentials: boolean, datasourceId: number) {
this.datasourceId = datasourceId;
this.backendAPIUrl = `/api/datasources/${this.datasourceId}/resources/zabbix-api`;
this.requestOptions = {
basicAuth: basicAuth,
withCredentials: withCredentials,
};
this.getTrend = this.getTrend_ZBXNEXT1193;
//getTrend = getTrend_30;
this.initVersion();
}
//////////////////////////
// Core method wrappers //
//////////////////////////
request(method: string, params?: any) {
if (!this.version) {
return this.initVersion().then(() => this.request(method, params));
}
return this.backendAPIRequest(method, params);
}
async backendAPIRequest(method: string, params: any = {}) {
const requestOptions: BackendSrvRequest = {
url: this.backendAPIUrl,
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
hideFromInspector: false,
data: {
datasourceId: this.datasourceId,
method,
params,
},
};
// Set request options for basic auth
if (this.requestOptions.basicAuth || this.requestOptions.withCredentials) {
requestOptions.withCredentials = true;
}
if (this.requestOptions.basicAuth) {
requestOptions.headers.Authorization = this.requestOptions.basicAuth;
}
const response = await getBackendSrv().fetch<any>(requestOptions).toPromise();
return response?.data?.result;
}
/**
* Get Zabbix API version
*/
getVersion() {
return this.backendAPIRequest('apiinfo.version');
}
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;
}
isZabbix54OrHigher() {
return semver.gte(this.version, '5.4.0');
}
////////////////////////////////
// Zabbix API method wrappers //
////////////////////////////////
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 !== undefined) {
params.severity = severity;
}
return this.request('event.acknowledge', params);
}
getGroups() {
const params = {
output: ['name', 'groupid'],
sortfield: 'name',
real_hosts: true,
};
return this.request('hostgroup.get', params);
}
getHosts(groupids) {
const params: any = {
output: ['hostid', 'name', 'host'],
sortfield: 'name',
};
if (groupids) {
params.groupids = groupids;
}
return this.request('host.get', params);
}
async getApps(hostids): Promise<any[]> {
if (this.isZabbix54OrHigher()) {
return [];
}
const params = {
output: 'extend',
hostids: hostids,
};
return this.request('application.get', params);
}
/**
* Get Zabbix items
* @param {[type]} hostids host ids
* @param {[type]} appids application ids
* @param {String} itemtype 'num' or 'text'
* @return {[type]} array of items
*/
getItems(hostids, appids, itemtype) {
const params: any = {
output: ['itemid', 'name', 'key_', 'value_type', 'hostid', 'status', 'state', 'units', 'valuemapid', 'delay'],
sortfield: 'name',
webitems: true,
filter: {},
selectHosts: ['hostid', 'name', 'host'],
};
if (hostids) {
params.hostids = hostids;
}
if (appids) {
params.applicationids = appids;
}
if (itemtype === 'num') {
// Return only numeric metrics
params.filter.value_type = [0, 3];
}
if (itemtype === 'text') {
// Return only text metrics
params.filter.value_type = [1, 2, 4];
}
if (this.isZabbix54OrHigher()) {
params.selectTags = 'extend';
}
return this.request('item.get', params).then(utils.expandItems);
}
getItemsByIDs(itemids) {
const params: any = {
itemids: itemids,
output: ['itemid', 'name', 'key_', 'value_type', 'hostid', 'status', 'state', 'units', 'valuemapid', 'delay'],
webitems: true,
selectHosts: ['hostid', 'name'],
};
if (this.isZabbix54OrHigher()) {
params.selectTags = 'extend';
}
return this.request('item.get', params).then((items) => utils.expandItems(items));
}
getMacros(hostids) {
const params = {
output: 'extend',
hostids: hostids,
};
return this.request('usermacro.get', params);
}
getGlobalMacros() {
const params = {
output: 'extend',
globalmacro: true,
};
return this.request('usermacro.get', params);
}
getLastValue(itemid) {
const params = {
output: ['lastvalue'],
itemids: itemid,
};
return this.request('item.get', params).then((items) => (items.length ? items[0].lastvalue : null));
}
/**
* Perform history query from Zabbix API
*
* @param {Array} items Array of Zabbix item objects
* @param {Number} timeFrom Time in seconds
* @param {Number} timeTill Time in seconds
* @return {Array} Array of Zabbix history objects
*/
getHistory(items, timeFrom, timeTill) {
// Group items by value type and perform request for each value type
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,
sortfield: 'clock',
sortorder: 'ASC',
time_from: timeFrom,
};
// Relative queries (e.g. last hour) don't include an end time
if (timeTill) {
params.time_till = timeTill;
}
return this.request('history.get', params);
});
return Promise.all(promises).then(_.flatten);
}
/**
* Perform trends query from Zabbix API
* Use trends api extension from ZBXNEXT-1193 patch.
*
* @param {Array} items Array of Zabbix item objects
* @param {Number} time_from Time in seconds
* @param {Number} time_till Time in seconds
* @return {Array} Array of Zabbix trend objects
*/
getTrend_ZBXNEXT1193(items, timeFrom, timeTill) {
// Group items by value type and perform request for each value type
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,
sortfield: 'clock',
sortorder: 'ASC',
time_from: timeFrom,
};
// Relative queries (e.g. last hour) don't include an end time
if (timeTill) {
params.time_till = timeTill;
}
return this.request('trend.get', params);
});
return Promise.all(promises).then(_.flatten);
}
getTrend_30(items, time_from, time_till, value_type) {
const self = this;
const itemids = _.map(items, 'itemid');
const params: any = {
output: ['itemid', 'clock', value_type],
itemids: itemids,
time_from: time_from,
};
// Relative queries (e.g. last hour) don't include an end time
if (time_till) {
params.time_till = time_till;
}
return self.request('trend.get', params);
}
getITService(serviceids?) {
const params = {
output: 'extend',
serviceids: serviceids,
};
return this.request('service.get', params);
}
getSLA(serviceids, timeRange, options) {
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);
}
async getSLA60(serviceids, timeRange, options) {
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 = {
output: 'extend',
serviceids,
};
const slaObjects = await this.request('sla.get', params);
if (slaObjects.length === 0) {
return {};
}
const sla = slaObjects[0];
// const periods = intervals.map(interval => ({
// period_from: interval.from,
// period_to: interval.to,
// }));
const sliParams: any = {
slaid: sla.slaid,
serviceids,
period_from: timeFrom,
period_to: timeTo,
periods: Math.min(intervals.length, 100),
};
const sliResponse = await this.request('sla.getsli', sliParams);
if (sliResponse.length === 0) {
return {};
}
const slaLikeResponse: any = {};
sliResponse.serviceids.forEach((serviceid) => {
slaLikeResponse[serviceid] = {
sla: [],
};
});
sliResponse.sli.forEach((sliItem, i) => {
sliItem.forEach((sli, j) => {
slaLikeResponse[sliResponse.serviceids[j]].sla.push({
downtimeTime: sli.downtime,
okTime: sli.uptime,
sla: sli.sli,
from: sliResponse.periods[i].period_from,
to: sliResponse.periods[i].period_to,
});
});
});
return slaLikeResponse;
}
getProblems(groupids, hostids, applicationids, options): Promise<ZBXProblem[]> {
const { timeFrom, timeTo, recent, severities, limit, acknowledged, tags } = options;
const params: any = {
output: 'extend',
selectAcknowledges: 'extend',
selectSuppressionData: 'extend',
selectTags: 'extend',
source: '0',
object: '0',
sortfield: ['eventid'],
sortorder: 'DESC',
evaltype: '0',
// preservekeys: '1',
groupids,
hostids,
applicationids,
recent,
};
if (severities) {
params.severities = severities;
}
if (acknowledged !== undefined) {
params.acknowledged = acknowledged;
}
if (tags) {
params.tags = tags;
}
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', 'groupid'],
selectHosts: ['hostid', 'name', 'host', 'maintenance_status', 'proxy_hostid'],
selectItems: ['itemid', '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,
applicationids: applicationids,
expandDescription: true,
expandData: true,
expandComment: true,
monitored: true,
skipDependent: true,
//only_true: true,
filter: {
value: 1,
},
selectGroups: ['groupid', 'name'],
selectHosts: ['hostid', 'name', 'host', 'maintenance_status', 'proxy_hostid'],
selectItems: ['itemid', 'name', 'key_', 'lastvalue'],
selectLastEvent: 'extend',
selectTags: 'extend',
};
if (showTriggers === ShowProblemTypes.Problems) {
params.filter.value = 1;
} else if (showTriggers === ShowProblemTypes.Recent || showTriggers === ShowProblemTypes.History) {
params.filter.value = [0, 1];
}
if (maintenance) {
params.maintenance = true;
}
if (timeFrom || timeTo) {
params.lastChangeSince = timeFrom;
params.lastChangeTill = timeTo;
}
return this.request('trigger.get', params);
}
getEvents(objectids, timeFrom, timeTo, showEvents, limit) {
const params: any = {
output: 'extend',
time_from: timeFrom,
time_till: timeTo,
objectids: objectids,
select_acknowledges: 'extend',
selectHosts: 'extend',
value: showEvents,
};
if (limit) {
params.limit = limit;
params.sortfield = 'clock';
params.sortorder = 'DESC';
}
return this.request('event.get', params).then(utils.mustArray);
}
getEventsHistory(groupids, hostids, applicationids, options) {
const { timeFrom, timeTo, severities, limit, value } = options;
const params: any = {
output: 'extend',
time_from: timeFrom,
time_till: timeTo,
value: '1',
source: '0',
object: '0',
evaltype: '0',
sortfield: ['eventid'],
sortorder: 'DESC',
select_acknowledges: 'extend',
selectTags: 'extend',
selectSuppressionData: ['maintenanceid', 'suppress_until'],
groupids,
hostids,
applicationids,
};
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) {
const params = {
output: 'extend',
eventids: eventids,
preservekeys: true,
select_acknowledges: 'extend',
selectTags: 'extend',
sortfield: 'clock',
sortorder: 'DESC',
};
return this.request('event.get', params);
}
getEventAlerts(eventids) {
const params = {
eventids: eventids,
output: ['alertid', 'eventid', 'message', 'clock', 'error'],
selectUsers: true,
};
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) {
const params: any = {
output: 'extend',
itemids: itemids,
expandDescription: true,
expandData: true,
expandComment: true,
monitored: true,
skipDependent: true,
//only_true: true,
// filter: {
// value: 1
// },
selectLastEvent: 'extend',
};
if (timeFrom || timeTo) {
params.lastChangeSince = timeFrom;
params.lastChangeTill = timeTo;
}
return this.request('trigger.get', params);
}
getHostAlerts(hostids, applicationids, options) {
const { minSeverity, acknowledged, count, timeFrom, timeTo } = options;
const params: any = {
output: 'extend',
hostids: hostids,
min_severity: minSeverity,
filter: { value: 1 },
expandDescription: true,
expandData: true,
expandComment: true,
monitored: true,
skipDependent: true,
selectLastEvent: 'extend',
selectGroups: 'extend',
selectHosts: ['hostid', 'host', 'name'],
};
if (count && acknowledged !== 0 && acknowledged !== 1) {
params.countOutput = true;
}
if (applicationids && applicationids.length) {
params.applicationids = applicationids;
}
if (timeFrom || timeTo) {
params.lastChangeSince = timeFrom;
params.lastChangeTill = timeTo;
}
return this.request('trigger.get', params).then((triggers) => {
if (!count || acknowledged === 0 || acknowledged === 1) {
triggers = filterTriggersByAcknowledge(triggers, acknowledged);
if (count) {
triggers = triggers.length;
}
}
return triggers;
});
}
getProxies() {
const params = {
output: ['proxyid', 'host'],
};
return this.request('proxy.get', params);
}
getScripts(hostids: string[], options?: any): Promise<ZBXScript[]> {
const params: any = {
output: 'extend',
hostids,
};
return this.request('script.get', params).then(utils.mustArray);
}
executeScript(hostid: string, scriptid: string): Promise<APIExecuteScriptResponse> {
const params: any = {
hostid,
scriptid,
};
return this.request('script.execute', params);
}
getValueMappings() {
const params = {
output: 'extend',
selectMappings: 'extend',
};
return this.request('valuemap.get', params);
}
}
function filterTriggersByAcknowledge(triggers, acknowledged) {
if (acknowledged === 0) {
return _.filter(triggers, (trigger) => trigger.lastEvent.acknowledged === '0');
} else if (acknowledged === 1) {
return _.filter(triggers, (trigger) => trigger.lastEvent.acknowledged === '1');
} else {
return triggers;
}
}
function getSLAInterval(intervalMs) {
// Too many intervals may cause significant load on the database, so decrease number of resulting points
const resolutionRatio = 100;
const interval = roundInterval(intervalMs * resolutionRatio) / 1000;
return Math.max(interval, MIN_SLA_INTERVAL);
}
function buildSLAIntervals(timeRange, interval) {
let [timeFrom, timeTo] = timeRange;
const intervals = [];
// Align time range with calculated interval
timeFrom = Math.floor(timeFrom / interval) * interval;
timeTo = Math.ceil(timeTo / interval) * interval;
for (let i = timeFrom; i <= timeTo - interval; i += interval) {
intervals.push({
from: i,
to: i + interval,
});
}
return intervals;
}
// Define zabbix API exception type
export class ZabbixAPIError {
code: number;
name: string;
data: string;
message: string;
constructor(error: JSONRPCError) {
this.code = error.code || null;
this.name = error.message || '';
this.data = error.data || '';
this.message = 'Zabbix API Error: ' + this.name + ' ' + this.data;
}
toString() {
return this.name + ' ' + this.data;
}
}

View File

@@ -0,0 +1,123 @@
/**
* This module allows to deduplicate function calls with the same params and
* cache result of function call.
*/
export class CachingProxy {
cacheEnabled: boolean;
ttl: number;
cache: any;
promises: any;
constructor(cacheOptions) {
this.cacheEnabled = cacheOptions.enabled;
this.ttl = cacheOptions.ttl || 600000; // 10 minutes by default
// Internal objects for data storing
this.cache = {};
this.promises = {};
}
/**
* Check that result is present in the cache and is up to date or send request otherwise.
*/
cacheRequest(func, funcName, funcScope) {
return cacheRequest(func, funcName, funcScope, this);
}
/**
* Wrap request to prevent multiple calls with same params when request is waiting for response.
*/
proxify(func, funcName, funcScope) {
if (!this.promises[funcName]) {
this.promises[funcName] = {};
}
const promiseKeeper = this.promises[funcName];
return callOnce(func, promiseKeeper, funcScope);
}
proxifyWithCache(func, funcName, funcScope) {
const proxified = this.proxify(func, funcName, funcScope);
return this.cacheRequest(proxified, funcName, funcScope);
}
_isExpired(cacheObject) {
if (cacheObject) {
const object_age = Date.now() - cacheObject.timestamp;
return !(cacheObject.timestamp && object_age < this.ttl);
} else {
return true;
}
}
}
/**
* Wrap request to prevent multiple calls
* with same params when waiting for result.
*/
function callOnce(func, promiseKeeper, funcScope) {
// tslint:disable-next-line: only-arrow-functions
return function () {
const hash = getRequestHash(arguments);
if (!promiseKeeper[hash]) {
promiseKeeper[hash] = Promise.resolve(
func
.apply(funcScope, arguments)
.then((result) => {
promiseKeeper[hash] = null;
return result;
})
.catch((err) => {
promiseKeeper[hash] = null;
throw err;
})
);
}
return promiseKeeper[hash];
};
}
function cacheRequest(func, funcName, funcScope, self) {
// tslint:disable-next-line: only-arrow-functions
return function () {
if (!self.cache[funcName]) {
self.cache[funcName] = {};
}
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) => {
if (result !== undefined) {
cacheObject[hash] = {
value: result,
timestamp: Date.now(),
};
}
return result;
});
}
};
}
function getRequestHash(args) {
const argsJson = JSON.stringify(args);
return getHash(argsJson);
}
function getHash(str: string): number {
let hash = 0,
i,
chr,
len;
if (str.length !== 0) {
for (i = 0, len = str.length; i < len; i++) {
chr = str.charCodeAt(i);
hash = (hash << 5) - hash + chr;
hash |= 0; // Convert to 32bit integer
}
}
return hash;
}

View File

@@ -0,0 +1,24 @@
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>;
getGroups: (groupFilter?) => any;
getHosts: (groupFilter?, hostFilter?) => any;
getApps: (groupFilter?, hostFilter?, appFilter?) => any;
getItems: (groupFilter?, hostFilter?, appFilter?, itemTagFilter?, itemFilter?, options?) => any;
getSLA: (itservices, timeRange, target, options?) => any;
supportsApplications: () => boolean;
}

View File

@@ -0,0 +1,108 @@
import { Zabbix } from './zabbix';
jest.mock('@grafana/runtime', () => ({
getBackendSrv: () => ({
datasourceRequest: jest.fn().mockResolvedValue({data: {result: ''}}),
}),
getBackendSrv: () => ({
datasourceRequest: jest.fn().mockResolvedValue({ data: { result: '' } }),
fetch: () => ({
toPromise: () => jest.fn().mockResolvedValue({ data: { result: '' } })
}),
}),
}), {virtual: true});
describe('Zabbix', () => {
let ctx = {};
let zabbix;
let options = {
url: 'http://localhost',
username: 'zabbix',
password: 'zabbix',
};
beforeEach(() => {
ctx.options = options;
// ctx.backendSrv = mocks.backendSrvMock;
// ctx.datasourceSrv = mocks.datasourceSrvMock;
zabbix = new Zabbix(ctx.options);
});
describe('When querying proxies', () => {
beforeEach(() => {
zabbix.zabbixAPI.getProxies = jest.fn().mockResolvedValue([
{ host: 'proxy-foo', proxyid: '10101' },
{ host: 'proxy-bar', proxyid: '10102' },
]);
});
it("should return all proxies if filter set to /.*/", done => {
zabbix.getFilteredProxies('/.*/').then(proxies => {
expect(proxies).toMatchObject([{ host: 'proxy-foo' }, { host: 'proxy-bar' }]);
done();
});
});
it("should return matched proxies if regex filter used", done => {
zabbix.getFilteredProxies('/.*-foo/').then(proxies => {
expect(proxies).toMatchObject([{ host: 'proxy-foo' }]);
done();
});
});
it("should return matched proxies if simple filter used", done => {
zabbix.getFilteredProxies('proxy-bar').then(proxies => {
expect(proxies).toMatchObject([{ host: 'proxy-bar' }]);
done();
});
});
it("should return empty list for empty filter", done => {
zabbix.getFilteredProxies('').then(proxies => {
expect(proxies).toEqual([]);
done();
});
});
});
describe('When filtering triggers by proxy', () => {
beforeEach(() => {
zabbix.zabbixAPI.getProxies = jest.fn().mockResolvedValue([
{ host: 'proxy-foo', proxyid: '10101' },
{ host: 'proxy-bar', proxyid: '10102' },
]);
ctx.triggers = [
{ triggerid: '1', hosts: [{ name: 'backend01', proxy_hostid: '0' }] },
{ triggerid: '2', hosts: [{ name: 'backend02', proxy_hostid: '0' }] },
{ triggerid: '3', hosts: [{ name: 'frontend01', proxy_hostid: '10101' }] },
{ triggerid: '4', hosts: [{ name: 'frontend02', proxy_hostid: '10101' }] },
{ triggerid: '5', hosts: [{ name: 'db01', proxy_hostid: '10102' }] },
{ triggerid: '6', hosts: [{ name: 'db02', proxy_hostid: '10102' }] },
];
});
it("should return all triggers for empty filter", done => {
zabbix.filterTriggersByProxy(ctx.triggers, '').then(triggers => {
const triggerids = triggers.map(t => t.triggerid);
expect(triggerids).toEqual(['1', '2', '3', '4', '5', '6']);
done();
});
});
it("should return triggers belonging proxy matched regex filter", done => {
zabbix.filterTriggersByProxy(ctx.triggers, '/.*-foo/').then(triggers => {
const triggerids = triggers.map(t => t.triggerid);
expect(triggerids).toEqual(['3', '4']);
done();
});
});
it("should return triggers belonging proxy matched name filter", done => {
zabbix.filterTriggersByProxy(ctx.triggers, 'proxy-bar').then(triggers => {
const triggerids = triggers.map(t => t.triggerid);
expect(triggerids).toEqual(['5', '6']);
done();
});
});
});
});

View File

@@ -0,0 +1,645 @@
import _ from 'lodash';
// eslint-disable-next-line
import moment from 'moment';
import semver from 'semver';
import * as utils from '../utils';
import responseHandler from '../responseHandler';
import { CachingProxy } from './proxy/cachingProxy';
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 { joinTriggersWithEvents, joinTriggersWithProblems } from '../problemsHandler';
import { ProblemDTO, ZBXItem, ZBXItemTag } 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',
'getProxies',
'getEventAlerts',
'getExtendedEventData',
'getProblems',
'getEventsHistory',
'getTriggersByIds',
'getScripts',
'getValueMappings',
];
const REQUESTS_TO_CACHE = [
'getGroups',
'getHosts',
'getApps',
'getItems',
'getMacros',
'getItemsByIDs',
'getITService',
'getProxies',
'getValueMappings',
];
const REQUESTS_TO_BIND = [
'getHistory',
'getTrend',
'getMacros',
'getItemsByIDs',
'getEvents',
'getAlerts',
'getHostAlerts',
'getAcknowledges',
'getITService',
'acknowledgeEvent',
'getProxies',
'getEventAlerts',
'getExtendedEventData',
'getScripts',
'executeScript',
'getValueMappings',
];
export class Zabbix implements ZabbixConnector {
enableDirectDBConnection: boolean;
cachingProxy: CachingProxy;
zabbixAPI: ZabbixAPIConnector;
getHistoryDB: any;
dbConnector: any;
getTrendsDB: any;
version: string;
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>;
getValueMappings: () => Promise<any>;
constructor(options) {
const {
basicAuth,
withCredentials,
cacheTTL,
enableDirectDBConnection,
dbConnectionDatasourceId,
dbConnectionDatasourceName,
dbConnectionRetentionPolicy,
datasourceId,
} = options;
this.enableDirectDBConnection = enableDirectDBConnection;
// Initialize caching proxy for requests
const cacheOptions = {
enabled: true,
ttl: cacheTTL,
};
this.cachingProxy = new CachingProxy(cacheOptions);
this.zabbixAPI = new ZabbixAPIConnector(basicAuth, withCredentials, datasourceId);
this.proxifyRequests();
this.cacheRequests();
this.bindRequests();
if (enableDirectDBConnection) {
const connectorOptions: any = { dbConnectionRetentionPolicy };
this.initDBConnector(dbConnectionDatasourceId, dbConnectionDatasourceName, connectorOptions).then(() => {
this.getHistoryDB = this.cachingProxy.proxifyWithCache(
this.dbConnector.getHistory,
'getHistory',
this.dbConnector
);
this.getTrendsDB = this.cachingProxy.proxifyWithCache(
this.dbConnector.getTrends,
'getTrends',
this.dbConnector
);
});
}
}
initDBConnector(datasourceId, datasourceName, options) {
return DBConnector.loadDatasource(datasourceId, datasourceName).then((ds) => {
const connectorOptions: any = { datasourceId, datasourceName };
if (ds.type === 'influxdb') {
connectorOptions.retentionPolicy = options.dbConnectionRetentionPolicy;
this.dbConnector = new InfluxDBConnector(connectorOptions);
} else {
this.dbConnector = new SQLConnector(connectorOptions);
}
return this.dbConnector;
});
}
proxifyRequests() {
for (const request of REQUESTS_TO_PROXYFY) {
this.zabbixAPI[request] = this.cachingProxy.proxify(this.zabbixAPI[request], request, this.zabbixAPI);
}
}
cacheRequests() {
for (const request of REQUESTS_TO_CACHE) {
this.zabbixAPI[request] = this.cachingProxy.cacheRequest(this.zabbixAPI[request], request, this.zabbixAPI);
}
}
bindRequests() {
for (const request of REQUESTS_TO_BIND) {
this[request] = this.zabbixAPI[request].bind(this.zabbixAPI);
}
}
/**
* Perform test query for Zabbix API and external history DB.
* @return {object} test result object:
* ```
* {
* zabbixVersion,
* dbConnectorStatus: {
* dsType,
* dsName
* }
* }
* ```
*/
testDataSource() {
let zabbixVersion;
let dbConnectorStatus;
return this.getVersion()
.then((version) => {
zabbixVersion = version;
return this.getAllGroups();
})
.then(() => {
if (this.enableDirectDBConnection) {
return this.dbConnector.testDataSource();
} else {
return Promise.resolve();
}
})
.catch((error) => {
return Promise.reject(error);
})
.then((testResult) => {
if (testResult) {
dbConnectorStatus = {
dsType: this.dbConnector.datasourceTypeName,
dsName: this.dbConnector.datasourceName,
};
}
return { zabbixVersion, dbConnectorStatus };
});
}
async getVersion() {
if (!this.version) {
if (this.zabbixAPI.version) {
this.version = this.zabbixAPI.version;
} else {
this.version = await this.zabbixAPI.initVersion();
}
}
return this.version;
}
supportsApplications() {
const version = this.version || this.zabbixAPI.version;
return version ? semver.lt(version, '5.4.0') : true;
}
supportSLA() {
const version = this.version || this.zabbixAPI.version;
return version ? semver.gte(version, '6.0.0') : true;
}
isZabbix54OrHigher() {
const version = this.version || this.zabbixAPI.version;
return version ? semver.gte(version, '5.4.0') : false;
}
getItemsFromTarget(target, options) {
const parts = ['group', 'host', 'application', 'itemTag', 'item'];
const filters = _.map(parts, (p) => target[p].filter);
return this.getItems(...filters, options);
}
getHostsFromTarget(target) {
const parts = ['group', 'host', 'application'];
const filters = _.map(parts, (p) => target[p].filter);
return Promise.all([this.getHosts(...filters), this.getApps(...filters)]).then((results) => {
const hosts = results[0];
let apps: AppsResponse = results[1];
if (apps.appFilterEmpty) {
apps = [];
}
return [hosts, apps];
});
}
getAllGroups() {
return this.zabbixAPI.getGroups();
}
getGroups(groupFilter) {
return this.getAllGroups().then((groups) => findByFilter(groups, groupFilter));
}
/**
* Get list of host belonging to given groups.
*/
getAllHosts(groupFilter) {
return this.getGroups(groupFilter).then((groups) => {
const groupids = _.map(groups, 'groupid');
return this.zabbixAPI.getHosts(groupids);
});
}
getHosts(groupFilter?, hostFilter?) {
return this.getAllHosts(groupFilter).then((hosts) => findByFilter(hosts, hostFilter));
}
/**
* Get list of applications belonging to given groups and hosts.
*/
async getAllApps(groupFilter, hostFilter) {
await this.getVersion();
if (!this.supportsApplications()) {
return [];
}
return this.getHosts(groupFilter, hostFilter).then((hosts) => {
const hostids = _.map(hosts, 'hostid');
return this.zabbixAPI.getApps(hostids);
});
}
async getApps(groupFilter?, hostFilter?, appFilter?): Promise<AppsResponse> {
await this.getVersion();
const skipAppFilter = !this.supportsApplications();
return this.getHosts(groupFilter, hostFilter).then((hosts) => {
const hostids = _.map(hosts, 'hostid');
if (appFilter && !skipAppFilter) {
return this.zabbixAPI.getApps(hostids).then((apps) => filterByQuery(apps, appFilter));
} else {
const appsResponse: AppsResponse = hostids;
appsResponse.hostids = hostids;
appsResponse.appFilterEmpty = true;
return Promise.resolve(appsResponse);
}
});
}
async getItemTags(groupFilter?, hostFilter?, itemTagFilter?) {
const items = await this.getAllItems(groupFilter, hostFilter, null, null, {});
let tags: ZBXItemTag[] = _.flatten(
items.map((item: ZBXItem) => {
if (item.tags) {
return item.tags;
} else {
return [];
}
})
);
tags = _.uniqBy(tags, (t) => t.tag + t.value || '');
const tagsStr = tags.map((t) => ({ name: utils.itemTagToString(t) }));
return findByFilter(tagsStr, itemTagFilter);
}
async getAllItems(groupFilter, hostFilter, appFilter, itemTagFilter, options: any = {}) {
const apps = await this.getApps(groupFilter, hostFilter, appFilter);
let items: any[];
if (this.isZabbix54OrHigher()) {
items = await this.zabbixAPI.getItems(apps.hostids, undefined, options.itemtype);
if (itemTagFilter) {
items = filterItemsByTag(items, itemTagFilter);
}
} else {
if (apps.appFilterEmpty) {
items = await this.zabbixAPI.getItems(apps.hostids, undefined, options.itemtype);
} else {
const appids = _.map(apps, 'applicationid');
items = await this.zabbixAPI.getItems(undefined, appids, options.itemtype);
}
}
if (!options.showDisabledItems) {
items = _.filter(items, { status: '0' });
}
return await this.expandUserMacro(items, false);
}
expandUserMacro(items, isTriggerItem) {
const hostids = getHostIds(items);
return this.getMacros(hostids).then((macros) => {
_.forEach(items, (item) => {
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?, itemTagFilter?, itemFilter?, options = {}) {
return this.getAllItems(groupFilter, hostFilter, appFilter, itemTagFilter, options).then((items) =>
filterByQuery(items, itemFilter)
);
}
getItemValues(groupFilter?, hostFilter?, appFilter?, itemFilter?, options: any = {}) {
return this.getItems(groupFilter, hostFilter, appFilter, null, 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));
}
getProblems(groupFilter, hostFilter, appFilter, proxyFilter?, options?): Promise<ProblemDTO[]> {
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 && hostFilter !== '/.*/') {
query.hostids = _.map(filteredHosts, 'hostid');
}
if (groupFilter) {
query.groupids = _.map(filteredGroups, 'groupid');
}
return query;
})
.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) {
return this.getFilteredProxies(proxyFilter).then((proxies) => {
if (proxyFilter && proxyFilter !== '/.*/' && triggers) {
const proxy_ids = proxies.map((proxy) => proxy.proxyid);
triggers = triggers.filter((trigger) => {
for (let i = 0; i < trigger.hosts.length; i++) {
const host = trigger.hosts[i];
if (proxy_ids.includes(host.proxy_hostid)) {
return true;
}
}
return false;
});
}
return triggers;
});
}
getFilteredProxies(proxyFilter) {
return this.zabbixAPI.getProxies().then((proxies) => {
proxies.forEach((proxy) => (proxy.name = proxy.host));
return findByFilter(proxies, proxyFilter);
});
}
getHistoryTS(items, timeRange, request) {
const [timeFrom, timeTo] = timeRange;
if (this.enableDirectDBConnection) {
return this.getHistoryDB(items, timeFrom, timeTo, request).then((history) =>
responseHandler.dataResponseToTimeSeries(history, items, request)
);
} else {
return this.zabbixAPI
.getHistory(items, timeFrom, timeTo)
.then((history) => responseHandler.handleHistory(history, items));
}
}
getTrends(items, timeRange, request) {
const [timeFrom, timeTo] = timeRange;
if (this.enableDirectDBConnection) {
return this.getTrendsDB(items, timeFrom, timeTo, request).then((history) =>
responseHandler.dataResponseToTimeSeries(history, items, request)
);
} else {
const valueType = request.consolidateBy || request.valueType;
return this.zabbixAPI
.getTrend(items, timeFrom, timeTo)
.then((history) => responseHandler.handleTrends(history, items, valueType))
.then(responseHandler.sortTimeseries); // Sort trend data, issue #202
}
}
getHistoryText(items, timeRange, target) {
const [timeFrom, timeTo] = timeRange;
if (items.length) {
return this.zabbixAPI.getHistory(items, timeFrom, timeTo).then((history) => {
if (target.resultFormat === 'table') {
return responseHandler.handleHistoryAsTable(history, items, target);
} else {
return responseHandler.handleText(history, items, target);
}
});
} else {
return Promise.resolve([]);
}
}
async getSLA(itservices, timeRange, target, options) {
const itServiceIds = _.map(itservices, 'serviceid');
if (this.supportSLA()) {
const slaResponse = await this.zabbixAPI.getSLA60(itServiceIds, timeRange, options);
return _.map(itServiceIds, (serviceid) => {
const itservice = _.find(itservices, { serviceid: serviceid });
return responseHandler.handleSLAResponse(itservice, target.slaProperty, slaResponse);
});
}
const slaResponse = await this.zabbixAPI.getSLA(itServiceIds, timeRange, options);
return _.map(itServiceIds, (serviceid) => {
const itservice = _.find(itservices, { serviceid: serviceid });
return responseHandler.handleSLAResponse(itservice, target.slaProperty, slaResponse);
});
}
}
///////////////////////////////////////////////////////////////////////////////
/**
* Find group, host, app or item by given name.
* @param list list of groups, apps or other
* @param name visible name
* @return array with finded element or empty array
*/
function findByName(list, name) {
const finded = _.find(list, { name: name });
if (finded) {
return [finded];
} else {
return [];
}
}
/**
* Different hosts can contains applications and items with same name.
* For this reason use _.filter, which return all elements instead _.find,
* which return only first finded.
* @param {[type]} list list of elements
* @param {[type]} name app name
* @return {[type]} array with finded element or empty array
*/
function filterByName(list, name) {
const finded = _.filter(list, { name: name });
if (finded) {
return finded;
} else {
return [];
}
}
function filterByRegex(list, regex) {
const filterPattern = utils.buildRegex(regex);
return _.filter(list, (zbx_obj) => {
return filterPattern.test(zbx_obj.name);
});
}
function findByFilter(list, filter) {
if (utils.isRegex(filter)) {
return filterByRegex(list, filter);
} else {
return findByName(list, filter);
}
}
function filterByQuery(list, filter) {
if (utils.isRegex(filter)) {
return filterByRegex(list, filter);
} else {
return filterByName(list, filter);
}
}
function getHostIds(items) {
const hostIds = _.map(items, (item) => {
return _.map(item.hosts, 'hostid');
});
return _.uniq(_.flatten(hostIds));
}
function filterItemsByTag(items: any[], itemTagFilter: string) {
if (utils.isRegex(itemTagFilter)) {
const filterPattern = utils.buildRegex(itemTagFilter);
return items.filter((item) => {
if (item.tags) {
const tags: string[] = item.tags.map((t) => utils.itemTagToString(t));
return tags.some((tag) => {
return filterPattern.test(tag);
});
} else {
return false;
}
});
} else {
return items.filter((item) => {
if (item.tags) {
const tags: string[] = item.tags.map((t) => utils.itemTagToString(t));
return tags.includes(itemTagFilter);
} else {
return false;
}
});
}
}