From 145bc80e787b37fa8a3fd80b17bde4496bfa32c0 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 11 Nov 2016 18:40:28 +0300 Subject: [PATCH 01/18] Grunt: remove tests from watch task. --- Gruntfile.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index 025733b..fd3051f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -33,7 +33,7 @@ module.exports = function(grunt) { watch: { rebuild_all: { files: ['src/**/*', 'plugin.json'], - tasks: ['default'], + tasks: ['watchTask'], options: {spawn: false} }, }, @@ -135,4 +135,14 @@ module.exports = function(grunt) { 'babel', 'mochaTest' ]); + + grunt.registerTask('watchTask', [ + 'clean', + 'copy:src_to_dist', + 'copy:pluginDef', + 'jshint', + 'jscs', + 'sass', + 'babel:dist' + ]); }; From 062d975319fe4634e05f4b83e554b081438630a2 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 11 Nov 2016 19:38:23 +0300 Subject: [PATCH 02/18] Refactor: improved working with promises in testDatasource() method. --- src/datasource-zabbix/datasource.js | 49 ++++++++++++++--------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/src/datasource-zabbix/datasource.js b/src/datasource-zabbix/datasource.js index 583d4f3..79edb6a 100644 --- a/src/datasource-zabbix/datasource.js +++ b/src/datasource-zabbix/datasource.js @@ -8,6 +8,7 @@ import DataProcessor from './DataProcessor'; import './zabbixAPI.service.js'; import './zabbixCache.service.js'; import './queryProcessor.service.js'; +import {ZabbixAPIError} from './zabbixAPICore.service.js'; class ZabbixAPIDatasource { @@ -257,38 +258,34 @@ class ZabbixAPIDatasource { * @return {object} Connection status and Zabbix API version */ testDatasource() { + let zabbixVersion; return this.zabbixAPI.getVersion() - .then(version => { - return this.zabbixAPI.login() - .then(auth => { - if (auth) { - return { - status: "success", - title: "Success", - message: "Zabbix API version: " + version - }; - } else { - return { - status: "error", - title: "Invalid user name or password", - message: "Zabbix API version: " + version - }; - } - }, error => { - return { - status: "error", - title: error.message, - message: error.data - }; - }); - }, error => { - console.log(error); + .then(version => { + zabbixVersion = version; + return this.zabbixAPI.login(); + }) + .then(() => { + return { + status: "success", + title: "Success", + message: "Zabbix API version: " + zabbixVersion + }; + }) + .catch(error => { + if (error instanceof ZabbixAPIError) { + return { + status: "error", + title: error.message, + message: error.data + }; + } else { return { status: "error", title: "Connection failed", message: "Could not connect to given url" }; - }); + } + }); } //////////////// From 6579c6acbc494b52ac69c1dbb9384f169e9c17ed Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 11 Nov 2016 21:53:10 +0300 Subject: [PATCH 03/18] Refactor: zabbixAPI.service --- src/datasource-zabbix/zabbixAPI.service.js | 170 ++++++++++----------- 1 file changed, 77 insertions(+), 93 deletions(-) diff --git a/src/datasource-zabbix/zabbixAPI.service.js b/src/datasource-zabbix/zabbixAPI.service.js index b8f9edb..e81f2aa 100644 --- a/src/datasource-zabbix/zabbixAPI.service.js +++ b/src/datasource-zabbix/zabbixAPI.service.js @@ -4,7 +4,7 @@ import * as utils from './utils'; import './zabbixAPICore.service'; /** @ngInject */ -function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) { +function ZabbixAPIServiceFactory($q, alertSrv, zabbixAPICoreService) { /** * Zabbix API Wrapper. @@ -25,6 +25,8 @@ function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) { }; this.loginPromise = null; + this.loginErrorCount = 0; + this.maxLoginAttempts = 3; this.$q = $q; this.alertSrv = alertSrv; @@ -39,27 +41,24 @@ function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) { ////////////////////////// request(method, params) { - var self = this; - - return this.zabbixAPICore - .request(this.url, method, params, this.requestOptions, this.auth) - .then((result) => { - return result; - }, (error) => { - // Handle API errors - if (isNotAuthorized(error.data)) { - return self.loginOnce().then( - function() { - return self.request(method, params); - }, - // Handle user.login method errors - function(error) { - self.alertAPIError(error.data); - }); + return this.zabbixAPICore.request(this.url, method, params, this.requestOptions, this.auth) + .catch(error => { + if (isNotAuthorized(error.data)) { + // Handle auth errors + this.loginErrorCount++; + if (this.loginErrorCount > this.maxLoginAttempts) { + this.loginErrorCount = 0; + return null; } 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); + } + }); } alertAPIError(message, timeout = 5000) { @@ -78,25 +77,16 @@ function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) { * @return login promise */ loginOnce() { - var self = this; - var deferred = this.$q.defer(); - if (!self.loginPromise) { - self.loginPromise = deferred.promise; - self.login().then( - function(auth) { - self.loginPromise = null; - self.auth = auth; - deferred.resolve(auth); - }, - function(error) { - self.loginPromise = null; - deferred.reject(error); - } + if (!this.loginPromise) { + this.loginPromise = Promise.resolve( + this.login().then(auth => { + this.auth = auth; + this.loginPromise = null; + return auth; + }) ); - } else { - return self.loginPromise; } - return deferred.promise; + return this.loginPromise; } /** @@ -197,13 +187,16 @@ function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) { } return this.request('item.get', params) - .then(items => { - return _.forEach(items, item => { - item.item = item.name; - item.name = utils.expandItemName(item.item, item.key_); - return item; - }); + .then(expandItems); + + function expandItems(items) { + items.forEach(item => { + item.item = item.name; + item.name = utils.expandItemName(item.item, item.key_); + return item; }); + return items; + } } getLastValue(itemid) { @@ -211,48 +204,42 @@ function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) { output: ['lastvalue'], itemids: itemid }; - return this.request('item.get', params).then(function(items) { - if (items.length) { - return items[0].lastvalue; - } else { - return null; - } - }); + 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} time_from Time in seconds - * @param {Number} time_till Time in seconds + * @param {Number} timeFrom Time in seconds + * @param {Number} timeTill Time in seconds * @return {Array} Array of Zabbix history objects */ - getHistory(items, time_from, time_till) { - var self = this; + getHistory(items, timeFrom, timeTill) { - // Group items by value type - var grouped_items = _.groupBy(items, 'value_type'); - - // Perform request for each value type - return this.$q.all(_.map(grouped_items, function (items, value_type) { - var itemids = _.map(items, 'itemid'); - var params = { + // Group items by value type and perform request for each value type + let grouped_items = _.groupBy(items, 'value_type'); + let promises = _.map(grouped_items, (items, value_type) => { + let itemids = _.map(items, 'itemid'); + let params = { output: 'extend', history: value_type, itemids: itemids, sortfield: 'clock', sortorder: 'ASC', - time_from: time_from + time_from: timeFrom }; // Relative queries (e.g. last hour) don't include an end time - if (time_till) { - params.time_till = time_till; + if (timeTill) { + params.time_till = timeTill; } - return self.request('history.get', params); - })).then(_.flatten); + return this.request('history.get', params); + }); + + return Promise.all(promises).then(_.flatten); } /** @@ -264,31 +251,30 @@ function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) { * @param {Number} time_till Time in seconds * @return {Array} Array of Zabbix trend objects */ - getTrend_ZBXNEXT1193(items, time_from, time_till) { - var self = this; + getTrend_ZBXNEXT1193(items, timeFrom, timeTill) { - // Group items by value type - var grouped_items = _.groupBy(items, 'value_type'); - - // Perform request for each value type - return this.$q.all(_.map(grouped_items, function (items, value_type) { - var itemids = _.map(items, 'itemid'); - var params = { + // Group items by value type and perform request for each value type + let grouped_items = _.groupBy(items, 'value_type'); + let promises = _.map(grouped_items, (items, value_type) => { + let itemids = _.map(items, 'itemid'); + let params = { output: 'extend', trend: value_type, itemids: itemids, sortfield: 'clock', sortorder: 'ASC', - time_from: time_from + time_from: timeFrom }; // Relative queries (e.g. last hour) don't include an end time - if (time_till) { - params.time_till = time_till; + if (timeTill) { + params.time_till = timeTill; } - return self.request('trend.get', params); - })).then(_.flatten); + return this.request('trend.get', params); + }); + + return Promise.all(promises).then(_.flatten); } getTrend_30(items, time_from, time_till, value_type) { @@ -312,7 +298,7 @@ function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) { return self.request('trend.get', params); } - getITService(/* optional */ serviceids) { + getITService(serviceids) { var params = { output: 'extend', serviceids: serviceids @@ -320,12 +306,12 @@ function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) { return this.request('service.get', params); } - getSLA(serviceids, from, to) { + getSLA(serviceids, timeFrom, timeTo) { var params = { serviceids: serviceids, intervals: [{ - from: from, - to: to + from: timeFrom, + to: timeTo }] }; return this.request('service.getsla', params); @@ -364,11 +350,11 @@ function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) { return this.request('trigger.get', params); } - getEvents(objectids, from, to, showEvents) { + getEvents(objectids, timeFrom, timeTo, showEvents) { var params = { output: 'extend', - time_from: from, - time_till: to, + time_from: timeFrom, + time_till: timeTo, objectids: objectids, select_acknowledges: 'extend', selectHosts: 'extend', @@ -389,11 +375,9 @@ function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) { }; return this.request('event.get', params) - .then(function (events) { - return _.filter(events, function(event) { - return event.acknowledges.length; - }); - }); + .then(events => { + return _.filter(events, (event) => event.acknowledges.length); + }); } } @@ -411,4 +395,4 @@ function isNotAuthorized(message) { angular .module('grafana.services') - .factory('zabbixAPIService', ZabbixAPIService); + .factory('zabbixAPIService', ZabbixAPIServiceFactory); From 4a73957c162e0cf9b4485903ac337258c9ea9027 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 11 Nov 2016 21:56:21 +0300 Subject: [PATCH 04/18] Refactor: improved working with promises in zabbixAPICore.service. --- .../zabbixAPICore.service.js | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/datasource-zabbix/zabbixAPICore.service.js b/src/datasource-zabbix/zabbixAPICore.service.js index 2a7473c..70017d2 100644 --- a/src/datasource-zabbix/zabbixAPICore.service.js +++ b/src/datasource-zabbix/zabbixAPICore.service.js @@ -17,8 +17,7 @@ class ZabbixAPICoreService { * @return {object} response.result */ request(api_url, method, params, options, auth) { - var deferred = this.$q.defer(); - var requestData = { + let requestData = { jsonrpc: '2.0', method: method, params: params, @@ -27,20 +26,19 @@ class ZabbixAPICoreService { if (auth === "") { // Reject immediately if not authenticated - deferred.reject({data: "Not authorised."}); - return deferred.promise; + return Promise.reject(new ZabbixAPIError({data: "Not authorised."})); } else if (auth) { // Set auth parameter only if it needed requestData.auth = auth; } - var requestOptions = { + let requestOptions = { method: 'POST', + url: api_url, + data: requestData, headers: { 'Content-Type': 'application/json' - }, - url: api_url, - data: requestData + } }; // Set request options for basic auth @@ -51,24 +49,23 @@ class ZabbixAPICoreService { requestOptions.headers.Authorization = options.basicAuth; } - this.backendSrv.datasourceRequest(requestOptions) - .then((response) => { - // General connection issues - if (!response.data) { - deferred.reject(response); - } + return this.datasourceRequest(requestOptions); + } + + 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 - else if (response.data.error) { - deferred.reject(response.data.error); - } + return Promise.reject(new ZabbixAPIError(response.data.error)); + } - deferred.resolve(response.data.result); - }, (error) => { - deferred.reject(error.err); - }); - - return deferred.promise; + // Success + return response.data.result; + }); } /** @@ -76,7 +73,7 @@ class ZabbixAPICoreService { * @return {string} auth token */ login(api_url, username, password, options) { - var params = { + let params = { user: username, password: password }; @@ -93,15 +90,18 @@ class ZabbixAPICoreService { } // Define zabbix API exception type -function ZabbixException(error) { - this.code = error.code; - this.errorType = error.message; - this.message = error.data; -} +export class ZabbixAPIError { + constructor(error) { + this.code = error.code; + this.name = error.data; + this.message = error.data; + this.data = error.data; + } -ZabbixException.prototype.toString = function() { - return this.errorType + ": " + this.message; -}; + toString() { + return this.name + ": " + this.message; + } +} angular .module('grafana.services') From 2e57b9b166a452552c666ba1d7439a6f390ef69f Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Sat, 12 Nov 2016 18:04:29 +0300 Subject: [PATCH 05/18] Refactor: queryProcessor.service. --- src/datasource-zabbix/datasource.js | 19 +- src/datasource-zabbix/query.controller.js | 92 +++---- .../queryProcessor.service.js | 260 +++++++----------- src/panel-triggers/editor.js | 62 ++--- 4 files changed, 166 insertions(+), 267 deletions(-) diff --git a/src/datasource-zabbix/datasource.js b/src/datasource-zabbix/datasource.js index 79edb6a..fc61f52 100644 --- a/src/datasource-zabbix/datasource.js +++ b/src/datasource-zabbix/datasource.js @@ -146,12 +146,10 @@ class ZabbixAPIDatasource { } queryNumericData(target, timeFrom, timeTo, useTrends) { - // Build query in asynchronous manner - return this.queryProcessor.build(target.group.filter, - target.host.filter, - target.application.filter, - target.item.filter, - 'num') + let options = { + itemtype: 'num' + }; + return this.queryProcessor.build(target, options) .then(items => { // Add hostname for items from multiple hosts var addHostName = utils.isRegex(target.host.filter); @@ -227,11 +225,10 @@ class ZabbixAPIDatasource { } queryTextData(target, timeFrom, timeTo) { - return this.queryProcessor.build(target.group.filter, - target.host.filter, - target.application.filter, - target.item.filter, - 'text') + let options = { + itemtype: 'text' + }; + return this.queryProcessor.build(target, options) .then(items => { if (items.length) { return this.zabbixAPI.getHistory(items, timeFrom, timeTo) diff --git a/src/datasource-zabbix/query.controller.js b/src/datasource-zabbix/query.controller.js index 5536d48..d8e6623 100644 --- a/src/datasource-zabbix/query.controller.js +++ b/src/datasource-zabbix/query.controller.js @@ -20,6 +20,7 @@ export class ZabbixQueryController extends QueryCtrl { this.zabbix = this.datasource.zabbixAPI; this.cache = this.datasource.zabbixCache; + this.queryProcessor = this.datasource.queryProcessor; this.$q = $q; // Use custom format for template variables @@ -115,76 +116,47 @@ export class ZabbixQueryController extends QueryCtrl { } suggestGroups() { - var self = this; - return this.cache.getGroups().then(groups => { - self.metric.groupList = groups; + return this.queryProcessor.getAllGroups() + .then(groups => { + this.metric.groupList = groups; return groups; }); } suggestHosts() { - var self = this; - var groupFilter = this.replaceTemplateVars(this.target.group.filter); - return this.datasource.queryProcessor - .filterGroups(self.metric.groupList, groupFilter) - .then(groups => { - var groupids = _.map(groups, 'groupid'); - return self.zabbix - .getHosts(groupids) - .then(hosts => { - self.metric.hostList = hosts; - return hosts; - }); - }); + let groupFilter = this.replaceTemplateVars(this.target.group.filter); + return this.queryProcessor.getAllHosts(groupFilter) + .then(hosts => { + this.metric.hostList = hosts; + return hosts; + }); } suggestApps() { - var self = this; - var hostFilter = this.replaceTemplateVars(this.target.host.filter); - return this.datasource.queryProcessor - .filterHosts(self.metric.hostList, hostFilter) - .then(hosts => { - var hostids = _.map(hosts, 'hostid'); - return self.zabbix - .getApps(hostids) - .then(apps => { - return self.metric.appList = apps; - }); - }); + let groupFilter = this.replaceTemplateVars(this.target.group.filter); + let hostFilter = this.replaceTemplateVars(this.target.host.filter); + return this.queryProcessor.getAllApps(groupFilter, hostFilter) + .then(apps => { + this.metric.appList = apps; + return apps; + }); } - suggestItems(itemtype='num') { - var self = this; - var appFilter = this.replaceTemplateVars(this.target.application.filter); - if (appFilter) { - // Filter by applications - return this.datasource.queryProcessor - .filterApps(self.metric.appList, appFilter) - .then(apps => { - var appids = _.map(apps, 'applicationid'); - return self.zabbix - .getItems(undefined, appids, itemtype) - .then(items => { - if (!self.target.options.showDisabledItems) { - items = _.filter(items, {'status': '0'}); - } - self.metric.itemList = 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; - }); - } + suggestItems(itemtype = 'num') { + let groupFilter = this.replaceTemplateVars(this.target.group.filter); + let hostFilter = this.replaceTemplateVars(this.target.host.filter); + let appFilter = this.replaceTemplateVars(this.target.application.filter); + let options = { + itemtype: itemtype, + showDisabledItems: this.target.options.showDisabledItems + }; + + return this.queryProcessor + .getAllItems(groupFilter, hostFilter, appFilter, options) + .then(items => { + this.metric.itemList = items; + return items; + }); } isRegex(str) { diff --git a/src/datasource-zabbix/queryProcessor.service.js b/src/datasource-zabbix/queryProcessor.service.js index d151ff0..90bb6a5 100644 --- a/src/datasource-zabbix/queryProcessor.service.js +++ b/src/datasource-zabbix/queryProcessor.service.js @@ -2,197 +2,137 @@ import angular from 'angular'; import _ from 'lodash'; import * as utils from './utils'; -/** @ngInject */ -angular.module('grafana.services').factory('QueryProcessor', function($q) { +function QueryProcessorFactory() { class QueryProcessor { constructor(zabbixCacheInstance) { this.cache = zabbixCacheInstance; - this.$q = $q; + } + + initializeCache() { + if (this.cache._initialized) { + return Promise.resolve(); + } else { + return this.cache.refresh(); + } } /** - * Build query in asynchronous manner + * Build query - convert target filters to array of Zabbix items */ - 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(target, options) { + function getFiltersFromTarget(target) { + let parts = ['group', 'host', 'application', 'item']; + return _.map(parts, p => target[p].filter); } + + return this.initializeCache() + .then(() => { + return this.getItems(...getFiltersFromTarget(target), options); + }); } /** * 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); - }); - } + return this.initializeCache() + .then(() => { + return this.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() { + getAllGroups() { return this.cache.getGroups(); } + getGroups(groupFilter) { + return this.getAllGroups() + .then(groups => findByFilter(groups, groupFilter)); + } + /** * 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); - }); + getAllHosts(groupFilter) { + return this.getGroups(groupFilter) + .then(groups => { + let groupids = _.map(groups, 'groupid'); + return this.cache.getHosts(groupids); + }); + } + + getHosts(groupFilter, hostFilter) { + return this.getAllHosts(groupFilter) + .then(hosts => findByFilter(hosts, hostFilter)); } /** * 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); - }); + getAllApps(groupFilter, hostFilter) { + return this.getHosts(groupFilter, hostFilter) + .then(hosts => { + let hostids = _.map(hosts, 'hostid'); + return this.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; - }); - } - }); + getApps(groupFilter, hostFilter, appFilter) { + return this.getHosts(groupFilter, hostFilter) + .then(hosts => { + let hostids = _.map(hosts, 'hostid'); + if (appFilter) { + return this.cache.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.cache.getItems(apps.hostids, undefined, options.itemtype); + } else { + let appids = _.map(apps, 'applicationid'); + return this.cache.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 */ 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; - } - }); - }) + let promises = [ + this.getGroups(groupFilter), + this.getHosts(groupFilter, hostFilter), + this.getApps(groupFilter, hostFilter, appFilter) ]; - return this.$q.all(promises).then(function(results) { - var filteredGroups = results[0]; - var filteredHosts = results[1]; - var filteredApps = results[2]; - var query = {}; + 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')); @@ -278,7 +218,11 @@ angular.module('grafana.services').factory('QueryProcessor', function($q) { } return QueryProcessor; -}); +} + +angular + .module('grafana.services') + .factory('QueryProcessor', QueryProcessorFactory); /** * Find group, host, app or item by given name. @@ -312,7 +256,7 @@ function filterByName(list, name) { } } -function findByRegex(list, regex) { +function filterByRegex(list, regex) { var filterPattern = utils.buildRegex(regex); return _.filter(list, function (zbx_obj) { return filterPattern.test(zbx_obj.name); @@ -321,15 +265,15 @@ function findByRegex(list, regex) { function findByFilter(list, filter) { if (utils.isRegex(filter)) { - return findByRegex(list, filter); + return filterByRegex(list, filter); } else { return findByName(list, filter); } } -function getByFilter(list, filter) { +function filterByQuery(list, filter) { if (utils.isRegex(filter)) { - return findByRegex(list, filter); + return filterByRegex(list, filter); } else { return filterByName(list, filter); } diff --git a/src/panel-triggers/editor.js b/src/panel-triggers/editor.js index 0c48fec..6c6377a 100644 --- a/src/panel-triggers/editor.js +++ b/src/panel-triggers/editor.js @@ -62,8 +62,6 @@ class TriggerPanelEditorCtrl { }; _.defaults(this, scopeDefaults); - var self = this; - // Get zabbix data sources var datasources = _.filter(this.datasourceSrv.getMetricSources(), datasource => { return datasource.meta.id === 'alexanderzobnin-zabbix-datasource'; @@ -75,10 +73,12 @@ class TriggerPanelEditorCtrl { this.panel.datasource = this.datasources[0]; } // Load datasource - this.datasourceSrv.get(this.panel.datasource).then(function (datasource) { - self.datasource = datasource; - self.initFilters(); - self.panelCtrl.refresh(); + this.datasourceSrv.get(this.panel.datasource) + .then(datasource => { + this.datasource = datasource; + this.queryProcessor = datasource.queryProcessor; + this.initFilters(); + this.panelCtrl.refresh(); }); } @@ -91,44 +91,30 @@ class TriggerPanelEditorCtrl { } suggestGroups() { - var self = this; - return this.datasource.zabbixCache - .getGroups() - .then(groups => { - self.metric.groupList = groups; - return groups; - }); + return this.queryProcessor.getAllGroups() + .then(groups => { + this.metric.groupList = groups; + return groups; + }); } suggestHosts() { - var self = this; - var groupFilter = this.datasource.replaceTemplateVars(this.panel.triggers.group.filter); - return this.datasource.queryProcessor - .filterGroups(self.metric.groupList, groupFilter) - .then(groups => { - var groupids = _.map(groups, 'groupid'); - return self.datasource.zabbixAPI - .getHosts(groupids) - .then(hosts => { - self.metric.hostList = hosts; - return hosts; - }); - }); + let groupFilter = this.datasource.replaceTemplateVars(this.panel.triggers.group.filter); + return this.queryProcessor.getAllHosts(groupFilter) + .then(hosts => { + this.metric.hostList = hosts; + return hosts; + }); } suggestApps() { - var self = this; - var hostFilter = this.datasource.replaceTemplateVars(this.panel.triggers.host.filter); - return this.datasource.queryProcessor - .filterHosts(self.metric.hostList, hostFilter) - .then(hosts => { - var hostids = _.map(hosts, 'hostid'); - return self.datasource.zabbixAPI - .getApps(hostids) - .then(apps => { - return self.metric.appList = apps; - }); - }); + let groupFilter = this.datasource.replaceTemplateVars(this.panel.triggers.group.filter); + let hostFilter = this.datasource.replaceTemplateVars(this.panel.triggers.host.filter); + return this.queryProcessor.getAllApps(groupFilter, hostFilter) + .then(apps => { + this.metric.appList = apps; + return apps; + }); } onVariableChange() { From 88ddcd0c42f7f45be5b74f69daf1dbb798a55fa0 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Sat, 12 Nov 2016 21:26:34 +0300 Subject: [PATCH 06/18] Refactor: move zabbix api response handle to separate module. --- src/datasource-zabbix/datasource.js | 16 +-- .../queryProcessor.service.js | 98 ---------------- src/datasource-zabbix/responseHandler.js | 106 ++++++++++++++++++ 3 files changed, 114 insertions(+), 106 deletions(-) create mode 100644 src/datasource-zabbix/responseHandler.js diff --git a/src/datasource-zabbix/datasource.js b/src/datasource-zabbix/datasource.js index fc61f52..71c2e6c 100644 --- a/src/datasource-zabbix/datasource.js +++ b/src/datasource-zabbix/datasource.js @@ -5,6 +5,7 @@ import * as utils from './utils'; import * as migrations from './migrations'; import * as metricFunctions from './metricFunctions'; import DataProcessor from './DataProcessor'; +import responseHandler from './responseHandler'; import './zabbixAPI.service.js'; import './zabbixCache.service.js'; import './queryProcessor.service.js'; @@ -120,11 +121,10 @@ class ZabbixAPIDatasource { } return this.zabbixAPI - .getSLA(target.itservice.serviceid, timeFrom, timeTo) - .then(slaObject => { - return this.queryProcessor - .handleSLAResponse(target.itservice, target.slaProperty, slaObject); - }); + .getSLA(target.itservice.serviceid, timeFrom, timeTo) + .then(slaObject => { + return responseHandler.handleSLAResponse(target.itservice, target.slaProperty, slaObject); + }); } }); @@ -168,7 +168,7 @@ class ZabbixAPIDatasource { getHistory = this.zabbixAPI .getTrend(items, timeFrom, timeTo) .then(history => { - return this.queryProcessor.handleTrends(history, items, addHostName, valueType); + return responseHandler.handleTrends(history, items, addHostName, valueType); }); } @@ -177,7 +177,7 @@ class ZabbixAPIDatasource { getHistory = this.zabbixCache .getHistory(items, timeFrom, timeTo) .then(history => { - return this.queryProcessor.handleHistory(history, items, addHostName); + return responseHandler.handleHistory(history, items, addHostName); }); } @@ -233,7 +233,7 @@ class ZabbixAPIDatasource { if (items.length) { return this.zabbixAPI.getHistory(items, timeFrom, timeTo) .then(history => { - return this.queryProcessor.convertHistory(history, items, false, (point) => { + return responseHandler.convertHistory(history, items, false, (point) => { let value = point.value; // Regex-based extractor diff --git a/src/datasource-zabbix/queryProcessor.service.js b/src/datasource-zabbix/queryProcessor.service.js index 90bb6a5..0951cad 100644 --- a/src/datasource-zabbix/queryProcessor.service.js +++ b/src/datasource-zabbix/queryProcessor.service.js @@ -147,74 +147,6 @@ function QueryProcessorFactory() { return query; }); } - - /** - * Convert Zabbix API history.get response to Grafana format - * - * @return {Array} Array of timeseries in Grafana format - * { - * target: "Metric name", - * datapoints: [[, ], ...] - * } - */ - convertHistory(history, items, addHostName, convertPointCallback) { - /** - * Response should be in the format: - * data: [ - * { - * target: "Metric name", - * datapoints: [[, ], ...] - * }, ... - * ] - */ - - // 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; @@ -278,33 +210,3 @@ function filterByQuery(list, filter) { 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 - ]; -} diff --git a/src/datasource-zabbix/responseHandler.js b/src/datasource-zabbix/responseHandler.js new file mode 100644 index 0000000..0f6a8e2 --- /dev/null +++ b/src/datasource-zabbix/responseHandler.js @@ -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: [[, ], ...] + * } + */ +function convertHistory(history, items, addHostName, convertPointCallback) { + /** + * Response should be in the format: + * data: [ + * { + * target: "Metric name", + * datapoints: [[, ], ...] + * }, ... + * ] + */ + + // 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) + }; + }); +} + +function handleHistory(history, items, addHostName) { + return convertHistory(history, items, addHostName, convertHistoryPoint); +} + +function handleTrends(history, items, addHostName, valueType) { + 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 +}; From 032927cf8f54a4458d8de94faf1d3fdbf5b8f7c8 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Sun, 13 Nov 2016 13:16:07 +0300 Subject: [PATCH 07/18] Refactor: queryProcessor renamend to queryBuilder. --- src/datasource-zabbix/datasource.js | 18 +++++++++--------- src/datasource-zabbix/query.controller.js | 10 +++++----- ...eryProcessor.service.js => queryBuilder.js} | 8 ++++---- .../specs/datasource_specs.js | 14 +++++++------- src/panel-triggers/editor.js | 8 ++++---- src/panel-triggers/module.js | 4 ++-- 6 files changed, 31 insertions(+), 31 deletions(-) rename src/datasource-zabbix/{queryProcessor.service.js => queryBuilder.js} (97%) diff --git a/src/datasource-zabbix/datasource.js b/src/datasource-zabbix/datasource.js index 71c2e6c..bd3c2b2 100644 --- a/src/datasource-zabbix/datasource.js +++ b/src/datasource-zabbix/datasource.js @@ -8,13 +8,13 @@ import DataProcessor from './DataProcessor'; import responseHandler from './responseHandler'; import './zabbixAPI.service.js'; import './zabbixCache.service.js'; -import './queryProcessor.service.js'; +import './queryBuilder.js'; import {ZabbixAPIError} from './zabbixAPICore.service.js'; class ZabbixAPIDatasource { /** @ngInject */ - constructor(instanceSettings, $q, templateSrv, alertSrv, zabbixAPIService, ZabbixCachingProxy, QueryProcessor) { + constructor(instanceSettings, $q, templateSrv, alertSrv, zabbixAPIService, ZabbixCachingProxy, QueryBuilder) { // General data source settings this.name = instanceSettings.name; @@ -42,7 +42,7 @@ class ZabbixAPIDatasource { this.zabbixCache = new ZabbixCachingProxy(this.zabbixAPI, this.cacheTTL); // Initialize query builder - this.queryProcessor = new QueryProcessor(this.zabbixCache); + this.queryBuilder = new QueryBuilder(this.zabbixCache); // Dependencies this.q = $q; @@ -149,7 +149,7 @@ class ZabbixAPIDatasource { let options = { itemtype: 'num' }; - return this.queryProcessor.build(target, options) + return this.queryBuilder.build(target, options) .then(items => { // Add hostname for items from multiple hosts var addHostName = utils.isRegex(target.host.filter); @@ -228,7 +228,7 @@ class ZabbixAPIDatasource { let options = { itemtype: 'text' }; - return this.queryProcessor.build(target, options) + return this.queryBuilder.build(target, options) .then(items => { if (items.length) { return this.zabbixAPI.getHistory(items, timeFrom, timeTo) @@ -318,13 +318,13 @@ class ZabbixAPIDatasource { if (template.app === '/.*/') { template.app = ''; } - result = this.queryProcessor.getItems(template.group, template.host, template.app); + result = this.queryBuilder.getItems(template.group, template.host, template.app); } else if (parts.length === 3) { // Get applications - result = this.queryProcessor.getApps(template.group, template.host); + result = this.queryBuilder.getApps(template.group, template.host); } else if (parts.length === 2) { // Get hosts - result = this.queryProcessor.getHosts(template.group); + result = this.queryBuilder.getHosts(template.group); } else if (parts.length === 1) { // Get groups result = this.zabbixCache.getGroups(template.group); @@ -350,7 +350,7 @@ class ZabbixAPIDatasource { // Show all triggers var showTriggers = [0, 1]; - var buildQuery = this.queryProcessor + var buildQuery = this.queryBuilder .buildTriggerQuery(this.replaceTemplateVars(annotation.group, {}), this.replaceTemplateVars(annotation.host, {}), this.replaceTemplateVars(annotation.application, {})); diff --git a/src/datasource-zabbix/query.controller.js b/src/datasource-zabbix/query.controller.js index d8e6623..19acdca 100644 --- a/src/datasource-zabbix/query.controller.js +++ b/src/datasource-zabbix/query.controller.js @@ -20,7 +20,7 @@ export class ZabbixQueryController extends QueryCtrl { this.zabbix = this.datasource.zabbixAPI; this.cache = this.datasource.zabbixCache; - this.queryProcessor = this.datasource.queryProcessor; + this.queryBuilder = this.datasource.queryBuilder; this.$q = $q; // Use custom format for template variables @@ -116,7 +116,7 @@ export class ZabbixQueryController extends QueryCtrl { } suggestGroups() { - return this.queryProcessor.getAllGroups() + return this.queryBuilder.getAllGroups() .then(groups => { this.metric.groupList = groups; return groups; @@ -125,7 +125,7 @@ export class ZabbixQueryController extends QueryCtrl { suggestHosts() { let groupFilter = this.replaceTemplateVars(this.target.group.filter); - return this.queryProcessor.getAllHosts(groupFilter) + return this.queryBuilder.getAllHosts(groupFilter) .then(hosts => { this.metric.hostList = hosts; return hosts; @@ -135,7 +135,7 @@ export class ZabbixQueryController extends QueryCtrl { suggestApps() { let groupFilter = this.replaceTemplateVars(this.target.group.filter); let hostFilter = this.replaceTemplateVars(this.target.host.filter); - return this.queryProcessor.getAllApps(groupFilter, hostFilter) + return this.queryBuilder.getAllApps(groupFilter, hostFilter) .then(apps => { this.metric.appList = apps; return apps; @@ -151,7 +151,7 @@ export class ZabbixQueryController extends QueryCtrl { showDisabledItems: this.target.options.showDisabledItems }; - return this.queryProcessor + return this.queryBuilder .getAllItems(groupFilter, hostFilter, appFilter, options) .then(items => { this.metric.itemList = items; diff --git a/src/datasource-zabbix/queryProcessor.service.js b/src/datasource-zabbix/queryBuilder.js similarity index 97% rename from src/datasource-zabbix/queryProcessor.service.js rename to src/datasource-zabbix/queryBuilder.js index 0951cad..8188e59 100644 --- a/src/datasource-zabbix/queryProcessor.service.js +++ b/src/datasource-zabbix/queryBuilder.js @@ -2,9 +2,9 @@ import angular from 'angular'; import _ from 'lodash'; import * as utils from './utils'; -function QueryProcessorFactory() { +function QueryBuilderFactory() { - class QueryProcessor { + class QueryBuilder { constructor(zabbixCacheInstance) { this.cache = zabbixCacheInstance; } @@ -149,12 +149,12 @@ function QueryProcessorFactory() { } } - return QueryProcessor; + return QueryBuilder; } angular .module('grafana.services') - .factory('QueryProcessor', QueryProcessorFactory); + .factory('QueryBuilder', QueryBuilderFactory); /** * Find group, host, app or item by given name. diff --git a/src/datasource-zabbix/specs/datasource_specs.js b/src/datasource-zabbix/specs/datasource_specs.js index 923d99a..7919856 100644 --- a/src/datasource-zabbix/specs/datasource_specs.js +++ b/src/datasource-zabbix/specs/datasource_specs.js @@ -22,10 +22,10 @@ describe('ZabbixDatasource', () => { ctx.alertSrv = {}; ctx.zabbixAPIService = () => {}; ctx.ZabbixCachingProxy = () => {}; - ctx.QueryProcessor = () => {}; + ctx.queryBuilder = () => {}; ctx.ds = new Datasource(ctx.instanceSettings, ctx.$q, ctx.templateSrv, ctx.alertSrv, - ctx.zabbixAPIService, ctx.ZabbixCachingProxy, ctx.QueryProcessor); + ctx.zabbixAPIService, ctx.ZabbixCachingProxy, ctx.queryBuilder); }); describe('When querying data', () => { @@ -147,7 +147,7 @@ describe('ZabbixDatasource', () => { ctx.ds.zabbixCache = { getGroups: () => Q.when([]) }; - ctx.ds.queryProcessor = { + ctx.ds.queryBuilder = { getGroups: () => Q.when([]), getHosts: () => Q.when([]), getApps: () => Q.when([]), @@ -180,7 +180,7 @@ describe('ZabbixDatasource', () => { {query: 'Back*.', expect: 'Back*'} ]; - let getHosts = sinon.spy(ctx.ds.queryProcessor, 'getHosts'); + let getHosts = sinon.spy(ctx.ds.queryBuilder, 'getHosts'); for (const test of tests) { ctx.ds.metricFindQuery(test.query); expect(getHosts).to.have.been.calledWith(test.expect); @@ -197,7 +197,7 @@ describe('ZabbixDatasource', () => { {query: 'Back*.*.', expect: ['Back*', '/.*/']} ]; - let getApps = sinon.spy(ctx.ds.queryProcessor, 'getApps'); + let getApps = sinon.spy(ctx.ds.queryBuilder, 'getApps'); for (const test of tests) { ctx.ds.metricFindQuery(test.query); expect(getApps).to.have.been.calledWith(test.expect[0], test.expect[1]); @@ -214,7 +214,7 @@ describe('ZabbixDatasource', () => { {query: 'Back*.*.cpu.*', expect: ['Back*', '/.*/', 'cpu']} ]; - let getItems = sinon.spy(ctx.ds.queryProcessor, 'getItems'); + let getItems = sinon.spy(ctx.ds.queryBuilder, 'getItems'); for (const test of tests) { ctx.ds.metricFindQuery(test.query); expect(getItems) @@ -227,7 +227,7 @@ describe('ZabbixDatasource', () => { it('should invoke method with proper arguments', (done) => { let query = '*.*'; - let getHosts = sinon.spy(ctx.ds.queryProcessor, 'getHosts'); + let getHosts = sinon.spy(ctx.ds.queryBuilder, 'getHosts'); ctx.ds.metricFindQuery(query); expect(getHosts).to.have.been.calledWith('/.*/'); done(); diff --git a/src/panel-triggers/editor.js b/src/panel-triggers/editor.js index 6c6377a..e9a3088 100644 --- a/src/panel-triggers/editor.js +++ b/src/panel-triggers/editor.js @@ -76,7 +76,7 @@ class TriggerPanelEditorCtrl { this.datasourceSrv.get(this.panel.datasource) .then(datasource => { this.datasource = datasource; - this.queryProcessor = datasource.queryProcessor; + this.queryBuilder = datasource.queryBuilder; this.initFilters(); this.panelCtrl.refresh(); }); @@ -91,7 +91,7 @@ class TriggerPanelEditorCtrl { } suggestGroups() { - return this.queryProcessor.getAllGroups() + return this.queryBuilder.getAllGroups() .then(groups => { this.metric.groupList = groups; return groups; @@ -100,7 +100,7 @@ class TriggerPanelEditorCtrl { suggestHosts() { let groupFilter = this.datasource.replaceTemplateVars(this.panel.triggers.group.filter); - return this.queryProcessor.getAllHosts(groupFilter) + return this.queryBuilder.getAllHosts(groupFilter) .then(hosts => { this.metric.hostList = hosts; return hosts; @@ -110,7 +110,7 @@ class TriggerPanelEditorCtrl { suggestApps() { let groupFilter = this.datasource.replaceTemplateVars(this.panel.triggers.group.filter); let hostFilter = this.datasource.replaceTemplateVars(this.panel.triggers.host.filter); - return this.queryProcessor.getAllApps(groupFilter, hostFilter) + return this.queryBuilder.getAllApps(groupFilter, hostFilter) .then(apps => { this.metric.appList = apps; return apps; diff --git a/src/panel-triggers/module.js b/src/panel-triggers/module.js index ce412f5..c583b7c 100644 --- a/src/panel-triggers/module.js +++ b/src/panel-triggers/module.js @@ -108,7 +108,7 @@ class TriggerPanelCtrl extends MetricsPanelCtrl { // Load datasource return this.datasourceSrv.get(this.panel.datasource).then(datasource => { var zabbix = datasource.zabbixAPI; - var queryProcessor = datasource.queryProcessor; + var queryBuilder = datasource.queryBuilder; var showEvents = self.panel.showEvents.value; var triggerFilter = self.panel.triggers; @@ -117,7 +117,7 @@ class TriggerPanelCtrl extends MetricsPanelCtrl { var hostFilter = datasource.replaceTemplateVars(triggerFilter.host.filter); var appFilter = datasource.replaceTemplateVars(triggerFilter.application.filter); - var buildQuery = queryProcessor.buildTriggerQuery(groupFilter, hostFilter, appFilter); + var buildQuery = queryBuilder.buildTriggerQuery(groupFilter, hostFilter, appFilter); return buildQuery.then(query => { return zabbix.getTriggers(query.groupids, query.hostids, From 775a422c84c70ae939db03ba3c537305a6e2affd Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Sun, 13 Nov 2016 15:00:58 +0300 Subject: [PATCH 08/18] Refactor: zabbixCache. --- src/datasource-zabbix/zabbixCache.service.js | 180 +++++++++---------- 1 file changed, 86 insertions(+), 94 deletions(-) diff --git a/src/datasource-zabbix/zabbixCache.service.js b/src/datasource-zabbix/zabbixCache.service.js index e2a9a00..55131c3 100644 --- a/src/datasource-zabbix/zabbixCache.service.js +++ b/src/datasource-zabbix/zabbixCache.service.js @@ -5,7 +5,7 @@ import _ from 'lodash'; // Each datasource instance must initialize its own cache. /** @ngInject */ -angular.module('grafana.services').factory('ZabbixCachingProxy', function($q, $interval) { +function ZabbixCachingProxyFactory($q, $interval) { class ZabbixCachingProxy { constructor(zabbixAPI, ttl) { @@ -37,79 +37,76 @@ angular.module('grafana.services').factory('ZabbixCachingProxy', function($q, $i $interval(_.bind(this.refresh, this), this.ttl); // Don't run duplicated history requests - this.getHistory = callHistoryOnce(_.bind(this.zabbixAPI.getHistory, this.zabbixAPI), - this.historyPromises); + 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); + this.groupPromises, getAPIRequestHash); this.hostPromises = {}; this.getHostsOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getHosts, this.zabbixAPI), - this.hostPromises); + this.hostPromises, getAPIRequestHash); this.appPromises = {}; this.getAppsOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getApps, this.zabbixAPI), - this.appPromises); + this.appPromises, getAPIRequestHash); this.itemPromises = {}; this.getItemsOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getItems, this.zabbixAPI), - this.itemPromises); + this.itemPromises, getAPIRequestHash); } _refresh() { - var self = this; - var promises = [ + let promises = [ this.zabbixAPI.getGroups() ]; - return this.$q.all(promises).then(function(results) { + return Promise.all(promises) + .then(results => { if (results.length) { - self._groups = results[0]; + this._groups = results[0]; } - self._initialized = true; + this._initialized = true; }); } getGroups() { - var self = this; if (this._groups) { - return this.$q.when(self._groups); + return Promise.resolve(this._groups); } else { return this.getGroupsOnce() - .then(groups => { - self._groups = groups; - return self._groups; - }); + .then(groups => { + this._groups = groups; + return 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; - }); + .then(hosts => { + // iss #196 - disable caching due performance issues + //this._hosts = _.union(this._hosts, hosts); + return hosts; + }); } getApps(hostids) { return this.getAppsOnce(hostids) - .then(apps => { - return apps; - }); + .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; - }); + .then(items => { + // iss #196 - disable caching due performance issues + //this._items = _.union(this._items, items); + return items; + }); } getHistoryFromCache(items, time_from, time_till) { @@ -156,61 +153,51 @@ angular.module('grafana.services').factory('ZabbixCachingProxy', function($q, $i } } - 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; -}); +} + +angular + .module('grafana.services') + .factory('ZabbixCachingProxy', ZabbixCachingProxyFactory); + +/** + * Wrap function to prevent multiple calls + * when waiting for result. + */ +function callOnce(func, promiseKeeper) { + return function() { + if (!promiseKeeper) { + promiseKeeper = Promise.resolve( + func.apply(this, arguments) + .then(result => { + promiseKeeper = null; + return result; + }) + ); + } + return promiseKeeper; + }; +} + +/** + * 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 getAPIRequestHash(args) { var requestStamp = _.map(args, arg => { @@ -223,15 +210,20 @@ function getAPIRequestHash(args) { 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) { - 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 + 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; }; From ce8d9f4be8687550363c2956236e463baaf20c45 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Sun, 13 Nov 2016 15:55:23 +0300 Subject: [PATCH 09/18] Refactor: replace angular $q by native Promise. --- src/datasource-zabbix/datasource.js | 15 +++++++-------- src/datasource-zabbix/query.controller.js | 17 ++++++++--------- .../specs/datasource_specs.js | 3 +-- src/datasource-zabbix/zabbixAPI.service.js | 3 +-- src/datasource-zabbix/zabbixAPICore.service.js | 3 +-- src/datasource-zabbix/zabbixCache.service.js | 18 +++++++----------- src/panel-triggers/editor.js | 13 ++++++------- src/panel-triggers/module.js | 2 +- 8 files changed, 32 insertions(+), 42 deletions(-) diff --git a/src/datasource-zabbix/datasource.js b/src/datasource-zabbix/datasource.js index bd3c2b2..0e4a419 100644 --- a/src/datasource-zabbix/datasource.js +++ b/src/datasource-zabbix/datasource.js @@ -14,7 +14,7 @@ import {ZabbixAPIError} from './zabbixAPICore.service.js'; class ZabbixAPIDatasource { /** @ngInject */ - constructor(instanceSettings, $q, templateSrv, alertSrv, zabbixAPIService, ZabbixCachingProxy, QueryBuilder) { + constructor(instanceSettings, templateSrv, alertSrv, zabbixAPIService, ZabbixCachingProxy, QueryBuilder) { // General data source settings this.name = instanceSettings.name; @@ -45,7 +45,6 @@ class ZabbixAPIDatasource { this.queryBuilder = new QueryBuilder(this.zabbixCache); // Dependencies - this.q = $q; this.templateSrv = templateSrv; this.alertSrv = alertSrv; @@ -129,7 +128,7 @@ class ZabbixAPIDatasource { }); // Data for panel (all targets) - return this.q.all(_.flatten(promises)) + return Promise.all(_.flatten(promises)) .then(_.flatten) .then(timeseries_data => { @@ -245,7 +244,7 @@ class ZabbixAPIDatasource { }); }); } else { - return this.q.when([]); + return Promise.resolve([]); } }); } @@ -318,18 +317,18 @@ class ZabbixAPIDatasource { if (template.app === '/.*/') { template.app = ''; } - result = this.queryBuilder.getItems(template.group, template.host, template.app); + result = this.queryBuilder.getItems(template.group, template.host, template.app, template.item); } else if (parts.length === 3) { // Get applications - result = this.queryBuilder.getApps(template.group, template.host); + result = this.queryBuilder.getApps(template.group, template.host, template.app); } else if (parts.length === 2) { // Get hosts - result = this.queryBuilder.getHosts(template.group); + result = this.queryBuilder.getHosts(template.group, template.host); } else if (parts.length === 1) { // Get groups result = this.zabbixCache.getGroups(template.group); } else { - result = this.q.when([]); + result = Promise.resolve([]); } return result.then(metrics => { diff --git a/src/datasource-zabbix/query.controller.js b/src/datasource-zabbix/query.controller.js index 19acdca..3e5bdd4 100644 --- a/src/datasource-zabbix/query.controller.js +++ b/src/datasource-zabbix/query.controller.js @@ -13,7 +13,7 @@ import './css/query-editor.css!'; export class ZabbixQueryController extends QueryCtrl { // ZabbixQueryCtrl constructor - constructor($scope, $injector, $rootScope, $sce, $q, templateSrv) { + constructor($scope, $injector, $rootScope, $sce, templateSrv) { // Call superclass constructor super($scope, $injector); @@ -21,7 +21,6 @@ export class ZabbixQueryController extends QueryCtrl { this.zabbix = this.datasource.zabbixAPI; this.cache = this.datasource.zabbixCache; this.queryBuilder = this.datasource.queryBuilder; - this.$q = $q; // Use custom format for template variables this.replaceTemplateVars = this.datasource.replaceTemplateVars; @@ -107,12 +106,13 @@ export class ZabbixQueryController extends QueryCtrl { } initFilters() { - var self = this; - var itemtype = self.editorModes[self.target.mode].value; - return this.$q.when(this.suggestGroups()) - .then(() => {return self.suggestHosts();}) - .then(() => {return self.suggestApps();}) - .then(() => {return self.suggestItems(itemtype);}); + let itemtype = this.editorModes[this.target.mode].value; + return Promise.all([ + this.suggestGroups(), + this.suggestHosts(), + this.suggestApps(), + this.suggestItems(itemtype) + ]); } suggestGroups() { @@ -303,7 +303,6 @@ export class ZabbixQueryController extends QueryCtrl { this.panelCtrl.refresh(); } } - } // Set templateUrl as static property diff --git a/src/datasource-zabbix/specs/datasource_specs.js b/src/datasource-zabbix/specs/datasource_specs.js index 7919856..f57b910 100644 --- a/src/datasource-zabbix/specs/datasource_specs.js +++ b/src/datasource-zabbix/specs/datasource_specs.js @@ -17,14 +17,13 @@ describe('ZabbixDatasource', () => { trendsFrom: '7d' } }; - ctx.$q = Q; ctx.templateSrv = {}; ctx.alertSrv = {}; ctx.zabbixAPIService = () => {}; ctx.ZabbixCachingProxy = () => {}; ctx.queryBuilder = () => {}; - ctx.ds = new Datasource(ctx.instanceSettings, ctx.$q, ctx.templateSrv, ctx.alertSrv, + ctx.ds = new Datasource(ctx.instanceSettings, ctx.templateSrv, ctx.alertSrv, ctx.zabbixAPIService, ctx.ZabbixCachingProxy, ctx.queryBuilder); }); diff --git a/src/datasource-zabbix/zabbixAPI.service.js b/src/datasource-zabbix/zabbixAPI.service.js index e81f2aa..735888b 100644 --- a/src/datasource-zabbix/zabbixAPI.service.js +++ b/src/datasource-zabbix/zabbixAPI.service.js @@ -4,7 +4,7 @@ import * as utils from './utils'; import './zabbixAPICore.service'; /** @ngInject */ -function ZabbixAPIServiceFactory($q, alertSrv, zabbixAPICoreService) { +function ZabbixAPIServiceFactory(alertSrv, zabbixAPICoreService) { /** * Zabbix API Wrapper. @@ -28,7 +28,6 @@ function ZabbixAPIServiceFactory($q, alertSrv, zabbixAPICoreService) { this.loginErrorCount = 0; this.maxLoginAttempts = 3; - this.$q = $q; this.alertSrv = alertSrv; this.zabbixAPICore = zabbixAPICoreService; diff --git a/src/datasource-zabbix/zabbixAPICore.service.js b/src/datasource-zabbix/zabbixAPICore.service.js index 70017d2..4ab33c1 100644 --- a/src/datasource-zabbix/zabbixAPICore.service.js +++ b/src/datasource-zabbix/zabbixAPICore.service.js @@ -7,8 +7,7 @@ import angular from 'angular'; class ZabbixAPICoreService { /** @ngInject */ - constructor($q, backendSrv) { - this.$q = $q; + constructor(backendSrv) { this.backendSrv = backendSrv; } diff --git a/src/datasource-zabbix/zabbixCache.service.js b/src/datasource-zabbix/zabbixCache.service.js index 55131c3..410284e 100644 --- a/src/datasource-zabbix/zabbixCache.service.js +++ b/src/datasource-zabbix/zabbixCache.service.js @@ -5,15 +5,13 @@ import _ from 'lodash'; // Each datasource instance must initialize its own cache. /** @ngInject */ -function ZabbixCachingProxyFactory($q, $interval) { +function ZabbixCachingProxyFactory($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; @@ -110,34 +108,32 @@ function ZabbixCachingProxyFactory($q, $interval) { } 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) { + var expired = _.filter(_.keyBy(items, 'itemid'), (item, itemid) => { return !historyStorage[itemid]; }); if (expired.length) { - this.zabbixAPI.getHistory(expired, time_from, time_till).then(function(history) { + return this.zabbixAPI.getHistory(expired, time_from, time_till).then(function(history) { var grouped_history = _.groupBy(history, 'itemid'); - _.forEach(expired, function(item) { + _.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, function(item) { + full_history = _.map(items, item => { return historyStorage[item.itemid].history; }); - deferred.resolve(_.flatten(full_history, true)); + return _.flatten(full_history, true); }); } else { full_history = _.map(items, function(item) { return historyStorage[item.itemid].history; }); - deferred.resolve(_.flatten(full_history, true)); + return Promise.resolve(_.flatten(full_history, true)); } - return deferred.promise; } getHistoryFromAPI(items, time_from, time_till) { diff --git a/src/panel-triggers/editor.js b/src/panel-triggers/editor.js index e9a3088..faebb37 100644 --- a/src/panel-triggers/editor.js +++ b/src/panel-triggers/editor.js @@ -19,12 +19,11 @@ import '../datasource-zabbix/css/query-editor.css!'; class TriggerPanelEditorCtrl { /** @ngInject */ - constructor($scope, $rootScope, $q, uiSegmentSrv, datasourceSrv, templateSrv, popoverSrv) { + constructor($scope, $rootScope, uiSegmentSrv, datasourceSrv, templateSrv, popoverSrv) { $scope.editor = this; this.panelCtrl = $scope.ctrl; this.panel = this.panelCtrl.panel; - this.$q = $q; this.datasourceSrv = datasourceSrv; this.templateSrv = templateSrv; this.popoverSrv = popoverSrv; @@ -83,11 +82,11 @@ class TriggerPanelEditorCtrl { } initFilters() { - var self = this; - return this.$q - .when(this.suggestGroups()) - .then(() => {return self.suggestHosts();}) - .then(() => {return self.suggestApps();}); + return Promise.all([ + this.suggestGroups(), + this.suggestHosts(), + this.suggestApps() + ]); } suggestGroups() { diff --git a/src/panel-triggers/module.js b/src/panel-triggers/module.js index c583b7c..33d7bd8 100644 --- a/src/panel-triggers/module.js +++ b/src/panel-triggers/module.js @@ -61,7 +61,7 @@ var defaultTimeFormat = "DD MMM YYYY HH:mm:ss"; class TriggerPanelCtrl extends MetricsPanelCtrl { /** @ngInject */ - constructor($scope, $injector, $q, $element, datasourceSrv, templateSrv, contextSrv) { + constructor($scope, $injector, $element, datasourceSrv, templateSrv, contextSrv) { super($scope, $injector); this.datasourceSrv = datasourceSrv; this.templateSrv = templateSrv; From d11f1b76166417bdad3fe57d3299e9b9cd06dfca Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Sun, 13 Nov 2016 17:15:50 +0300 Subject: [PATCH 10/18] Refactor: move zabbix requests to single place (zabbix.js module). --- src/datasource-zabbix/datasource.js | 147 ++++++------- .../{queryBuilder.js => zabbix.js} | 76 +++---- src/panel-triggers/module.js | 202 +++++++++--------- 3 files changed, 202 insertions(+), 223 deletions(-) rename src/datasource-zabbix/{queryBuilder.js => zabbix.js} (72%) diff --git a/src/datasource-zabbix/datasource.js b/src/datasource-zabbix/datasource.js index 0e4a419..f055406 100644 --- a/src/datasource-zabbix/datasource.js +++ b/src/datasource-zabbix/datasource.js @@ -6,15 +6,15 @@ import * as migrations from './migrations'; import * as metricFunctions from './metricFunctions'; import DataProcessor from './DataProcessor'; import responseHandler from './responseHandler'; -import './zabbixAPI.service.js'; -import './zabbixCache.service.js'; -import './queryBuilder.js'; +import './zabbix.js'; import {ZabbixAPIError} from './zabbixAPICore.service.js'; class ZabbixAPIDatasource { /** @ngInject */ - constructor(instanceSettings, templateSrv, alertSrv, zabbixAPIService, ZabbixCachingProxy, QueryBuilder) { + constructor(instanceSettings, templateSrv, alertSrv, Zabbix) { + this.templateSrv = templateSrv; + this.alertSrv = alertSrv; // General data source settings this.name = instanceSettings.name; @@ -34,19 +34,7 @@ class ZabbixAPIDatasource { var ttl = instanceSettings.jsonData.cacheTTL || '1h'; this.cacheTTL = utils.parseInterval(ttl); - // Initialize Zabbix API - 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.queryBuilder = new QueryBuilder(this.zabbixCache); - - // Dependencies - this.templateSrv = templateSrv; - this.alertSrv = alertSrv; + this.zabbix = new Zabbix(this.url, this.username, this.password, this.basicAuth, this.withCredentials, this.cacheTTL); // Use custom format for template variables this.replaceTemplateVars = _.partial(replaceTemplateVars, this.templateSrv); @@ -119,7 +107,7 @@ class ZabbixAPIDatasource { return []; } - return this.zabbixAPI + return this.zabbix .getSLA(target.itservice.serviceid, timeFrom, timeTo) .then(slaObject => { return responseHandler.handleSLAResponse(target.itservice, target.slaProperty, slaObject); @@ -148,7 +136,7 @@ class ZabbixAPIDatasource { let options = { itemtype: 'num' }; - return this.queryBuilder.build(target, options) + return this.zabbix.getItemsFromTarget(target, options) .then(items => { // Add hostname for items from multiple hosts var addHostName = utils.isRegex(target.host.filter); @@ -164,7 +152,7 @@ class ZabbixAPIDatasource { }); var valueType = trendValueFunc ? trendValueFunc.params[0] : "avg"; - getHistory = this.zabbixAPI + getHistory = this.zabbix .getTrend(items, timeFrom, timeTo) .then(history => { return responseHandler.handleTrends(history, items, addHostName, valueType); @@ -173,7 +161,7 @@ class ZabbixAPIDatasource { // Use history else { - getHistory = this.zabbixCache + getHistory = this.zabbix .getHistory(items, timeFrom, timeTo) .then(history => { return responseHandler.handleHistory(history, items, addHostName); @@ -227,10 +215,10 @@ class ZabbixAPIDatasource { let options = { itemtype: 'text' }; - return this.queryBuilder.build(target, options) + return this.zabbix.getItemsFromTarget(target, options) .then(items => { if (items.length) { - return this.zabbixAPI.getHistory(items, timeFrom, timeTo) + return this.zabbix.getHistory(items, timeFrom, timeTo) .then(history => { return responseHandler.convertHistory(history, items, false, (point) => { let value = point.value; @@ -255,10 +243,10 @@ class ZabbixAPIDatasource { */ testDatasource() { let zabbixVersion; - return this.zabbixAPI.getVersion() + return this.zabbix.getVersion() .then(version => { zabbixVersion = version; - return this.zabbixAPI.login(); + return this.zabbix.login(); }) .then(() => { return { @@ -317,16 +305,16 @@ class ZabbixAPIDatasource { if (template.app === '/.*/') { template.app = ''; } - result = this.queryBuilder.getItems(template.group, template.host, template.app, template.item); + result = this.zabbix.getItems(template.group, template.host, template.app, template.item); } else if (parts.length === 3) { // Get applications - result = this.queryBuilder.getApps(template.group, template.host, template.app); + result = this.zabbix.getApps(template.group, template.host, template.app); } else if (parts.length === 2) { // Get hosts - result = this.queryBuilder.getHosts(template.group, template.host); + result = this.zabbix.getHosts(template.group, template.host); } else if (parts.length === 1) { // Get groups - result = this.zabbixCache.getGroups(template.group); + result = this.zabbix.getGroups(template.group); } else { result = Promise.resolve([]); } @@ -349,64 +337,61 @@ class ZabbixAPIDatasource { // Show all triggers var showTriggers = [0, 1]; - var buildQuery = this.queryBuilder - .buildTriggerQuery(this.replaceTemplateVars(annotation.group, {}), - this.replaceTemplateVars(annotation.host, {}), - this.replaceTemplateVars(annotation.application, {})); + var getTriggers = this.zabbix + .getTriggers(this.replaceTemplateVars(annotation.group, {}), + this.replaceTemplateVars(annotation.host, {}), + this.replaceTemplateVars(annotation.application, {}), + showTriggers); - return buildQuery.then(query => { - return this.zabbixAPI - .getTriggers(query.groupids, query.hostids, query.applicationids, showTriggers) - .then(triggers => { + return getTriggers.then(triggers => { - // Filter triggers by description - if (utils.isRegex(annotation.trigger)) { - triggers = _.filter(triggers, trigger => { - return utils.buildRegex(annotation.trigger).test(trigger.description); - }); - } else if (annotation.trigger) { - triggers = _.filter(triggers, trigger => { - return trigger.description === annotation.trigger; + // Filter triggers by description + if (utils.isRegex(annotation.trigger)) { + triggers = _.filter(triggers, trigger => { + return utils.buildRegex(annotation.trigger).test(trigger.description); + }); + } else if (annotation.trigger) { + triggers = _.filter(triggers, trigger => { + return trigger.description === annotation.trigger; + }); + } + + // Remove events below the chose severity + triggers = _.filter(triggers, trigger => { + return Number(trigger.priority) >= Number(annotation.minseverity); + }); + + var objectids = _.map(triggers, 'triggerid'); + return this.zabbix + .getEvents(objectids, timeFrom, timeTo, showOkEvents) + .then(events => { + var indexedTriggers = _.keyBy(triggers, 'triggerid'); + + // Hide acknowledged events if option enabled + if (annotation.hideAcknowledged) { + events = _.filter(events, event => { + return !event.acknowledges.length; }); } - // Remove events below the chose severity - triggers = _.filter(triggers, trigger => { - return Number(trigger.priority) >= Number(annotation.minseverity); + return _.map(events, event => { + let tags; + if (annotation.showHostname) { + tags = _.map(event.hosts, 'name'); + } + + // Show event type (OK or Problem) + let title = Number(event.value) ? 'Problem' : 'OK'; + + let formatted_acknowledges = utils.formatAcknowledges(event.acknowledges); + return { + annotation: annotation, + time: event.clock * 1000, + title: title, + tags: tags, + text: indexedTriggers[event.objectid].description + formatted_acknowledges + }; }); - - var objectids = _.map(triggers, 'triggerid'); - return this.zabbixAPI - .getEvents(objectids, timeFrom, timeTo, showOkEvents) - .then(events => { - var indexedTriggers = _.keyBy(triggers, 'triggerid'); - - // Hide acknowledged events if option enabled - if (annotation.hideAcknowledged) { - events = _.filter(events, event => { - return !event.acknowledges.length; - }); - } - - return _.map(events, event => { - let tags; - if (annotation.showHostname) { - tags = _.map(event.hosts, 'name'); - } - - // Show event type (OK or Problem) - let title = Number(event.value) ? 'Problem' : 'OK'; - - let formatted_acknowledges = utils.formatAcknowledges(event.acknowledges); - return { - annotation: annotation, - time: event.clock * 1000, - title: title, - tags: tags, - text: indexedTriggers[event.objectid].description + formatted_acknowledges - }; - }); - }); }); }); } diff --git a/src/datasource-zabbix/queryBuilder.js b/src/datasource-zabbix/zabbix.js similarity index 72% rename from src/datasource-zabbix/queryBuilder.js rename to src/datasource-zabbix/zabbix.js index 8188e59..7500b91 100644 --- a/src/datasource-zabbix/queryBuilder.js +++ b/src/datasource-zabbix/zabbix.js @@ -1,45 +1,40 @@ import angular from 'angular'; import _ from 'lodash'; import * as utils from './utils'; +import './zabbixAPI.service.js'; +import './zabbixCache.service.js'; -function QueryBuilderFactory() { +// Use factory() instead service() for multiple data sources support. +// Each Zabbix data source instance should initialize its own API instance. - class QueryBuilder { - constructor(zabbixCacheInstance) { - this.cache = zabbixCacheInstance; +/** @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 cache + this.cache = new ZabbixCachingProxy(this.zabbixAPI, cacheTTL); + + // Proxy methods + this.getHistory = this.cache.getHistory.bind(this.cache); + + 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); } - initializeCache() { - if (this.cache._initialized) { - return Promise.resolve(); - } else { - return this.cache.refresh(); - } - } - - /** - * Build query - convert target filters to array of Zabbix items - */ - build(target, options) { - function getFiltersFromTarget(target) { - let parts = ['group', 'host', 'application', 'item']; - return _.map(parts, p => target[p].filter); - } - - return this.initializeCache() - .then(() => { - return this.getItems(...getFiltersFromTarget(target), options); - }); - } - - /** - * Build trigger query in asynchronous manner - */ - buildTriggerQuery(groupFilter, hostFilter, appFilter) { - return this.initializeCache() - .then(() => { - return this.buildTriggerQueryFromCache(groupFilter, hostFilter, appFilter); - }); + getItemsFromTarget(target, options) { + let parts = ['group', 'host', 'application', 'item']; + let filters = _.map(parts, p => target[p].filter); + return this.getItems(...filters, options); } getAllGroups() { @@ -120,7 +115,7 @@ function QueryBuilderFactory() { /** * Build query - convert target filters to array of Zabbix items */ - buildTriggerQueryFromCache(groupFilter, hostFilter, appFilter) { + getTriggers(groupFilter, hostFilter, appFilter, showTriggers) { let promises = [ this.getGroups(groupFilter), this.getHosts(groupFilter, hostFilter), @@ -145,16 +140,21 @@ function QueryBuilderFactory() { } return query; + }).then(query => { + return this.zabbixAPI + .getTriggers(query.groupids, query.hostids, query.applicationids, showTriggers); }); } } - return QueryBuilder; + return Zabbix; } angular .module('grafana.services') - .factory('QueryBuilder', QueryBuilderFactory); + .factory('Zabbix', ZabbixFactory); + +/////////////////////////////////////////////////////////////////////////////// /** * Find group, host, app or item by given name. diff --git a/src/panel-triggers/module.js b/src/panel-triggers/module.js index 33d7bd8..7ccbfd5 100644 --- a/src/panel-triggers/module.js +++ b/src/panel-triggers/module.js @@ -106,9 +106,9 @@ class TriggerPanelCtrl extends MetricsPanelCtrl { var self = this; // Load datasource - return this.datasourceSrv.get(this.panel.datasource).then(datasource => { - var zabbix = datasource.zabbixAPI; - var queryBuilder = datasource.queryBuilder; + return this.datasourceSrv.get(this.panel.datasource) + .then(datasource => { + var zabbix = datasource.zabbix; var showEvents = self.panel.showEvents.value; var triggerFilter = self.panel.triggers; @@ -117,118 +117,112 @@ class TriggerPanelCtrl extends MetricsPanelCtrl { var hostFilter = datasource.replaceTemplateVars(triggerFilter.host.filter); var appFilter = datasource.replaceTemplateVars(triggerFilter.application.filter); - var buildQuery = queryBuilder.buildTriggerQuery(groupFilter, hostFilter, appFilter); - return buildQuery.then(query => { - return zabbix.getTriggers(query.groupids, - query.hostids, - query.applicationids, - showEvents) - .then(triggers => { - return _.map(triggers, trigger => { - let triggerObj = trigger; + var getTriggers = zabbix.getTriggers(groupFilter, hostFilter, appFilter, showEvents); + return getTriggers.then(triggers => { + return _.map(triggers, trigger => { + let triggerObj = trigger; - // Format last change and age - trigger.lastchangeUnix = Number(trigger.lastchange); - let timestamp = moment.unix(trigger.lastchangeUnix); - if (self.panel.customLastChangeFormat) { - // User defined format - triggerObj.lastchange = timestamp.format(self.panel.lastChangeFormat); - } else { - triggerObj.lastchange = timestamp.format(self.defaultTimeFormat); - } - triggerObj.age = timestamp.fromNow(true); + // Format last change and age + trigger.lastchangeUnix = Number(trigger.lastchange); + let timestamp = moment.unix(trigger.lastchangeUnix); + if (self.panel.customLastChangeFormat) { + // User defined format + triggerObj.lastchange = timestamp.format(self.panel.lastChangeFormat); + } else { + triggerObj.lastchange = timestamp.format(self.defaultTimeFormat); + } + triggerObj.age = timestamp.fromNow(true); - // Set host that the trigger belongs - if (trigger.hosts.length) { - triggerObj.host = trigger.hosts[0].name; - triggerObj.hostTechName = trigger.hosts[0].host; - } + // Set host that the trigger belongs + if (trigger.hosts.length) { + triggerObj.host = trigger.hosts[0].name; + triggerObj.hostTechName = trigger.hosts[0].host; + } - // Set color - if (trigger.value === '1') { - // Problem state - triggerObj.color = self.panel.triggerSeverity[trigger.priority].color; - } else { - // OK state - triggerObj.color = self.panel.okEventColor; - } + // Set color + if (trigger.value === '1') { + // Problem state + triggerObj.color = self.panel.triggerSeverity[trigger.priority].color; + } else { + // OK state + triggerObj.color = self.panel.okEventColor; + } - triggerObj.severity = self.panel.triggerSeverity[trigger.priority].severity; - return triggerObj; - }); - }) - .then(triggerList => { + triggerObj.severity = self.panel.triggerSeverity[trigger.priority].severity; + return triggerObj; + }); + }) + .then(triggerList => { - // Request acknowledges for trigger - var eventids = _.map(triggerList, trigger => { - return trigger.lastEvent.eventid; + // Request acknowledges for trigger + var eventids = _.map(triggerList, trigger => { + return trigger.lastEvent.eventid; + }); + + return zabbix.getAcknowledges(eventids) + .then(events => { + + // Map events to triggers + _.each(triggerList, trigger => { + var event = _.find(events, event => { + return event.eventid === trigger.lastEvent.eventid; }); - return zabbix.getAcknowledges(eventids) - .then(events => { - - // Map events to triggers - _.each(triggerList, trigger => { - var event = _.find(events, event => { - return event.eventid === trigger.lastEvent.eventid; - }); - - if (event) { - trigger.acknowledges = _.map(event.acknowledges, ack => { - let timestamp = moment.unix(ack.clock); - if (self.panel.customLastChangeFormat) { - ack.time = timestamp.format(self.panel.lastChangeFormat); - } else { - ack.time = timestamp.format(self.defaultTimeFormat); - } - ack.user = ack.alias + ' (' + ack.name + ' ' + ack.surname + ')'; - return ack; - }); - - // Mark acknowledged triggers with different color - if (self.panel.markAckEvents && trigger.acknowledges.length) { - trigger.color = self.panel.ackEventColor; - } - } - }); - - // Filter triggers by description - var triggerFilter = self.panel.triggers.trigger.filter; - if (triggerFilter) { - triggerList = filterTriggers(triggerList, triggerFilter); - } - - // Filter acknowledged triggers - if (self.panel.showTriggers === 'unacknowledged') { - triggerList = _.filter(triggerList, trigger => { - return !trigger.acknowledges; - }); - } else if (self.panel.showTriggers === 'acknowledged') { - triggerList = _.filter(triggerList, 'acknowledges'); + if (event) { + trigger.acknowledges = _.map(event.acknowledges, ack => { + let timestamp = moment.unix(ack.clock); + if (self.panel.customLastChangeFormat) { + ack.time = timestamp.format(self.panel.lastChangeFormat); } else { - triggerList = triggerList; + ack.time = timestamp.format(self.defaultTimeFormat); } - - // Filter triggers by severity - triggerList = _.filter(triggerList, trigger => { - return self.panel.triggerSeverity[trigger.priority].show; - }); - - // Sort triggers - if (self.panel.sortTriggersBy.value === 'priority') { - triggerList = _.sortBy(triggerList, 'priority').reverse(); - } else { - triggerList = _.sortBy(triggerList, 'lastchangeUnix').reverse(); - } - - // Limit triggers number - self.triggerList = triggerList.slice(0, self.panel.limit); - - // Notify panel that request is finished - self.setTimeQueryEnd(); - self.loading = false; + ack.user = ack.alias + ' (' + ack.name + ' ' + ack.surname + ')'; + return ack; }); + + // Mark acknowledged triggers with different color + if (self.panel.markAckEvents && trigger.acknowledges.length) { + trigger.color = self.panel.ackEventColor; + } + } }); + + // Filter triggers by description + var triggerFilter = self.panel.triggers.trigger.filter; + if (triggerFilter) { + triggerList = filterTriggers(triggerList, triggerFilter); + } + + // Filter acknowledged triggers + if (self.panel.showTriggers === 'unacknowledged') { + triggerList = _.filter(triggerList, trigger => { + return !trigger.acknowledges; + }); + } else if (self.panel.showTriggers === 'acknowledged') { + triggerList = _.filter(triggerList, 'acknowledges'); + } else { + triggerList = triggerList; + } + + // Filter triggers by severity + triggerList = _.filter(triggerList, trigger => { + return self.panel.triggerSeverity[trigger.priority].show; + }); + + // Sort triggers + if (self.panel.sortTriggersBy.value === 'priority') { + triggerList = _.sortBy(triggerList, 'priority').reverse(); + } else { + triggerList = _.sortBy(triggerList, 'lastchangeUnix').reverse(); + } + + // Limit triggers number + self.triggerList = triggerList.slice(0, self.panel.limit); + + // Notify panel that request is finished + self.setTimeQueryEnd(); + self.loading = false; + }); }); }); } From 0bf7f4b870e235939b4ae152b3c8f1c8eaea96db Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Sun, 13 Nov 2016 18:40:01 +0300 Subject: [PATCH 11/18] Refactor: fix query.controller.js --- src/datasource-zabbix/query.controller.js | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/datasource-zabbix/query.controller.js b/src/datasource-zabbix/query.controller.js index 3e5bdd4..48465a5 100644 --- a/src/datasource-zabbix/query.controller.js +++ b/src/datasource-zabbix/query.controller.js @@ -14,13 +14,8 @@ export class ZabbixQueryController extends QueryCtrl { // ZabbixQueryCtrl constructor constructor($scope, $injector, $rootScope, $sce, templateSrv) { - - // Call superclass constructor super($scope, $injector); - - this.zabbix = this.datasource.zabbixAPI; - this.cache = this.datasource.zabbixCache; - this.queryBuilder = this.datasource.queryBuilder; + this.zabbix = this.datasource.zabbix; // Use custom format for template variables this.replaceTemplateVars = this.datasource.replaceTemplateVars; @@ -116,7 +111,7 @@ export class ZabbixQueryController extends QueryCtrl { } suggestGroups() { - return this.queryBuilder.getAllGroups() + return this.zabbix.getAllGroups() .then(groups => { this.metric.groupList = groups; return groups; @@ -125,7 +120,7 @@ export class ZabbixQueryController extends QueryCtrl { suggestHosts() { let groupFilter = this.replaceTemplateVars(this.target.group.filter); - return this.queryBuilder.getAllHosts(groupFilter) + return this.zabbix.getAllHosts(groupFilter) .then(hosts => { this.metric.hostList = hosts; return hosts; @@ -135,7 +130,7 @@ export class ZabbixQueryController extends QueryCtrl { suggestApps() { let groupFilter = this.replaceTemplateVars(this.target.group.filter); let hostFilter = this.replaceTemplateVars(this.target.host.filter); - return this.queryBuilder.getAllApps(groupFilter, hostFilter) + return this.zabbix.getAllApps(groupFilter, hostFilter) .then(apps => { this.metric.appList = apps; return apps; @@ -151,7 +146,7 @@ export class ZabbixQueryController extends QueryCtrl { showDisabledItems: this.target.options.showDisabledItems }; - return this.queryBuilder + return this.zabbix .getAllItems(groupFilter, hostFilter, appFilter, options) .then(items => { this.metric.itemList = items; From 834b953e4100969f42a8114624ce2b2269ae080e Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Sun, 13 Nov 2016 18:40:50 +0300 Subject: [PATCH 12/18] Add host name to metric if multiple hosts returned. --- src/datasource-zabbix/responseHandler.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/datasource-zabbix/responseHandler.js b/src/datasource-zabbix/responseHandler.js index 0f6a8e2..7ebe7a9 100644 --- a/src/datasource-zabbix/responseHandler.js +++ b/src/datasource-zabbix/responseHandler.js @@ -22,7 +22,7 @@ function convertHistory(history, items, addHostName, convertPointCallback) { // Group history by itemid var grouped_history = _.groupBy(history, 'itemid'); - var hosts = _.uniq(_.flatten(_.map(items, 'hosts')),'hostid'); //uniq is needed to deduplicate + 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}); @@ -38,11 +38,11 @@ function convertHistory(history, items, addHostName, convertPointCallback) { }); } -function handleHistory(history, items, addHostName) { +function handleHistory(history, items, addHostName = true) { return convertHistory(history, items, addHostName, convertHistoryPoint); } -function handleTrends(history, items, addHostName, valueType) { +function handleTrends(history, items, valueType, addHostName = true) { var convertPointCallback = _.partial(convertTrendPoint, valueType); return convertHistory(history, items, addHostName, convertPointCallback); } From 6e0f59cd696d642f72a18dcede845e2d5c74984c Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Sun, 13 Nov 2016 18:41:10 +0300 Subject: [PATCH 13/18] Refactor: datasource.js --- src/datasource-zabbix/datasource.js | 202 ++++++++++++++-------------- 1 file changed, 100 insertions(+), 102 deletions(-) diff --git a/src/datasource-zabbix/datasource.js b/src/datasource-zabbix/datasource.js index f055406..22331a3 100644 --- a/src/datasource-zabbix/datasource.js +++ b/src/datasource-zabbix/datasource.js @@ -58,12 +58,12 @@ class ZabbixAPIDatasource { // Create request for each target var promises = _.map(options.targets, target => { - // Prevent changes of original object target = _.cloneDeep(target); + this.replaceTargetVariables(target, options); + // Metrics or Text query mode if (target.mode !== 1) { - // Migrate old targets target = migrations.migrate(target); @@ -72,30 +72,9 @@ class ZabbixAPIDatasource { 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) { return this.queryNumericData(target, timeFrom, timeTo, useTrends); - } - - // Query text data - else if (target.mode === 2) { + } else if (target.mode === 2) { return this.queryTextData(target, timeFrom, timeTo); } } @@ -107,8 +86,7 @@ class ZabbixAPIDatasource { return []; } - return this.zabbix - .getSLA(target.itservice.serviceid, timeFrom, timeTo) + return this.zabbix.getSLA(target.itservice.serviceid, timeFrom, timeTo) .then(slaObject => { return responseHandler.handleSLAResponse(target.itservice, target.slaProperty, slaObject); }); @@ -119,15 +97,9 @@ class ZabbixAPIDatasource { return Promise.all(_.flatten(promises)) .then(_.flatten) .then(timeseries_data => { - - // Series downsampling - var data = _.map(timeseries_data, timeseries => { - if (timeseries.datapoints.length > options.maxDataPoints) { - timeseries.datapoints = DataProcessor - .groupBy(options.interval, DataProcessor.AVERAGE, timeseries.datapoints); - } - return timeseries; - }); + return downsampleSeries(timeseries_data, options); + }) + .then(data => { return { data: data }; }); } @@ -137,78 +109,75 @@ class ZabbixAPIDatasource { itemtype: 'num' }; return this.zabbix.getItemsFromTarget(target, options) - .then(items => { - // Add hostname for items from multiple hosts - var addHostName = utils.isRegex(target.host.filter); - var getHistory; + .then(items => { + let getHistoryPromise; - // Use trends - if (useTrends) { - - // Find trendValue() function and get specified trend value - var trendFunctions = _.map(metricFunctions.getCategories()['Trends'], 'name'); - var trendValueFunc = _.find(target.functions, func => { - return _.includes(trendFunctions, func.def.name); + if (useTrends) { + let valueType = this.getTrendValueType(target); + getHistoryPromise = this.zabbix.getTrend(items, timeFrom, timeTo) + .then(history => { + return responseHandler.handleTrends(history, items, valueType); }); - var valueType = trendValueFunc ? trendValueFunc.params[0] : "avg"; - - getHistory = this.zabbix - .getTrend(items, timeFrom, timeTo) - .then(history => { - return responseHandler.handleTrends(history, items, addHostName, valueType); - }); - } - + } else { // Use history - else { - getHistory = this.zabbix - .getHistory(items, timeFrom, timeTo) - .then(history => { - return responseHandler.handleHistory(history, items, addHostName); - }); - } - - return getHistory.then(timeseries_data => { - let transformFunctions = bindFunctionDefs(target.functions, 'Transform'); - let aggregationFunctions = bindFunctionDefs(target.functions, 'Aggregate'); - let filterFunctions = bindFunctionDefs(target.functions, 'Filter'); - let aliasFunctions = bindFunctionDefs(target.functions, 'Alias'); - - // Apply transformation functions - timeseries_data = _.map(timeseries_data, timeseries => { - timeseries.datapoints = sequence(transformFunctions)(timeseries.datapoints); - return timeseries; + getHistoryPromise = this.zabbix.getHistory(items, timeFrom, timeTo) + .then(history => { + return responseHandler.handleHistory(history, items); }); + } - // Apply filter functions - if (filterFunctions.length) { - timeseries_data = sequence(filterFunctions)(timeseries_data); - } - - // Apply aggregations - if (aggregationFunctions.length) { - let dp = _.map(timeseries_data, 'datapoints'); - dp = sequence(aggregationFunctions)(dp); - - let aggFuncNames = _.map(metricFunctions.getCategories()['Aggregate'], 'name'); - let lastAgg = _.findLast(target.functions, func => { - return _.includes(aggFuncNames, func.def.name); - }); - - timeseries_data = [ - { - target: lastAgg.text, - datapoints: dp - } - ]; - } - - // Apply alias functions - _.each(timeseries_data, sequence(aliasFunctions)); - - return timeseries_data; - }); + return getHistoryPromise.then(timeseries_data => { + return this.applyDataProcessingFunctions(timeseries_data, target); }); + }); + } + + getTrendValueType(target) { + // Find trendValue() function and get specified trend value + var trendFunctions = _.map(metricFunctions.getCategories()['Trends'], 'name'); + var trendValueFunc = _.find(target.functions, func => { + return _.includes(trendFunctions, func.def.name); + }); + return trendValueFunc ? trendValueFunc.params[0] : "avg"; + } + + applyDataProcessingFunctions(timeseries_data, target) { + let transformFunctions = bindFunctionDefs(target.functions, 'Transform'); + let aggregationFunctions = bindFunctionDefs(target.functions, 'Aggregate'); + let filterFunctions = bindFunctionDefs(target.functions, 'Filter'); + let aliasFunctions = bindFunctionDefs(target.functions, 'Alias'); + + // Apply transformation functions + timeseries_data = _.map(timeseries_data, timeseries => { + timeseries.datapoints = sequence(transformFunctions)(timeseries.datapoints); + return timeseries; + }); + + // Apply filter functions + if (filterFunctions.length) { + timeseries_data = sequence(filterFunctions)(timeseries_data); + } + + // Apply aggregations + if (aggregationFunctions.length) { + let dp = _.map(timeseries_data, 'datapoints'); + dp = sequence(aggregationFunctions)(dp); + + let aggFuncNames = _.map(metricFunctions.getCategories()['Aggregate'], 'name'); + let lastAgg = _.findLast(target.functions, func => { + return _.includes(aggFuncNames, func.def.name); + }); + + timeseries_data = [{ + target: lastAgg.text, + datapoints: dp + }]; + } + + // Apply alias functions + _.each(timeseries_data, sequence(aliasFunctions)); + + return timeseries_data; } queryTextData(target, timeFrom, timeTo) { @@ -320,7 +289,7 @@ class ZabbixAPIDatasource { } return result.then(metrics => { - return _.map(metrics, formatMetric); + return metrics.map(formatMetric); }); } @@ -396,6 +365,25 @@ 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); + + target.functions.forEach(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); + } + }); + }); + } + } function bindFunctionDefs(functionDefs, category) { @@ -410,6 +398,16 @@ function bindFunctionDefs(functionDefs, category) { }); } +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; + }); +} + function formatMetric(metricObj) { return { text: metricObj.name, From 4dc4e3b8ea894684bbc21f4a6d6e1952b5fe1b72 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Sun, 13 Nov 2016 19:02:37 +0300 Subject: [PATCH 14/18] Refactor: fix tests. --- src/datasource-zabbix/datasource.js | 2 +- .../specs/datasource_specs.js | 22 +++++++------------ 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/datasource-zabbix/datasource.js b/src/datasource-zabbix/datasource.js index 22331a3..9b2f991 100644 --- a/src/datasource-zabbix/datasource.js +++ b/src/datasource-zabbix/datasource.js @@ -373,7 +373,7 @@ class ZabbixAPIDatasource { }); target.textFilter = this.replaceTemplateVars(target.textFilter, options.scopedVars); - target.functions.forEach(func => { + _.forEach(target.functions, func => { func.params = func.params.map(param => { if (typeof param === 'number') { return +this.templateSrv.replace(param.toString(), options.scopedVars); diff --git a/src/datasource-zabbix/specs/datasource_specs.js b/src/datasource-zabbix/specs/datasource_specs.js index f57b910..6b4dc45 100644 --- a/src/datasource-zabbix/specs/datasource_specs.js +++ b/src/datasource-zabbix/specs/datasource_specs.js @@ -19,12 +19,9 @@ describe('ZabbixDatasource', () => { }; ctx.templateSrv = {}; ctx.alertSrv = {}; - ctx.zabbixAPIService = () => {}; - ctx.ZabbixCachingProxy = () => {}; - ctx.queryBuilder = () => {}; + ctx.zabbix = () => {}; - ctx.ds = new Datasource(ctx.instanceSettings, ctx.templateSrv, ctx.alertSrv, - ctx.zabbixAPIService, ctx.ZabbixCachingProxy, ctx.queryBuilder); + ctx.ds = new Datasource(ctx.instanceSettings, ctx.templateSrv, ctx.alertSrv, ctx.zabbix); }); describe('When querying data', () => { @@ -143,10 +140,7 @@ describe('ZabbixDatasource', () => { describe('When invoking metricFindQuery()', () => { beforeEach(() => { ctx.ds.replaceTemplateVars = (str) => str; - ctx.ds.zabbixCache = { - getGroups: () => Q.when([]) - }; - ctx.ds.queryBuilder = { + ctx.ds.zabbix = { getGroups: () => Q.when([]), getHosts: () => Q.when([]), getApps: () => Q.when([]), @@ -162,7 +156,7 @@ describe('ZabbixDatasource', () => { {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) { ctx.ds.metricFindQuery(test.query); expect(getGroups).to.have.been.calledWith(test.expect); @@ -179,7 +173,7 @@ describe('ZabbixDatasource', () => { {query: 'Back*.', expect: 'Back*'} ]; - let getHosts = sinon.spy(ctx.ds.queryBuilder, 'getHosts'); + let getHosts = sinon.spy(ctx.ds.zabbix, 'getHosts'); for (const test of tests) { ctx.ds.metricFindQuery(test.query); expect(getHosts).to.have.been.calledWith(test.expect); @@ -196,7 +190,7 @@ describe('ZabbixDatasource', () => { {query: 'Back*.*.', expect: ['Back*', '/.*/']} ]; - let getApps = sinon.spy(ctx.ds.queryBuilder, 'getApps'); + let getApps = sinon.spy(ctx.ds.zabbix, 'getApps'); for (const test of tests) { ctx.ds.metricFindQuery(test.query); expect(getApps).to.have.been.calledWith(test.expect[0], test.expect[1]); @@ -213,7 +207,7 @@ describe('ZabbixDatasource', () => { {query: 'Back*.*.cpu.*', expect: ['Back*', '/.*/', 'cpu']} ]; - let getItems = sinon.spy(ctx.ds.queryBuilder, 'getItems'); + let getItems = sinon.spy(ctx.ds.zabbix, 'getItems'); for (const test of tests) { ctx.ds.metricFindQuery(test.query); expect(getItems) @@ -226,7 +220,7 @@ describe('ZabbixDatasource', () => { it('should invoke method with proper arguments', (done) => { let query = '*.*'; - let getHosts = sinon.spy(ctx.ds.queryBuilder, 'getHosts'); + let getHosts = sinon.spy(ctx.ds.zabbix, 'getHosts'); ctx.ds.metricFindQuery(query); expect(getHosts).to.have.been.calledWith('/.*/'); done(); From fd772fab153f1134ef3728d6581dfab6f3c1fae2 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 14 Nov 2016 17:27:13 +0300 Subject: [PATCH 15/18] Refactor: dataProcessor.js --- src/datasource-zabbix/DataProcessor.js | 281 ------------------------ src/datasource-zabbix/dataProcessor.js | 291 +++++++++++++++++++++++++ src/datasource-zabbix/datasource.js | 8 +- 3 files changed, 295 insertions(+), 285 deletions(-) delete mode 100644 src/datasource-zabbix/DataProcessor.js create mode 100644 src/datasource-zabbix/dataProcessor.js diff --git a/src/datasource-zabbix/DataProcessor.js b/src/datasource-zabbix/DataProcessor.js deleted file mode 100644 index cb6c4c2..0000000 --- a/src/datasource-zabbix/DataProcessor.js +++ /dev/null @@ -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: [[, ], ...] - */ - 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: { '': [[, ], ...] } - // return [{ '': }, { '': }, ...] - 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; -} diff --git a/src/datasource-zabbix/dataProcessor.js b/src/datasource-zabbix/dataProcessor.js new file mode 100644 index 0000000..dd299b3 --- /dev/null +++ b/src/datasource-zabbix/dataProcessor.js @@ -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: [[, ], ...] + */ +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: { '': [[, ], ...] } + // return [{ '': }, { '': }, ...] + 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; + } +}; diff --git a/src/datasource-zabbix/datasource.js b/src/datasource-zabbix/datasource.js index 9b2f991..f4867e1 100644 --- a/src/datasource-zabbix/datasource.js +++ b/src/datasource-zabbix/datasource.js @@ -4,7 +4,7 @@ import * as dateMath from 'app/core/utils/datemath'; import * as utils from './utils'; import * as migrations from './migrations'; import * as metricFunctions from './metricFunctions'; -import DataProcessor from './DataProcessor'; +import dataProcessor from './dataProcessor'; import responseHandler from './responseHandler'; import './zabbix.js'; import {ZabbixAPIError} from './zabbixAPICore.service.js'; @@ -394,15 +394,15 @@ function bindFunctionDefs(functionDefs, category) { return _.map(aggFuncDefs, function(func) { 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); + timeseries.datapoints = dataProcessor + .groupBy(options.interval, dataProcessor.AVERAGE, timeseries.datapoints); } return timeseries; }); From d15e9c8acd1929bcfe88a68342786e22edc69856 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 14 Nov 2016 18:01:54 +0300 Subject: [PATCH 16/18] Refactor: zabbixCache renamed to zabbixCachingProxy. --- src/datasource-zabbix/zabbix.js | 20 +++++++++---------- ...rvice.js => zabbixCachingProxy.service.js} | 0 2 files changed, 10 insertions(+), 10 deletions(-) rename src/datasource-zabbix/{zabbixCache.service.js => zabbixCachingProxy.service.js} (100%) diff --git a/src/datasource-zabbix/zabbix.js b/src/datasource-zabbix/zabbix.js index 7500b91..335c362 100644 --- a/src/datasource-zabbix/zabbix.js +++ b/src/datasource-zabbix/zabbix.js @@ -2,7 +2,7 @@ import angular from 'angular'; import _ from 'lodash'; import * as utils from './utils'; import './zabbixAPI.service.js'; -import './zabbixCache.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. @@ -17,11 +17,11 @@ function ZabbixFactory(zabbixAPIService, ZabbixCachingProxy) { var ZabbixAPI = zabbixAPIService; this.zabbixAPI = new ZabbixAPI(url, username, password, basicAuth, withCredentials); - // Initialize cache - this.cache = new ZabbixCachingProxy(this.zabbixAPI, cacheTTL); + // Initialize caching proxy for requests + this.cachingProxy = new ZabbixCachingProxy(this.zabbixAPI, cacheTTL); // Proxy methods - this.getHistory = this.cache.getHistory.bind(this.cache); + this.getHistory = this.cachingProxy.getHistory.bind(this.cachingProxy); this.getTrend = this.zabbixAPI.getTrend.bind(this.zabbixAPI); this.getEvents = this.zabbixAPI.getEvents.bind(this.zabbixAPI); @@ -38,7 +38,7 @@ function ZabbixFactory(zabbixAPIService, ZabbixCachingProxy) { } getAllGroups() { - return this.cache.getGroups(); + return this.cachingProxy.getGroups(); } getGroups(groupFilter) { @@ -53,7 +53,7 @@ function ZabbixFactory(zabbixAPIService, ZabbixCachingProxy) { return this.getGroups(groupFilter) .then(groups => { let groupids = _.map(groups, 'groupid'); - return this.cache.getHosts(groupids); + return this.cachingProxy.getHosts(groupids); }); } @@ -69,7 +69,7 @@ function ZabbixFactory(zabbixAPIService, ZabbixCachingProxy) { return this.getHosts(groupFilter, hostFilter) .then(hosts => { let hostids = _.map(hosts, 'hostid'); - return this.cache.getApps(hostids); + return this.cachingProxy.getApps(hostids); }); } @@ -78,7 +78,7 @@ function ZabbixFactory(zabbixAPIService, ZabbixCachingProxy) { .then(hosts => { let hostids = _.map(hosts, 'hostid'); if (appFilter) { - return this.cache.getApps(hostids) + return this.cachingProxy.getApps(hostids) .then(apps => filterByQuery(apps, appFilter)); } else { return { @@ -93,10 +93,10 @@ function ZabbixFactory(zabbixAPIService, ZabbixCachingProxy) { return this.getApps(groupFilter, hostFilter, appFilter) .then(apps => { if (apps.appFilterEmpty) { - return this.cache.getItems(apps.hostids, undefined, options.itemtype); + return this.cachingProxy.getItems(apps.hostids, undefined, options.itemtype); } else { let appids = _.map(apps, 'applicationid'); - return this.cache.getItems(undefined, appids, options.itemtype); + return this.cachingProxy.getItems(undefined, appids, options.itemtype); } }) .then(items => { diff --git a/src/datasource-zabbix/zabbixCache.service.js b/src/datasource-zabbix/zabbixCachingProxy.service.js similarity index 100% rename from src/datasource-zabbix/zabbixCache.service.js rename to src/datasource-zabbix/zabbixCachingProxy.service.js From f56c4c66d641212979c4fa45d5799d85bf634de2 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 14 Nov 2016 22:28:37 +0300 Subject: [PATCH 17/18] ZabbixAPI: improved requests caching. Cache groups, hosts, apps and items. --- src/datasource-zabbix/zabbix.js | 6 +- .../zabbixCachingProxy.service.js | 87 ++++++++++++------- 2 files changed, 59 insertions(+), 34 deletions(-) diff --git a/src/datasource-zabbix/zabbix.js b/src/datasource-zabbix/zabbix.js index 335c362..fd995b0 100644 --- a/src/datasource-zabbix/zabbix.js +++ b/src/datasource-zabbix/zabbix.js @@ -18,7 +18,11 @@ function ZabbixFactory(zabbixAPIService, ZabbixCachingProxy) { this.zabbixAPI = new ZabbixAPI(url, username, password, basicAuth, withCredentials); // Initialize caching proxy for requests - this.cachingProxy = new ZabbixCachingProxy(this.zabbixAPI, cacheTTL); + let cacheOptions = { + enabled: true, + ttl: cacheTTL + }; + this.cachingProxy = new ZabbixCachingProxy(this.zabbixAPI, cacheOptions); // Proxy methods this.getHistory = this.cachingProxy.getHistory.bind(this.cachingProxy); diff --git a/src/datasource-zabbix/zabbixCachingProxy.service.js b/src/datasource-zabbix/zabbixCachingProxy.service.js index 410284e..ffca3b2 100644 --- a/src/datasource-zabbix/zabbixCachingProxy.service.js +++ b/src/datasource-zabbix/zabbixCachingProxy.service.js @@ -5,12 +5,13 @@ import _ from 'lodash'; // Each datasource instance must initialize its own cache. /** @ngInject */ -function ZabbixCachingProxyFactory($interval) { +function ZabbixCachingProxyFactory() { class ZabbixCachingProxy { - constructor(zabbixAPI, ttl) { + constructor(zabbixAPI, cacheOptions) { this.zabbixAPI = zabbixAPI; - this.ttl = ttl; + this.cacheEnabled = cacheOptions.enabled; + this.ttl = cacheOptions.ttl || 600000; // 10 minutes by default // Internal objects for data storing this._groups = undefined; @@ -22,6 +23,13 @@ function ZabbixCachingProxyFactory($interval) { trends: {} }; + this.cache = { + groups: {}, + hosts: {}, + applications: {}, + items: {} + }; + // Check is a service initialized or not this._initialized = undefined; @@ -32,7 +40,7 @@ function ZabbixCachingProxyFactory($interval) { this.refresh = callOnce(this._refresh, this.refreshPromise); // Update cache periodically - $interval(_.bind(this.refresh, this), this.ttl); + // $interval(_.bind(this.refresh, this), this.ttl); // Don't run duplicated history requests this.getHistory = callAPIRequestOnce(_.bind(this.zabbixAPI.getHistory, this.zabbixAPI), @@ -41,19 +49,19 @@ function ZabbixCachingProxyFactory($interval) { // Don't run duplicated requests this.groupPromises = {}; this.getGroupsOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getGroups, this.zabbixAPI), - this.groupPromises, getAPIRequestHash); + this.groupPromises, getRequestHash); this.hostPromises = {}; this.getHostsOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getHosts, this.zabbixAPI), - this.hostPromises, getAPIRequestHash); + this.hostPromises, getRequestHash); this.appPromises = {}; this.getAppsOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getApps, this.zabbixAPI), - this.appPromises, getAPIRequestHash); + this.appPromises, getRequestHash); this.itemPromises = {}; this.getItemsOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getItems, this.zabbixAPI), - this.itemPromises, getAPIRequestHash); + this.itemPromises, getRequestHash); } _refresh() { @@ -70,41 +78,50 @@ function ZabbixCachingProxyFactory($interval) { }); } - getGroups() { - if (this._groups) { - return Promise.resolve(this._groups); + isExpired(cacheObject) { + if (cacheObject) { + let object_age = Date.now() - cacheObject.timestamp; + return !(cacheObject.timestamp && object_age < this.ttl); } else { - return this.getGroupsOnce() - .then(groups => { - this._groups = groups; - return groups; + 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.getHostsOnce(groupids) - .then(hosts => { - // iss #196 - disable caching due performance issues - //this._hosts = _.union(this._hosts, hosts); - return hosts; - }); + return this.proxyRequest(this.getHostsOnce, [groupids], this.cache.hosts); } getApps(hostids) { - return this.getAppsOnce(hostids) - .then(apps => { - return apps; - }); + return this.proxyRequest(this.getAppsOnce, [hostids], this.cache.applications); } getItems(hostids, appids, itemtype) { - return this.getItemsOnce(hostids, appids, itemtype) - .then(items => { - // iss #196 - disable caching due performance issues - //this._items = _.union(this._items, items); - return items; - }); + let params = [hostids, appids, itemtype]; + return this.proxyRequest(this.getItemsOnce, params, this.cache.items); } getHistoryFromCache(items, time_from, time_till) { @@ -195,12 +212,16 @@ function callAPIRequestOnce(func, promiseKeeper, argsHashFunc) { }; } -function getAPIRequestHash(args) { +function getRequestHash(args) { var requestStamp = _.map(args, arg => { if (arg === undefined) { return 'undefined'; } else { - return arg.toString(); + if (_.isArray(arg)) { + return arg.sort().toString(); + } else { + return arg.toString(); + } } }).join(); return requestStamp.getHash(); From 9392d45ad92fc91aacf431213123c8cf1c77cb9d Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 14 Nov 2016 22:36:21 +0300 Subject: [PATCH 18/18] Refactor: removed unused code from zabbixCachingProxy. --- src/datasource-zabbix/utils.js | 19 ++++++ .../zabbixCachingProxy.service.js | 66 ++----------------- 2 files changed, 23 insertions(+), 62 deletions(-) diff --git a/src/datasource-zabbix/utils.js b/src/datasource-zabbix/utils.js index 01e0932..c410d57 100644 --- a/src/datasource-zabbix/utils.js +++ b/src/datasource-zabbix/utils.js @@ -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 if (!_.includes) { _.includes = _.contains; diff --git a/src/datasource-zabbix/zabbixCachingProxy.service.js b/src/datasource-zabbix/zabbixCachingProxy.service.js index ffca3b2..b1cd7ce 100644 --- a/src/datasource-zabbix/zabbixCachingProxy.service.js +++ b/src/datasource-zabbix/zabbixCachingProxy.service.js @@ -14,34 +14,17 @@ function ZabbixCachingProxyFactory() { this.ttl = cacheOptions.ttl || 600000; // 10 minutes by default // Internal objects for data storing - this._groups = undefined; - this._hosts = undefined; - this._applications = undefined; - this._items = undefined; - this.storage = { - history: {}, - trends: {} - }; - this.cache = { groups: {}, hosts: {}, applications: {}, - items: {} + items: {}, + 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 = callAPIRequestOnce(_.bind(this.zabbixAPI.getHistory, this.zabbixAPI), this.historyPromises, getHistoryRequestHash); @@ -64,20 +47,6 @@ function ZabbixCachingProxyFactory() { this.itemPromises, getRequestHash); } - _refresh() { - let promises = [ - this.zabbixAPI.getGroups() - ]; - - return Promise.all(promises) - .then(results => { - if (results.length) { - this._groups = results[0]; - } - this._initialized = true; - }); - } - isExpired(cacheObject) { if (cacheObject) { let object_age = Date.now() - cacheObject.timestamp; @@ -125,7 +94,7 @@ function ZabbixCachingProxyFactory() { } getHistoryFromCache(items, time_from, time_till) { - var historyStorage = this.storage.history; + var historyStorage = this.cache.history; var full_history; var expired = _.filter(_.keyBy(items, 'itemid'), (item, itemid) => { return !historyStorage[itemid]; @@ -156,14 +125,6 @@ function ZabbixCachingProxyFactory() { 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}); - } } return ZabbixCachingProxy; @@ -173,25 +134,6 @@ angular .module('grafana.services') .factory('ZabbixCachingProxy', ZabbixCachingProxyFactory); -/** - * Wrap function to prevent multiple calls - * when waiting for result. - */ -function callOnce(func, promiseKeeper) { - return function() { - if (!promiseKeeper) { - promiseKeeper = Promise.resolve( - func.apply(this, arguments) - .then(result => { - promiseKeeper = null; - return result; - }) - ); - } - return promiseKeeper; - }; -} - /** * Wrap zabbix API request to prevent multiple calls * with same params when waiting for result.