Commit 30b62e17 by Matt Page

Add an OpenTSDB datasource.

This adds support for querying OpenTSDB for metric data.
parent 336cf768
...@@ -10,4 +10,5 @@ define([ ...@@ -10,4 +10,5 @@ define([
'./graphiteImport', './graphiteImport',
'./influxTargetCtrl', './influxTargetCtrl',
'./playlistCtrl', './playlistCtrl',
], function () {}); './opentsdbTargetCtrl',
\ No newline at end of file ], function () {});
...@@ -64,4 +64,4 @@ function (angular) { ...@@ -64,4 +64,4 @@ function (angular) {
}); });
}); });
\ No newline at end of file
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;
}
});
});
<div class="editor-row" style="margin-top: 10px;">
<div ng-repeat="target in panel.targets"
class="grafana-target"
ng-class="{'grafana-target-hidden': target.hide}"
ng-controller="OpenTSDBTargetCtrl"
ng-init="init()">
<div class="grafana-target-inner-wrapper">
<div class="grafana-target-inner">
<ul class="grafana-target-controls">
<li class="dropdown">
<a class="pointer dropdown-toggle"
data-toggle="dropdown"
tabindex="1">
<i class="icon-cog"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem">
<a tabindex="1"
ng-click="duplicate()">
Duplicate
</a>
</li>
</ul>
</li>
<li>
<a class="pointer" tabindex="1" ng-click="removeTarget(target)">
<i class="icon-remove"></i>
</a>
</li>
</ul>
<ul class="grafana-target-controls-left">
<li>
<a class="grafana-target-segment"
ng-click="target.hide = !target.hide; get_data();"
role="menuitem">
<i class="icon-eye-open"></i>
</a>
</li>
</ul>
<ul class="grafana-segment-list" role="menu">
<li class="grafana-target-segment">
Metric:
<input type="text"
class="input-large grafana-target-segment-input"
ng-model="target.metric"
spellcheck='false'
bs-typeahead="suggestMetrics"
placeholder="metric name"
data-min-length=0 data-items=100
ng-blur="targetBlur()"
>
<a bs-tooltip="target.errors.metric"
style="color: rgb(229, 189, 28)"
ng-show="target.errors.metric">
<i class="icon-warning-sign"></i>
</a>
</li>
<li class="grafana-target-segment">
Aggregator:
<select ng-model="target.aggregator"
class="grafana-target-segment-input input-small"
ng-options="agg for agg in aggregators"
ng-change="targetBlur()"
>
<option value="">Pick one</option>
</select>
<a bs-tooltip="target.errors.aggregator"
style="color: rgb(229, 189, 28)"
ng-show="target.errors.aggregator">
<i class="icon-warning-sign"></i>
</a>
</li>
<li class="grafana-target-segment">
Compute Rate:
<input type="checkbox"
ng-model="target.shouldComputeRate"
ng-change="targetBlur()"
>
<div ng-hide="!target.shouldComputeRate">
Counter:
<input type="checkbox"
ng-disabled="!target.shouldComputeRate"
ng-model="target.isCounter"
ng-change="targetBlur()"
>
</div>
</li>
<li class="grafana-target-segment">
Downsample:
<input type="checkbox"
ng-model="target.shouldDownsample"
ng-change="targetBlur(target)"
>
<div ng-hide="!target.shouldDownsample">
<table>
<tr>
<td>
Interval:
</td>
<td>
<input type="text"
class="input-small"
ng-disabled="!target.shouldDownsample"
ng-model="target.downsampleInterval"
ng-change="targetBlur()">
</td>
<td>
<a bs-tooltip="target.errors.downsampleInterval"
style="color: rgb(229, 189, 28)"
ng-show="target.errors.downsampleInterval">
<i class="icon-warning-sign"></i>
</a>
</td>
</tr>
<tr>
<td>Aggregator:</td>
<td>
<select ng-model="target.downsampleAggregator"
class="grafana-target-segment-input input-small"
ng-options="agg for agg in aggregators"
ng-change="targetBlur()"
>
<option value="">Pick one</option>
</select>
</td>
<td>
<a bs-tooltip="target.errors.downsampleAggregator"
style="color: rgb(229, 189, 28)"
ng-show="target.errors.downsampleAggregator">
<i class="icon-warning-sign"></i>
</a>
</td>
</tr>
</table>
</div>
</li>
<li class="grafana-target-segment">
<div>
Tags:
<input type="text"
class="input-small grafana-target-segment-input"
spellcheck='false'
bs-typeahead="suggestTagKeys"
data-min-length=0 data-items=100
ng-model="target.currentTagKey"
placeholder="key">
<input type="text"
class="input-small grafana-target-segment-input"
spellcheck='false'
bs-typeahead="suggestTagValues"
data-min-length=0 data-items=100
ng-model="target.currentTagValue"
placeholder="value">
<a ng-click="addTag()">
<i class="icon-plus-sign"></i>
</a>
<a bs-tooltip="target.errors.tags"
style="color: rgb(229, 189, 28)"
ng-show="target.errors.tags">
<i class="icon-warning-sign"></i>
</a>
</div>
<table ng-hide="_.isEmpty(target.tags)">
<tr>
<th>Key</th>
<th>Value</td>
<tr ng-repeat="(key, value) in target.tags track by $index">
<td>{{ key }}</td>
<td>{{ value }}</td>
<td>
<a ng-click="removeTag(key)">
<i class="icon-remove"></i>
</a>
</td>
</tr>
</table>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>
</div>
...@@ -4,19 +4,18 @@ define([ ...@@ -4,19 +4,18 @@ define([
'config', 'config',
'./graphite/graphiteDatasource', './graphite/graphiteDatasource',
'./influxdb/influxdbDatasource', './influxdb/influxdbDatasource',
'./opentsdb/opentsdbDatasource',
], ],
function (angular, _, config) { function (angular, _, config) {
'use strict'; 'use strict';
var module = angular.module('kibana.services'); 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() { this.init = function() {
var defaultDatasource = _.findWhere(_.values(config.datasources), { default: true } ); var defaultDatasource = _.findWhere(_.values(config.datasources), { default: true } );
this.default = this.datasourceFactory(defaultDatasource); this.default = this.datasourceFactory(defaultDatasource);
}; };
this.datasourceFactory = function(ds) { this.datasourceFactory = function(ds) {
...@@ -25,6 +24,8 @@ function (angular, _, config) { ...@@ -25,6 +24,8 @@ function (angular, _, config) {
return new GraphiteDatasource(ds); return new GraphiteDatasource(ds);
case 'influxdb': case 'influxdb':
return new InfluxDatasource(ds); return new InfluxDatasource(ds);
case 'opentsdb':
return new OpenTSDBDatasource(ds);
} }
}; };
...@@ -50,4 +51,4 @@ function (angular, _, config) { ...@@ -50,4 +51,4 @@ function (angular, _, config) {
this.init(); this.init();
}); });
}); });
\ No newline at end of file
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;
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment