From 9131a83f8498ca28b33cf7896de9e0f3e058e802 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 5 May 2015 13:34:00 +0300 Subject: [PATCH 01/16] 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 a2e1e0dfc9cb26670cf747bb6391bd9f3253c941 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 5 May 2015 18:19:04 +0300 Subject: [PATCH 02/16] 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 03/16] 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 04/16] 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 05/16] 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 06/16] 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 07/16] 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 08/16] 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 09/16] 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 10/16] 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 11/16] 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 12/16] 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 13/16] 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 330d90053fe8f07525d85366a319628b5bab599e Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 14 May 2015 23:50:36 +0300 Subject: [PATCH 14/16] 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 15/16] 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 16/16] 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);