diff --git a/plugins/datasource-zabbix/addMetricFunction.js b/plugins/datasource-zabbix/addMetricFunction.js new file mode 100644 index 0000000..4bc4014 --- /dev/null +++ b/plugins/datasource-zabbix/addMetricFunction.js @@ -0,0 +1,107 @@ +define([ + 'angular', + 'lodash', + 'jquery', + './metricFunctions' +], +function (angular, _, $, metricFunctions) { + 'use strict'; + + angular + .module('grafana.directives') + .directive('addMetricFunction', function($compile) { + var inputTemplate = ''; + + var buttonTemplate = '' + + ''; + + return { + link: function($scope, elem) { + var categories = metricFunctions.getCategories(); + var allFunctions = getAllFunctionNames(categories); + + $scope.functionMenu = createFunctionDropDownMenu(categories); + + var $input = $(inputTemplate); + var $button = $(buttonTemplate); + $input.appendTo(elem); + $button.appendTo(elem); + + $input.attr('data-provide', 'typeahead'); + $input.typeahead({ + source: allFunctions, + minLength: 1, + items: 10, + updater: function (value) { + var funcDef = metricFunctions.getFuncDef(value); + if (!funcDef) { + // try find close match + value = value.toLowerCase(); + funcDef = _.find(allFunctions, function(funcName) { + return funcName.toLowerCase().indexOf(value) === 0; + }); + + if (!funcDef) { return; } + } + + $scope.$apply(function() { + $scope.addFunction(funcDef); + }); + + $input.trigger('blur'); + return ''; + } + }); + + $button.click(function() { + $button.hide(); + $input.show(); + $input.focus(); + }); + + $input.keyup(function() { + elem.toggleClass('open', $input.val() === ''); + }); + + $input.blur(function() { + // clicking the function dropdown menu wont + // work if you remove class at once + setTimeout(function() { + $input.val(''); + $input.hide(); + $button.show(); + elem.removeClass('open'); + }, 200); + }); + + $compile(elem.contents())($scope); + } + }; + }); + + function getAllFunctionNames(categories) { + return _.reduce(categories, function(list, category) { + _.each(category, function(func) { + list.push(func.name); + }); + return list; + }, []); + } + + function createFunctionDropDownMenu(categories) { + return _.map(categories, function(list, category) { + return { + text: category, + submenu: _.map(list, function(value) { + return { + text: value.name, + click: "addFunction('" + value.name + "')", + }; + }) + }; + }); + } +}); diff --git a/plugins/datasource-zabbix/dataProcessingService.js b/plugins/datasource-zabbix/dataProcessingService.js new file mode 100644 index 0000000..b976b4a --- /dev/null +++ b/plugins/datasource-zabbix/dataProcessingService.js @@ -0,0 +1,247 @@ +define([ + 'angular', + 'lodash', + 'moment', + './utils' +], +function (angular, _, moment, utils) { + 'use strict'; + + var module = angular.module('grafana.services'); + + module.service('DataProcessingService', function() { + var self = this; + + /** + * Downsample datapoints series + */ + this.downsampleSeries = function(datapoints, time_to, ms_interval, func) { + var downsampledSeries = []; + var timeWindow = { + from: time_to * 1000 - ms_interval, + to: time_to * 1000 + }; + + var points_sum = 0; + var points_num = 0; + var value_avg = 0; + var frame = []; + + for (var i = datapoints.length - 1; i >= 0; i -= 1) { + if (timeWindow.from < datapoints[i][1] && datapoints[i][1] <= timeWindow.to) { + points_sum += datapoints[i][0]; + points_num++; + frame.push(datapoints[i][0]); + } + else { + value_avg = points_num ? points_sum / points_num : 0; + + if (func === "max") { + downsampledSeries.push([_.max(frame), timeWindow.to]); + } + else if (func === "min") { + downsampledSeries.push([_.min(frame), timeWindow.to]); + } + + // avg by default + else { + downsampledSeries.push([value_avg, timeWindow.to]); + } + + // Shift time window + timeWindow.to = timeWindow.from; + timeWindow.from -= ms_interval; + + points_sum = 0; + points_num = 0; + frame = []; + + // Process point again + i++; + } + } + return downsampledSeries.reverse(); + }; + + /** + * Group points by given time interval + * datapoints: [[, ], ...] + */ + this.groupBy = function(interval, groupByCallback, datapoints) { + var ms_interval = utils.parseInterval(interval); + + // Calculate frame timestamps + var min_timestamp = datapoints[0][1]; + var frames = _.groupBy(datapoints, function(point) { + var group_time = Math.floor(point[1] / ms_interval) * ms_interval; + + // Prevent points outside of time range + if (group_time < min_timestamp) { + group_time = min_timestamp; + } + return group_time; + }); + + // frame: { '': [[, ], ...] } + // return [{ '': }, { '': }, ...] + var grouped = _.mapValues(frames, function(frame) { + var points = _.map(frame, function(point) { + return point[0]; + }); + return groupByCallback(points); + }); + + // Convert points to Grafana format + return sortByTime(_.map(grouped, function(value, timestamp) { + return [Number(value), Number(timestamp)]; + })); + }; + + this.sumSeries = function(timeseries) { + + // Calculate new points for interpolation + var new_timestamps = _.uniq(_.map(_.flatten(timeseries, true), function(point) { + return point[1]; + })); + new_timestamps = _.sortBy(new_timestamps); + + var interpolated_timeseries = _.map(timeseries, function(series) { + var timestamps = _.map(series, function(point) { + return point[1]; + }); + var new_points = _.map(_.difference(new_timestamps, timestamps), function(timestamp) { + return [null, timestamp]; + }); + var new_series = series.concat(new_points); + return sortByTime(new_series); + }); + + _.each(interpolated_timeseries, interpolateSeries); + + var new_timeseries = []; + var sum; + for (var i = new_timestamps.length - 1; i >= 0; i--) { + sum = 0; + for (var j = interpolated_timeseries.length - 1; j >= 0; j--) { + sum += interpolated_timeseries[j][i][0]; + } + new_timeseries.push([sum, new_timestamps[i]]); + } + + return sortByTime(new_timeseries); + }; + + function sortByTime(series) { + return _.sortBy(series, function(point) { + return point[1]; + }); + } + + /** + * Interpolate series with gaps + */ + function interpolateSeries(series) { + var left, right; + + // Interpolate series + for (var i = series.length - 1; i >= 0; i--) { + if (!series[i][0]) { + left = findNearestLeft(series, series[i]); + right = findNearestRight(series, series[i]); + if (!left) { + left = right; + } + if (!right) { + right = left; + } + series[i][0] = linearInterpolation(series[i][1], left, right); + } + } + return series; + } + + function linearInterpolation(timestamp, left, right) { + if (left[1] === right[1]) { + return (left[0] + right[0]) / 2; + } else { + return (left[0] + (right[0] - left[0]) / (right[1] - left[1]) * (timestamp - left[1])); + } + } + + function findNearestRight(series, point) { + var point_index = _.indexOf(series, point); + var nearestRight; + for (var i = point_index; i < series.length; i++) { + if (series[i][0]) { + return series[i]; + } + } + return nearestRight; + } + + function findNearestLeft(series, point) { + var point_index = _.indexOf(series, point); + var nearestLeft; + for (var i = point_index; i > 0; i--) { + if (series[i][0]) { + return series[i]; + } + } + return nearestLeft; + } + + this.AVERAGE = function(values) { + var sum = 0; + _.each(values, function(value) { + sum += value; + }); + return sum / values.length; + }; + + this.MIN = function(values) { + return _.min(values); + }; + + this.MAX = function(values) { + return _.max(values); + }; + + this.MEDIAN = function(values) { + var sorted = _.sortBy(values); + return sorted[Math.floor(sorted.length / 2)]; + }; + + this.setAlias = function(alias, timeseries) { + timeseries.target = alias; + return timeseries; + }; + + this.aggregationFunctions = { + avg: this.AVERAGE, + min: this.MIN, + max: this.MAX, + median: this.MEDIAN + }; + + this.groupByWrapper = function(interval, groupFunc, datapoints) { + var groupByCallback = self.aggregationFunctions[groupFunc]; + return self.groupBy(interval, groupByCallback, datapoints); + }; + + this.aggregateWrapper = function(groupByCallback, interval, datapoints) { + var flattenedPoints = _.flatten(datapoints, true); + return self.groupBy(interval, groupByCallback, flattenedPoints); + }; + + this.metricFunctions = { + groupBy: this.groupByWrapper, + average: _.partial(this.aggregateWrapper, this.AVERAGE), + min: _.partial(this.aggregateWrapper, this.MIN), + max: _.partial(this.aggregateWrapper, this.MAX), + median: _.partial(this.aggregateWrapper, this.MEDIAN), + sumSeries: this.sumSeries, + setAlias: this.setAlias, + }; + + }); +}); \ No newline at end of file diff --git a/plugins/datasource-zabbix/datasource.js b/plugins/datasource-zabbix/datasource.js index 07355e0..0c6172f 100644 --- a/plugins/datasource-zabbix/datasource.js +++ b/plugins/datasource-zabbix/datasource.js @@ -2,18 +2,24 @@ define([ 'angular', 'lodash', 'app/core/utils/datemath', + './utils', + './metricFunctions', + './queryProcessor', './directives', - './zabbixAPIWrapper', + './zabbixAPI', './helperFunctions', - './zabbixCacheSrv', - './queryCtrl' + './dataProcessingService', + './zabbixCache', + './queryCtrl', + './addMetricFunction', + './metricFunctionEditor' ], -function (angular, _, dateMath) { +function (angular, _, dateMath, utils, metricFunctions) { 'use strict'; /** @ngInject */ - function ZabbixAPIDatasource(instanceSettings, $q, backendSrv, templateSrv, alertSrv, - ZabbixAPI, zabbixHelperSrv, ZabbixCache) { + function ZabbixAPIDatasource(instanceSettings, $q, templateSrv, alertSrv, zabbixHelperSrv, + ZabbixAPI, ZabbixCache, QueryProcessor, DataProcessingService) { // General data source settings this.name = instanceSettings.name; @@ -35,48 +41,62 @@ function (angular, _, dateMath) { // Initialize cache service this.zabbixCache = new ZabbixCache(this.zabbixAPI); + // Initialize query builder + this.queryProcessor = new QueryProcessor(this.zabbixCache); + + console.log(this.zabbixCache); + + //////////////////////// + // Datasource methods // + //////////////////////// + /** * Test connection to Zabbix API - * * @return {object} Connection status and Zabbix API version */ this.testDatasource = function() { var self = this; - return this.zabbixAPI.getZabbixAPIVersion().then(function (apiVersion) { - return self.zabbixAPI.performZabbixAPILogin().then(function (auth) { + return this.zabbixAPI.getVersion().then(function (version) { + return self.zabbixAPI.login().then(function (auth) { if (auth) { return { status: "success", title: "Success", - message: "Zabbix API version: " + apiVersion + message: "Zabbix API version: " + version }; } else { return { status: "error", title: "Invalid user name or password", - message: "Zabbix API version: " + apiVersion + message: "Zabbix API version: " + version }; } + }, function(error) { + console.log(error); + return { + status: "error", + title: "Connection failed", + message: error + }; }); - }, function(error) { + }, + function(error) { + console.log(error); return { status: "error", title: "Connection failed", - message: "Could not connect to " + error.config.url + message: "Could not connect to given url" }; }); }; /** - * 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. + * Query panel data. Calls for each panel in dashboard. + * @param {Object} options Contains time range, targets and other info. + * @return {Object} Grafana metrics object with timeseries data for each target. */ this.query = function(options) { + var self = this; // get from & to in seconds var from = Math.ceil(dateMath.parse(options.range.from) / 1000); @@ -88,102 +108,83 @@ function (angular, _, dateMath) { if (target.mode !== 1) { - // Don't show undefined and hidden targets - if (target.hide || !target.group || !target.host || - !target.application || !target.item) { + // Don't request undefined and hidden targets + if (target.hide || !target.group || + !target.host || !target.item) { return []; } // Replace templated variables - var groupname = templateSrv.replace(target.group.name, options.scopedVars); - var hostname = templateSrv.replace(target.host.name, options.scopedVars); - var appname = templateSrv.replace(target.application.name, options.scopedVars); - var itemname = templateSrv.replace(target.item.name, options.scopedVars); - - // Extract zabbix groups, hosts and apps from string: - // "{host1,host2,...,hostN}" --> [host1, host2, ..., hostN] - var groups = zabbixHelperSrv.splitMetrics(groupname); - var hosts = zabbixHelperSrv.splitMetrics(hostname); - var apps = zabbixHelperSrv.splitMetrics(appname); - - // Remove hostnames from item names and then - // extract item names - // "hostname: itemname" --> "itemname" - var delete_hostname_pattern = /(?:\[[\w\.]+]:\s)/g; - var itemnames = zabbixHelperSrv.splitMetrics(itemname.replace(delete_hostname_pattern, '')); - - var self = this; + var groupFilter = templateSrv.replace(target.group.filter, options.scopedVars); + var hostFilter = templateSrv.replace(target.host.filter, options.scopedVars); + var appFilter = templateSrv.replace(target.application.filter, options.scopedVars); + var itemFilter = templateSrv.replace(target.item.filter, options.scopedVars); // Query numeric data - if (!target.mode) { + if (!target.mode || target.mode === 0) { - // Find items by item names and perform queries - return this.zabbixAPI.itemFindQuery(groups, hosts, apps) - .then(function (items) { + // Build query in asynchronous manner + return self.queryProcessor.build(groupFilter, hostFilter, appFilter, itemFilter) + .then(function(items) { + // Add hostname for items from multiple hosts + var addHostName = target.host.isRegex; - // Filter hosts by regex - if (target.host.visible_name === 'All') { - if (target.hostFilter && _.every(items, _.identity.hosts)) { + var getHistory; + if ((from < useTrendsFrom) && self.trends) { - // Use templated variables in filter - var host_pattern = new RegExp(templateSrv.replace(target.hostFilter, options.scopedVars)); - items = _.filter(items, function (item) { - return _.some(item.hosts, function (host) { - return host_pattern.test(host.name); - }); - }); - } - } - - if (itemnames[0] === 'All') { - - // Filter items by regex - if (target.itemFilter) { - - // Use templated variables in filter - var item_pattern = new RegExp(templateSrv.replace(target.itemFilter, options.scopedVars)); - return _.filter(items, function (item) { - return item_pattern.test(zabbixHelperSrv.expandItemName(item)); - }); - } else { - return items; - } + // Use trends + var valueType = target.downsampleFunction ? target.downsampleFunction.value : "avg"; + getHistory = self.zabbixAPI.getTrends(items, from, to).then(function(history) { + return self.queryProcessor.handleTrends(history, addHostName, valueType); + }); } else { - // Filtering items - return _.filter(items, function (item) { - return _.contains(itemnames, zabbixHelperSrv.expandItemName(item)); + // Use history + getHistory = self.zabbixAPI.getHistory(items, from, to).then(function(history) { + return self.queryProcessor.handleHistory(history, addHostName); }); } - }).then(function (items) { - items = _.flatten(items); - // Use alias only for single metric, otherwise use item names - var alias = target.item.name === 'All' || itemnames.length > 1 ? - undefined : templateSrv.replace(target.alias, options.scopedVars); + return getHistory.then(function (timeseries_data) { + timeseries_data = _.map(timeseries_data, function (timeseries) { - var history; - if ((from < useTrendsFrom) && self.trends) { - var points = target.downsampleFunction ? target.downsampleFunction.value : "avg"; - history = self.zabbixAPI.getTrends(items, from, to) - .then(_.bind(zabbixHelperSrv.handleTrendResponse, zabbixHelperSrv, items, alias, target.scale, points)); - } else { - history = self.zabbixAPI.getHistory(items, from, to) - .then(_.bind(zabbixHelperSrv.handleHistoryResponse, zabbixHelperSrv, items, alias, target.scale)); - } + // Filter only transform functions + var transformFunctions = bindFunctionDefs(target.functions, 'Transform'); - return history.then(function (timeseries) { - var timeseries_data = _.flatten(timeseries); - return _.map(timeseries_data, function (timeseries) { - - // Series downsampling - if (timeseries.datapoints.length > options.maxDataPoints) { - var ms_interval = Math.floor((to - from) / options.maxDataPoints) * 1000; - var downsampleFunc = target.downsampleFunction ? target.downsampleFunction.value : "avg"; - timeseries.datapoints = zabbixHelperSrv.downsampleSeries(timeseries.datapoints, to, ms_interval, downsampleFunc); + // Metric data processing + var dp = timeseries.datapoints; + for (var i = 0; i < transformFunctions.length; i++) { + dp = transformFunctions[i](dp); } + timeseries.datapoints = dp; + return timeseries; }); + + // Aggregations + var aggregationFunctions = bindFunctionDefs(target.functions, 'Aggregate'); + var dp = _.map(timeseries_data, 'datapoints'); + if (aggregationFunctions.length) { + for (var i = 0; i < aggregationFunctions.length; i++) { + dp = aggregationFunctions[i](dp); + } + var lastAgg = _.findLast(target.functions, function(func) { + return _.contains( + _.map(metricFunctions.getCategories()['Aggregate'], 'name'), func.def.name); + }); + timeseries_data = [{ + target: lastAgg.text, + datapoints: dp + }]; + } + + // Apply alias functions + var aliasFunctions = bindFunctionDefs(target.functions, 'Alias'); + for (var j = 0; j < aliasFunctions.length; j++) { + _.each(timeseries_data, aliasFunctions[j]); + } + + return timeseries_data; }); }); } @@ -236,12 +237,34 @@ function (angular, _, dateMath) { } }, this); - return $q.all(_.flatten(promises)).then(function (results) { - var timeseries_data = _.flatten(results); - return { data: timeseries_data }; - }); + // Data for panel (all targets) + return $q.all(_.flatten(promises)) + .then(_.flatten) + .then(function (timeseries_data) { + + // Series downsampling + var data = _.map(timeseries_data, function(timeseries) { + var DPS = DataProcessingService; + if (timeseries.datapoints.length > options.maxDataPoints) { + timeseries.datapoints = DPS.groupBy(options.interval, DPS.AVERAGE, timeseries.datapoints); + } + return timeseries; + }); + return { data: data }; + }); }; + function bindFunctionDefs(functionDefs, category) { + var aggregationFunctions = _.map(metricFunctions.getCategories()[category], 'name'); + var aggFuncDefs = _.filter(functionDefs, function(func) { + return _.contains(aggregationFunctions, func.def.name); + }); + return _.map(aggFuncDefs, function(func) { + var funcInstance = metricFunctions.createFuncInstance(func.def, func.params); + return funcInstance.bindFunction(DataProcessingService.metricFunctions); + }); + } + //////////////// // Templating // //////////////// @@ -254,6 +277,8 @@ function (angular, _, dateMath) { * of metrics in "{metric1,metcic2,...,metricN}" format. */ this.metricFindQuery = function (query) { + var metrics; + // Split query. Query structure: // group.host.app.item var parts = []; @@ -271,49 +296,22 @@ function (angular, _, dateMath) { // Get items if (parts.length === 4) { - return this.zabbixAPI.itemFindQuery(template.group, template.host, template.app) - .then(function (result) { - return _.map(result, function (item) { - var itemname = zabbixHelperSrv.expandItemName(item); - return { - text: itemname, - expandable: false - }; - }); - }); + var items = this.queryProcessor.filterItems(template.host, template.app, true); + metrics = _.map(items, formatMetric); } // Get applications else if (parts.length === 3) { - return this.zabbixAPI.appFindQuery(template.host, template.group).then(function (result) { - return _.map(result, function (app) { - return { - text: app.name, - expandable: false - }; - }); - }); + var apps = this.queryProcessor.filterApplications(template.host); + metrics = _.map(apps, formatMetric); } // Get hosts else if (parts.length === 2) { - return this.zabbixAPI.hostFindQuery(template.group).then(function (result) { - return _.map(result, function (host) { - return { - text: host.name, - expandable: false - }; - }); - }); + var hosts = this.queryProcessor.filterHosts(template.group); + metrics = _.map(hosts, formatMetric); } // Get groups else if (parts.length === 1) { - return this.zabbixAPI.getGroupByName(template.group).then(function (result) { - return _.map(result, function (hostgroup) { - return { - text: hostgroup.name, - expandable: false - }; - }); - }); + metrics = _.map(this.zabbixCache.getGroups(template.group), formatMetric); } // Return empty object for invalid request else { @@ -321,8 +319,17 @@ function (angular, _, dateMath) { d.resolve([]); return d.promise; } + + return $q.when(metrics); }; + function formatMetric(metricObj) { + return { + text: metricObj.name, + expandable: false + }; + } + ///////////////// // Annotations // ///////////////// diff --git a/plugins/datasource-zabbix/helperFunctions.js b/plugins/datasource-zabbix/helperFunctions.js index f1b84fd..d844d92 100644 --- a/plugins/datasource-zabbix/helperFunctions.js +++ b/plugins/datasource-zabbix/helperFunctions.js @@ -47,7 +47,7 @@ function (angular, _) { return $q.when(_.map(grouped_history, function (history, itemid) { var item = indexed_items[itemid]; return { - target: (item.hosts ? item.hosts[0].name+': ' : '') + target: (item.host ? item.host + ': ' : '') + (alias ? alias : self.expandItemName(item)), datapoints: _.map(history, function (p) { diff --git a/plugins/datasource-zabbix/metricFunctionEditor.js b/plugins/datasource-zabbix/metricFunctionEditor.js new file mode 100644 index 0000000..2b14d69 --- /dev/null +++ b/plugins/datasource-zabbix/metricFunctionEditor.js @@ -0,0 +1,244 @@ +define([ + 'angular', + 'lodash', + 'jquery', +], +function (angular, _, $) { + 'use strict'; + + angular + .module('grafana.directives') + .directive('metricFunctionEditor', function($compile, templateSrv) { + + var funcSpanTemplate = '{{func.def.name}}('; + var paramTemplate = ''; + + var funcControlsTemplate = + '
' + + '' + + '' + + '' + + '' + + '
'; + + return { + restrict: 'A', + link: function postLink($scope, elem) { + var $funcLink = $(funcSpanTemplate); + var $funcControls = $(funcControlsTemplate); + var func = $scope.func; + var funcDef = func.def; + var scheduledRelink = false; + var paramCountAtLink = 0; + + function clickFuncParam(paramIndex) { + /*jshint validthis:true */ + + var $link = $(this); + var $input = $link.next(); + + $input.val(func.params[paramIndex]); + $input.css('width', ($link.width() + 16) + 'px'); + + $link.hide(); + $input.show(); + $input.focus(); + $input.select(); + + var typeahead = $input.data('typeahead'); + if (typeahead) { + $input.val(''); + typeahead.lookup(); + } + } + + function scheduledRelinkIfNeeded() { + if (paramCountAtLink === func.params.length) { + return; + } + + if (!scheduledRelink) { + scheduledRelink = true; + setTimeout(function() { + relink(); + scheduledRelink = false; + }, 200); + } + } + + function inputBlur(paramIndex) { + /*jshint validthis:true */ + var $input = $(this); + var $link = $input.prev(); + var newValue = $input.val(); + + if (newValue !== '' || func.def.params[paramIndex].optional) { + $link.html(templateSrv.highlightVariablesAsHtml(newValue)); + + func.updateParam($input.val(), paramIndex); + scheduledRelinkIfNeeded(); + + $scope.$apply($scope.targetChanged); + } + + $input.hide(); + $link.show(); + } + + function inputKeyPress(paramIndex, e) { + /*jshint validthis:true */ + if(e.which === 13) { + inputBlur.call(this, paramIndex); + } + } + + function inputKeyDown() { + /*jshint validthis:true */ + this.style.width = (3 + this.value.length) * 8 + 'px'; + } + + function addTypeahead($input, paramIndex) { + $input.attr('data-provide', 'typeahead'); + + var options = funcDef.params[paramIndex].options; + if (funcDef.params[paramIndex].type === 'int') { + options = _.map(options, function(val) { return val.toString(); }); + } + + $input.typeahead({ + source: options, + minLength: 0, + items: 20, + updater: function (value) { + setTimeout(function() { + inputBlur.call($input[0], paramIndex); + }, 0); + return value; + } + }); + + var typeahead = $input.data('typeahead'); + typeahead.lookup = function () { + this.query = this.$element.val() || ''; + return this.process(this.source); + }; + } + + function toggleFuncControls() { + var targetDiv = elem.closest('.tight-form'); + + if (elem.hasClass('show-function-controls')) { + elem.removeClass('show-function-controls'); + targetDiv.removeClass('has-open-function'); + $funcControls.hide(); + return; + } + + elem.addClass('show-function-controls'); + targetDiv.addClass('has-open-function'); + + $funcControls.show(); + } + + function addElementsAndCompile() { + $funcControls.appendTo(elem); + $funcLink.appendTo(elem); + + _.each(funcDef.params, function(param, index) { + if (param.optional && func.params.length <= index) { + return; + } + + if (index > 0) { + $(', ').appendTo(elem); + } + + var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]); + var $paramLink = $('' + paramValue + ''); + var $input = $(paramTemplate); + + paramCountAtLink++; + + $paramLink.appendTo(elem); + $input.appendTo(elem); + + $input.blur(_.partial(inputBlur, index)); + $input.keyup(inputKeyDown); + $input.keypress(_.partial(inputKeyPress, index)); + $paramLink.click(_.partial(clickFuncParam, index)); + + if (funcDef.params[index].options) { + addTypeahead($input, index); + } + + }); + + $(')').appendTo(elem); + + $compile(elem.contents())($scope); + } + + function ifJustAddedFocusFistParam() { + if ($scope.func.added) { + $scope.func.added = false; + setTimeout(function() { + elem.find('.graphite-func-param-link').first().click(); + }, 10); + } + } + + function registerFuncControlsToggle() { + $funcLink.click(toggleFuncControls); + } + + function registerFuncControlsActions() { + $funcControls.click(function(e) { + var $target = $(e.target); + if ($target.hasClass('fa-remove')) { + toggleFuncControls(); + $scope.$apply(function() { + $scope.removeFunction($scope.func); + }); + return; + } + + if ($target.hasClass('fa-arrow-left')) { + $scope.$apply(function() { + _.move($scope.target.functions, $scope.$index, $scope.$index - 1); + $scope.targetChanged(); + }); + return; + } + + if ($target.hasClass('fa-arrow-right')) { + $scope.$apply(function() { + _.move($scope.target.functions, $scope.$index, $scope.$index + 1); + $scope.targetChanged(); + }); + return; + } + + if ($target.hasClass('fa-question-circle')) { + window.open("http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions." + funcDef.name,'_blank'); + return; + } + }); + } + + function relink() { + elem.children().remove(); + + addElementsAndCompile(); + ifJustAddedFocusFistParam(); + registerFuncControlsToggle(); + registerFuncControlsActions(); + } + + relink(); + } + }; + + }); + +}); diff --git a/plugins/datasource-zabbix/metricFunctions.js b/plugins/datasource-zabbix/metricFunctions.js new file mode 100644 index 0000000..cf55092 --- /dev/null +++ b/plugins/datasource-zabbix/metricFunctions.js @@ -0,0 +1,204 @@ +define([ + 'lodash', + 'jquery' +], +function (_, $) { + 'use strict'; + + var index = []; + var categories = { + Transform: [], + Aggregate: [], + Alias: [] + }; + + function addFuncDef(funcDef) { + funcDef.params = funcDef.params || []; + funcDef.defaultParams = funcDef.defaultParams || []; + + if (funcDef.category) { + categories[funcDef.category].push(funcDef); + } + index[funcDef.name] = funcDef; + index[funcDef.shortName || funcDef.name] = funcDef; + } + + addFuncDef({ + name: 'groupBy', + category: 'Transform', + params: [ + { name: 'interval', type: 'string'}, + { name: 'function', type: 'string', options: ['avg', 'min', 'max', 'median'] } + ], + defaultParams: ['1m', 'avg'], + }); + + addFuncDef({ + name: 'sumSeries', + category: 'Aggregate', + params: [], + defaultParams: [], + }); + + addFuncDef({ + name: 'median', + category: 'Aggregate', + params: [ + { name: 'interval', type: 'string'} + ], + defaultParams: ['1m'], + }); + + addFuncDef({ + name: 'average', + category: 'Aggregate', + params: [ + { name: 'interval', type: 'string' } + ], + defaultParams: ['1m'], + }); + + addFuncDef({ + name: 'min', + category: 'Aggregate', + params: [ + { name: 'interval', type: 'string' } + ], + defaultParams: ['1m'], + }); + + addFuncDef({ + name: 'max', + category: 'Aggregate', + params: [ + { name: 'interval', type: 'string' } + ], + defaultParams: ['1m'], + }); + + addFuncDef({ + name: 'setAlias', + category: 'Alias', + params: [ + { name: 'alias', type: 'string'} + ], + defaultParams: [], + }); + + _.each(categories, function(funcList, catName) { + categories[catName] = _.sortBy(funcList, 'name'); + }); + + function FuncInstance(funcDef, params) { + this.def = funcDef; + + if (params) { + this.params = params; + } else { + // Create with default params + this.params = []; + this.params = funcDef.defaultParams.slice(0); + } + + this.updateText(); + } + + FuncInstance.prototype.bindFunction = function(metricFunctions) { + var func = metricFunctions[this.def.name]; + if (func) { + + // Bind function arguments + var bindedFunc = func; + for (var i = 0; i < this.params.length; i++) { + bindedFunc = _.partial(bindedFunc, this.params[i]); + } + return bindedFunc; + } else { + throw { message: 'Method not found ' + this.def.name }; + } + }; + + FuncInstance.prototype.render = function(metricExp) { + var str = this.def.name + '('; + var parameters = _.map(this.params, function(value, index) { + + var paramType = this.def.params[index].type; + if (paramType === 'int' || paramType === 'value_or_series' || paramType === 'boolean') { + return value; + } + else if (paramType === 'int_or_interval' && $.isNumeric(value)) { + return value; + } + + return "'" + value + "'"; + + }, this); + + if (metricExp) { + parameters.unshift(metricExp); + } + + return str + parameters.join(', ') + ')'; + }; + + FuncInstance.prototype._hasMultipleParamsInString = function(strValue, index) { + if (strValue.indexOf(',') === -1) { + return false; + } + + return this.def.params[index + 1] && this.def.params[index + 1].optional; + }; + + FuncInstance.prototype.updateParam = function(strValue, index) { + // handle optional parameters + // if string contains ',' and next param is optional, split and update both + if (this._hasMultipleParamsInString(strValue, index)) { + _.each(strValue.split(','), function(partVal, idx) { + this.updateParam(partVal.trim(), idx); + }, this); + return; + } + + if (strValue === '' && this.def.params[index].optional) { + this.params.splice(index, 1); + } + else { + this.params[index] = strValue; + } + + this.updateText(); + }; + + FuncInstance.prototype.updateText = function () { + if (this.params.length === 0) { + this.text = this.def.name + '()'; + return; + } + + var text = this.def.name + '('; + text += this.params.join(', '); + text += ')'; + this.text = text; + }; + + return { + createFuncInstance: function(funcDef, params) { + if (_.isString(funcDef)) { + if (!index[funcDef]) { + throw { message: 'Method not found ' + name }; + } + funcDef = index[funcDef]; + } + return new FuncInstance(funcDef, params); + }, + + getFuncDef: function(name) { + return index[name]; + }, + + getCategories: function() { + return categories; + } + }; + +}); diff --git a/plugins/datasource-zabbix/partials/query.editor.html b/plugins/datasource-zabbix/partials/query.editor.html index 383d965..b5ff671 100644 --- a/plugins/datasource-zabbix/partials/query.editor.html +++ b/plugins/datasource-zabbix/partials/query.editor.html @@ -76,62 +76,52 @@
@@ -159,51 +149,40 @@
  • Application
  • - - - - +
  • +
  • Item
  • - - - - -
  • - -
  • - Filter - -
  • -
  • + ng-model="target.item.filter" + bs-typeahead="getItemNames" + ng-change="onTargetPartChange(target.item)" + ng-blur="onItemBlur()" + data-min-length=0 + data-items=100 + class="input-large tight-form-input" + ng-style="target.item.style">
  • +
  • Options
  • +
  • + + +
  • + + -
  • +
    -
    +
    + +
    +
    + +