Commit a4ca679b by Torkel Ödegaard

Merge branch 'master' of github.com:torkelo/grafana-private into dashboard_loading_refactoring

parents 438455bc da451d01
......@@ -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)
......
......@@ -3,7 +3,7 @@
*/
require.config({
baseUrl: 'app',
// urlArgs: 'r=@REV@',
paths: {
config: ['../config', '../config.sample'],
settings: 'components/settings',
......
......@@ -12,4 +12,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;
}
});
});
......@@ -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);
......
......@@ -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
......
<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,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);
}
};
......
......@@ -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 groups = {};
if (groupByField) {
groups = _.groupBy(series.points, function (point) {
return point[groupByCol];
});
}
else {
groups[series.columns[valueCol]] = series.points;
}
var target = data.alias || series.name + "." + column;
_.each(groups, function(groupPoints, key) {
var datapoints = [];
var value;
for (var i = 0; i < groupPoints.length; i++) {
var metricValue = isNaN(groupPoints[i][valueCol]) ? null : groupPoints[i][valueCol];
datapoints[i] = [metricValue, groupPoints[i][timeCol]];
}
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]];
seriesName = alias ? alias : key;
// if mulitple groups append key to alias
if (alias && groupByField) {
seriesName += key;
}
output.push({ target: target, datapoints: datapoints });
output.push({ target: seriesName, datapoints: datapoints });
});
});
return output;
......
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;
});
});
......@@ -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'
},
},
......
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;
......
......@@ -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');
});
};
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