diff --git a/src/datasource-zabbix/datasource.js b/src/datasource-zabbix/datasource.js index 2f5d792..2e914a4 100644 --- a/src/datasource-zabbix/datasource.js +++ b/src/datasource-zabbix/datasource.js @@ -49,8 +49,6 @@ export class ZabbixAPIDatasource { // Use custom format for template variables this.replaceTemplateVars = _.partial(replaceTemplateVars, this.templateSrv); - - console.log(this.zabbixCache); } //////////////////////// @@ -63,17 +61,18 @@ export class ZabbixAPIDatasource { * @return {Object} Grafana metrics object with timeseries data for each target. */ query(options) { - var self = this; - - // get from & to in seconds var timeFrom = Math.ceil(dateMath.parse(options.range.from) / 1000); var timeTo = Math.ceil(dateMath.parse(options.range.to) / 1000); + var useTrendsFrom = Math.ceil(dateMath.parse('now-' + this.trendsFrom) / 1000); - var useTrends = (timeFrom < useTrendsFrom) && this.trends; + var useTrends = (timeFrom <= useTrendsFrom) && this.trends; // Create request for each target var promises = _.map(options.targets, target => { + // Prevent changes of original object + target = _.cloneDeep(target); + if (target.mode !== 1) { // Migrate old targets @@ -85,21 +84,26 @@ export class ZabbixAPIDatasource { } // Replace templated variables - var groupFilter = this.replaceTemplateVars(target.group.filter, options.scopedVars); - var hostFilter = this.replaceTemplateVars(target.host.filter, options.scopedVars); - var appFilter = this.replaceTemplateVars(target.application.filter, options.scopedVars); - var itemFilter = this.replaceTemplateVars(target.item.filter, options.scopedVars); + target.group.filter = this.replaceTemplateVars(target.group.filter, options.scopedVars); + target.host.filter = this.replaceTemplateVars(target.host.filter, options.scopedVars); + target.application.filter = this.replaceTemplateVars(target.application.filter, options.scopedVars); + target.item.filter = this.replaceTemplateVars(target.item.filter, options.scopedVars); + target.textFilter = this.replaceTemplateVars(target.textFilter, options.scopedVars); + + _.forEach(target.functions, func => { + func.params = _.map(func.params, param => { + return this.templateSrv.replace(param, options.scopedVars); + }); + }); // Query numeric data if (!target.mode || target.mode === 0) { - return self.queryNumericData(target, groupFilter, hostFilter, appFilter, itemFilter, - timeFrom, timeTo, useTrends, options, self); + return this.queryNumericData(target, timeFrom, timeTo, useTrends); } // Query text data else if (target.mode === 2) { - return self.queryTextData(target, groupFilter, hostFilter, appFilter, itemFilter, - timeFrom, timeTo, options, self); + return this.queryTextData(target, timeFrom, timeTo); } } @@ -113,11 +117,11 @@ export class ZabbixAPIDatasource { return this.zabbixAPI .getSLA(target.itservice.serviceid, timeFrom, timeTo) .then(slaObject => { - return self.queryProcessor + return this.queryProcessor .handleSLAResponse(target.itservice, target.slaProperty, slaObject); }); } - }, this); + }); // Data for panel (all targets) return this.q.all(_.flatten(promises)) @@ -136,10 +140,13 @@ export class ZabbixAPIDatasource { }); } - queryNumericData(target, groupFilter, hostFilter, appFilter, itemFilter, timeFrom, timeTo, useTrends, options, self) { + queryNumericData(target, timeFrom, timeTo, useTrends) { // Build query in asynchronous manner - return self.queryProcessor - .build(groupFilter, hostFilter, appFilter, itemFilter, 'num') + return this.queryProcessor.build(target.group.filter, + target.host.filter, + target.application.filter, + target.item.filter, + 'num') .then(items => { // Add hostname for items from multiple hosts var addHostName = utils.isRegex(target.host.filter); @@ -151,55 +158,47 @@ export class ZabbixAPIDatasource { // Find trendValue() function and get specified trend value var trendFunctions = _.map(metricFunctions.getCategories()['Trends'], 'name'); var trendValueFunc = _.find(target.functions, func => { - return _.contains(trendFunctions, func.def.name); + return _.includes(trendFunctions, func.def.name); }); var valueType = trendValueFunc ? trendValueFunc.params[0] : "avg"; - getHistory = self.zabbixAPI + getHistory = this.zabbixAPI .getTrend(items, timeFrom, timeTo) .then(history => { - return self.queryProcessor.handleTrends(history, items, addHostName, valueType); + return this.queryProcessor.handleTrends(history, items, addHostName, valueType); }); } // Use history else { - getHistory = self.zabbixCache + getHistory = this.zabbixCache .getHistory(items, timeFrom, timeTo) .then(history => { - return self.queryProcessor.handleHistory(history, items, addHostName); + return this.queryProcessor.handleHistory(history, items, addHostName); }); } return getHistory.then(timeseries_data => { + let transformFunctions = bindFunctionDefs(target.functions, 'Transform'); + let aggregationFunctions = bindFunctionDefs(target.functions, 'Aggregate'); + let aliasFunctions = bindFunctionDefs(target.functions, 'Alias'); // Apply transformation functions timeseries_data = _.map(timeseries_data, timeseries => { - - // Filter only transformation functions - var transformFunctions = bindFunctionDefs(target.functions, 'Transform', DataProcessor); - - // Timeseries processing - var dp = timeseries.datapoints; - for (var i = 0; i < transformFunctions.length; i++) { - dp = transformFunctions[i](dp); - } - timeseries.datapoints = dp; - + timeseries.datapoints = sequence(transformFunctions)(timeseries.datapoints); return timeseries; }); // Apply aggregations - var aggregationFunctions = bindFunctionDefs(target.functions, 'Aggregate', DataProcessor); - 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, func => { - return _.contains( - _.map(metricFunctions.getCategories()['Aggregate'], 'name'), func.def.name); + let dp = _.map(timeseries_data, 'datapoints'); + dp = sequence(aggregationFunctions)(dp); + + let aggFuncNames = _.map(metricFunctions.getCategories()['Aggregate'], 'name'); + let lastAgg = _.findLast(target.functions, func => { + return _.includes(aggFuncNames, func.def.name); }); + timeseries_data = [ { target: lastAgg.text, @@ -209,49 +208,36 @@ export class ZabbixAPIDatasource { } // Apply alias functions - var aliasFunctions = bindFunctionDefs(target.functions, 'Alias', DataProcessor); - for (var j = 0; j < aliasFunctions.length; j++) { - _.each(timeseries_data, aliasFunctions[j]); - } + _.each(timeseries_data, sequence(aliasFunctions)); return timeseries_data; }); }); } - queryTextData(target, groupFilter, hostFilter, appFilter, itemFilter, timeFrom, timeTo, options, self) { - return self.queryProcessor - .build(groupFilter, hostFilter, appFilter, itemFilter, 'text') + queryTextData(target, timeFrom, timeTo) { + return this.queryProcessor.build(target.group.filter, + target.host.filter, + target.application.filter, + target.item.filter, + 'text') .then(items => { if (items.length) { - var textItemsPromises = _.map(items, item => { - return self.zabbixAPI.getLastValue(item.itemid); - }); - return self.q.all(textItemsPromises) - .then(result => { - return _.map(result, (lastvalue, index) => { - var extractedValue; + return this.zabbixAPI.getHistory(items, timeFrom, timeTo) + .then(history => { + return this.queryProcessor.convertHistory(history, items, false, (point) => { + let value = point.value; + + // Regex-based extractor if (target.textFilter) { - var text_extract_pattern = new RegExp(self.replaceTemplateVars(target.textFilter, options.scopedVars)); - extractedValue = text_extract_pattern.exec(lastvalue); - if (extractedValue) { - if (target.useCaptureGroups) { - extractedValue = extractedValue[1]; - } else { - extractedValue = extractedValue[0]; - } - } - } else { - extractedValue = lastvalue; + value = extractText(point.value, target.textFilter, target.useCaptureGroups); } - return { - target: items[index].name, - datapoints: [[extractedValue, timeTo * 1000]] - }; + + return [value, point.clock * 1000]; }); }); } else { - return self.q.when([]); + return this.q.when([]); } }); } @@ -308,12 +294,12 @@ export class ZabbixAPIDatasource { * of metrics in "{metric1,metcic2,...,metricN}" format. */ metricFindQuery(query) { - // Split query. Query structure: - // group.host.app.item - var self = this; - var parts = []; - _.each(query.split('.'), function (part) { - part = self.replaceTemplateVars(part, {}); + let result; + let parts = []; + + // Split query. Query structure: group.host.app.item + _.each(query.split('.'), part => { + part = this.replaceTemplateVars(part, {}); // Replace wildcard to regex if (part === '*') { @@ -321,7 +307,7 @@ export class ZabbixAPIDatasource { } parts.push(part); }); - var template = _.object(['group', 'host', 'app', 'item'], parts); + let template = _.zipObject(['group', 'host', 'app', 'item'], parts); // Get items if (parts.length === 4) { @@ -329,40 +315,23 @@ export class ZabbixAPIDatasource { if (template.app === '/.*/') { template.app = ''; } - return this.queryProcessor - .getItems(template.group, template.host, template.app) - .then(items => { - return _.map(items, formatMetric); - }); - } - // Get applications - else if (parts.length === 3) { - return this.queryProcessor - .getApps(template.group, template.host) - .then(apps => { - return _.map(apps, formatMetric); - }); - } - // Get hosts - else if (parts.length === 2) { - return this.queryProcessor - .getHosts(template.group) - .then(hosts => { - return _.map(hosts, formatMetric); - }); - } - // Get groups - else if (parts.length === 1) { - return this.zabbixCache - .getGroups(template.group) - .then(groups => { - return _.map(groups, formatMetric); - }); - } - // Return empty object for invalid request - else { - return this.q.when([]); + result = this.queryProcessor.getItems(template.group, template.host, template.app); + } else if (parts.length === 3) { + // Get applications + result = this.queryProcessor.getApps(template.group, template.host); + } else if (parts.length === 2) { + // Get hosts + result = this.queryProcessor.getHosts(template.group); + } else if (parts.length === 1) { + // Get groups + result = this.zabbixCache.getGroups(template.group); + } else { + result = this.q.when([]); } + + return result.then(metrics => { + return _.map(metrics, formatMetric); + }); } ///////////////// @@ -409,7 +378,7 @@ export class ZabbixAPIDatasource { return self.zabbixAPI .getEvents(objectids, timeFrom, timeTo, showOkEvents) .then(events => { - var indexedTriggers = _.indexBy(triggers, 'triggerid'); + var indexedTriggers = _.groupBy(triggers, 'triggerid'); // Hide acknowledged events if option enabled if (annotation.hideAcknowledged) { @@ -442,11 +411,10 @@ export class ZabbixAPIDatasource { } -function bindFunctionDefs(functionDefs, category, DataProcessor) { - 'use strict'; +function bindFunctionDefs(functionDefs, category) { var aggregationFunctions = _.map(metricFunctions.getCategories()[category], 'name'); var aggFuncDefs = _.filter(functionDefs, function(func) { - return _.contains(aggregationFunctions, func.def.name); + return _.includes(aggregationFunctions, func.def.name); }); return _.map(aggFuncDefs, function(func) { @@ -455,8 +423,14 @@ function bindFunctionDefs(functionDefs, category, DataProcessor) { }); } +function filterFunctionDefs(funcs, category) { + let filteredFuncs = _.map(metricFunctions.getCategories()[category]); + return _.filter(funcs, func => { + return _.includes(filteredFuncs, func.def.name); + }); +} + function formatMetric(metricObj) { - 'use strict'; return { text: metricObj.name, expandable: false @@ -496,3 +470,27 @@ function replaceTemplateVars(templateSrv, target, scopedVars) { } return replacedTarget; } + +function extractText(str, pattern, useCaptureGroups) { + let extractPattern = new RegExp(pattern); + let extractedValue = extractPattern.exec(str); + if (extractedValue) { + if (useCaptureGroups) { + extractedValue = extractedValue[1]; + } else { + extractedValue = extractedValue[0]; + } + } + return extractedValue; +} + +// Apply function one by one: +// sequence([a(), b(), c()]) = c(b(a())); +function sequence(funcsArray) { + return function(result) { + for (var i = 0; i < funcsArray.length; i++) { + result = funcsArray[i].call(this, result); + } + return result; + }; +} diff --git a/src/datasource-zabbix/queryProcessor.service.js b/src/datasource-zabbix/queryProcessor.service.js index 557b2f1..6931468 100644 --- a/src/datasource-zabbix/queryProcessor.service.js +++ b/src/datasource-zabbix/queryProcessor.service.js @@ -230,7 +230,7 @@ angular.module('grafana.services').factory('QueryProcessor', function($q) { // Group history by itemid var grouped_history = _.groupBy(history, 'itemid'); - var hosts = _.indexBy(_.flatten(_.map(items, 'hosts')), 'hostid'); + var hosts = _.groupBy(_.flatten(_.map(items, 'hosts')), 'hostid'); return _.map(grouped_history, function(hist, itemid) { var item = _.find(items, {'itemid': itemid}); diff --git a/src/datasource-zabbix/utils.js b/src/datasource-zabbix/utils.js index 5925da0..1f05572 100644 --- a/src/datasource-zabbix/utils.js +++ b/src/datasource-zabbix/utils.js @@ -35,7 +35,7 @@ export function isTemplateVariable(str, templateVariables) { var variables = _.map(templateVariables, variable => { return '$' + variable.name; }); - return _.contains(variables, str); + return _.includes(variables, str); } else { return false; } diff --git a/src/datasource-zabbix/zabbixAPI.service.js b/src/datasource-zabbix/zabbixAPI.service.js index fda4a29..0d6c82f 100644 --- a/src/datasource-zabbix/zabbixAPI.service.js +++ b/src/datasource-zabbix/zabbixAPI.service.js @@ -114,6 +114,15 @@ function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) { // Zabbix API method wrappers // //////////////////////////////// + acknowledgeEvent(eventid, message) { + var params = { + eventids: eventid, + message: message + }; + + return this.request('event.acknowledge', params); + } + getGroups() { var params = { output: ['name'], diff --git a/src/datasource-zabbix/zabbixCache.service.js b/src/datasource-zabbix/zabbixCache.service.js index e4c9868..9970568 100644 --- a/src/datasource-zabbix/zabbixCache.service.js +++ b/src/datasource-zabbix/zabbixCache.service.js @@ -117,7 +117,7 @@ angular.module('grafana.services').factory('ZabbixCachingProxy', function($q, $i var deferred = this.$q.defer(); var historyStorage = this.storage.history; var full_history; - var expired = _.filter(_.indexBy(items, 'itemid'), function(item, itemid) { + var expired = _.filter(_.groupBy(items, 'itemid'), function(item, itemid) { return !historyStorage[itemid]; }); if (expired.length) { diff --git a/src/panel-triggers/ack-tooltip.directive.js b/src/panel-triggers/ack-tooltip.directive.js new file mode 100644 index 0000000..3622db4 --- /dev/null +++ b/src/panel-triggers/ack-tooltip.directive.js @@ -0,0 +1,113 @@ +import angular from 'angular'; +import $ from 'jquery'; +import Drop from 'tether-drop'; + +/** @ngInject */ +angular + .module('grafana.directives') + .directive('ackTooltip', function($sanitize, $compile) { + let buttonTemplate = ''; + + return { + scope: { + ack: "=", + trigger: "=", + onAck: "=", + context: "=" + }, + link: function(scope, element) { + let acknowledges = scope.ack; + let $button = $(buttonTemplate); + $button.appendTo(element); + + $button.click(function() { + let tooltip = '
'; + + if (acknowledges && acknowledges.length) { + tooltip += '' + + '' + + '' + + '' + + ''; + for (let ack of acknowledges) { + tooltip += '' + + '' + + ''; + } + tooltip += '
TimeUserComments
' + ack.time + '' + ack.user + '' + ack.message + '
'; + } else { + tooltip += 'Add acknowledge'; + } + + let addAckButtonTemplate = '
' + + '
'; + tooltip += addAckButtonTemplate; + tooltip += '
'; + + let drop = new Drop({ + target: element[0], + content: tooltip, + position: "bottom left", + classes: 'drop-popover ack-tooltip', + openOn: 'hover', + hoverCloseDelay: 500, + tetherOptions: { + constraints: [{to: 'window', pin: true, attachment: "both"}] + } + }); + + drop.open(); + drop.on('close', closeDrop); + + $('#add-acknowledge-btn').on('click', onAddAckButtonClick); + + function onAddAckButtonClick() { + let inputTemplate = '
' + + '' + + '' + + '
'; + + let $input = $(inputTemplate); + let $addAckButton = $('.ack-tooltip .ack-add-button'); + $addAckButton.replaceWith($input); + $('.ack-tooltip #cancel-ack-button').on('click', onAckCancelButtonClick); + $('.ack-tooltip #send-ack-button').on('click', onAckSendlButtonClick); + } + + function onAckCancelButtonClick() { + $('.ack-tooltip .ack-input-group').replaceWith(addAckButtonTemplate); + $('#add-acknowledge-btn').on('click', onAddAckButtonClick); + } + + function onAckSendlButtonClick() { + let message = $('.ack-tooltip #ack-message')[0].value; + let onAck = scope.onAck.bind(scope.context); + onAck(scope.trigger, message).then(() => { + closeDrop(); + }); + } + + function closeDrop() { + setTimeout(function() { + drop.destroy(); + }); + } + + }); + + $compile(element.contents())(scope); + } + }; + }); diff --git a/src/panel-triggers/editor.html b/src/panel-triggers/editor.html index 069da01..ea3333c 100644 --- a/src/panel-triggers/editor.html +++ b/src/panel-triggers/editor.html @@ -150,7 +150,7 @@ Show fields
  • - +
  • +
  • + + + +
  • + +
    + +
    +
    +
    -
    +
    +
    + +
    +
    diff --git a/src/panel-triggers/module.html b/src/panel-triggers/module.html index fddc004..23c3ce0 100644 --- a/src/panel-triggers/module.html +++ b/src/panel-triggers/module.html @@ -9,6 +9,11 @@ Host + +
    + Technical Name +
    +
    Status
    @@ -31,21 +36,31 @@ +
    {{trigger.host}}
    + + +
    + {{trigger.hostTechName}} +
    + +
    {{ctrl.triggerStatusMap[trigger.value]}}
    +
    {{trigger.severity}}
    +
    {{trigger.description}} @@ -68,43 +83,16 @@ {{trigger.comments}}
    - - -
    -
    - - - - - - - - - - - - - - - -
    TimeUserComments
    - {{ack.time}} - - {{ack.user}} - - {{ack.message}} -
    -
    -
    + {{trigger.lastchange}} + {{trigger.age}} + @@ -121,12 +109,12 @@ - - - + + diff --git a/src/panel-triggers/module.js b/src/panel-triggers/module.js index 7de6390..ce412f5 100644 --- a/src/panel-triggers/module.js +++ b/src/panel-triggers/module.js @@ -16,6 +16,7 @@ import moment from 'moment'; import * as utils from '../datasource-zabbix/utils'; import {MetricsPanelCtrl} from 'app/plugins/sdk'; import {triggerPanelEditor} from './editor'; +import './ack-tooltip.directive'; import './css/panel_triggers.css!'; var defaultSeverity = [ @@ -47,6 +48,7 @@ var panelDefaults = { showEvents: { text: 'Problems', value: '1' }, triggerSeverity: defaultSeverity, okEventColor: 'rgba(0, 245, 153, 0.45)', + ackEventColor: 'rgba(0, 0, 0, 0)' }; var triggerStatusMap = { @@ -59,10 +61,11 @@ var defaultTimeFormat = "DD MMM YYYY HH:mm:ss"; class TriggerPanelCtrl extends MetricsPanelCtrl { /** @ngInject */ - constructor($scope, $injector, $q, $element, datasourceSrv, templateSrv) { + constructor($scope, $injector, $q, $element, datasourceSrv, templateSrv, contextSrv) { super($scope, $injector); this.datasourceSrv = datasourceSrv; this.templateSrv = templateSrv; + this.contextSrv = contextSrv; this.triggerStatusMap = triggerStatusMap; this.defaultTimeFormat = defaultTimeFormat; @@ -122,11 +125,11 @@ class TriggerPanelCtrl extends MetricsPanelCtrl { showEvents) .then(triggers => { return _.map(triggers, trigger => { - var triggerObj = trigger; + let triggerObj = trigger; // Format last change and age trigger.lastchangeUnix = Number(trigger.lastchange); - var timestamp = moment.unix(trigger.lastchangeUnix); + let timestamp = moment.unix(trigger.lastchangeUnix); if (self.panel.customLastChangeFormat) { // User defined format triggerObj.lastchange = timestamp.format(self.panel.lastChangeFormat); @@ -138,6 +141,7 @@ class TriggerPanelCtrl extends MetricsPanelCtrl { // Set host that the trigger belongs if (trigger.hosts.length) { triggerObj.host = trigger.hosts[0].name; + triggerObj.hostTechName = trigger.hosts[0].host; } // Set color @@ -171,11 +175,20 @@ class TriggerPanelCtrl extends MetricsPanelCtrl { if (event) { trigger.acknowledges = _.map(event.acknowledges, ack => { - var time = new Date(+ack.clock * 1000); - ack.time = time.toLocaleString(); + let timestamp = moment.unix(ack.clock); + if (self.panel.customLastChangeFormat) { + ack.time = timestamp.format(self.panel.lastChangeFormat); + } else { + ack.time = timestamp.format(self.defaultTimeFormat); + } ack.user = ack.alias + ' (' + ack.name + ' ' + ack.surname + ')'; return ack; }); + + // Mark acknowledged triggers with different color + if (self.panel.markAckEvents && trigger.acknowledges.length) { + trigger.color = self.panel.ackEventColor; + } } }); @@ -209,7 +222,7 @@ class TriggerPanelCtrl extends MetricsPanelCtrl { } // Limit triggers number - self.triggerList = _.first(triggerList, self.panel.limit); + self.triggerList = triggerList.slice(0, self.panel.limit); // Notify panel that request is finished self.setTimeQueryEnd(); @@ -224,8 +237,17 @@ class TriggerPanelCtrl extends MetricsPanelCtrl { trigger.showComment = !trigger.showComment; } - switchAcknowledges(trigger) { - trigger.showAcknowledges = !trigger.showAcknowledges; + acknowledgeTrigger(trigger, message) { + let self = this; + let eventid = trigger.lastEvent.eventid; + let grafana_user = this.contextSrv.user.name; + let ack_message = grafana_user + ' (Grafana): ' + message; + return this.datasourceSrv.get(this.panel.datasource).then(datasource => { + let zabbix = datasource.zabbixAPI; + return zabbix.acknowledgeEvent(eventid, ack_message).then(() => { + self.refresh(); + }); + }); } } diff --git a/src/panel-triggers/sass/panel_triggers.scss b/src/panel-triggers/sass/panel_triggers.scss index 478f7dd..156c5dc 100644 --- a/src/panel-triggers/sass/panel_triggers.scss +++ b/src/panel-triggers/sass/panel_triggers.scss @@ -106,3 +106,37 @@ $grafanaListAccent: lighten($dark-2, 2%); height: 0px; line-height: 0px; } + +.ack-tooltip { + .drop-content { + // Rewrite tooltip width + max-width: 70rem !important; + min-width: 30rem !important; + } + + .ack-comments { + width: 60%; + } + + .ack-add-button { + padding-top: 1rem; + } + + table td, th { + padding-right: 1rem; + } + + .ack-input-group { + padding-top: 1rem; + + input { + border: 1px solid; + border-radius: 2px; + width: 50%; + } + + button { + margin-left: 1rem; + } + } +} diff --git a/src/plugin.json b/src/plugin.json index e44aaf2..9a30f99 100644 --- a/src/plugin.json +++ b/src/plugin.json @@ -31,7 +31,7 @@ {"name": "Metric Editor", "path": "img/screenshot-metric_editor.png"}, {"name": "Triggers", "path": "img/screenshot-triggers.png"} ], - "version": "3.0.0", + "version": "3.1.0-pre1", "updated": "2016-07-03" },