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; + } + + }); + +});