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;
+ });
+
+});