diff --git a/README.md b/README.md index 2450443..3b2d7db 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,18 @@ # grafana-zabbix - #### Zabbix API datasource for Grafana dashboard Display your Zabbix data directly in Grafana dashboards! Useful metric editor with host group and application filtering: -![grafana - zabbix datasource](https://cloud.githubusercontent.com/assets/4932851/7441162/4f6af788-f0e4-11e4-887b-34d987d00c40.png) +![alt tag](https://cloud.githubusercontent.com/assets/4932851/7454206/34bf9f8c-f27a-11e4-8e96-a73829f188c4.png) +![alt tag](https://cloud.githubusercontent.com/assets/4932851/7441162/4f6af788-f0e4-11e4-887b-34d987d00c40.png) + ## Installation ### Grafana 1.9.x -Download [latest release](https://github.com/alexanderzobnin/grafana-zabbix/releases/latest) and unpack `zabbix` directory into `/plugins/datasource/`. Then edit Grafana config.js: +Download latest release and unpack into `/plugins/datasource/`. Then edit Grafana config.js: * Add dependencies ``` @@ -36,4 +37,33 @@ Download [latest release](https://github.com/alexanderzobnin/grafana-zabbix/rele ``` ### Grafana 2.0.x -Now in development. +Download source code and put `zabbix` directory into `/public/app/plugins/datasource/`. + * Edit plugin.json (located in `zabbix` directory) and set your `username` and `password` + + ``` + { + "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, + "annotations": true + } + + ``` + * Restart grafana server. + * Add zabbix datasource in Grafana's "Data Sources" menu (see [Data Sources docs](http://docs.grafana.org/datasources/graphite/) for more info) and setup your Zabbix API url. + * **Important!** Change `Access` to `direct`! diff --git a/zabbix/datasource.js b/zabbix/datasource.js new file mode 100644 index 0000000..45c38b8 --- /dev/null +++ b/zabbix/datasource.js @@ -0,0 +1,401 @@ +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 = 'zabbix'; + + this.url = datasource.url; + + // TODO: fix passing username and password from config.html + this.username = datasource.meta.username; + this.password = datasource.meta.password; + + // No datapoints limit by default + this.limitMetrics = datasource.limitMetrics || 0; + this.supportMetrics = true; + this.supportAnnotations = true; + + // For testing + this.ds = datasource; + } + + + 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; + + // Remove undefined and hidden targets + var displayedTargets = _.filter(targets, function (target) { + return (!target.hide && target.item); + }); + + if (displayedTargets.length) { + // Extract zabbix api item objects from targets + var target_items = _.map(displayedTargets, '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, '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 + 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 + /////////////////////////////////////////////////////////////////////// + + + // Request data from Zabbix API + ZabbixAPIDatasource.prototype.performZabbixAPIRequest = 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 backendSrv.datasourceRequest(options); + }); + } else { + performedQuery = backendSrv.datasourceRequest(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) { + + // 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.performZabbixAPIRequest(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; + }; + + + // 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 backendSrv.datasourceRequest(options).then(function (result) { + if (!result.data) { + return null; + } + return result.data.result; + }); + }; + + + // Get the list of host groups + ZabbixAPIDatasource.prototype.performHostGroupSuggestQuery = function() { + 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 this.performZabbixAPIRequest(data); + }; + + + // Get the list of hosts + ZabbixAPIDatasource.prototype.performHostSuggestQuery = function(groupid) { + var data = { + jsonrpc: '2.0', + method: 'host.get', + params: { + output: ['name'], + sortfield: 'name' + }, + auth: this.auth, + id: 1 + }; + if (groupid) { + data.params.groupids = groupid; + } + + return this.performZabbixAPIRequest(data); + }; + + + // Get the list of applications + ZabbixAPIDatasource.prototype.performAppSuggestQuery = function(hostid) { + var data = { + jsonrpc: '2.0', + method: 'application.get', + params: { + output: ['name'], + sortfield: 'name', + hostids: hostid + }, + auth: this.auth, + id: 1 + }; + + return this.performZabbixAPIRequest(data); + }; + + + // Get the list of host items + ZabbixAPIDatasource.prototype.performItemSuggestQuery = function(hostid, applicationid) { + 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) { + data.params.applicationids = applicationid; + } + + return this.performZabbixAPIRequest(data); + }; + + + 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 backendSrv.datasourceRequest(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 backendSrv.datasourceRequest(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..7dbe967 --- /dev/null +++ b/zabbix/partials/config.html @@ -0,0 +1,25 @@ +
+ +
+ +
Zabbix API details
+ +
+
    +
  • + User +
  • +
  • + +
  • +
  • + Password +
  • +
  • + +
  • +
+
+
+ + diff --git a/zabbix/partials/query.editor.html b/zabbix/partials/query.editor.html new file mode 100644 index 0000000..a480c0a --- /dev/null +++ b/zabbix/partials/query.editor.html @@ -0,0 +1,124 @@ +
+ +
+ +
+ + +
    +
  • + {{targetLetters[$index]}} +
  • +
  • + + + +
  • +
+ + + +
+
+
+
diff --git a/zabbix/plugin.json b/zabbix/plugin.json new file mode 100644 index 0000000..8fd4ad4 --- /dev/null +++ b/zabbix/plugin.json @@ -0,0 +1,21 @@ +{ + "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, + "annotations": true +} diff --git a/zabbix/queryCtrl.js b/zabbix/queryCtrl.js new file mode 100644 index 0000000..2c511dc --- /dev/null +++ b/zabbix/queryCtrl.js @@ -0,0 +1,255 @@ +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(); + 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) { + $scope.updateItemList($scope.target.host.hostid, $scope.target.application.applicationid); + } else { + $scope.updateItemList($scope.target.host.hostid, null); + } + } + + 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); + $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() { + 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 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() { + setItemAlias(); + $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; + + 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(); + } + }); + }; + + + /** + * 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); + } + }); + 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 = []; + } + }; + + + /** + * 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; + } + + }); + +});