From 30b62e172df25bf27bc1d1e68c4ee21a64d3a1df Mon Sep 17 00:00:00 2001 From: Matt Page Date: Fri, 14 Mar 2014 12:14:16 -0700 Subject: [PATCH] Add an OpenTSDB datasource. This adds support for querying OpenTSDB for metric data. --- src/app/controllers/all.js | 3 +- src/app/controllers/influxTargetCtrl.js | 2 +- src/app/controllers/opentsdbTargetCtrl.js | 107 ++++++++++ src/app/partials/opentsdb/editor.html | 191 ++++++++++++++++++ src/app/services/datasourceSrv.js | 9 +- .../services/opentsdb/opentsdbDatasource.js | 155 ++++++++++++++ 6 files changed, 461 insertions(+), 6 deletions(-) create mode 100644 src/app/controllers/opentsdbTargetCtrl.js create mode 100644 src/app/partials/opentsdb/editor.html create mode 100644 src/app/services/opentsdb/opentsdbDatasource.js diff --git a/src/app/controllers/all.js b/src/app/controllers/all.js index 2626137657c..b3348bb0a58 100644 --- a/src/app/controllers/all.js +++ b/src/app/controllers/all.js @@ -10,4 +10,5 @@ define([ './graphiteImport', './influxTargetCtrl', './playlistCtrl', -], function () {}); \ No newline at end of file + './opentsdbTargetCtrl', +], function () {}); diff --git a/src/app/controllers/influxTargetCtrl.js b/src/app/controllers/influxTargetCtrl.js index 66439c5e757..467b135a961 100644 --- a/src/app/controllers/influxTargetCtrl.js +++ b/src/app/controllers/influxTargetCtrl.js @@ -64,4 +64,4 @@ function (angular) { }); -}); \ No newline at end of file +}); diff --git a/src/app/controllers/opentsdbTargetCtrl.js b/src/app/controllers/opentsdbTargetCtrl.js new file mode 100644 index 00000000000..ba66986532d --- /dev/null +++ b/src/app/controllers/opentsdbTargetCtrl.js @@ -0,0 +1,107 @@ +define([ + 'angular', + 'underscore', + 'kbn' +], +function (angular, _, kbn) { + 'use strict'; + + var module = angular.module('kibana.controllers'); + + module.controller('OpenTSDBTargetCtrl', function($scope) { + + $scope.init = function() { + $scope.target.errors = validateTarget($scope.target); + $scope.aggregators = ['avg', 'sum', 'min', 'max', 'dev', 'zimsum', 'mimmin', 'mimmax']; + }; + + $scope.targetBlur = function() { + $scope.target.errors = validateTarget($scope.target); + + if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) { + $scope.oldTarget = angular.copy($scope.target); + $scope.get_data(); + } + }; + + $scope.duplicate = function() { + var clone = angular.copy($scope.target); + $scope.panel.targets.push(clone); + }; + + $scope.suggestMetrics = function(query, callback) { + $scope.datasource + .performSuggestQuery(query, 'metrics') + .then(callback); + }; + + $scope.suggestTagKeys = function(query, callback) { + $scope.datasource + .performSuggestQuery(query, 'tagk') + .then(callback); + }; + + $scope.suggestTagValues = function(query, callback) { + $scope.datasource + .performSuggestQuery(query, 'tagv') + .then(callback); + }; + + $scope.addTag = function() { + if (!$scope.target.tags) { + $scope.target.tags = {}; + } + + $scope.target.errors = validateTarget($scope.target); + + if (!$scope.target.errors.tags) { + $scope.target.tags[$scope.target.currentTagKey] = $scope.target.currentTagValue; + $scope.target.currentTagKey = ''; + $scope.target.currentTagValue = ''; + $scope.targetBlur(); + } + }; + + $scope.removeTag = function(key) { + delete $scope.target.tags[key]; + $scope.targetBlur(); + }; + + function validateTarget(target) { + var errs = {}; + + if (!target.metric) { + errs.metric = "You must supply a metric name."; + } + + if (!target.aggregator) { + errs.aggregator = "You must choose an aggregation function."; + } + + if (target.shouldDownsample) { + try { + if (target.downsampleInterval) { + kbn.describe_interval(target.downsampleInterval); + } else { + errs.downsampleInterval = "You must supply a downsample interval (e.g. '1m' or '1h')."; + } + } catch(err) { + errs.downsampleInterval = err.message; + } + + if (!target.downsampleAggregator) { + errs.downsampleAggregator = "You must choose an aggregation function for downsampling."; + } + } + + if (target.tags && _.has(target.tags, target.currentTagKey)) { + errs.tags = "Duplicate tag key '" + target.currentTagKey + "'."; + } + + return errs; + } + + + }); + +}); diff --git a/src/app/partials/opentsdb/editor.html b/src/app/partials/opentsdb/editor.html new file mode 100644 index 00000000000..ebc0c6190b6 --- /dev/null +++ b/src/app/partials/opentsdb/editor.html @@ -0,0 +1,191 @@ +
+
+ +
+
+ + + + + + +
+
+
+
+
diff --git a/src/app/services/datasourceSrv.js b/src/app/services/datasourceSrv.js index d05bd11e278..7ad122539e9 100644 --- a/src/app/services/datasourceSrv.js +++ b/src/app/services/datasourceSrv.js @@ -4,19 +4,18 @@ define([ 'config', './graphite/graphiteDatasource', './influxdb/influxdbDatasource', + './opentsdb/opentsdbDatasource', ], function (angular, _, config) { 'use strict'; var module = angular.module('kibana.services'); - module.service('datasourceSrv', function($q, filterSrv, $http, GraphiteDatasource, InfluxDatasource) { + module.service('datasourceSrv', function($q, filterSrv, $http, GraphiteDatasource, InfluxDatasource, OpenTSDBDatasource) { this.init = function() { - var defaultDatasource = _.findWhere(_.values(config.datasources), { default: true } ); this.default = this.datasourceFactory(defaultDatasource); - }; this.datasourceFactory = function(ds) { @@ -25,6 +24,8 @@ function (angular, _, config) { return new GraphiteDatasource(ds); case 'influxdb': return new InfluxDatasource(ds); + case 'opentsdb': + return new OpenTSDBDatasource(ds); } }; @@ -50,4 +51,4 @@ function (angular, _, config) { this.init(); }); -}); \ No newline at end of file +}); diff --git a/src/app/services/opentsdb/opentsdbDatasource.js b/src/app/services/opentsdb/opentsdbDatasource.js new file mode 100644 index 00000000000..8766c224982 --- /dev/null +++ b/src/app/services/opentsdb/opentsdbDatasource.js @@ -0,0 +1,155 @@ +define([ + 'angular', + 'underscore', + 'kbn' +], +function (angular, _, kbn) { + 'use strict'; + + var module = angular.module('kibana.services'); + + module.factory('OpenTSDBDatasource', function($q, $http) { + + function OpenTSDBDatasource(datasource) { + this.type = 'opentsdb'; + this.editorSrc = 'app/partials/opentsdb/editor.html'; + this.url = datasource.url; + this.name = datasource.name; + } + + // Called once per panel (graph) + OpenTSDBDatasource.prototype.query = function(options) { + var start = convertToTSDBTime(options.range.from); + var end = convertToTSDBTime(options.range.to); + var queries = _.compact(_.map(options.targets, convertTargetToQuery)); + + // No valid targets, return the empty result to save a round trip. + if (_.isEmpty(queries)) { + var d = $q.defer(); + d.resolve({ data: [] }); + return d.promise; + } + + var groupByTags = {}; + _.each(queries, function(query) { + _.each(query.tags, function(val, key) { + if (val === "*") { + groupByTags[key] = true; + } + }); + }); + + return this.performTimeSeriesQuery(queries, start, end) + .then(function(response) { + var result = _.map(response.data, function(metricData) { + return transformMetricData(metricData, groupByTags); + }); + return { data: result }; + }); + }; + + OpenTSDBDatasource.prototype.performTimeSeriesQuery = function(queries, start, end) { + var reqBody = { + start: start, + queries: queries + }; + + // Relative queries (e.g. last hour) don't include an end time + if (end) { + reqBody.end = end; + } + + var options = { + method: 'POST', + url: this.url + '/api/query', + data: reqBody + }; + + return $http(options); + }; + + OpenTSDBDatasource.prototype.performSuggestQuery = function(query, type) { + var options = { + method: 'GET', + url: this.url + '/api/suggest', + params: { + type: type, + q: query + } + }; + return $http(options).then(function(result) { + return result.data; + }); + }; + + function transformMetricData(md, groupByTags) { + var dps = []; + + // TSDB returns datapoints has a hash of ts => value. + // Can't use _.pairs(invert()) because it stringifies keys/values + _.each(md.dps, function (v, k) { + dps.push([v, k]); + }); + + var target = md.metric; + if (!_.isEmpty(md.tags)) { + var tagData = []; + + _.each(_.pairs(md.tags), function(tag) { + if (_.has(groupByTags, tag[0])) { + tagData.push(tag[0] + "=" + tag[1]); + } + }); + + if (!_.isEmpty(tagData)) { + target = target + "{" + tagData.join(", ") + "}"; + } + } + + return { target: target, datapoints: dps }; + } + + function convertTargetToQuery(target) { + if (!target.metric) { + return null; + } + + var query = { + metric: target.metric, + aggregator: "avg" + }; + + if (target.aggregator) { + query.aggregator = target.aggregator; + } + + if (target.shouldComputeRate) { + query.rate = true; + query.rateOptions = { + counter: !!target.isCounter + }; + } + + if (target.shouldDownsample) { + query.downsample = target.downsampleInterval + "-" + target.downsampleAggregator; + } + + query.tags = angular.copy(target.tags); + + return query; + } + + function convertToTSDBTime(date) { + if (date === 'now') { + return null; + } + + date = kbn.parseDate(date); + + return date.getTime(); + } + + return OpenTSDBDatasource; + }); + +});