Merge branch 'master' into dashboard_loading_refactoring

This commit is contained in:
Torkel Ödegaard
2014-06-14 14:51:09 +02:00
16 changed files with 572 additions and 69 deletions
+4
View File
@@ -14,6 +14,10 @@
- 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)
- Influxdb group by support (Issue #441) thx @piis3
#### Changes
- Graphite panel is now renamed graph (Existing dashboards will still work)
+1 -1
View File
@@ -3,7 +3,7 @@
*/
require.config({
baseUrl: 'app',
// urlArgs: 'r=@REV@',
paths: {
config: ['../config', '../config.sample'],
settings: 'components/settings',
+1
View File
@@ -12,4 +12,5 @@ define([
'./influxTargetCtrl',
'./playlistCtrl',
'./inspectCtrl',
'./opentsdbTargetCtrl',
], function () {});
+118
View File
@@ -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;
}
});
});
+3 -3
View File
@@ -114,10 +114,10 @@ function (angular, _, $) {
$paramLink.appendTo(elem);
$input.appendTo(elem);
$input.blur(_.partial(inputBlur));
$input.blur(inputBlur);
$input.keyup(inputKeyDown);
$input.keypress(_.partial(inputKeyPress));
$paramLink.click(_.partial(clickFuncParam));
$input.keypress(inputKeyPress);
$paramLink.click(clickFuncParam);
addTypeahead($input);
+22 -6
View File
@@ -75,16 +75,13 @@
<li>
<a class="grafana-target-segment"
ng-click="target.condiction_filter = !target.condiction_filter; get_data();"
ng-click="target.condition_filter = !target.condition_filter; get_data();"
bs-tooltip="'Add a where clause'"
role="menuitem">
<i class="icon-filter"></i>
</a>
</li>
<li ng-show="target.condiction_filter">
<select class="input-small grafana-target-segment-input"
ng-change="get_data()"
ng-model="target.condition_add"
ng-options="f for f in ['and', 'or']" ></select>
<li ng-show="target.condition_filter">
<input type="text"
class="input-small grafana-target-segment-input"
ng-model="target.condition_key"
@@ -118,6 +115,25 @@
spellcheck='false'
ng-model-onblur ng-change="get_data()" >
</li>
<li>
<a class="grafana-target-segment"
ng-click="target.groupby_field_add = !target.groupby_field_add; get_data();"
bs-tooltip="'Add a group by column'"
role="menuitem">
<i class="icon-plus"></i>
</a>
</li>
<li ng-show="target.groupby_field_add">
<input type="text"
class="input-small grafana-target-segment-input"
ng-model="target.groupby_field"
placeholder="column"
spellcheck="false"
bs-typeahead="listColumns"
data-min-length=0
ng-blur="get_data()">
</li>
<li class="grafana-target-segment">
as
+176
View File
@@ -0,0 +1,176 @@
<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>
<div class="clearfix"></div>
</div>
</div>
</div>
</div>
+4 -1
View File
@@ -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);
}
};
+68 -29
View File
@@ -17,7 +17,6 @@ function (angular, _, kbn) {
this.username = datasource.username;
this.password = datasource.password;
this.name = datasource.name;
this.templateSettings = {
interpolate : /\[\[([\s\S]+?)\]\]/g,
};
@@ -33,6 +32,7 @@ function (angular, _, kbn) {
}
var timeFilter = getTimeFilter(options);
var groupByField;
if (target.rawQuery) {
query = target.query;
@@ -43,6 +43,10 @@ function (angular, _, kbn) {
var groupByIndex = lowerCaseQueryElements.indexOf("group");
var orderIndex = lowerCaseQueryElements.indexOf("order");
if (lowerCaseQueryElements[1].indexOf(',')) {
groupByField = lowerCaseQueryElements[1].replace(',', '');
}
if (whereIndex !== -1) {
queryElements.splice(whereIndex + 1, 0, timeFilter, "and");
}
@@ -63,9 +67,10 @@ function (angular, _, kbn) {
query = filterSrv.applyTemplateToTarget(query);
}
else {
var template = "select [[func]](\"[[column]]\") as \"[[column]]_[[func]]\" from \"[[series]]\" " +
var template = "select [[group]][[group_comma]] [[func]](\"[[column]]\") as \"[[column]]_[[func]]\" from \"[[series]]\" " +
"where [[timeFilter]] [[condition_add]] [[condition_key]] [[condition_op]] [[condition_value]] " +
"group by time([[interval]]) order asc";
"group by time([[interval]])[[group_comma]] [[group]] order asc";
var templateData = {
series: target.series,
@@ -73,10 +78,12 @@ function (angular, _, kbn) {
func: target.function,
timeFilter: timeFilter,
interval: target.interval || options.interval,
condition_add: target.condiction_filter ? target.condition_add : '',
condition_key: target.condiction_filter ? target.condition_key : '',
condition_op: target.condiction_filter ? target.condition_op : '',
condition_value: target.condiction_filter ? target.condition_value: ''
condition_add: target.condition_filter ? 'and' : '',
condition_key: target.condition_filter ? target.condition_key : '',
condition_op: target.condition_filter ? target.condition_op : '',
condition_value: target.condition_filter ? target.condition_value : '',
group_comma: target.groupby_field_add && target.groupby_field ? ',' : '',
group: target.groupby_field_add ? target.groupby_field : '',
};
query = _.template(template, templateData, this.templateSettings);
@@ -86,10 +93,15 @@ function (angular, _, kbn) {
alias = filterSrv.applyTemplateToTarget(target.alias);
}
if (target.groupby_field_add) {
groupByField = target.groupby_field;
}
target.query = query;
}
return this.doInfluxRequest(query, alias).then(handleInfluxQueryResponse);
var handleResponse = _.partial(handleInfluxQueryResponse, alias, groupByField);
return this.doInfluxRequest(query, alias).then(handleResponse);
}, this);
@@ -126,7 +138,7 @@ function (angular, _, kbn) {
return $q.reject(err);
}
return this.doInfluxRequest(query, 'filters')
return this.doInfluxRequest(query)
.then(function (results) {
return _.map(results[0].points, function (metric) {
return {
@@ -139,16 +151,18 @@ function (angular, _, kbn) {
function retry(deferred, callback, delay) {
return callback().then(undefined, function(reason) {
if (reason.status !== 0) {
if (reason.status !== 0 || reason.status >= 300) {
deferred.reject(reason);
}
setTimeout(function() {
return retry(deferred, callback, Math.min(delay * 2, 30000));
}, delay);
else {
setTimeout(function() {
return retry(deferred, callback, Math.min(delay * 2, 30000));
}, delay);
}
});
}
InfluxDatasource.prototype.doInfluxRequest = function(query, alias) {
InfluxDatasource.prototype.doInfluxRequest = function(query) {
var _this = this;
var deferred = $q.defer();
@@ -170,7 +184,6 @@ function (angular, _, kbn) {
};
return $http(options).success(function (data) {
data.alias = alias;
deferred.resolve(data);
});
}, 10);
@@ -178,28 +191,54 @@ function (angular, _, kbn) {
return deferred.promise;
};
function handleInfluxQueryResponse(data) {
function handleInfluxQueryResponse(alias, groupByField, data) {
var output = [];
_.each(data, function(series) {
var seriesName;
var timeCol = series.columns.indexOf('time');
var valueCol = 1;
var groupByCol = -1;
if (groupByField) {
groupByCol = series.columns.indexOf(groupByField);
}
// find value column
_.each(series.columns, function(column, index) {
if (column === "time" || column === "sequence_number") {
return;
if (column !== 'time' && column !== 'sequence_number' && column !== groupByField) {
valueCol = index;
}
var target = data.alias || series.name + "." + column;
var datapoints = [];
var value;
for (var i = 0; i < series.points.length; i++) {
value = isNaN(series.points[i][index]) ? null : series.points[i][index];
datapoints[i] = [value, series.points[i][timeCol]];
}
output.push({ target: target, datapoints: datapoints });
});
var groups = {};
if (groupByField) {
groups = _.groupBy(series.points, function (point) {
return point[groupByCol];
});
}
else {
groups[series.columns[valueCol]] = series.points;
}
_.each(groups, function(groupPoints, key) {
var datapoints = [];
for (var i = 0; i < groupPoints.length; i++) {
var metricValue = isNaN(groupPoints[i][valueCol]) ? null : groupPoints[i][valueCol];
datapoints[i] = [metricValue, groupPoints[i][timeCol]];
}
seriesName = alias ? alias : key;
// if mulitple groups append key to alias
if (alias && groupByField) {
seriesName += key;
}
output.push({ target: seriesName, datapoints: datapoints });
});
});
return output;
@@ -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;
});
});
+2 -2
View File
@@ -18,8 +18,8 @@ function (Settings) {
},
influxdb: {
type: 'influxdb',
url: "http://my_influxdb_server:8080/db/database_name",
user: 'admin',
url: "http://my_influxdb_server:8086/db/database_name",
username: 'admin',
password: 'admin'
},
},
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+4
View File
@@ -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;
+11 -24
View File
@@ -11,40 +11,27 @@ module.exports = function(grunt) {
'htmlmin:build',
'ngtemplates',
'cssmin:build',
'build:grafanaVersion',
'ngmin:build',
'requirejs:build',
'concat:js',
'filerev',
'usemin',
'clean:temp',
'build:write_revision',
'uglify:dest'
]);
// run a string replacement on the require config, using the latest revision number as the cache buster
grunt.registerTask('build:write_revision', function() {
grunt.event.once('git-describe', function (desc) {
grunt.config('string-replace.config', {
files: {
'<%= destDir %>/app/components/require.config.js': '<%= destDir %>/app/components/require.config.js',
'<%= destDir %>/app/app.js': '<%= destDir %>/app/app.js'
},
options: {
replacements: [
{
pattern: /@REV@/g,
replacement: desc.object
},
{
pattern: /@grafanaVersion@/g,
replacement: '<%= pkg.version %>'
}
]
}
});
grunt.task.run('string-replace:config');
grunt.registerTask('build:grafanaVersion', function() {
grunt.config('string-replace.config', {
files: {
'<%= tempDir %>/app/app.js': '<%= tempDir %>/app/app.js'
},
options: {
replacements: [{ pattern: /@grafanaVersion@/g, replacement: '<%= pkg.version %>' }]
}
});
grunt.task.run('git-describe');
grunt.task.run('string-replace:config');
});
};