diff --git a/.gitignore b/.gitignore index b543f8f..20d58b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ *.sublime-workspace *.sublime-project + +.idea/ \ No newline at end of file diff --git a/zabbix/datasource.js b/zabbix/datasource.js index d1e4b04..7e487a5 100644 --- a/zabbix/datasource.js +++ b/zabbix/datasource.js @@ -2,7 +2,7 @@ define([ 'angular', 'lodash', 'kbn', - './queryCtrl', + './queryCtrl' ], function (angular, _, kbn) { 'use strict'; @@ -11,6 +11,12 @@ function (angular, _, kbn) { module.factory('ZabbixAPIDatasource', function($q, backendSrv, templateSrv) { + /** + * Datasource initialization. Calls when you refresh page, add + * or modify datasource. + * + * @param {Object} datasource Grafana datasource object. + */ function ZabbixAPIDatasource(datasource) { this.name = datasource.name; this.url = datasource.url; @@ -19,12 +25,22 @@ function (angular, _, kbn) { this.username = datasource.meta.username; this.password = datasource.meta.password; - // For testing - this.ds = datasource; + // Limit metrics per panel for templated request + this.limitmetrics = datasource.meta.limitmetrics || 50; } + /** + * Calls for each panel in dashboard. + * + * @param {Object} options Query options. Contains time range, targets + * and other info. + * + * @return {Object} Grafana metrics object with timeseries data + * for each target. + */ ZabbixAPIDatasource.prototype.query = function(options) { + // get from & to in seconds var from = Math.ceil(kbn.parseDate(options.range.from).getTime() / 1000); var to = Math.ceil(kbn.parseDate(options.range.to).getTime() / 1000); @@ -32,24 +48,113 @@ function (angular, _, kbn) { // Create request for each target var promises = _.map(options.targets, function(target) { - // Remove undefined and hidden targets - if (target.hide || !target.item) { + // Don't show undefined and hidden targets + if (target.hide || !target.group || !target.host + || !target.application || !target.item) { return []; } - // Perform request and then handle result - return this.performTimeSeriesQuery(target.item, from, to).then(_.partial( - this.handleZabbixAPIResponse, target)); + // Replace templated variables + var groupname = templateSrv.replace(target.group.name); + var hostname = templateSrv.replace(target.host.name); + var appname = templateSrv.replace(target.application.name); + var itemname = templateSrv.replace(target.item.name); + + // Extract zabbix groups, hosts and apps from string: + // "{host1,host2,...,hostN}" --> [host1, host2, ..., hostN] + var groups = splitMetrics(groupname); + var hosts = splitMetrics(hostname); + var apps = splitMetrics(appname); + + // Remove hostnames from item names and then + // extract item names + // "hostname: itemname" --> "itemname" + var delete_hostname_pattern = /(?:\[[\w\.]+\]\:\s)/g; + var itemnames = splitMetrics(itemname.replace(delete_hostname_pattern, '')); + + // Find items by item names and perform queries + var self = this; + return this.itemFindQuery(groups, hosts, apps) + .then(function (items) { + if (itemnames == 'All') { + return items; + } else { + + // Filtering items + return _.filter(items, function (item) { + return _.contains(itemnames, expandItemName(item)); + }); + } + }).then(function (items) { + + // Don't perform query for high number of items + // to prevent Grafana slowdown + if (items.length > self.limitmetrics) { + return []; + } else { + items = _.flatten(items); + return self.performTimeSeriesQuery(items, from, to) + .then(_.partial(self.handleHistoryResponse, items)); + } + }); }, this); - return $q.all(promises).then(function(results) { + return $q.all(_.flatten(promises)).then(function (results) { return { data: _.flatten(results) }; }); }; - // Request data from Zabbix API - ZabbixAPIDatasource.prototype.handleZabbixAPIResponse = function(target, response) { + /** + * Perform history query from Zabbix API + * + * @param {Array} items Array of Zabbix item objects + * @param {Number} start Time in seconds + * @param {Number} end Time in seconds + * + * @return {Array} Array of Zabbix history objects + */ + ZabbixAPIDatasource.prototype.performTimeSeriesQuery = function(items, start, end) { + // Group items by value type + var grouped_items = _.groupBy(items, 'value_type'); + + // Perform request for each value type + return $q.all(_.map(grouped_items, function (items, value_type) { + var itemids = _.map(items, 'itemid'); + var params = { + output: 'extend', + history: value_type, + itemids: itemids, + sortfield: 'clock', + sortorder: 'ASC', + time_from: start + }; + + // Relative queries (e.g. last hour) don't include an end time + if (end) { + params.time_till = end; + } + + return this.performZabbixAPIRequest('history.get', params); + }, this)).then(function (results) { + return _.flatten(results); + }); + }; + + + /** + * Convert Zabbix API data to Grafana format + * + * @param {Array} items Array of Zabbix Items + * @param {Array} history Array of Zabbix History + * + * @return {Array} Array of timeseries in Grafana format + * { + * target: "Metric name", + * datapoints: [[, ], ...] + * } + */ + ZabbixAPIDatasource.prototype.handleHistoryResponse = function(items, history) { /** * Response should be in the format: * data: [ @@ -64,20 +169,34 @@ function (angular, _, kbn) { * ] */ - var series = { - target: target.alias, - datapoints: _.map(response, function (p) { - // Value must be a number for properly work - var value = Number(p.value); - return [value, p.clock * 1000]; - }) - }; + // Group items and history by itemid + var indexed_items = _.indexBy(items, 'itemid'); + var grouped_history = _.groupBy(history, 'itemid'); - return $q.when(series); + return $q.when(_.map(grouped_history, function (history, itemid) { + var item = indexed_items[itemid]; + var series = { + target: (item.hosts ? item.hosts[0].name+': ' : '') + expandItemName(item), + datapoints: _.map(history, function (p) { + + // Value must be a number for properly work + var value = Number(p.value); + return [value, p.clock * 1000]; + }) + }; + return series; + })); }; - // Request data from Zabbix API + /** + * Request data from Zabbix API + * + * @param {string} method Zabbix API method name + * @param {object} params method params + * + * @return {object} data.result field or [] + */ ZabbixAPIDatasource.prototype.performZabbixAPIRequest = function(method, params) { var options = { method: 'POST', @@ -94,54 +213,28 @@ function (angular, _, kbn) { } }; - 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) { + var self = this; + return backendSrv.datasourceRequest(options).then(function (response) { if (!response.data) { return []; } + // Handle Zabbix API errors + else if (response.data.error) { + + // Handle auth errors + if (response.data.error.data == "Session terminated, re-login, please." || + response.data.error.data == 'Not authorised.') { + return self.performZabbixAPILogin().then(function (response) { + self.auth = response; + return self.performZabbixAPIRequest(method, params); + }); + } + } 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 params = { - output: 'extend', - history: items.value_type, - itemids: items.itemid, - sortfield: 'clock', - sortorder: 'ASC', - time_from: start - }; - - // Relative queries (e.g. last hour) don't include an end time - if (end) { - params.time_till = end; - } - - return this.performZabbixAPIRequest('history.get', params); - }; - - // Get authentication token ZabbixAPIDatasource.prototype.performZabbixAPILogin = function() { var options = { @@ -172,8 +265,11 @@ function (angular, _, kbn) { ZabbixAPIDatasource.prototype.performHostGroupSuggestQuery = function() { var params = { output: ['name'], - real_hosts: true, //Return only host groups that contain hosts - sortfield: 'name' + sortfield: 'name', + // Return only host groups that contain hosts + real_hosts: true, + // Return only host groups that contain monitored hosts. + monitored_hosts: true }; return this.performZabbixAPIRequest('hostgroup.get', params); @@ -181,100 +277,334 @@ function (angular, _, kbn) { // Get the list of hosts - ZabbixAPIDatasource.prototype.performHostSuggestQuery = function(groupid) { + ZabbixAPIDatasource.prototype.performHostSuggestQuery = function(groupids) { var params = { - output: ['name'], - sortfield: 'name' + output: ['name', 'host'], + sortfield: 'name', + // Return only hosts that have items with numeric type of information. + with_simple_graph_items: true, + // Return only monitored hosts. + monitored_hosts: true }; // Return only hosts in given group - if (groupid) { - params.groupids = groupid; + if (groupids) { + params.groupids = groupids; } return this.performZabbixAPIRequest('host.get', params); }; // Get the list of applications - ZabbixAPIDatasource.prototype.performAppSuggestQuery = function(hostid) { + ZabbixAPIDatasource.prototype.performAppSuggestQuery = function(hostids, /* optional */ groupids) { var params = { output: ['name'], - sortfield: 'name', - hostids: hostid + sortfield: 'name' }; + if (hostids) { + params.hostids = hostids; + } + else if (groupids) { + params.groupids = groupids; + } return this.performZabbixAPIRequest('application.get', params); }; - // Get the list of host items - ZabbixAPIDatasource.prototype.performItemSuggestQuery = function(hostid, applicationid) { + /** + * Items request + * + * @param {string or Array} hostids /////////////////////////// + * @param {string or Array} applicationids // Zabbix API parameters // + * @param {string or Array} groupids /////////////////////////// + * + * @return {string or Array} Array of Zabbix API item objects + */ + ZabbixAPIDatasource.prototype.performItemSuggestQuery = function(hostids, applicationids, /* optional */ groupids) { var params = { output: ['name', 'key_', 'value_type', 'delay'], sortfield: 'name', - hostids: hostid, - //Include web items in the result webitems: true, // Return only numeric items filter: { value_type: [0,3] - } + }, + // Return only enabled items + monitored: true, + searchByAny: true }; + + // Filter by hosts or by groups + if (hostids) { + params.hostids = hostids; + } else if (groupids) { + params.groupids = groupids; + } + // If application selected return only relative items - if (applicationid) { - params.applicationids = applicationid; + if (applicationids) { + params.applicationids = applicationids; + } + + // Return host property for multiple hosts + if (!hostids || (_.isArray(hostids) && hostids.length > 1)) { + params.selectHosts = ['name']; } return this.performZabbixAPIRequest('item.get', params); }; - // For templated query - ZabbixAPIDatasource.prototype.metricFindQuery = function (query) { - var interpolated; - try { - interpolated = templateSrv.replace(query); - } - catch (err) { - return $q.reject(err); - } - - var parts = interpolated.split('.'); - var template = { - 'group': parts[0], - 'host': parts[1], - 'item': parts[2] - }; - + /** + * Find groups by names + * + * @param {string or array} group group names + * @return {array} array of Zabbix API hostgroup objects + */ + ZabbixAPIDatasource.prototype.findZabbixGroup = function (group) { var params = { output: ['name'], - sortfield: 'name', - // Case insensitive search search: { - name : template.group + name: group + }, + searchByAny: true, + searchWildcardsEnabled: true + } + return this.performZabbixAPIRequest('hostgroup.get', params); + }; + + + /** + * Find hosts by names + * + * @param {string or array} hostnames hosts names + * @return {array} array of Zabbix API host objects + */ + ZabbixAPIDatasource.prototype.findZabbixHost = function (hostnames) { + var params = { + output: ['host', 'name'], + search: { + host: hostnames, + name: hostnames + }, + searchByAny: true, + searchWildcardsEnabled: true + } + return this.performZabbixAPIRequest('host.get', params); + }; + + + /** + * Find applications by names + * + * @param {string or array} application applications names + * @return {array} array of Zabbix API application objects + */ + ZabbixAPIDatasource.prototype.findZabbixApp = function (application) { + var params = { + output: ['name'], + search: { + name: application + }, + searchByAny: true, + searchWildcardsEnabled: true, + } + return this.performZabbixAPIRequest('application.get', params); + }; + + + /** + * For templated query. + * Find metrics from templated request. + * + * @param {string} query Query from Templating + * @return {string} Metric name - group, host, app or item or list + * of metrics in "{metric1,metcic2,...,metricN}" format. + */ + ZabbixAPIDatasource.prototype.metricFindQuery = function (query) { + // Split query. Query structure: + // group.host.app.item + var parts = []; + _.each(query.split('.'), function (part) { + part = templateSrv.replace(part); + if (part[0] === '{') { + // Convert multiple mettrics to array + // "{metric1,metcic2,...,metricN}" --> [metric1, metcic2,..., metricN] + parts.push(splitMetrics(part)); + } else { + parts.push(part); } - }; + }); + var template = _.object(['group', 'host', 'app', 'item'], parts) + + // Get items + if (parts.length === 4) { + return this.itemFindQuery(template.group, template.host, template.app).then(function (result) { + return _.map(result, function (item) { + var itemname = expandItemName(item) + return { + text: itemname, + expandable: false + }; + }); + }); + } + // Get applications + else if (parts.length === 3) { + return this.appFindQuery(template.host, template.group).then(function (result) { + return _.map(result, function (app) { + return { + text: app.name, + expandable: false + }; + }); + }); + } + // Get hosts + else if (parts.length === 2) { + return this.hostFindQuery(template.group).then(function (result) { + return _.map(result, function (host) { + return { + text: host.name, + expandable: false + }; + }); + }); + } + // Get groups + else if (parts.length === 1) { + return this.performHostGroupSuggestQuery().then(function (result) { + return _.map(result, function (hostgroup) { + return { + text: hostgroup.name, + expandable: false + }; + }); + }); + } + // Return empty object for invalid request + else { + var d = $q.defer(); + d.resolve([]); + return d.promise; + } + }; + + + /** + * Find items belongs to passed groups, hosts and + * applications + * + * @param {string or array} groups + * @param {string or array} hosts + * @param {string or array} apps + * + * @return {array} array of Zabbix API item objects + */ + ZabbixAPIDatasource.prototype.itemFindQuery = function(groups, hosts, apps) { + var promises = []; + + // Get hostids from names + if (hosts && hosts != '*') { + promises.push(this.findZabbixHost(hosts)); + } + // Get groupids from names + else if (groups) { + promises.push(this.findZabbixGroup(groups)); + } + // Get applicationids from names + if (apps) { + promises.push(this.findZabbixApp(apps)); + } var self = this; - return this.performZabbixAPIRequest('hostgroup.get', params) - .then(function (result) { - var groupid = null; - if (result.length && template.group) { - groupid = result[0].groupid; - } - return self.performHostSuggestQuery(groupid) - .then(function (result) { - return _.map(result, function (host) { - return { - text: host.name, - expandable: false - }; - }); - }); - }); + return $q.all(promises).then(function (results) { + results = _.flatten(results); + if (groups) { + var groupids = _.map(_.filter(results, function (object) { + return object.groupid; + }), 'groupid'); + } + if (hosts && hosts != '*') { + var hostids = _.map(_.filter(results, function (object) { + return object.hostid; + }), 'hostid'); + } + if (apps) { + var applicationids = _.map(_.filter(results, function (object) { + return object.applicationid; + }), 'applicationid'); + } + + return self.performItemSuggestQuery(hostids, applicationids, groupids); + }); }; + /** + * Find applications belongs to passed groups and hosts + * + * @param {string or array} hosts + * @param {string or array} groups + * + * @return {array} array of Zabbix API application objects + */ + ZabbixAPIDatasource.prototype.appFindQuery = function(hosts, groups) { + var promises = []; + + // Get hostids from names + if (hosts && hosts != '*') { + promises.push(this.findZabbixHost(hosts)); + } + // Get groupids from names + else if (groups) { + promises.push(this.findZabbixGroup(groups)); + } + + var self = this; + return $q.all(promises).then(function (results) { + results = _.flatten(results); + if (groups) { + var groupids = _.map(_.filter(results, function (object) { + return object.groupid; + }), 'groupid'); + } + if (hosts && hosts != '*') { + var hostids = _.map(_.filter(results, function (object) { + return object.hostid; + }), 'hostid'); + } + + return self.performAppSuggestQuery(hostids, groupids); + }); + }; + + + /** + * Find hosts belongs to passed groups + * + * @param {string or array} groups + * @return {array} array of Zabbix API host objects + */ + ZabbixAPIDatasource.prototype.hostFindQuery = function(groups) { + var self = this; + return this.findZabbixGroup(groups).then(function (results) { + results = _.flatten(results); + var groupids = _.map(_.filter(results, function (object) { + return object.groupid; + }), 'groupid'); + + return self.performHostSuggestQuery(groupids); + }); + }; + + + ///////////////// + // Annotations // + ///////////////// + + ZabbixAPIDatasource.prototype.annotationQuery = function(annotation, rangeUnparsed) { var from = Math.ceil(kbn.parseDate(rangeUnparsed.from).getTime() / 1000); var to = Math.ceil(kbn.parseDate(rangeUnparsed.to).getTime() / 1000); @@ -287,37 +617,76 @@ function (angular, _, kbn) { }, }; - return this.performZabbixAPIRequest('trigger.get', params).then(function (result) { - if(result) { - var obs = {}; - obs = _.indexBy(result, 'triggerid'); + return this.performZabbixAPIRequest('trigger.get', params) + .then(function (result) { + if(result) { + var obs = {}; + obs = _.indexBy(result, 'triggerid'); - var params = { - output: 'extend', - sortorder: 'DESC', - time_from: from, - time_till: to, - objectids: _.keys(obs) - }; + var params = { + output: 'extend', + sortorder: 'DESC', + time_from: from, + time_till: to, + objectids: _.keys(obs) + }; - return self.performZabbixAPIRequest('event.get', params).then(function (result) { - var events = []; - _.each(result, function(e) { - events.push({ - annotation: annotation, - time: e.clock * 1000, - title: obs[e.objectid].description, - text: e.eventid, - }); + return self.performZabbixAPIRequest('event.get', params) + .then(function (result) { + var events = []; + _.each(result, function(e) { + events.push({ + annotation: annotation, + time: e.clock * 1000, + title: obs[e.objectid].description, + text: e.eventid, + }); + }); + return events; }); - return events; - }); - } else { - return []; - } + } else { + return []; + } }); }; return ZabbixAPIDatasource; }); }); + + +/** + * Convert multiple mettrics to array + * "{metric1,metcic2,...,metricN}" --> [metric1, metcic2,..., metricN] + * + * @param {string} metrics "{metric1,metcic2,...,metricN}" + * @return {Array} [metric1, metcic2,..., metricN] + */ +function splitMetrics(metrics) { + var remove_brackets_pattern = /^{|}$/g; + var metric_split_pattern = /,(?!\s)/g; + return metrics.replace(remove_brackets_pattern, '').split(metric_split_pattern) +} + + +/** + * 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; +}; \ No newline at end of file diff --git a/zabbix/partials/query.editor.html b/zabbix/partials/query.editor.html index a480c0a..ce67aaf 100644 --- a/zabbix/partials/query.editor.html +++ b/zabbix/partials/query.editor.html @@ -46,7 +46,7 @@
  • - - + ng-options="host.visible_name ? host.visible_name : host.name for host in metric.hostList track by host.name" > +
  • - - + bs-tooltip="target.item.name.length > 25 ? target.item.name : ''" + ng-options="item.name for item in metric.itemList track by item.name" > + CPU system time (avg1) * - * @param item: zabbix api item object - * @return: expanded item name (string) + * @param {Object} item Zabbix item object + * @return {string} expanded item name */ 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(','); + if (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]); - }; + // replace item parameters + for (var i = key_params.length; i >= 1; i--) { + name = name.replace('$' + i, key_params[i - 1]); + }; + } return name; }; @@ -253,3 +245,17 @@ function (angular, _) { }); }); + + +/** + * Convert multiple mettrics to array + * "{metric1,metcic2,...,metricN}" --> [metric1, metcic2,..., metricN] + * + * @param {string} metrics "{metric1,metcic2,...,metricN}" + * @return {Array} [metric1, metcic2,..., metricN] + */ +function splitMetrics(metrics) { + var remove_brackets_pattern = /^{|}$/g; + var metric_split_pattern = /,(?!\s)/g; + return metrics.replace(remove_brackets_pattern, '').split(metric_split_pattern) +} \ No newline at end of file