Commit 6885cea0 by Torkel Ödegaard

Improvement and polish to the OpenTSDB query editor (Issue #492)

parents 87a8b1b3 30b62e17
......@@ -14,6 +14,9 @@
- Improvement to InfluxDB query editor and function/value column selection (Issue #473)
- Initial support for filtering (templated queries) for InfluxDB (PR #375) - thx @mavimo
- Row editing and adding new panel is now a lot quicker and easier with the new row menu (Issue #475)
- New datasource! Initial support for OpenTSDB (PR #211) - thx @mpage
- Improvement and polish to the OpenTSDB query editor (Issue #492)
#### Changes
- Graphite panel is now renamed graph (Existing dashboards will still work)
......
......@@ -11,4 +11,5 @@ define([
'./influxTargetCtrl',
'./playlistCtrl',
'./inspectCtrl',
'./opentsdbTargetCtrl',
], function () {});
define([
'angular',
'underscore',
'kbn'
],
function (angular, _, kbn) {
'use strict';
var module = angular.module('kibana.controllers');
module.controller('OpenTSDBTargetCtrl', function($scope, $timeout) {
$scope.init = function() {
$scope.target.errors = validateTarget($scope.target);
$scope.aggregators = ['avg', 'sum', 'min', 'max', 'dev', 'zimsum', 'mimmin', 'mimmax'];
if (!$scope.target.aggregator) {
$scope.target.aggregator = 'sum';
}
if (!$scope.target.downsampleAggregator) {
$scope.target.downsampleAggregator = 'sum';
}
$scope.$on('typeahead-updated', function() {
$timeout($scope.targetBlur);
});
};
$scope.targetBlur = function() {
$scope.target.errors = validateTarget($scope.target);
// this does not work so good
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.addTagMode) {
$scope.addTagMode = true;
return;
}
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.addTagMode = false;
};
$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.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.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>
<input type="text"
class="input-xxlarge 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
<li>
<select ng-model="target.aggregator"
class="grafana-target-segment-input input-small"
ng-options="agg for agg in aggregators"
ng-change="targetBlur()">
</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">
Rate:
<input type="checkbox"
class="grafana-target-option-checkbox"
ng-model="target.shouldComputeRate"
ng-change="targetBlur()"
>
</li>
<li class="grafana-target-segment" ng-hide="!target.shouldComputeRate">
Counter:
<input type="checkbox"
class="grafana-target-option-checkbox"
ng-disabled="!target.shouldComputeRate"
ng-model="target.isCounter"
ng-change="targetBlur()">
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="grafana-target-inner">
<ul class="grafana-segment-list" role="menu">
<li class="grafana-target-segment">
Downsample:
<input type="checkbox"
class="grafana-target-option-checkbox"
ng-model="target.shouldDownsample"
ng-change="targetBlur(target)"
>
</li>
<li ng-hide="!target.shouldDownsample">
<input type="text"
class="input-small grafana-target-segment-input"
ng-disabled="!target.shouldDownsample"
ng-model="target.downsampleInterval"
ng-change="targetBlur()"
placeholder="interval"
>
</li>
<li class="grafana-target-segment" ng-hide="!target.shouldDownsample">
Aggregator
</li>
<li ng-hide="!target.shouldDownsample">
<select ng-model="target.downsampleAggregator"
class="grafana-target-segment-input input-small"
ng-options="agg for agg in aggregators"
ng-change="targetBlur()">
</select>
</li>
<li class="grafana-target-segment">
Tags:
</li>
<li ng-repeat="(key, value) in target.tags track by $index" class="grafana-target-segment">
{{key}}&nbsp;=&nbsp;{{value}}
<a ng-click="removeTag(key)">
<i class="icon-remove"></i>
</a>
</li>
<li class="grafana-target-segment" ng-hide="addTagMode">
<a ng-click="addTag()">
<i class="icon-plus-sign"></i>
</a>
</li>
<li ng-show="addTagMode">
<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>
</li>
</ul>
<!-- <ul class="grafana-segment-list" role="menu">
<li>
<input type="text"
class="input-xlarge 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>
<select ng-model="target.aggregator"
class="grafana-target-segment-input input-small"
ng-options="agg for agg in aggregators"
ng-change="targetBlur()">
</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">
Rate:
<input type="checkbox"
class="grafana-target-option-checkbox"
ng-model="target.shouldComputeRate"
ng-change="targetBlur()"
>
<span ng-hide="!target.shouldComputeRate">
Counter:
<input type="checkbox"
class="grafana-target-option-checkbox"
ng-disabled="!target.shouldComputeRate"
ng-model="target.isCounter"
ng-change="targetBlur()"
>
</span>
</li>
<li class="grafana-target-segment">
Downsample:
<input type="checkbox"
class="grafana-target-option-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">
Tags:
</li>
<li>
<div>
<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,13 +4,14 @@ define([
'config',
'./graphite/graphiteDatasource',
'./influxdb/influxdbDatasource',
'./opentsdb/opentsdbDatasource',
],
function (angular, _, config) {
'use strict';
var module = angular.module('kibana.services');
module.service('datasourceSrv', function($q, $http, GraphiteDatasource, InfluxDatasource) {
module.service('datasourceSrv', function($q, filterSrv, $http, GraphiteDatasource, InfluxDatasource, OpenTSDBDatasource) {
this.init = function() {
var defaultDatasource = _.findWhere(_.values(config.datasources), { default: true });
......@@ -23,6 +24,8 @@ function (angular, _, config) {
return new GraphiteDatasource(ds);
case 'influxdb':
return new InfluxDatasource(ds);
case 'opentsdb':
return new OpenTSDBDatasource(ds);
}
};
......
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(filterSrv, 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;
});
});
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -371,6 +371,10 @@ input[type=text].grafana-target-segment-input {
padding: 8px 4px;
}
input[type=checkbox].grafana-target-option-checkbox {
margin: 0;
}
select.grafana-target-segment-input {
border: none;
border-right: 1px solid @grafanaTargetSegmentBorder;
......
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