From 9131a83f8498ca28b33cf7896de9e0f3e058e802 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 5 May 2015 13:34:00 +0300 Subject: [PATCH 01/27] Initial --- zabbix/datasource.js | 370 ++++++++++++++++++++++++ zabbix/partials/annotations.editor.html | 8 + zabbix/partials/query.editor.html | 133 +++++++++ zabbix/queryCtrl.js | 243 ++++++++++++++++ 4 files changed, 754 insertions(+) create mode 100644 zabbix/datasource.js create mode 100644 zabbix/partials/annotations.editor.html create mode 100644 zabbix/partials/query.editor.html create mode 100644 zabbix/queryCtrl.js diff --git a/zabbix/datasource.js b/zabbix/datasource.js new file mode 100644 index 0000000..20ed043 --- /dev/null +++ b/zabbix/datasource.js @@ -0,0 +1,370 @@ +define([ + 'angular', + 'lodash', + 'kbn', + 'moment' +], +function (angular, _, kbn) { + 'use strict'; + + var module = angular.module('grafana.services'); + + module.factory('ZabbixAPIDatasource', function($q, $http, templateSrv) { + + function ZabbixAPIDatasource(datasource) { + this.name = datasource.name; + this.type = 'ZabbixAPIDatasource'; + this.supportMetrics = true; + this.url = datasource.url; + this.username = datasource.username; + this.password = datasource.password; + // No datapoints limit by default + this.limitmetrics = datasource.limitmetrics || 0; + + this.partials = datasource.partials || 'plugins/datasource/zabbix/partials'; + this.editorSrc = this.partials + '/query.editor.html'; + this.annotationEditorSrc = this.partials + '/annotations.editor.html'; + this.supportAnnotations = true; + } + + + ZabbixAPIDatasource.prototype.query = function(options) { + // get from & to in seconds + var from = kbn.parseDate(options.range.from).getTime(); + var to = kbn.parseDate(options.range.to).getTime(); + // Need for find target alias + var targets = options.targets; + + // Check that all targets defined + var targetsDefined = options.targets.every(function (target, index, array) { + return target.item; + }); + if (targetsDefined) { + // Extract zabbix api item objects from targets + var target_items = _.map(options.targets, 'item'); + } else { + + // No valid targets, return the empty dataset + var d = $q.defer(); + d.resolve({ data: [] }); + return d.promise; + } + + from = Math.ceil(from/1000); + to = Math.ceil(to/1000); + + var performedQuery; + + // Check authorization first + if (!this.auth) { + var self = this; + performedQuery = this.performZabbixAPILogin().then(function (response) { + self.auth = response; + return self.performTimeSeriesQuery(target_items, from, to); + }); + } else { + performedQuery = this.performTimeSeriesQuery(target_items, from, to); + } + + return performedQuery.then(function (response) { + /** + * Response should be in the format: + * data: [ + * { + * target: "Metric name", + * datapoints: [[, ], ...] + * }, + * { + * target: "Metric name", + * datapoints: [[, ], ...] + * }, + * ] + */ + + // Index returned datapoints by item/metric id + var indexed_result = _.groupBy(response.data.result, function (history_item) { + return history_item.itemid; + }); + + // Reduce timeseries to the same size for stacking and tooltip work properly + var min_length = _.min(_.map(indexed_result, function (history) { + return history.length; + })); + _.each(indexed_result, function (item) { + item.splice(0, item.length - min_length); + }); + + // Sort result as the same as targets for display + // stacked timeseries in proper order + var sorted_history = _.sortBy(indexed_result, function (value, key, list) { + return _.indexOf(_.map(target_items, 'itemid'), key); + }); + + var series = _.map(sorted_history, + // Foreach itemid index: iterate over the data points and + // normalize to Grafana response format. + function (history, index) { + return { + // Lookup itemid:alias map + //target: targets[itemid].alias, + target: targets[index].alias, + + datapoints: _.map(history, function (p) { + + // Value must be a number for properly work + var value = Number(p.value); + + // TODO: Correct time for proper stacking + //var clock = Math.round(Number(p.clock) / 60) * 60; + return [value, p.clock * 1000]; + }) + }; + }) + return $q.when({data: series}); + }); + }; + + + /////////////////////////////////////////////////////////////////////// + /// Query methods + /////////////////////////////////////////////////////////////////////// + + + /** + * Perform time series query to Zabbix API + * + * @param items: array of zabbix api item objects + */ + ZabbixAPIDatasource.prototype.performTimeSeriesQuery = function(items, start, end) { + var item_ids = items.map(function (item, index, array) { + return item.itemid; + }); + // TODO: if different value types passed? + // Perform multiple api request. + var hystory_type = items[0].value_type; + var options = { + method: 'POST', + url: this.url, + data: { + jsonrpc: '2.0', + method: 'history.get', + params: { + output: 'extend', + history: hystory_type, + itemids: item_ids, + sortfield: 'clock', + sortorder: 'ASC', + limit: this.limitmetrics, + time_from: start, + }, + auth: this.auth, + id: 1 + }, + }; + // Relative queries (e.g. last hour) don't include an end time + if (end) { + options.data.params.time_till = end; + } + + return $http(options); + }; + + + // Get authentication token + ZabbixAPIDatasource.prototype.performZabbixAPILogin = function() { + var options = { + url : this.url, + method : 'POST', + data: { + jsonrpc: '2.0', + method: 'user.login', + params: { + user: this.username, + password: this.password + }, + auth: null, + id: 1 + }, + }; + return $http(options).then(function (result) { + if (!result.data) { + return null; + } + return result.data.result; + }); + }; + + + // Get the list of host groups + ZabbixAPIDatasource.prototype.performHostGroupSuggestQuery = function() { + var options = { + url : this.url, + method : 'POST', + data: { + jsonrpc: '2.0', + method: 'hostgroup.get', + params: { + output: ['name'], + sortfield: 'name' + }, + auth: this.auth, + id: 1 + }, + }; + return $http(options).then(function (result) { + if (!result.data) { + return []; + } + return result.data.result; + }); + }; + + + // Get the list of hosts + ZabbixAPIDatasource.prototype.performHostSuggestQuery = function(groupid) { + var options = { + url : this.url, + method : 'POST', + data: { + jsonrpc: '2.0', + method: 'host.get', + params: { + output: ['name'], + sortfield: 'name' + }, + auth: this.auth, + id: 1 + }, + }; + if (groupid) { + options.data.params.groupids = groupid; + } + return $http(options).then(function (result) { + if (!result.data) { + return []; + } + return result.data.result; + }); + }; + + + // Get the list of applications + ZabbixAPIDatasource.prototype.performAppSuggestQuery = function(hostid) { + var options = { + url : this.url, + method : 'POST', + data: { + jsonrpc: '2.0', + method: 'application.get', + params: { + output: ['name'], + sortfield: 'name', + hostids: hostid + }, + auth: this.auth, + id: 1 + }, + }; + return $http(options).then(function (result) { + if (!result.data) { + return []; + } + return result.data.result; + }); + }; + + + // Get the list of host items + ZabbixAPIDatasource.prototype.performItemSuggestQuery = function(hostid, applicationid) { + var options = { + url : this.url, + method : 'POST', + data: { + jsonrpc: '2.0', + method: 'item.get', + params: { + output: ['name', 'key_', 'value_type', 'delay'], + sortfield: 'name', + hostids: hostid + }, + auth: this.auth, + id: 1 + }, + }; + // If application selected return only relative items + if (applicationid) { + options.data.params.applicationids = applicationid; + } + return $http(options).then(function (result) { + if (!result.data) { + return []; + } + return result.data.result; + }); + }; + + + ZabbixAPIDatasource.prototype.annotationQuery = function(annotation, rangeUnparsed) { + var from = kbn.parseDate(rangeUnparsed.from).getTime(); + var to = kbn.parseDate(rangeUnparsed.to).getTime(); + var self = this; + from = Math.ceil(from/1000); + to = Math.ceil(to/1000); + + var tid_options = { + method: 'POST', + url: self.url + '', + data: { + jsonrpc: '2.0', + method: 'trigger.get', + params: { + output: ['triggerid', 'description'], + itemids: annotation.aids.split(','), // TODO: validate / pull automatically from dashboard. + limit: self.limitmetrics, + }, + auth: self.auth, + id: 1 + }, + }; + + return $http(tid_options).then(function(result) { + var obs = {}; + obs = _.indexBy(result.data.result, 'triggerid'); + + var options = { + method: 'POST', + url: self.url + '', + data: { + jsonrpc: '2.0', + method: 'event.get', + params: { + output: 'extend', + sortorder: 'DESC', + time_from: from, + time_till: to, + objectids: _.keys(obs), + limit: self.limitmetrics, + }, + auth: self.auth, + id: 1 + }, + }; + + return $http(options).then(function(result2) { + var list = []; + _.each(result2.data.result, function(e) { + list.push({ + annotation: annotation, + time: e.clock * 1000, + title: obs[e.objectid].description, + text: e.eventid, + }); + }); + return list; + }); + }); + }; + + return ZabbixAPIDatasource; + }); +}); diff --git a/zabbix/partials/annotations.editor.html b/zabbix/partials/annotations.editor.html new file mode 100644 index 0000000..cea2581 --- /dev/null +++ b/zabbix/partials/annotations.editor.html @@ -0,0 +1,8 @@ +
+
+
Item ids Example: 123, 45, 678
+
+ +
+
+
diff --git a/zabbix/partials/query.editor.html b/zabbix/partials/query.editor.html new file mode 100644 index 0000000..faa2b26 --- /dev/null +++ b/zabbix/partials/query.editor.html @@ -0,0 +1,133 @@ + +
+
+ +
+ + +
    +
  • + {{targetLetters[$index]}} +
  • +
  • + + + +
  • +
+ + + +
+ +
+
+
diff --git a/zabbix/queryCtrl.js b/zabbix/queryCtrl.js new file mode 100644 index 0000000..f70c2bf --- /dev/null +++ b/zabbix/queryCtrl.js @@ -0,0 +1,243 @@ +define([ + 'angular', + 'lodash' +], +function (angular, _) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + var targetLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + + var hostGroupList = []; + var hostList = []; + var applicationList = []; + var itemList = []; + + module.controller('ZabbixAPITargetCtrl', function($scope) { + + $scope.init = function() { + $scope.targetLetters = targetLetters; + $scope.metric = { + hostGroupList: ["Loading..."], + hostList: ["Loading..."], + applicationList: ["Loading..."], + itemList: ["Loading..."] + }; + + // Update host group, host, application and item lists + //$scope.updateHostGroupList(); + $scope.updateHostList(); + if ($scope.target.host) { + $scope.updateAppList($scope.target.host.hostid); + if ($scope.target.application) { + $scope.updateItemList($scope.target.host.hostid, $scope.target.application.applicationid); + } else { + $scope.updateItemList($scope.target.host.hostid, null); + } + } + + $scope.target.errors = validateTarget($scope.target); + }; + + $scope.targetBlur = function() { + $scope.target.errors = validateTarget($scope.target); + if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) { + $scope.oldTarget = angular.copy($scope.target); + $scope.get_data(); + } + }; + + // Call when host group selected + $scope.selectHostGroup = function() { + + // Update host list + if ($scope.target.hostGroup) { + $scope.updateHostList($scope.target.hostGroup.groupid); + } else { + $scope.updateHostList(''); + } + + $scope.target.errors = validateTarget($scope.target); + if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) { + $scope.oldTarget = angular.copy($scope.target); + $scope.get_data(); + } + }; + + // Call when host selected + $scope.selectHost = function() { + + // Update item list + if ($scope.target.application) { + $scope.updateItemList($scope.target.host.hostid, $scope.target.application.applicationid); + } else { + $scope.updateItemList($scope.target.host.hostid, null); + } + + // Update application list + $scope.updateAppList($scope.target.host.hostid); + + $scope.target.errors = validateTarget($scope.target); + if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) { + $scope.oldTarget = angular.copy($scope.target); + $scope.get_data(); + } + }; + + + // Call when application selected + $scope.selectApplication = function() { + + // Update item list + if ($scope.target.application) { + $scope.updateItemList($scope.target.host.hostid, $scope.target.application.applicationid); + } else { + $scope.updateItemList($scope.target.host.hostid, null); + } + + $scope.target.errors = validateTarget($scope.target); + if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) { + $scope.oldTarget = angular.copy($scope.target); + $scope.get_data(); + } + }; + + + // Call when item selected + $scope.selectItem = function() { + $scope.target.errors = validateTarget($scope.target); + if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) { + $scope.oldTarget = angular.copy($scope.target); + $scope.get_data(); + } + }; + + + $scope.duplicate = function() { + var clone = angular.copy($scope.target); + $scope.panel.targets.push(clone); + }; + + + $scope.moveMetricQuery = function(fromIndex, toIndex) { + _.move($scope.panel.targets, fromIndex, toIndex); + }; + + ////////////////////////////// + // SUGGESTION QUERIES + ////////////////////////////// + + + /** + * Update list of host groups + */ + $scope.updateHostGroupList = function() { + $scope.datasource.performHostGroupSuggestQuery().then(function (series) { + $scope.metric.hostGroupList = series; + if ($scope.target.hostGroup) { + $scope.target.hostGroup = $scope.metric.hostGroupList.filter(function (item, index, array) { + + // Find selected host in metric.hostList + return (item.groupid == $scope.target.hostGroup.groupid); + }).pop(); + } + }); + }; + + + /** + * Update list of hosts + */ + $scope.updateHostList = function(groupid) { + $scope.datasource.performHostSuggestQuery(groupid).then(function (series) { + $scope.metric.hostList = series; + $scope.target.host = $scope.metric.hostList.filter(function (item, index, array) { + + // Find selected host in metric.hostList + return (item.hostid == $scope.target.host.hostid); + }).pop(); + }); + }; + + + /** + * Update list of host applications + */ + $scope.updateAppList = function(hostid) { + $scope.datasource.performAppSuggestQuery(hostid).then(function (series) { + $scope.metric.applicationList = series; + if ($scope.target.application) { + $scope.target.application = $scope.metric.applicationList.filter(function (item, index, array) { + + // Find selected application in metric.hostList + return (item.applicationid == $scope.target.application.applicationid); + }).pop(); + } + }); + }; + + + /** + * Update list of items + */ + $scope.updateItemList = function(hostid, applicationid) { + + // Update only if host selected + if (hostid) { + $scope.datasource.performItemSuggestQuery(hostid, applicationid).then(function (series) { + $scope.metric.itemList = series; + + // Expand item parameters + $scope.metric.itemList.forEach(function (item, index, array) { + if (item && item.key_ && item.name) { + item.expandedName = expandItemName(item); + } + }); + $scope.target.item = $scope.metric.itemList.filter(function (item, index, array) { + + // Find selected item in metric.hostList + return (item.itemid == $scope.target.item.itemid); + }).pop(); + }); + } else { + $scope.metric.itemList = []; + } + }; + + + /** + * Expand item parameters, for example: + * CPU $2 time ($3) --> CPU system time (avg1) + * + * @param item: zabbix api item object + * @return: expanded item name (string) + */ + function expandItemName(item) { + var name = item.name; + var key = item.key_; + + // extract params from key: + // "system.cpu.util[,system,avg1]" --> ["", "system", "avg1"] + var key_params = key.substring(key.indexOf('[') + 1, key.lastIndexOf(']')).split(','); + + // replace item parameters + for (var i = key_params.length; i >= 1; i--) { + name = name.replace('$' + i, key_params[i - 1]); + }; + return name; + }; + + + ////////////////////////////// + // VALIDATION + ////////////////////////////// + + function validateTarget(target) { + var errs = {}; + + return errs; + } + + }); + +}); From e05009e466cd719a38e92fe507cfb8bd4052a3d6 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 5 May 2015 13:42:07 +0300 Subject: [PATCH 02/27] Initial --- zabbix/datasource.js | 359 ++++++++++++++++++++++++ zabbix/partials/annotations.editor.html | 8 + zabbix/partials/config.html | 36 +++ zabbix/partials/query.editor.html | 143 ++++++++++ zabbix/plugin.json | 20 ++ zabbix/queryCtrl.js | 238 ++++++++++++++++ 6 files changed, 804 insertions(+) create mode 100644 zabbix/datasource.js create mode 100644 zabbix/partials/annotations.editor.html create mode 100644 zabbix/partials/config.html create mode 100644 zabbix/partials/query.editor.html create mode 100644 zabbix/plugin.json create mode 100644 zabbix/queryCtrl.js diff --git a/zabbix/datasource.js b/zabbix/datasource.js new file mode 100644 index 0000000..c2a7c4f --- /dev/null +++ b/zabbix/datasource.js @@ -0,0 +1,359 @@ +define([ + 'angular', + 'lodash', + 'kbn', + './queryCtrl', +], +function (angular, _, kbn) { + 'use strict'; + + var module = angular.module('grafana.services'); + + module.factory('ZabbixAPIDatasource', function($q, backendSrv, templateSrv) { + + function ZabbixAPIDatasource(datasource) { + this.name = datasource.name; + this.type = 'ZabbixAPIDatasource'; + this.supportMetrics = true; + this.url = datasource.url; + + // from plugin.json file + this.username = datasource.meta.username; + this.password = datasource.meta.password; + // No limits by default + this.limitmetrics = datasource.meta.limitmetrics || 0; + + this.partials = datasource.partials || 'plugins/datasource/zabbix/partials'; + this.editorSrc = this.partials + '/query.editor.html'; + + this.annotationEditorSrc = this.partials + '/annotations.editor.html'; + this.supportAnnotations = true; + + // Get authentication token + var authRequestData = { + jsonrpc: '2.0', + method: 'user.login', + params: { + user: this.username, + password: this.password + }, + auth: null, + id: 1 + }; + var zabbixDataSource = this; + this.doZabbixAPIRequest({'data': authRequestData}) + .then(function (response) { + zabbixDataSource.auth = response.data.result; + }); + } + + + ZabbixAPIDatasource.prototype.doZabbixAPIRequest = function(options) { + options.url = this.url; + options.method = 'POST'; + options.headers = {'Content-Type': 'application/json'}; + return backendSrv.datasourceRequest(options); + }; + + /////////////////////////////////////////////////////////////////////// + /// Query methods + /////////////////////////////////////////////////////////////////////// + + + ZabbixAPIDatasource.prototype.query = function(options) { + // get from & to in seconds + var from = kbn.parseDate(options.range.from).getTime(); + var to = kbn.parseDate(options.range.to).getTime(); + // Need for find target alias + var targets = options.targets; + + // Check that all targets defined + var targetsDefined = options.targets.every(function (target, index, array) { + return target.item; + }); + if (targetsDefined) { + // Extract zabbix api item objects from targets + var target_items = _.map(options.targets, 'item'); + } else { + + // No valid targets, return the empty dataset + var d = $q.defer(); + d.resolve({ data: [] }); + return d.promise; + } + + from = Math.ceil(from/1000); + to = Math.ceil(to/1000); + + return this.performTimeSeriesQuery(target_items, from, to) + .then(function (response) { + // Response should be in the format: + // data: [ + // { + // target: "Metric name", + // datapoints: [[, ], ...] + // }, + // { + // target: "Metric name", + // datapoints: [[, ], ...] + // }, + // ] + + // Index returned datapoints by item/metric id + var indexed_result = _.groupBy(response.data.result, function (history_item) { + return history_item.itemid; + }); + + // Reduce timeseries to the same size for stacking and tooltip work properly + var min_length = _.min(_.map(indexed_result, function (history) { + return history.length; + })); + _.each(indexed_result, function (item) { + item.splice(0, item.length - min_length); + }); + + // Sort result as the same as targets for display + // stacked timeseries in proper order + var sorted_history = _.sortBy(indexed_result, function (value, key, list) { + return _.indexOf(_.map(target_items, 'itemid'), key); + }); + + var series = _.map(sorted_history, + // Foreach itemid index: iterate over the data points and + // normalize to Grafana response format. + function (history, index) { + return { + // Lookup itemid:alias map + //target: targets[itemid].alias, + target: targets[index].alias, + + datapoints: _.map(history, function (p) { + + // Value must be a number for properly work + var value = Number(p.value); + + // TODO: Correct time for proper stacking + //var clock = Math.round(Number(p.clock) / 60) * 60; + return [value, p.clock * 1000]; + }) + }; + }) + return $q.when({data: series}); + }); + }; + + + /** + * Perform time series query to Zabbix API + * + * @param items: array of zabbix api item objects + */ + ZabbixAPIDatasource.prototype.performTimeSeriesQuery = function(items, start, end) { + var item_ids = items.map(function (item, index, array) { + return item.itemid; + }); + // TODO: if different value types passed? + // Perform multiple api request. + var hystory_type = items[0].value_type; + var options = { + method: 'POST', + url: this.url, + data: { + jsonrpc: '2.0', + method: 'history.get', + params: { + output: 'extend', + history: hystory_type, + itemids: item_ids, + sortfield: 'clock', + sortorder: 'ASC', + limit: this.limitmetrics, + time_from: start, + }, + auth: this.auth, + id: 1 + }, + }; + // Relative queries (e.g. last hour) don't include an end time + if (end) { + options.data.params.time_till = end; + } + + return this.doZabbixAPIRequest(options); + }; + + + // Get the list of host groups + ZabbixAPIDatasource.prototype.performHostGroupSuggestQuery = function() { + var options = { + url : this.url, + method : 'POST', + data: { + jsonrpc: '2.0', + method: 'hostgroup.get', + params: { + output: ['name'], + sortfield: 'name' + }, + auth: this.auth, + id: 1 + }, + }; + return this.doZabbixAPIRequest(options).then(function (result) { + if (!result.data) { + return []; + } + return result.data.result; + }); + }; + + + // Get the list of hosts + ZabbixAPIDatasource.prototype.performHostSuggestQuery = function(groupid) { + var options = { + url : this.url, + method : 'POST', + data: { + jsonrpc: '2.0', + method: 'host.get', + params: { + output: ['name'], + sortfield: 'name' + }, + auth: this.auth, + id: 1 + }, + }; + if (groupid) { + options.data.params.groupids = groupid; + } + return this.doZabbixAPIRequest(options).then(function (result) { + if (!result.data) { + return []; + } + return result.data.result; + }); + }; + + + // Get the list of applications + ZabbixAPIDatasource.prototype.performAppSuggestQuery = function(hostid) { + var options = { + url : this.url, + method : 'POST', + data: { + jsonrpc: '2.0', + method: 'application.get', + params: { + output: ['name'], + sortfield: 'name', + hostids: hostid + }, + auth: this.auth, + id: 1 + }, + }; + return this.doZabbixAPIRequest(options).then(function (result) { + if (!result.data) { + return []; + } + return result.data.result; + }); + }; + + + // Get the list of host items + ZabbixAPIDatasource.prototype.performItemSuggestQuery = function(hostid, applicationid) { + var options = { + url : this.url, + method : 'POST', + data: { + jsonrpc: '2.0', + method: 'item.get', + params: { + output: ['name', 'key_', 'value_type', 'delay'], + sortfield: 'name', + hostids: hostid + }, + auth: this.auth, + id: 1 + }, + }; + // If application selected return only relative items + if (applicationid) { + options.data.params.applicationids = applicationid; + } + return this.doZabbixAPIRequest(options).then(function (result) { + if (!result.data) { + return []; + } + return result.data.result; + }); + }; + + + ZabbixAPIDatasource.prototype.annotationQuery = function(annotation, rangeUnparsed) { + var from = kbn.parseDate(rangeUnparsed.from).getTime(); + var to = kbn.parseDate(rangeUnparsed.to).getTime(); + var self = this; + from = Math.ceil(from/1000); + to = Math.ceil(to/1000); + + var tid_options = { + method: 'POST', + url: self.url + '', + data: { + jsonrpc: '2.0', + method: 'trigger.get', + params: { + output: ['triggerid', 'description'], + itemids: annotation.aids.split(','), // TODO: validate / pull automatically from dashboard. + limit: self.limitmetrics, + }, + auth: self.auth, + id: 1 + }, + }; + + return this.doZabbixAPIRequest(tid_options).then(function(result) { + var obs = {}; + obs = _.indexBy(result.data.result, 'triggerid'); + + var options = { + method: 'POST', + url: self.url + '', + data: { + jsonrpc: '2.0', + method: 'event.get', + params: { + output: 'extend', + sortorder: 'DESC', + time_from: from, + time_till: to, + objectids: _.keys(obs), + limit: self.limitmetrics, + }, + auth: self.auth, + id: 1 + }, + }; + + return this.doZabbixAPIRequest(options).then(function(result2) { + var list = []; + _.each(result2.data.result, function(e) { + list.push({ + annotation: annotation, + time: e.clock * 1000, + title: obs[e.objectid].description, + text: e.eventid, + }); + }); + return list; + }); + }); + }; + + + return ZabbixAPIDatasource; + }); +}); diff --git a/zabbix/partials/annotations.editor.html b/zabbix/partials/annotations.editor.html new file mode 100644 index 0000000..cea2581 --- /dev/null +++ b/zabbix/partials/annotations.editor.html @@ -0,0 +1,8 @@ +
+
+
Item ids Example: 123, 45, 678
+
+ +
+
+
diff --git a/zabbix/partials/config.html b/zabbix/partials/config.html new file mode 100644 index 0000000..3c345de --- /dev/null +++ b/zabbix/partials/config.html @@ -0,0 +1,36 @@ +
+ +
+ +
Zabbix Details
+ +
+
    +
  • + Database +
  • +
  • + +
  • +
+
+
+
+
    +
  • + User +
  • +
  • + +
  • +
  • + Password +
  • +
  • + +
  • +
+
+
+ + diff --git a/zabbix/partials/query.editor.html b/zabbix/partials/query.editor.html new file mode 100644 index 0000000..cd4f141 --- /dev/null +++ b/zabbix/partials/query.editor.html @@ -0,0 +1,143 @@ +
+ +
+ + + + + + + + + +
+ +
+
diff --git a/zabbix/plugin.json b/zabbix/plugin.json new file mode 100644 index 0000000..05768ec --- /dev/null +++ b/zabbix/plugin.json @@ -0,0 +1,20 @@ +{ + "pluginType": "datasource", + "name": "Zabbix", + + "type": "zabbix", + "serviceName": "ZabbixAPIDatasource", + + "module": "plugins/datasource/zabbix/datasource", + + "partials": { + "config": "app/plugins/datasource/zabbix/partials/config.html", + "query": "app/plugins/datasource/zabbix/partials/query.editor.html", + "annotations": "app/plugins/datasource/zabbix/partials/annotations.editor.html" + }, + + "username": "guest", + "password": "", + + "metrics": true +} diff --git a/zabbix/queryCtrl.js b/zabbix/queryCtrl.js new file mode 100644 index 0000000..0e24476 --- /dev/null +++ b/zabbix/queryCtrl.js @@ -0,0 +1,238 @@ +define([ + 'angular', + 'lodash' +], +function (angular, _) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + var targetLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + + module.controller('ZabbixAPIQueryCtrl', function($scope) { + + $scope.init = function() { + $scope.targetLetters = targetLetters; + $scope.metric = { + hostGroupList: ["Loading..."], + hostList: ["Loading..."], + applicationList: ["Loading..."], + itemList: ["Loading..."] + }; + + // Update host group, host, application and item lists + $scope.updateHostGroupList(); + $scope.updateHostList(); + if ($scope.target.host) { + $scope.updateAppList($scope.target.host.hostid); + if ($scope.target.application) { + $scope.updateItemList($scope.target.host.hostid, $scope.target.application.applicationid); + } else { + $scope.updateItemList($scope.target.host.hostid, null); + } + } + + $scope.target.errors = validateTarget($scope.target); + }; + + $scope.targetBlur = function() { + $scope.target.errors = validateTarget($scope.target); + if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) { + $scope.oldTarget = angular.copy($scope.target); + $scope.get_data(); + } + }; + + // Call when host group selected + $scope.selectHostGroup = function() { + + // Update host list + if ($scope.target.hostGroup) { + $scope.updateHostList($scope.target.hostGroup.groupid); + } else { + $scope.updateHostList(''); + } + + $scope.target.errors = validateTarget($scope.target); + if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) { + $scope.oldTarget = angular.copy($scope.target); + $scope.get_data(); + } + }; + + // Call when host selected + $scope.selectHost = function() { + + // Update item list + if ($scope.target.application) { + $scope.updateItemList($scope.target.host.hostid, $scope.target.application.applicationid); + } else { + $scope.updateItemList($scope.target.host.hostid, null); + } + + // Update application list + $scope.updateAppList($scope.target.host.hostid); + + $scope.target.errors = validateTarget($scope.target); + if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) { + $scope.oldTarget = angular.copy($scope.target); + $scope.get_data(); + } + }; + + + // Call when application selected + $scope.selectApplication = function() { + + // Update item list + if ($scope.target.application) { + $scope.updateItemList($scope.target.host.hostid, $scope.target.application.applicationid); + } else { + $scope.updateItemList($scope.target.host.hostid, null); + } + + $scope.target.errors = validateTarget($scope.target); + if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) { + $scope.oldTarget = angular.copy($scope.target); + $scope.get_data(); + } + }; + + + // Call when item selected + $scope.selectItem = function() { + $scope.target.errors = validateTarget($scope.target); + if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) { + $scope.oldTarget = angular.copy($scope.target); + $scope.get_data(); + } + }; + + + $scope.duplicate = function() { + var clone = angular.copy($scope.target); + $scope.panel.targets.push(clone); + }; + + + $scope.moveMetricQuery = function(fromIndex, toIndex) { + _.move($scope.panel.targets, fromIndex, toIndex); + }; + + ////////////////////////////// + // SUGGESTION QUERIES + ////////////////////////////// + + + /** + * Update list of host groups + */ + $scope.updateHostGroupList = function() { + $scope.datasource.performHostGroupSuggestQuery().then(function (series) { + $scope.metric.hostGroupList = series; + if ($scope.target.hostGroup) { + $scope.target.hostGroup = $scope.metric.hostGroupList.filter(function (item, index, array) { + + // Find selected host in metric.hostList + return (item.groupid == $scope.target.hostGroup.groupid); + }).pop(); + } + }); + }; + + + /** + * Update list of hosts + */ + $scope.updateHostList = function(groupid) { + $scope.datasource.performHostSuggestQuery(groupid).then(function (series) { + $scope.metric.hostList = series; + $scope.target.host = $scope.metric.hostList.filter(function (item, index, array) { + + // Find selected host in metric.hostList + return (item.hostid == $scope.target.host.hostid); + }).pop(); + }); + }; + + + /** + * Update list of host applications + */ + $scope.updateAppList = function(hostid) { + $scope.datasource.performAppSuggestQuery(hostid).then(function (series) { + $scope.metric.applicationList = series; + if ($scope.target.application) { + $scope.target.application = $scope.metric.applicationList.filter(function (item, index, array) { + + // Find selected application in metric.hostList + return (item.applicationid == $scope.target.application.applicationid); + }).pop(); + } + }); + }; + + + /** + * Update list of items + */ + $scope.updateItemList = function(hostid, applicationid) { + + // Update only if host selected + if (hostid) { + $scope.datasource.performItemSuggestQuery(hostid, applicationid).then(function (series) { + $scope.metric.itemList = series; + + // Expand item parameters + $scope.metric.itemList.forEach(function (item, index, array) { + if (item && item.key_ && item.name) { + item.expandedName = expandItemName(item); + } + }); + $scope.target.item = $scope.metric.itemList.filter(function (item, index, array) { + + // Find selected item in metric.hostList + return (item.itemid == $scope.target.item.itemid); + }).pop(); + }); + } else { + $scope.metric.itemList = []; + } + }; + + + /** + * Expand item parameters, for example: + * CPU $2 time ($3) --> CPU system time (avg1) + * + * @param item: zabbix api item object + * @return: expanded item name (string) + */ + function expandItemName(item) { + var name = item.name; + var key = item.key_; + + // extract params from key: + // "system.cpu.util[,system,avg1]" --> ["", "system", "avg1"] + var key_params = key.substring(key.indexOf('[') + 1, key.lastIndexOf(']')).split(','); + + // replace item parameters + for (var i = key_params.length; i >= 1; i--) { + name = name.replace('$' + i, key_params[i - 1]); + }; + return name; + }; + + + ////////////////////////////// + // VALIDATION + ////////////////////////////// + + function validateTarget(target) { + var errs = {}; + + return errs; + } + + }); + +}); From b6b1b67442044880b5ee1557b8cfffa56d949da9 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 5 May 2015 15:25:14 +0300 Subject: [PATCH 03/27] Fix authorization. --- zabbix/datasource.js | 206 +++++++++++++++++++++++-------------------- 1 file changed, 110 insertions(+), 96 deletions(-) diff --git a/zabbix/datasource.js b/zabbix/datasource.js index c2a7c4f..6832af2 100644 --- a/zabbix/datasource.js +++ b/zabbix/datasource.js @@ -2,14 +2,14 @@ define([ 'angular', 'lodash', 'kbn', - './queryCtrl', + 'moment' ], function (angular, _, kbn) { 'use strict'; var module = angular.module('grafana.services'); - module.factory('ZabbixAPIDatasource', function($q, backendSrv, templateSrv) { + module.factory('ZabbixAPIDatasource', function($q, $http, templateSrv) { function ZabbixAPIDatasource(datasource) { this.name = datasource.name; @@ -17,49 +17,20 @@ function (angular, _, kbn) { this.supportMetrics = true; this.url = datasource.url; - // from plugin.json file + // TODO: fix passing username and password from config.html this.username = datasource.meta.username; this.password = datasource.meta.password; - // No limits by default - this.limitmetrics = datasource.meta.limitmetrics || 0; + + // No datapoints limit by default + this.limitmetrics = datasource.limitmetrics || 0; this.partials = datasource.partials || 'plugins/datasource/zabbix/partials'; this.editorSrc = this.partials + '/query.editor.html'; - this.annotationEditorSrc = this.partials + '/annotations.editor.html'; this.supportAnnotations = true; - - // Get authentication token - var authRequestData = { - jsonrpc: '2.0', - method: 'user.login', - params: { - user: this.username, - password: this.password - }, - auth: null, - id: 1 - }; - var zabbixDataSource = this; - this.doZabbixAPIRequest({'data': authRequestData}) - .then(function (response) { - zabbixDataSource.auth = response.data.result; - }); } - ZabbixAPIDatasource.prototype.doZabbixAPIRequest = function(options) { - options.url = this.url; - options.method = 'POST'; - options.headers = {'Content-Type': 'application/json'}; - return backendSrv.datasourceRequest(options); - }; - - /////////////////////////////////////////////////////////////////////// - /// Query methods - /////////////////////////////////////////////////////////////////////// - - ZabbixAPIDatasource.prototype.query = function(options) { // get from & to in seconds var from = kbn.parseDate(options.range.from).getTime(); @@ -85,64 +56,83 @@ function (angular, _, kbn) { from = Math.ceil(from/1000); to = Math.ceil(to/1000); - return this.performTimeSeriesQuery(target_items, from, to) - .then(function (response) { - // Response should be in the format: - // data: [ - // { - // target: "Metric name", - // datapoints: [[, ], ...] - // }, - // { - // target: "Metric name", - // datapoints: [[, ], ...] - // }, - // ] + var performedQuery; - // Index returned datapoints by item/metric id - var indexed_result = _.groupBy(response.data.result, function (history_item) { - return history_item.itemid; - }); - - // Reduce timeseries to the same size for stacking and tooltip work properly - var min_length = _.min(_.map(indexed_result, function (history) { - return history.length; - })); - _.each(indexed_result, function (item) { - item.splice(0, item.length - min_length); - }); - - // Sort result as the same as targets for display - // stacked timeseries in proper order - var sorted_history = _.sortBy(indexed_result, function (value, key, list) { - return _.indexOf(_.map(target_items, 'itemid'), key); - }); - - var series = _.map(sorted_history, - // Foreach itemid index: iterate over the data points and - // normalize to Grafana response format. - function (history, index) { - return { - // Lookup itemid:alias map - //target: targets[itemid].alias, - target: targets[index].alias, - - datapoints: _.map(history, function (p) { - - // Value must be a number for properly work - var value = Number(p.value); - - // TODO: Correct time for proper stacking - //var clock = Math.round(Number(p.clock) / 60) * 60; - return [value, p.clock * 1000]; - }) - }; - }) - return $q.when({data: series}); + // Check authorization first + if (!this.auth) { + var self = this; + performedQuery = this.performZabbixAPILogin().then(function (response) { + self.auth = response; + return self.performTimeSeriesQuery(target_items, from, to); }); + } else { + performedQuery = this.performTimeSeriesQuery(target_items, from, to); + } + + return performedQuery.then(function (response) { + /** + * Response should be in the format: + * data: [ + * { + * target: "Metric name", + * datapoints: [[, ], ...] + * }, + * { + * target: "Metric name", + * datapoints: [[, ], ...] + * }, + * ] + */ + + // Index returned datapoints by item/metric id + var indexed_result = _.groupBy(response.data.result, function (history_item) { + return history_item.itemid; + }); + + // Reduce timeseries to the same size for stacking and tooltip work properly + var min_length = _.min(_.map(indexed_result, function (history) { + return history.length; + })); + _.each(indexed_result, function (item) { + item.splice(0, item.length - min_length); + }); + + // Sort result as the same as targets for display + // stacked timeseries in proper order + var sorted_history = _.sortBy(indexed_result, function (value, key, list) { + return _.indexOf(_.map(target_items, 'itemid'), key); + }); + + var series = _.map(sorted_history, + // Foreach itemid index: iterate over the data points and + // normalize to Grafana response format. + function (history, index) { + return { + // Lookup itemid:alias map + //target: targets[itemid].alias, + target: targets[index].alias, + + datapoints: _.map(history, function (p) { + + // Value must be a number for properly work + var value = Number(p.value); + + // TODO: Correct time for proper stacking + //var clock = Math.round(Number(p.clock) / 60) * 60; + return [value, p.clock * 1000]; + }) + }; + }) + return $q.when({data: series}); + }); }; + /////////////////////////////////////////////////////////////////////// + /// Query methods + /////////////////////////////////////////////////////////////////////// + + /** * Perform time series query to Zabbix API * @@ -179,7 +169,32 @@ function (angular, _, kbn) { options.data.params.time_till = end; } - return this.doZabbixAPIRequest(options); + return $http(options); + }; + + + // Get authentication token + ZabbixAPIDatasource.prototype.performZabbixAPILogin = function() { + var options = { + url : this.url, + method : 'POST', + data: { + jsonrpc: '2.0', + method: 'user.login', + params: { + user: this.username, + password: this.password + }, + auth: null, + id: 1 + }, + }; + return $http(options).then(function (result) { + if (!result.data) { + return null; + } + return result.data.result; + }); }; @@ -199,7 +214,7 @@ function (angular, _, kbn) { id: 1 }, }; - return this.doZabbixAPIRequest(options).then(function (result) { + return $http(options).then(function (result) { if (!result.data) { return []; } @@ -227,7 +242,7 @@ function (angular, _, kbn) { if (groupid) { options.data.params.groupids = groupid; } - return this.doZabbixAPIRequest(options).then(function (result) { + return $http(options).then(function (result) { if (!result.data) { return []; } @@ -253,7 +268,7 @@ function (angular, _, kbn) { id: 1 }, }; - return this.doZabbixAPIRequest(options).then(function (result) { + return $http(options).then(function (result) { if (!result.data) { return []; } @@ -283,7 +298,7 @@ function (angular, _, kbn) { if (applicationid) { options.data.params.applicationids = applicationid; } - return this.doZabbixAPIRequest(options).then(function (result) { + return $http(options).then(function (result) { if (!result.data) { return []; } @@ -315,7 +330,7 @@ function (angular, _, kbn) { }, }; - return this.doZabbixAPIRequest(tid_options).then(function(result) { + return $http(tid_options).then(function(result) { var obs = {}; obs = _.indexBy(result.data.result, 'triggerid'); @@ -338,7 +353,7 @@ function (angular, _, kbn) { }, }; - return this.doZabbixAPIRequest(options).then(function(result2) { + return $http(options).then(function(result2) { var list = []; _.each(result2.data.result, function(e) { list.push({ @@ -353,7 +368,6 @@ function (angular, _, kbn) { }); }; - return ZabbixAPIDatasource; }); }); From 9d6b8608a9bc8ffc581e18fc0b3e4c83b04f83d8 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 5 May 2015 15:26:08 +0300 Subject: [PATCH 04/27] Add .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b543f8f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.sublime-workspace +*.sublime-project From 6323c962e542cf1ab5594f7d020c61c022c9db3e Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 5 May 2015 15:46:13 +0300 Subject: [PATCH 05/27] Fix configs. --- zabbix/datasource.js | 5 ----- zabbix/partials/config.html | 43 ++++++++++++++----------------------- zabbix/plugin.json | 3 ++- 3 files changed, 18 insertions(+), 33 deletions(-) diff --git a/zabbix/datasource.js b/zabbix/datasource.js index 6832af2..c91d479 100644 --- a/zabbix/datasource.js +++ b/zabbix/datasource.js @@ -23,11 +23,6 @@ function (angular, _, kbn) { // No datapoints limit by default this.limitmetrics = datasource.limitmetrics || 0; - - this.partials = datasource.partials || 'plugins/datasource/zabbix/partials'; - this.editorSrc = this.partials + '/query.editor.html'; - this.annotationEditorSrc = this.partials + '/annotations.editor.html'; - this.supportAnnotations = true; } diff --git a/zabbix/partials/config.html b/zabbix/partials/config.html index 3c345de..81f0e44 100644 --- a/zabbix/partials/config.html +++ b/zabbix/partials/config.html @@ -2,35 +2,24 @@
-
Zabbix Details
+
Zabbix API details
-
    -
  • - Database -
  • -
  • - -
  • -
-
-
-
-
    -
  • - User -
  • -
  • - -
  • -
  • - Password -
  • -
  • - -
  • -
-
+
    +
  • + User +
  • +
  • + +
  • +
  • + Password +
  • +
  • + +
  • +
+
diff --git a/zabbix/plugin.json b/zabbix/plugin.json index 05768ec..8fd4ad4 100644 --- a/zabbix/plugin.json +++ b/zabbix/plugin.json @@ -16,5 +16,6 @@ "username": "guest", "password": "", - "metrics": true + "metrics": true, + "annotations": true } From a2e1e0dfc9cb26670cf747bb6391bd9f3253c941 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 5 May 2015 18:19:04 +0300 Subject: [PATCH 06/27] Add .gitignore. --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e379c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.sublime-project +*.sublime-workspace From 78fb8fdfecdee8de0a3bafa0b581581802a8c2d2 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 7 May 2015 16:52:05 +0300 Subject: [PATCH 07/27] Fix reloading query editor with no target's info. Some refactoring. --- zabbix/datasource.js | 158 ++++++++++++++++++++++--------------------- zabbix/queryCtrl.js | 2 +- 2 files changed, 82 insertions(+), 78 deletions(-) diff --git a/zabbix/datasource.js b/zabbix/datasource.js index 20ed043..bbf1820 100644 --- a/zabbix/datasource.js +++ b/zabbix/datasource.js @@ -14,16 +14,19 @@ function (angular, _, kbn) { function ZabbixAPIDatasource(datasource) { this.name = datasource.name; this.type = 'ZabbixAPIDatasource'; - this.supportMetrics = true; + this.url = datasource.url; this.username = datasource.username; this.password = datasource.password; + // No datapoints limit by default this.limitmetrics = datasource.limitmetrics || 0; this.partials = datasource.partials || 'plugins/datasource/zabbix/partials'; this.editorSrc = this.partials + '/query.editor.html'; this.annotationEditorSrc = this.partials + '/annotations.editor.html'; + + this.supportMetrics = true; this.supportAnnotations = true; } @@ -130,6 +133,38 @@ function (angular, _, kbn) { /////////////////////////////////////////////////////////////////////// + // Request data from Zabbix API + ZabbixAPIDatasource.prototype.doZabbixAPIRequest = function(request_data) { + var options = { + method: 'POST', + url: this.url, + data: request_data + }; + + var performedQuery; + + // Check authorization first + if (!this.auth) { + var self = this; + performedQuery = this.performZabbixAPILogin().then(function (response) { + self.auth = response; + options.data.auth = response; + return $http(options); + }); + } else { + performedQuery = $http(options); + } + + // Handle response + return performedQuery.then(function (response) { + if (!response.data) { + return []; + } + return response.data.result; + }); + }; + + /** * Perform time series query to Zabbix API * @@ -186,6 +221,7 @@ function (angular, _, kbn) { id: 1 }, }; + return $http(options).then(function (result) { if (!result.data) { return null; @@ -197,110 +233,78 @@ function (angular, _, kbn) { // Get the list of host groups ZabbixAPIDatasource.prototype.performHostGroupSuggestQuery = function() { - var options = { - url : this.url, - method : 'POST', - data: { - jsonrpc: '2.0', - method: 'hostgroup.get', - params: { - output: ['name'], - sortfield: 'name' - }, - auth: this.auth, - id: 1 + var data = { + jsonrpc: '2.0', + method: 'hostgroup.get', + params: { + output: ['name'], + sortfield: 'name' }, + auth: this.auth, + id: 1 }; - return $http(options).then(function (result) { - if (!result.data) { - return []; - } - return result.data.result; - }); + + return this.doZabbixAPIRequest(data); }; // Get the list of hosts ZabbixAPIDatasource.prototype.performHostSuggestQuery = function(groupid) { - var options = { - url : this.url, - method : 'POST', - data: { - jsonrpc: '2.0', - method: 'host.get', - params: { - output: ['name'], - sortfield: 'name' - }, - auth: this.auth, - id: 1 + var data = { + jsonrpc: '2.0', + method: 'host.get', + params: { + output: ['name'], + sortfield: 'name' }, + auth: this.auth, + id: 1 }; if (groupid) { - options.data.params.groupids = groupid; + data.params.groupids = groupid; } - return $http(options).then(function (result) { - if (!result.data) { - return []; - } - return result.data.result; - }); + + return this.doZabbixAPIRequest(data); }; // Get the list of applications ZabbixAPIDatasource.prototype.performAppSuggestQuery = function(hostid) { - var options = { - url : this.url, - method : 'POST', - data: { - jsonrpc: '2.0', - method: 'application.get', - params: { - output: ['name'], - sortfield: 'name', - hostids: hostid - }, - auth: this.auth, - id: 1 + var data = { + jsonrpc: '2.0', + method: 'application.get', + params: { + output: ['name'], + sortfield: 'name', + hostids: hostid }, + auth: this.auth, + id: 1 }; - return $http(options).then(function (result) { - if (!result.data) { - return []; - } - return result.data.result; - }); + + return this.doZabbixAPIRequest(data); }; // Get the list of host items ZabbixAPIDatasource.prototype.performItemSuggestQuery = function(hostid, applicationid) { - var options = { - url : this.url, - method : 'POST', - data: { - jsonrpc: '2.0', - method: 'item.get', - params: { - output: ['name', 'key_', 'value_type', 'delay'], - sortfield: 'name', - hostids: hostid - }, - auth: this.auth, - id: 1 + var data = { + jsonrpc: '2.0', + method: 'item.get', + params: { + output: ['name', 'key_', 'value_type', 'delay'], + sortfield: 'name', + hostids: hostid }, + auth: this.auth, + id: 1 }; // If application selected return only relative items if (applicationid) { - options.data.params.applicationids = applicationid; + data.params.applicationids = applicationid; } - return $http(options).then(function (result) { - if (!result.data) { - return []; - } - return result.data.result; - }); + + return this.doZabbixAPIRequest(data); }; diff --git a/zabbix/queryCtrl.js b/zabbix/queryCtrl.js index f70c2bf..588a652 100644 --- a/zabbix/queryCtrl.js +++ b/zabbix/queryCtrl.js @@ -25,7 +25,7 @@ function (angular, _) { }; // Update host group, host, application and item lists - //$scope.updateHostGroupList(); + $scope.updateHostGroupList(); $scope.updateHostList(); if ($scope.target.host) { $scope.updateAppList($scope.target.host.hostid); From f11e0842db803a28610fb6a3ded5a21d0985197c Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 7 May 2015 18:26:49 +0300 Subject: [PATCH 08/27] Fix host group list update. --- zabbix/partials/query.editor.html | 243 +++++++++++++++--------------- zabbix/queryCtrl.js | 11 +- 2 files changed, 126 insertions(+), 128 deletions(-) diff --git a/zabbix/partials/query.editor.html b/zabbix/partials/query.editor.html index faa2b26..35b4e8d 100644 --- a/zabbix/partials/query.editor.html +++ b/zabbix/partials/query.editor.html @@ -1,133 +1,132 @@ -
-
+
- -
    -
  • - {{targetLetters[$index]}} -
  • -
  • - - - -
  • -
+
    +
  • + {{targetLetters[$index]}} +
  • +
  • + + + +
  • +
- + -
+
-
+
diff --git a/zabbix/queryCtrl.js b/zabbix/queryCtrl.js index 588a652..29b1192 100644 --- a/zabbix/queryCtrl.js +++ b/zabbix/queryCtrl.js @@ -8,11 +8,6 @@ function (angular, _) { var module = angular.module('grafana.controllers'); var targetLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' - var hostGroupList = []; - var hostList = []; - var applicationList = []; - var itemList = []; - module.controller('ZabbixAPITargetCtrl', function($scope) { $scope.init = function() { @@ -26,7 +21,11 @@ function (angular, _) { // Update host group, host, application and item lists $scope.updateHostGroupList(); - $scope.updateHostList(); + if ($scope.target.hostGroup) { + $scope.updateHostList($scope.target.hostGroup.groupid); + } else { + $scope.updateHostList(); + } if ($scope.target.host) { $scope.updateAppList($scope.target.host.hostid); if ($scope.target.application) { From bc1a3bbda819daaf752f01df5272bb1d6c9c353f Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 7 May 2015 19:21:00 +0300 Subject: [PATCH 09/27] Add installation instructions. --- README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/README.md b/README.md index c02e1c2..1585da6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,33 @@ # grafana-zabbix Zabbix API datasource for Grafana dashboard + +## Installation + +### Grafana 1.9.x + +Download latest release and unpack into `/plugins/datasource/`. Then edit Grafana config.js: + * Add dependencies + + ``` + plugins: { + panels: [], + dependencies: ['datasource/zabbix/datasource', 'datasource/zabbix/queryCtrl'], + } + ``` + * Add datasource and setup your Zabbix API url, username and password + + ``` + datasources: { + ... + }, + zabbix: { + type: 'ZabbixAPIDatasource', + url: 'http://www.zabbix.org/zabbix/api_jsonrpc.php', + username: 'guest', + password: '' + } + }, + ``` + +### Grafana 2.0.x +Now in development. \ No newline at end of file From a1d31d54585cb66fc7d17ace65b690e7db0b4ecf Mon Sep 17 00:00:00 2001 From: nucleusv Date: Fri, 8 May 2015 01:25:08 +0300 Subject: [PATCH 10/27] Written ptototype.query method This method now uses the same request doZabbixAPIRequest without code dublication. Please check on your installation cause I have only one metric per graph --- zabbix/datasource.js | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/zabbix/datasource.js b/zabbix/datasource.js index bbf1820..103bc2e 100644 --- a/zabbix/datasource.js +++ b/zabbix/datasource.js @@ -56,20 +56,11 @@ function (angular, _, kbn) { from = Math.ceil(from/1000); to = Math.ceil(to/1000); - var performedQuery; + - // Check authorization first - if (!this.auth) { - var self = this; - performedQuery = this.performZabbixAPILogin().then(function (response) { - self.auth = response; - return self.performTimeSeriesQuery(target_items, from, to); - }); - } else { - performedQuery = this.performTimeSeriesQuery(target_items, from, to); - } - - return performedQuery.then(function (response) { + return this.performTimeSeriesQuery(target_items, from, to).then(function (response) { + + console.log(response); /** * Response should be in the format: * data: [ @@ -85,7 +76,7 @@ function (angular, _, kbn) { */ // Index returned datapoints by item/metric id - var indexed_result = _.groupBy(response.data.result, function (history_item) { + var indexed_result = _.groupBy(response, function (history_item) { return history_item.itemid; }); @@ -177,10 +168,8 @@ function (angular, _, kbn) { // TODO: if different value types passed? // Perform multiple api request. var hystory_type = items[0].value_type; - var options = { - method: 'POST', - url: this.url, - data: { + + var data = { jsonrpc: '2.0', method: 'history.get', params: { @@ -194,14 +183,14 @@ function (angular, _, kbn) { }, auth: this.auth, id: 1 - }, - }; + }; + // Relative queries (e.g. last hour) don't include an end time if (end) { - options.data.params.time_till = end; + data.params.time_till = end; } - return $http(options); + return this.doZabbixAPIRequest(data); }; From 4b4432923045740a28bd21f60c1e04d6fd80a37d Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 8 May 2015 22:13:52 +0300 Subject: [PATCH 11/27] Some refactoring. --- zabbix/datasource.js | 43 ++++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/zabbix/datasource.js b/zabbix/datasource.js index 103bc2e..3598f9b 100644 --- a/zabbix/datasource.js +++ b/zabbix/datasource.js @@ -35,6 +35,7 @@ function (angular, _, kbn) { // get from & to in seconds var from = kbn.parseDate(options.range.from).getTime(); var to = kbn.parseDate(options.range.to).getTime(); + // Need for find target alias var targets = options.targets; @@ -46,7 +47,6 @@ function (angular, _, kbn) { // Extract zabbix api item objects from targets var target_items = _.map(options.targets, 'item'); } else { - // No valid targets, return the empty dataset var d = $q.defer(); d.resolve({ data: [] }); @@ -56,11 +56,7 @@ function (angular, _, kbn) { from = Math.ceil(from/1000); to = Math.ceil(to/1000); - - return this.performTimeSeriesQuery(target_items, from, to).then(function (response) { - - console.log(response); /** * Response should be in the format: * data: [ @@ -165,26 +161,27 @@ function (angular, _, kbn) { var item_ids = items.map(function (item, index, array) { return item.itemid; }); + // TODO: if different value types passed? // Perform multiple api request. - var hystory_type = items[0].value_type; - - var data = { - jsonrpc: '2.0', - method: 'history.get', - params: { - output: 'extend', - history: hystory_type, - itemids: item_ids, - sortfield: 'clock', - sortorder: 'ASC', - limit: this.limitmetrics, - time_from: start, - }, - auth: this.auth, - id: 1 - }; - + var history_type = items[0].value_type; + + var data = { + jsonrpc: '2.0', + method: 'history.get', + params: { + output: 'extend', + history: history_type, + itemids: item_ids, + sortfield: 'clock', + sortorder: 'ASC', + limit: this.limitmetrics, + time_from: start, + }, + auth: this.auth, + id: 1 + }; + // Relative queries (e.g. last hour) don't include an end time if (end) { data.params.time_till = end; From 6ea38462f698171ea1121c1b8d8772442d0888b5 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 8 May 2015 22:55:05 +0300 Subject: [PATCH 12/27] Fixed update of host list and item list when host or item not selected. --- zabbix/queryCtrl.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/zabbix/queryCtrl.js b/zabbix/queryCtrl.js index 29b1192..e367f49 100644 --- a/zabbix/queryCtrl.js +++ b/zabbix/queryCtrl.js @@ -135,7 +135,6 @@ function (angular, _) { $scope.metric.hostGroupList = series; if ($scope.target.hostGroup) { $scope.target.hostGroup = $scope.metric.hostGroupList.filter(function (item, index, array) { - // Find selected host in metric.hostList return (item.groupid == $scope.target.hostGroup.groupid); }).pop(); @@ -150,11 +149,13 @@ function (angular, _) { $scope.updateHostList = function(groupid) { $scope.datasource.performHostSuggestQuery(groupid).then(function (series) { $scope.metric.hostList = series; - $scope.target.host = $scope.metric.hostList.filter(function (item, index, array) { - // Find selected host in metric.hostList - return (item.hostid == $scope.target.host.hostid); - }).pop(); + if ($scope.target.host) { + $scope.target.host = $scope.metric.hostList.filter(function (item, index, array) { + // Find selected host in metric.hostList + return (item.hostid == $scope.target.host.hostid); + }).pop(); + } }); }; @@ -167,7 +168,6 @@ function (angular, _) { $scope.metric.applicationList = series; if ($scope.target.application) { $scope.target.application = $scope.metric.applicationList.filter(function (item, index, array) { - // Find selected application in metric.hostList return (item.applicationid == $scope.target.application.applicationid); }).pop(); @@ -192,11 +192,12 @@ function (angular, _) { item.expandedName = expandItemName(item); } }); - $scope.target.item = $scope.metric.itemList.filter(function (item, index, array) { - - // Find selected item in metric.hostList - return (item.itemid == $scope.target.item.itemid); - }).pop(); + if ($scope.target.item) { + $scope.target.item = $scope.metric.itemList.filter(function (item, index, array) { + // Find selected item in metric.hostList + return (item.itemid == $scope.target.item.itemid); + }).pop(); + } }); } else { $scope.metric.itemList = []; From e98441d0497f42181163d9f625cfcd54dcdaab8a Mon Sep 17 00:00:00 2001 From: nucleusv Date: Sat, 9 May 2015 14:53:04 +0300 Subject: [PATCH 13/27] Update README.md --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1585da6..e96ecff 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,16 @@ # grafana-zabbix Zabbix API datasource for Grafana dashboard +![alt tag](https://cloud.githubusercontent.com/assets/4932851/7454206/34bf9f8c-f27a-11e4-8e96-a73829f188c4.png) + + +Query editor allows to add metric by step-by-step selection from host group, host, application dropdown menus. + +![alt tag](https://cloud.githubusercontent.com/assets/4932851/7441162/4f6af788-f0e4-11e4-887b-34d987d00c40.png) +![alt tag](https://cloud.githubusercontent.com/assets/4932851/7441163/56f28f16-f0e4-11e4-9d46-54181c2a2e7e.png) +![alt tag](https://cloud.githubusercontent.com/assets/4932851/7441167/5f29cc94-f0e4-11e4-8d39-7580f33201f6.png) + + ## Installation ### Grafana 1.9.x @@ -30,4 +40,4 @@ Download latest release and unpack into `/plugins/dat ``` ### Grafana 2.0.x -Now in development. \ No newline at end of file +Now in development. From 8ee4dd6a5cf8ad351a7af9b0c87e39d34ab530d3 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Sat, 9 May 2015 16:05:30 +0300 Subject: [PATCH 14/27] Fixed history request for multiple value type items. --- zabbix/datasource.js | 90 ++++++++++++++++++++++++++++++-------------- 1 file changed, 62 insertions(+), 28 deletions(-) diff --git a/zabbix/datasource.js b/zabbix/datasource.js index 3598f9b..fcdc3cf 100644 --- a/zabbix/datasource.js +++ b/zabbix/datasource.js @@ -39,6 +39,7 @@ function (angular, _, kbn) { // Need for find target alias var targets = options.targets; + // TODO: remove undefined targets from request // Check that all targets defined var targetsDefined = options.targets.every(function (target, index, array) { return target.item; @@ -72,9 +73,7 @@ function (angular, _, kbn) { */ // Index returned datapoints by item/metric id - var indexed_result = _.groupBy(response, function (history_item) { - return history_item.itemid; - }); + var indexed_result = _.groupBy(response, 'itemid'); // Reduce timeseries to the same size for stacking and tooltip work properly var min_length = _.min(_.map(indexed_result, function (history) { @@ -124,6 +123,9 @@ function (angular, _, kbn) { ZabbixAPIDatasource.prototype.doZabbixAPIRequest = function(request_data) { var options = { method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, url: this.url, data: request_data }; @@ -158,36 +160,68 @@ function (angular, _, kbn) { * @param items: array of zabbix api item objects */ ZabbixAPIDatasource.prototype.performTimeSeriesQuery = function(items, start, end) { - var item_ids = items.map(function (item, index, array) { - return item.itemid; + + // Group items by value type for separate requests + var items_by_value_type = _.groupBy(items, 'value_type'); + + var self = this; + var apiRequests = []; + + // Prepare requests for each value type + _.each(items_by_value_type, function (value, key, list) { + var item_ids = _.map(value, 'itemid'); + var history_type = key; + var data = { + jsonrpc: '2.0', + method: 'history.get', + params: { + output: 'extend', + history: history_type, + itemids: item_ids, + sortfield: 'clock', + sortorder: 'ASC', + limit: self.limitmetrics, + time_from: start, + }, + auth: self.auth, + id: 1 + }; + + // Relative queries (e.g. last hour) don't include an end time + if (end) { + data.params.time_till = end; + } + + apiRequests.push(self.doZabbixAPIRequest(data)); }); - // TODO: if different value types passed? - // Perform multiple api request. - var history_type = items[0].value_type; + return this.handleMultipleRequest(apiRequests); + }; - var data = { - jsonrpc: '2.0', - method: 'history.get', - params: { - output: 'extend', - history: history_type, - itemids: item_ids, - sortfield: 'clock', - sortorder: 'ASC', - limit: this.limitmetrics, - time_from: start, - }, - auth: this.auth, - id: 1 - }; - // Relative queries (e.g. last hour) don't include an end time - if (end) { - data.params.time_till = end; - } + // Handle multiple request + ZabbixAPIDatasource.prototype.handleMultipleRequest = function(apiRequests) { + var history = []; + var performedQuery = null; - return this.doZabbixAPIRequest(data); + // Build chain of api requests and put all history data into single array + _.each(apiRequests, function (apiRequest) { + if(!performedQuery) { + performedQuery = apiRequest.then(function (response) { + history = history.concat(response); + return history; + }); + } else { + performedQuery = performedQuery.then(function () { + return apiRequest.then(function (response) { + history = history.concat(response); + return history; + }); + }); + } + }); + + return performedQuery; }; From f9532059220110da6b1f9f2fe504a2b662d4f0c2 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Sun, 10 May 2015 09:23:08 +0300 Subject: [PATCH 15/27] Filter non-numeric items in item menu. --- zabbix/datasource.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/zabbix/datasource.js b/zabbix/datasource.js index fcdc3cf..0d55f92 100644 --- a/zabbix/datasource.js +++ b/zabbix/datasource.js @@ -314,7 +314,10 @@ function (angular, _, kbn) { params: { output: ['name', 'key_', 'value_type', 'delay'], sortfield: 'name', - hostids: hostid + hostids: hostid, + filter: { + value_type: [0,3] + } }, auth: this.auth, id: 1 From 20029dc618990ca5a5d8bb9c15b70de7223ec37e Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 11 May 2015 22:41:38 +0300 Subject: [PATCH 16/27] Include web items in the item selection. --- zabbix/datasource.js | 1 + 1 file changed, 1 insertion(+) diff --git a/zabbix/datasource.js b/zabbix/datasource.js index 0d55f92..ce15b04 100644 --- a/zabbix/datasource.js +++ b/zabbix/datasource.js @@ -315,6 +315,7 @@ function (angular, _, kbn) { output: ['name', 'key_', 'value_type', 'delay'], sortfield: 'name', hostids: hostid, + webitems: true, //Include web items in the result filter: { value_type: [0,3] } From 572bba924ee6a1e6f35a7eee012f28c0f850caf8 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 11 May 2015 22:49:05 +0300 Subject: [PATCH 17/27] Return only host groups that contain hosts. --- zabbix/datasource.js | 1 + 1 file changed, 1 insertion(+) diff --git a/zabbix/datasource.js b/zabbix/datasource.js index ce15b04..ad4ccc0 100644 --- a/zabbix/datasource.js +++ b/zabbix/datasource.js @@ -258,6 +258,7 @@ function (angular, _, kbn) { method: 'hostgroup.get', params: { output: ['name'], + real_hosts: true, //Return only host groups that contain hosts sortfield: 'name' }, auth: this.auth, From 4d733f2aad0fa8aa8de795a85b24271ed24ac1fd Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 12 May 2015 20:18:36 +0300 Subject: [PATCH 18/27] Fix "ZabbixAPIQueryCtrl not a function" error. --- zabbix/datasource.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zabbix/datasource.js b/zabbix/datasource.js index c91d479..8009463 100644 --- a/zabbix/datasource.js +++ b/zabbix/datasource.js @@ -2,7 +2,7 @@ define([ 'angular', 'lodash', 'kbn', - 'moment' + './queryCtrl', ], function (angular, _, kbn) { 'use strict'; From f4c1daa8d03974484a8f8b4aa45df7ec0f515367 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 12 May 2015 20:19:32 +0300 Subject: [PATCH 19/27] Fix update host group, host, application and item lists. --- zabbix/queryCtrl.js | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/zabbix/queryCtrl.js b/zabbix/queryCtrl.js index 0e24476..a194b27 100644 --- a/zabbix/queryCtrl.js +++ b/zabbix/queryCtrl.js @@ -21,7 +21,11 @@ function (angular, _) { // Update host group, host, application and item lists $scope.updateHostGroupList(); - $scope.updateHostList(); + if ($scope.target.hostGroup) { + $scope.updateHostList($scope.target.hostGroup.groupid); + } else { + $scope.updateHostList(); + } if ($scope.target.host) { $scope.updateAppList($scope.target.host.hostid); if ($scope.target.application) { @@ -131,7 +135,6 @@ function (angular, _) { $scope.metric.hostGroupList = series; if ($scope.target.hostGroup) { $scope.target.hostGroup = $scope.metric.hostGroupList.filter(function (item, index, array) { - // Find selected host in metric.hostList return (item.groupid == $scope.target.hostGroup.groupid); }).pop(); @@ -146,11 +149,13 @@ function (angular, _) { $scope.updateHostList = function(groupid) { $scope.datasource.performHostSuggestQuery(groupid).then(function (series) { $scope.metric.hostList = series; - $scope.target.host = $scope.metric.hostList.filter(function (item, index, array) { - // Find selected host in metric.hostList - return (item.hostid == $scope.target.host.hostid); - }).pop(); + if ($scope.target.host) { + $scope.target.host = $scope.metric.hostList.filter(function (item, index, array) { + // Find selected host in metric.hostList + return (item.hostid == $scope.target.host.hostid); + }).pop(); + } }); }; @@ -163,7 +168,6 @@ function (angular, _) { $scope.metric.applicationList = series; if ($scope.target.application) { $scope.target.application = $scope.metric.applicationList.filter(function (item, index, array) { - // Find selected application in metric.hostList return (item.applicationid == $scope.target.application.applicationid); }).pop(); @@ -188,11 +192,12 @@ function (angular, _) { item.expandedName = expandItemName(item); } }); - $scope.target.item = $scope.metric.itemList.filter(function (item, index, array) { - - // Find selected item in metric.hostList - return (item.itemid == $scope.target.item.itemid); - }).pop(); + if ($scope.target.item) { + $scope.target.item = $scope.metric.itemList.filter(function (item, index, array) { + // Find selected item in metric.hostList + return (item.itemid == $scope.target.item.itemid); + }).pop(); + } }); } else { $scope.metric.itemList = []; From 4302dc61702f0e46058ec4dd79af3adc9099d4ca Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 12 May 2015 20:30:59 +0300 Subject: [PATCH 20/27] Changes from grafana-1.9 branch. --- zabbix/datasource.js | 264 ++++++++++++++++++++++++------------------- 1 file changed, 145 insertions(+), 119 deletions(-) diff --git a/zabbix/datasource.js b/zabbix/datasource.js index 8009463..f86c273 100644 --- a/zabbix/datasource.js +++ b/zabbix/datasource.js @@ -30,9 +30,11 @@ function (angular, _, kbn) { // get from & to in seconds var from = kbn.parseDate(options.range.from).getTime(); var to = kbn.parseDate(options.range.to).getTime(); + // Need for find target alias var targets = options.targets; + // TODO: remove undefined targets from request // Check that all targets defined var targetsDefined = options.targets.every(function (target, index, array) { return target.item; @@ -41,7 +43,6 @@ function (angular, _, kbn) { // Extract zabbix api item objects from targets var target_items = _.map(options.targets, 'item'); } else { - // No valid targets, return the empty dataset var d = $q.defer(); d.resolve({ data: [] }); @@ -51,20 +52,7 @@ function (angular, _, kbn) { from = Math.ceil(from/1000); to = Math.ceil(to/1000); - var performedQuery; - - // Check authorization first - if (!this.auth) { - var self = this; - performedQuery = this.performZabbixAPILogin().then(function (response) { - self.auth = response; - return self.performTimeSeriesQuery(target_items, from, to); - }); - } else { - performedQuery = this.performTimeSeriesQuery(target_items, from, to); - } - - return performedQuery.then(function (response) { + return this.performTimeSeriesQuery(target_items, from, to).then(function (response) { /** * Response should be in the format: * data: [ @@ -80,9 +68,7 @@ function (angular, _, kbn) { */ // Index returned datapoints by item/metric id - var indexed_result = _.groupBy(response.data.result, function (history_item) { - return history_item.itemid; - }); + var indexed_result = _.groupBy(response, 'itemid'); // Reduce timeseries to the same size for stacking and tooltip work properly var min_length = _.min(_.map(indexed_result, function (history) { @@ -128,43 +114,109 @@ function (angular, _, kbn) { /////////////////////////////////////////////////////////////////////// + // Request data from Zabbix API + ZabbixAPIDatasource.prototype.doZabbixAPIRequest = function(request_data) { + var options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + url: this.url, + data: request_data + }; + + var performedQuery; + + // Check authorization first + if (!this.auth) { + var self = this; + performedQuery = this.performZabbixAPILogin().then(function (response) { + self.auth = response; + options.data.auth = response; + return $http(options); + }); + } else { + performedQuery = $http(options); + } + + // Handle response + return performedQuery.then(function (response) { + if (!response.data) { + return []; + } + return response.data.result; + }); + }; + + /** * Perform time series query to Zabbix API * * @param items: array of zabbix api item objects */ ZabbixAPIDatasource.prototype.performTimeSeriesQuery = function(items, start, end) { - var item_ids = items.map(function (item, index, array) { - return item.itemid; - }); - // TODO: if different value types passed? - // Perform multiple api request. - var hystory_type = items[0].value_type; - var options = { - method: 'POST', - url: this.url, - data: { + + // Group items by value type for separate requests + var items_by_value_type = _.groupBy(items, 'value_type'); + + var self = this; + var apiRequests = []; + + // Prepare requests for each value type + _.each(items_by_value_type, function (value, key, list) { + var item_ids = _.map(value, 'itemid'); + var history_type = key; + var data = { jsonrpc: '2.0', method: 'history.get', params: { - output: 'extend', - history: hystory_type, - itemids: item_ids, - sortfield: 'clock', - sortorder: 'ASC', - limit: this.limitmetrics, - time_from: start, + output: 'extend', + history: history_type, + itemids: item_ids, + sortfield: 'clock', + sortorder: 'ASC', + limit: self.limitmetrics, + time_from: start, }, - auth: this.auth, + auth: self.auth, id: 1 - }, - }; - // Relative queries (e.g. last hour) don't include an end time - if (end) { - options.data.params.time_till = end; - } + }; - return $http(options); + // Relative queries (e.g. last hour) don't include an end time + if (end) { + data.params.time_till = end; + } + + apiRequests.push(self.doZabbixAPIRequest(data)); + }); + + return this.handleMultipleRequest(apiRequests); + }; + + + // Handle multiple request + ZabbixAPIDatasource.prototype.handleMultipleRequest = function(apiRequests) { + var history = []; + var performedQuery = null; + + // Build chain of api requests and put all history data into single array + _.each(apiRequests, function (apiRequest) { + if(!performedQuery) { + performedQuery = apiRequest.then(function (response) { + history = history.concat(response); + return history; + }); + } else { + performedQuery = performedQuery.then(function () { + return apiRequest.then(function (response) { + history = history.concat(response); + return history; + }); + }); + } + }); + + return performedQuery; }; @@ -184,6 +236,7 @@ function (angular, _, kbn) { id: 1 }, }; + return $http(options).then(function (result) { if (!result.data) { return null; @@ -195,110 +248,83 @@ function (angular, _, kbn) { // Get the list of host groups ZabbixAPIDatasource.prototype.performHostGroupSuggestQuery = function() { - var options = { - url : this.url, - method : 'POST', - data: { - jsonrpc: '2.0', - method: 'hostgroup.get', - params: { - output: ['name'], - sortfield: 'name' - }, - auth: this.auth, - id: 1 + var data = { + jsonrpc: '2.0', + method: 'hostgroup.get', + params: { + output: ['name'], + real_hosts: true, //Return only host groups that contain hosts + sortfield: 'name' }, + auth: this.auth, + id: 1 }; - return $http(options).then(function (result) { - if (!result.data) { - return []; - } - return result.data.result; - }); + + return this.doZabbixAPIRequest(data); }; // Get the list of hosts ZabbixAPIDatasource.prototype.performHostSuggestQuery = function(groupid) { - var options = { - url : this.url, - method : 'POST', - data: { - jsonrpc: '2.0', - method: 'host.get', - params: { - output: ['name'], - sortfield: 'name' - }, - auth: this.auth, - id: 1 + var data = { + jsonrpc: '2.0', + method: 'host.get', + params: { + output: ['name'], + sortfield: 'name' }, + auth: this.auth, + id: 1 }; if (groupid) { - options.data.params.groupids = groupid; + data.params.groupids = groupid; } - return $http(options).then(function (result) { - if (!result.data) { - return []; - } - return result.data.result; - }); + + return this.doZabbixAPIRequest(data); }; // Get the list of applications ZabbixAPIDatasource.prototype.performAppSuggestQuery = function(hostid) { - var options = { - url : this.url, - method : 'POST', - data: { - jsonrpc: '2.0', - method: 'application.get', - params: { - output: ['name'], - sortfield: 'name', - hostids: hostid - }, - auth: this.auth, - id: 1 + var data = { + jsonrpc: '2.0', + method: 'application.get', + params: { + output: ['name'], + sortfield: 'name', + hostids: hostid }, + auth: this.auth, + id: 1 }; - return $http(options).then(function (result) { - if (!result.data) { - return []; - } - return result.data.result; - }); + + return this.doZabbixAPIRequest(data); }; // Get the list of host items ZabbixAPIDatasource.prototype.performItemSuggestQuery = function(hostid, applicationid) { - var options = { - url : this.url, - method : 'POST', - data: { - jsonrpc: '2.0', - method: 'item.get', - params: { - output: ['name', 'key_', 'value_type', 'delay'], - sortfield: 'name', - hostids: hostid - }, - auth: this.auth, - id: 1 + var data = { + jsonrpc: '2.0', + method: 'item.get', + params: { + output: ['name', 'key_', 'value_type', 'delay'], + sortfield: 'name', + hostids: hostid, + webitems: true, //Include web items in the result + filter: { + value_type: [0,3] + } }, + auth: this.auth, + id: 1 }; // If application selected return only relative items if (applicationid) { - options.data.params.applicationids = applicationid; + data.params.applicationids = applicationid; } - return $http(options).then(function (result) { - if (!result.data) { - return []; - } - return result.data.result; - }); + + return this.doZabbixAPIRequest(data); }; From 4a9885946b882abeee5fef5fda9ae8e105d1c50c Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 12 May 2015 20:41:10 +0300 Subject: [PATCH 21/27] limitmetrics renamed to limitMetrics. --- zabbix/datasource.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/zabbix/datasource.js b/zabbix/datasource.js index f86c273..67f3184 100644 --- a/zabbix/datasource.js +++ b/zabbix/datasource.js @@ -22,7 +22,7 @@ function (angular, _, kbn) { this.password = datasource.meta.password; // No datapoints limit by default - this.limitmetrics = datasource.limitmetrics || 0; + this.limitMetrics = datasource.limitMetrics || 0; } @@ -175,7 +175,7 @@ function (angular, _, kbn) { itemids: item_ids, sortfield: 'clock', sortorder: 'ASC', - limit: self.limitmetrics, + limit: self.limitMetrics, time_from: start, }, auth: self.auth, @@ -344,7 +344,7 @@ function (angular, _, kbn) { params: { output: ['triggerid', 'description'], itemids: annotation.aids.split(','), // TODO: validate / pull automatically from dashboard. - limit: self.limitmetrics, + limit: self.limitMetrics, }, auth: self.auth, id: 1 @@ -367,7 +367,7 @@ function (angular, _, kbn) { time_from: from, time_till: to, objectids: _.keys(obs), - limit: self.limitmetrics, + limit: self.limitMetrics, }, auth: self.auth, id: 1 From 69d781e6982ec5ebfdc89833581ddac900aa3a24 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 12 May 2015 22:58:34 +0300 Subject: [PATCH 22/27] Replace $http to backendSrv for grafana-2-like request style. --- zabbix/datasource.js | 21 +++++++++++++-------- zabbix/partials/config.html | 7 +++---- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/zabbix/datasource.js b/zabbix/datasource.js index 67f3184..5ca2ac1 100644 --- a/zabbix/datasource.js +++ b/zabbix/datasource.js @@ -9,12 +9,12 @@ function (angular, _, kbn) { var module = angular.module('grafana.services'); - module.factory('ZabbixAPIDatasource', function($q, $http, templateSrv) { + module.factory('ZabbixAPIDatasource', function($q, backendSrv, templateSrv) { function ZabbixAPIDatasource(datasource) { this.name = datasource.name; - this.type = 'ZabbixAPIDatasource'; - this.supportMetrics = true; + this.type = 'zabbix'; + this.url = datasource.url; // TODO: fix passing username and password from config.html @@ -23,6 +23,11 @@ function (angular, _, kbn) { // No datapoints limit by default this.limitMetrics = datasource.limitMetrics || 0; + this.supportMetrics = true; + this.supportAnnotations = true; + + // For testing + this.ds = datasource; } @@ -133,10 +138,10 @@ function (angular, _, kbn) { performedQuery = this.performZabbixAPILogin().then(function (response) { self.auth = response; options.data.auth = response; - return $http(options); + return backendSrv.datasourceRequest(options); }); } else { - performedQuery = $http(options); + performedQuery = backendSrv.datasourceRequest(options); } // Handle response @@ -237,7 +242,7 @@ function (angular, _, kbn) { }, }; - return $http(options).then(function (result) { + return backendSrv.datasourceRequest(options).then(function (result) { if (!result.data) { return null; } @@ -351,7 +356,7 @@ function (angular, _, kbn) { }, }; - return $http(tid_options).then(function(result) { + return backendSrv.datasourceRequest(tid_options).then(function(result) { var obs = {}; obs = _.indexBy(result.data.result, 'triggerid'); @@ -374,7 +379,7 @@ function (angular, _, kbn) { }, }; - return $http(options).then(function(result2) { + return backendSrv.datasourceRequest(options).then(function(result2) { var list = []; _.each(result2.data.result, function(e) { list.push({ diff --git a/zabbix/partials/config.html b/zabbix/partials/config.html index 81f0e44..a8f4506 100644 --- a/zabbix/partials/config.html +++ b/zabbix/partials/config.html @@ -1,22 +1,21 @@

-
Zabbix API details
-
+
  • User
  • - +
  • Password
  • - +
From 330d90053fe8f07525d85366a319628b5bab599e Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 14 May 2015 23:50:36 +0300 Subject: [PATCH 23/27] Fix #8 and #10 issues. Remove timeseries reduce (not working properly). --- zabbix/datasource.js | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/zabbix/datasource.js b/zabbix/datasource.js index ad4ccc0..f95c9e8 100644 --- a/zabbix/datasource.js +++ b/zabbix/datasource.js @@ -39,14 +39,14 @@ function (angular, _, kbn) { // Need for find target alias var targets = options.targets; - // TODO: remove undefined targets from request - // Check that all targets defined - var targetsDefined = options.targets.every(function (target, index, array) { - return target.item; + // Remove undefined and hidden targets + var displayedTargets = _.filter(targets, function (target) { + return (!target.hide && target.item); }); - if (targetsDefined) { + + if (displayedTargets.length) { // Extract zabbix api item objects from targets - var target_items = _.map(options.targets, 'item'); + var target_items = _.map(displayedTargets, 'item'); } else { // No valid targets, return the empty dataset var d = $q.defer(); @@ -75,13 +75,15 @@ function (angular, _, kbn) { // Index returned datapoints by item/metric id var indexed_result = _.groupBy(response, 'itemid'); + // TODO: realize correct timeseries reduce + /* // Reduce timeseries to the same size for stacking and tooltip work properly var min_length = _.min(_.map(indexed_result, function (history) { return history.length; })); _.each(indexed_result, function (item) { item.splice(0, item.length - min_length); - }); + });*/ // Sort result as the same as targets for display // stacked timeseries in proper order @@ -120,7 +122,7 @@ function (angular, _, kbn) { // Request data from Zabbix API - ZabbixAPIDatasource.prototype.doZabbixAPIRequest = function(request_data) { + ZabbixAPIDatasource.prototype.performZabbixAPIRequest = function(request_data) { var options = { method: 'POST', headers: { @@ -192,7 +194,7 @@ function (angular, _, kbn) { data.params.time_till = end; } - apiRequests.push(self.doZabbixAPIRequest(data)); + apiRequests.push(self.performZabbixAPIRequest(data)); }); return this.handleMultipleRequest(apiRequests); @@ -265,7 +267,7 @@ function (angular, _, kbn) { id: 1 }; - return this.doZabbixAPIRequest(data); + return this.performZabbixAPIRequest(data); }; @@ -285,7 +287,7 @@ function (angular, _, kbn) { data.params.groupids = groupid; } - return this.doZabbixAPIRequest(data); + return this.performZabbixAPIRequest(data); }; @@ -303,7 +305,7 @@ function (angular, _, kbn) { id: 1 }; - return this.doZabbixAPIRequest(data); + return this.performZabbixAPIRequest(data); }; @@ -329,7 +331,7 @@ function (angular, _, kbn) { data.params.applicationids = applicationid; } - return this.doZabbixAPIRequest(data); + return this.performZabbixAPIRequest(data); }; From bdae2d6e5ec96b4a5ac2d8e92521529c8ce2083a Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 14 May 2015 23:57:36 +0300 Subject: [PATCH 24/27] Fix #7 issue. --- zabbix/queryCtrl.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/zabbix/queryCtrl.js b/zabbix/queryCtrl.js index e367f49..e64432c 100644 --- a/zabbix/queryCtrl.js +++ b/zabbix/queryCtrl.js @@ -65,17 +65,18 @@ function (angular, _) { // Call when host selected $scope.selectHost = function() { + if ($scope.target.host) { + // Update item list + if ($scope.target.application) { + $scope.updateItemList($scope.target.host.hostid, $scope.target.application.applicationid); + } else { + $scope.updateItemList($scope.target.host.hostid, null); + } - // Update item list - if ($scope.target.application) { - $scope.updateItemList($scope.target.host.hostid, $scope.target.application.applicationid); - } else { - $scope.updateItemList($scope.target.host.hostid, null); + // Update application list + $scope.updateAppList($scope.target.host.hostid); } - // Update application list - $scope.updateAppList($scope.target.host.hostid); - $scope.target.errors = validateTarget($scope.target); if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) { $scope.oldTarget = angular.copy($scope.target); From 2b2f9979335f1fffa42a831d20ceb39eadd5edd3 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 15 May 2015 00:17:14 +0300 Subject: [PATCH 25/27] Implement #9 (take alias from zabbix item name). --- zabbix/queryCtrl.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/zabbix/queryCtrl.js b/zabbix/queryCtrl.js index e64432c..e0a6835 100644 --- a/zabbix/queryCtrl.js +++ b/zabbix/queryCtrl.js @@ -6,7 +6,7 @@ function (angular, _) { 'use strict'; var module = angular.module('grafana.controllers'); - var targetLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + var targetLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; module.controller('ZabbixAPITargetCtrl', function($scope) { @@ -35,10 +35,20 @@ function (angular, _) { } } + setItemAlias(); + $scope.target.errors = validateTarget($scope.target); }; + // Take alias from item name by default + function setItemAlias() { + if (!$scope.target.alias && $scope.target.item) { + $scope.target.alias = $scope.target.item.expandedName; + } + }; + $scope.targetBlur = function() { + setItemAlias(); $scope.target.errors = validateTarget($scope.target); if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) { $scope.oldTarget = angular.copy($scope.target); @@ -105,6 +115,7 @@ function (angular, _) { // Call when item selected $scope.selectItem = function() { + setItemAlias(); $scope.target.errors = validateTarget($scope.target); if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) { $scope.oldTarget = angular.copy($scope.target); From 7b5f056d1834a1344c71c824dfdf64c159633136 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Sat, 16 May 2015 19:17:36 +0300 Subject: [PATCH 26/27] Query editor UI fixes. --- zabbix/partials/config.html | 3 +- zabbix/partials/query.editor.html | 151 +++++++++++++----------------- 2 files changed, 68 insertions(+), 86 deletions(-) diff --git a/zabbix/partials/config.html b/zabbix/partials/config.html index a8f4506..7dbe967 100644 --- a/zabbix/partials/config.html +++ b/zabbix/partials/config.html @@ -1,9 +1,10 @@

+
Zabbix API details
-
+
- - - + -