Merge branch 'refactor-es6'. Datasource refactoring.

This commit is contained in:
Alexander Zobnin
2016-11-16 22:33:57 +03:00
16 changed files with 1322 additions and 1480 deletions

View File

@@ -33,7 +33,7 @@ module.exports = function(grunt) {
watch: { watch: {
rebuild_all: { rebuild_all: {
files: ['src/**/*', 'plugin.json'], files: ['src/**/*', 'plugin.json'],
tasks: ['default'], tasks: ['watchTask'],
options: {spawn: false} options: {spawn: false}
}, },
}, },
@@ -135,4 +135,14 @@ module.exports = function(grunt) {
'babel', 'babel',
'mochaTest' 'mochaTest'
]); ]);
grunt.registerTask('watchTask', [
'clean',
'copy:src_to_dist',
'copy:pluginDef',
'jshint',
'jscs',
'sass',
'babel:dist'
]);
}; };

View File

@@ -1,281 +0,0 @@
import _ from 'lodash';
import * as utils from './utils';
export default class DataProcessor {
/**
* Downsample datapoints series
*/
static downsampleSeries(datapoints, time_to, ms_interval, func) {
var downsampledSeries = [];
var 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 = [];
for (var 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 {
value_avg = points_num ? points_sum / points_num : 0;
if (func === "max") {
downsampledSeries.push([_.max(frame), timeWindow.to]);
}
else if (func === "min") {
downsampledSeries.push([_.min(frame), timeWindow.to]);
}
// avg by default
else {
downsampledSeries.push([value_avg, timeWindow.to]);
}
// Shift time window
timeWindow.to = timeWindow.from;
timeWindow.from -= ms_interval;
points_sum = 0;
points_num = 0;
frame = [];
// Process point again
i++;
}
}
return downsampledSeries.reverse();
}
/**
* Group points by given time interval
* datapoints: [[<value>, <unixtime>], ...]
*/
static groupBy(interval, groupByCallback, datapoints) {
var ms_interval = utils.parseInterval(interval);
// Calculate frame timestamps
var frames = _.groupBy(datapoints, function(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) {
return point[0];
});
return groupByCallback(points);
});
// Convert points to Grafana format
return sortByTime(_.map(grouped, function(value, timestamp) {
return [Number(value), Number(timestamp)];
}));
}
static sumSeries(timeseries) {
// Calculate new points for interpolation
var new_timestamps = _.uniq(_.map(_.flatten(timeseries, true), function(point) {
return point[1];
}));
new_timestamps = _.sortBy(new_timestamps);
var interpolated_timeseries = _.map(timeseries, function(series) {
var timestamps = _.map(series, function(point) {
return point[1];
});
var new_points = _.map(_.difference(new_timestamps, timestamps), function(timestamp) {
return [null, timestamp];
});
var 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--) {
sum = 0;
for (var j = interpolated_timeseries.length - 1; j >= 0; j--) {
sum += interpolated_timeseries[j][i][0];
}
new_timeseries.push([sum, new_timestamps[i]]);
}
return sortByTime(new_timeseries);
}
static limit(order, n, orderByFunc, timeseries) {
let orderByCallback = DataProcessor.aggregationFunctions[orderByFunc];
let sortByIteratee = (ts) => {
let values = _.map(ts.datapoints, (point) => {
return point[0];
});
return orderByCallback(values);
};
let sortedTimeseries = _.sortBy(timeseries, sortByIteratee);
if (order === 'bottom') {
return sortedTimeseries.slice(0, n);
} else {
return sortedTimeseries.slice(-n);
}
}
static AVERAGE(values) {
var sum = 0;
_.each(values, function(value) {
sum += value;
});
return sum / values.length;
}
static MIN(values) {
return _.min(values);
}
static MAX(values) {
return _.max(values);
}
static MEDIAN(values) {
var sorted = _.sortBy(values);
return sorted[Math.floor(sorted.length / 2)];
}
static setAlias(alias, timeseries) {
timeseries.target = alias;
return timeseries;
}
static scale(factor, datapoints) {
return _.map(datapoints, point => {
return [
point[0] * factor,
point[1]
];
});
}
static delta(datapoints) {
let newSeries = [];
let deltaValue;
for (var i = 1; i < datapoints.length; i++) {
deltaValue = datapoints[i][0] - datapoints[i - 1][0];
newSeries.push([deltaValue, datapoints[i][1]]);
}
return newSeries;
}
static groupByWrapper(interval, groupFunc, datapoints) {
var groupByCallback = DataProcessor.aggregationFunctions[groupFunc];
return DataProcessor.groupBy(interval, groupByCallback, datapoints);
}
static aggregateByWrapper(interval, aggregateFunc, datapoints) {
// Flatten all points in frame and then just use groupBy()
var flattenedPoints = _.flatten(datapoints, true);
var groupByCallback = DataProcessor.aggregationFunctions[aggregateFunc];
return DataProcessor.groupBy(interval, groupByCallback, flattenedPoints);
}
static aggregateWrapper(groupByCallback, interval, datapoints) {
var flattenedPoints = _.flatten(datapoints, true);
return DataProcessor.groupBy(interval, groupByCallback, flattenedPoints);
}
static get aggregationFunctions() {
return {
avg: this.AVERAGE,
min: this.MIN,
max: this.MAX,
median: this.MEDIAN
};
}
static get metricFunctions() {
return {
groupBy: this.groupByWrapper,
scale: this.scale,
delta: this.delta,
aggregateBy: this.aggregateByWrapper,
average: _.partial(this.aggregateWrapper, this.AVERAGE),
min: _.partial(this.aggregateWrapper, this.MIN),
max: _.partial(this.aggregateWrapper, this.MAX),
median: _.partial(this.aggregateWrapper, this.MEDIAN),
sumSeries: this.sumSeries,
top: _.partial(this.limit, 'top'),
bottom: _.partial(this.limit, 'bottom'),
setAlias: this.setAlias,
};
}
}
function sortByTime(series) {
return _.sortBy(series, function(point) {
return point[1];
});
}
/**
* Interpolate series with gaps
*/
function interpolateSeries(series) {
var left, right;
// Interpolate series
for (var i = series.length - 1; i >= 0; i--) {
if (!series[i][0]) {
left = findNearestLeft(series, series[i]);
right = findNearestRight(series, series[i]);
if (!left) {
left = right;
}
if (!right) {
right = left;
}
series[i][0] = linearInterpolation(series[i][1], left, right);
}
}
return series;
}
function linearInterpolation(timestamp, left, right) {
if (left[1] === right[1]) {
return (left[0] + right[0]) / 2;
} else {
return (left[0] + (right[0] - left[0]) / (right[1] - left[1]) * (timestamp - left[1]));
}
}
function findNearestRight(series, point) {
var point_index = _.indexOf(series, point);
var nearestRight;
for (var i = point_index; i < series.length; i++) {
if (series[i][0] !== null) {
return series[i];
}
}
return nearestRight;
}
function findNearestLeft(series, point) {
var point_index = _.indexOf(series, point);
var nearestLeft;
for (var i = point_index; i > 0; i--) {
if (series[i][0] !== null) {
return series[i];
}
}
return nearestLeft;
}

View File

@@ -0,0 +1,291 @@
import _ from 'lodash';
import * as utils from './utils';
/**
* Downsample datapoints series
*/
function downsampleSeries(datapoints, time_to, ms_interval, func) {
var downsampledSeries = [];
var 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 = [];
for (var 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 {
value_avg = points_num ? points_sum / points_num : 0;
if (func === "max") {
downsampledSeries.push([_.max(frame), timeWindow.to]);
}
else if (func === "min") {
downsampledSeries.push([_.min(frame), timeWindow.to]);
}
// avg by default
else {
downsampledSeries.push([value_avg, timeWindow.to]);
}
// Shift time window
timeWindow.to = timeWindow.from;
timeWindow.from -= ms_interval;
points_sum = 0;
points_num = 0;
frame = [];
// Process point again
i++;
}
}
return downsampledSeries.reverse();
}
/**
* Group points by given time interval
* datapoints: [[<value>, <unixtime>], ...]
*/
function groupBy(interval, groupByCallback, datapoints) {
var ms_interval = utils.parseInterval(interval);
// Calculate frame timestamps
var frames = _.groupBy(datapoints, function(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) {
return point[0];
});
return groupByCallback(points);
});
// Convert points to Grafana format
return sortByTime(_.map(grouped, function(value, timestamp) {
return [Number(value), Number(timestamp)];
}));
}
function sumSeries(timeseries) {
// Calculate new points for interpolation
var new_timestamps = _.uniq(_.map(_.flatten(timeseries, true), function(point) {
return point[1];
}));
new_timestamps = _.sortBy(new_timestamps);
var interpolated_timeseries = _.map(timeseries, function(series) {
var timestamps = _.map(series, function(point) {
return point[1];
});
var new_points = _.map(_.difference(new_timestamps, timestamps), function(timestamp) {
return [null, timestamp];
});
var 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--) {
sum = 0;
for (var j = interpolated_timeseries.length - 1; j >= 0; j--) {
sum += interpolated_timeseries[j][i][0];
}
new_timeseries.push([sum, new_timestamps[i]]);
}
return sortByTime(new_timeseries);
}
function limit(order, n, orderByFunc, timeseries) {
let orderByCallback = aggregationFunctions[orderByFunc];
let sortByIteratee = (ts) => {
let values = _.map(ts.datapoints, (point) => {
return point[0];
});
return orderByCallback(values);
};
let sortedTimeseries = _.sortBy(timeseries, sortByIteratee);
if (order === 'bottom') {
return sortedTimeseries.slice(0, n);
} else {
return sortedTimeseries.slice(-n);
}
}
function AVERAGE(values) {
var sum = 0;
_.each(values, function(value) {
sum += value;
});
return sum / values.length;
}
function MIN(values) {
return _.min(values);
}
function MAX(values) {
return _.max(values);
}
function MEDIAN(values) {
var sorted = _.sortBy(values);
return sorted[Math.floor(sorted.length / 2)];
}
function setAlias(alias, timeseries) {
timeseries.target = alias;
return timeseries;
}
function scale(factor, datapoints) {
return _.map(datapoints, point => {
return [
point[0] * factor,
point[1]
];
});
}
function delta(datapoints) {
let newSeries = [];
let deltaValue;
for (var i = 1; i < datapoints.length; i++) {
deltaValue = datapoints[i][0] - datapoints[i - 1][0];
newSeries.push([deltaValue, datapoints[i][1]]);
}
return newSeries;
}
function groupByWrapper(interval, groupFunc, datapoints) {
var groupByCallback = aggregationFunctions[groupFunc];
return groupBy(interval, groupByCallback, datapoints);
}
function aggregateByWrapper(interval, aggregateFunc, datapoints) {
// Flatten all points in frame and then just use groupBy()
var flattenedPoints = _.flatten(datapoints, true);
var groupByCallback = aggregationFunctions[aggregateFunc];
return groupBy(interval, groupByCallback, flattenedPoints);
}
function aggregateWrapper(groupByCallback, interval, datapoints) {
var flattenedPoints = _.flatten(datapoints, true);
return groupBy(interval, groupByCallback, flattenedPoints);
}
function sortByTime(series) {
return _.sortBy(series, function(point) {
return point[1];
});
}
/**
* Interpolate series with gaps
*/
function interpolateSeries(series) {
var left, right;
// Interpolate series
for (var i = series.length - 1; i >= 0; i--) {
if (!series[i][0]) {
left = findNearestLeft(series, series[i]);
right = findNearestRight(series, series[i]);
if (!left) {
left = right;
}
if (!right) {
right = left;
}
series[i][0] = linearInterpolation(series[i][1], left, right);
}
}
return series;
}
function linearInterpolation(timestamp, left, right) {
if (left[1] === right[1]) {
return (left[0] + right[0]) / 2;
} else {
return (left[0] + (right[0] - left[0]) / (right[1] - left[1]) * (timestamp - left[1]));
}
}
function findNearestRight(series, point) {
var point_index = _.indexOf(series, point);
var nearestRight;
for (var i = point_index; i < series.length; i++) {
if (series[i][0] !== null) {
return series[i];
}
}
return nearestRight;
}
function findNearestLeft(series, point) {
var point_index = _.indexOf(series, point);
var nearestLeft;
for (var i = point_index; i > 0; i--) {
if (series[i][0] !== null) {
return series[i];
}
}
return nearestLeft;
}
let metricFunctions = {
groupBy: groupByWrapper,
scale: scale,
delta: delta,
aggregateBy: aggregateByWrapper,
average: _.partial(aggregateWrapper, AVERAGE),
min: _.partial(aggregateWrapper, MIN),
max: _.partial(aggregateWrapper, MAX),
median: _.partial(aggregateWrapper, MEDIAN),
sumSeries: sumSeries,
top: _.partial(limit, 'top'),
bottom: _.partial(limit, 'bottom'),
setAlias: setAlias
};
let aggregationFunctions = {
avg: AVERAGE,
min: MIN,
max: MAX,
median: MEDIAN
};
export default {
downsampleSeries: downsampleSeries,
groupBy: groupBy,
AVERAGE: AVERAGE,
MIN: MIN,
MAX: MAX,
MEDIAN: MEDIAN,
get aggregationFunctions() {
return aggregationFunctions;
},
get metricFunctions() {
return metricFunctions;
}
};

View File

@@ -4,15 +4,17 @@ import * as dateMath from 'app/core/utils/datemath';
import * as utils from './utils'; import * as utils from './utils';
import * as migrations from './migrations'; import * as migrations from './migrations';
import * as metricFunctions from './metricFunctions'; import * as metricFunctions from './metricFunctions';
import DataProcessor from './DataProcessor'; import dataProcessor from './dataProcessor';
import './zabbixAPI.service.js'; import responseHandler from './responseHandler';
import './zabbixCache.service.js'; import './zabbix.js';
import './queryProcessor.service.js'; import {ZabbixAPIError} from './zabbixAPICore.service.js';
class ZabbixAPIDatasource { class ZabbixAPIDatasource {
/** @ngInject */ /** @ngInject */
constructor(instanceSettings, $q, templateSrv, alertSrv, zabbixAPIService, ZabbixCachingProxy, QueryProcessor) { constructor(instanceSettings, templateSrv, alertSrv, Zabbix) {
this.templateSrv = templateSrv;
this.alertSrv = alertSrv;
// General data source settings // General data source settings
this.name = instanceSettings.name; this.name = instanceSettings.name;
@@ -32,20 +34,7 @@ class ZabbixAPIDatasource {
var ttl = instanceSettings.jsonData.cacheTTL || '1h'; var ttl = instanceSettings.jsonData.cacheTTL || '1h';
this.cacheTTL = utils.parseInterval(ttl); this.cacheTTL = utils.parseInterval(ttl);
// Initialize Zabbix API this.zabbix = new Zabbix(this.url, this.username, this.password, this.basicAuth, this.withCredentials, this.cacheTTL);
var ZabbixAPI = zabbixAPIService;
this.zabbixAPI = new ZabbixAPI(this.url, this.username, this.password, this.basicAuth, this.withCredentials);
// Initialize cache service
this.zabbixCache = new ZabbixCachingProxy(this.zabbixAPI, this.cacheTTL);
// Initialize query builder
this.queryProcessor = new QueryProcessor(this.zabbixCache);
// Dependencies
this.q = $q;
this.templateSrv = templateSrv;
this.alertSrv = alertSrv;
// Use custom format for template variables // Use custom format for template variables
this.replaceTemplateVars = _.partial(replaceTemplateVars, this.templateSrv); this.replaceTemplateVars = _.partial(replaceTemplateVars, this.templateSrv);
@@ -69,12 +58,12 @@ class ZabbixAPIDatasource {
// Create request for each target // Create request for each target
var promises = _.map(options.targets, target => { var promises = _.map(options.targets, target => {
// Prevent changes of original object // Prevent changes of original object
target = _.cloneDeep(target); target = _.cloneDeep(target);
this.replaceTargetVariables(target, options);
// Metrics or Text query mode
if (target.mode !== 1) { if (target.mode !== 1) {
// Migrate old targets // Migrate old targets
target = migrations.migrate(target); target = migrations.migrate(target);
@@ -83,30 +72,9 @@ class ZabbixAPIDatasource {
return []; return [];
} }
// Replace templated variables
target.group.filter = this.replaceTemplateVars(target.group.filter, options.scopedVars);
target.host.filter = this.replaceTemplateVars(target.host.filter, options.scopedVars);
target.application.filter = this.replaceTemplateVars(target.application.filter, options.scopedVars);
target.item.filter = this.replaceTemplateVars(target.item.filter, options.scopedVars);
target.textFilter = this.replaceTemplateVars(target.textFilter, options.scopedVars);
_.forEach(target.functions, func => {
func.params = _.map(func.params, param => {
if (typeof param === 'number') {
return +this.templateSrv.replace(param.toString(), options.scopedVars);
} else {
return this.templateSrv.replace(param, options.scopedVars);
}
});
});
// Query numeric data
if (!target.mode || target.mode === 0) { if (!target.mode || target.mode === 0) {
return this.queryNumericData(target, timeFrom, timeTo, useTrends); return this.queryNumericData(target, timeFrom, timeTo, useTrends);
} } else if (target.mode === 2) {
// Query text data
else if (target.mode === 2) {
return this.queryTextData(target, timeFrom, timeTo); return this.queryTextData(target, timeFrom, timeTo);
} }
} }
@@ -118,71 +86,62 @@ class ZabbixAPIDatasource {
return []; return [];
} }
return this.zabbixAPI return this.zabbix.getSLA(target.itservice.serviceid, timeFrom, timeTo)
.getSLA(target.itservice.serviceid, timeFrom, timeTo)
.then(slaObject => { .then(slaObject => {
return this.queryProcessor return responseHandler.handleSLAResponse(target.itservice, target.slaProperty, slaObject);
.handleSLAResponse(target.itservice, target.slaProperty, slaObject);
}); });
} }
}); });
// Data for panel (all targets) // Data for panel (all targets)
return this.q.all(_.flatten(promises)) return Promise.all(_.flatten(promises))
.then(_.flatten) .then(_.flatten)
.then(timeseries_data => { .then(timeseries_data => {
return downsampleSeries(timeseries_data, options);
// Series downsampling })
var data = _.map(timeseries_data, timeseries => { .then(data => {
if (timeseries.datapoints.length > options.maxDataPoints) {
timeseries.datapoints = DataProcessor
.groupBy(options.interval, DataProcessor.AVERAGE, timeseries.datapoints);
}
return timeseries;
});
return { data: data }; return { data: data };
}); });
} }
queryNumericData(target, timeFrom, timeTo, useTrends) { queryNumericData(target, timeFrom, timeTo, useTrends) {
// Build query in asynchronous manner let options = {
return this.queryProcessor.build(target.group.filter, itemtype: 'num'
target.host.filter, };
target.application.filter, return this.zabbix.getItemsFromTarget(target, options)
target.item.filter,
'num')
.then(items => { .then(items => {
// Add hostname for items from multiple hosts let getHistoryPromise;
var addHostName = utils.isRegex(target.host.filter);
var getHistory;
// Use trends
if (useTrends) { if (useTrends) {
let valueType = this.getTrendValueType(target);
getHistoryPromise = this.zabbix.getTrend(items, timeFrom, timeTo)
.then(history => {
return responseHandler.handleTrends(history, items, valueType);
});
} else {
// Use history
getHistoryPromise = this.zabbix.getHistory(items, timeFrom, timeTo)
.then(history => {
return responseHandler.handleHistory(history, items);
});
}
return getHistoryPromise.then(timeseries_data => {
return this.applyDataProcessingFunctions(timeseries_data, target);
});
});
}
getTrendValueType(target) {
// Find trendValue() function and get specified trend value // Find trendValue() function and get specified trend value
var trendFunctions = _.map(metricFunctions.getCategories()['Trends'], 'name'); var trendFunctions = _.map(metricFunctions.getCategories()['Trends'], 'name');
var trendValueFunc = _.find(target.functions, func => { var trendValueFunc = _.find(target.functions, func => {
return _.includes(trendFunctions, func.def.name); return _.includes(trendFunctions, func.def.name);
}); });
var valueType = trendValueFunc ? trendValueFunc.params[0] : "avg"; return trendValueFunc ? trendValueFunc.params[0] : "avg";
getHistory = this.zabbixAPI
.getTrend(items, timeFrom, timeTo)
.then(history => {
return this.queryProcessor.handleTrends(history, items, addHostName, valueType);
});
} }
// Use history applyDataProcessingFunctions(timeseries_data, target) {
else {
getHistory = this.zabbixCache
.getHistory(items, timeFrom, timeTo)
.then(history => {
return this.queryProcessor.handleHistory(history, items, addHostName);
});
}
return getHistory.then(timeseries_data => {
let transformFunctions = bindFunctionDefs(target.functions, 'Transform'); let transformFunctions = bindFunctionDefs(target.functions, 'Transform');
let aggregationFunctions = bindFunctionDefs(target.functions, 'Aggregate'); let aggregationFunctions = bindFunctionDefs(target.functions, 'Aggregate');
let filterFunctions = bindFunctionDefs(target.functions, 'Filter'); let filterFunctions = bindFunctionDefs(target.functions, 'Filter');
@@ -209,33 +168,28 @@ class ZabbixAPIDatasource {
return _.includes(aggFuncNames, func.def.name); return _.includes(aggFuncNames, func.def.name);
}); });
timeseries_data = [ timeseries_data = [{
{
target: lastAgg.text, target: lastAgg.text,
datapoints: dp datapoints: dp
} }];
];
} }
// Apply alias functions // Apply alias functions
_.each(timeseries_data, sequence(aliasFunctions)); _.each(timeseries_data, sequence(aliasFunctions));
return timeseries_data; return timeseries_data;
});
});
} }
queryTextData(target, timeFrom, timeTo) { queryTextData(target, timeFrom, timeTo) {
return this.queryProcessor.build(target.group.filter, let options = {
target.host.filter, itemtype: 'text'
target.application.filter, };
target.item.filter, return this.zabbix.getItemsFromTarget(target, options)
'text')
.then(items => { .then(items => {
if (items.length) { if (items.length) {
return this.zabbixAPI.getHistory(items, timeFrom, timeTo) return this.zabbix.getHistory(items, timeFrom, timeTo)
.then(history => { .then(history => {
return this.queryProcessor.convertHistory(history, items, false, (point) => { return responseHandler.convertHistory(history, items, false, (point) => {
let value = point.value; let value = point.value;
// Regex-based extractor // Regex-based extractor
@@ -247,7 +201,7 @@ class ZabbixAPIDatasource {
}); });
}); });
} else { } else {
return this.q.when([]); return Promise.resolve([]);
} }
}); });
} }
@@ -257,37 +211,33 @@ class ZabbixAPIDatasource {
* @return {object} Connection status and Zabbix API version * @return {object} Connection status and Zabbix API version
*/ */
testDatasource() { testDatasource() {
return this.zabbixAPI.getVersion() let zabbixVersion;
return this.zabbix.getVersion()
.then(version => { .then(version => {
return this.zabbixAPI.login() zabbixVersion = version;
.then(auth => { return this.zabbix.login();
if (auth) { })
.then(() => {
return { return {
status: "success", status: "success",
title: "Success", title: "Success",
message: "Zabbix API version: " + version message: "Zabbix API version: " + zabbixVersion
}; };
} else { })
return { .catch(error => {
status: "error", if (error instanceof ZabbixAPIError) {
title: "Invalid user name or password",
message: "Zabbix API version: " + version
};
}
}, error => {
return { return {
status: "error", status: "error",
title: error.message, title: error.message,
message: error.data message: error.data
}; };
}); } else {
}, error => {
console.log(error);
return { return {
status: "error", status: "error",
title: "Connection failed", title: "Connection failed",
message: "Could not connect to given url" message: "Could not connect to given url"
}; };
}
}); });
} }
@@ -324,22 +274,22 @@ class ZabbixAPIDatasource {
if (template.app === '/.*/') { if (template.app === '/.*/') {
template.app = ''; template.app = '';
} }
result = this.queryProcessor.getItems(template.group, template.host, template.app); result = this.zabbix.getItems(template.group, template.host, template.app, template.item);
} else if (parts.length === 3) { } else if (parts.length === 3) {
// Get applications // Get applications
result = this.queryProcessor.getApps(template.group, template.host); result = this.zabbix.getApps(template.group, template.host, template.app);
} else if (parts.length === 2) { } else if (parts.length === 2) {
// Get hosts // Get hosts
result = this.queryProcessor.getHosts(template.group); result = this.zabbix.getHosts(template.group, template.host);
} else if (parts.length === 1) { } else if (parts.length === 1) {
// Get groups // Get groups
result = this.zabbixCache.getGroups(template.group); result = this.zabbix.getGroups(template.group);
} else { } else {
result = this.q.when([]); result = Promise.resolve([]);
} }
return result.then(metrics => { return result.then(metrics => {
return _.map(metrics, formatMetric); return metrics.map(formatMetric);
}); });
} }
@@ -356,15 +306,13 @@ class ZabbixAPIDatasource {
// Show all triggers // Show all triggers
var showTriggers = [0, 1]; var showTriggers = [0, 1];
var buildQuery = this.queryProcessor var getTriggers = this.zabbix
.buildTriggerQuery(this.replaceTemplateVars(annotation.group, {}), .getTriggers(this.replaceTemplateVars(annotation.group, {}),
this.replaceTemplateVars(annotation.host, {}), this.replaceTemplateVars(annotation.host, {}),
this.replaceTemplateVars(annotation.application, {})); this.replaceTemplateVars(annotation.application, {}),
showTriggers);
return buildQuery.then(query => { return getTriggers.then(triggers => {
return this.zabbixAPI
.getTriggers(query.groupids, query.hostids, query.applicationids, showTriggers)
.then(triggers => {
// Filter triggers by description // Filter triggers by description
if (utils.isRegex(annotation.trigger)) { if (utils.isRegex(annotation.trigger)) {
@@ -383,7 +331,7 @@ class ZabbixAPIDatasource {
}); });
var objectids = _.map(triggers, 'triggerid'); var objectids = _.map(triggers, 'triggerid');
return this.zabbixAPI return this.zabbix
.getEvents(objectids, timeFrom, timeTo, showOkEvents) .getEvents(objectids, timeFrom, timeTo, showOkEvents)
.then(events => { .then(events => {
var indexedTriggers = _.keyBy(triggers, 'triggerid'); var indexedTriggers = _.keyBy(triggers, 'triggerid');
@@ -415,6 +363,24 @@ class ZabbixAPIDatasource {
}); });
}); });
}); });
}
// Replace template variables
replaceTargetVariables(target, options) {
let parts = ['group', 'host', 'application', 'item'];
parts.forEach(p => {
target[p].filter = this.replaceTemplateVars(target[p].filter, options.scopedVars);
});
target.textFilter = this.replaceTemplateVars(target.textFilter, options.scopedVars);
_.forEach(target.functions, func => {
func.params = func.params.map(param => {
if (typeof param === 'number') {
return +this.templateSrv.replace(param.toString(), options.scopedVars);
} else {
return this.templateSrv.replace(param, options.scopedVars);
}
});
}); });
} }
@@ -428,7 +394,17 @@ function bindFunctionDefs(functionDefs, category) {
return _.map(aggFuncDefs, function(func) { return _.map(aggFuncDefs, function(func) {
var funcInstance = metricFunctions.createFuncInstance(func.def, func.params); var funcInstance = metricFunctions.createFuncInstance(func.def, func.params);
return funcInstance.bindFunction(DataProcessor.metricFunctions); return funcInstance.bindFunction(dataProcessor.metricFunctions);
});
}
function downsampleSeries(timeseries_data, options) {
return _.map(timeseries_data, timeseries => {
if (timeseries.datapoints.length > options.maxDataPoints) {
timeseries.datapoints = dataProcessor
.groupBy(options.interval, dataProcessor.AVERAGE, timeseries.datapoints);
}
return timeseries;
}); });
} }

View File

@@ -13,14 +13,9 @@ import './css/query-editor.css!';
export class ZabbixQueryController extends QueryCtrl { export class ZabbixQueryController extends QueryCtrl {
// ZabbixQueryCtrl constructor // ZabbixQueryCtrl constructor
constructor($scope, $injector, $rootScope, $sce, $q, templateSrv) { constructor($scope, $injector, $rootScope, $sce, templateSrv) {
// Call superclass constructor
super($scope, $injector); super($scope, $injector);
this.zabbix = this.datasource.zabbix;
this.zabbix = this.datasource.zabbixAPI;
this.cache = this.datasource.zabbixCache;
this.$q = $q;
// Use custom format for template variables // Use custom format for template variables
this.replaceTemplateVars = this.datasource.replaceTemplateVars; this.replaceTemplateVars = this.datasource.replaceTemplateVars;
@@ -106,85 +101,57 @@ export class ZabbixQueryController extends QueryCtrl {
} }
initFilters() { initFilters() {
var self = this; let itemtype = this.editorModes[this.target.mode].value;
var itemtype = self.editorModes[self.target.mode].value; return Promise.all([
return this.$q.when(this.suggestGroups()) this.suggestGroups(),
.then(() => {return self.suggestHosts();}) this.suggestHosts(),
.then(() => {return self.suggestApps();}) this.suggestApps(),
.then(() => {return self.suggestItems(itemtype);}); this.suggestItems(itemtype)
]);
} }
suggestGroups() { suggestGroups() {
var self = this; return this.zabbix.getAllGroups()
return this.cache.getGroups().then(groups => { .then(groups => {
self.metric.groupList = groups; this.metric.groupList = groups;
return groups; return groups;
}); });
} }
suggestHosts() { suggestHosts() {
var self = this; let groupFilter = this.replaceTemplateVars(this.target.group.filter);
var groupFilter = this.replaceTemplateVars(this.target.group.filter); return this.zabbix.getAllHosts(groupFilter)
return this.datasource.queryProcessor
.filterGroups(self.metric.groupList, groupFilter)
.then(groups => {
var groupids = _.map(groups, 'groupid');
return self.zabbix
.getHosts(groupids)
.then(hosts => { .then(hosts => {
self.metric.hostList = hosts; this.metric.hostList = hosts;
return hosts; return hosts;
}); });
});
} }
suggestApps() { suggestApps() {
var self = this; let groupFilter = this.replaceTemplateVars(this.target.group.filter);
var hostFilter = this.replaceTemplateVars(this.target.host.filter); let hostFilter = this.replaceTemplateVars(this.target.host.filter);
return this.datasource.queryProcessor return this.zabbix.getAllApps(groupFilter, hostFilter)
.filterHosts(self.metric.hostList, hostFilter)
.then(hosts => {
var hostids = _.map(hosts, 'hostid');
return self.zabbix
.getApps(hostids)
.then(apps => { .then(apps => {
return self.metric.appList = apps; this.metric.appList = apps;
}); return apps;
}); });
} }
suggestItems(itemtype = 'num') { suggestItems(itemtype = 'num') {
var self = this; let groupFilter = this.replaceTemplateVars(this.target.group.filter);
var appFilter = this.replaceTemplateVars(this.target.application.filter); let hostFilter = this.replaceTemplateVars(this.target.host.filter);
if (appFilter) { let appFilter = this.replaceTemplateVars(this.target.application.filter);
// Filter by applications let options = {
return this.datasource.queryProcessor itemtype: itemtype,
.filterApps(self.metric.appList, appFilter) showDisabledItems: this.target.options.showDisabledItems
.then(apps => { };
var appids = _.map(apps, 'applicationid');
return self.zabbix return this.zabbix
.getItems(undefined, appids, itemtype) .getAllItems(groupFilter, hostFilter, appFilter, options)
.then(items => { .then(items => {
if (!self.target.options.showDisabledItems) { this.metric.itemList = items;
items = _.filter(items, {'status': '0'});
}
self.metric.itemList = items;
return items; return items;
}); });
});
} else {
// Return all items belonged to selected hosts
var hostids = _.map(self.metric.hostList, 'hostid');
return self.zabbix
.getItems(hostids, undefined, itemtype)
.then(items => {
if (!self.target.options.showDisabledItems) {
items = _.filter(items, {'status': '0'});
}
self.metric.itemList = items;
return items;
});
}
} }
isRegex(str) { isRegex(str) {
@@ -331,7 +298,6 @@ export class ZabbixQueryController extends QueryCtrl {
this.panelCtrl.refresh(); this.panelCtrl.refresh();
} }
} }
} }
// Set templateUrl as static property // Set templateUrl as static property

View File

@@ -1,366 +0,0 @@
import angular from 'angular';
import _ from 'lodash';
import * as utils from './utils';
/** @ngInject */
angular.module('grafana.services').factory('QueryProcessor', function($q) {
class QueryProcessor {
constructor(zabbixCacheInstance) {
this.cache = zabbixCacheInstance;
this.$q = $q;
}
/**
* Build query in asynchronous manner
*/
build(groupFilter, hostFilter, appFilter, itemFilter, itemtype) {
var self = this;
if (this.cache._initialized) {
return this.$q.when(self.buildFromCache(groupFilter, hostFilter, appFilter, itemFilter, itemtype));
} else {
return this.cache.refresh().then(function() {
return self.buildFromCache(groupFilter, hostFilter, appFilter, itemFilter, itemtype);
});
}
}
/**
* Build trigger query in asynchronous manner
*/
buildTriggerQuery(groupFilter, hostFilter, appFilter) {
var self = this;
if (this.cache._initialized) {
return this.$q.when(self.buildTriggerQueryFromCache(groupFilter, hostFilter, appFilter));
} else {
return this.cache.refresh().then(function() {
return self.buildTriggerQueryFromCache(groupFilter, hostFilter, appFilter);
});
}
}
filterGroups(groups, groupFilter) {
return this.$q.when(
findByFilter(groups, groupFilter)
);
}
/**
* Get list of host belonging to given groups.
* @return list of hosts
*/
filterHosts(hosts, hostFilter) {
return this.$q.when(
findByFilter(hosts, hostFilter)
);
}
filterApps(apps, appFilter) {
return this.$q.when(
findByFilter(apps, appFilter)
);
}
/**
* Build query - convert target filters to array of Zabbix items
*/
buildFromCache(groupFilter, hostFilter, appFilter, itemFilter, itemtype, showDisabledItems) {
return this.getItems(groupFilter, hostFilter, appFilter, itemtype, showDisabledItems)
.then(items => {
return getByFilter(items, itemFilter);
});
}
getGroups() {
return this.cache.getGroups();
}
/**
* Get list of host belonging to given groups.
* @return list of hosts
*/
getHosts(groupFilter) {
var self = this;
return this.cache
.getGroups()
.then(groups => {
return findByFilter(groups, groupFilter);
})
.then(groups => {
var groupids = _.map(groups, 'groupid');
return self.cache.getHosts(groupids);
});
}
/**
* Get list of applications belonging to given groups and hosts.
* @return list of applications belonging to given hosts
*/
getApps(groupFilter, hostFilter) {
var self = this;
return this.getHosts(groupFilter)
.then(hosts => {
return findByFilter(hosts, hostFilter);
})
.then(hosts => {
var hostids = _.map(hosts, 'hostid');
return self.cache.getApps(hostids);
});
}
getItems(groupFilter, hostFilter, appFilter, itemtype, showDisabledItems) {
var self = this;
return this.getHosts(groupFilter)
.then(hosts => {
return findByFilter(hosts, hostFilter);
})
.then(hosts => {
var hostids = _.map(hosts, 'hostid');
if (appFilter) {
return self.cache
.getApps(hostids)
.then(apps => {
// Use getByFilter for proper item filtering
return getByFilter(apps, appFilter);
});
} else {
return {
appFilterEmpty: true,
hostids: hostids
};
}
})
.then(apps => {
if (apps.appFilterEmpty) {
return self.cache
.getItems(apps.hostids, undefined, itemtype)
.then(items => {
if (showDisabledItems) {
items = _.filter(items, {'status': '0'});
}
return items;
});
} else {
var appids = _.map(apps, 'applicationid');
return self.cache
.getItems(undefined, appids, itemtype)
.then(items => {
if (showDisabledItems) {
items = _.filter(items, {'status': '0'});
}
return items;
});
}
});
}
/**
* Build query - convert target filters to array of Zabbix items
*/
buildTriggerQueryFromCache(groupFilter, hostFilter, appFilter) {
var promises = [
this.cache.getGroups().then(function(groups) {
return _.filter(groups, function(group) {
if (utils.isRegex(groupFilter)) {
return utils.buildRegex(groupFilter).test(group.name);
} else {
return group.name === groupFilter;
}
});
}),
this.getHosts(groupFilter).then(function(hosts) {
return _.filter(hosts, function(host) {
if (utils.isRegex(hostFilter)) {
return utils.buildRegex(hostFilter).test(host.name);
} else {
return host.name === hostFilter;
}
});
}),
this.getApps(groupFilter, hostFilter).then(function(apps) {
return _.filter(apps, function(app) {
if (utils.isRegex(appFilter)) {
return utils.buildRegex(appFilter).test(app.name);
} else {
return app.name === appFilter;
}
});
})
];
return this.$q.all(promises).then(function(results) {
var filteredGroups = results[0];
var filteredHosts = results[1];
var filteredApps = results[2];
var query = {};
if (appFilter) {
query.applicationids = _.flatten(_.map(filteredApps, 'applicationid'));
}
if (hostFilter) {
query.hostids = _.map(filteredHosts, 'hostid');
}
if (groupFilter) {
query.groupids = _.map(filteredGroups, 'groupid');
}
return query;
});
}
/**
* Convert Zabbix API history.get response to Grafana format
*
* @return {Array} Array of timeseries in Grafana format
* {
* target: "Metric name",
* datapoints: [[<value>, <unixtime>], ...]
* }
*/
convertHistory(history, items, addHostName, convertPointCallback) {
/**
* Response should be in the format:
* data: [
* {
* target: "Metric name",
* datapoints: [[<value>, <unixtime>], ...]
* }, ...
* ]
*/
// Group history by itemid
var grouped_history = _.groupBy(history, 'itemid');
var hosts = _.uniq(_.flatten(_.map(items, 'hosts')),'hostid'); //uniq is needed to deduplicate
return _.map(grouped_history, function(hist, 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 {
target: alias,
datapoints: _.map(hist, convertPointCallback)
};
});
}
handleHistory(history, items, addHostName) {
return this.convertHistory(history, items, addHostName, convertHistoryPoint);
}
handleTrends(history, items, addHostName, valueType) {
var convertPointCallback = _.partial(convertTrendPoint, valueType);
return this.convertHistory(history, items, addHostName, convertPointCallback);
}
handleSLAResponse(itservice, slaProperty, slaObject) {
var targetSLA = slaObject[itservice.serviceid].sla[0];
if (slaProperty.property === 'status') {
var targetStatus = parseInt(slaObject[itservice.serviceid].status);
return {
target: itservice.name + ' ' + slaProperty.name,
datapoints: [
[targetStatus, targetSLA.to * 1000]
]
};
} else {
return {
target: itservice.name + ' ' + slaProperty.name,
datapoints: [
[targetSLA[slaProperty.property], targetSLA.from * 1000],
[targetSLA[slaProperty.property], targetSLA.to * 1000]
]
};
}
}
}
return QueryProcessor;
});
/**
* 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 undefined
*/
function findByName(list, name) {
var finded = _.find(list, {'name': name});
if (finded) {
return [finded];
} else {
return undefined;
}
}
/**
* 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 undefined
*/
function filterByName(list, name) {
var finded = _.filter(list, {'name': name});
if (finded) {
return finded;
} else {
return undefined;
}
}
function findByRegex(list, regex) {
var filterPattern = utils.buildRegex(regex);
return _.filter(list, function (zbx_obj) {
return filterPattern.test(zbx_obj.name);
});
}
function findByFilter(list, filter) {
if (utils.isRegex(filter)) {
return findByRegex(list, filter);
} else {
return findByName(list, filter);
}
}
function getByFilter(list, filter) {
if (utils.isRegex(filter)) {
return findByRegex(list, filter);
} else {
return filterByName(list, filter);
}
}
function convertHistoryPoint(point) {
// Value must be a number for properly work
return [
Number(point.value),
point.clock * 1000
];
}
function convertTrendPoint(valueType, point) {
var value;
switch (valueType) {
case "min":
value = point.value_min;
break;
case "max":
value = point.value_max;
break;
case "avg":
value = point.value_avg;
break;
default:
value = point.value_avg;
}
return [
Number(value),
point.clock * 1000
];
}

View File

@@ -0,0 +1,106 @@
import _ from 'lodash';
/**
* Convert Zabbix API history.get response to Grafana format
*
* @return {Array} Array of timeseries in Grafana format
* {
* target: "Metric name",
* datapoints: [[<value>, <unixtime>], ...]
* }
*/
function convertHistory(history, items, addHostName, convertPointCallback) {
/**
* Response should be in the format:
* data: [
* {
* target: "Metric name",
* datapoints: [[<value>, <unixtime>], ...]
* }, ...
* ]
*/
// Group history by itemid
var grouped_history = _.groupBy(history, 'itemid');
var hosts = _.uniqBy(_.flatten(_.map(items, 'hosts')), 'hostid'); //uniqBy is needed to deduplicate
return _.map(grouped_history, function(hist, 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 {
target: alias,
datapoints: _.map(hist, convertPointCallback)
};
});
}
function handleHistory(history, items, addHostName = true) {
return convertHistory(history, items, addHostName, convertHistoryPoint);
}
function handleTrends(history, items, valueType, addHostName = true) {
var convertPointCallback = _.partial(convertTrendPoint, valueType);
return convertHistory(history, items, addHostName, convertPointCallback);
}
function handleSLAResponse(itservice, slaProperty, slaObject) {
var targetSLA = slaObject[itservice.serviceid].sla[0];
if (slaProperty.property === 'status') {
var targetStatus = parseInt(slaObject[itservice.serviceid].status);
return {
target: itservice.name + ' ' + slaProperty.name,
datapoints: [
[targetStatus, targetSLA.to * 1000]
]
};
} else {
return {
target: itservice.name + ' ' + slaProperty.name,
datapoints: [
[targetSLA[slaProperty.property], targetSLA.from * 1000],
[targetSLA[slaProperty.property], targetSLA.to * 1000]
]
};
}
}
function convertHistoryPoint(point) {
// Value must be a number for properly work
return [
Number(point.value),
point.clock * 1000
];
}
function convertTrendPoint(valueType, point) {
var value;
switch (valueType) {
case "min":
value = point.value_min;
break;
case "max":
value = point.value_max;
break;
case "avg":
value = point.value_avg;
break;
default:
value = point.value_avg;
}
return [
Number(value),
point.clock * 1000
];
}
export default {
handleHistory: handleHistory,
convertHistory: convertHistory,
handleTrends: handleTrends,
handleSLAResponse: handleSLAResponse
};

View File

@@ -17,15 +17,11 @@ describe('ZabbixDatasource', () => {
trendsFrom: '7d' trendsFrom: '7d'
} }
}; };
ctx.$q = Q;
ctx.templateSrv = {}; ctx.templateSrv = {};
ctx.alertSrv = {}; ctx.alertSrv = {};
ctx.zabbixAPIService = () => {}; ctx.zabbix = () => {};
ctx.ZabbixCachingProxy = () => {};
ctx.QueryProcessor = () => {};
ctx.ds = new Datasource(ctx.instanceSettings, ctx.$q, ctx.templateSrv, ctx.alertSrv, ctx.ds = new Datasource(ctx.instanceSettings, ctx.templateSrv, ctx.alertSrv, ctx.zabbix);
ctx.zabbixAPIService, ctx.ZabbixCachingProxy, ctx.QueryProcessor);
}); });
describe('When querying data', () => { describe('When querying data', () => {
@@ -144,10 +140,7 @@ describe('ZabbixDatasource', () => {
describe('When invoking metricFindQuery()', () => { describe('When invoking metricFindQuery()', () => {
beforeEach(() => { beforeEach(() => {
ctx.ds.replaceTemplateVars = (str) => str; ctx.ds.replaceTemplateVars = (str) => str;
ctx.ds.zabbixCache = { ctx.ds.zabbix = {
getGroups: () => Q.when([])
};
ctx.ds.queryProcessor = {
getGroups: () => Q.when([]), getGroups: () => Q.when([]),
getHosts: () => Q.when([]), getHosts: () => Q.when([]),
getApps: () => Q.when([]), getApps: () => Q.when([]),
@@ -163,7 +156,7 @@ describe('ZabbixDatasource', () => {
{query: 'Back*', expect: 'Back*'} {query: 'Back*', expect: 'Back*'}
]; ];
let getGroups = sinon.spy(ctx.ds.zabbixCache, 'getGroups'); let getGroups = sinon.spy(ctx.ds.zabbix, 'getGroups');
for (const test of tests) { for (const test of tests) {
ctx.ds.metricFindQuery(test.query); ctx.ds.metricFindQuery(test.query);
expect(getGroups).to.have.been.calledWith(test.expect); expect(getGroups).to.have.been.calledWith(test.expect);
@@ -180,7 +173,7 @@ describe('ZabbixDatasource', () => {
{query: 'Back*.', expect: 'Back*'} {query: 'Back*.', expect: 'Back*'}
]; ];
let getHosts = sinon.spy(ctx.ds.queryProcessor, 'getHosts'); let getHosts = sinon.spy(ctx.ds.zabbix, 'getHosts');
for (const test of tests) { for (const test of tests) {
ctx.ds.metricFindQuery(test.query); ctx.ds.metricFindQuery(test.query);
expect(getHosts).to.have.been.calledWith(test.expect); expect(getHosts).to.have.been.calledWith(test.expect);
@@ -197,7 +190,7 @@ describe('ZabbixDatasource', () => {
{query: 'Back*.*.', expect: ['Back*', '/.*/']} {query: 'Back*.*.', expect: ['Back*', '/.*/']}
]; ];
let getApps = sinon.spy(ctx.ds.queryProcessor, 'getApps'); let getApps = sinon.spy(ctx.ds.zabbix, 'getApps');
for (const test of tests) { for (const test of tests) {
ctx.ds.metricFindQuery(test.query); ctx.ds.metricFindQuery(test.query);
expect(getApps).to.have.been.calledWith(test.expect[0], test.expect[1]); expect(getApps).to.have.been.calledWith(test.expect[0], test.expect[1]);
@@ -214,7 +207,7 @@ describe('ZabbixDatasource', () => {
{query: 'Back*.*.cpu.*', expect: ['Back*', '/.*/', 'cpu']} {query: 'Back*.*.cpu.*', expect: ['Back*', '/.*/', 'cpu']}
]; ];
let getItems = sinon.spy(ctx.ds.queryProcessor, 'getItems'); let getItems = sinon.spy(ctx.ds.zabbix, 'getItems');
for (const test of tests) { for (const test of tests) {
ctx.ds.metricFindQuery(test.query); ctx.ds.metricFindQuery(test.query);
expect(getItems) expect(getItems)
@@ -227,7 +220,7 @@ describe('ZabbixDatasource', () => {
it('should invoke method with proper arguments', (done) => { it('should invoke method with proper arguments', (done) => {
let query = '*.*'; let query = '*.*';
let getHosts = sinon.spy(ctx.ds.queryProcessor, 'getHosts'); let getHosts = sinon.spy(ctx.ds.zabbix, 'getHosts');
ctx.ds.metricFindQuery(query); ctx.ds.metricFindQuery(query);
expect(getHosts).to.have.been.calledWith('/.*/'); expect(getHosts).to.have.been.calledWith('/.*/');
done(); done();

View File

@@ -93,6 +93,25 @@ export function convertToZabbixAPIUrl(url) {
} }
} }
/**
* Wrap function to prevent multiple calls
* when waiting for result.
*/
export function callOnce(func, promiseKeeper) {
return function() {
if (!promiseKeeper) {
promiseKeeper = Promise.resolve(
func.apply(this, arguments)
.then(result => {
promiseKeeper = null;
return result;
})
);
}
return promiseKeeper;
};
}
// Fix for backward compatibility with lodash 2.4 // Fix for backward compatibility with lodash 2.4
if (!_.includes) { if (!_.includes) {
_.includes = _.contains; _.includes = _.contains;

View File

@@ -0,0 +1,216 @@
import angular from 'angular';
import _ from 'lodash';
import * as utils from './utils';
import './zabbixAPI.service.js';
import './zabbixCachingProxy.service.js';
// Use factory() instead service() for multiple data sources support.
// Each Zabbix data source instance should initialize its own API instance.
/** @ngInject */
function ZabbixFactory(zabbixAPIService, ZabbixCachingProxy) {
class Zabbix {
constructor(url, username, password, basicAuth, withCredentials, cacheTTL) {
// Initialize Zabbix API
var ZabbixAPI = zabbixAPIService;
this.zabbixAPI = new ZabbixAPI(url, username, password, basicAuth, withCredentials);
// Initialize caching proxy for requests
let cacheOptions = {
enabled: true,
ttl: cacheTTL
};
this.cachingProxy = new ZabbixCachingProxy(this.zabbixAPI, cacheOptions);
// Proxy methods
this.getHistory = this.cachingProxy.getHistory.bind(this.cachingProxy);
this.getTrend = this.zabbixAPI.getTrend.bind(this.zabbixAPI);
this.getEvents = this.zabbixAPI.getEvents.bind(this.zabbixAPI);
this.getAcknowledges = this.zabbixAPI.getAcknowledges.bind(this.zabbixAPI);
this.getSLA = this.zabbixAPI.getSLA.bind(this.zabbixAPI);
this.getVersion = this.zabbixAPI.getVersion.bind(this.zabbixAPI);
this.login = this.zabbixAPI.login.bind(this.zabbixAPI);
}
getItemsFromTarget(target, options) {
let parts = ['group', 'host', 'application', 'item'];
let filters = _.map(parts, p => target[p].filter);
return this.getItems(...filters, options);
}
getAllGroups() {
return this.cachingProxy.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 => {
let groupids = _.map(groups, 'groupid');
return this.cachingProxy.getHosts(groupids);
});
}
getHosts(groupFilter, hostFilter) {
return this.getAllHosts(groupFilter)
.then(hosts => findByFilter(hosts, hostFilter));
}
/**
* Get list of applications belonging to given groups and hosts.
*/
getAllApps(groupFilter, hostFilter) {
return this.getHosts(groupFilter, hostFilter)
.then(hosts => {
let hostids = _.map(hosts, 'hostid');
return this.cachingProxy.getApps(hostids);
});
}
getApps(groupFilter, hostFilter, appFilter) {
return this.getHosts(groupFilter, hostFilter)
.then(hosts => {
let hostids = _.map(hosts, 'hostid');
if (appFilter) {
return this.cachingProxy.getApps(hostids)
.then(apps => filterByQuery(apps, appFilter));
} else {
return {
appFilterEmpty: true,
hostids: hostids
};
}
});
}
getAllItems(groupFilter, hostFilter, appFilter, options = {}) {
return this.getApps(groupFilter, hostFilter, appFilter)
.then(apps => {
if (apps.appFilterEmpty) {
return this.cachingProxy.getItems(apps.hostids, undefined, options.itemtype);
} else {
let appids = _.map(apps, 'applicationid');
return this.cachingProxy.getItems(undefined, appids, options.itemtype);
}
})
.then(items => {
if (!options.showDisabledItems) {
items = _.filter(items, {'status': '0'});
}
return items;
});
}
getItems(groupFilter, hostFilter, appFilter, itemFilter, options = {}) {
return this.getAllItems(groupFilter, hostFilter, appFilter, options)
.then(items => filterByQuery(items, itemFilter));
}
/**
* Build query - convert target filters to array of Zabbix items
*/
getTriggers(groupFilter, hostFilter, appFilter, showTriggers) {
let promises = [
this.getGroups(groupFilter),
this.getHosts(groupFilter, hostFilter),
this.getApps(groupFilter, hostFilter, appFilter)
];
return Promise.all(promises)
.then(results => {
let filteredGroups = results[0];
let filteredHosts = results[1];
let filteredApps = results[2];
let query = {};
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 => {
return this.zabbixAPI
.getTriggers(query.groupids, query.hostids, query.applicationids, showTriggers);
});
}
}
return Zabbix;
}
angular
.module('grafana.services')
.factory('Zabbix', ZabbixFactory);
///////////////////////////////////////////////////////////////////////////////
/**
* 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 undefined
*/
function findByName(list, name) {
var finded = _.find(list, {'name': name});
if (finded) {
return [finded];
} else {
return undefined;
}
}
/**
* 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 undefined
*/
function filterByName(list, name) {
var finded = _.filter(list, {'name': name});
if (finded) {
return finded;
} else {
return undefined;
}
}
function filterByRegex(list, regex) {
var filterPattern = utils.buildRegex(regex);
return _.filter(list, function (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);
}
}

View File

@@ -4,7 +4,7 @@ import * as utils from './utils';
import './zabbixAPICore.service'; import './zabbixAPICore.service';
/** @ngInject */ /** @ngInject */
function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) { function ZabbixAPIServiceFactory(alertSrv, zabbixAPICoreService) {
/** /**
* Zabbix API Wrapper. * Zabbix API Wrapper.
@@ -25,8 +25,9 @@ function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) {
}; };
this.loginPromise = null; this.loginPromise = null;
this.loginErrorCount = 0;
this.maxLoginAttempts = 3;
this.$q = $q;
this.alertSrv = alertSrv; this.alertSrv = alertSrv;
this.zabbixAPICore = zabbixAPICoreService; this.zabbixAPICore = zabbixAPICoreService;
@@ -39,25 +40,22 @@ function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) {
////////////////////////// //////////////////////////
request(method, params) { request(method, params) {
var self = this; return this.zabbixAPICore.request(this.url, method, params, this.requestOptions, this.auth)
.catch(error => {
return this.zabbixAPICore
.request(this.url, method, params, this.requestOptions, this.auth)
.then((result) => {
return result;
}, (error) => {
// Handle API errors
if (isNotAuthorized(error.data)) { if (isNotAuthorized(error.data)) {
return self.loginOnce().then( // Handle auth errors
function() { this.loginErrorCount++;
return self.request(method, params); if (this.loginErrorCount > this.maxLoginAttempts) {
}, this.loginErrorCount = 0;
// Handle user.login method errors return null;
function(error) {
self.alertAPIError(error.data);
});
} else { } else {
this.alertSrv.set("Connection Error", error.data, 'error', 5000); return this.loginOnce()
.then(() => this.request(method, params));
}
} else {
// Handle API errors
let message = error.data ? error.data : error.statusText;
this.alertAPIError(message);
} }
}); });
} }
@@ -78,25 +76,16 @@ function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) {
* @return login promise * @return login promise
*/ */
loginOnce() { loginOnce() {
var self = this; if (!this.loginPromise) {
var deferred = this.$q.defer(); this.loginPromise = Promise.resolve(
if (!self.loginPromise) { this.login().then(auth => {
self.loginPromise = deferred.promise; this.auth = auth;
self.login().then( this.loginPromise = null;
function(auth) { return auth;
self.loginPromise = null; })
self.auth = auth;
deferred.resolve(auth);
},
function(error) {
self.loginPromise = null;
deferred.reject(error);
}
); );
} else {
return self.loginPromise;
} }
return deferred.promise; return this.loginPromise;
} }
/** /**
@@ -197,13 +186,16 @@ function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) {
} }
return this.request('item.get', params) return this.request('item.get', params)
.then(items => { .then(expandItems);
return _.forEach(items, item => {
function expandItems(items) {
items.forEach(item => {
item.item = item.name; item.item = item.name;
item.name = utils.expandItemName(item.item, item.key_); item.name = utils.expandItemName(item.item, item.key_);
return item; return item;
}); });
}); return items;
}
} }
getLastValue(itemid) { getLastValue(itemid) {
@@ -211,48 +203,42 @@ function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) {
output: ['lastvalue'], output: ['lastvalue'],
itemids: itemid itemids: itemid
}; };
return this.request('item.get', params).then(function(items) { return this.request('item.get', params)
if (items.length) { .then(items => items.length ? items[0].lastvalue : null);
return items[0].lastvalue;
} else {
return null;
}
});
} }
/** /**
* Perform history query from Zabbix API * Perform history query from Zabbix API
* *
* @param {Array} items Array of Zabbix item objects * @param {Array} items Array of Zabbix item objects
* @param {Number} time_from Time in seconds * @param {Number} timeFrom Time in seconds
* @param {Number} time_till Time in seconds * @param {Number} timeTill Time in seconds
* @return {Array} Array of Zabbix history objects * @return {Array} Array of Zabbix history objects
*/ */
getHistory(items, time_from, time_till) { getHistory(items, timeFrom, timeTill) {
var self = this;
// Group items by value type // Group items by value type and perform request for each value type
var grouped_items = _.groupBy(items, 'value_type'); let grouped_items = _.groupBy(items, 'value_type');
let promises = _.map(grouped_items, (items, value_type) => {
// Perform request for each value type let itemids = _.map(items, 'itemid');
return this.$q.all(_.map(grouped_items, function (items, value_type) { let params = {
var itemids = _.map(items, 'itemid');
var params = {
output: 'extend', output: 'extend',
history: value_type, history: value_type,
itemids: itemids, itemids: itemids,
sortfield: 'clock', sortfield: 'clock',
sortorder: 'ASC', sortorder: 'ASC',
time_from: time_from time_from: timeFrom
}; };
// Relative queries (e.g. last hour) don't include an end time // Relative queries (e.g. last hour) don't include an end time
if (time_till) { if (timeTill) {
params.time_till = time_till; params.time_till = timeTill;
} }
return self.request('history.get', params); return this.request('history.get', params);
})).then(_.flatten); });
return Promise.all(promises).then(_.flatten);
} }
/** /**
@@ -264,31 +250,30 @@ function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) {
* @param {Number} time_till Time in seconds * @param {Number} time_till Time in seconds
* @return {Array} Array of Zabbix trend objects * @return {Array} Array of Zabbix trend objects
*/ */
getTrend_ZBXNEXT1193(items, time_from, time_till) { getTrend_ZBXNEXT1193(items, timeFrom, timeTill) {
var self = this;
// Group items by value type // Group items by value type and perform request for each value type
var grouped_items = _.groupBy(items, 'value_type'); let grouped_items = _.groupBy(items, 'value_type');
let promises = _.map(grouped_items, (items, value_type) => {
// Perform request for each value type let itemids = _.map(items, 'itemid');
return this.$q.all(_.map(grouped_items, function (items, value_type) { let params = {
var itemids = _.map(items, 'itemid');
var params = {
output: 'extend', output: 'extend',
trend: value_type, trend: value_type,
itemids: itemids, itemids: itemids,
sortfield: 'clock', sortfield: 'clock',
sortorder: 'ASC', sortorder: 'ASC',
time_from: time_from time_from: timeFrom
}; };
// Relative queries (e.g. last hour) don't include an end time // Relative queries (e.g. last hour) don't include an end time
if (time_till) { if (timeTill) {
params.time_till = time_till; params.time_till = timeTill;
} }
return self.request('trend.get', params); return this.request('trend.get', params);
})).then(_.flatten); });
return Promise.all(promises).then(_.flatten);
} }
getTrend_30(items, time_from, time_till, value_type) { getTrend_30(items, time_from, time_till, value_type) {
@@ -312,7 +297,7 @@ function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) {
return self.request('trend.get', params); return self.request('trend.get', params);
} }
getITService(/* optional */ serviceids) { getITService(serviceids) {
var params = { var params = {
output: 'extend', output: 'extend',
serviceids: serviceids serviceids: serviceids
@@ -320,12 +305,12 @@ function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) {
return this.request('service.get', params); return this.request('service.get', params);
} }
getSLA(serviceids, from, to) { getSLA(serviceids, timeFrom, timeTo) {
var params = { var params = {
serviceids: serviceids, serviceids: serviceids,
intervals: [{ intervals: [{
from: from, from: timeFrom,
to: to to: timeTo
}] }]
}; };
return this.request('service.getsla', params); return this.request('service.getsla', params);
@@ -364,11 +349,11 @@ function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) {
return this.request('trigger.get', params); return this.request('trigger.get', params);
} }
getEvents(objectids, from, to, showEvents) { getEvents(objectids, timeFrom, timeTo, showEvents) {
var params = { var params = {
output: 'extend', output: 'extend',
time_from: from, time_from: timeFrom,
time_till: to, time_till: timeTo,
objectids: objectids, objectids: objectids,
select_acknowledges: 'extend', select_acknowledges: 'extend',
selectHosts: 'extend', selectHosts: 'extend',
@@ -389,10 +374,8 @@ function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) {
}; };
return this.request('event.get', params) return this.request('event.get', params)
.then(function (events) { .then(events => {
return _.filter(events, function(event) { return _.filter(events, (event) => event.acknowledges.length);
return event.acknowledges.length;
});
}); });
} }
@@ -411,4 +394,4 @@ function isNotAuthorized(message) {
angular angular
.module('grafana.services') .module('grafana.services')
.factory('zabbixAPIService', ZabbixAPIService); .factory('zabbixAPIService', ZabbixAPIServiceFactory);

View File

@@ -7,8 +7,7 @@ import angular from 'angular';
class ZabbixAPICoreService { class ZabbixAPICoreService {
/** @ngInject */ /** @ngInject */
constructor($q, backendSrv) { constructor(backendSrv) {
this.$q = $q;
this.backendSrv = backendSrv; this.backendSrv = backendSrv;
} }
@@ -17,8 +16,7 @@ class ZabbixAPICoreService {
* @return {object} response.result * @return {object} response.result
*/ */
request(api_url, method, params, options, auth) { request(api_url, method, params, options, auth) {
var deferred = this.$q.defer(); let requestData = {
var requestData = {
jsonrpc: '2.0', jsonrpc: '2.0',
method: method, method: method,
params: params, params: params,
@@ -27,20 +25,19 @@ class ZabbixAPICoreService {
if (auth === "") { if (auth === "") {
// Reject immediately if not authenticated // Reject immediately if not authenticated
deferred.reject({data: "Not authorised."}); return Promise.reject(new ZabbixAPIError({data: "Not authorised."}));
return deferred.promise;
} else if (auth) { } else if (auth) {
// Set auth parameter only if it needed // Set auth parameter only if it needed
requestData.auth = auth; requestData.auth = auth;
} }
var requestOptions = { let requestOptions = {
method: 'POST', method: 'POST',
url: api_url,
data: requestData,
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, }
url: api_url,
data: requestData
}; };
// Set request options for basic auth // Set request options for basic auth
@@ -51,24 +48,23 @@ class ZabbixAPICoreService {
requestOptions.headers.Authorization = options.basicAuth; requestOptions.headers.Authorization = options.basicAuth;
} }
this.backendSrv.datasourceRequest(requestOptions) return this.datasourceRequest(requestOptions);
.then((response) => {
// General connection issues
if (!response.data) {
deferred.reject(response);
} }
datasourceRequest(requestOptions) {
return this.backendSrv.datasourceRequest(requestOptions)
.then(response => {
if (!response.data) {
return Promise.reject(new ZabbixAPIError({data: "General Error, no data"}));
} else if (response.data.error) {
// Handle Zabbix API errors // Handle Zabbix API errors
else if (response.data.error) { return Promise.reject(new ZabbixAPIError(response.data.error));
deferred.reject(response.data.error);
} }
deferred.resolve(response.data.result); // Success
}, (error) => { return response.data.result;
deferred.reject(error.err);
}); });
return deferred.promise;
} }
/** /**
@@ -76,7 +72,7 @@ class ZabbixAPICoreService {
* @return {string} auth token * @return {string} auth token
*/ */
login(api_url, username, password, options) { login(api_url, username, password, options) {
var params = { let params = {
user: username, user: username,
password: password password: password
}; };
@@ -93,15 +89,18 @@ class ZabbixAPICoreService {
} }
// Define zabbix API exception type // Define zabbix API exception type
function ZabbixException(error) { export class ZabbixAPIError {
constructor(error) {
this.code = error.code; this.code = error.code;
this.errorType = error.message; this.name = error.data;
this.message = error.data; this.message = error.data;
this.data = error.data;
} }
ZabbixException.prototype.toString = function() { toString() {
return this.errorType + ": " + this.message; return this.name + ": " + this.message;
}; }
}
angular angular
.module('grafana.services') .module('grafana.services')

View File

@@ -1,240 +0,0 @@
import angular from 'angular';
import _ from 'lodash';
// Use factory() instead service() for multiple datasources support.
// Each datasource instance must initialize its own cache.
/** @ngInject */
angular.module('grafana.services').factory('ZabbixCachingProxy', function($q, $interval) {
class ZabbixCachingProxy {
constructor(zabbixAPI, ttl) {
this.zabbixAPI = zabbixAPI;
this.ttl = ttl;
this.$q = $q;
// Internal objects for data storing
this._groups = undefined;
this._hosts = undefined;
this._applications = undefined;
this._items = undefined;
this.storage = {
history: {},
trends: {}
};
// Check is a service initialized or not
this._initialized = undefined;
this.refreshPromise = false;
this.historyPromises = {};
// Wrap _refresh() method to call it once.
this.refresh = callOnce(this._refresh, this.refreshPromise);
// Update cache periodically
$interval(_.bind(this.refresh, this), this.ttl);
// Don't run duplicated history requests
this.getHistory = callHistoryOnce(_.bind(this.zabbixAPI.getHistory, this.zabbixAPI),
this.historyPromises);
// Don't run duplicated requests
this.groupPromises = {};
this.getGroupsOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getGroups, this.zabbixAPI),
this.groupPromises);
this.hostPromises = {};
this.getHostsOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getHosts, this.zabbixAPI),
this.hostPromises);
this.appPromises = {};
this.getAppsOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getApps, this.zabbixAPI),
this.appPromises);
this.itemPromises = {};
this.getItemsOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getItems, this.zabbixAPI),
this.itemPromises);
}
_refresh() {
var self = this;
var promises = [
this.zabbixAPI.getGroups()
];
return this.$q.all(promises).then(function(results) {
if (results.length) {
self._groups = results[0];
}
self._initialized = true;
});
}
getGroups() {
var self = this;
if (this._groups) {
return this.$q.when(self._groups);
} else {
return this.getGroupsOnce()
.then(groups => {
self._groups = groups;
return self._groups;
});
}
}
getHosts(groupids) {
//var self = this;
return this.getHostsOnce(groupids)
.then(hosts => {
// iss #196 - disable caching due performance issues
//self._hosts = _.union(self._hosts, hosts);
return hosts;
});
}
getApps(hostids) {
return this.getAppsOnce(hostids)
.then(apps => {
return apps;
});
}
getItems(hostids, appids, itemtype) {
//var self = this;
return this.getItemsOnce(hostids, appids, itemtype)
.then(items => {
// iss #196 - disable caching due performance issues
//self._items = _.union(self._items, items);
return items;
});
}
getHistoryFromCache(items, time_from, time_till) {
var deferred = this.$q.defer();
var historyStorage = this.storage.history;
var full_history;
var expired = _.filter(_.keyBy(items, 'itemid'), function(item, itemid) {
return !historyStorage[itemid];
});
if (expired.length) {
this.zabbixAPI.getHistory(expired, time_from, time_till).then(function(history) {
var grouped_history = _.groupBy(history, 'itemid');
_.forEach(expired, function(item) {
var itemid = item.itemid;
historyStorage[itemid] = item;
historyStorage[itemid].time_from = time_from;
historyStorage[itemid].time_till = time_till;
historyStorage[itemid].history = grouped_history[itemid];
});
full_history = _.map(items, function(item) {
return historyStorage[item.itemid].history;
});
deferred.resolve(_.flatten(full_history, true));
});
} else {
full_history = _.map(items, function(item) {
return historyStorage[item.itemid].history;
});
deferred.resolve(_.flatten(full_history, true));
}
return deferred.promise;
}
getHistoryFromAPI(items, time_from, time_till) {
return this.zabbixAPI.getHistory(items, time_from, time_till);
}
getHost(hostid) {
return _.find(this._hosts, {'hostid': hostid});
}
getItem(itemid) {
return _.find(this._items, {'itemid': itemid});
}
}
function callAPIRequestOnce(func, promiseKeeper) {
return function() {
var hash = getAPIRequestHash(arguments);
var deferred = $q.defer();
if (!promiseKeeper[hash]) {
promiseKeeper[hash] = deferred.promise;
func.apply(this, arguments).then(function(result) {
deferred.resolve(result);
promiseKeeper[hash] = null;
});
} else {
return promiseKeeper[hash];
}
return deferred.promise;
};
}
function callHistoryOnce(func, promiseKeeper) {
return function() {
var itemids = _.map(arguments[0], 'itemid');
var stamp = itemids.join() + arguments[1] + arguments[2];
var hash = stamp.getHash();
var deferred = $q.defer();
if (!promiseKeeper[hash]) {
promiseKeeper[hash] = deferred.promise;
func.apply(this, arguments).then(function(result) {
deferred.resolve(result);
promiseKeeper[hash] = null;
});
} else {
return promiseKeeper[hash];
}
return deferred.promise;
};
}
function callOnce(func, promiseKeeper) {
return function() {
var deferred = $q.defer();
if (!promiseKeeper) {
promiseKeeper = deferred.promise;
func.apply(this, arguments).then(function(result) {
deferred.resolve(result);
promiseKeeper = null;
});
} else {
return promiseKeeper;
}
return deferred.promise;
};
}
return ZabbixCachingProxy;
});
function getAPIRequestHash(args) {
var requestStamp = _.map(args, arg => {
if (arg === undefined) {
return 'undefined';
} else {
return arg.toString();
}
}).join();
return requestStamp.getHash();
}
String.prototype.getHash = function() {
var hash = 0, i, chr, len;
if (this.length === 0) {
return hash;
}
for (i = 0, len = this.length; i < len; i++) {
chr = this.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
};
// Fix for backward compatibility with lodash 2.4
if (!_.keyBy) {_.keyBy = _.indexBy;}

View File

@@ -0,0 +1,191 @@
import angular from 'angular';
import _ from 'lodash';
// Use factory() instead service() for multiple datasources support.
// Each datasource instance must initialize its own cache.
/** @ngInject */
function ZabbixCachingProxyFactory() {
class ZabbixCachingProxy {
constructor(zabbixAPI, cacheOptions) {
this.zabbixAPI = zabbixAPI;
this.cacheEnabled = cacheOptions.enabled;
this.ttl = cacheOptions.ttl || 600000; // 10 minutes by default
// Internal objects for data storing
this.cache = {
groups: {},
hosts: {},
applications: {},
items: {},
history: {},
trends: {}
};
this.historyPromises = {};
// Don't run duplicated history requests
this.getHistory = callAPIRequestOnce(_.bind(this.zabbixAPI.getHistory, this.zabbixAPI),
this.historyPromises, getHistoryRequestHash);
// Don't run duplicated requests
this.groupPromises = {};
this.getGroupsOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getGroups, this.zabbixAPI),
this.groupPromises, getRequestHash);
this.hostPromises = {};
this.getHostsOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getHosts, this.zabbixAPI),
this.hostPromises, getRequestHash);
this.appPromises = {};
this.getAppsOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getApps, this.zabbixAPI),
this.appPromises, getRequestHash);
this.itemPromises = {};
this.getItemsOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getItems, this.zabbixAPI),
this.itemPromises, getRequestHash);
}
isExpired(cacheObject) {
if (cacheObject) {
let object_age = Date.now() - cacheObject.timestamp;
return !(cacheObject.timestamp && object_age < this.ttl);
} else {
return true;
}
}
/**
* Check that result is present in cache and up to date
* or send request to API.
*/
proxyRequest(request, params, cacheObject) {
let hash = getRequestHash(params);
if (this.cacheEnabled && !this.isExpired(cacheObject[hash])) {
return Promise.resolve(cacheObject[hash].value);
} else {
return request(...params)
.then(result => {
cacheObject[hash] = {
value: result,
timestamp: Date.now()
};
return result;
});
}
}
getGroups() {
return this.proxyRequest(this.getGroupsOnce, [], this.cache.groups);
}
getHosts(groupids) {
return this.proxyRequest(this.getHostsOnce, [groupids], this.cache.hosts);
}
getApps(hostids) {
return this.proxyRequest(this.getAppsOnce, [hostids], this.cache.applications);
}
getItems(hostids, appids, itemtype) {
let params = [hostids, appids, itemtype];
return this.proxyRequest(this.getItemsOnce, params, this.cache.items);
}
getHistoryFromCache(items, time_from, time_till) {
var historyStorage = this.cache.history;
var full_history;
var expired = _.filter(_.keyBy(items, 'itemid'), (item, itemid) => {
return !historyStorage[itemid];
});
if (expired.length) {
return this.zabbixAPI.getHistory(expired, time_from, time_till).then(function(history) {
var grouped_history = _.groupBy(history, 'itemid');
_.forEach(expired, item => {
var itemid = item.itemid;
historyStorage[itemid] = item;
historyStorage[itemid].time_from = time_from;
historyStorage[itemid].time_till = time_till;
historyStorage[itemid].history = grouped_history[itemid];
});
full_history = _.map(items, item => {
return historyStorage[item.itemid].history;
});
return _.flatten(full_history, true);
});
} else {
full_history = _.map(items, function(item) {
return historyStorage[item.itemid].history;
});
return Promise.resolve(_.flatten(full_history, true));
}
}
getHistoryFromAPI(items, time_from, time_till) {
return this.zabbixAPI.getHistory(items, time_from, time_till);
}
}
return ZabbixCachingProxy;
}
angular
.module('grafana.services')
.factory('ZabbixCachingProxy', ZabbixCachingProxyFactory);
/**
* Wrap zabbix API request to prevent multiple calls
* with same params when waiting for result.
*/
function callAPIRequestOnce(func, promiseKeeper, argsHashFunc) {
return function() {
var hash = argsHashFunc(arguments);
if (!promiseKeeper[hash]) {
promiseKeeper[hash] = Promise.resolve(
func.apply(this, arguments)
.then(result => {
promiseKeeper[hash] = null;
return result;
})
);
}
return promiseKeeper[hash];
};
}
function getRequestHash(args) {
var requestStamp = _.map(args, arg => {
if (arg === undefined) {
return 'undefined';
} else {
if (_.isArray(arg)) {
return arg.sort().toString();
} else {
return arg.toString();
}
}
}).join();
return requestStamp.getHash();
}
function getHistoryRequestHash(args) {
let itemids = _.map(args[0], 'itemid');
let stamp = itemids.join() + args[1] + args[2];
return stamp.getHash();
}
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);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
}
return hash;
};
// Fix for backward compatibility with lodash 2.4
if (!_.keyBy) {_.keyBy = _.indexBy;}

View File

@@ -19,12 +19,11 @@ import '../datasource-zabbix/css/query-editor.css!';
class TriggerPanelEditorCtrl { class TriggerPanelEditorCtrl {
/** @ngInject */ /** @ngInject */
constructor($scope, $rootScope, $q, uiSegmentSrv, datasourceSrv, templateSrv, popoverSrv) { constructor($scope, $rootScope, uiSegmentSrv, datasourceSrv, templateSrv, popoverSrv) {
$scope.editor = this; $scope.editor = this;
this.panelCtrl = $scope.ctrl; this.panelCtrl = $scope.ctrl;
this.panel = this.panelCtrl.panel; this.panel = this.panelCtrl.panel;
this.$q = $q;
this.datasourceSrv = datasourceSrv; this.datasourceSrv = datasourceSrv;
this.templateSrv = templateSrv; this.templateSrv = templateSrv;
this.popoverSrv = popoverSrv; this.popoverSrv = popoverSrv;
@@ -62,8 +61,6 @@ class TriggerPanelEditorCtrl {
}; };
_.defaults(this, scopeDefaults); _.defaults(this, scopeDefaults);
var self = this;
// Get zabbix data sources // Get zabbix data sources
var datasources = _.filter(this.datasourceSrv.getMetricSources(), datasource => { var datasources = _.filter(this.datasourceSrv.getMetricSources(), datasource => {
return datasource.meta.id === 'alexanderzobnin-zabbix-datasource'; return datasource.meta.id === 'alexanderzobnin-zabbix-datasource';
@@ -75,59 +72,47 @@ class TriggerPanelEditorCtrl {
this.panel.datasource = this.datasources[0]; this.panel.datasource = this.datasources[0];
} }
// Load datasource // Load datasource
this.datasourceSrv.get(this.panel.datasource).then(function (datasource) { this.datasourceSrv.get(this.panel.datasource)
self.datasource = datasource; .then(datasource => {
self.initFilters(); this.datasource = datasource;
self.panelCtrl.refresh(); this.queryBuilder = datasource.queryBuilder;
this.initFilters();
this.panelCtrl.refresh();
}); });
} }
initFilters() { initFilters() {
var self = this; return Promise.all([
return this.$q this.suggestGroups(),
.when(this.suggestGroups()) this.suggestHosts(),
.then(() => {return self.suggestHosts();}) this.suggestApps()
.then(() => {return self.suggestApps();}); ]);
} }
suggestGroups() { suggestGroups() {
var self = this; return this.queryBuilder.getAllGroups()
return this.datasource.zabbixCache
.getGroups()
.then(groups => { .then(groups => {
self.metric.groupList = groups; this.metric.groupList = groups;
return groups; return groups;
}); });
} }
suggestHosts() { suggestHosts() {
var self = this; let groupFilter = this.datasource.replaceTemplateVars(this.panel.triggers.group.filter);
var groupFilter = this.datasource.replaceTemplateVars(this.panel.triggers.group.filter); return this.queryBuilder.getAllHosts(groupFilter)
return this.datasource.queryProcessor
.filterGroups(self.metric.groupList, groupFilter)
.then(groups => {
var groupids = _.map(groups, 'groupid');
return self.datasource.zabbixAPI
.getHosts(groupids)
.then(hosts => { .then(hosts => {
self.metric.hostList = hosts; this.metric.hostList = hosts;
return hosts; return hosts;
}); });
});
} }
suggestApps() { suggestApps() {
var self = this; let groupFilter = this.datasource.replaceTemplateVars(this.panel.triggers.group.filter);
var hostFilter = this.datasource.replaceTemplateVars(this.panel.triggers.host.filter); let hostFilter = this.datasource.replaceTemplateVars(this.panel.triggers.host.filter);
return this.datasource.queryProcessor return this.queryBuilder.getAllApps(groupFilter, hostFilter)
.filterHosts(self.metric.hostList, hostFilter)
.then(hosts => {
var hostids = _.map(hosts, 'hostid');
return self.datasource.zabbixAPI
.getApps(hostids)
.then(apps => { .then(apps => {
return self.metric.appList = apps; this.metric.appList = apps;
}); return apps;
}); });
} }

View File

@@ -61,7 +61,7 @@ var defaultTimeFormat = "DD MMM YYYY HH:mm:ss";
class TriggerPanelCtrl extends MetricsPanelCtrl { class TriggerPanelCtrl extends MetricsPanelCtrl {
/** @ngInject */ /** @ngInject */
constructor($scope, $injector, $q, $element, datasourceSrv, templateSrv, contextSrv) { constructor($scope, $injector, $element, datasourceSrv, templateSrv, contextSrv) {
super($scope, $injector); super($scope, $injector);
this.datasourceSrv = datasourceSrv; this.datasourceSrv = datasourceSrv;
this.templateSrv = templateSrv; this.templateSrv = templateSrv;
@@ -106,9 +106,9 @@ class TriggerPanelCtrl extends MetricsPanelCtrl {
var self = this; var self = this;
// Load datasource // Load datasource
return this.datasourceSrv.get(this.panel.datasource).then(datasource => { return this.datasourceSrv.get(this.panel.datasource)
var zabbix = datasource.zabbixAPI; .then(datasource => {
var queryProcessor = datasource.queryProcessor; var zabbix = datasource.zabbix;
var showEvents = self.panel.showEvents.value; var showEvents = self.panel.showEvents.value;
var triggerFilter = self.panel.triggers; var triggerFilter = self.panel.triggers;
@@ -117,13 +117,8 @@ class TriggerPanelCtrl extends MetricsPanelCtrl {
var hostFilter = datasource.replaceTemplateVars(triggerFilter.host.filter); var hostFilter = datasource.replaceTemplateVars(triggerFilter.host.filter);
var appFilter = datasource.replaceTemplateVars(triggerFilter.application.filter); var appFilter = datasource.replaceTemplateVars(triggerFilter.application.filter);
var buildQuery = queryProcessor.buildTriggerQuery(groupFilter, hostFilter, appFilter); var getTriggers = zabbix.getTriggers(groupFilter, hostFilter, appFilter, showEvents);
return buildQuery.then(query => { return getTriggers.then(triggers => {
return zabbix.getTriggers(query.groupids,
query.hostids,
query.applicationids,
showEvents)
.then(triggers => {
return _.map(triggers, trigger => { return _.map(triggers, trigger => {
let triggerObj = trigger; let triggerObj = trigger;
@@ -230,7 +225,6 @@ class TriggerPanelCtrl extends MetricsPanelCtrl {
}); });
}); });
}); });
});
} }
switchComment(trigger) { switchComment(trigger) {