diff --git a/.gitignore b/.gitignore index dbb0ecd..a3019ca 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,8 @@ docs/site/ # Tests -dist/test/ -dist/test-setup/ +# dist/test/ +# dist/test-setup/ vendor/npm src/vendor/npm diff --git a/dist/test-setup/cssStub.js b/dist/test-setup/cssStub.js new file mode 100644 index 0000000..f053ebf --- /dev/null +++ b/dist/test-setup/cssStub.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/dist/test-setup/jest-setup.js b/dist/test-setup/jest-setup.js new file mode 100644 index 0000000..ff528b8 --- /dev/null +++ b/dist/test-setup/jest-setup.js @@ -0,0 +1,50 @@ +// JSHint options +/* globals global: false */ + +import {JSDOM} from 'jsdom'; + +// Mock Grafana modules that are not available outside of the core project +// Required for loading module.js +jest.mock('angular', () => { + return { + module: function() { + return { + directive: function() {}, + service: function() {}, + factory: function() {} + }; + } + }; +}, {virtual: true}); + +jest.mock('app/plugins/sdk', () => { + return { + QueryCtrl: null + }; +}, {virtual: true}); + +jest.mock('app/core/utils/datemath', () => { + const datemath = require('./modules/datemath'); + return { + parse: datemath.parse, + parseDateMath: datemath.parseDateMath, + isValid: datemath.isValid + }; +}, {virtual: true}); + +jest.mock('app/core/table_model', () => { + return {}; +}, {virtual: true}); + +jest.mock('./css/query-editor.css!', () => { + return ""; +}, {virtual: true}); + +jest.mock('jquery', () => 'module not found', {virtual: true}); + +// Required for loading angularjs +let dom = new JSDOM(''); +// Setup jsdom +global.window = dom.window; +global.document = global.window.document; +global.Node = window.Node; diff --git a/dist/test-setup/modules/datemath.js b/dist/test-setup/modules/datemath.js new file mode 100644 index 0000000..efbf0bf --- /dev/null +++ b/dist/test-setup/modules/datemath.js @@ -0,0 +1,111 @@ +import _ from 'lodash'; +import moment from 'moment'; + +var units = ['y', 'M', 'w', 'd', 'h', 'm', 's']; + +export function parse(text, roundUp) { + if (!text) { return undefined; } + if (moment.isMoment(text)) { return text; } + if (_.isDate(text)) { return moment(text); } + + var time; + var mathString = ''; + var index; + var parseString; + + if (text.substring(0, 3) === 'now') { + time = moment(); + mathString = text.substring('now'.length); + } else { + index = text.indexOf('||'); + if (index === -1) { + parseString = text; + mathString = ''; // nothing else + } else { + parseString = text.substring(0, index); + mathString = text.substring(index + 2); + } + // We're going to just require ISO8601 timestamps, k? + time = moment(parseString, moment.ISO_8601); + } + + if (!mathString.length) { + return time; + } + + return parseDateMath(mathString, time, roundUp); +} + +export function isValid(text) { + var date = parse(text); + if (!date) { + return false; + } + + if (moment.isMoment(date)) { + return date.isValid(); + } + + return false; +} + +export function parseDateMath(mathString, time, roundUp) { + var dateTime = time; + var i = 0; + var len = mathString.length; + + while (i < len) { + var c = mathString.charAt(i++); + var type; + var num; + var unit; + + if (c === '/') { + type = 0; + } else if (c === '+') { + type = 1; + } else if (c === '-') { + type = 2; + } else { + return undefined; + } + + if (isNaN(mathString.charAt(i))) { + num = 1; + } else if (mathString.length === 2) { + num = mathString.charAt(i); + } else { + var numFrom = i; + while (!isNaN(mathString.charAt(i))) { + i++; + if (i > 10) { return undefined; } + } + num = parseInt(mathString.substring(numFrom, i), 10); + } + + if (type === 0) { + // rounding is only allowed on whole, single, units (eg M or 1M, not 0.5M or 2M) + if (num !== 1) { + return undefined; + } + } + unit = mathString.charAt(i++); + + if (!_.includes(units, unit)) { + return undefined; + } else { + if (type === 0) { + if (roundUp) { + dateTime.endOf(unit); + } else { + dateTime.startOf(unit); + } + } else if (type === 1) { + dateTime.add(num, unit); + } else if (type === 2) { + dateTime.subtract(num, unit); + } + } + } + return dateTime; +} diff --git a/dist/test/components/config.js b/dist/test/components/config.js new file mode 100644 index 0000000..ad0987c --- /dev/null +++ b/dist/test/components/config.js @@ -0,0 +1,13 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var ZabbixAppConfigCtrl = exports.ZabbixAppConfigCtrl = function ZabbixAppConfigCtrl() { + _classCallCheck(this, ZabbixAppConfigCtrl); +}; + +ZabbixAppConfigCtrl.templateUrl = 'components/config.html'; diff --git a/dist/test/datasource-zabbix/add-metric-function.directive.js b/dist/test/datasource-zabbix/add-metric-function.directive.js new file mode 100644 index 0000000..26b55cc --- /dev/null +++ b/dist/test/datasource-zabbix/add-metric-function.directive.js @@ -0,0 +1,116 @@ +'use strict'; + +var _angular = require('angular'); + +var _angular2 = _interopRequireDefault(_angular); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _jquery = require('jquery'); + +var _jquery2 = _interopRequireDefault(_jquery); + +var _metricFunctions = require('./metricFunctions'); + +var metricFunctions = _interopRequireWildcard(_metricFunctions); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** @ngInject */ +_angular2.default.module('grafana.directives').directive('addMetricFunction', function ($compile) { + var inputTemplate = ''; + + var buttonTemplate = '' + ''; + + return { + link: function link($scope, elem) { + var categories = metricFunctions.getCategories(); + var allFunctions = getAllFunctionNames(categories); + + $scope.functionMenu = createFunctionDropDownMenu(categories); + + var $input = (0, _jquery2.default)(inputTemplate); + var $button = (0, _jquery2.default)(buttonTemplate); + $input.appendTo(elem); + $button.appendTo(elem); + + $input.attr('data-provide', 'typeahead'); + $input.typeahead({ + source: allFunctions, + minLength: 1, + items: 10, + updater: function updater(value) { + var funcDef = metricFunctions.getFuncDef(value); + if (!funcDef) { + // try find close match + value = value.toLowerCase(); + funcDef = _lodash2.default.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 _lodash2.default.reduce(categories, function (list, category) { + _lodash2.default.each(category, function (func) { + list.push(func.name); + }); + return list; + }, []); +} + +function createFunctionDropDownMenu(categories) { + return _lodash2.default.map(categories, function (list, category) { + return { + text: category, + submenu: _lodash2.default.map(list, function (value) { + return { + text: value.name, + click: "ctrl.addFunction('" + value.name + "')" + }; + }) + }; + }); +} diff --git a/dist/test/datasource-zabbix/benchmarks/timeseries_bench.js b/dist/test/datasource-zabbix/benchmarks/timeseries_bench.js new file mode 100644 index 0000000..8e84417 --- /dev/null +++ b/dist/test/datasource-zabbix/benchmarks/timeseries_bench.js @@ -0,0 +1,72 @@ +'use strict'; + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _timeseries = require('../timeseries'); + +var _timeseries2 = _interopRequireDefault(_timeseries); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var datapoints = [[10.7104, 1498409636085], [10.578, 1498409651011], [10.5985, 1498409666628], [10.6877, 1498409681525], [10.5495, 1498409696586], [10.5981, 1498409711009], [10.5076, 1498409726949], [11.4807, 1498409741853], [11.6165, 1498409756165], [11.8575, 1498409771018], [11.9936, 1498409786056], [10.7566, 1498409801942], [10.7484, 1498409816010], [10.6038, 1498409831018], [10.2932, 1498409846010], [10.4912, 1498409861946], [10.4151, 1498409876871], [10.2401, 1498409891710], [10.4921, 1498409906143], [10.4413, 1498409921477], [10.6318, 1498409936147], [10.5277, 1498409951915], [10.6333, 1498409966052], [10.6417, 1498409981944], [10.4505, 1498409996867], [10.5812, 1498410011770], [10.4934, 1498410026573], [10.5731, 1498410041317], [10.5, 1498410056213], [10.6505, 1498410071013], [9.4035, 1498410086387]]; + +var series_set = [[[1.0247, 1498409631773], [0.9988, 1498409646697], [0.9817, 1498409661239], [0.9569, 1498409676045], [1.0331, 1498409691922], [1.0755, 1498409706546], [1.1862, 1498409721525], [1.2984, 1498409736175], [1.2389, 1498409751817], [1.1452, 1498409766783], [1.102, 1498409781699], [0.9647, 1498409796664], [1.0063, 1498409811627], [1.0318, 1498409826887], [1.065, 1498409841645], [1.0907, 1498409856647], [1.0229, 1498409871521], [1.0654, 1498409886031], [1.0568, 1498409901544], [1.0818, 1498409916194], [1.1335, 1498409931672], [1.057, 1498409946673], [1.0243, 1498409961669], [1.0329, 1498409976637], [1.1428, 1498409991563], [1.2198, 1498410006441], [1.2192, 1498410021230], [1.2615, 1498410036027], [1.1765, 1498410051907], [1.2352, 1498410066109], [1.0557, 1498410081043]], [[10.7104, 1498409636085], [10.578, 1498409651011], [10.5985, 1498409666628], [10.6877, 1498409681525], [10.5495, 1498409696586], [10.5981, 1498409711009], [10.5076, 1498409726949], [11.4807, 1498409741853], [11.6165, 1498409756165], [11.8575, 1498409771018], [11.9936, 1498409786056], [10.7566, 1498409801942], [10.7484, 1498409816010], [10.6038, 1498409831018], [10.2932, 1498409846010], [10.4912, 1498409861946], [10.4151, 1498409876871], [10.2401, 1498409891710], [10.4921, 1498409906143], [10.4413, 1498409921477], [10.6318, 1498409936147], [10.5277, 1498409951915], [10.6333, 1498409966052], [10.6417, 1498409981944], [10.4505, 1498409996867], [10.5812, 1498410011770], [10.4934, 1498410026573], [10.5731, 1498410041317], [10.5, 1498410056213], [10.6505, 1498410071013], [9.4035, 1498410086387]]]; + +var growing_series = [[10755200, 1498332216642], [10761200, 1498332276802], [10767200, 1498332336367], [10773200, 1498332396584], [10779200, 1498332456880], [10785200, 1498332516479], [10791200, 1498332576610], [10797200, 1498332636353], [10803200, 1498332696513], [10809200, 1498332756884], [10815200, 1498332816890], [10821200, 1498332876305], [10827200, 1498332936384], [10833200, 1498332996659], [10839200, 1498333056965], [10845200, 1498333116748], [10851200, 1498333176687], [10857200, 1498333236646], [10863200, 1498333297034], [10869200, 1498333356358], [10875200, 1498333416445], [4800, 1498333536686], [17900, 1498333667962], [24000, 1498333729157], [29500, 1498333783662], [34800, 1498333836813], [40700, 1498333896403], [46800, 1498333956953], [52800, 1498334016976], [6000, 1498334136593], [12000, 1498334196567]]; + +module.exports = [{ + name: 'groupBy', + tests: { + 'groupBy(AVERAGE)': function groupByAVERAGE() { + _timeseries2.default.groupBy(datapoints, '5m', _timeseries2.default.AVERAGE); + }, + 'groupBy(MAX)': function groupByMAX() { + _timeseries2.default.groupBy(datapoints, '5m', _timeseries2.default.COUNT); + } + } +}, { + name: 'sumSeries', + tests: { + 'sumSeries()': function sumSeries() { + _timeseries2.default.sumSeries(series_set); + }, + 'groupBy(MAX)->sumSeries()': function groupByMAXSumSeries() { + var prepeared_series = _lodash2.default.map(series_set, function (datapoints) { + return _timeseries2.default.groupBy(datapoints, '5m', _timeseries2.default.MAX); + }); + _timeseries2.default.sumSeries(prepeared_series); + } + } +}, { + name: 'delta vs rate', + tests: { + 'delta()': function delta() { + _timeseries2.default.delta(growing_series); + }, + 'rate()': function rate() { + _timeseries2.default.rate(growing_series); + } + } +}, { + name: 'scale', + tests: { + 'scale()': function scale() { + _timeseries2.default.scale(datapoints, 42); + }, + 'scale_perf()': function scale_perf() { + _timeseries2.default.scale_perf(datapoints, 42); + } + } +}, { + name: 'groupBy vs groupBy_perf', + tests: { + 'groupBy()': function groupBy() { + _timeseries2.default.groupBy(datapoints, '5m', _timeseries2.default.AVERAGE); + }, + 'groupBy_perf()': function groupBy_perf() { + _timeseries2.default.groupBy_perf(datapoints, '5m', _timeseries2.default.AVERAGE); + } + } +}]; diff --git a/dist/test/datasource-zabbix/config.controller.js b/dist/test/datasource-zabbix/config.controller.js new file mode 100644 index 0000000..02e522f --- /dev/null +++ b/dist/test/datasource-zabbix/config.controller.js @@ -0,0 +1,50 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.ZabbixDSConfigController = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var SUPPORTED_SQL_DS = ['mysql', 'postgres']; + +var defaultConfig = { + dbConnection: { + enable: false + } +}; + +var ZabbixDSConfigController = exports.ZabbixDSConfigController = function () { + /** @ngInject */ + function ZabbixDSConfigController($scope, $injector, datasourceSrv) { + _classCallCheck(this, ZabbixDSConfigController); + + this.datasourceSrv = datasourceSrv; + + _lodash2.default.defaults(this.current.jsonData, defaultConfig); + this.sqlDataSources = this.getSupportedSQLDataSources(); + } + + _createClass(ZabbixDSConfigController, [{ + key: 'getSupportedSQLDataSources', + value: function getSupportedSQLDataSources() { + var datasources = this.datasourceSrv.getAll(); + return _lodash2.default.filter(datasources, function (ds) { + return _lodash2.default.includes(SUPPORTED_SQL_DS, ds.type); + }); + } + }]); + + return ZabbixDSConfigController; +}(); + +ZabbixDSConfigController.templateUrl = 'datasource-zabbix/partials/config.html'; diff --git a/dist/test/datasource-zabbix/constants.js b/dist/test/datasource-zabbix/constants.js new file mode 100644 index 0000000..08418be --- /dev/null +++ b/dist/test/datasource-zabbix/constants.js @@ -0,0 +1,29 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +// Editor modes +var MODE_METRICS = exports.MODE_METRICS = 0; +var MODE_ITSERVICE = exports.MODE_ITSERVICE = 1; +var MODE_TEXT = exports.MODE_TEXT = 2; +var MODE_ITEMID = exports.MODE_ITEMID = 3; +var MODE_TRIGGERS = exports.MODE_TRIGGERS = 4; + +// Triggers severity +var SEV_NOT_CLASSIFIED = exports.SEV_NOT_CLASSIFIED = 0; +var SEV_INFORMATION = exports.SEV_INFORMATION = 1; +var SEV_WARNING = exports.SEV_WARNING = 2; +var SEV_AVERAGE = exports.SEV_AVERAGE = 3; +var SEV_HIGH = exports.SEV_HIGH = 4; +var SEV_DISASTER = exports.SEV_DISASTER = 5; + +var SHOW_ALL_TRIGGERS = exports.SHOW_ALL_TRIGGERS = [0, 1]; +var SHOW_ALL_EVENTS = exports.SHOW_ALL_EVENTS = [0, 1]; +var SHOW_OK_EVENTS = exports.SHOW_OK_EVENTS = 1; + +// Data point +var DATAPOINT_VALUE = exports.DATAPOINT_VALUE = 0; +var DATAPOINT_TS = exports.DATAPOINT_TS = 1; + +var TRIGGER_SEVERITY = exports.TRIGGER_SEVERITY = [{ val: 0, text: 'Not classified' }, { val: 1, text: 'Information' }, { val: 2, text: 'Warning' }, { val: 3, text: 'Average' }, { val: 4, text: 'High' }, { val: 5, text: 'Disaster' }]; diff --git a/dist/test/datasource-zabbix/dataProcessor.js b/dist/test/datasource-zabbix/dataProcessor.js new file mode 100644 index 0000000..61a2ab1 --- /dev/null +++ b/dist/test/datasource-zabbix/dataProcessor.js @@ -0,0 +1,191 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _utils = require('./utils'); + +var utils = _interopRequireWildcard(_utils); + +var _timeseries = require('./timeseries'); + +var _timeseries2 = _interopRequireDefault(_timeseries); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var downsampleSeries = _timeseries2.default.downsample; +var groupBy = _timeseries2.default.groupBy_perf; +var groupBy_exported = function groupBy_exported(interval, groupFunc, datapoints) { + return groupBy(datapoints, interval, groupFunc); +}; +var sumSeries = _timeseries2.default.sumSeries; +var delta = _timeseries2.default.delta; +var rate = _timeseries2.default.rate; +var scale = function scale(factor, datapoints) { + return _timeseries2.default.scale_perf(datapoints, factor); +}; +var simpleMovingAverage = function simpleMovingAverage(n, datapoints) { + return _timeseries2.default.simpleMovingAverage(datapoints, n); +}; +var expMovingAverage = function expMovingAverage(a, datapoints) { + return _timeseries2.default.expMovingAverage(datapoints, a); +}; + +var SUM = _timeseries2.default.SUM; +var COUNT = _timeseries2.default.COUNT; +var AVERAGE = _timeseries2.default.AVERAGE; +var MIN = _timeseries2.default.MIN; +var MAX = _timeseries2.default.MAX; +var MEDIAN = _timeseries2.default.MEDIAN; +var PERCENTIL = _timeseries2.default.PERCENTIL; + +function limit(order, n, orderByFunc, timeseries) { + var orderByCallback = aggregationFunctions[orderByFunc]; + var sortByIteratee = function sortByIteratee(ts) { + var values = _lodash2.default.map(ts.datapoints, function (point) { + return point[0]; + }); + return orderByCallback(values); + }; + var sortedTimeseries = _lodash2.default.sortBy(timeseries, sortByIteratee); + if (order === 'bottom') { + return sortedTimeseries.slice(0, n); + } else { + return sortedTimeseries.slice(-n); + } +} + +function sortSeries(direction, timeseries) { + return _lodash2.default.orderBy(timeseries, [function (ts) { + return ts.target.toLowerCase(); + }], direction); +} + +function setAlias(alias, timeseries) { + timeseries.target = alias; + return timeseries; +} + +function replaceAlias(regexp, newAlias, timeseries) { + var pattern = void 0; + if (utils.isRegex(regexp)) { + pattern = utils.buildRegex(regexp); + } else { + pattern = regexp; + } + + var alias = timeseries.target.replace(pattern, newAlias); + timeseries.target = alias; + return timeseries; +} + +function setAliasByRegex(alias, timeseries) { + timeseries.target = extractText(timeseries.target, alias); + return timeseries; +} + +function extractText(str, pattern) { + var extractPattern = new RegExp(pattern); + var extractedValue = extractPattern.exec(str); + extractedValue = extractedValue[0]; + return extractedValue; +} + +function groupByWrapper(interval, groupFunc, datapoints) { + var groupByCallback = aggregationFunctions[groupFunc]; + return groupBy(datapoints, interval, groupByCallback); +} + +function aggregateByWrapper(interval, aggregateFunc, datapoints) { + // Flatten all points in frame and then just use groupBy() + var flattenedPoints = _lodash2.default.flatten(datapoints, true); + var groupByCallback = aggregationFunctions[aggregateFunc]; + return groupBy(flattenedPoints, interval, groupByCallback); +} + +function aggregateWrapper(groupByCallback, interval, datapoints) { + var flattenedPoints = _lodash2.default.flatten(datapoints, true); + return groupBy(flattenedPoints, interval, groupByCallback); +} + +function percentil(interval, n, datapoints) { + var flattenedPoints = _lodash2.default.flatten(datapoints, true); + var groupByCallback = _lodash2.default.partial(PERCENTIL, n); + return groupBy(flattenedPoints, interval, groupByCallback); +} + +function timeShift(interval, range) { + var shift = utils.parseTimeShiftInterval(interval) / 1000; + return _lodash2.default.map(range, function (time) { + return time - shift; + }); +} + +function unShiftTimeSeries(interval, datapoints) { + var unshift = utils.parseTimeShiftInterval(interval); + return _lodash2.default.map(datapoints, function (dp) { + return [dp[0], dp[1] + unshift]; + }); +} + +var metricFunctions = { + groupBy: groupByWrapper, + scale: scale, + delta: delta, + rate: rate, + movingAverage: simpleMovingAverage, + exponentialMovingAverage: expMovingAverage, + aggregateBy: aggregateByWrapper, + // Predefined aggs + percentil: percentil, + average: _lodash2.default.partial(aggregateWrapper, AVERAGE), + min: _lodash2.default.partial(aggregateWrapper, MIN), + max: _lodash2.default.partial(aggregateWrapper, MAX), + median: _lodash2.default.partial(aggregateWrapper, MEDIAN), + sum: _lodash2.default.partial(aggregateWrapper, SUM), + count: _lodash2.default.partial(aggregateWrapper, COUNT), + sumSeries: sumSeries, + top: _lodash2.default.partial(limit, 'top'), + bottom: _lodash2.default.partial(limit, 'bottom'), + sortSeries: sortSeries, + timeShift: timeShift, + setAlias: setAlias, + setAliasByRegex: setAliasByRegex, + replaceAlias: replaceAlias +}; + +var aggregationFunctions = { + avg: AVERAGE, + min: MIN, + max: MAX, + median: MEDIAN, + sum: SUM, + count: COUNT +}; + +exports.default = { + downsampleSeries: downsampleSeries, + groupBy: groupBy_exported, + AVERAGE: AVERAGE, + MIN: MIN, + MAX: MAX, + MEDIAN: MEDIAN, + SUM: SUM, + COUNT: COUNT, + unShiftTimeSeries: unShiftTimeSeries, + + get aggregationFunctions() { + return aggregationFunctions; + }, + + get metricFunctions() { + return metricFunctions; + } +}; diff --git a/dist/test/datasource-zabbix/datasource.js b/dist/test/datasource-zabbix/datasource.js new file mode 100644 index 0000000..d626c34 --- /dev/null +++ b/dist/test/datasource-zabbix/datasource.js @@ -0,0 +1,879 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.zabbixTemplateFormat = exports.ZabbixAPIDatasource = undefined; + +var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _datemath = require('app/core/utils/datemath'); + +var dateMath = _interopRequireWildcard(_datemath); + +var _utils = require('./utils'); + +var utils = _interopRequireWildcard(_utils); + +var _migrations = require('./migrations'); + +var migrations = _interopRequireWildcard(_migrations); + +var _metricFunctions = require('./metricFunctions'); + +var metricFunctions = _interopRequireWildcard(_metricFunctions); + +var _constants = require('./constants'); + +var c = _interopRequireWildcard(_constants); + +var _dataProcessor = require('./dataProcessor'); + +var _dataProcessor2 = _interopRequireDefault(_dataProcessor); + +var _responseHandler = require('./responseHandler'); + +var _responseHandler2 = _interopRequireDefault(_responseHandler); + +require('./zabbix.js'); + +require('./zabbixAlerting.service.js'); + +var _zabbixAPICoreService = require('./zabbixAPICore.service.js'); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var ZabbixAPIDatasource = function () { + + /** @ngInject */ + function ZabbixAPIDatasource(instanceSettings, templateSrv, alertSrv, dashboardSrv, zabbixAlertingSrv, Zabbix) { + _classCallCheck(this, ZabbixAPIDatasource); + + this.templateSrv = templateSrv; + this.alertSrv = alertSrv; + this.dashboardSrv = dashboardSrv; + this.zabbixAlertingSrv = zabbixAlertingSrv; + + // Use custom format for template variables + this.replaceTemplateVars = _lodash2.default.partial(replaceTemplateVars, this.templateSrv); + + // General data source settings + this.name = instanceSettings.name; + this.url = instanceSettings.url; + this.basicAuth = instanceSettings.basicAuth; + this.withCredentials = instanceSettings.withCredentials; + + var jsonData = instanceSettings.jsonData; + + // Zabbix API credentials + this.username = jsonData.username; + this.password = jsonData.password; + + // Use trends instead history since specified time + this.trends = jsonData.trends; + this.trendsFrom = jsonData.trendsFrom || '7d'; + this.trendsRange = jsonData.trendsRange || '4d'; + + // Set cache update interval + var ttl = jsonData.cacheTTL || '1h'; + this.cacheTTL = utils.parseInterval(ttl); + + // Alerting options + this.alertingEnabled = jsonData.alerting; + this.addThresholds = jsonData.addThresholds; + this.alertingMinSeverity = jsonData.alertingMinSeverity || c.SEV_WARNING; + + // Direct DB Connection options + var dbConnectionOptions = jsonData.dbConnection || {}; + this.enableDirectDBConnection = dbConnectionOptions.enable; + this.sqlDatasourceId = dbConnectionOptions.datasourceId; + + var zabbixOptions = { + username: this.username, + password: this.password, + basicAuth: this.basicAuth, + withCredentials: this.withCredentials, + cacheTTL: this.cacheTTL, + enableDirectDBConnection: this.enableDirectDBConnection, + sqlDatasourceId: this.sqlDatasourceId + }; + + this.zabbix = new Zabbix(this.url, zabbixOptions); + } + + //////////////////////// + // Datasource methods // + //////////////////////// + + /** + * 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. + */ + + + _createClass(ZabbixAPIDatasource, [{ + key: 'query', + value: function query(options) { + var _this = this; + + // Get alerts for current panel + if (this.alertingEnabled) { + this.alertQuery(options).then(function (alert) { + _this.zabbixAlertingSrv.setPanelAlertState(options.panelId, alert.state); + + _this.zabbixAlertingSrv.removeZabbixThreshold(options.panelId); + if (_this.addThresholds) { + _lodash2.default.forEach(alert.thresholds, function (threshold) { + _this.zabbixAlertingSrv.setPanelThreshold(options.panelId, threshold); + }); + } + }); + } + + // Create request for each target + var promises = _lodash2.default.map(options.targets, function (t) { + // Don't request undefined and hidden targets + if (t.hide) { + return []; + } + + var timeFrom = Math.ceil(dateMath.parse(options.range.from) / 1000); + var timeTo = Math.ceil(dateMath.parse(options.range.to) / 1000); + + // Prevent changes of original object + var target = _lodash2.default.cloneDeep(t); + _this.replaceTargetVariables(target, options); + + // Apply Time-related functions (timeShift(), etc) + var timeFunctions = bindFunctionDefs(target.functions, 'Time'); + if (timeFunctions.length) { + var _sequence = sequence(timeFunctions)([timeFrom, timeTo]), + _sequence2 = _slicedToArray(_sequence, 2), + time_from = _sequence2[0], + time_to = _sequence2[1]; + + timeFrom = time_from; + timeTo = time_to; + } + var timeRange = [timeFrom, timeTo]; + + var useTrends = _this.isUseTrends(timeRange); + + // Metrics or Text query mode + if (!target.mode || target.mode === c.MODE_METRICS || target.mode === c.MODE_TEXT) { + // Migrate old targets + target = migrations.migrate(target); + + // Don't request undefined and hidden targets + if (target.hide || !target.group || !target.host || !target.item) { + return []; + } + + if (!target.mode || target.mode === c.MODE_METRICS) { + return _this.queryNumericData(target, timeRange, useTrends, options); + } else if (target.mode === c.MODE_TEXT) { + return _this.queryTextData(target, timeRange); + } + } else if (target.mode === c.MODE_ITEMID) { + // Item ID mode + if (!target.itemids) { + return []; + } + return _this.queryItemIdData(target, timeRange, useTrends, options); + } else if (target.mode === c.MODE_ITSERVICE) { + // IT services mode + return _this.queryITServiceData(target, timeRange, options); + } else if (target.mode === c.MODE_TRIGGERS) { + // Triggers mode + return _this.queryTriggersData(target, timeRange); + } else { + return []; + } + }); + + // Data for panel (all targets) + return Promise.all(_lodash2.default.flatten(promises)).then(_lodash2.default.flatten).then(function (data) { + return { data: data }; + }); + } + + /** + * Query target data for Metrics mode + */ + + }, { + key: 'queryNumericData', + value: function queryNumericData(target, timeRange, useTrends, options) { + var _this2 = this; + + var getItemOptions = { + itemtype: 'num' + }; + return this.zabbix.getItemsFromTarget(target, getItemOptions).then(function (items) { + return _this2.queryNumericDataForItems(items, target, timeRange, useTrends, options); + }); + } + + /** + * Query history for numeric items + */ + + }, { + key: 'queryNumericDataForItems', + value: function queryNumericDataForItems(items, target, timeRange, useTrends, options) { + var _this3 = this; + + var _timeRange = _slicedToArray(timeRange, 2), + timeFrom = _timeRange[0], + timeTo = _timeRange[1]; + + var getHistoryPromise = void 0; + options.consolidateBy = getConsolidateBy(target); + + if (useTrends) { + if (this.enableDirectDBConnection) { + getHistoryPromise = this.zabbix.getTrendsDB(items, timeFrom, timeTo, options).then(function (history) { + return _this3.zabbix.dbConnector.handleGrafanaTSResponse(history, items); + }); + } else { + var valueType = this.getTrendValueType(target); + getHistoryPromise = this.zabbix.getTrend(items, timeFrom, timeTo).then(function (history) { + return _responseHandler2.default.handleTrends(history, items, valueType); + }).then(function (timeseries) { + // Sort trend data, issue #202 + _lodash2.default.forEach(timeseries, function (series) { + series.datapoints = _lodash2.default.sortBy(series.datapoints, function (point) { + return point[c.DATAPOINT_TS]; + }); + }); + return timeseries; + }); + } + } else { + // Use history + if (this.enableDirectDBConnection) { + getHistoryPromise = this.zabbix.getHistoryDB(items, timeFrom, timeTo, options).then(function (history) { + return _this3.zabbix.dbConnector.handleGrafanaTSResponse(history, items); + }); + } else { + getHistoryPromise = this.zabbix.getHistory(items, timeFrom, timeTo).then(function (history) { + return _responseHandler2.default.handleHistory(history, items); + }); + } + } + + return getHistoryPromise.then(function (timeseries) { + return _this3.applyDataProcessingFunctions(timeseries, target); + }).then(function (timeseries) { + return downsampleSeries(timeseries, options); + }).catch(function (error) { + console.log(error); + return []; + }); + } + }, { + key: 'getTrendValueType', + value: function getTrendValueType(target) { + // Find trendValue() function and get specified trend value + var trendFunctions = _lodash2.default.map(metricFunctions.getCategories()['Trends'], 'name'); + var trendValueFunc = _lodash2.default.find(target.functions, function (func) { + return _lodash2.default.includes(trendFunctions, func.def.name); + }); + return trendValueFunc ? trendValueFunc.params[0] : "avg"; + } + }, { + key: 'applyDataProcessingFunctions', + value: function applyDataProcessingFunctions(timeseries_data, target) { + var transformFunctions = bindFunctionDefs(target.functions, 'Transform'); + var aggregationFunctions = bindFunctionDefs(target.functions, 'Aggregate'); + var filterFunctions = bindFunctionDefs(target.functions, 'Filter'); + var aliasFunctions = bindFunctionDefs(target.functions, 'Alias'); + + // Apply transformation functions + timeseries_data = _lodash2.default.cloneDeep(_lodash2.default.map(timeseries_data, function (timeseries) { + timeseries.datapoints = sequence(transformFunctions)(timeseries.datapoints); + return timeseries; + })); + + // Apply filter functions + if (filterFunctions.length) { + timeseries_data = sequence(filterFunctions)(timeseries_data); + } + + // Apply aggregations + if (aggregationFunctions.length) { + var dp = _lodash2.default.map(timeseries_data, 'datapoints'); + dp = sequence(aggregationFunctions)(dp); + + var aggFuncNames = _lodash2.default.map(metricFunctions.getCategories()['Aggregate'], 'name'); + var lastAgg = _lodash2.default.findLast(target.functions, function (func) { + return _lodash2.default.includes(aggFuncNames, func.def.name); + }); + + timeseries_data = [{ + target: lastAgg.text, + datapoints: dp + }]; + } + + // Apply alias functions + _lodash2.default.forEach(timeseries_data, sequence(aliasFunctions)); + + // Apply Time-related functions (timeShift(), etc) + // Find timeShift() function and get specified trend value + this.applyTimeShiftFunction(timeseries_data, target); + + return timeseries_data; + } + }, { + key: 'applyTimeShiftFunction', + value: function applyTimeShiftFunction(timeseries_data, target) { + // Find timeShift() function and get specified interval + var timeShiftFunc = _lodash2.default.find(target.functions, function (func) { + return func.def.name === 'timeShift'; + }); + if (timeShiftFunc) { + var shift = timeShiftFunc.params[0]; + _lodash2.default.forEach(timeseries_data, function (series) { + series.datapoints = _dataProcessor2.default.unShiftTimeSeries(shift, series.datapoints); + }); + } + } + + /** + * Query target data for Text mode + */ + + }, { + key: 'queryTextData', + value: function queryTextData(target, timeRange) { + var _this4 = this; + + var _timeRange2 = _slicedToArray(timeRange, 2), + timeFrom = _timeRange2[0], + timeTo = _timeRange2[1]; + + var options = { + itemtype: 'text' + }; + return this.zabbix.getItemsFromTarget(target, options).then(function (items) { + if (items.length) { + return _this4.zabbix.getHistory(items, timeFrom, timeTo).then(function (history) { + return _responseHandler2.default.handleText(history, items, target); + }); + } else { + return Promise.resolve([]); + } + }); + } + + /** + * Query target data for Item ID mode + */ + + }, { + key: 'queryItemIdData', + value: function queryItemIdData(target, timeRange, useTrends, options) { + var _this5 = this; + + var itemids = target.itemids; + itemids = this.templateSrv.replace(itemids, options.scopedVars, zabbixItemIdsTemplateFormat); + itemids = _lodash2.default.map(itemids.split(','), function (itemid) { + return itemid.trim(); + }); + + if (!itemids) { + return []; + } + + return this.zabbix.getItemsByIDs(itemids).then(function (items) { + return _this5.queryNumericDataForItems(items, target, timeRange, useTrends, options); + }); + } + + /** + * Query target data for IT Services mode + */ + + }, { + key: 'queryITServiceData', + value: function queryITServiceData(target, timeRange, options) { + var _this6 = this; + + // Don't show undefined and hidden targets + if (target.hide || !target.itservice && !target.itServiceFilter || !target.slaProperty) { + return []; + } + + var itServiceIds = []; + var itServices = []; + var itServiceFilter = void 0; + var isOldVersion = target.itservice && !target.itServiceFilter; + + if (isOldVersion) { + // Backward compatibility + itServiceFilter = '/.*/'; + } else { + itServiceFilter = this.replaceTemplateVars(target.itServiceFilter, options.scopedVars); + } + + return this.zabbix.getITServices(itServiceFilter).then(function (itservices) { + itServices = itservices; + if (isOldVersion) { + itServices = _lodash2.default.filter(itServices, { 'serviceid': target.itservice.serviceid }); + } + + itServiceIds = _lodash2.default.map(itServices, 'serviceid'); + return itServiceIds; + }).then(function (serviceids) { + return _this6.zabbix.getSLA(serviceids, timeRange); + }).then(function (slaResponse) { + return _lodash2.default.map(itServiceIds, function (serviceid) { + var itservice = _lodash2.default.find(itServices, { 'serviceid': serviceid }); + return _responseHandler2.default.handleSLAResponse(itservice, target.slaProperty, slaResponse); + }); + }); + } + }, { + key: 'queryTriggersData', + value: function queryTriggersData(target, timeRange) { + var _this7 = this; + + var _timeRange3 = _slicedToArray(timeRange, 2), + timeFrom = _timeRange3[0], + timeTo = _timeRange3[1]; + + return this.zabbix.getHostsFromTarget(target).then(function (results) { + var _results = _slicedToArray(results, 2), + hosts = _results[0], + apps = _results[1]; + + if (hosts.length) { + var hostids = _lodash2.default.map(hosts, 'hostid'); + var appids = _lodash2.default.map(apps, 'applicationid'); + var options = { + minSeverity: target.triggers.minSeverity, + acknowledged: target.triggers.acknowledged, + count: target.triggers.count, + timeFrom: timeFrom, + timeTo: timeTo + }; + return _this7.zabbix.getHostAlerts(hostids, appids, options).then(function (triggers) { + return _responseHandler2.default.handleTriggersResponse(triggers, timeRange); + }); + } else { + return Promise.resolve([]); + } + }); + } + + /** + * Test connection to Zabbix API + * @return {object} Connection status and Zabbix API version + */ + + }, { + key: 'testDatasource', + value: function testDatasource() { + var _this8 = this; + + var zabbixVersion = void 0; + return this.zabbix.getVersion().then(function (version) { + zabbixVersion = version; + return _this8.zabbix.login(); + }).then(function () { + if (_this8.enableDirectDBConnection) { + return _this8.zabbix.dbConnector.testSQLDataSource(); + } else { + return Promise.resolve(); + } + }).then(function () { + return { + status: "success", + title: "Success", + message: "Zabbix API version: " + zabbixVersion + }; + }).catch(function (error) { + if (error instanceof _zabbixAPICoreService.ZabbixAPIError) { + return { + status: "error", + title: error.message, + message: error.message + }; + } else if (error.data && error.data.message) { + return { + status: "error", + title: "Connection failed", + message: "Connection failed: " + error.data.message + }; + } else { + return { + status: "error", + title: "Connection failed", + message: "Could not connect to given url" + }; + } + }); + } + + //////////////// + // Templating // + //////////////// + + /** + * 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. + */ + + }, { + key: 'metricFindQuery', + value: function metricFindQuery(query) { + var _this9 = this; + + var result = void 0; + var parts = []; + + // Split query. Query structure: group.host.app.item + _lodash2.default.each(utils.splitTemplateQuery(query), function (part) { + part = _this9.replaceTemplateVars(part, {}); + + // Replace wildcard to regex + if (part === '*') { + part = '/.*/'; + } + parts.push(part); + }); + var template = _lodash2.default.zipObject(['group', 'host', 'app', 'item'], parts); + + // Get items + if (parts.length === 4) { + // Search for all items, even it's not belong to any application + if (template.app === '/.*/') { + template.app = ''; + } + result = this.zabbix.getItems(template.group, template.host, template.app, template.item); + } else if (parts.length === 3) { + // Get applications + result = this.zabbix.getApps(template.group, template.host, template.app); + } else if (parts.length === 2) { + // Get hosts + result = this.zabbix.getHosts(template.group, template.host); + } else if (parts.length === 1) { + // Get groups + result = this.zabbix.getGroups(template.group); + } else { + result = Promise.resolve([]); + } + + return result.then(function (metrics) { + return _lodash2.default.map(metrics, formatMetric); + }); + } + + ///////////////// + // Annotations // + ///////////////// + + }, { + key: 'annotationQuery', + value: function annotationQuery(options) { + var _this10 = this; + + var timeFrom = Math.ceil(dateMath.parse(options.rangeRaw.from) / 1000); + var timeTo = Math.ceil(dateMath.parse(options.rangeRaw.to) / 1000); + var annotation = options.annotation; + var showOkEvents = annotation.showOkEvents ? c.SHOW_ALL_EVENTS : c.SHOW_OK_EVENTS; + + // Show all triggers + var triggersOptions = { + showTriggers: c.SHOW_ALL_TRIGGERS, + hideHostsInMaintenance: false + }; + + var getTriggers = this.zabbix.getTriggers(this.replaceTemplateVars(annotation.group, {}), this.replaceTemplateVars(annotation.host, {}), this.replaceTemplateVars(annotation.application, {}), triggersOptions); + + return getTriggers.then(function (triggers) { + + // Filter triggers by description + var triggerName = _this10.replaceTemplateVars(annotation.trigger, {}); + if (utils.isRegex(triggerName)) { + triggers = _lodash2.default.filter(triggers, function (trigger) { + return utils.buildRegex(triggerName).test(trigger.description); + }); + } else if (triggerName) { + triggers = _lodash2.default.filter(triggers, function (trigger) { + return trigger.description === triggerName; + }); + } + + // Remove events below the chose severity + triggers = _lodash2.default.filter(triggers, function (trigger) { + return Number(trigger.priority) >= Number(annotation.minseverity); + }); + + var objectids = _lodash2.default.map(triggers, 'triggerid'); + return _this10.zabbix.getEvents(objectids, timeFrom, timeTo, showOkEvents).then(function (events) { + var indexedTriggers = _lodash2.default.keyBy(triggers, 'triggerid'); + + // Hide acknowledged events if option enabled + if (annotation.hideAcknowledged) { + events = _lodash2.default.filter(events, function (event) { + return !event.acknowledges.length; + }); + } + + return _lodash2.default.map(events, function (event) { + var tags = void 0; + if (annotation.showHostname) { + tags = _lodash2.default.map(event.hosts, 'name'); + } + + // Show event type (OK or Problem) + var title = Number(event.value) ? 'Problem' : 'OK'; + + var formatted_acknowledges = utils.formatAcknowledges(event.acknowledges); + return { + annotation: annotation, + time: event.clock * 1000, + title: title, + tags: tags, + text: indexedTriggers[event.objectid].description + formatted_acknowledges + }; + }); + }); + }); + } + + /** + * Get triggers and its details for panel's targets + * Returns alert state ('ok' if no fired triggers, or 'alerting' if at least 1 trigger is fired) + * or empty object if no related triggers are finded. + */ + + }, { + key: 'alertQuery', + value: function alertQuery(options) { + var _this11 = this; + + var enabled_targets = filterEnabledTargets(options.targets); + var getPanelItems = _lodash2.default.map(enabled_targets, function (t) { + var target = _lodash2.default.cloneDeep(t); + _this11.replaceTargetVariables(target, options); + return _this11.zabbix.getItemsFromTarget(target, { itemtype: 'num' }); + }); + + return Promise.all(getPanelItems).then(function (results) { + var items = _lodash2.default.flatten(results); + var itemids = _lodash2.default.map(items, 'itemid'); + + if (itemids.length === 0) { + return []; + } + return _this11.zabbix.getAlerts(itemids); + }).then(function (triggers) { + triggers = _lodash2.default.filter(triggers, function (trigger) { + return trigger.priority >= _this11.alertingMinSeverity; + }); + + if (!triggers || triggers.length === 0) { + return {}; + } + + var state = 'ok'; + + var firedTriggers = _lodash2.default.filter(triggers, { value: '1' }); + if (firedTriggers.length) { + state = 'alerting'; + } + + var thresholds = _lodash2.default.map(triggers, function (trigger) { + return getTriggerThreshold(trigger.expression); + }); + + return { + panelId: options.panelId, + state: state, + thresholds: thresholds + }; + }); + } + + // Replace template variables + + }, { + key: 'replaceTargetVariables', + value: function replaceTargetVariables(target, options) { + var _this12 = this; + + var parts = ['group', 'host', 'application', 'item']; + _lodash2.default.forEach(parts, function (p) { + if (target[p] && target[p].filter) { + target[p].filter = _this12.replaceTemplateVars(target[p].filter, options.scopedVars); + } + }); + target.textFilter = this.replaceTemplateVars(target.textFilter, options.scopedVars); + + _lodash2.default.forEach(target.functions, function (func) { + func.params = _lodash2.default.map(func.params, function (param) { + if (typeof param === 'number') { + return +_this12.templateSrv.replace(param.toString(), options.scopedVars); + } else { + return _this12.templateSrv.replace(param, options.scopedVars); + } + }); + }); + } + }, { + key: 'isUseTrends', + value: function isUseTrends(timeRange) { + var _timeRange4 = _slicedToArray(timeRange, 2), + timeFrom = _timeRange4[0], + timeTo = _timeRange4[1]; + + var useTrendsFrom = Math.ceil(dateMath.parse('now-' + this.trendsFrom) / 1000); + var useTrendsRange = Math.ceil(utils.parseInterval(this.trendsRange) / 1000); + var useTrends = this.trends && (timeFrom <= useTrendsFrom || timeTo - timeFrom >= useTrendsRange); + return useTrends; + } + }]); + + return ZabbixAPIDatasource; +}(); + +function bindFunctionDefs(functionDefs, category) { + var aggregationFunctions = _lodash2.default.map(metricFunctions.getCategories()[category], 'name'); + var aggFuncDefs = _lodash2.default.filter(functionDefs, function (func) { + return _lodash2.default.includes(aggregationFunctions, func.def.name); + }); + + return _lodash2.default.map(aggFuncDefs, function (func) { + var funcInstance = metricFunctions.createFuncInstance(func.def, func.params); + return funcInstance.bindFunction(_dataProcessor2.default.metricFunctions); + }); +} + +function getConsolidateBy(target) { + var consolidateBy = 'avg'; + var funcDef = _lodash2.default.find(target.functions, function (func) { + return func.def.name === 'consolidateBy'; + }); + if (funcDef && funcDef.params && funcDef.params.length) { + consolidateBy = funcDef.params[0]; + } + return consolidateBy; +} + +function downsampleSeries(timeseries_data, options) { + var defaultAgg = _dataProcessor2.default.aggregationFunctions['avg']; + var consolidateByFunc = _dataProcessor2.default.aggregationFunctions[options.consolidateBy] || defaultAgg; + return _lodash2.default.map(timeseries_data, function (timeseries) { + if (timeseries.datapoints.length > options.maxDataPoints) { + timeseries.datapoints = _dataProcessor2.default.groupBy(options.interval, consolidateByFunc, timeseries.datapoints); + } + return timeseries; + }); +} + +function formatMetric(metricObj) { + return { + text: metricObj.name, + expandable: false + }; +} + +/** + * Custom formatter for template variables. + * Default Grafana "regex" formatter returns + * value1|value2 + * This formatter returns + * (value1|value2) + * This format needed for using in complex regex with + * template variables, for example + * /CPU $cpu_item.*time/ where $cpu_item is system,user,iowait + */ +function zabbixTemplateFormat(value) { + if (typeof value === 'string') { + return utils.escapeRegex(value); + } + + var escapedValues = _lodash2.default.map(value, utils.escapeRegex); + return '(' + escapedValues.join('|') + ')'; +} + +function zabbixItemIdsTemplateFormat(value) { + if (typeof value === 'string') { + return value; + } + return value.join(','); +} + +/** + * If template variables are used in request, replace it using regex format + * and wrap with '/' for proper multi-value work. Example: + * $variable selected as a, b, c + * We use filter $variable + * $variable -> a|b|c -> /a|b|c/ + * /$variable/ -> /a|b|c/ -> /a|b|c/ + */ +function replaceTemplateVars(templateSrv, target, scopedVars) { + var replacedTarget = templateSrv.replace(target, scopedVars, zabbixTemplateFormat); + if (target !== replacedTarget && !utils.isRegex(replacedTarget)) { + replacedTarget = '/^' + replacedTarget + '$/'; + } + return replacedTarget; +} + +// 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; + }; +} + +function filterEnabledTargets(targets) { + return _lodash2.default.filter(targets, function (target) { + return !(target.hide || !target.group || !target.host || !target.item); + }); +} + +function getTriggerThreshold(expression) { + var thresholdPattern = /.*[<>=]{1,2}([\d\.]+)/; + var finded_thresholds = expression.match(thresholdPattern); + if (finded_thresholds && finded_thresholds.length >= 2) { + var threshold = finded_thresholds[1]; + threshold = Number(threshold); + return threshold; + } else { + return null; + } +} + +exports.ZabbixAPIDatasource = ZabbixAPIDatasource; +exports.zabbixTemplateFormat = zabbixTemplateFormat; + +// Fix for backward compatibility with lodash 2.4 + +if (!_lodash2.default.includes) { + _lodash2.default.includes = _lodash2.default.contains; +} +if (!_lodash2.default.keyBy) { + _lodash2.default.keyBy = _lodash2.default.indexBy; +} diff --git a/dist/test/datasource-zabbix/metric-function-editor.directive.js b/dist/test/datasource-zabbix/metric-function-editor.directive.js new file mode 100644 index 0000000..02135e9 --- /dev/null +++ b/dist/test/datasource-zabbix/metric-function-editor.directive.js @@ -0,0 +1,246 @@ +'use strict'; + +var _angular = require('angular'); + +var _angular2 = _interopRequireDefault(_angular); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _jquery = require('jquery'); + +var _jquery2 = _interopRequireDefault(_jquery); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** @ngInject */ +_angular2.default.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 = (0, _jquery2.default)(funcSpanTemplate); + var $funcControls = (0, _jquery2.default)(funcControlsTemplate); + var ctrl = $scope.ctrl; + var func = $scope.func; + var funcDef = func.def; + var scheduledRelink = false; + var paramCountAtLink = 0; + + function clickFuncParam(paramIndex) { + /*jshint validthis:true */ + + var $link = (0, _jquery2.default)(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 = (0, _jquery2.default)(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(function () { + ctrl.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' || funcDef.params[paramIndex].type === 'float') { + options = _lodash2.default.map(options, function (val) { + return val.toString(); + }); + } + + $input.typeahead({ + source: options, + minLength: 0, + items: 20, + updater: function updater(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); + + _lodash2.default.each(funcDef.params, function (param, index) { + if (param.optional && func.params.length <= index) { + return; + } + + if (index > 0) { + (0, _jquery2.default)(', ').appendTo(elem); + } + + var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]); + var $paramLink = (0, _jquery2.default)('' + paramValue + ''); + var $input = (0, _jquery2.default)(paramTemplate); + + paramCountAtLink++; + + $paramLink.appendTo(elem); + $input.appendTo(elem); + + $input.blur(_lodash2.default.partial(inputBlur, index)); + $input.keyup(inputKeyDown); + $input.keypress(_lodash2.default.partial(inputKeyPress, index)); + $paramLink.click(_lodash2.default.partial(clickFuncParam, index)); + + if (funcDef.params[index].options) { + addTypeahead($input, index); + } + }); + + (0, _jquery2.default)(')').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 = (0, _jquery2.default)(e.target); + if ($target.hasClass('fa-remove')) { + toggleFuncControls(); + $scope.$apply(function () { + ctrl.removeFunction($scope.func); + }); + return; + } + + if ($target.hasClass('fa-arrow-left')) { + $scope.$apply(function () { + _lodash2.default.move($scope.target.functions, $scope.$index, $scope.$index - 1); + ctrl.targetChanged(); + }); + return; + } + + if ($target.hasClass('fa-arrow-right')) { + $scope.$apply(function () { + _lodash2.default.move($scope.target.functions, $scope.$index, $scope.$index + 1); + ctrl.targetChanged(); + }); + return; + } + + if ($target.hasClass('fa-question-circle')) { + var docSite = "http://docs.grafana-zabbix.org/reference/functions/"; + window.open(docSite + '#' + funcDef.name.toLowerCase(), '_blank'); + return; + } + }); + } + + function relink() { + elem.children().remove(); + + addElementsAndCompile(); + ifJustAddedFocusFistParam(); + registerFuncControlsToggle(); + registerFuncControlsActions(); + } + + relink(); + } + }; +}); diff --git a/dist/test/datasource-zabbix/metricFunctions.js b/dist/test/datasource-zabbix/metricFunctions.js new file mode 100644 index 0000000..4ed2437 --- /dev/null +++ b/dist/test/datasource-zabbix/metricFunctions.js @@ -0,0 +1,357 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +exports.createFuncInstance = createFuncInstance; +exports.getFuncDef = getFuncDef; +exports.getCategories = getCategories; + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _jquery = require('jquery'); + +var _jquery2 = _interopRequireDefault(_jquery); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var index = []; +var categories = { + Transform: [], + Aggregate: [], + Filter: [], + Trends: [], + Time: [], + Alias: [], + Special: [] +}; + +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; +} + +// Transform + +addFuncDef({ + name: 'groupBy', + category: 'Transform', + params: [{ name: 'interval', type: 'string' }, { name: 'function', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count', 'median'] }], + defaultParams: ['1m', 'avg'] +}); + +addFuncDef({ + name: 'scale', + category: 'Transform', + params: [{ name: 'factor', type: 'float', options: [100, 0.01, 10, -1] }], + defaultParams: [100] +}); + +addFuncDef({ + name: 'delta', + category: 'Transform', + params: [], + defaultParams: [] +}); + +addFuncDef({ + name: 'rate', + category: 'Transform', + params: [], + defaultParams: [] +}); + +addFuncDef({ + name: 'movingAverage', + category: 'Transform', + params: [{ name: 'factor', type: 'int', options: [6, 10, 60, 100, 600] }], + defaultParams: [10] +}); + +addFuncDef({ + name: 'exponentialMovingAverage', + category: 'Transform', + params: [{ name: 'smoothing', type: 'float', options: [6, 10, 60, 100, 600] }], + defaultParams: [0.2] +}); + +// Aggregate + +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: 'percentil', + category: 'Aggregate', + params: [{ name: 'interval', type: 'string' }, { name: 'percent', type: 'float', options: [25, 50, 75, 90, 95, 99, 99.9] }], + defaultParams: ['1m', 95] +}); + +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: 'sum', + category: 'Aggregate', + params: [{ name: 'interval', type: 'string' }], + defaultParams: ['1m'] +}); + +addFuncDef({ + name: 'count', + category: 'Aggregate', + params: [{ name: 'interval', type: 'string' }], + defaultParams: ['1m'] +}); + +addFuncDef({ + name: 'aggregateBy', + category: 'Aggregate', + params: [{ name: 'interval', type: 'string' }, { name: 'function', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count', 'median'] }], + defaultParams: ['1m', 'avg'] +}); + +// Filter + +addFuncDef({ + name: 'top', + category: 'Filter', + params: [{ name: 'number', type: 'int' }, { name: 'value', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count', 'median'] }], + defaultParams: [5, 'avg'] +}); + +addFuncDef({ + name: 'bottom', + category: 'Filter', + params: [{ name: 'number', type: 'int' }, { name: 'value', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count', 'median'] }], + defaultParams: [5, 'avg'] +}); + +addFuncDef({ + name: 'sortSeries', + category: 'Filter', + params: [{ name: 'direction', type: 'string', options: ['asc', 'desc'] }], + defaultParams: ['asc'] +}); + +// Trends + +addFuncDef({ + name: 'trendValue', + category: 'Trends', + params: [{ name: 'type', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count'] }], + defaultParams: ['avg'] +}); + +// Time + +addFuncDef({ + name: 'timeShift', + category: 'Time', + params: [{ name: 'interval', type: 'string', options: ['24h', '7d', '1M', '+24h', '-24h'] }], + defaultParams: ['24h'] +}); + +//Alias + +addFuncDef({ + name: 'setAlias', + category: 'Alias', + params: [{ name: 'alias', type: 'string' }], + defaultParams: [] +}); + +addFuncDef({ + name: 'setAliasByRegex', + category: 'Alias', + params: [{ name: 'aliasByRegex', type: 'string' }], + defaultParams: [] +}); + +addFuncDef({ + name: 'replaceAlias', + category: 'Alias', + params: [{ name: 'regexp', type: 'string' }, { name: 'newAlias', type: 'string' }], + defaultParams: ['/(.*)/', '$1'] +}); + +// Special +addFuncDef({ + name: 'consolidateBy', + category: 'Special', + params: [{ name: 'type', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count'] }], + defaultParams: ['avg'] +}); + +_lodash2.default.each(categories, function (funcList, catName) { + categories[catName] = _lodash2.default.sortBy(funcList, 'name'); +}); + +var FuncInstance = function () { + function FuncInstance(funcDef, params) { + _classCallCheck(this, FuncInstance); + + this.def = funcDef; + + if (params) { + this.params = params; + } else { + // Create with default params + this.params = []; + this.params = funcDef.defaultParams.slice(0); + } + + this.updateText(); + } + + _createClass(FuncInstance, [{ + key: 'bindFunction', + value: function bindFunction(metricFunctions) { + var func = metricFunctions[this.def.name]; + if (func) { + + // Bind function arguments + var bindedFunc = func; + var param; + for (var i = 0; i < this.params.length; i++) { + param = this.params[i]; + + // Convert numeric params + if (this.def.params[i].type === 'int' || this.def.params[i].type === 'float') { + param = Number(param); + } + bindedFunc = _lodash2.default.partial(bindedFunc, param); + } + return bindedFunc; + } else { + throw { message: 'Method not found ' + this.def.name }; + } + } + }, { + key: 'render', + value: function render(metricExp) { + var str = this.def.name + '('; + var parameters = _lodash2.default.map(this.params, function (value, index) { + + var paramType = this.def.params[index].type; + if (paramType === 'int' || paramType === 'float' || paramType === 'value_or_series' || paramType === 'boolean') { + return value; + } else if (paramType === 'int_or_interval' && _jquery2.default.isNumeric(value)) { + return value; + } + + return "'" + value + "'"; + }, this); + + if (metricExp) { + parameters.unshift(metricExp); + } + + return str + parameters.join(', ') + ')'; + } + }, { + key: '_hasMultipleParamsInString', + value: function _hasMultipleParamsInString(strValue, index) { + if (strValue.indexOf(',') === -1) { + return false; + } + + return this.def.params[index + 1] && this.def.params[index + 1].optional; + } + }, { + key: 'updateParam', + value: function updateParam(strValue, index) { + // handle optional parameters + // if string contains ',' and next param is optional, split and update both + if (this._hasMultipleParamsInString(strValue, index)) { + _lodash2.default.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(); + } + }, { + key: 'updateText', + value: function updateText() { + 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 FuncInstance; +}(); + +function createFuncInstance(funcDef, params) { + if (_lodash2.default.isString(funcDef)) { + if (!index[funcDef]) { + throw { message: 'Method not found ' + name }; + } + funcDef = index[funcDef]; + } + return new FuncInstance(funcDef, params); +} + +function getFuncDef(name) { + return index[name]; +} + +function getCategories() { + return categories; +} diff --git a/dist/test/datasource-zabbix/migrations.js b/dist/test/datasource-zabbix/migrations.js new file mode 100644 index 0000000..dc422c0 --- /dev/null +++ b/dist/test/datasource-zabbix/migrations.js @@ -0,0 +1,48 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.isGrafana2target = isGrafana2target; +exports.migrateFrom2To3version = migrateFrom2To3version; +exports.migrate = migrate; +/** + * Query format migration. + * This module can detect query format version and make migration. + */ + +function isGrafana2target(target) { + if (!target.mode || target.mode === 0 || target.mode === 2) { + if ((target.hostFilter || target.itemFilter || target.downsampleFunction || target.host && target.host.host) && target.item.filter === undefined && target.host.filter === undefined) { + return true; + } else { + return false; + } + } else { + return false; + } +} + +function migrateFrom2To3version(target) { + target.group.filter = target.group.name === "*" ? "/.*/" : target.group.name; + target.host.filter = target.host.name === "*" ? convertToRegex(target.hostFilter) : target.host.name; + target.application.filter = target.application.name === "*" ? "" : target.application.name; + target.item.filter = target.item.name === "All" ? convertToRegex(target.itemFilter) : target.item.name; + return target; +} + +function migrate(target) { + if (isGrafana2target(target)) { + return migrateFrom2To3version(target); + } else { + return target; + } +} + +function convertToRegex(str) { + if (str) { + return '/' + str + '/'; + } else { + return '/.*/'; + } +} diff --git a/dist/test/datasource-zabbix/module.js b/dist/test/datasource-zabbix/module.js new file mode 100644 index 0000000..6a44f8d --- /dev/null +++ b/dist/test/datasource-zabbix/module.js @@ -0,0 +1,32 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.AnnotationsQueryCtrl = exports.QueryOptionsCtrl = exports.QueryCtrl = exports.ConfigCtrl = exports.Datasource = undefined; + +var _datasource = require('./datasource'); + +var _query = require('./query.controller'); + +var _config = require('./config.controller'); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var ZabbixQueryOptionsController = function ZabbixQueryOptionsController() { + _classCallCheck(this, ZabbixQueryOptionsController); +}; + +ZabbixQueryOptionsController.templateUrl = 'datasource-zabbix/partials/query.options.html'; + +var ZabbixAnnotationsQueryController = function ZabbixAnnotationsQueryController() { + _classCallCheck(this, ZabbixAnnotationsQueryController); +}; + +ZabbixAnnotationsQueryController.templateUrl = 'datasource-zabbix/partials/annotations.editor.html'; + +exports.Datasource = _datasource.ZabbixAPIDatasource; +exports.ConfigCtrl = _config.ZabbixDSConfigController; +exports.QueryCtrl = _query.ZabbixQueryController; +exports.QueryOptionsCtrl = ZabbixQueryOptionsController; +exports.AnnotationsQueryCtrl = ZabbixAnnotationsQueryController; diff --git a/dist/test/datasource-zabbix/query.controller.js b/dist/test/datasource-zabbix/query.controller.js new file mode 100644 index 0000000..da9b36e --- /dev/null +++ b/dist/test/datasource-zabbix/query.controller.js @@ -0,0 +1,393 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.ZabbixQueryController = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _sdk = require('app/plugins/sdk'); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _constants = require('./constants'); + +var c = _interopRequireWildcard(_constants); + +var _utils = require('./utils'); + +var utils = _interopRequireWildcard(_utils); + +var _metricFunctions = require('./metricFunctions'); + +var metricFunctions = _interopRequireWildcard(_metricFunctions); + +var _migrations = require('./migrations'); + +var migrations = _interopRequireWildcard(_migrations); + +require('./add-metric-function.directive'); + +require('./metric-function-editor.directive'); + +require('./css/query-editor.css!'); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var ZabbixQueryController = exports.ZabbixQueryController = function (_QueryCtrl) { + _inherits(ZabbixQueryController, _QueryCtrl); + + // ZabbixQueryCtrl constructor + function ZabbixQueryController($scope, $injector, $rootScope, $sce, templateSrv) { + _classCallCheck(this, ZabbixQueryController); + + var _this = _possibleConstructorReturn(this, (ZabbixQueryController.__proto__ || Object.getPrototypeOf(ZabbixQueryController)).call(this, $scope, $injector)); + + _this.zabbix = _this.datasource.zabbix; + + // Use custom format for template variables + _this.replaceTemplateVars = _this.datasource.replaceTemplateVars; + _this.templateSrv = templateSrv; + + _this.editorModes = [{ value: 'num', text: 'Metrics', mode: c.MODE_METRICS }, { value: 'text', text: 'Text', mode: c.MODE_TEXT }, { value: 'itservice', text: 'IT Services', mode: c.MODE_ITSERVICE }, { value: 'itemid', text: 'Item ID', mode: c.MODE_ITEMID }, { value: 'triggers', text: 'Triggers', mode: c.MODE_TRIGGERS }]; + + _this.$scope.editorMode = { + METRICS: c.MODE_METRICS, + TEXT: c.MODE_TEXT, + ITSERVICE: c.MODE_ITSERVICE, + ITEMID: c.MODE_ITEMID, + TRIGGERS: c.MODE_TRIGGERS + }; + + _this.slaPropertyList = [{ name: "Status", property: "status" }, { name: "SLA", property: "sla" }, { name: "OK time", property: "okTime" }, { name: "Problem time", property: "problemTime" }, { name: "Down time", property: "downtimeTime" }]; + + _this.ackFilters = [{ text: 'all triggers', value: 2 }, { text: 'unacknowledged', value: 0 }, { text: 'acknowledged', value: 1 }]; + + _this.triggerSeverity = c.TRIGGER_SEVERITY; + + // Map functions for bs-typeahead + _this.getGroupNames = _lodash2.default.bind(_this.getMetricNames, _this, 'groupList'); + _this.getHostNames = _lodash2.default.bind(_this.getMetricNames, _this, 'hostList', true); + _this.getApplicationNames = _lodash2.default.bind(_this.getMetricNames, _this, 'appList'); + _this.getItemNames = _lodash2.default.bind(_this.getMetricNames, _this, 'itemList'); + _this.getITServices = _lodash2.default.bind(_this.getMetricNames, _this, 'itServiceList'); + _this.getVariables = _lodash2.default.bind(_this.getTemplateVariables, _this); + + // Update metric suggestion when template variable was changed + $rootScope.$on('template-variable-value-updated', function () { + return _this.onVariableChange(); + }); + + // Update metrics when item selected from dropdown + $scope.$on('typeahead-updated', function () { + _this.onTargetBlur(); + }); + + _this.init = function () { + var target = this.target; + + // Migrate old targets + target = migrations.migrate(target); + + var scopeDefaults = { + metric: {}, + oldTarget: _lodash2.default.cloneDeep(this.target), + queryOptionsText: this.renderQueryOptionsText() + }; + _lodash2.default.defaults(this, scopeDefaults); + + // Load default values + var targetDefaults = { + 'mode': c.MODE_METRICS, + 'group': { 'filter': "" }, + 'host': { 'filter': "" }, + 'application': { 'filter': "" }, + 'item': { 'filter': "" }, + 'functions': [], + 'triggers': { + 'count': true, + 'minSeverity': 3, + 'acknowledged': 2 + }, + 'options': { + 'showDisabledItems': false + } + }; + _lodash2.default.defaults(target, targetDefaults); + + // Create function instances from saved JSON + target.functions = _lodash2.default.map(target.functions, function (func) { + return metricFunctions.createFuncInstance(func.def, func.params); + }); + + if (target.mode === c.MODE_METRICS || target.mode === c.MODE_TEXT || target.mode === c.MODE_TRIGGERS) { + this.initFilters(); + } else if (target.mode === c.MODE_ITSERVICE) { + _lodash2.default.defaults(target, { slaProperty: { name: "SLA", property: "sla" } }); + this.suggestITServices(); + } + }; + + _this.init(); + _this.queryOptionsText = _this.renderQueryOptionsText(); + return _this; + } + + _createClass(ZabbixQueryController, [{ + key: 'initFilters', + value: function initFilters() { + var itemtype = _lodash2.default.find(this.editorModes, { 'mode': this.target.mode }); + itemtype = itemtype ? itemtype.value : null; + return Promise.all([this.suggestGroups(), this.suggestHosts(), this.suggestApps(), this.suggestItems(itemtype)]); + } + + // Get list of metric names for bs-typeahead directive + + }, { + key: 'getMetricNames', + value: function getMetricNames(metricList, addAllValue) { + var metrics = _lodash2.default.uniq(_lodash2.default.map(this.metric[metricList], 'name')); + + // Add template variables + _lodash2.default.forEach(this.templateSrv.variables, function (variable) { + metrics.unshift('$' + variable.name); + }); + + if (addAllValue) { + metrics.unshift('/.*/'); + } + + return metrics; + } + }, { + key: 'getTemplateVariables', + value: function getTemplateVariables() { + return _lodash2.default.map(this.templateSrv.variables, function (variable) { + return '$' + variable.name; + }); + } + }, { + key: 'suggestGroups', + value: function suggestGroups() { + var _this2 = this; + + return this.zabbix.getAllGroups().then(function (groups) { + _this2.metric.groupList = groups; + return groups; + }); + } + }, { + key: 'suggestHosts', + value: function suggestHosts() { + var _this3 = this; + + var groupFilter = this.replaceTemplateVars(this.target.group.filter); + return this.zabbix.getAllHosts(groupFilter).then(function (hosts) { + _this3.metric.hostList = hosts; + return hosts; + }); + } + }, { + key: 'suggestApps', + value: function suggestApps() { + var _this4 = this; + + var groupFilter = this.replaceTemplateVars(this.target.group.filter); + var hostFilter = this.replaceTemplateVars(this.target.host.filter); + return this.zabbix.getAllApps(groupFilter, hostFilter).then(function (apps) { + _this4.metric.appList = apps; + return apps; + }); + } + }, { + key: 'suggestItems', + value: function suggestItems() { + var _this5 = this; + + var itemtype = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'num'; + + var groupFilter = this.replaceTemplateVars(this.target.group.filter); + var hostFilter = this.replaceTemplateVars(this.target.host.filter); + var appFilter = this.replaceTemplateVars(this.target.application.filter); + var options = { + itemtype: itemtype, + showDisabledItems: this.target.options.showDisabledItems + }; + + return this.zabbix.getAllItems(groupFilter, hostFilter, appFilter, options).then(function (items) { + _this5.metric.itemList = items; + return items; + }); + } + }, { + key: 'suggestITServices', + value: function suggestITServices() { + var _this6 = this; + + return this.zabbix.getITService().then(function (itservices) { + _this6.metric.itServiceList = itservices; + return itservices; + }); + } + }, { + key: 'isRegex', + value: function isRegex(str) { + return utils.isRegex(str); + } + }, { + key: 'isVariable', + value: function isVariable(str) { + return utils.isTemplateVariable(str, this.templateSrv.variables); + } + }, { + key: 'onTargetBlur', + value: function onTargetBlur() { + var newTarget = _lodash2.default.cloneDeep(this.target); + if (!_lodash2.default.isEqual(this.oldTarget, this.target)) { + this.oldTarget = newTarget; + this.targetChanged(); + } + } + }, { + key: 'onVariableChange', + value: function onVariableChange() { + if (this.isContainsVariables()) { + this.targetChanged(); + } + } + + /** + * Check query for template variables + */ + + }, { + key: 'isContainsVariables', + value: function isContainsVariables() { + var _this7 = this; + + return _lodash2.default.some(['group', 'host', 'application'], function (field) { + if (_this7.target[field] && _this7.target[field].filter) { + return utils.isTemplateVariable(_this7.target[field].filter, _this7.templateSrv.variables); + } else { + return false; + } + }); + } + }, { + key: 'parseTarget', + value: function parseTarget() {} + // Parse target + + + // Validate target and set validation info + + }, { + key: 'validateTarget', + value: function validateTarget() { + // validate + } + }, { + key: 'targetChanged', + value: function targetChanged() { + this.initFilters(); + this.parseTarget(); + this.panelCtrl.refresh(); + } + }, { + key: 'addFunction', + value: function addFunction(funcDef) { + var newFunc = metricFunctions.createFuncInstance(funcDef); + newFunc.added = true; + this.target.functions.push(newFunc); + + this.moveAliasFuncLast(); + + if (newFunc.params.length && newFunc.added || newFunc.def.params.length === 0) { + this.targetChanged(); + } + } + }, { + key: 'removeFunction', + value: function removeFunction(func) { + this.target.functions = _lodash2.default.without(this.target.functions, func); + this.targetChanged(); + } + }, { + key: 'moveAliasFuncLast', + value: function moveAliasFuncLast() { + var aliasFunc = _lodash2.default.find(this.target.functions, function (func) { + return func.def.name === 'alias' || func.def.name === 'aliasByNode' || func.def.name === 'aliasByMetric'; + }); + + if (aliasFunc) { + this.target.functions = _lodash2.default.without(this.target.functions, aliasFunc); + this.target.functions.push(aliasFunc); + } + } + }, { + key: 'toggleQueryOptions', + value: function toggleQueryOptions() { + this.showQueryOptions = !this.showQueryOptions; + } + }, { + key: 'onQueryOptionChange', + value: function onQueryOptionChange() { + this.queryOptionsText = this.renderQueryOptionsText(); + this.onTargetBlur(); + } + }, { + key: 'renderQueryOptionsText', + value: function renderQueryOptionsText() { + var optionsMap = { + showDisabledItems: "Show disabled items" + }; + var options = []; + _lodash2.default.forOwn(this.target.options, function (value, key) { + if (value) { + if (value === true) { + // Show only option name (if enabled) for boolean options + options.push(optionsMap[key]); + } else { + // Show "option = value" for another options + options.push(optionsMap[key] + " = " + value); + } + } + }); + return "Options: " + options.join(', '); + } + + /** + * Switch query editor to specified mode. + * Modes: + * 0 - items + * 1 - IT services + * 2 - Text metrics + */ + + }, { + key: 'switchEditorMode', + value: function switchEditorMode(mode) { + this.target.mode = mode; + this.init(); + this.targetChanged(); + } + }]); + + return ZabbixQueryController; +}(_sdk.QueryCtrl); + +// Set templateUrl as static property + + +ZabbixQueryController.templateUrl = 'datasource-zabbix/partials/query.editor.html'; diff --git a/dist/test/datasource-zabbix/responseHandler.js b/dist/test/datasource-zabbix/responseHandler.js new file mode 100644 index 0000000..beded2b --- /dev/null +++ b/dist/test/datasource-zabbix/responseHandler.js @@ -0,0 +1,210 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _table_model = require('app/core/table_model'); + +var _table_model2 = _interopRequireDefault(_table_model); + +var _constants = require('./constants'); + +var c = _interopRequireWildcard(_constants); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } + +/** + * Convert Zabbix API history.get response to Grafana format + * + * @return {Array} Array of timeseries in Grafana format + * { + * target: "Metric name", + * datapoints: [[, ], ...] + * } + */ +function convertHistory(history, items, addHostName, convertPointCallback) { + /** + * Response should be in the format: + * data: [ + * { + * target: "Metric name", + * datapoints: [[, ], ...] + * }, ... + * ] + */ + + // Group history by itemid + var grouped_history = _lodash2.default.groupBy(history, 'itemid'); + var hosts = _lodash2.default.uniqBy(_lodash2.default.flatten(_lodash2.default.map(items, 'hosts')), 'hostid'); //uniqBy is needed to deduplicate + + return _lodash2.default.map(grouped_history, function (hist, itemid) { + var item = _lodash2.default.find(items, { 'itemid': itemid }); + var alias = item.name; + if (_lodash2.default.keys(hosts).length > 1 && addHostName) { + //only when actual multi hosts selected + var host = _lodash2.default.find(hosts, { 'hostid': item.hostid }); + alias = host.name + ": " + alias; + } + return { + target: alias, + datapoints: _lodash2.default.map(hist, convertPointCallback) + }; + }); +} + +function handleHistory(history, items) { + var addHostName = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; + + return convertHistory(history, items, addHostName, convertHistoryPoint); +} + +function handleTrends(history, items, valueType) { + var addHostName = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true; + + var convertPointCallback = _lodash2.default.partial(convertTrendPoint, valueType); + return convertHistory(history, items, addHostName, convertPointCallback); +} + +function handleText(history, items, target) { + var addHostName = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true; + + var convertTextCallback = _lodash2.default.partial(convertText, target); + return convertHistory(history, items, addHostName, convertTextCallback); +} + +function convertText(target, point) { + var value = point.value; + + // Regex-based extractor + if (target.textFilter) { + value = extractText(point.value, target.textFilter, target.useCaptureGroups); + } + + return [value, point.clock * 1000 + Math.round(point.ns / 1000000)]; +} + +function extractText(str, pattern, useCaptureGroups) { + var extractPattern = new RegExp(pattern); + var extractedValue = extractPattern.exec(str); + if (extractedValue) { + if (useCaptureGroups) { + extractedValue = extractedValue[1]; + } else { + extractedValue = extractedValue[0]; + } + } + return extractedValue; +} + +function handleSLAResponse(itservice, slaProperty, slaObject) { + var targetSLA = slaObject[itservice.serviceid].sla[0]; + if (slaProperty.property === 'status') { + var targetStatus = parseInt(slaObject[itservice.serviceid].status); + return { + target: itservice.name + ' ' + slaProperty.name, + datapoints: [[targetStatus, targetSLA.to * 1000]] + }; + } else { + return { + target: itservice.name + ' ' + slaProperty.name, + datapoints: [[targetSLA[slaProperty.property], targetSLA.from * 1000], [targetSLA[slaProperty.property], targetSLA.to * 1000]] + }; + } +} + +function handleTriggersResponse(triggers, timeRange) { + if (_lodash2.default.isNumber(triggers)) { + return { + target: "triggers count", + datapoints: [[triggers, timeRange[1] * 1000]] + }; + } else { + var stats = getTriggerStats(triggers); + var table = new _table_model2.default(); + table.addColumn({ text: 'Host group' }); + _lodash2.default.each(_lodash2.default.orderBy(c.TRIGGER_SEVERITY, ['val'], ['desc']), function (severity) { + table.addColumn({ text: severity.text }); + }); + _lodash2.default.each(stats, function (severity_stats, group) { + var row = _lodash2.default.map(_lodash2.default.orderBy(_lodash2.default.toPairs(severity_stats), function (s) { + return s[0]; + }, ['desc']), function (s) { + return s[1]; + }); + row = _lodash2.default.concat.apply(_lodash2.default, [[group]].concat(_toConsumableArray(row))); + table.rows.push(row); + }); + return table; + } +} + +function getTriggerStats(triggers) { + var groups = _lodash2.default.uniq(_lodash2.default.flattenDeep(_lodash2.default.map(triggers, function (trigger) { + return _lodash2.default.map(trigger.groups, 'name'); + }))); + // let severity = _.map(c.TRIGGER_SEVERITY, 'text'); + var stats = {}; + _lodash2.default.each(groups, function (group) { + stats[group] = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }; // severity:count + }); + _lodash2.default.each(triggers, function (trigger) { + _lodash2.default.each(trigger.groups, function (group) { + stats[group.name][trigger.priority]++; + }); + }); + return stats; +} + +function convertHistoryPoint(point) { + // Value must be a number for properly work + return [Number(point.value), point.clock * 1000 + Math.round(point.ns / 1000000)]; +} + +function convertTrendPoint(valueType, point) { + var value; + switch (valueType) { + case "min": + value = point.value_min; + break; + case "max": + value = point.value_max; + break; + case "avg": + value = point.value_avg; + break; + case "sum": + value = point.value_sum; + break; + case "count": + value = point.value_count; + break; + default: + value = point.value_avg; + } + + return [Number(value), point.clock * 1000]; +} + +exports.default = { + handleHistory: handleHistory, + convertHistory: convertHistory, + handleTrends: handleTrends, + handleText: handleText, + handleSLAResponse: handleSLAResponse, + handleTriggersResponse: handleTriggersResponse +}; + +// Fix for backward compatibility with lodash 2.4 + +if (!_lodash2.default.uniqBy) { + _lodash2.default.uniqBy = _lodash2.default.uniq; +} diff --git a/dist/test/datasource-zabbix/specs/datasource.spec.js b/dist/test/datasource-zabbix/specs/datasource.spec.js new file mode 100644 index 0000000..f4101e6 --- /dev/null +++ b/dist/test/datasource-zabbix/specs/datasource.spec.js @@ -0,0 +1,424 @@ +"use strict"; + +var _lodash = require("lodash"); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _q = require("q"); + +var _q2 = _interopRequireDefault(_q); + +var _module = require("../module"); + +var _datasource = require("../datasource"); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +describe('ZabbixDatasource', function () { + var ctx = {}; + + beforeEach(function () { + ctx.instanceSettings = { + jsonData: { + alerting: true, + username: 'zabbix', + password: 'zabbix', + trends: true, + trendsFrom: '14d', + trendsRange: '7d', + dbConnection: { + enabled: false + } + } + }; + ctx.templateSrv = {}; + ctx.alertSrv = {}; + ctx.dashboardSrv = {}; + ctx.zabbixAlertingSrv = {}; + ctx.zabbix = function () {}; + + ctx.ds = new _module.Datasource(ctx.instanceSettings, ctx.templateSrv, ctx.alertSrv, ctx.dashboardSrv, ctx.zabbixAlertingSrv, ctx.zabbix); + }); + + describe('When querying data', function () { + beforeEach(function () { + ctx.ds.replaceTemplateVars = function (str) { + return str; + }; + ctx.ds.alertQuery = function () { + return _q2.default.when([]); + }; + }); + + ctx.options = { + targets: [{ + group: { filter: "" }, + host: { filter: "" }, + application: { filter: "" }, + item: { filter: "" } + }], + range: { from: 'now-7d', to: 'now' } + }; + + it('should return an empty array when no targets are set', function (done) { + var options = { + targets: [], + range: { from: 'now-6h', to: 'now' } + }; + ctx.ds.query(options).then(function (result) { + expect(result.data.length).toBe(0); + done(); + }); + }); + + it('should use trends if it enabled and time more than trendsFrom', function (done) { + var ranges = ['now-7d', 'now-168h', 'now-1M', 'now-1y']; + + _lodash2.default.forEach(ranges, function (range) { + ctx.options.range.from = range; + ctx.ds.queryNumericData = jest.fn(); + ctx.ds.query(ctx.options); + + // Check that useTrends options is true + var callArgs = ctx.ds.queryNumericData.mock.calls[0]; + expect(callArgs[2]).toBe(true); + ctx.ds.queryNumericData.mockClear(); + }); + + done(); + }); + + it('shouldnt use trends if it enabled and time less than trendsFrom', function (done) { + var ranges = ['now-6d', 'now-167h', 'now-1h', 'now-30m', 'now-30s']; + + _lodash2.default.forEach(ranges, function (range) { + ctx.options.range.from = range; + ctx.ds.queryNumericData = jest.fn(); + ctx.ds.query(ctx.options); + + // Check that useTrends options is false + var callArgs = ctx.ds.queryNumericData.mock.calls[0]; + expect(callArgs[2]).toBe(false); + ctx.ds.queryNumericData.mockClear(); + }); + done(); + }); + }); + + describe('When replacing template variables', function () { + + function testReplacingVariable(target, varValue, expectedResult, done) { + ctx.ds.templateSrv.replace = function () { + return (0, _datasource.zabbixTemplateFormat)(varValue); + }; + + var result = ctx.ds.replaceTemplateVars(target); + expect(result).toBe(expectedResult); + done(); + } + + /* + * Alphanumerics, spaces, dots, dashes and underscores + * are allowed in Zabbix host name. + * 'AaBbCc0123 .-_' + */ + it('should return properly escaped regex', function (done) { + var target = '$host'; + var template_var_value = 'AaBbCc0123 .-_'; + var expected_result = '/^AaBbCc0123 \\.-_$/'; + + testReplacingVariable(target, template_var_value, expected_result, done); + }); + + /* + * Single-value variable + * $host = backend01 + * $host => /^backend01|backend01$/ + */ + it('should return proper regex for single value', function (done) { + var target = '$host'; + var template_var_value = 'backend01'; + var expected_result = '/^backend01$/'; + + testReplacingVariable(target, template_var_value, expected_result, done); + }); + + /* + * Multi-value variable + * $host = [backend01, backend02] + * $host => /^(backend01|backend01)$/ + */ + it('should return proper regex for multi-value', function (done) { + var target = '$host'; + var template_var_value = ['backend01', 'backend02']; + var expected_result = '/^(backend01|backend02)$/'; + + testReplacingVariable(target, template_var_value, expected_result, done); + }); + }); + + describe('When invoking metricFindQuery()', function () { + beforeEach(function () { + ctx.ds.replaceTemplateVars = function (str) { + return str; + }; + ctx.ds.zabbix = { + getGroups: jest.fn().mockReturnValue(_q2.default.when([])), + getHosts: jest.fn().mockReturnValue(_q2.default.when([])), + getApps: jest.fn().mockReturnValue(_q2.default.when([])), + getItems: jest.fn().mockReturnValue(_q2.default.when([])) + }; + }); + + it('should return groups', function (done) { + var tests = [{ query: '*', expect: '/.*/' }, { query: '', expect: '' }, { query: 'Backend', expect: 'Backend' }, { query: 'Back*', expect: 'Back*' }]; + + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = tests[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var test = _step.value; + + ctx.ds.metricFindQuery(test.query); + expect(ctx.ds.zabbix.getGroups).toBeCalledWith(test.expect); + ctx.ds.zabbix.getGroups.mockClear(); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + done(); + }); + + it('should return hosts', function (done) { + var tests = [{ query: '*.*', expect: ['/.*/', '/.*/'] }, { query: '.', expect: ['', ''] }, { query: 'Backend.*', expect: ['Backend', '/.*/'] }, { query: 'Back*.', expect: ['Back*', ''] }]; + + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = tests[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var test = _step2.value; + + ctx.ds.metricFindQuery(test.query); + expect(ctx.ds.zabbix.getHosts).toBeCalledWith(test.expect[0], test.expect[1]); + ctx.ds.zabbix.getHosts.mockClear(); + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + + done(); + }); + + it('should return applications', function (done) { + var tests = [{ query: '*.*.*', expect: ['/.*/', '/.*/', '/.*/'] }, { query: '.*.', expect: ['', '/.*/', ''] }, { query: 'Backend.backend01.*', expect: ['Backend', 'backend01', '/.*/'] }, { query: 'Back*.*.', expect: ['Back*', '/.*/', ''] }]; + + var _iteratorNormalCompletion3 = true; + var _didIteratorError3 = false; + var _iteratorError3 = undefined; + + try { + for (var _iterator3 = tests[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { + var test = _step3.value; + + ctx.ds.metricFindQuery(test.query); + expect(ctx.ds.zabbix.getApps).toBeCalledWith(test.expect[0], test.expect[1], test.expect[2]); + ctx.ds.zabbix.getApps.mockClear(); + } + } catch (err) { + _didIteratorError3 = true; + _iteratorError3 = err; + } finally { + try { + if (!_iteratorNormalCompletion3 && _iterator3.return) { + _iterator3.return(); + } + } finally { + if (_didIteratorError3) { + throw _iteratorError3; + } + } + } + + done(); + }); + + it('should return items', function (done) { + var tests = [{ query: '*.*.*.*', expect: ['/.*/', '/.*/', '', '/.*/'] }, { query: '.*.*.*', expect: ['', '/.*/', '', '/.*/'] }, { query: 'Backend.backend01.*.*', expect: ['Backend', 'backend01', '', '/.*/'] }, { query: 'Back*.*.cpu.*', expect: ['Back*', '/.*/', 'cpu', '/.*/'] }]; + + var _iteratorNormalCompletion4 = true; + var _didIteratorError4 = false; + var _iteratorError4 = undefined; + + try { + for (var _iterator4 = tests[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { + var test = _step4.value; + + ctx.ds.metricFindQuery(test.query); + expect(ctx.ds.zabbix.getItems).toBeCalledWith(test.expect[0], test.expect[1], test.expect[2], test.expect[3]); + ctx.ds.zabbix.getItems.mockClear(); + } + } catch (err) { + _didIteratorError4 = true; + _iteratorError4 = err; + } finally { + try { + if (!_iteratorNormalCompletion4 && _iterator4.return) { + _iterator4.return(); + } + } finally { + if (_didIteratorError4) { + throw _iteratorError4; + } + } + } + + done(); + }); + + it('should invoke method with proper arguments', function (done) { + var query = '*.*'; + + ctx.ds.metricFindQuery(query); + expect(ctx.ds.zabbix.getHosts).toBeCalledWith('/.*/', '/.*/'); + done(); + }); + }); + + describe('When querying alerts', function () { + var options = {}; + + beforeEach(function () { + ctx.ds.replaceTemplateVars = function (str) { + return str; + }; + + var targetItems = [{ + "itemid": "1", + "name": "test item", + "key_": "test.key", + "value_type": "3", + "hostid": "10631", + "status": "0", + "state": "0", + "hosts": [{ "hostid": "10631", "name": "Test host" }], + "item": "Test item" + }]; + ctx.ds.zabbix.getItemsFromTarget = function () { + return Promise.resolve(targetItems); + }; + + options = { + "panelId": 10, + "targets": [{ + "application": { "filter": "" }, + "group": { "filter": "Test group" }, + "host": { "filter": "Test host" }, + "item": { "filter": "Test item" } + }] + }; + }); + + it('should return threshold when comparative symbol is `less than`', function () { + + var itemTriggers = [{ + "triggerid": "15383", + "priority": "4", + "expression": "{15915}<100" + }]; + + ctx.ds.zabbix.getAlerts = function () { + return Promise.resolve(itemTriggers); + }; + + return ctx.ds.alertQuery(options).then(function (resp) { + expect(resp.thresholds).toHaveLength(1); + expect(resp.thresholds[0]).toBe(100); + return resp; + }); + }); + + it('should return threshold when comparative symbol is `less than or equal`', function () { + + var itemTriggers = [{ + "triggerid": "15383", + "priority": "4", + "expression": "{15915}<=100" + }]; + + ctx.ds.zabbix.getAlerts = function () { + return Promise.resolve(itemTriggers); + }; + + return ctx.ds.alertQuery(options).then(function (resp) { + expect(resp.thresholds.length).toBe(1); + expect(resp.thresholds[0]).toBe(100); + return resp; + }); + }); + + it('should return threshold when comparative symbol is `greater than or equal`', function () { + + var itemTriggers = [{ + "triggerid": "15383", + "priority": "4", + "expression": "{15915}>=30" + }]; + + ctx.ds.zabbix.getAlerts = function () { + return Promise.resolve(itemTriggers); + }; + + return ctx.ds.alertQuery(options).then(function (resp) { + expect(resp.thresholds.length).toBe(1); + expect(resp.thresholds[0]).toBe(30); + return resp; + }); + }); + + it('should return threshold when comparative symbol is `equal`', function () { + + var itemTriggers = [{ + "triggerid": "15383", + "priority": "4", + "expression": "{15915}=50" + }]; + + ctx.ds.zabbix.getAlerts = function () { + return Promise.resolve(itemTriggers); + }; + + return ctx.ds.alertQuery(options).then(function (resp) { + expect(resp.thresholds.length).toBe(1); + expect(resp.thresholds[0]).toBe(50); + return resp; + }); + }); + }); +}); diff --git a/dist/test/datasource-zabbix/specs/timeseries.spec.js b/dist/test/datasource-zabbix/specs/timeseries.spec.js new file mode 100644 index 0000000..f5db4a6 --- /dev/null +++ b/dist/test/datasource-zabbix/specs/timeseries.spec.js @@ -0,0 +1,33 @@ +'use strict'; + +var _timeseries = require('../timeseries'); + +var _timeseries2 = _interopRequireDefault(_timeseries); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +describe('timeseries processing functions', function () { + + describe('sumSeries()', function () { + it('should properly sum series', function (done) { + var series = [[[0, 1], [1, 2], [1, 3]], [[2, 1], [3, 2], [4, 3]]]; + + var expected = [[2, 1], [4, 2], [5, 3]]; + + var result = _timeseries2.default.sumSeries(series); + expect(result).toEqual(expected); + done(); + }); + + it('should properly sum series with nulls', function (done) { + // issue #286 + var series = [[[1, 1], [1, 2], [1, 3]], [[3, 2], [4, 3]]]; + + var expected = [[1, 1], [4, 2], [5, 3]]; + + var result = _timeseries2.default.sumSeries(series); + expect(result).toEqual(expected); + done(); + }); + }); +}); // import _ from 'lodash'; diff --git a/dist/test/datasource-zabbix/specs/utils.spec.js b/dist/test/datasource-zabbix/specs/utils.spec.js new file mode 100644 index 0000000..42f2c67 --- /dev/null +++ b/dist/test/datasource-zabbix/specs/utils.spec.js @@ -0,0 +1,131 @@ +'use strict'; + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _utils = require('../utils'); + +var utils = _interopRequireWildcard(_utils); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +describe('Utils', function () { + + describe('expandItemName()', function () { + + it('should properly expand unquoted params', function (done) { + var test_cases = [{ + name: 'CPU $2 time', + key: 'system.cpu.util[,user,avg1]', + expected: "CPU user time" + }, { + name: 'CPU $2 time - $3', + key: 'system.cpu.util[,system,avg1]', + expected: "CPU system time - avg1" + }, { + name: 'CPU - $1 - $2 - $3', + key: 'system.cpu.util[,system,avg1]', + expected: "CPU - - system - avg1" + }]; + + _lodash2.default.each(test_cases, function (test_case) { + var expandedName = utils.expandItemName(test_case.name, test_case.key); + expect(expandedName).toBe(test_case.expected); + }); + done(); + }); + + it('should properly expand quoted params with commas', function (done) { + var test_cases = [{ + name: 'CPU $2 time', + key: 'system.cpu.util["type=user,value=avg",user]', + expected: "CPU user time" + }, { + name: 'CPU $1 time', + key: 'system.cpu.util["type=user,value=avg","user"]', + expected: "CPU type=user,value=avg time" + }, { + name: 'CPU $1 time $3', + key: 'system.cpu.util["type=user,value=avg",,"user"]', + expected: "CPU type=user,value=avg time user" + }, { + name: 'CPU $1 $2 $3', + key: 'system.cpu.util["type=user,value=avg",time,"user"]', + expected: "CPU type=user,value=avg time user" + }]; + + _lodash2.default.each(test_cases, function (test_case) { + var expandedName = utils.expandItemName(test_case.name, test_case.key); + expect(expandedName).toBe(test_case.expected); + }); + done(); + }); + + it('should properly expand array params', function (done) { + var test_cases = [{ + name: 'CPU $2 - $3 time', + key: 'system.cpu.util[,[user,system],avg1]', + expected: "CPU user,system - avg1 time" + }, { + name: 'CPU $2 - $3 time', + key: 'system.cpu.util[,["user,system",iowait],avg1]', + expected: 'CPU "user,system",iowait - avg1 time' + }, { + name: 'CPU - $2 - $3 - $4', + key: 'system.cpu.util[,[],["user,system",iowait],avg1]', + expected: 'CPU - - "user,system",iowait - avg1' + }]; + + _lodash2.default.each(test_cases, function (test_case) { + var expandedName = utils.expandItemName(test_case.name, test_case.key); + expect(expandedName).toBe(test_case.expected); + }); + done(); + }); + }); + + describe('splitTemplateQuery()', function () { + + // Backward compatibility + it('should properly split query in old format', function (done) { + var test_cases = [{ + query: '/alu/./tw-(nyc|que|brx|dwt|brk)-sta_(w|d)*-alu-[0-9{2}/', + expected: ['/alu/', '/tw-(nyc|que|brx|dwt|brk)-sta_(\w|\d)*-alu-[0-9{2}/'] + }, { + query: 'a.b.c.d', + expected: ['a', 'b', 'c', 'd'] + }]; + + _lodash2.default.each(test_cases, function (test_case) { + var splitQuery = utils.splitTemplateQuery(test_case.query); + expect(splitQuery).toEqual(test_case.expected); + }); + done(); + }); + + it('should properly split query', function (done) { + var test_cases = [{ + query: '{alu}{/tw-(nyc|que|brx|dwt|brk)-sta_(w|d)*-alu-[0-9]*/}', + expected: ['alu', '/tw-(nyc|que|brx|dwt|brk)-sta_(\w|\d)*-alu-[0-9]*/'] + }, { + query: '{alu}{/tw-(nyc|que|brx|dwt|brk)-sta_(w|d)*-alu-[0-9]{2}/}', + expected: ['alu', '/tw-(nyc|que|brx|dwt|brk)-sta_(\w|\d)*-alu-[0-9]{2}/'] + }, { + query: '{a}{b}{c}{d}', + expected: ['a', 'b', 'c', 'd'] + }, { + query: '{a}{b.c.d}', + expected: ['a', 'b.c.d'] + }]; + + _lodash2.default.each(test_cases, function (test_case) { + var splitQuery = utils.splitTemplateQuery(test_case.query); + expect(splitQuery).toEqual(test_case.expected); + }); + done(); + }); + }); +}); diff --git a/dist/test/datasource-zabbix/timeseries.js b/dist/test/datasource-zabbix/timeseries.js new file mode 100644 index 0000000..bc0ed69 --- /dev/null +++ b/dist/test/datasource-zabbix/timeseries.js @@ -0,0 +1,502 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _utils = require('./utils'); + +var utils = _interopRequireWildcard(_utils); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * timeseries.js + * + * This module contains functions for working with time series. + * + * datapoints - array of points where point is [value, timestamp]. In almost all cases (if other wasn't + * explicitly said) we assume datapoints are sorted by timestamp. Timestamp is the number of milliseconds + * since 1 January 1970 00:00:00 UTC. + * + */ + +var POINT_VALUE = 0; +var POINT_TIMESTAMP = 1; + +/** + * Downsample time series by using given function (avg, min, max). + */ +function downsample(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([_lodash2.default.max(frame), timeWindow.to]); + } else if (func === "min") { + downsampledSeries.push([_lodash2.default.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: [[, ], ...] + */ +function groupBy(datapoints, interval, groupByCallback) { + var ms_interval = utils.parseInterval(interval); + + // Calculate frame timestamps + var frames = _lodash2.default.groupBy(datapoints, function (point) { + // Calculate time for group of points + return Math.floor(point[1] / ms_interval) * ms_interval; + }); + + // frame: { '': [[, ], ...] } + // return [{ '': }, { '': }, ...] + var grouped = _lodash2.default.mapValues(frames, function (frame) { + var points = _lodash2.default.map(frame, function (point) { + return point[0]; + }); + return groupByCallback(points); + }); + + // Convert points to Grafana format + return sortByTime(_lodash2.default.map(grouped, function (value, timestamp) { + return [Number(value), Number(timestamp)]; + })); +} + +function groupBy_perf(datapoints, interval, groupByCallback) { + if (datapoints.length === 0) { + return []; + } + + var ms_interval = utils.parseInterval(interval); + var grouped_series = []; + var frame_values = []; + var frame_value = void 0; + var frame_ts = datapoints.length ? getPointTimeFrame(datapoints[0][POINT_TIMESTAMP], ms_interval) : 0; + var point_frame_ts = frame_ts; + var point = void 0; + + for (var i = 0; i < datapoints.length; i++) { + point = datapoints[i]; + point_frame_ts = getPointTimeFrame(point[POINT_TIMESTAMP], ms_interval); + if (point_frame_ts === frame_ts) { + frame_values.push(point[POINT_VALUE]); + } else if (point_frame_ts > frame_ts) { + frame_value = groupByCallback(frame_values); + grouped_series.push([frame_value, frame_ts]); + + // Move frame window to next non-empty interval and fill empty by null + frame_ts += ms_interval; + while (frame_ts < point_frame_ts) { + grouped_series.push([null, frame_ts]); + frame_ts += ms_interval; + } + frame_values = [point[POINT_VALUE]]; + } + } + + frame_value = groupByCallback(frame_values); + grouped_series.push([frame_value, frame_ts]); + + return grouped_series; +} + +/** + * Summarize set of time series into one. + * @param {datapoints[]} timeseries array of time series + */ +function sumSeries(timeseries) { + + // Calculate new points for interpolation + var new_timestamps = _lodash2.default.uniq(_lodash2.default.map(_lodash2.default.flatten(timeseries, true), function (point) { + return point[1]; + })); + new_timestamps = _lodash2.default.sortBy(new_timestamps); + + var interpolated_timeseries = _lodash2.default.map(timeseries, function (series) { + series = fillZeroes(series, new_timestamps); + var timestamps = _lodash2.default.map(series, function (point) { + return point[1]; + }); + var new_points = _lodash2.default.map(_lodash2.default.difference(new_timestamps, timestamps), function (timestamp) { + return [null, timestamp]; + }); + var new_series = series.concat(new_points); + return sortByTime(new_series); + }); + + _lodash2.default.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 scale(datapoints, factor) { + return _lodash2.default.map(datapoints, function (point) { + return [point[0] * factor, point[1]]; + }); +} + +function scale_perf(datapoints, factor) { + for (var i = 0; i < datapoints.length; i++) { + datapoints[i] = [datapoints[i][POINT_VALUE] * factor, datapoints[i][POINT_TIMESTAMP]]; + } + + return datapoints; +} + +/** + * Simple delta. Calculate value delta between points. + * @param {*} datapoints + */ +function delta(datapoints) { + var newSeries = []; + var deltaValue = void 0; + for (var i = 1; i < datapoints.length; i++) { + deltaValue = datapoints[i][0] - datapoints[i - 1][0]; + newSeries.push([deltaValue, datapoints[i][1]]); + } + return newSeries; +} + +/** + * Calculates rate per second. Resistant to counter reset. + * @param {*} datapoints + */ +function rate(datapoints) { + var newSeries = []; + var point = void 0, + point_prev = void 0; + var valueDelta = 0; + var timeDelta = 0; + for (var i = 1; i < datapoints.length; i++) { + point = datapoints[i]; + point_prev = datapoints[i - 1]; + + // Convert ms to seconds + timeDelta = (point[POINT_TIMESTAMP] - point_prev[POINT_TIMESTAMP]) / 1000; + + // Handle counter reset - use previous value + if (point[POINT_VALUE] >= point_prev[POINT_VALUE]) { + valueDelta = (point[POINT_VALUE] - point_prev[POINT_VALUE]) / timeDelta; + } + + newSeries.push([valueDelta, point[POINT_TIMESTAMP]]); + } + return newSeries; +} + +function simpleMovingAverage(datapoints, n) { + var sma = []; + var w_sum = void 0; + var w_avg = null; + var w_count = 0; + + // Initial window + for (var j = n; j > 0; j--) { + if (datapoints[n - j][POINT_VALUE] !== null) { + w_avg += datapoints[n - j][POINT_VALUE]; + w_count++; + } + } + if (w_count > 0) { + w_avg = w_avg / w_count; + } else { + w_avg = null; + } + sma.push([w_avg, datapoints[n - 1][POINT_TIMESTAMP]]); + + for (var i = n; i < datapoints.length; i++) { + // Insert next value + if (datapoints[i][POINT_VALUE] !== null) { + w_sum = w_avg * w_count; + w_avg = (w_sum + datapoints[i][POINT_VALUE]) / (w_count + 1); + w_count++; + } + // Remove left side point + if (datapoints[i - n][POINT_VALUE] !== null) { + w_sum = w_avg * w_count; + if (w_count > 1) { + w_avg = (w_sum - datapoints[i - n][POINT_VALUE]) / (w_count - 1); + w_count--; + } else { + w_avg = null; + w_count = 0; + } + } + sma.push([w_avg, datapoints[i][POINT_TIMESTAMP]]); + } + return sma; +} + +function expMovingAverage(datapoints, n) { + var ema = [datapoints[0]]; + var ema_prev = datapoints[0][POINT_VALUE]; + var ema_cur = void 0; + var a = void 0; + + if (n > 1) { + // Calculate a from window size + a = 2 / (n + 1); + + // Initial window, use simple moving average + var w_avg = null; + var w_count = 0; + for (var j = n; j > 0; j--) { + if (datapoints[n - j][POINT_VALUE] !== null) { + w_avg += datapoints[n - j][POINT_VALUE]; + w_count++; + } + } + if (w_count > 0) { + w_avg = w_avg / w_count; + // Actually, we should set timestamp from datapoints[n-1] and start calculation of EMA from n. + // But in order to start EMA from first point (not from Nth) we should expand time range and request N additional + // points outside left side of range. We can't do that, so this trick is used for pretty view of first N points. + // We calculate AVG for first N points, but then start from 2nd point, not from Nth. In general, it means we + // assume that previous N values (0-N, 0-(N-1), ..., 0-1) have the same average value as a first N values. + ema = [[w_avg, datapoints[0][POINT_TIMESTAMP]]]; + ema_prev = w_avg; + n = 1; + } + } else { + // Use predefined a and start from 1st point (use it as initial EMA value) + a = n; + n = 1; + } + + for (var i = n; i < datapoints.length; i++) { + if (datapoints[i][POINT_VALUE] !== null) { + ema_cur = a * datapoints[i][POINT_VALUE] + (1 - a) * ema_prev; + ema_prev = ema_cur; + ema.push([ema_cur, datapoints[i][POINT_TIMESTAMP]]); + } else { + ema.push([null, datapoints[i][POINT_TIMESTAMP]]); + } + } + return ema; +} + +function PERCENTIL(n, values) { + var sorted = _lodash2.default.sortBy(values); + return sorted[Math.floor(sorted.length * n / 100)]; +} + +function COUNT(values) { + return values.length; +} + +function SUM(values) { + var sum = null; + for (var i = 0; i < values.length; i++) { + if (values[i] !== null) { + sum += values[i]; + } + } + return sum; +} + +function AVERAGE(values) { + var values_non_null = getNonNullValues(values); + if (values_non_null.length === 0) { + return null; + } + return SUM(values_non_null) / values_non_null.length; +} + +function getNonNullValues(values) { + var values_non_null = []; + for (var i = 0; i < values.length; i++) { + if (values[i] !== null) { + values_non_null.push(values[i]); + } + } + return values_non_null; +} + +function MIN(values) { + return _lodash2.default.min(values); +} + +function MAX(values) { + return _lodash2.default.max(values); +} + +function MEDIAN(values) { + var sorted = _lodash2.default.sortBy(values); + return sorted[Math.floor(sorted.length / 2)]; +} + +/////////////////////// +// Utility functions // +/////////////////////// + +/** + * For given point calculate corresponding time frame. + * + * |__*_|_*__|___*| -> |*___|*___|*___| + * + * @param {*} timestamp + * @param {*} ms_interval + */ +function getPointTimeFrame(timestamp, ms_interval) { + return Math.floor(timestamp / ms_interval) * ms_interval; +} + +function sortByTime(series) { + return _lodash2.default.sortBy(series, function (point) { + return point[1]; + }); +} + +/** + * Fill empty front and end of series by zeroes. + * + * | *** | | *** | + * |___ ___| -> |*** ***| + * @param {*} series + * @param {*} timestamps + */ +function fillZeroes(series, timestamps) { + var prepend = []; + var append = []; + var new_point = void 0; + for (var i = 0; i < timestamps.length; i++) { + if (timestamps[i] < series[0][POINT_TIMESTAMP]) { + new_point = [0, timestamps[i]]; + prepend.push(new_point); + } else if (timestamps[i] > series[series.length - 1][POINT_TIMESTAMP]) { + new_point = [0, timestamps[i]]; + append.push(new_point); + } + } + return _lodash2.default.concat(_lodash2.default.concat(prepend, series), append); +} + +/** + * 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, i); + right = findNearestRight(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, pointIndex) { + for (var i = pointIndex; i < series.length; i++) { + if (series[i][0] !== null) { + return series[i]; + } + } + return null; +} + +function findNearestLeft(series, pointIndex) { + for (var i = pointIndex; i > 0; i--) { + if (series[i][0] !== null) { + return series[i]; + } + } + return null; +} + +//////////// +// Export // +//////////// + +var exportedFunctions = { + downsample: downsample, + groupBy: groupBy, + groupBy_perf: groupBy_perf, + sumSeries: sumSeries, + scale: scale, + scale_perf: scale_perf, + delta: delta, + rate: rate, + simpleMovingAverage: simpleMovingAverage, + expMovingAverage: expMovingAverage, + SUM: SUM, + COUNT: COUNT, + AVERAGE: AVERAGE, + MIN: MIN, + MAX: MAX, + MEDIAN: MEDIAN, + PERCENTIL: PERCENTIL +}; + +exports.default = exportedFunctions; diff --git a/dist/test/datasource-zabbix/utils.js b/dist/test/datasource-zabbix/utils.js new file mode 100644 index 0000000..6ca5c96 --- /dev/null +++ b/dist/test/datasource-zabbix/utils.js @@ -0,0 +1,256 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.regexPattern = undefined; +exports.expandItemName = expandItemName; +exports.expandItems = expandItems; +exports.containsMacro = containsMacro; +exports.replaceMacro = replaceMacro; +exports.splitTemplateQuery = splitTemplateQuery; +exports.isRegex = isRegex; +exports.isTemplateVariable = isTemplateVariable; +exports.buildRegex = buildRegex; +exports.escapeRegex = escapeRegex; +exports.parseInterval = parseInterval; +exports.parseTimeShiftInterval = parseTimeShiftInterval; +exports.formatAcknowledges = formatAcknowledges; +exports.convertToZabbixAPIUrl = convertToZabbixAPIUrl; +exports.callOnce = callOnce; + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _moment = require('moment'); + +var _moment2 = _interopRequireDefault(_moment); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * Expand Zabbix item name + * + * @param {string} name item name, ie "CPU $2 time" + * @param {string} key item key, ie system.cpu.util[,system,avg1] + * @return {string} expanded name, ie "CPU system time" + */ +function expandItemName(name, key) { + + // extract params from key: + // "system.cpu.util[,system,avg1]" --> ["", "system", "avg1"] + var key_params_str = key.substring(key.indexOf('[') + 1, key.lastIndexOf(']')); + var key_params = splitKeyParams(key_params_str); + + // replace item parameters + for (var i = key_params.length; i >= 1; i--) { + name = name.replace('$' + i, key_params[i - 1]); + } + return name; +} + +function expandItems(items) { + _lodash2.default.forEach(items, function (item) { + item.item = item.name; + item.name = expandItemName(item.item, item.key_); + return item; + }); + return items; +} + +function splitKeyParams(paramStr) { + var params = []; + var quoted = false; + var in_array = false; + var split_symbol = ','; + var param = ''; + + _lodash2.default.forEach(paramStr, function (symbol) { + if (symbol === '"' && in_array) { + param += symbol; + } else if (symbol === '"' && quoted) { + quoted = false; + } else if (symbol === '"' && !quoted) { + quoted = true; + } else if (symbol === '[' && !quoted) { + in_array = true; + } else if (symbol === ']' && !quoted) { + in_array = false; + } else if (symbol === split_symbol && !quoted && !in_array) { + params.push(param); + param = ''; + } else { + param += symbol; + } + }); + + params.push(param); + return params; +} + +var MACRO_PATTERN = /{\$[A-Z0-9_\.]+}/g; + +function containsMacro(itemName) { + return MACRO_PATTERN.test(itemName); +} + +function replaceMacro(item, macros) { + var itemName = item.name; + var item_macros = itemName.match(MACRO_PATTERN); + _lodash2.default.forEach(item_macros, function (macro) { + var host_macros = _lodash2.default.filter(macros, function (m) { + if (m.hostid) { + return m.hostid === item.hostid; + } else { + // Add global macros + return true; + } + }); + + var macro_def = _lodash2.default.find(host_macros, { macro: macro }); + if (macro_def && macro_def.value) { + var macro_value = macro_def.value; + var macro_regex = new RegExp(escapeMacro(macro)); + itemName = itemName.replace(macro_regex, macro_value); + } + }); + + return itemName; +} + +function escapeMacro(macro) { + macro = macro.replace(/\$/, '\\\$'); + return macro; +} + +/** + * Split template query to parts of zabbix entities + * group.host.app.item -> [group, host, app, item] + * {group}{host.com} -> [group, host.com] + */ +function splitTemplateQuery(query) { + var splitPattern = /\{[^\{\}]*\}|\{\/.*\/\}/g; + var split = void 0; + + if (isContainsBraces(query)) { + var result = query.match(splitPattern); + split = _lodash2.default.map(result, function (part) { + return _lodash2.default.trim(part, '{}'); + }); + } else { + split = query.split('.'); + } + + return split; +} + +function isContainsBraces(query) { + var bracesPattern = /^\{.+\}$/; + return bracesPattern.test(query); +} + +// Pattern for testing regex +var regexPattern = exports.regexPattern = /^\/(.*)\/([gmi]*)$/m; + +function isRegex(str) { + return regexPattern.test(str); +} + +function isTemplateVariable(str, templateVariables) { + var variablePattern = /^\$\w+/; + if (variablePattern.test(str)) { + var variables = _lodash2.default.map(templateVariables, function (variable) { + return '$' + variable.name; + }); + return _lodash2.default.includes(variables, str); + } else { + return false; + } +} + +function buildRegex(str) { + var matches = str.match(regexPattern); + var pattern = matches[1]; + var flags = matches[2] !== "" ? matches[2] : undefined; + return new RegExp(pattern, flags); +} + +// Need for template variables replace +// From Grafana's templateSrv.js +function escapeRegex(value) { + return value.replace(/[\\^$*+?.()|[\]{}\/]/g, '\\$&'); +} + +function parseInterval(interval) { + var intervalPattern = /(^[\d]+)(y|M|w|d|h|m|s)/g; + var momentInterval = intervalPattern.exec(interval); + return _moment2.default.duration(Number(momentInterval[1]), momentInterval[2]).valueOf(); +} + +function parseTimeShiftInterval(interval) { + var intervalPattern = /^([\+\-]*)([\d]+)(y|M|w|d|h|m|s)/g; + var momentInterval = intervalPattern.exec(interval); + var duration = 0; + + if (momentInterval[1] === '+') { + duration = 0 - _moment2.default.duration(Number(momentInterval[2]), momentInterval[3]).valueOf(); + } else { + duration = _moment2.default.duration(Number(momentInterval[2]), momentInterval[3]).valueOf(); + } + + return duration; +} + +/** + * Format acknowledges. + * + * @param {array} acknowledges array of Zabbix acknowledge objects + * @return {string} HTML-formatted table + */ +function formatAcknowledges(acknowledges) { + if (acknowledges.length) { + var formatted_acknowledges = '

Acknowledges:
' + ''; + _lodash2.default.each(_lodash2.default.map(acknowledges, function (ack) { + var timestamp = _moment2.default.unix(ack.clock); + return ''; + }), function (ack) { + formatted_acknowledges = formatted_acknowledges.concat(ack); + }); + formatted_acknowledges = formatted_acknowledges.concat('
TimeUserComments
' + timestamp.format("DD MMM YYYY HH:mm:ss") + '' + ack.alias + ' (' + ack.name + ' ' + ack.surname + ')' + '' + ack.message + '
'); + return formatted_acknowledges; + } else { + return ''; + } +} + +function convertToZabbixAPIUrl(url) { + var zabbixAPIUrlPattern = /.*api_jsonrpc.php$/; + var trimSlashPattern = /(.*?)[\/]*$/; + if (url.match(zabbixAPIUrlPattern)) { + return url; + } else { + return url.replace(trimSlashPattern, "$1"); + } +} + +/** + * Wrap function to prevent multiple calls + * when waiting for result. + */ +function callOnce(func, promiseKeeper) { + return function () { + if (!promiseKeeper) { + promiseKeeper = Promise.resolve(func.apply(this, arguments).then(function (result) { + promiseKeeper = null; + return result; + })); + } + return promiseKeeper; + }; +} + +// Fix for backward compatibility with lodash 2.4 +if (!_lodash2.default.includes) { + _lodash2.default.includes = _lodash2.default.contains; +} diff --git a/dist/test/datasource-zabbix/zabbix.js b/dist/test/datasource-zabbix/zabbix.js new file mode 100644 index 0000000..e90c745 --- /dev/null +++ b/dist/test/datasource-zabbix/zabbix.js @@ -0,0 +1,338 @@ +'use strict'; + +var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _angular = require('angular'); + +var _angular2 = _interopRequireDefault(_angular); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _utils = require('./utils'); + +var utils = _interopRequireWildcard(_utils); + +require('./zabbixAPI.service.js'); + +require('./zabbixCachingProxy.service.js'); + +require('./zabbixDBConnector'); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +// Use factory() instead service() for multiple data sources support. +// Each Zabbix data source instance should initialize its own API instance. + +/** @ngInject */ +function ZabbixFactory(zabbixAPIService, ZabbixCachingProxy, ZabbixDBConnector) { + var Zabbix = function () { + function Zabbix(url, options) { + _classCallCheck(this, Zabbix); + + var username = options.username, + password = options.password, + basicAuth = options.basicAuth, + withCredentials = options.withCredentials, + cacheTTL = options.cacheTTL, + enableDirectDBConnection = options.enableDirectDBConnection, + sqlDatasourceId = options.sqlDatasourceId; + + // Initialize Zabbix API + + var ZabbixAPI = zabbixAPIService; + this.zabbixAPI = new ZabbixAPI(url, username, password, basicAuth, withCredentials); + + if (enableDirectDBConnection) { + this.dbConnector = new ZabbixDBConnector(sqlDatasourceId); + } + + // Initialize caching proxy for requests + var cacheOptions = { + enabled: true, + ttl: cacheTTL + }; + this.cachingProxy = new ZabbixCachingProxy(this.zabbixAPI, this.dbConnector, cacheOptions); + + // Proxy methods + this.getHistory = this.cachingProxy.getHistory.bind(this.cachingProxy); + this.getMacros = this.cachingProxy.getMacros.bind(this.cachingProxy); + this.getItemsByIDs = this.cachingProxy.getItemsByIDs.bind(this.cachingProxy); + + if (enableDirectDBConnection) { + this.getHistoryDB = this.cachingProxy.getHistoryDB.bind(this.cachingProxy); + this.getTrendsDB = this.cachingProxy.getTrendsDB.bind(this.cachingProxy); + } + + this.getTrend = this.zabbixAPI.getTrend.bind(this.zabbixAPI); + this.getEvents = this.zabbixAPI.getEvents.bind(this.zabbixAPI); + this.getAlerts = this.zabbixAPI.getAlerts.bind(this.zabbixAPI); + this.getHostAlerts = this.zabbixAPI.getHostAlerts.bind(this.zabbixAPI); + this.getAcknowledges = this.zabbixAPI.getAcknowledges.bind(this.zabbixAPI); + this.getITService = this.zabbixAPI.getITService.bind(this.zabbixAPI); + this.getSLA = this.zabbixAPI.getSLA.bind(this.zabbixAPI); + this.getVersion = this.zabbixAPI.getVersion.bind(this.zabbixAPI); + this.login = this.zabbixAPI.login.bind(this.zabbixAPI); + } + + _createClass(Zabbix, [{ + key: 'getItemsFromTarget', + value: function getItemsFromTarget(target, options) { + var parts = ['group', 'host', 'application', 'item']; + var filters = _lodash2.default.map(parts, function (p) { + return target[p].filter; + }); + return this.getItems.apply(this, _toConsumableArray(filters).concat([options])); + } + }, { + key: 'getHostsFromTarget', + value: function getHostsFromTarget(target) { + var parts = ['group', 'host', 'application']; + var filters = _lodash2.default.map(parts, function (p) { + return target[p].filter; + }); + return Promise.all([this.getHosts.apply(this, _toConsumableArray(filters)), this.getApps.apply(this, _toConsumableArray(filters))]).then(function (results) { + var _results = _slicedToArray(results, 2), + hosts = _results[0], + apps = _results[1]; + + if (apps.appFilterEmpty) { + apps = []; + } + return [hosts, apps]; + }); + } + }, { + key: 'getAllGroups', + value: function getAllGroups() { + return this.cachingProxy.getGroups(); + } + }, { + key: 'getGroups', + value: function getGroups(groupFilter) { + return this.getAllGroups().then(function (groups) { + return findByFilter(groups, groupFilter); + }); + } + + /** + * Get list of host belonging to given groups. + */ + + }, { + key: 'getAllHosts', + value: function getAllHosts(groupFilter) { + var _this = this; + + return this.getGroups(groupFilter).then(function (groups) { + var groupids = _lodash2.default.map(groups, 'groupid'); + return _this.cachingProxy.getHosts(groupids); + }); + } + }, { + key: 'getHosts', + value: function getHosts(groupFilter, hostFilter) { + return this.getAllHosts(groupFilter).then(function (hosts) { + return findByFilter(hosts, hostFilter); + }); + } + + /** + * Get list of applications belonging to given groups and hosts. + */ + + }, { + key: 'getAllApps', + value: function getAllApps(groupFilter, hostFilter) { + var _this2 = this; + + return this.getHosts(groupFilter, hostFilter).then(function (hosts) { + var hostids = _lodash2.default.map(hosts, 'hostid'); + return _this2.cachingProxy.getApps(hostids); + }); + } + }, { + key: 'getApps', + value: function getApps(groupFilter, hostFilter, appFilter) { + var _this3 = this; + + return this.getHosts(groupFilter, hostFilter).then(function (hosts) { + var hostids = _lodash2.default.map(hosts, 'hostid'); + if (appFilter) { + return _this3.cachingProxy.getApps(hostids).then(function (apps) { + return filterByQuery(apps, appFilter); + }); + } else { + return { + appFilterEmpty: true, + hostids: hostids + }; + } + }); + } + }, { + key: 'getAllItems', + value: function getAllItems(groupFilter, hostFilter, appFilter) { + var _this4 = this; + + var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; + + return this.getApps(groupFilter, hostFilter, appFilter).then(function (apps) { + if (apps.appFilterEmpty) { + return _this4.cachingProxy.getItems(apps.hostids, undefined, options.itemtype); + } else { + var appids = _lodash2.default.map(apps, 'applicationid'); + return _this4.cachingProxy.getItems(undefined, appids, options.itemtype); + } + }).then(function (items) { + if (!options.showDisabledItems) { + items = _lodash2.default.filter(items, { 'status': '0' }); + } + + return items; + }).then(this.expandUserMacro.bind(this)); + } + }, { + key: 'expandUserMacro', + value: function expandUserMacro(items) { + var hostids = getHostIds(items); + return this.getMacros(hostids).then(function (macros) { + _lodash2.default.forEach(items, function (item) { + if (utils.containsMacro(item.name)) { + item.name = utils.replaceMacro(item, macros); + } + }); + return items; + }); + } + }, { + key: 'getItems', + value: function getItems(groupFilter, hostFilter, appFilter, itemFilter) { + var options = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {}; + + return this.getAllItems(groupFilter, hostFilter, appFilter, options).then(function (items) { + return filterByQuery(items, itemFilter); + }); + } + }, { + key: 'getITServices', + value: function getITServices(itServiceFilter) { + return this.cachingProxy.getITServices().then(function (itServices) { + return findByFilter(itServices, itServiceFilter); + }); + } + + /** + * Build query - convert target filters to array of Zabbix items + */ + + }, { + key: 'getTriggers', + value: function getTriggers(groupFilter, hostFilter, appFilter, options) { + var _this5 = this; + + var promises = [this.getGroups(groupFilter), this.getHosts(groupFilter, hostFilter), this.getApps(groupFilter, hostFilter, appFilter)]; + + return Promise.all(promises).then(function (results) { + var filteredGroups = results[0]; + var filteredHosts = results[1]; + var filteredApps = results[2]; + var query = {}; + + if (appFilter) { + query.applicationids = _lodash2.default.flatten(_lodash2.default.map(filteredApps, 'applicationid')); + } + if (hostFilter) { + query.hostids = _lodash2.default.map(filteredHosts, 'hostid'); + } + if (groupFilter) { + query.groupids = _lodash2.default.map(filteredGroups, 'groupid'); + } + + return query; + }).then(function (query) { + return _this5.zabbixAPI.getTriggers(query.groupids, query.hostids, query.applicationids, options); + }); + } + }]); + + return Zabbix; + }(); + + return Zabbix; +} + +_angular2.default.module('grafana.services').factory('Zabbix', ZabbixFactory); + +/////////////////////////////////////////////////////////////////////////////// + +/** + * Find group, host, app or item by given name. + * @param list list of groups, apps or other + * @param name visible name + * @return array with finded element or empty array + */ +function findByName(list, name) { + var finded = _lodash2.default.find(list, { 'name': name }); + if (finded) { + return [finded]; + } else { + return []; + } +} + +/** + * Different hosts can contains applications and items with same name. + * For this reason use _.filter, which return all elements instead _.find, + * which return only first finded. + * @param {[type]} list list of elements + * @param {[type]} name app name + * @return {[type]} array with finded element or empty array + */ +function filterByName(list, name) { + var finded = _lodash2.default.filter(list, { 'name': name }); + if (finded) { + return finded; + } else { + return []; + } +} + +function filterByRegex(list, regex) { + var filterPattern = utils.buildRegex(regex); + return _lodash2.default.filter(list, function (zbx_obj) { + return filterPattern.test(zbx_obj.name); + }); +} + +function findByFilter(list, filter) { + if (utils.isRegex(filter)) { + return filterByRegex(list, filter); + } else { + return findByName(list, filter); + } +} + +function filterByQuery(list, filter) { + if (utils.isRegex(filter)) { + return filterByRegex(list, filter); + } else { + return filterByName(list, filter); + } +} + +function getHostIds(items) { + var hostIds = _lodash2.default.map(items, function (item) { + return _lodash2.default.map(item.hosts, 'hostid'); + }); + return _lodash2.default.uniq(_lodash2.default.flatten(hostIds)); +} diff --git a/dist/test/datasource-zabbix/zabbixAPI.service.js b/dist/test/datasource-zabbix/zabbixAPI.service.js new file mode 100644 index 0000000..726ad45 --- /dev/null +++ b/dist/test/datasource-zabbix/zabbixAPI.service.js @@ -0,0 +1,561 @@ +'use strict'; + +var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _angular = require('angular'); + +var _angular2 = _interopRequireDefault(_angular); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _utils = require('./utils'); + +var utils = _interopRequireWildcard(_utils); + +require('./zabbixAPICore.service'); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/** @ngInject */ +function ZabbixAPIServiceFactory(alertSrv, zabbixAPICoreService) { + + /** + * Zabbix API Wrapper. + * Creates Zabbix API instance with given parameters (url, credentials and other). + * Wraps API calls and provides high-level methods. + */ + var ZabbixAPI = function () { + function ZabbixAPI(api_url, username, password, basicAuth, withCredentials) { + _classCallCheck(this, ZabbixAPI); + + this.url = api_url; + this.username = username; + this.password = password; + this.auth = ""; + + this.requestOptions = { + basicAuth: basicAuth, + withCredentials: withCredentials + }; + + this.loginPromise = null; + this.loginErrorCount = 0; + this.maxLoginAttempts = 3; + + this.alertSrv = alertSrv; + this.zabbixAPICore = zabbixAPICoreService; + + this.getTrend = this.getTrend_ZBXNEXT1193; + //getTrend = getTrend_30; + } + + ////////////////////////// + // Core method wrappers // + ////////////////////////// + + _createClass(ZabbixAPI, [{ + key: 'request', + value: function request(method, params) { + var _this = this; + + return this.zabbixAPICore.request(this.url, method, params, this.requestOptions, this.auth).catch(function (error) { + if (isNotAuthorized(error.data)) { + // Handle auth errors + _this.loginErrorCount++; + if (_this.loginErrorCount > _this.maxLoginAttempts) { + _this.loginErrorCount = 0; + return null; + } else { + return _this.loginOnce().then(function () { + return _this.request(method, params); + }); + } + } else { + // Handle API errors + var message = error.data ? error.data : error.statusText; + _this.alertAPIError(message); + } + }); + } + }, { + key: 'alertAPIError', + value: function alertAPIError(message) { + var timeout = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 5000; + + this.alertSrv.set("Zabbix API Error", message, 'error', timeout); + } + + /** + * When API unauthenticated or auth token expired each request produce login() + * call. But auth token is common to all requests. This function wraps login() method + * and call it once. If login() already called just wait for it (return its promise). + * @return login promise + */ + + }, { + key: 'loginOnce', + value: function loginOnce() { + var _this2 = this; + + if (!this.loginPromise) { + this.loginPromise = Promise.resolve(this.login().then(function (auth) { + _this2.auth = auth; + _this2.loginPromise = null; + return auth; + })); + } + return this.loginPromise; + } + + /** + * Get authentication token. + */ + + }, { + key: 'login', + value: function login() { + return this.zabbixAPICore.login(this.url, this.username, this.password, this.requestOptions); + } + + /** + * Get Zabbix API version + */ + + }, { + key: 'getVersion', + value: function getVersion() { + return this.zabbixAPICore.getVersion(this.url, this.requestOptions); + } + + //////////////////////////////// + // Zabbix API method wrappers // + //////////////////////////////// + + }, { + key: 'acknowledgeEvent', + value: function acknowledgeEvent(eventid, message) { + var params = { + eventids: eventid, + message: message + }; + + return this.request('event.acknowledge', params); + } + }, { + key: 'getGroups', + value: function getGroups() { + var params = { + output: ['name'], + sortfield: 'name', + real_hosts: true + }; + + return this.request('hostgroup.get', params); + } + }, { + key: 'getHosts', + value: function getHosts(groupids) { + var params = { + output: ['name', 'host'], + sortfield: 'name' + }; + if (groupids) { + params.groupids = groupids; + } + + return this.request('host.get', params); + } + }, { + key: 'getApps', + value: function getApps(hostids) { + var params = { + output: 'extend', + hostids: hostids + }; + + return this.request('application.get', params); + } + + /** + * Get Zabbix items + * @param {[type]} hostids host ids + * @param {[type]} appids application ids + * @param {String} itemtype 'num' or 'text' + * @return {[type]} array of items + */ + + }, { + key: 'getItems', + value: function getItems(hostids, appids, itemtype) { + var params = { + output: ['name', 'key_', 'value_type', 'hostid', 'status', 'state'], + sortfield: 'name', + webitems: true, + filter: {}, + selectHosts: ['hostid', 'name'] + }; + if (hostids) { + params.hostids = hostids; + } + if (appids) { + params.applicationids = appids; + } + if (itemtype === 'num') { + // Return only numeric metrics + params.filter.value_type = [0, 3]; + } + if (itemtype === 'text') { + // Return only text metrics + params.filter.value_type = [1, 2, 4]; + } + + return this.request('item.get', params).then(utils.expandItems); + } + }, { + key: 'getItemsByIDs', + value: function getItemsByIDs(itemids) { + var params = { + itemids: itemids, + output: ['name', 'key_', 'value_type', 'hostid', 'status', 'state'], + webitems: true, + selectHosts: ['hostid', 'name'] + }; + + return this.request('item.get', params).then(utils.expandItems); + } + }, { + key: 'getMacros', + value: function getMacros(hostids) { + var params = { + output: 'extend', + hostids: hostids + }; + + return this.request('usermacro.get', params); + } + }, { + key: 'getGlobalMacros', + value: function getGlobalMacros() { + var params = { + output: 'extend', + globalmacro: true + }; + + return this.request('usermacro.get', params); + } + }, { + key: 'getLastValue', + value: function getLastValue(itemid) { + var params = { + output: ['lastvalue'], + itemids: itemid + }; + return this.request('item.get', params).then(function (items) { + return items.length ? items[0].lastvalue : null; + }); + } + + /** + * Perform history query from Zabbix API + * + * @param {Array} items Array of Zabbix item objects + * @param {Number} timeFrom Time in seconds + * @param {Number} timeTill Time in seconds + * @return {Array} Array of Zabbix history objects + */ + + }, { + key: 'getHistory', + value: function getHistory(items, timeFrom, timeTill) { + var _this3 = this; + + // Group items by value type and perform request for each value type + var grouped_items = _lodash2.default.groupBy(items, 'value_type'); + var promises = _lodash2.default.map(grouped_items, function (items, value_type) { + var itemids = _lodash2.default.map(items, 'itemid'); + var params = { + output: 'extend', + history: value_type, + itemids: itemids, + sortfield: 'clock', + sortorder: 'ASC', + time_from: timeFrom + }; + + // Relative queries (e.g. last hour) don't include an end time + if (timeTill) { + params.time_till = timeTill; + } + + return _this3.request('history.get', params); + }); + + return Promise.all(promises).then(_lodash2.default.flatten); + } + + /** + * Perform trends query from Zabbix API + * Use trends api extension from ZBXNEXT-1193 patch. + * + * @param {Array} items Array of Zabbix item objects + * @param {Number} time_from Time in seconds + * @param {Number} time_till Time in seconds + * @return {Array} Array of Zabbix trend objects + */ + + }, { + key: 'getTrend_ZBXNEXT1193', + value: function getTrend_ZBXNEXT1193(items, timeFrom, timeTill) { + var _this4 = this; + + // Group items by value type and perform request for each value type + var grouped_items = _lodash2.default.groupBy(items, 'value_type'); + var promises = _lodash2.default.map(grouped_items, function (items, value_type) { + var itemids = _lodash2.default.map(items, 'itemid'); + var params = { + output: 'extend', + trend: value_type, + itemids: itemids, + sortfield: 'clock', + sortorder: 'ASC', + time_from: timeFrom + }; + + // Relative queries (e.g. last hour) don't include an end time + if (timeTill) { + params.time_till = timeTill; + } + + return _this4.request('trend.get', params); + }); + + return Promise.all(promises).then(_lodash2.default.flatten); + } + }, { + key: 'getTrend_30', + value: function getTrend_30(items, time_from, time_till, value_type) { + var self = this; + var itemids = _lodash2.default.map(items, 'itemid'); + + var params = { + output: ["itemid", "clock", value_type], + itemids: itemids, + time_from: time_from + }; + + // Relative queries (e.g. last hour) don't include an end time + if (time_till) { + params.time_till = time_till; + } + + return self.request('trend.get', params); + } + }, { + key: 'getITService', + value: function getITService(serviceids) { + var params = { + output: 'extend', + serviceids: serviceids + }; + return this.request('service.get', params); + } + }, { + key: 'getSLA', + value: function getSLA(serviceids, timeRange) { + var _timeRange = _slicedToArray(timeRange, 2), + timeFrom = _timeRange[0], + timeTo = _timeRange[1]; + + var params = { + serviceids: serviceids, + intervals: [{ + from: timeFrom, + to: timeTo + }] + }; + return this.request('service.getsla', params); + } + }, { + key: 'getTriggers', + value: function getTriggers(groupids, hostids, applicationids, options) { + var showTriggers = options.showTriggers, + hideHostsInMaintenance = options.hideHostsInMaintenance, + timeFrom = options.timeFrom, + timeTo = options.timeTo; + + + var params = { + output: 'extend', + groupids: groupids, + hostids: hostids, + applicationids: applicationids, + expandDescription: true, + expandData: true, + expandComment: true, + monitored: true, + skipDependent: true, + //only_true: true, + filter: { + value: 1 + }, + selectGroups: ['name'], + selectHosts: ['name', 'host'], + selectItems: ['name', 'key_', 'lastvalue'], + selectLastEvent: 'extend' + }; + + if (showTriggers) { + params.filter.value = showTriggers; + } + + if (hideHostsInMaintenance) { + params.maintenance = false; + } + + if (timeFrom || timeTo) { + params.lastChangeSince = timeFrom; + params.lastChangeTill = timeTo; + } + + return this.request('trigger.get', params); + } + }, { + key: 'getEvents', + value: function getEvents(objectids, timeFrom, timeTo, showEvents) { + var params = { + output: 'extend', + time_from: timeFrom, + time_till: timeTo, + objectids: objectids, + select_acknowledges: 'extend', + selectHosts: 'extend', + value: showEvents + }; + + return this.request('event.get', params); + } + }, { + key: 'getAcknowledges', + value: function getAcknowledges(eventids) { + var params = { + output: 'extend', + eventids: eventids, + preservekeys: true, + select_acknowledges: 'extend', + sortfield: 'clock', + sortorder: 'DESC' + }; + + return this.request('event.get', params).then(function (events) { + return _lodash2.default.filter(events, function (event) { + return event.acknowledges.length; + }); + }); + } + }, { + key: 'getAlerts', + value: function getAlerts(itemids, timeFrom, timeTo) { + var params = { + output: 'extend', + itemids: itemids, + expandDescription: true, + expandData: true, + expandComment: true, + monitored: true, + skipDependent: true, + //only_true: true, + // filter: { + // value: 1 + // }, + selectLastEvent: 'extend' + }; + + if (timeFrom || timeTo) { + params.lastChangeSince = timeFrom; + params.lastChangeTill = timeTo; + } + + return this.request('trigger.get', params); + } + }, { + key: 'getHostAlerts', + value: function getHostAlerts(hostids, applicationids, options) { + var minSeverity = options.minSeverity, + acknowledged = options.acknowledged, + count = options.count, + timeFrom = options.timeFrom, + timeTo = options.timeTo; + + var params = { + output: 'extend', + hostids: hostids, + min_severity: minSeverity, + filter: { value: 1 }, + expandDescription: true, + expandData: true, + expandComment: true, + monitored: true, + skipDependent: true, + selectLastEvent: 'extend', + selectGroups: 'extend', + selectHosts: ['host', 'name'] + }; + + if (count && acknowledged !== 0 && acknowledged !== 1) { + params.countOutput = true; + } + + if (applicationids && applicationids.length) { + params.applicationids = applicationids; + } + + if (timeFrom || timeTo) { + params.lastChangeSince = timeFrom; + params.lastChangeTill = timeTo; + } + + return this.request('trigger.get', params).then(function (triggers) { + if (!count || acknowledged === 0 || acknowledged === 1) { + triggers = filterTriggersByAcknowledge(triggers, acknowledged); + if (count) { + triggers = triggers.length; + } + } + return triggers; + }); + } + }]); + + return ZabbixAPI; + }(); + + return ZabbixAPI; +} + +function filterTriggersByAcknowledge(triggers, acknowledged) { + if (acknowledged === 0) { + return _lodash2.default.filter(triggers, function (trigger) { + return trigger.lastEvent.acknowledged === "0"; + }); + } else if (acknowledged === 1) { + return _lodash2.default.filter(triggers, function (trigger) { + return trigger.lastEvent.acknowledged === "1"; + }); + } else { + return triggers; + } +} + +function isNotAuthorized(message) { + return message === "Session terminated, re-login, please." || message === "Not authorised." || message === "Not authorized."; +} + +_angular2.default.module('grafana.services').factory('zabbixAPIService', ZabbixAPIServiceFactory); diff --git a/dist/test/datasource-zabbix/zabbixAPICore.service.js b/dist/test/datasource-zabbix/zabbixAPICore.service.js new file mode 100644 index 0000000..c5ccee9 --- /dev/null +++ b/dist/test/datasource-zabbix/zabbixAPICore.service.js @@ -0,0 +1,142 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.ZabbixAPIError = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /** + * General Zabbix API methods + */ + +var _angular = require('angular'); + +var _angular2 = _interopRequireDefault(_angular); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var ZabbixAPICoreService = function () { + + /** @ngInject */ + function ZabbixAPICoreService(backendSrv) { + _classCallCheck(this, ZabbixAPICoreService); + + this.backendSrv = backendSrv; + } + + /** + * Request data from Zabbix API + * @return {object} response.result + */ + + + _createClass(ZabbixAPICoreService, [{ + key: 'request', + value: function request(api_url, method, params, options, auth) { + var requestData = { + jsonrpc: '2.0', + method: method, + params: params, + id: 1 + }; + + if (auth === "") { + // Reject immediately if not authenticated + return Promise.reject(new ZabbixAPIError({ data: "Not authorised." })); + } else if (auth) { + // Set auth parameter only if it needed + requestData.auth = auth; + } + + var requestOptions = { + method: 'POST', + url: api_url, + data: requestData, + headers: { + 'Content-Type': 'application/json' + } + }; + + // Set request options for basic auth + if (options.basicAuth || options.withCredentials) { + requestOptions.withCredentials = true; + } + if (options.basicAuth) { + requestOptions.headers.Authorization = options.basicAuth; + } + + return this.datasourceRequest(requestOptions); + } + }, { + key: 'datasourceRequest', + value: function datasourceRequest(requestOptions) { + return this.backendSrv.datasourceRequest(requestOptions).then(function (response) { + if (!response.data) { + return Promise.reject(new ZabbixAPIError({ data: "General Error, no data" })); + } else if (response.data.error) { + + // Handle Zabbix API errors + return Promise.reject(new ZabbixAPIError(response.data.error)); + } + + // Success + return response.data.result; + }); + } + + /** + * Get authentication token. + * @return {string} auth token + */ + + }, { + key: 'login', + value: function login(api_url, username, password, options) { + var params = { + user: username, + password: password + }; + return this.request(api_url, 'user.login', params, options, null); + } + + /** + * Get Zabbix API version + * Matches the version of Zabbix starting from Zabbix 2.0.4 + */ + + }, { + key: 'getVersion', + value: function getVersion(api_url, options) { + return this.request(api_url, 'apiinfo.version', [], options); + } + }]); + + return ZabbixAPICoreService; +}(); + +// Define zabbix API exception type + + +var ZabbixAPIError = exports.ZabbixAPIError = function () { + function ZabbixAPIError(error) { + _classCallCheck(this, ZabbixAPIError); + + this.code = error.code || null; + this.name = error.message || ""; + this.data = error.data || ""; + this.message = "Zabbix API Error: " + this.name + " " + this.data; + } + + _createClass(ZabbixAPIError, [{ + key: 'toString', + value: function toString() { + return this.name + " " + this.data; + } + }]); + + return ZabbixAPIError; +}(); + +_angular2.default.module('grafana.services').service('zabbixAPICoreService', ZabbixAPICoreService); diff --git a/dist/test/datasource-zabbix/zabbixAlerting.service.js b/dist/test/datasource-zabbix/zabbixAlerting.service.js new file mode 100644 index 0000000..4f1135e --- /dev/null +++ b/dist/test/datasource-zabbix/zabbixAlerting.service.js @@ -0,0 +1,127 @@ +'use strict'; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _jquery = require('jquery'); + +var _jquery2 = _interopRequireDefault(_jquery); + +var _angular = require('angular'); + +var _angular2 = _interopRequireDefault(_angular); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var ZabbixAlertingService = function () { + + /** @ngInject */ + function ZabbixAlertingService(dashboardSrv) { + _classCallCheck(this, ZabbixAlertingService); + + this.dashboardSrv = dashboardSrv; + } + + _createClass(ZabbixAlertingService, [{ + key: 'isFullScreen', + value: function isFullScreen() { + return this.dashboardSrv.dash.meta.fullscreen; + } + }, { + key: 'setPanelAlertState', + value: function setPanelAlertState(panelId, alertState) { + var panelIndex = void 0; + + var panelContainers = _lodash2.default.filter((0, _jquery2.default)('.panel-container'), function (elem) { + return elem.clientHeight && elem.clientWidth; + }); + + var panelModels = this.getPanelModels(); + + if (this.isFullScreen()) { + panelIndex = 0; + } else { + panelIndex = _lodash2.default.findIndex(panelModels, function (panel) { + return panel.id === panelId; + }); + } + + if (panelIndex >= 0) { + var alertClass = "panel-has-alert panel-alert-state--ok panel-alert-state--alerting"; + (0, _jquery2.default)(panelContainers[panelIndex]).removeClass(alertClass); + + if (alertState) { + if (alertState === 'alerting') { + alertClass = "panel-has-alert panel-alert-state--" + alertState; + (0, _jquery2.default)(panelContainers[panelIndex]).addClass(alertClass); + } + if (alertState === 'ok') { + alertClass = "panel-alert-state--" + alertState; + (0, _jquery2.default)(panelContainers[panelIndex]).addClass(alertClass); + (0, _jquery2.default)(panelContainers[panelIndex]).removeClass("panel-has-alert"); + } + } + } + } + }, { + key: 'getPanelModels', + value: function getPanelModels() { + return _lodash2.default.flatten(_lodash2.default.map(this.dashboardSrv.dash.rows, function (row) { + if (row.collapse) { + return []; + } else { + return row.panels; + } + })); + } + }, { + key: 'getPanelModel', + value: function getPanelModel(panelId) { + var panelModels = this.getPanelModels(); + + return _lodash2.default.find(panelModels, function (panel) { + return panel.id === panelId; + }); + } + }, { + key: 'setPanelThreshold', + value: function setPanelThreshold(panelId, threshold) { + var panel = this.getPanelModel(panelId); + var containsThreshold = _lodash2.default.find(panel.thresholds, { value: threshold }); + + if (panel && panel.type === "graph" && !containsThreshold) { + var thresholdOptions = { + colorMode: "custom", + fill: false, + line: true, + lineColor: "rgb(255, 0, 0)", + op: "gt", + value: threshold, + source: "zabbix" + }; + + panel.thresholds.push(thresholdOptions); + } + } + }, { + key: 'removeZabbixThreshold', + value: function removeZabbixThreshold(panelId) { + var panel = this.getPanelModel(panelId); + + if (panel && panel.type === "graph") { + panel.thresholds = _lodash2.default.filter(panel.thresholds, function (threshold) { + return threshold.source !== "zabbix"; + }); + } + } + }]); + + return ZabbixAlertingService; +}(); + +_angular2.default.module('grafana.services').service('zabbixAlertingSrv', ZabbixAlertingService); diff --git a/dist/test/datasource-zabbix/zabbixCachingProxy.service.js b/dist/test/datasource-zabbix/zabbixCachingProxy.service.js new file mode 100644 index 0000000..a3b12f5 --- /dev/null +++ b/dist/test/datasource-zabbix/zabbixCachingProxy.service.js @@ -0,0 +1,263 @@ +'use strict'; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _angular = require('angular'); + +var _angular2 = _interopRequireDefault(_angular); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +// Use factory() instead service() for multiple datasources support. +// Each datasource instance must initialize its own cache. + +/** @ngInject */ +function ZabbixCachingProxyFactory() { + var ZabbixCachingProxy = function () { + function ZabbixCachingProxy(zabbixAPI, zabbixDBConnector, cacheOptions) { + _classCallCheck(this, ZabbixCachingProxy); + + this.zabbixAPI = zabbixAPI; + this.dbConnector = zabbixDBConnector; + this.cacheEnabled = cacheOptions.enabled; + this.ttl = cacheOptions.ttl || 600000; // 10 minutes by default + + // Internal objects for data storing + this.cache = { + groups: {}, + hosts: {}, + applications: {}, + items: {}, + history: {}, + trends: {}, + macros: {}, + globalMacros: {}, + itServices: {} + }; + + this.historyPromises = {}; + + // Don't run duplicated history requests + this.getHistory = callAPIRequestOnce(_lodash2.default.bind(this.zabbixAPI.getHistory, this.zabbixAPI), this.historyPromises, getHistoryRequestHash); + + if (this.dbConnector) { + this.getHistoryDB = callAPIRequestOnce(_lodash2.default.bind(this.dbConnector.getHistory, this.dbConnector), this.historyPromises, getDBQueryHash); + this.getTrendsDB = callAPIRequestOnce(_lodash2.default.bind(this.dbConnector.getTrends, this.dbConnector), this.historyPromises, getDBQueryHash); + } + + // Don't run duplicated requests + this.groupPromises = {}; + this.getGroupsOnce = callAPIRequestOnce(_lodash2.default.bind(this.zabbixAPI.getGroups, this.zabbixAPI), this.groupPromises, getRequestHash); + + this.hostPromises = {}; + this.getHostsOnce = callAPIRequestOnce(_lodash2.default.bind(this.zabbixAPI.getHosts, this.zabbixAPI), this.hostPromises, getRequestHash); + + this.appPromises = {}; + this.getAppsOnce = callAPIRequestOnce(_lodash2.default.bind(this.zabbixAPI.getApps, this.zabbixAPI), this.appPromises, getRequestHash); + + this.itemPromises = {}; + this.getItemsOnce = callAPIRequestOnce(_lodash2.default.bind(this.zabbixAPI.getItems, this.zabbixAPI), this.itemPromises, getRequestHash); + + this.itemByIdPromises = {}; + this.getItemsByIdOnce = callAPIRequestOnce(_lodash2.default.bind(this.zabbixAPI.getItemsByIDs, this.zabbixAPI), this.itemPromises, getRequestHash); + + this.itServicesPromises = {}; + this.getITServicesOnce = callAPIRequestOnce(_lodash2.default.bind(this.zabbixAPI.getITService, this.zabbixAPI), this.itServicesPromises, getRequestHash); + + this.macroPromises = {}; + this.getMacrosOnce = callAPIRequestOnce(_lodash2.default.bind(this.zabbixAPI.getMacros, this.zabbixAPI), this.macroPromises, getRequestHash); + + this.globalMacroPromises = {}; + this.getGlobalMacrosOnce = callAPIRequestOnce(_lodash2.default.bind(this.zabbixAPI.getGlobalMacros, this.zabbixAPI), this.globalMacroPromises, getRequestHash); + } + + _createClass(ZabbixCachingProxy, [{ + key: 'isExpired', + value: function isExpired(cacheObject) { + if (cacheObject) { + var object_age = Date.now() - cacheObject.timestamp; + return !(cacheObject.timestamp && object_age < this.ttl); + } else { + return true; + } + } + + /** + * Check that result is present in cache and up to date + * or send request to API. + */ + + }, { + key: 'proxyRequest', + value: function proxyRequest(request, params, cacheObject) { + var hash = getRequestHash(params); + if (this.cacheEnabled && !this.isExpired(cacheObject[hash])) { + return Promise.resolve(cacheObject[hash].value); + } else { + return request.apply(undefined, _toConsumableArray(params)).then(function (result) { + cacheObject[hash] = { + value: result, + timestamp: Date.now() + }; + return result; + }); + } + } + }, { + key: 'getGroups', + value: function getGroups() { + return this.proxyRequest(this.getGroupsOnce, [], this.cache.groups); + } + }, { + key: 'getHosts', + value: function getHosts(groupids) { + return this.proxyRequest(this.getHostsOnce, [groupids], this.cache.hosts); + } + }, { + key: 'getApps', + value: function getApps(hostids) { + return this.proxyRequest(this.getAppsOnce, [hostids], this.cache.applications); + } + }, { + key: 'getItems', + value: function getItems(hostids, appids, itemtype) { + var params = [hostids, appids, itemtype]; + return this.proxyRequest(this.getItemsOnce, params, this.cache.items); + } + }, { + key: 'getItemsByIDs', + value: function getItemsByIDs(itemids) { + var params = [itemids]; + return this.proxyRequest(this.getItemsByIdOnce, params, this.cache.items); + } + }, { + key: 'getITServices', + value: function getITServices() { + return this.proxyRequest(this.getITServicesOnce, [], this.cache.itServices); + } + }, { + key: 'getMacros', + value: function getMacros(hostids) { + // Merge global macros and host macros + var promises = [this.proxyRequest(this.getMacrosOnce, [hostids], this.cache.macros), this.proxyRequest(this.getGlobalMacrosOnce, [], this.cache.globalMacros)]; + + return Promise.all(promises).then(_lodash2.default.flatten); + } + }, { + key: 'getHistoryFromCache', + value: function getHistoryFromCache(items, time_from, time_till) { + var historyStorage = this.cache.history; + var full_history; + var expired = _lodash2.default.filter(_lodash2.default.keyBy(items, 'itemid'), function (item, itemid) { + return !historyStorage[itemid]; + }); + if (expired.length) { + return this.zabbixAPI.getHistory(expired, time_from, time_till).then(function (history) { + var grouped_history = _lodash2.default.groupBy(history, 'itemid'); + _lodash2.default.forEach(expired, function (item) { + var itemid = item.itemid; + historyStorage[itemid] = item; + historyStorage[itemid].time_from = time_from; + historyStorage[itemid].time_till = time_till; + historyStorage[itemid].history = grouped_history[itemid]; + }); + full_history = _lodash2.default.map(items, function (item) { + return historyStorage[item.itemid].history; + }); + return _lodash2.default.flatten(full_history, true); + }); + } else { + full_history = _lodash2.default.map(items, function (item) { + return historyStorage[item.itemid].history; + }); + return Promise.resolve(_lodash2.default.flatten(full_history, true)); + } + } + }, { + key: 'getHistoryFromAPI', + value: function getHistoryFromAPI(items, time_from, time_till) { + return this.zabbixAPI.getHistory(items, time_from, time_till); + } + }]); + + return ZabbixCachingProxy; + }(); + + return ZabbixCachingProxy; +} + +_angular2.default.module('grafana.services').factory('ZabbixCachingProxy', ZabbixCachingProxyFactory); + +/** + * Wrap zabbix API request to prevent multiple calls + * with same params when waiting for result. + */ +function callAPIRequestOnce(func, promiseKeeper, argsHashFunc) { + return function () { + var hash = argsHashFunc(arguments); + if (!promiseKeeper[hash]) { + promiseKeeper[hash] = Promise.resolve(func.apply(this, arguments).then(function (result) { + promiseKeeper[hash] = null; + return result; + })); + } + return promiseKeeper[hash]; + }; +} + +function getRequestHash(args) { + var requestStamp = _lodash2.default.map(args, function (arg) { + if (arg === undefined) { + return 'undefined'; + } else { + if (_lodash2.default.isArray(arg)) { + return arg.sort().toString(); + } else { + return arg.toString(); + } + } + }).join(); + return requestStamp.getHash(); +} + +function getHistoryRequestHash(args) { + var itemids = _lodash2.default.map(args[0], 'itemid'); + var stamp = itemids.join() + args[1] + args[2]; + return stamp.getHash(); +} + +function getDBQueryHash(args) { + var itemids = _lodash2.default.map(args[0], 'itemid'); + var consolidateBy = args[3].consolidateBy; + var intervalMs = args[3].intervalMs; + var stamp = itemids.join() + args[1] + args[2] + consolidateBy + intervalMs; + return stamp.getHash(); +} + +String.prototype.getHash = function () { + var hash = 0, + i, + chr, + len; + if (this.length !== 0) { + for (i = 0, len = this.length; i < len; i++) { + chr = this.charCodeAt(i); + hash = (hash << 5) - hash + chr; + hash |= 0; // Convert to 32bit integer + } + } + return hash; +}; + +// Fix for backward compatibility with lodash 2.4 +if (!_lodash2.default.keyBy) { + _lodash2.default.keyBy = _lodash2.default.indexBy; +} diff --git a/dist/test/datasource-zabbix/zabbixDBConnector.js b/dist/test/datasource-zabbix/zabbixDBConnector.js new file mode 100644 index 0000000..49eb5d0 --- /dev/null +++ b/dist/test/datasource-zabbix/zabbixDBConnector.js @@ -0,0 +1,287 @@ +'use strict'; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _angular = require('angular'); + +var _angular2 = _interopRequireDefault(_angular); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var DEFAULT_QUERY_LIMIT = 10000; +var HISTORY_TO_TABLE_MAP = { + '0': 'history', + '1': 'history_str', + '2': 'history_log', + '3': 'history_uint', + '4': 'history_text' +}; + +var TREND_TO_TABLE_MAP = { + '0': 'trends', + '3': 'trends_uint' +}; + +var consolidateByFunc = { + 'avg': 'AVG', + 'min': 'MIN', + 'max': 'MAX', + 'sum': 'SUM', + 'count': 'COUNT' +}; + +var consolidateByTrendColumns = { + 'avg': 'value_avg', + 'min': 'value_min', + 'max': 'value_max' +}; + +/** @ngInject */ +function ZabbixDBConnectorFactory(datasourceSrv, backendSrv) { + var ZabbixDBConnector = function () { + function ZabbixDBConnector(sqlDataSourceId) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + _classCallCheck(this, ZabbixDBConnector); + + var limit = options.limit; + + + this.sqlDataSourceId = sqlDataSourceId; + this.limit = limit || DEFAULT_QUERY_LIMIT; + + this.loadSQLDataSource(sqlDataSourceId); + } + + /** + * Try to load DS with given id to check it's exist. + * @param {*} datasourceId ID of SQL data source + */ + + + _createClass(ZabbixDBConnector, [{ + key: 'loadSQLDataSource', + value: function loadSQLDataSource(datasourceId) { + var _this = this; + + var ds = _lodash2.default.find(datasourceSrv.getAll(), { 'id': datasourceId }); + if (ds) { + return datasourceSrv.loadDatasource(ds.name).then(function (ds) { + _this.sqlDataSourceType = ds.meta.id; + return ds; + }); + } else { + return Promise.reject('SQL Data Source with ID ' + datasourceId + ' not found'); + } + } + + /** + * Try to invoke test query for one of Zabbix database tables. + */ + + }, { + key: 'testSQLDataSource', + value: function testSQLDataSource() { + var testQuery = TEST_MYSQL_QUERY; + if (this.sqlDataSourceType === 'postgres') { + testQuery = TEST_POSTGRES_QUERY; + } + return this.invokeSQLQuery(testQuery); + } + }, { + key: 'getHistory', + value: function getHistory(items, timeFrom, timeTill, options) { + var _this2 = this; + + var intervalMs = options.intervalMs, + consolidateBy = options.consolidateBy; + + var intervalSec = Math.ceil(intervalMs / 1000); + + consolidateBy = consolidateBy || 'avg'; + var aggFunction = consolidateByFunc[consolidateBy]; + + // Group items by value type and perform request for each value type + var grouped_items = _lodash2.default.groupBy(items, 'value_type'); + var promises = _lodash2.default.map(grouped_items, function (items, value_type) { + var itemids = _lodash2.default.map(items, 'itemid').join(', '); + var table = HISTORY_TO_TABLE_MAP[value_type]; + + var dialect = _this2.sqlDataSourceType; + var query = buildSQLHistoryQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction, dialect); + + query = compactSQLQuery(query); + return _this2.invokeSQLQuery(query); + }); + + return Promise.all(promises).then(function (results) { + return _lodash2.default.flatten(results); + }); + } + }, { + key: 'getTrends', + value: function getTrends(items, timeFrom, timeTill, options) { + var _this3 = this; + + var intervalMs = options.intervalMs, + consolidateBy = options.consolidateBy; + + var intervalSec = Math.ceil(intervalMs / 1000); + + consolidateBy = consolidateBy || 'avg'; + var aggFunction = consolidateByFunc[consolidateBy]; + + // Group items by value type and perform request for each value type + var grouped_items = _lodash2.default.groupBy(items, 'value_type'); + var promises = _lodash2.default.map(grouped_items, function (items, value_type) { + var itemids = _lodash2.default.map(items, 'itemid').join(', '); + var table = TREND_TO_TABLE_MAP[value_type]; + var valueColumn = _lodash2.default.includes(['avg', 'min', 'max'], consolidateBy) ? consolidateBy : 'avg'; + valueColumn = consolidateByTrendColumns[valueColumn]; + + var dialect = _this3.sqlDataSourceType; + var query = buildSQLTrendsQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction, valueColumn, dialect); + + query = compactSQLQuery(query); + return _this3.invokeSQLQuery(query); + }); + + return Promise.all(promises).then(function (results) { + return _lodash2.default.flatten(results); + }); + } + }, { + key: 'handleGrafanaTSResponse', + value: function handleGrafanaTSResponse(history, items) { + var addHostName = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; + + return convertGrafanaTSResponse(history, items, addHostName); + } + }, { + key: 'invokeSQLQuery', + value: function invokeSQLQuery(query) { + var queryDef = { + refId: 'A', + format: 'time_series', + datasourceId: this.sqlDataSourceId, + rawSql: query, + maxDataPoints: this.limit + }; + + return backendSrv.datasourceRequest({ + url: '/api/tsdb/query', + method: 'POST', + data: { + queries: [queryDef] + } + }).then(function (response) { + var results = response.data.results; + if (results['A']) { + return results['A'].series; + } else { + return null; + } + }); + } + }]); + + return ZabbixDBConnector; + }(); + + return ZabbixDBConnector; +} + +_angular2.default.module('grafana.services').factory('ZabbixDBConnector', ZabbixDBConnectorFactory); + +/////////////////////////////////////////////////////////////////////////////// + +function convertGrafanaTSResponse(time_series, items, addHostName) { + var hosts = _lodash2.default.uniqBy(_lodash2.default.flatten(_lodash2.default.map(items, 'hosts')), 'hostid'); //uniqBy is needed to deduplicate + var grafanaSeries = _lodash2.default.map(time_series, function (series) { + var itemid = series.name; + var item = _lodash2.default.find(items, { 'itemid': itemid }); + var alias = item.name; + if (_lodash2.default.keys(hosts).length > 1 && addHostName) { + //only when actual multi hosts selected + var host = _lodash2.default.find(hosts, { 'hostid': item.hostid }); + alias = host.name + ": " + alias; + } + // zabbixCachingProxy deduplicates requests and returns one time series for equal queries. + // Clone is needed to prevent changing of series object shared between all targets. + var datapoints = _lodash2.default.cloneDeep(series.points); + return { + target: alias, + datapoints: datapoints + }; + }); + + return _lodash2.default.sortBy(grafanaSeries, 'target'); +} + +function compactSQLQuery(query) { + return query.replace(/\s+/g, ' '); +} + +function buildSQLHistoryQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction) { + var dialect = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : 'mysql'; + + if (dialect === 'postgres') { + return buildPostgresHistoryQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction); + } else { + return buildMysqlHistoryQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction); + } +} + +function buildSQLTrendsQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction, valueColumn) { + var dialect = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : 'mysql'; + + if (dialect === 'postgres') { + return buildPostgresTrendsQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction, valueColumn); + } else { + return buildMysqlTrendsQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction, valueColumn); + } +} + +/////////// +// MySQL // +/////////// + +function buildMysqlHistoryQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction) { + var time_expression = 'clock DIV ' + intervalSec + ' * ' + intervalSec; + var query = '\n SELECT itemid AS metric, ' + time_expression + ' AS time_sec, ' + aggFunction + '(value) AS value\n FROM ' + table + '\n WHERE itemid IN (' + itemids + ')\n AND clock > ' + timeFrom + ' AND clock < ' + timeTill + '\n GROUP BY ' + time_expression + ', metric\n ORDER BY time_sec ASC\n '; + return query; +} + +function buildMysqlTrendsQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction, valueColumn) { + var time_expression = 'clock DIV ' + intervalSec + ' * ' + intervalSec; + var query = '\n SELECT itemid AS metric, ' + time_expression + ' AS time_sec, ' + aggFunction + '(' + valueColumn + ') AS value\n FROM ' + table + '\n WHERE itemid IN (' + itemids + ')\n AND clock > ' + timeFrom + ' AND clock < ' + timeTill + '\n GROUP BY ' + time_expression + ', metric\n ORDER BY time_sec ASC\n '; + return query; +} + +var TEST_MYSQL_QUERY = 'SELECT itemid AS metric, clock AS time_sec, value_avg AS value FROM trends_uint LIMIT 1'; + +//////////////// +// PostgreSQL // +//////////////// + +var itemid_format = 'FM99999999999999999999'; + +function buildPostgresHistoryQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction) { + var time_expression = 'clock / ' + intervalSec + ' * ' + intervalSec; + var query = '\n SELECT to_char(itemid, \'' + itemid_format + '\') AS metric, ' + time_expression + ' AS time, ' + aggFunction + '(value) AS value\n FROM ' + table + '\n WHERE itemid IN (' + itemids + ')\n AND clock > ' + timeFrom + ' AND clock < ' + timeTill + '\n GROUP BY 1, 2\n ORDER BY time ASC\n '; + return query; +} + +function buildPostgresTrendsQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction, valueColumn) { + var time_expression = 'clock / ' + intervalSec + ' * ' + intervalSec; + var query = '\n SELECT to_char(itemid, \'' + itemid_format + '\') AS metric, ' + time_expression + ' AS time, ' + aggFunction + '(' + valueColumn + ') AS value\n FROM ' + table + '\n WHERE itemid IN (' + itemids + ')\n AND clock > ' + timeFrom + ' AND clock < ' + timeTill + '\n GROUP BY 1, 2\n ORDER BY time ASC\n '; + return query; +} + +var TEST_POSTGRES_QUERY = '\n SELECT to_char(itemid, \'' + itemid_format + '\') AS metric, clock AS time, value_avg AS value\n FROM trends_uint LIMIT 1\n'; diff --git a/dist/test/module.js b/dist/test/module.js new file mode 100644 index 0000000..ff7f441 --- /dev/null +++ b/dist/test/module.js @@ -0,0 +1,17 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.ConfigCtrl = undefined; + +var _config = require('./components/config'); + +var _sdk = require('app/plugins/sdk'); + +(0, _sdk.loadPluginCss)({ + dark: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.dark.css', + light: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.light.css' +}); + +exports.ConfigCtrl = _config.ZabbixAppConfigCtrl; diff --git a/dist/test/panel-triggers/ack-tooltip.directive.js b/dist/test/panel-triggers/ack-tooltip.directive.js new file mode 100644 index 0000000..59e9c8d --- /dev/null +++ b/dist/test/panel-triggers/ack-tooltip.directive.js @@ -0,0 +1,129 @@ +'use strict'; + +var _angular = require('angular'); + +var _angular2 = _interopRequireDefault(_angular); + +var _jquery = require('jquery'); + +var _jquery2 = _interopRequireDefault(_jquery); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +System.config({ + paths: { + tether: System.getConfig().baseURL + "plugins/alexanderzobnin-zabbix-app/vendor/npm/tether.min.js" + } +}); + +var Drop = void 0; +System.amdRequire(["plugins/alexanderzobnin-zabbix-app/vendor/npm/drop.min.js"], function (drop) { + Drop = drop; +}); + +/** @ngInject */ +_angular2.default.module('grafana.directives').directive('ackTooltip', function ($sanitize, $compile) { + var buttonTemplate = ''; + + return { + scope: { + ack: "=", + trigger: "=", + onAck: "=", + context: "=" + }, + link: function link(scope, element) { + var acknowledges = scope.ack; + var $button = (0, _jquery2.default)(buttonTemplate); + $button.appendTo(element); + + $button.click(function () { + var tooltip = '
'; + + if (acknowledges && acknowledges.length) { + tooltip += '' + '' + '' + '' + ''; + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = acknowledges[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var ack = _step.value; + + tooltip += '' + '' + ''; + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + tooltip += '
TimeUserComments
' + ack.time + '' + ack.user + '' + ack.message + '
'; + } else { + tooltip += 'Add acknowledge'; + } + + var addAckButtonTemplate = '
' + '
'; + tooltip += addAckButtonTemplate; + tooltip += '
'; + + var 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); + + (0, _jquery2.default)('#add-acknowledge-btn').on('click', onAddAckButtonClick); + + function onAddAckButtonClick() { + var inputTemplate = '
' + '' + '' + '
'; + + var $input = (0, _jquery2.default)(inputTemplate); + var $addAckButton = (0, _jquery2.default)('.ack-tooltip .ack-add-button'); + $addAckButton.replaceWith($input); + (0, _jquery2.default)('.ack-tooltip #cancel-ack-button').on('click', onAckCancelButtonClick); + (0, _jquery2.default)('.ack-tooltip #send-ack-button').on('click', onAckSendlButtonClick); + } + + function onAckCancelButtonClick() { + (0, _jquery2.default)('.ack-tooltip .ack-input-group').replaceWith(addAckButtonTemplate); + (0, _jquery2.default)('#add-acknowledge-btn').on('click', onAddAckButtonClick); + } + + function onAckSendlButtonClick() { + var message = (0, _jquery2.default)('.ack-tooltip #ack-message')[0].value; + var onAck = scope.onAck.bind(scope.context); + onAck(scope.trigger, message).then(function () { + closeDrop(); + }); + } + + function closeDrop() { + setTimeout(function () { + drop.destroy(); + }); + } + }); + + $compile(element.contents())(scope); + } + }; +}); diff --git a/dist/test/panel-triggers/editor.js b/dist/test/panel-triggers/editor.js new file mode 100644 index 0000000..1403a32 --- /dev/null +++ b/dist/test/panel-triggers/editor.js @@ -0,0 +1,224 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /** + * Grafana-Zabbix + * Zabbix plugin for Grafana. + * http://github.com/alexanderzobnin/grafana-zabbix + * + * Trigger panel. + * This feature sponsored by CORE IT + * http://www.coreit.fr + * + * Copyright 2015 Alexander Zobnin alexanderzobnin@gmail.com + * Licensed under the Apache License, Version 2.0 + */ + +exports.triggerPanelEditor = triggerPanelEditor; + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _utils = require('../datasource-zabbix/utils'); + +var utils = _interopRequireWildcard(_utils); + +require('../datasource-zabbix/css/query-editor.css!'); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var TriggerPanelEditorCtrl = function () { + + /** @ngInject */ + function TriggerPanelEditorCtrl($scope, $rootScope, uiSegmentSrv, datasourceSrv, templateSrv, popoverSrv) { + var _this = this; + + _classCallCheck(this, TriggerPanelEditorCtrl); + + $scope.editor = this; + this.panelCtrl = $scope.ctrl; + this.panel = this.panelCtrl.panel; + + this.datasourceSrv = datasourceSrv; + this.templateSrv = templateSrv; + this.popoverSrv = popoverSrv; + + // Map functions for bs-typeahead + this.getGroupNames = _lodash2.default.partial(getMetricNames, this, 'groupList'); + this.getHostNames = _lodash2.default.partial(getMetricNames, this, 'hostList'); + this.getApplicationNames = _lodash2.default.partial(getMetricNames, this, 'appList'); + + // Update metric suggestion when template variable was changed + $rootScope.$on('template-variable-value-updated', function () { + return _this.onVariableChange(); + }); + + this.fontSizes = ['80%', '90%', '100%', '110%', '120%', '130%', '150%', '160%', '180%', '200%', '220%', '250%']; + this.ackFilters = ['all triggers', 'unacknowledged', 'acknowledged']; + this.sortByFields = [{ text: 'last change', value: 'lastchange' }, { text: 'severity', value: 'priority' }]; + this.showEventsFields = [{ text: 'All', value: [0, 1] }, { text: 'OK', value: [0] }, { text: 'Problems', value: 1 }]; + + // Load scope defaults + var scopeDefaults = { + metric: {}, + inputStyles: {}, + oldTarget: _lodash2.default.cloneDeep(this.panel.triggers) + }; + _lodash2.default.defaults(this, scopeDefaults); + + // Set default datasource + this.datasources = _lodash2.default.map(this.getZabbixDataSources(), 'name'); + if (!this.panel.datasource) { + this.panel.datasource = this.datasources[0]; + } + // Load datasource + this.datasourceSrv.get(this.panel.datasource).then(function (datasource) { + _this.datasource = datasource; + _this.zabbix = datasource.zabbix; + _this.queryBuilder = datasource.queryBuilder; + _this.initFilters(); + _this.panelCtrl.refresh(); + }); + } + + _createClass(TriggerPanelEditorCtrl, [{ + key: 'initFilters', + value: function initFilters() { + return Promise.all([this.suggestGroups(), this.suggestHosts(), this.suggestApps()]); + } + }, { + key: 'suggestGroups', + value: function suggestGroups() { + var _this2 = this; + + return this.zabbix.getAllGroups().then(function (groups) { + _this2.metric.groupList = groups; + return groups; + }); + } + }, { + key: 'suggestHosts', + value: function suggestHosts() { + var _this3 = this; + + var groupFilter = this.datasource.replaceTemplateVars(this.panel.triggers.group.filter); + return this.zabbix.getAllHosts(groupFilter).then(function (hosts) { + _this3.metric.hostList = hosts; + return hosts; + }); + } + }, { + key: 'suggestApps', + value: function suggestApps() { + var _this4 = this; + + var groupFilter = this.datasource.replaceTemplateVars(this.panel.triggers.group.filter); + var hostFilter = this.datasource.replaceTemplateVars(this.panel.triggers.host.filter); + return this.zabbix.getAllApps(groupFilter, hostFilter).then(function (apps) { + _this4.metric.appList = apps; + return apps; + }); + } + }, { + key: 'onVariableChange', + value: function onVariableChange() { + if (this.isContainsVariables()) { + this.targetChanged(); + } + } + + /** + * Check query for template variables + */ + + }, { + key: 'isContainsVariables', + value: function isContainsVariables() { + var _this5 = this; + + return _lodash2.default.some(['group', 'host', 'application'], function (field) { + return utils.isTemplateVariable(_this5.panel.triggers[field].filter, _this5.templateSrv.variables); + }); + } + }, { + key: 'targetChanged', + value: function targetChanged() { + this.initFilters(); + this.panelCtrl.refresh(); + } + }, { + key: 'parseTarget', + value: function parseTarget() { + this.initFilters(); + var newTarget = _lodash2.default.cloneDeep(this.panel.triggers); + if (!_lodash2.default.isEqual(this.oldTarget, this.panel.triggers)) { + this.oldTarget = newTarget; + this.panelCtrl.refresh(); + } + } + }, { + key: 'refreshTriggerSeverity', + value: function refreshTriggerSeverity() { + _lodash2.default.each(this.triggerList, function (trigger) { + trigger.color = this.panel.triggerSeverity[trigger.priority].color; + trigger.severity = this.panel.triggerSeverity[trigger.priority].severity; + }); + this.panelCtrl.refresh(); + } + }, { + key: 'datasourceChanged', + value: function datasourceChanged() { + this.panelCtrl.refresh(); + } + }, { + key: 'changeTriggerSeverityColor', + value: function changeTriggerSeverityColor(trigger, color) { + this.panel.triggerSeverity[trigger.priority].color = color; + this.refreshTriggerSeverity(); + } + }, { + key: 'isRegex', + value: function isRegex(str) { + return utils.isRegex(str); + } + }, { + key: 'isVariable', + value: function isVariable(str) { + return utils.isTemplateVariable(str, this.templateSrv.variables); + } + }, { + key: 'getZabbixDataSources', + value: function getZabbixDataSources() { + var ZABBIX_DS_ID = 'alexanderzobnin-zabbix-datasource'; + return _lodash2.default.filter(this.datasourceSrv.getMetricSources(), function (datasource) { + return datasource.meta.id === ZABBIX_DS_ID && datasource.value; + }); + } + }]); + + return TriggerPanelEditorCtrl; +}(); + +// Get list of metric names for bs-typeahead directive + + +function getMetricNames(scope, metricList) { + return _lodash2.default.uniq(_lodash2.default.map(scope.metric[metricList], 'name')); +} + +function triggerPanelEditor() { + return { + restrict: 'E', + scope: true, + templateUrl: 'public/plugins/alexanderzobnin-zabbix-app/panel-triggers/editor.html', + controller: TriggerPanelEditorCtrl + }; +} diff --git a/dist/test/panel-triggers/module.js b/dist/test/panel-triggers/module.js new file mode 100644 index 0000000..e22f1ca --- /dev/null +++ b/dist/test/panel-triggers/module.js @@ -0,0 +1,425 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.PanelCtrl = exports.TriggerPanelCtrl = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _jquery = require('jquery'); + +var _jquery2 = _interopRequireDefault(_jquery); + +var _moment = require('moment'); + +var _moment2 = _interopRequireDefault(_moment); + +var _sdk = require('app/plugins/sdk'); + +var _utils = require('../datasource-zabbix/utils'); + +var utils = _interopRequireWildcard(_utils); + +var _editor = require('./editor'); + +require('./ack-tooltip.directive'); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /** + * Grafana-Zabbix + * Zabbix plugin for Grafana. + * http://github.com/alexanderzobnin/grafana-zabbix + * + * Trigger panel. + * This feature sponsored by CORE IT + * http://www.coreit.fr + * + * Copyright 2015 Alexander Zobnin alexanderzobnin@gmail.com + * Licensed under the Apache License, Version 2.0 + */ + +(0, _sdk.loadPluginCss)({ + dark: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.dark.css', + light: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.light.css' +}); + +var defaultSeverity = [{ priority: 0, severity: 'Not classified', color: '#B7DBAB', show: true }, { priority: 1, severity: 'Information', color: '#82B5D8', show: true }, { priority: 2, severity: 'Warning', color: '#E5AC0E', show: true }, { priority: 3, severity: 'Average', color: '#C15C17', show: true }, { priority: 4, severity: 'High', color: '#BF1B00', show: true }, { priority: 5, severity: 'Disaster', color: '#890F02', show: true }]; + +var panelDefaults = { + datasource: null, + triggers: { + group: { filter: "" }, + host: { filter: "" }, + application: { filter: "" }, + trigger: { filter: "" } + }, + hostField: true, + statusField: false, + severityField: false, + lastChangeField: true, + ageField: true, + infoField: true, + limit: 10, + showTriggers: 'all triggers', + hideHostsInMaintenance: false, + sortTriggersBy: { text: 'last change', value: 'lastchange' }, + showEvents: { text: 'Problems', value: '1' }, + triggerSeverity: defaultSeverity, + okEventColor: 'rgba(0, 245, 153, 0.45)', + ackEventColor: 'rgba(0, 0, 0, 0)', + scroll: true, + pageSize: 10, + fontSize: '100%' +}; + +var triggerStatusMap = { + '0': 'OK', + '1': 'Problem' +}; + +var defaultTimeFormat = "DD MMM YYYY HH:mm:ss"; + +var TriggerPanelCtrl = function (_PanelCtrl) { + _inherits(TriggerPanelCtrl, _PanelCtrl); + + /** @ngInject */ + function TriggerPanelCtrl($scope, $injector, $element, datasourceSrv, templateSrv, contextSrv, dashboardSrv) { + _classCallCheck(this, TriggerPanelCtrl); + + var _this = _possibleConstructorReturn(this, (TriggerPanelCtrl.__proto__ || Object.getPrototypeOf(TriggerPanelCtrl)).call(this, $scope, $injector)); + + _this.datasourceSrv = datasourceSrv; + _this.templateSrv = templateSrv; + _this.contextSrv = contextSrv; + _this.dashboardSrv = dashboardSrv; + + _this.triggerStatusMap = triggerStatusMap; + _this.defaultTimeFormat = defaultTimeFormat; + _this.pageIndex = 0; + _this.triggerList = []; + _this.currentTriggersPage = []; + + // Load panel defaults + // _.cloneDeep() need for prevent changing shared defaultSeverity. + // Load object "by value" istead "by reference". + _lodash2.default.defaults(_this.panel, _lodash2.default.cloneDeep(panelDefaults)); + + _this.events.on('init-edit-mode', _this.onInitEditMode.bind(_this)); + _this.events.on('refresh', _this.onRefresh.bind(_this)); + return _this; + } + + _createClass(TriggerPanelCtrl, [{ + key: 'onInitEditMode', + value: function onInitEditMode() { + this.addEditorTab('Options', _editor.triggerPanelEditor, 2); + } + }, { + key: 'onRefresh', + value: function onRefresh() { + var _this2 = this; + + // ignore fetching data if another panel is in fullscreen + if (this.otherPanelInFullscreenMode()) { + return; + } + + // clear loading/error state + delete this.error; + this.loading = true; + + return this.refreshData().then(function (triggerList) { + // Limit triggers number + _this2.triggerList = triggerList.slice(0, _this2.panel.limit); + + _this2.getCurrentTriggersPage(); + + // Notify panel that request is finished + _this2.loading = false; + + _this2.render(_this2.triggerList); + }); + } + }, { + key: 'refreshData', + value: function refreshData() { + return this.getTriggers().then(this.getAcknowledges.bind(this)).then(this.filterTriggers.bind(this)); + } + }, { + key: 'getTriggers', + value: function getTriggers() { + var _this3 = this; + + return this.datasourceSrv.get(this.panel.datasource).then(function (datasource) { + var zabbix = datasource.zabbix; + _this3.zabbix = zabbix; + _this3.datasource = datasource; + var showEvents = _this3.panel.showEvents.value; + var triggerFilter = _this3.panel.triggers; + var hideHostsInMaintenance = _this3.panel.hideHostsInMaintenance; + + // Replace template variables + var groupFilter = datasource.replaceTemplateVars(triggerFilter.group.filter); + var hostFilter = datasource.replaceTemplateVars(triggerFilter.host.filter); + var appFilter = datasource.replaceTemplateVars(triggerFilter.application.filter); + + var triggersOptions = { + showTriggers: showEvents, + hideHostsInMaintenance: hideHostsInMaintenance + }; + + return zabbix.getTriggers(groupFilter, hostFilter, appFilter, triggersOptions); + }).then(function (triggers) { + return _lodash2.default.map(triggers, _this3.formatTrigger.bind(_this3)); + }); + } + }, { + key: 'getAcknowledges', + value: function getAcknowledges(triggerList) { + var _this4 = this; + + // Request acknowledges for trigger + var eventids = _lodash2.default.map(triggerList, function (trigger) { + return trigger.lastEvent.eventid; + }); + + return this.zabbix.getAcknowledges(eventids).then(function (events) { + + // Map events to triggers + _lodash2.default.each(triggerList, function (trigger) { + var event = _lodash2.default.find(events, function (event) { + return event.eventid === trigger.lastEvent.eventid; + }); + + if (event) { + trigger.acknowledges = _lodash2.default.map(event.acknowledges, function (ack) { + var timestamp = _moment2.default.unix(ack.clock); + if (_this4.panel.customLastChangeFormat) { + ack.time = timestamp.format(_this4.panel.lastChangeFormat); + } else { + ack.time = timestamp.format(_this4.defaultTimeFormat); + } + ack.user = ack.alias + ' (' + ack.name + ' ' + ack.surname + ')'; + return ack; + }); + + // Mark acknowledged triggers with different color + if (_this4.panel.markAckEvents && trigger.acknowledges.length) { + trigger.color = _this4.panel.ackEventColor; + } + } + }); + + return triggerList; + }); + } + }, { + key: 'filterTriggers', + value: function filterTriggers(triggerList) { + var _this5 = this; + + // Filter triggers by description + var triggerFilter = this.panel.triggers.trigger.filter; + triggerFilter = this.datasource.replaceTemplateVars(triggerFilter); + if (triggerFilter) { + triggerList = _filterTriggers(triggerList, triggerFilter); + } + + // Filter acknowledged triggers + if (this.panel.showTriggers === 'unacknowledged') { + triggerList = _lodash2.default.filter(triggerList, function (trigger) { + return !trigger.acknowledges; + }); + } else if (this.panel.showTriggers === 'acknowledged') { + triggerList = _lodash2.default.filter(triggerList, 'acknowledges'); + } else { + triggerList = triggerList; + } + + // Filter triggers by severity + triggerList = _lodash2.default.filter(triggerList, function (trigger) { + return _this5.panel.triggerSeverity[trigger.priority].show; + }); + + // Sort triggers + if (this.panel.sortTriggersBy.value === 'priority') { + triggerList = _lodash2.default.sortBy(triggerList, 'priority').reverse(); + } else { + triggerList = _lodash2.default.sortBy(triggerList, 'lastchangeUnix').reverse(); + } + + return triggerList; + } + }, { + key: 'formatTrigger', + value: function formatTrigger(trigger) { + var triggerObj = trigger; + + // Format last change and age + trigger.lastchangeUnix = Number(trigger.lastchange); + var timestamp = _moment2.default.unix(trigger.lastchangeUnix); + if (this.panel.customLastChangeFormat) { + // User defined format + triggerObj.lastchange = timestamp.format(this.panel.lastChangeFormat); + } else { + triggerObj.lastchange = timestamp.format(this.defaultTimeFormat); + } + triggerObj.age = timestamp.fromNow(true); + + // Set host that the trigger belongs + if (trigger.hosts.length) { + triggerObj.host = trigger.hosts[0].name; + triggerObj.hostTechName = trigger.hosts[0].host; + } + + // Set color + if (trigger.value === '1') { + // Problem state + triggerObj.color = this.panel.triggerSeverity[trigger.priority].color; + } else { + // OK state + triggerObj.color = this.panel.okEventColor; + } + + triggerObj.severity = this.panel.triggerSeverity[trigger.priority].severity; + return triggerObj; + } + }, { + key: 'switchComment', + value: function switchComment(trigger) { + trigger.showComment = !trigger.showComment; + } + }, { + key: 'acknowledgeTrigger', + value: function acknowledgeTrigger(trigger, message) { + var eventid = trigger.lastEvent.eventid; + var grafana_user = this.contextSrv.user.name; + var ack_message = grafana_user + ' (Grafana): ' + message; + return this.datasourceSrv.get(this.panel.datasource).then(function (datasource) { + var zabbixAPI = datasource.zabbix.zabbixAPI; + return zabbixAPI.acknowledgeEvent(eventid, ack_message); + }).then(this.onRefresh.bind(this)); + } + }, { + key: 'getCurrentTriggersPage', + value: function getCurrentTriggersPage() { + var pageSize = this.panel.pageSize || 10; + var startPos = this.pageIndex * pageSize; + var endPos = Math.min(startPos + pageSize, this.triggerList.length); + this.currentTriggersPage = this.triggerList.slice(startPos, endPos); + return this.currentTriggersPage; + } + }, { + key: 'link', + value: function link(scope, elem, attrs, ctrl) { + var data; + var panel = ctrl.panel; + var pageCount = 0; + data = ctrl.triggerList; + + function getTableHeight() { + var panelHeight = ctrl.height; + + if (pageCount > 1) { + panelHeight -= 26; + } + + return panelHeight - 31 + 'px'; + } + + function switchPage(e) { + var el = (0, _jquery2.default)(e.currentTarget); + ctrl.pageIndex = parseInt(el.text(), 10) - 1; + + var pageSize = ctrl.panel.pageSize || 10; + var startPos = ctrl.pageIndex * pageSize; + var endPos = Math.min(startPos + pageSize, ctrl.triggerList.length); + ctrl.currentTriggersPage = ctrl.triggerList.slice(startPos, endPos); + + scope.$apply(); + renderPanel(); + } + + function appendPaginationControls(footerElem) { + footerElem.empty(); + + var pageSize = ctrl.panel.pageSize || 5; + pageCount = Math.ceil(data.length / pageSize); + if (pageCount === 1) { + return; + } + + var startPage = Math.max(ctrl.pageIndex - 3, 0); + var endPage = Math.min(pageCount, startPage + 9); + + var paginationList = (0, _jquery2.default)('
    '); + + for (var i = startPage; i < endPage; i++) { + var activeClass = i === ctrl.pageIndex ? 'active' : ''; + var pageLinkElem = (0, _jquery2.default)('
  • ' + (i + 1) + '
  • '); + paginationList.append(pageLinkElem); + } + + footerElem.append(paginationList); + } + + function renderPanel() { + var panelElem = elem.parents('.panel'); + var rootElem = elem.find('.triggers-panel-scroll'); + var footerElem = elem.find('.triggers-panel-footer'); + + elem.css({ 'font-size': panel.fontSize }); + panelElem.addClass('triggers-panel-wrapper'); + appendPaginationControls(footerElem); + + rootElem.css({ 'max-height': panel.scroll ? getTableHeight() : '' }); + } + + elem.on('click', '.triggers-panel-page-link', switchPage); + + var unbindDestroy = scope.$on('$destroy', function () { + elem.off('click', '.triggers-panel-page-link'); + unbindDestroy(); + }); + + ctrl.events.on('render', function (renderData) { + data = renderData || data; + if (data) { + renderPanel(); + } + ctrl.renderingCompleted(); + }); + } + }]); + + return TriggerPanelCtrl; +}(_sdk.PanelCtrl); + +TriggerPanelCtrl.templateUrl = 'panel-triggers/module.html'; + +function _filterTriggers(triggers, triggerFilter) { + if (utils.isRegex(triggerFilter)) { + return _lodash2.default.filter(triggers, function (trigger) { + return utils.buildRegex(triggerFilter).test(trigger.description); + }); + } else { + return _lodash2.default.filter(triggers, function (trigger) { + return trigger.description === triggerFilter; + }); + } +} + +exports.TriggerPanelCtrl = TriggerPanelCtrl; +exports.PanelCtrl = TriggerPanelCtrl; diff --git a/dist/test/test-setup/cssStub.js b/dist/test/test-setup/cssStub.js new file mode 100644 index 0000000..1f5824a --- /dev/null +++ b/dist/test/test-setup/cssStub.js @@ -0,0 +1,3 @@ +"use strict"; + +module.exports = {}; diff --git a/dist/test/test-setup/jest-setup.js b/dist/test/test-setup/jest-setup.js new file mode 100644 index 0000000..31d1125 --- /dev/null +++ b/dist/test/test-setup/jest-setup.js @@ -0,0 +1,52 @@ +'use strict'; + +var _jsdom = require('jsdom'); + +// Mock Grafana modules that are not available outside of the core project +// Required for loading module.js +jest.mock('angular', function () { + return { + module: function module() { + return { + directive: function directive() {}, + service: function service() {}, + factory: function factory() {} + }; + } + }; +}, { virtual: true }); // JSHint options +/* globals global: false */ + +jest.mock('app/plugins/sdk', function () { + return { + QueryCtrl: null + }; +}, { virtual: true }); + +jest.mock('app/core/utils/datemath', function () { + var datemath = require('./modules/datemath'); + return { + parse: datemath.parse, + parseDateMath: datemath.parseDateMath, + isValid: datemath.isValid + }; +}, { virtual: true }); + +jest.mock('app/core/table_model', function () { + return {}; +}, { virtual: true }); + +jest.mock('./css/query-editor.css!', function () { + return ""; +}, { virtual: true }); + +jest.mock('jquery', function () { + return 'module not found'; +}, { virtual: true }); + +// Required for loading angularjs +var dom = new _jsdom.JSDOM(''); +// Setup jsdom +global.window = dom.window; +global.document = global.window.document; +global.Node = window.Node; diff --git a/dist/test/test-setup/modules/datemath.js b/dist/test/test-setup/modules/datemath.js new file mode 100644 index 0000000..063ad04 --- /dev/null +++ b/dist/test/test-setup/modules/datemath.js @@ -0,0 +1,135 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.parse = parse; +exports.isValid = isValid; +exports.parseDateMath = parseDateMath; + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _moment = require('moment'); + +var _moment2 = _interopRequireDefault(_moment); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var units = ['y', 'M', 'w', 'd', 'h', 'm', 's']; + +function parse(text, roundUp) { + if (!text) { + return undefined; + } + if (_moment2.default.isMoment(text)) { + return text; + } + if (_lodash2.default.isDate(text)) { + return (0, _moment2.default)(text); + } + + var time; + var mathString = ''; + var index; + var parseString; + + if (text.substring(0, 3) === 'now') { + time = (0, _moment2.default)(); + mathString = text.substring('now'.length); + } else { + index = text.indexOf('||'); + if (index === -1) { + parseString = text; + mathString = ''; // nothing else + } else { + parseString = text.substring(0, index); + mathString = text.substring(index + 2); + } + // We're going to just require ISO8601 timestamps, k? + time = (0, _moment2.default)(parseString, _moment2.default.ISO_8601); + } + + if (!mathString.length) { + return time; + } + + return parseDateMath(mathString, time, roundUp); +} + +function isValid(text) { + var date = parse(text); + if (!date) { + return false; + } + + if (_moment2.default.isMoment(date)) { + return date.isValid(); + } + + return false; +} + +function parseDateMath(mathString, time, roundUp) { + var dateTime = time; + var i = 0; + var len = mathString.length; + + while (i < len) { + var c = mathString.charAt(i++); + var type; + var num; + var unit; + + if (c === '/') { + type = 0; + } else if (c === '+') { + type = 1; + } else if (c === '-') { + type = 2; + } else { + return undefined; + } + + if (isNaN(mathString.charAt(i))) { + num = 1; + } else if (mathString.length === 2) { + num = mathString.charAt(i); + } else { + var numFrom = i; + while (!isNaN(mathString.charAt(i))) { + i++; + if (i > 10) { + return undefined; + } + } + num = parseInt(mathString.substring(numFrom, i), 10); + } + + if (type === 0) { + // rounding is only allowed on whole, single, units (eg M or 1M, not 0.5M or 2M) + if (num !== 1) { + return undefined; + } + } + unit = mathString.charAt(i++); + + if (!_lodash2.default.includes(units, unit)) { + return undefined; + } else { + if (type === 0) { + if (roundUp) { + dateTime.endOf(unit); + } else { + dateTime.startOf(unit); + } + } else if (type === 1) { + dateTime.add(num, unit); + } else if (type === 2) { + dateTime.subtract(num, unit); + } + } + } + return dateTime; +}