Commit 93550e9e by Torkel Ödegaard

Work on fixing stacking issues with InfluxdB, added fill(0) and fill(null)…

Work on fixing stacking issues with InfluxdB, added fill(0) and fill(null) option to InfluxDB query editor, also a panel wide group by time option that supports setting a low limit, Fixes issues #673, #674, #534, #743
parent 3157fc65
......@@ -8,25 +8,6 @@ function($, _, moment) {
var kbn = {};
/**
* Calculate a graph interval
*
* from:: Date object containing the start time
* to:: Date object containing the finish time
* size:: Calculate to approximately this many bars
* user_interval:: User specified histogram interval
*
*/
kbn.calculate_interval = function(from,to,size,user_interval) {
if(_.isObject(from)) {
from = from.valueOf();
}
if(_.isObject(to)) {
to = to.valueOf();
}
return user_interval === 0 ? kbn.round_interval((to - from)/size) : user_interval;
};
kbn.round_interval = function(interval) {
switch (true) {
// 0.5s
......@@ -131,6 +112,28 @@ function($, _, moment) {
s: 1
};
kbn.calculateInterval = function(range, resolution, userInterval) {
var lowLimitMs = 1; // 1 millisecond default low limit
var intervalMs, lowLimitInterval;
if (userInterval) {
if (userInterval[0] === '>') {
lowLimitInterval = userInterval.slice(1);
lowLimitMs = kbn.interval_to_ms(lowLimitInterval);
}
else {
return userInterval;
}
}
intervalMs = kbn.round_interval((range.to.valueOf() - range.from.valueOf()) / resolution);
if (lowLimitMs > intervalMs) {
intervalMs = lowLimitMs;
}
return kbn.secondsToHms(intervalMs / 1000);
};
kbn.describe_interval = function (string) {
var matches = string.match(kbn.interval_regex);
if (!matches || !_.has(kbn.intervals_in_seconds, matches[2])) {
......
......@@ -16,13 +16,19 @@ function (angular) {
target.function = target.function || 'mean';
target.column = target.column || 'value';
// backward compatible correction of schema
if (target.condition_value) {
target.condition_expression = target.condition_key + ' ' + target.condition_op + ' ' + target.condition_value;
target.condition = target.condition_key + ' ' + target.condition_op + ' ' + target.condition_value;
delete target.condition_key;
delete target.condition_op;
delete target.condition_value;
}
if (target.groupby_field_add === false) {
target.groupby_field = '';
delete target.groupby_field_add;
}
$scope.rawQuery = false;
$scope.functions = [
......
......@@ -182,13 +182,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
$scope.range = timeSrv.timeRange();
$scope.rangeUnparsed = timeSrv.timeRange(false);
$scope.resolution = Math.ceil($(window).width() * ($scope.panel.span / 12));
$scope.interval = '10m';
if ($scope.range) {
$scope.interval = kbn.secondsToHms(
kbn.calculate_interval($scope.range.from, $scope.range.to, $scope.resolution, 0) / 1000
);
}
$scope.interval = kbn.calculateInterval($scope.range, $scope.resolution, $scope.panel.interval);
};
$scope.get_data = function() {
......@@ -355,6 +349,14 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
$scope.render();
};
$scope.toggleEditorHelp = function(index) {
if ($scope.editorHelpIndex === index) {
$scope.editorHelpIndex = null;
return;
}
$scope.editorHelpIndex = index;
};
panelSrv.init($scope);
});
......
<div class="editor-row" style="margin-top: 10px;">
<div class="editor-row">
<div ng-repeat="target in panel.targets"
class="grafana-target"
......@@ -30,11 +30,9 @@
</li>
</ul>
<ul class="grafana-target-controls-left">
<ul class="grafana-segment-list">
<li>
<a class="grafana-target-segment"
ng-click="target.hide = !target.hide; get_data();"
role="menuitem">
<a class="grafana-target-segment" ng-click="target.hide = !target.hide; get_data();" role="menuitem">
<i class="icon-eye-open"></i>
</a>
</li>
......@@ -53,13 +51,12 @@
ng-model-onblur
ng-blur="get_data()">
</li>
</ul>
<!-- Query editor mode -->
<ul class="grafana-segment-list" role="menu" ng-hide="target.rawQuery">
<li class="grafana-target-segment">
from series
series
</li>
<li>
<input type="text"
......@@ -105,46 +102,59 @@
<!-- Query editor mode -->
<ul class="grafana-segment-list" role="menu" ng-hide="target.rawQuery">
<li class="grafana-target-segment">
<i class="icon-eye-open invisible"></i>
</li>
<li class="grafana-target-segment">
select
</li>
<li class="dropdown">
<span influxdb-func-editor class="grafana-target-segment grafana-target-function">
<span influxdb-func-editor class="grafana-target-segment">
</span>
</li>
<li>
<a class="grafana-target-segment" ng-click="target.condition_filter = !target.condition_filter; get_data();"
bs-tooltip="'Add a where clause'" role="menuitem" data-placement="right">
<i class="icon-filter"></i>
</a>
</li>
<li ng-show="target.condition_filter">
<input type="text" class="input-large grafana-target-text-input" ng-model="target.condition_expression"
spellcheck='false' placeholder="column ~= value" ng-blur="get_data()">
<li class="grafana-target-segment">
where
</li>
<li>
<input type="text" class="input-medium grafana-target-text-input" ng-model="target.condition"
bs-tooltip="'Add a where clause'" data-placement="right" spellcheck='false' placeholder="column ~= value" ng-blur="get_data()">
</li>
<li class="grafana-target-segment">
group by time
</li>
<li>
<input type="text" class="input-mini grafana-target-text-input" ng-model="target.interval"
spellcheck='false' placeholder="{{interval}}" data-placement="right"
bs-tooltip="'Leave blank for auto handling based on time range and panel width'"
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" data-placement="right">
<li class="grafana-target-segment">
<i class="icon-plus"></i>
</a>
</li>
<li ng-show="target.groupby_field_add">
<input type="text" class="input-small grafana-target-text-input" ng-model="target.groupby_field"
<li>
<input type="text" class="input-small grafana-target-text-input" ng-model="target.groupby_field" bs-tooltip="'Add a group by column or leave blank'"
placeholder="column" spellcheck="false" bs-typeahead="listColumns" data-min-length=0 ng-blur="get_data()">
</li>
<li class="dropdown">
<a class="grafana-target-segment pointer" data-toggle="dropdown" bs-tooltip="'Insert missing values, important when stacking'" data-placement="right">
<span ng-show="target.fill">
fill ({{target.fill}})
</span>
<span ng-show="!target.fill">
no fill
</span>
</a>
<ul class="dropdown-menu">
<li><a ng-click="target.fill = ''">no fill</a></li>
<li><a ng-click="target.fill = 'null'">fill (null)</a></li>
<li><a ng-click="target.fill = '0'">fill (0)</a></li>
</ul>
</li>
</ul>
<div class="clearfix"></div>
......@@ -152,20 +162,93 @@
</div>
</div>
</div>
<section class="grafana-metric-options">
<div class="grafana-target">
<div class="grafana-target-inner">
<ul class="grafana-segment-list">
<li class="grafana-target-segment">
<i class="icon-cogs"></i>
</li>
<li class="grafana-target-segment">
group by time
</li>
<li>
<input type="text" class="input-medium grafana-target-text-input" ng-model="panel.interval" ng-blur="get_data();"
spellcheck='false' placeholder="example: >10s">
</li>
<li class="grafana-target-segment">
<i class="icon-question-sign" bs-tooltip="'Set a low limit by having a greater sign: example: >60s'" data-placement="right"></i>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="grafana-target-inner">
<ul class="grafana-segment-list">
<li class="grafana-target-segment grafana-target-segment-icon">
<i class="icon-info-sign"></i>
</li>
<li class="grafana-target-segment">
<a ng-click="toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
alias patterns
</a>
</li>
<li class="grafana-target-segment">
<a ng-click="toggleEditorHelp(2)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
stacking &amp; and fill
</a>
</li>
<li class="grafana-target-segment">
<a ng-click="toggleEditorHelp(3)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
group by time
</a>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</section>
<div class="editor-row">
<div class="pull-left metrics-editor-help" style="margin-top: 30px;">
<div class="span6">
<span class="pointer">
<i class="icon-question-sign"></i> alias patterns:
</span>
<ul class="hide">
<li>$s = series name</li>
<li>$g = group by</li>
<li>$[0-9] part of series name for series names seperated by dots.</li>
<ul>
</div>
</div>
<div class="pull-left" style="margin-top: 30px;">
<div class="span6" ng-if="editorHelpIndex === 1">
Alias patterns:
<ul>
<li>$s = series name</li>
<li>$g = group by</li>
<li>$[0-9] part of series name for series names seperated by dots.</li>
</ul>
</div>
<div class="span6" ng-if="editorHelpIndex === 2">
Stacking and fill:
<ul>
<li>When stacking is enabled it important that points align</li>
<li>If there are missing points for one series it can cause gaps or missing bars</li>
<li>You must use fill(0), and select a group by time low limit</li>
<li>Use the group by time option below your queries and specify for example &gt;10s if your metrics are written every 10 seconds</li>
<li>This will insert zeros for series that are missing measurements and will make stacking work properly</li>
</ul>
</div>
<div class="span6" ng-if="editorHelpIndex === 3">
Group by time:
<ul>
<li>Group by time is important, otherwise the query could return many thousands of datapoints that will slow down Grafana</li>
<li>Leave the group by time field empty for each query and it will be calculated based on time range and pixel width of the graph</li>
<li>If you use fill(0) or fill(null) set a low limit for the auto group by time interval</li>
<li>The low limit can only be set in the group by time option below your queries</li>
<li>You set a low limit by adding a greater sign before the interval</li>
<li>Example: &gt;60s if you write metrics to InfluxDB every 60 seconds</li>
</ul>
</div>
</div>
</div>
......@@ -22,24 +22,28 @@ function () {
seriesName = '"' + seriesName+ '"';
}
if (target.groupby_field_add) {
if (target.groupby_field) {
query += target.groupby_field + ', ';
}
query += target.function + '(' + target.column + ')';
query += ' from ' + seriesName + ' where [[$timeFilter]]';
if (target.condition_filter) {
query += ' and ' + target.condition_expression;
if (target.condition) {
query += ' and ' + target.condition;
}
query += ' group by time([[$interval]])';
if (target.groupby_field_add) {
if (target.groupby_field) {
query += ', ' + target.groupby_field;
this.groupByField = target.groupby_field;
}
if (target.fill) {
query += ' fill(' + target.fill + ')';
}
query += " order asc";
target.query = query;
......
......@@ -61,6 +61,10 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
});
};
InfluxDatasource.prototype._getGroupByTimeInterval = function(target, options) {
return target.interval || options.interval;
};
InfluxDatasource.prototype.annotationQuery = function(annotation, rangeUnparsed) {
var timeFilter = getTimeFilter({ range: rangeUnparsed });
var query = _.template(annotation.query, { timeFilter: timeFilter, "$timeFilter": timeFilter }, this.templateSettings);
......
......@@ -135,7 +135,6 @@
list-style: none;
margin: 0;
margin-right: 90px;
margin-left: 30px;
>li {
float: left;
}
......@@ -143,9 +142,6 @@
.grafana-metric-options {
margin-top: 35px;
.grafana-segment-list {
margin-left: 0;
}
}
// fix for fixed positioned panel & scrolling
......@@ -183,6 +179,15 @@
&.annotation-segment {
padding: 8px 15px;
}
}
.grafana-target-segment-icon {
i {
width: 15px;
text-align: center;
display: inline-block;
}
}
.grafana-target-function {
......@@ -208,15 +213,6 @@ input[type=text].grafana-function-param-input {
padding: 0;
}
.grafana-target-controls-left {
list-style: none;
float: left;
margin: 0px;
li {
display: inline-block;
}
}
.grafana-target-controls {
float: right;
list-style: none;
......
......@@ -36,8 +36,8 @@ define([
var data = ctx.scope.render.getCall(0).args[0];
expect(data.length).to.be(2);
});
});
});
});
......
......@@ -10,9 +10,7 @@ define([
series: 'google.test',
column: 'value',
function: 'mean',
condition_filter: true,
condition_expression: "code=1",
groupby_field_add: true,
condition: "code=1",
groupby_field: 'code'
});
......@@ -29,6 +27,23 @@ define([
});
describe('series with fill and minimum group by time', function() {
var builder = new InfluxQueryBuilder({
series: 'google.test',
column: 'value',
function: 'mean',
fill: '0',
});
var query = builder.build();
it('should generate correct query', function() {
expect(query).to.be('select mean(value) from "google.test" where [[$timeFilter]] ' +
'group by time([[$interval]]) fill(0) order asc');
});
});
describe('old style raw query', function() {
var builder = new InfluxQueryBuilder({
query: 'select host, mean(value) from asd.asd where time > now() - 1h group by time(1s), code order asc',
......
......@@ -10,6 +10,9 @@ define([
beforeEach(module('grafana.services'));
beforeEach(ctx.providePhase());
beforeEach(ctx.createService('InfluxDatasource'));
beforeEach(function() {
ctx.ds = new ctx.service({ urls: [''], user: 'test', password: 'mupp' });
});
describe('When querying influxdb with one target using query editor target spec', function() {
var results;
......@@ -28,10 +31,8 @@ define([
}];
beforeEach(function() {
var ds = new ctx.service({ urls: [''], user: 'test', password: 'mupp' });
ctx.$httpBackend.expect('GET', urlExpected).respond(response);
ds.query(query).then(function(data) { results = data; });
ctx.ds.query(query).then(function(data) { results = data; });
ctx.$httpBackend.flush();
});
......@@ -58,10 +59,8 @@ define([
var response = [];
beforeEach(function() {
var ds = new ctx.service({ urls: [''], user: 'test', password: 'mupp' });
ctx.$httpBackend.expect('GET', urlExpected).respond(response);
ds.query(query).then(function(data) { results = data; });
ctx.ds.query(query).then(function(data) { results = data; });
ctx.$httpBackend.flush();
});
......@@ -71,6 +70,18 @@ define([
});
describe('When calculating group by time interval', function() {
it('if blank should use auto interval', function() {
var result = ctx.ds._getGroupByTimeInterval({}, { interval:'0.1s' });
expect(result).to.be('0.1s');
});
it('if target interval specified should use that interval', function() {
var result = ctx.ds._getGroupByTimeInterval({interval: '10s'}, { interval:'0.1s' });
expect(result).to.be('10s');
});
});
});
});
......
......@@ -41,7 +41,6 @@ define([
});
describe('nanosecond formatting', function () {
it('should translate 25 to 25 ns', function () {
var str = kbn.nanosFormat(25, 2);
expect(str).to.be("25 ns");
......@@ -68,4 +67,38 @@ define([
});
});
describe('calculateInterval', function() {
it('1h 100 resultion', function() {
var range = { from: kbn.parseDate('now-1h'), to: kbn.parseDate('now') };
var str = kbn.calculateInterval(range, 100, null);
expect(str).to.be('30s');
});
it('10m 1600 resolution', function() {
var range = { from: kbn.parseDate('now-10m'), to: kbn.parseDate('now') };
var str = kbn.calculateInterval(range, 1600, null);
expect(str).to.be('0.1s');
});
it('fixed user interval', function() {
var range = { from: kbn.parseDate('now-10m'), to: kbn.parseDate('now') };
var str = kbn.calculateInterval(range, 1600, '10s');
expect(str).to.be('10s');
});
it('short time range and user low limit', function() {
var range = { from: kbn.parseDate('now-10m'), to: kbn.parseDate('now') };
var str = kbn.calculateInterval(range, 1600, '>10s');
expect(str).to.be('10s');
});
it('large time range and user low limit', function() {
var range = { from: kbn.parseDate('now-14d'), to: kbn.parseDate('now') };
var str = kbn.calculateInterval(range, 1000, '>10s');
expect(str).to.be('30m');
});
});
});
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