mirror of https://github.com/grafana/grafana.git
Improvement and polish to the OpenTSDB query editor (Issue #492)
This commit is contained in:
commit
6885cea0fd
|
|
@ -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 () {});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
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;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
<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}} = {{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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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(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;
|
||||
});
|
||||
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue