mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge remote-tracking branch 'upstream/master'
Conflicts: public/app/plugins/datasource/opentsdb/datasource.js
This commit is contained in:
@@ -264,7 +264,7 @@ function (angular, _, config, kbn, moment) {
|
||||
var query = {
|
||||
query: { query_string: { query: queryString } },
|
||||
facets: { tags: { terms: { field: "tags", order: "term", size: 50 } } },
|
||||
size: this.searchMaxResults,
|
||||
size: 10000,
|
||||
sort: ["_uid"],
|
||||
};
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ function (angular, _, $, config, kbn, moment) {
|
||||
maxDataPoints: options.maxDataPoints,
|
||||
};
|
||||
|
||||
var params = this.buildGraphiteParams(graphOptions);
|
||||
var params = this.buildGraphiteParams(graphOptions, options.scopedVars);
|
||||
|
||||
if (options.format === 'png') {
|
||||
return $q.when(this.url + '/render' + '?' + params.join('&'));
|
||||
@@ -111,6 +111,7 @@ function (angular, _, $, config, kbn, moment) {
|
||||
var list = [];
|
||||
for (var i = 0; i < results.data.length; i++) {
|
||||
var e = results.data[i];
|
||||
|
||||
list.push({
|
||||
annotation: annotation,
|
||||
time: e.when * 1000,
|
||||
@@ -195,6 +196,12 @@ function (angular, _, $, config, kbn, moment) {
|
||||
});
|
||||
};
|
||||
|
||||
GraphiteDatasource.prototype.testDatasource = function() {
|
||||
return this.metricFindQuery('*').then(function () {
|
||||
return { status: "success", message: "Data source is working", title: "Success" };
|
||||
});
|
||||
};
|
||||
|
||||
GraphiteDatasource.prototype.listDashboards = function(query) {
|
||||
return this.doGraphiteRequest({ method: 'GET', url: '/dashboard/find/', params: {query: query || ''} })
|
||||
.then(function(results) {
|
||||
@@ -231,7 +238,7 @@ function (angular, _, $, config, kbn, moment) {
|
||||
'#Y', '#Z'
|
||||
];
|
||||
|
||||
GraphiteDatasource.prototype.buildGraphiteParams = function(options) {
|
||||
GraphiteDatasource.prototype.buildGraphiteParams = function(options, scopedVars) {
|
||||
var graphite_options = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout'];
|
||||
var clean_options = [], targets = {};
|
||||
var target, targetValue, i;
|
||||
@@ -252,7 +259,7 @@ function (angular, _, $, config, kbn, moment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
targetValue = templateSrv.replace(target.target);
|
||||
targetValue = templateSrv.replace(target.target, scopedVars);
|
||||
targetValue = targetValue.replace(intervalFormatFixRegex, fixIntervalFormat);
|
||||
targets[this._seriesRefLetters[i]] = targetValue;
|
||||
}
|
||||
|
||||
@@ -327,6 +327,13 @@ function (_, $) {
|
||||
defaultParams: [100]
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
name: "changed",
|
||||
category: categories.Special,
|
||||
params: [],
|
||||
defaultParams: []
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
name: 'scale',
|
||||
category: categories.Transform,
|
||||
@@ -530,6 +537,16 @@ function (_, $) {
|
||||
defaultParams: [10]
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
name: 'weightedAverage',
|
||||
category: categories.Filter,
|
||||
params: [
|
||||
{ name: 'other', type: 'value_or_series', optional: true },
|
||||
{ name: "node", type: "int", options: [0,1,2,3,4,5,6,7,8,9,10,12] },
|
||||
],
|
||||
defaultParams: ['#A', 4]
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
name: 'movingMedian',
|
||||
category: categories.Filter,
|
||||
|
||||
@@ -119,6 +119,8 @@ define([
|
||||
identifierStartTable[i] =
|
||||
i >= 48 && i <= 57 || // 0-9
|
||||
i === 36 || // $
|
||||
i === 126 || // ~
|
||||
i === 124 || // |
|
||||
i >= 65 && i <= 90 || // A-Z
|
||||
i === 95 || // _
|
||||
i === 45 || // -
|
||||
|
||||
@@ -142,13 +142,13 @@ define([
|
||||
name: this.consumeToken().value,
|
||||
};
|
||||
|
||||
// consume left paranthesis
|
||||
// consume left parenthesis
|
||||
this.consumeToken();
|
||||
|
||||
node.params = this.functionParameters();
|
||||
|
||||
if (!this.match(')')) {
|
||||
this.errorMark('Expected closing paranthesis');
|
||||
this.errorMark('Expected closing parenthesis');
|
||||
}
|
||||
|
||||
this.consumeToken();
|
||||
|
||||
@@ -74,7 +74,9 @@
|
||||
ng-show="showTextEditor" />
|
||||
|
||||
<ul class="tight-form-list" role="menu" ng-hide="showTextEditor">
|
||||
<li ng-repeat="segment in segments" role="menuitem" graphite-segment></li>
|
||||
<li ng-repeat="segment in segments" role="menuitem">
|
||||
<metric-segment segment="segment" get-alt-segments="getAltSegments($index)" on-value-changed="segmentValueChanged(segment, $index)"></metric-segment>
|
||||
</li>
|
||||
<li ng-repeat="func in functions">
|
||||
<span graphite-func-editor class="tight-form-item tight-form-func">
|
||||
</span>
|
||||
|
||||
@@ -152,23 +152,18 @@ function (angular, _, config, gfunc, Parser) {
|
||||
}
|
||||
|
||||
$scope.getAltSegments = function (index) {
|
||||
$scope.altSegments = [];
|
||||
|
||||
var query = index === 0 ? '*' : getSegmentPathUpTo(index) + '.*';
|
||||
|
||||
return $scope.datasource.metricFindQuery(query)
|
||||
.then(function(segments) {
|
||||
$scope.altSegments = _.map(segments, function(segment) {
|
||||
return $scope.datasource.metricFindQuery(query).then(function(segments) {
|
||||
var altSegments = _.map(segments, function(segment) {
|
||||
return new MetricSegment({ value: segment.text, expandable: segment.expandable });
|
||||
});
|
||||
|
||||
if ($scope.altSegments.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (altSegments.length === 0) { return altSegments; }
|
||||
|
||||
// add template variables
|
||||
_.each(templateSrv.variables, function(variable) {
|
||||
$scope.altSegments.unshift(new MetricSegment({
|
||||
altSegments.unshift(new MetricSegment({
|
||||
type: 'template',
|
||||
value: '$' + variable.name,
|
||||
expandable: true,
|
||||
@@ -176,10 +171,12 @@ function (angular, _, config, gfunc, Parser) {
|
||||
});
|
||||
|
||||
// add wildcard option
|
||||
$scope.altSegments.unshift(new MetricSegment('*'));
|
||||
altSegments.unshift(new MetricSegment('*'));
|
||||
return altSegments;
|
||||
})
|
||||
.then(null, function(err) {
|
||||
$scope.parserError = err.message || 'Failed to issue metric query';
|
||||
return [];
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -28,39 +28,43 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
|
||||
|
||||
this.supportAnnotations = true;
|
||||
this.supportMetrics = true;
|
||||
this.editorSrc = 'app/features/influxdb/partials/query.editor.html';
|
||||
this.annotationEditorSrc = 'app/features/influxdb/partials/annotations.editor.html';
|
||||
}
|
||||
|
||||
InfluxDatasource.prototype.query = function(options) {
|
||||
var timeFilter = getTimeFilter(options);
|
||||
var i, y;
|
||||
|
||||
var promises = _.map(options.targets, function(target) {
|
||||
if (target.hide) {
|
||||
return [];
|
||||
}
|
||||
var allQueries = _.map(options.targets, function(target) {
|
||||
if (target.hide) { return []; }
|
||||
|
||||
// build query
|
||||
var queryBuilder = new InfluxQueryBuilder(target);
|
||||
var query = queryBuilder.build();
|
||||
console.log('query builder result:' + query);
|
||||
|
||||
// replace grafana variables
|
||||
query = query.replace('$timeFilter', timeFilter);
|
||||
var query = queryBuilder.build();
|
||||
query = query.replace(/\$interval/g, (target.interval || options.interval));
|
||||
return query;
|
||||
|
||||
// replace templated variables
|
||||
query = templateSrv.replace(query);
|
||||
}).join("\n");
|
||||
|
||||
var alias = target.alias ? templateSrv.replace(target.alias) : '';
|
||||
// replace grafana variables
|
||||
allQueries = allQueries.replace(/\$timeFilter/g, timeFilter);
|
||||
|
||||
var handleResponse = _.partial(handleInfluxQueryResponse, alias);
|
||||
return this._seriesQuery(query).then(handleResponse);
|
||||
// replace templated variables
|
||||
allQueries = templateSrv.replace(allQueries, options.scopedVars);
|
||||
return this._seriesQuery(allQueries).then(function(data) {
|
||||
if (!data || !data.results || !data.results[0].series) {
|
||||
return [];
|
||||
}
|
||||
|
||||
}, this);
|
||||
var seriesList = [];
|
||||
for (i = 0; i < data.results.length; i++) {
|
||||
var alias = (options.targets[i] || {}).alias;
|
||||
var targetSeries = new InfluxSeries({ series: data.results[i].series, alias: alias }).getTimeSeries();
|
||||
for (y = 0; y < targetSeries.length; y++) {
|
||||
seriesList.push(targetSeries[y]);
|
||||
}
|
||||
}
|
||||
|
||||
return $q.all(promises).then(function(results) {
|
||||
return { data: _.flatten(results) };
|
||||
return { data: seriesList };
|
||||
});
|
||||
};
|
||||
|
||||
@@ -69,12 +73,15 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
|
||||
var query = annotation.query.replace('$timeFilter', timeFilter);
|
||||
query = templateSrv.replace(query);
|
||||
|
||||
return this._seriesQuery(query).then(function(results) {
|
||||
return new InfluxSeries({ seriesList: results, annotation: annotation }).getAnnotations();
|
||||
return this._seriesQuery(query).then(function(data) {
|
||||
if (!data || !data.results || !data.results[0]) {
|
||||
throw { message: 'No results in response from InfluxDB' };
|
||||
}
|
||||
return new InfluxSeries({ series: data.results[0].series, annotation: annotation }).getAnnotations();
|
||||
});
|
||||
};
|
||||
|
||||
InfluxDatasource.prototype.metricFindQuery = function (query, queryType) {
|
||||
InfluxDatasource.prototype.metricFindQuery = function (query) {
|
||||
var interpolated;
|
||||
try {
|
||||
interpolated = templateSrv.replace(query);
|
||||
@@ -83,39 +90,33 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
|
||||
return $q.reject(err);
|
||||
}
|
||||
|
||||
console.log('metricFindQuery called with: ' + [query, queryType].join(', '));
|
||||
|
||||
return this._seriesQuery(interpolated, queryType).then(function (results) {
|
||||
return this._seriesQuery(interpolated).then(function (results) {
|
||||
if (!results || results.results.length === 0) { return []; }
|
||||
|
||||
var influxResults = results.results[0];
|
||||
if (!influxResults.series) {
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log('metric find query response', results);
|
||||
var series = influxResults.series[0];
|
||||
|
||||
switch (queryType) {
|
||||
case 'MEASUREMENTS':
|
||||
if (query.indexOf('SHOW MEASUREMENTS') === 0) {
|
||||
return _.map(series.values, function(value) { return { text: value[0], expandable: true }; });
|
||||
case 'TAG_KEYS':
|
||||
var tagKeys = _.flatten(series.values);
|
||||
return _.map(tagKeys, function(tagKey) { return { text: tagKey, expandable: true }; });
|
||||
case 'TAG_VALUES':
|
||||
var tagValues = _.flatten(series.values);
|
||||
return _.map(tagValues, function(tagValue) { return { text: tagValue, expandable: true }; });
|
||||
default: // template values service does not pass in a a query type
|
||||
var flattenedValues = _.flatten(series.values);
|
||||
return _.map(flattenedValues, function(value) { return { text: value, expandable: true }; });
|
||||
}
|
||||
|
||||
var flattenedValues = _.flatten(series.values);
|
||||
return _.map(flattenedValues, function(value) { return { text: value, expandable: true }; });
|
||||
});
|
||||
};
|
||||
|
||||
function retry(deferred, callback, delay) {
|
||||
return callback().then(undefined, function(reason) {
|
||||
if (reason.status !== 0 || reason.status >= 300) {
|
||||
reason.message = 'InfluxDB Error: <br/>' + reason.data;
|
||||
if (reason.data && reason.data.error) {
|
||||
reason.message = 'InfluxDB Error Response: ' + reason.data.error;
|
||||
}
|
||||
else {
|
||||
reason.message = 'InfluxDB Error: ' + reason.message;
|
||||
}
|
||||
deferred.reject(reason);
|
||||
}
|
||||
else {
|
||||
@@ -127,7 +128,13 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
|
||||
}
|
||||
|
||||
InfluxDatasource.prototype._seriesQuery = function(query) {
|
||||
return this._influxRequest('GET', '/query', {q: query});
|
||||
return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'});
|
||||
};
|
||||
|
||||
InfluxDatasource.prototype.testDatasource = function() {
|
||||
return this.metricFindQuery('SHOW MEASUREMENTS LIMIT 1').then(function () {
|
||||
return { status: "success", message: "Data source is working", title: "Success" };
|
||||
});
|
||||
};
|
||||
|
||||
InfluxDatasource.prototype._influxRequest = function(method, url, data) {
|
||||
@@ -174,11 +181,6 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
function handleInfluxQueryResponse(alias, seriesList) {
|
||||
var influxSeries = new InfluxSeries({ seriesList: seriesList, alias: alias });
|
||||
return influxSeries.getTimeSeries();
|
||||
}
|
||||
|
||||
function getTimeFilter(options) {
|
||||
var from = getInfluxTime(options.range.from);
|
||||
var until = getInfluxTime(options.range.to);
|
||||
|
||||
@@ -11,22 +11,36 @@ function (angular, _, $) {
|
||||
.directive('influxdbFuncEditor', function($compile) {
|
||||
|
||||
var funcSpanTemplate = '<a gf-dropdown="functionMenu" class="dropdown-toggle" ' +
|
||||
'data-toggle="dropdown">{{target.function}}</a><span>(</span>';
|
||||
'data-toggle="dropdown">{{field.func}}</a><span>(</span>';
|
||||
|
||||
var paramTemplate = '<input type="text" style="display:none"' +
|
||||
' class="input-mini tight-form-func-param"></input>';
|
||||
|
||||
var functionList = [
|
||||
'count', 'mean', 'sum', 'min', 'max', 'mode', 'distinct', 'median',
|
||||
'derivative', 'stddev', 'first', 'last', 'difference'
|
||||
];
|
||||
|
||||
var functionMenu = _.map(functionList, function(func) {
|
||||
return { text: func, click: "changeFunction('" + func + "');" };
|
||||
});
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
field: "=",
|
||||
getFields: "&",
|
||||
onChange: "&",
|
||||
},
|
||||
link: function postLink($scope, elem) {
|
||||
var $funcLink = $(funcSpanTemplate);
|
||||
|
||||
$scope.functionMenu = _.map($scope.functions, function(func) {
|
||||
return {
|
||||
text: func,
|
||||
click: "changeFunction('" + func + "');"
|
||||
};
|
||||
});
|
||||
$scope.functionMenu = functionMenu;
|
||||
|
||||
$scope.changeFunction = function(func) {
|
||||
$scope.field.func = func;
|
||||
$scope.onChange();
|
||||
};
|
||||
|
||||
function clickFuncParam() {
|
||||
/*jshint validthis:true */
|
||||
@@ -34,7 +48,7 @@ function (angular, _, $) {
|
||||
var $link = $(this);
|
||||
var $input = $link.next();
|
||||
|
||||
$input.val($scope.target.column);
|
||||
$input.val($scope.field.name);
|
||||
$input.css('width', ($link.width() + 16) + 'px');
|
||||
|
||||
$link.hide();
|
||||
@@ -58,8 +72,8 @@ function (angular, _, $) {
|
||||
if ($input.val() !== '') {
|
||||
$link.text($input.val());
|
||||
|
||||
$scope.target.column = $input.val();
|
||||
$scope.$apply($scope.get_data);
|
||||
$scope.field.name = $input.val();
|
||||
$scope.$apply($scope.onChange());
|
||||
}
|
||||
|
||||
$input.hide();
|
||||
@@ -83,8 +97,10 @@ function (angular, _, $) {
|
||||
$input.attr('data-provide', 'typeahead');
|
||||
|
||||
$input.typeahead({
|
||||
source: function () {
|
||||
return $scope.listColumns.apply(null, arguments);
|
||||
source: function (query, callback) {
|
||||
return $scope.getFields().then(function(results) {
|
||||
callback(results);
|
||||
});
|
||||
},
|
||||
minLength: 0,
|
||||
items: 20,
|
||||
@@ -108,7 +124,7 @@ function (angular, _, $) {
|
||||
function addElementsAndCompile() {
|
||||
$funcLink.appendTo(elem);
|
||||
|
||||
var $paramLink = $('<a ng-click="" class="graphite-func-param-link">' + $scope.target.column + '</a>');
|
||||
var $paramLink = $('<a ng-click="" class="graphite-func-param-link">' + $scope.field.name + '</a>');
|
||||
var $input = $(paramTemplate);
|
||||
|
||||
$paramLink.appendTo(elem);
|
||||
|
||||
@@ -5,7 +5,7 @@ function (_) {
|
||||
'use strict';
|
||||
|
||||
function InfluxSeries(options) {
|
||||
this.seriesList = options.seriesList;
|
||||
this.series = options.series;
|
||||
this.alias = options.alias;
|
||||
this.annotation = options.annotation;
|
||||
}
|
||||
@@ -15,40 +15,65 @@ function (_) {
|
||||
p.getTimeSeries = function() {
|
||||
var output = [];
|
||||
var self = this;
|
||||
var i, j;
|
||||
|
||||
console.log(self.seriesList);
|
||||
if (!self.seriesList || !self.seriesList.results || !self.seriesList.results[0]) {
|
||||
if (self.series.length === 0) {
|
||||
return output;
|
||||
}
|
||||
|
||||
this.seriesList = self.seriesList.results[0].series;
|
||||
|
||||
_.each(self.seriesList, function(series) {
|
||||
var datapoints = [];
|
||||
for (var i = 0; i < series.values.length; i++) {
|
||||
datapoints[i] = [series.values[i][1], new Date(series.values[i][0]).getTime()];
|
||||
}
|
||||
|
||||
var seriesName = series.name;
|
||||
_.each(self.series, function(series) {
|
||||
var columns = series.columns.length;
|
||||
var tags = _.map(series.tags, function(value, key) {
|
||||
return key + ': ' + value;
|
||||
});
|
||||
|
||||
if (tags.length > 0) {
|
||||
seriesName = seriesName + ' {' + tags.join(', ') + '}';
|
||||
}
|
||||
for (j = 1; j < columns; j++) {
|
||||
var seriesName = series.name;
|
||||
var columnName = series.columns[j];
|
||||
if (columnName !== 'value') {
|
||||
seriesName = seriesName + '.' + columnName;
|
||||
}
|
||||
|
||||
output.push({ target: seriesName, datapoints: datapoints });
|
||||
if (self.alias) {
|
||||
seriesName = self._getSeriesName(series);
|
||||
} else if (series.tags) {
|
||||
seriesName = seriesName + ' {' + tags.join(', ') + '}';
|
||||
}
|
||||
|
||||
var datapoints = [];
|
||||
if (series.values) {
|
||||
for (i = 0; i < series.values.length; i++) {
|
||||
datapoints[i] = [series.values[i][j], series.values[i][0]];
|
||||
}
|
||||
}
|
||||
|
||||
output.push({ target: seriesName, datapoints: datapoints});
|
||||
}
|
||||
});
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
p._getSeriesName = function(series) {
|
||||
var regex = /\$(\w+)|\[\[([\s\S]+?)\]\]/g;
|
||||
|
||||
return this.alias.replace(regex, function(match, g1, g2) {
|
||||
var group = g1 || g2;
|
||||
|
||||
if (group === 'm' || group === 'measurement') { return series.name; }
|
||||
if (group.indexOf('tag_') !== 0) { return match; }
|
||||
|
||||
var tag = group.replace('tag_', '');
|
||||
if (!series.tags) { return match; }
|
||||
return series.tags[tag];
|
||||
});
|
||||
};
|
||||
|
||||
p.getAnnotations = function () {
|
||||
var list = [];
|
||||
var self = this;
|
||||
|
||||
_.each(this.seriesList, function (series) {
|
||||
_.each(this.series, function (series) {
|
||||
var titleCol = null;
|
||||
var timeCol = null;
|
||||
var tagsCol = null;
|
||||
@@ -63,19 +88,15 @@ function (_) {
|
||||
if (column === self.annotation.textColumn) { textCol = index; return; }
|
||||
});
|
||||
|
||||
_.each(series.points, function (point) {
|
||||
_.each(series.values, function (value) {
|
||||
var data = {
|
||||
annotation: self.annotation,
|
||||
time: point[timeCol],
|
||||
title: point[titleCol],
|
||||
tags: point[tagsCol],
|
||||
text: point[textCol]
|
||||
time: + new Date(value[timeCol]),
|
||||
title: value[titleCol],
|
||||
tags: value[tagsCol],
|
||||
text: value[textCol]
|
||||
};
|
||||
|
||||
if (tagsCol) {
|
||||
data.tags = point[tagsCol];
|
||||
}
|
||||
|
||||
list.push(data);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
<div class="grafana-info-box" style="margin: 30px 0 0 0">
|
||||
<h5>Data source implementation: Alpha stage</h5>
|
||||
<ul>
|
||||
<li>This data source implementation is not complete, a lot is not working and implemented yet</li>
|
||||
<li>Updates can be tracked, and feedback directed <a href="https://github.com/grafana/grafana/issues/1525">here #1525</a>.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div ng-include="httpConfigPartialSrc"></div>
|
||||
<br>
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
<div class="editor-row">
|
||||
|
||||
<div ng-repeat="target in panel.targets"
|
||||
class="tight-form"
|
||||
ng-class="{'tight-form-disabled': target.hide}"
|
||||
ng-controller="InfluxQueryCtrl"
|
||||
ng-init="init()">
|
||||
<ul class="tight-form-list pull-right">
|
||||
<li ng-show="parserError" class="tight-form-item">
|
||||
<a bs-tooltip="parserError" style="color: rgb(229, 189, 28)" role="menuitem">
|
||||
<i class="fa fa-warning"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
<a class="pointer" tabindex="1" ng-click="toggleQueryMode()">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
<div ng-repeat="target in panel.targets" ng-controller="InfluxQueryCtrl" ng-init="init()" ng-class="{'tight-form-disabled': target.hide}" class="tight-form-container-no-item-borders" style="margin-bottom: 10px">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list pull-right">
|
||||
<li ng-show="parserError" class="tight-form-item">
|
||||
<a bs-tooltip="parserError" style="color: rgb(229, 189, 28)" role="menuitem">
|
||||
<i class="fa fa-warning"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
<a class="pointer" tabindex="1" ng-click="toggleQueryMode()">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
<div class="dropdown">
|
||||
<a class="pointer dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
@@ -62,28 +59,187 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<input type="text"
|
||||
class="tight-form-clear-input span10"
|
||||
ng-model="target.query"
|
||||
focus-me="target.rawQuery"
|
||||
spellcheck='false'
|
||||
ng-model-onblur ng-change="targetTextChanged()"
|
||||
ng-show="target.rawQuery" />
|
||||
<input type="text" class="tight-form-clear-input" style="width: 80%" ng-model="target.query" focus-me="target.rawQuery" spellcheck='false' ng-model-onblur ng-change="get_data()" ng-show="target.rawQuery"/>
|
||||
|
||||
<ul class="tight-form-list" role="menu" ng-hide="target.rawQuery">
|
||||
<li ng-repeat="segment in segments" role="menuitem" graphite-segment></li>
|
||||
<li ng-repeat="func in functions">
|
||||
<span graphite-func-editor class="tight-form-item tight-form-func">
|
||||
</span>
|
||||
</li>
|
||||
<li class="dropdown" graphite-add-func>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<ul class="tight-form-list" role="menu" ng-hide="target.rawQuery">
|
||||
<li class="tight-form-item query-keyword" style="width: 75px;">
|
||||
SELECT
|
||||
</li>
|
||||
<li class="dropdown" ng-repeat="field in target.fields">
|
||||
<span influxdb-func-editor field="field" get-fields="getFields()" on-change="fieldChanged(field)" class="tight-form-item">
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<metric-segment segment="addFieldSegment" get-alt-segments="getFieldSegments()" on-value-changed="addField()"></metric-segment>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<div class="tight-form" ng-hide="target.rawQuery">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item">
|
||||
<i class="fa fa-eye invisible"></i>
|
||||
</li>
|
||||
<li class="tight-form-item query-keyword" style="width: 75px;">
|
||||
FROM
|
||||
</li>
|
||||
<li>
|
||||
<metric-segment segment="measurementSegment" get-alt-segments="getMeasurements()" on-value-changed="measurementChanged()"></metric-segment>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<div class="tight-form" ng-hide="target.rawQuery">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item">
|
||||
<i class="fa fa-eye invisible"></i>
|
||||
</li>
|
||||
|
||||
<li class="tight-form-item query-keyword" style="width: 75px;">
|
||||
WHERE
|
||||
</li>
|
||||
|
||||
<li ng-repeat="segment in tagSegments">
|
||||
<metric-segment segment="segment" get-alt-segments="getTagsOrValues(segment, $index)" on-value-changed="tagSegmentUpdated(segment, $index)"></metric-segment>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list" ng-hide="target.rawQuery">
|
||||
<li class="tight-form-item">
|
||||
<i class="fa fa-eye invisible"></i>
|
||||
</li>
|
||||
|
||||
<li class="tight-form-item query-keyword">
|
||||
GROUP BY
|
||||
</li>
|
||||
|
||||
<li class="tight-form-item">
|
||||
time($interval)
|
||||
</li>
|
||||
|
||||
<li ng-repeat="segment in groupBySegments">
|
||||
<metric-segment segment="segment" get-alt-segments="getGroupByTagSegments(segment, 0)" on-value-changed="groupByTagUpdated(segment, $index)"></metric-segment>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
<a class="tight-form-item 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>
|
||||
<ul class="tight-form-list pull-right">
|
||||
<li class="tight-form-item">
|
||||
Alias pattern
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="input-medium tight-form-input" ng-model="target.alias" spellcheck='false' placeholder="alias" ng-blur="get_data()">
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="grafana-metric-options">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item tight-form-item-icon">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Group by time interval
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="input-medium tight-form-input" ng-model="panel.interval" ng-blur="get_data();"
|
||||
spellcheck='false' placeholder="example: >10s">
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
<i class="fa fa-question-circle" 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="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item tight-form-item-icon">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
<a ng-click="toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
|
||||
alias patterns
|
||||
</a>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
<a ng-click="toggleEditorHelp(2)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
|
||||
stacking & and fill
|
||||
</a>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<div class="editor-row">
|
||||
<div class="pull-left" style="margin-top: 30px;">
|
||||
|
||||
<div class="grafana-info-box span6" ng-if="editorHelpIndex === 1">
|
||||
<h5>Alias patterns</h5>
|
||||
<ul>
|
||||
<li>$m = replaced with measurement name</li>
|
||||
<li>$measurement = replaced with measurement name</li>
|
||||
<li>$tag_hostname = replaced with the value of the hostname tag</li>
|
||||
<li>You can also use [[tag_hostname]] pattern replacement syntax</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="grafana-info-box span6" ng-if="editorHelpIndex === 2">
|
||||
<h5>Stacking and fill</h5>
|
||||
<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 >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="grafana-info-box span6" ng-if="editorHelpIndex === 3">
|
||||
<h5>Group by time</h5>
|
||||
<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: >60s if you write metrics to InfluxDB every 60 seconds</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -13,5 +13,6 @@
|
||||
"annotations": "app/plugins/datasource/influxdb/partials/annotations.editor.html"
|
||||
},
|
||||
|
||||
"metrics": true
|
||||
"metrics": true,
|
||||
"annotations": true
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
define([
|
||||
'lodash'
|
||||
'lodash'
|
||||
],
|
||||
function (_) {
|
||||
'use strict';
|
||||
@@ -8,36 +8,105 @@ function (_) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
function renderTagCondition (tag, index) {
|
||||
var str = "";
|
||||
if (index > 0) {
|
||||
str = (tag.condition || 'AND') + ' ';
|
||||
}
|
||||
|
||||
if (tag.value && tag.value[0] === '/' && tag.value[tag.value.length - 1] === '/') {
|
||||
return str + '"' +tag.key + '"' + ' =~ ' + tag.value;
|
||||
}
|
||||
return str + '"' + tag.key + '"' + " = '" + tag.value + "'";
|
||||
}
|
||||
|
||||
var p = InfluxQueryBuilder.prototype;
|
||||
|
||||
p.build = function() {
|
||||
return this.target.rawQuery ? this._modifyRawQuery() : this._buildQuery();
|
||||
};
|
||||
|
||||
p.buildExploreQuery = function(type, withKey) {
|
||||
var query;
|
||||
var measurement;
|
||||
|
||||
if (type === 'TAG_KEYS') {
|
||||
query = 'SHOW TAG KEYS';
|
||||
measurement = this.target.measurement;
|
||||
} else if (type === 'TAG_VALUES') {
|
||||
query = 'SHOW TAG VALUES';
|
||||
measurement = this.target.measurement;
|
||||
} else if (type === 'MEASUREMENTS') {
|
||||
query = 'SHOW MEASUREMENTS';
|
||||
} else if (type === 'FIELDS') {
|
||||
query = 'SHOW FIELD KEYS FROM "' + this.target.measurement + '"';
|
||||
return query;
|
||||
}
|
||||
|
||||
if (measurement) {
|
||||
query += ' FROM "' + measurement + '"';
|
||||
}
|
||||
|
||||
if (withKey) {
|
||||
query += ' WITH KEY = "' + withKey + '"';
|
||||
}
|
||||
|
||||
if (this.target.tags && this.target.tags.length > 0) {
|
||||
var whereConditions = _.reduce(this.target.tags, function(memo, tag) {
|
||||
// do not add a condition for the key we want to explore for
|
||||
if (tag.key === withKey) {
|
||||
return memo;
|
||||
}
|
||||
memo.push(renderTagCondition(tag, memo.length));
|
||||
return memo;
|
||||
}, []);
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
query += ' WHERE ' + whereConditions.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
p._buildQuery = function() {
|
||||
var target = this.target;
|
||||
|
||||
console.log('Build Query: target = ', target);
|
||||
|
||||
if (!target.measurement) {
|
||||
throw "Metric measurement is missing";
|
||||
}
|
||||
|
||||
var query = 'SELECT ';
|
||||
var measurement = target.measurement;
|
||||
var aggregationFunc = target.function || 'mean';
|
||||
if (!target.fields) {
|
||||
target.fields = [{name: 'value', func: target.function || 'mean'}];
|
||||
}
|
||||
|
||||
if(!measurement.match('^/.*/') && !measurement.match(/^merge\(.*\)/)) {
|
||||
var query = 'SELECT ';
|
||||
var i;
|
||||
for (i = 0; i < target.fields.length; i++) {
|
||||
var field = target.fields[i];
|
||||
if (i > 0) {
|
||||
query += ', ';
|
||||
}
|
||||
query += field.func + '(' + field.name + ')';
|
||||
}
|
||||
|
||||
var measurement = target.measurement;
|
||||
if (!measurement.match('^/.*/') && !measurement.match(/^merge\(.*\)/)) {
|
||||
measurement = '"' + measurement+ '"';
|
||||
}
|
||||
|
||||
query += aggregationFunc + '(value)';
|
||||
query += ' FROM ' + measurement + ' WHERE $timeFilter';
|
||||
query += _.map(target.tags, function(value, key) {
|
||||
return ' AND ' + key + '=' + "'" + value + "'";
|
||||
}).join('');
|
||||
query += ' FROM ' + measurement + ' WHERE ';
|
||||
var conditions = _.map(target.tags, function(tag, index) {
|
||||
return renderTagCondition(tag, index);
|
||||
});
|
||||
|
||||
query += conditions.join(' ');
|
||||
query += (conditions.length > 0 ? ' AND ' : '') + '$timeFilter';
|
||||
|
||||
query += ' GROUP BY time($interval)';
|
||||
if (target.groupByTags && target.groupByTags.length > 0) {
|
||||
query += ', "' + target.groupByTags.join('", "') + '"';
|
||||
}
|
||||
|
||||
if (target.fill) {
|
||||
query += ' fill(' + target.fill + ')';
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash'
|
||||
'lodash',
|
||||
'./queryBuilder',
|
||||
],
|
||||
function (angular, _) {
|
||||
function (angular, _, InfluxQueryBuilder) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
@@ -10,16 +11,95 @@ function (angular, _) {
|
||||
module.controller('InfluxQueryCtrl', function($scope, $timeout, $sce, templateSrv, $q) {
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.segments = $scope.target.segments || [];
|
||||
var target = $scope.target;
|
||||
target.tags = target.tags || [];
|
||||
target.groupByTags = target.groupByTags || [];
|
||||
target.fields = target.fields || [{
|
||||
name: 'value',
|
||||
func: target.function || 'mean'
|
||||
}];
|
||||
|
||||
$scope.functionsSelect = [
|
||||
'count', 'mean', 'sum', 'min',
|
||||
'max', 'mode', 'distinct', 'median',
|
||||
'derivative', 'stddev', 'first', 'last',
|
||||
'difference'
|
||||
];
|
||||
$scope.queryBuilder = new InfluxQueryBuilder(target);
|
||||
|
||||
checkOtherSegments(0);
|
||||
if (!target.measurement) {
|
||||
$scope.measurementSegment = MetricSegment.newSelectMeasurement();
|
||||
} else {
|
||||
$scope.measurementSegment = new MetricSegment(target.measurement);
|
||||
}
|
||||
|
||||
$scope.addFieldSegment = MetricSegment.newPlusButton();
|
||||
|
||||
$scope.tagSegments = [];
|
||||
_.each(target.tags, function(tag) {
|
||||
if (tag.condition) {
|
||||
$scope.tagSegments.push(MetricSegment.newCondition(tag.condition));
|
||||
}
|
||||
$scope.tagSegments.push(new MetricSegment({value: tag.key, type: 'key', cssClass: 'query-segment-key' }));
|
||||
$scope.tagSegments.push(new MetricSegment.newOperator("="));
|
||||
$scope.tagSegments.push(new MetricSegment({value: tag.value, type: 'value', cssClass: 'query-segment-value'}));
|
||||
});
|
||||
|
||||
$scope.fixTagSegments();
|
||||
|
||||
$scope.groupBySegments = [];
|
||||
_.each(target.groupByTags, function(tag) {
|
||||
$scope.groupBySegments.push(new MetricSegment(tag));
|
||||
});
|
||||
|
||||
$scope.groupBySegments.push(MetricSegment.newPlusButton());
|
||||
|
||||
$scope.removeTagFilterSegment = new MetricSegment({fake: true, value: '-- remove tag filter --'});
|
||||
$scope.removeGroupBySegment = new MetricSegment({fake: true, value: '-- remove group by --'});
|
||||
};
|
||||
|
||||
$scope.fixTagSegments = function() {
|
||||
var count = $scope.tagSegments.length;
|
||||
var lastSegment = $scope.tagSegments[Math.max(count-1, 0)];
|
||||
|
||||
if (!lastSegment || lastSegment.type !== 'plus-button') {
|
||||
$scope.tagSegments.push(MetricSegment.newPlusButton());
|
||||
}
|
||||
};
|
||||
|
||||
$scope.groupByTagUpdated = function(segment, index) {
|
||||
if (segment.value === $scope.removeGroupBySegment.value) {
|
||||
$scope.target.groupByTags.splice(index, 1);
|
||||
$scope.groupBySegments.splice(index, 1);
|
||||
$scope.$parent.get_data();
|
||||
return;
|
||||
}
|
||||
|
||||
if (index === $scope.groupBySegments.length-1) {
|
||||
$scope.groupBySegments.push(MetricSegment.newPlusButton());
|
||||
}
|
||||
|
||||
segment.type = 'group-by-key';
|
||||
segment.fake = false;
|
||||
|
||||
$scope.target.groupByTags[index] = segment.value;
|
||||
$scope.$parent.get_data();
|
||||
};
|
||||
|
||||
$scope.changeFunction = function(func) {
|
||||
$scope.target.function = func;
|
||||
$scope.$parent.get_data();
|
||||
};
|
||||
|
||||
$scope.measurementChanged = function() {
|
||||
$scope.target.measurement = $scope.measurementSegment.value;
|
||||
$scope.$parent.get_data();
|
||||
};
|
||||
|
||||
$scope.getFields = function() {
|
||||
var fieldsQuery = $scope.queryBuilder.buildExploreQuery('FIELDS');
|
||||
return $scope.datasource.metricFindQuery(fieldsQuery)
|
||||
.then(function(results) {
|
||||
var values = _.pluck(results, 'text');
|
||||
if ($scope.target.fields.length > 1) {
|
||||
values.splice(0, 0, "-- remove from select --");
|
||||
}
|
||||
return values;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.toggleQueryMode = function () {
|
||||
@@ -35,102 +115,157 @@ function (angular, _) {
|
||||
$scope.panel.targets.push(clone);
|
||||
};
|
||||
|
||||
$scope.getAltSegments = function (index) {
|
||||
$scope.altSegments = [];
|
||||
$scope.getMeasurements = function () {
|
||||
var query = $scope.queryBuilder.buildExploreQuery('MEASUREMENTS');
|
||||
return $scope.datasource.metricFindQuery(query)
|
||||
.then($scope.transformToSegments)
|
||||
.then($scope.addTemplateVariableSegments)
|
||||
.then(null, $scope.handleQueryError);
|
||||
};
|
||||
|
||||
var measurement = $scope.segments[0].value;
|
||||
var queryType, query;
|
||||
if (index === 0) {
|
||||
queryType = 'MEASUREMENTS';
|
||||
query = 'SHOW MEASUREMENTS';
|
||||
} else if (index % 2 === 1) {
|
||||
queryType = 'TAG_KEYS';
|
||||
query = 'SHOW TAG KEYS FROM ' + measurement;
|
||||
} else {
|
||||
queryType = 'TAG_VALUES';
|
||||
query = "SHOW TAG VALUES FROM " + measurement + " WITH KEY = " + $scope.segments[$scope.segments.length - 2].value;
|
||||
}
|
||||
$scope.handleQueryError = function(err) {
|
||||
$scope.parserError = err.message || 'Failed to issue metric query';
|
||||
return [];
|
||||
};
|
||||
|
||||
console.log('getAltSegments: query' , query);
|
||||
|
||||
return $scope.datasource.metricFindQuery(query, queryType).then(function(results) {
|
||||
console.log('get alt segments: response', results);
|
||||
$scope.altSegments = _.map(results, function(segment) {
|
||||
return new MetricSegment({ value: segment.text, expandable: segment.expandable });
|
||||
});
|
||||
|
||||
_.each(templateSrv.variables, function(variable) {
|
||||
$scope.altSegments.unshift(new MetricSegment({
|
||||
type: 'template',
|
||||
value: '$' + variable.name,
|
||||
expandable: true,
|
||||
}));
|
||||
});
|
||||
}, function(err) {
|
||||
$scope.parserError = err.message || 'Failed to issue metric query';
|
||||
$scope.transformToSegments = function(results) {
|
||||
return _.map(results, function(segment) {
|
||||
return new MetricSegment({ value: segment.text, expandable: segment.expandable });
|
||||
});
|
||||
};
|
||||
|
||||
$scope.segmentValueChanged = function (segment, segmentIndex) {
|
||||
delete $scope.parserError;
|
||||
|
||||
if (segment.expandable) {
|
||||
return checkOtherSegments(segmentIndex + 1).then(function () {
|
||||
setSegmentFocus(segmentIndex + 1);
|
||||
$scope.targetChanged();
|
||||
});
|
||||
}
|
||||
else {
|
||||
$scope.segments = $scope.segments.splice(0, segmentIndex + 1);
|
||||
}
|
||||
|
||||
setSegmentFocus(segmentIndex + 1);
|
||||
$scope.targetChanged();
|
||||
$scope.addTemplateVariableSegments = function(segments) {
|
||||
_.each(templateSrv.variables, function(variable) {
|
||||
segments.unshift(new MetricSegment({ type: 'template', value: '$' + variable.name, expandable: true }));
|
||||
});
|
||||
return segments;
|
||||
};
|
||||
|
||||
$scope.targetChanged = function() {
|
||||
if ($scope.parserError) {
|
||||
return;
|
||||
$scope.getTagsOrValues = function(segment, index) {
|
||||
var query;
|
||||
|
||||
if (segment.type === 'key' || segment.type === 'plus-button') {
|
||||
query = $scope.queryBuilder.buildExploreQuery('TAG_KEYS');
|
||||
} else if (segment.type === 'value') {
|
||||
query = $scope.queryBuilder.buildExploreQuery('TAG_VALUES', $scope.tagSegments[index-2].value);
|
||||
} else if (segment.type === 'condition') {
|
||||
return $q.when([new MetricSegment('AND'), new MetricSegment('OR')]);
|
||||
}
|
||||
else {
|
||||
return $q.when([]);
|
||||
}
|
||||
|
||||
$scope.target.measurement = '';
|
||||
$scope.target.tags = {};
|
||||
$scope.target.measurement = $scope.segments[0].value;
|
||||
return $scope.datasource.metricFindQuery(query)
|
||||
.then($scope.transformToSegments)
|
||||
.then($scope.addTemplateVariableSegments)
|
||||
.then(function(results) {
|
||||
if (segment.type === 'key') {
|
||||
results.splice(0, 0, angular.copy($scope.removeTagFilterSegment));
|
||||
}
|
||||
return results;
|
||||
})
|
||||
.then(null, $scope.handleQueryError);
|
||||
};
|
||||
|
||||
for (var i = 1; i+1 < $scope.segments.length; i += 2) {
|
||||
var key = $scope.segments[i].value;
|
||||
$scope.target.tags[key] = $scope.segments[i+1].value;
|
||||
$scope.getFieldSegments = function() {
|
||||
var fieldsQuery = $scope.queryBuilder.buildExploreQuery('FIELDS');
|
||||
return $scope.datasource.metricFindQuery(fieldsQuery)
|
||||
.then($scope.transformToSegments)
|
||||
.then(null, $scope.handleQueryError);
|
||||
};
|
||||
|
||||
$scope.addField = function() {
|
||||
$scope.target.fields.push({name: $scope.addFieldSegment.value, func: 'mean'});
|
||||
_.extend($scope.addFieldSegment, MetricSegment.newPlusButton());
|
||||
};
|
||||
|
||||
$scope.fieldChanged = function(field) {
|
||||
if (field.name === '-- remove from select --') {
|
||||
$scope.target.fields = _.without($scope.target.fields, field);
|
||||
}
|
||||
$scope.get_data();
|
||||
};
|
||||
|
||||
$scope.getGroupByTagSegments = function(segment) {
|
||||
var query = $scope.queryBuilder.buildExploreQuery('TAG_KEYS');
|
||||
|
||||
return $scope.datasource.metricFindQuery(query)
|
||||
.then($scope.transformToSegments)
|
||||
.then($scope.addTemplateVariableSegments)
|
||||
.then(function(results) {
|
||||
if (segment.type !== 'plus-button') {
|
||||
results.splice(0, 0, angular.copy($scope.removeGroupBySegment));
|
||||
}
|
||||
return results;
|
||||
})
|
||||
.then(null, $scope.handleQueryError);
|
||||
};
|
||||
|
||||
$scope.tagSegmentUpdated = function(segment, index) {
|
||||
$scope.tagSegments[index] = segment;
|
||||
|
||||
// handle remove tag condition
|
||||
if (segment.value === $scope.removeTagFilterSegment.value) {
|
||||
$scope.tagSegments.splice(index, 3);
|
||||
if ($scope.tagSegments.length === 0) {
|
||||
$scope.tagSegments.push(MetricSegment.newPlusButton());
|
||||
} else if ($scope.tagSegments.length > 2) {
|
||||
$scope.tagSegments.splice(Math.max(index-1, 0), 1);
|
||||
if ($scope.tagSegments[$scope.tagSegments.length-1].type !== 'plus-button') {
|
||||
$scope.tagSegments.push(MetricSegment.newPlusButton());
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (segment.type === 'plus-button') {
|
||||
if (index > 2) {
|
||||
$scope.tagSegments.splice(index, 0, MetricSegment.newCondition('AND'));
|
||||
}
|
||||
$scope.tagSegments.push(MetricSegment.newOperator('='));
|
||||
$scope.tagSegments.push(MetricSegment.newFake('select tag value', 'value', 'query-segment-value'));
|
||||
segment.type = 'key';
|
||||
segment.cssClass = 'query-segment-key';
|
||||
}
|
||||
|
||||
if ((index+1) === $scope.tagSegments.length) {
|
||||
$scope.tagSegments.push(MetricSegment.newPlusButton());
|
||||
}
|
||||
}
|
||||
|
||||
$scope.rebuildTargetTagConditions();
|
||||
};
|
||||
|
||||
$scope.rebuildTargetTagConditions = function() {
|
||||
var tags = [];
|
||||
var tagIndex = 0;
|
||||
_.each($scope.tagSegments, function(segment2, index) {
|
||||
if (segment2.type === 'key') {
|
||||
if (tags.length === 0) {
|
||||
tags.push({});
|
||||
}
|
||||
tags[tagIndex].key = segment2.value;
|
||||
}
|
||||
else if (segment2.type === 'value') {
|
||||
tags[tagIndex].value = segment2.value;
|
||||
$scope.tagSegments[index-1] = $scope.getTagValueOperator(segment2.value);
|
||||
}
|
||||
else if (segment2.type === 'condition') {
|
||||
tags.push({ condition: segment2.value });
|
||||
tagIndex += 1;
|
||||
}
|
||||
});
|
||||
|
||||
$scope.target.tags = tags;
|
||||
$scope.$parent.get_data();
|
||||
};
|
||||
|
||||
function checkOtherSegments(fromIndex) {
|
||||
if (fromIndex === 0) {
|
||||
$scope.segments.push(MetricSegment.newSelectMetric());
|
||||
return;
|
||||
$scope.getTagValueOperator = function(tagValue) {
|
||||
if (tagValue[0] === '/' && tagValue[tagValue.length - 1] === '/') {
|
||||
return MetricSegment.newOperator('=~');
|
||||
}
|
||||
|
||||
if ($scope.segments.length === 0) {
|
||||
throw('should always have a scope segment?');
|
||||
}
|
||||
|
||||
if (_.last($scope.segments).fake) {
|
||||
return $q.when([]);
|
||||
} else if ($scope.segments.length % 2 === 1) {
|
||||
$scope.segments.push(MetricSegment.newSelectTag());
|
||||
return $q.when([]);
|
||||
} else {
|
||||
$scope.segments.push(MetricSegment.newSelectTagValue());
|
||||
return $q.when([]);
|
||||
}
|
||||
}
|
||||
|
||||
function setSegmentFocus(segmentIndex) {
|
||||
_.each($scope.segments, function(segment, index) {
|
||||
segment.focus = segmentIndex === index;
|
||||
});
|
||||
}
|
||||
return MetricSegment.newOperator('=');
|
||||
};
|
||||
|
||||
function MetricSegment(options) {
|
||||
if (options === '*' || options.value === '*') {
|
||||
@@ -146,19 +281,33 @@ function (angular, _) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cssClass = options.cssClass;
|
||||
this.type = options.type;
|
||||
this.fake = options.fake;
|
||||
this.value = options.value;
|
||||
this.type = options.type;
|
||||
this.expandable = options.expandable;
|
||||
this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
|
||||
this.html = options.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
|
||||
}
|
||||
|
||||
MetricSegment.newSelectMetric = function() {
|
||||
return new MetricSegment({value: 'select metric', fake: true});
|
||||
MetricSegment.newSelectMeasurement = function() {
|
||||
return new MetricSegment({value: 'select measurement', fake: true});
|
||||
};
|
||||
|
||||
MetricSegment.newSelectTag = function() {
|
||||
return new MetricSegment({value: 'select tag', fake: true});
|
||||
MetricSegment.newFake = function(text, type, cssClass) {
|
||||
return new MetricSegment({value: text, fake: true, type: type, cssClass: cssClass});
|
||||
};
|
||||
|
||||
MetricSegment.newCondition = function(condition) {
|
||||
return new MetricSegment({value: condition, type: 'condition', cssClass: 'query-keyword' });
|
||||
};
|
||||
|
||||
MetricSegment.newOperator = function(op) {
|
||||
return new MetricSegment({value: op, type: 'operator', cssClass: 'query-segment-operator' });
|
||||
};
|
||||
|
||||
MetricSegment.newPlusButton = function() {
|
||||
return new MetricSegment({fake: true, html: '<i class="fa fa-plus "></i>', type: 'plus-button' });
|
||||
};
|
||||
|
||||
MetricSegment.newSelectTagValue = function() {
|
||||
|
||||
@@ -42,9 +42,9 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
|
||||
query = query.replace(/\$interval/g, (target.interval || options.interval));
|
||||
|
||||
// replace templated variables
|
||||
query = templateSrv.replace(query);
|
||||
query = templateSrv.replace(query, options.scopedVars);
|
||||
|
||||
var alias = target.alias ? templateSrv.replace(target.alias) : '';
|
||||
var alias = target.alias ? templateSrv.replace(target.alias, options.scopedVars) : '';
|
||||
|
||||
var handleResponse = _.partial(handleInfluxQueryResponse, alias, queryBuilder.groupByField);
|
||||
return this._seriesQuery(query).then(handleResponse);
|
||||
@@ -99,6 +99,12 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
|
||||
});
|
||||
};
|
||||
|
||||
InfluxDatasource.prototype.testDatasource = function() {
|
||||
return this.metricFindQuery('list series').then(function () {
|
||||
return { status: "success", message: "Data source is working", title: "Success" };
|
||||
});
|
||||
};
|
||||
|
||||
InfluxDatasource.prototype.metricFindQuery = function (query) {
|
||||
var interpolated;
|
||||
try {
|
||||
|
||||
474
public/app/plugins/datasource/kairosdb/datasource.js
Normal file
474
public/app/plugins/datasource/kairosdb/datasource.js
Normal file
@@ -0,0 +1,474 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'kbn',
|
||||
'./queryCtrl',
|
||||
],
|
||||
function (angular, _, kbn) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.services');
|
||||
|
||||
module.factory('KairosDBDatasource', function($q, $http, templateSrv) {
|
||||
|
||||
function KairosDBDatasource(datasource) {
|
||||
this.type = datasource.type;
|
||||
this.url = datasource.url;
|
||||
this.name = datasource.name;
|
||||
this.supportMetrics = true;
|
||||
}
|
||||
|
||||
// Called once per panel (graph)
|
||||
KairosDBDatasource.prototype.query = function(options) {
|
||||
var start = options.range.from;
|
||||
var end = options.range.to;
|
||||
|
||||
var queries = _.compact(_.map(options.targets, _.partial(convertTargetToQuery, options)));
|
||||
var plotParams = _.compact(_.map(options.targets, function(target) {
|
||||
var alias = target.alias;
|
||||
if (typeof target.alias === 'undefined' || target.alias === "") {
|
||||
alias = target.metric;
|
||||
}
|
||||
|
||||
if (!target.hide) {
|
||||
return { alias: alias, exouter: target.exOuter };
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
var handleKairosDBQueryResponseAlias = _.partial(handleKairosDBQueryResponse, plotParams);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
return this.performTimeSeriesQuery(queries, start, end).then(handleKairosDBQueryResponseAlias, handleQueryError);
|
||||
};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
/// Query methods
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
|
||||
KairosDBDatasource.prototype.performTimeSeriesQuery = function(queries, start, end) {
|
||||
var reqBody = {
|
||||
metrics: queries,
|
||||
cache_time: 0
|
||||
};
|
||||
|
||||
convertToKairosTime(start, reqBody, 'start');
|
||||
convertToKairosTime(end, reqBody, 'end');
|
||||
|
||||
var options = {
|
||||
method: 'POST',
|
||||
url: this.url + '/api/v1/datapoints/query',
|
||||
data: reqBody
|
||||
};
|
||||
|
||||
return $http(options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the list of metrics
|
||||
* @returns {*|Promise}
|
||||
*/
|
||||
KairosDBDatasource.prototype.performMetricSuggestQuery = function() {
|
||||
var options = {
|
||||
url : this.url + '/api/v1/metricnames',
|
||||
method : 'GET'
|
||||
};
|
||||
|
||||
return $http(options).then(function(response) {
|
||||
if (!response.data) {
|
||||
return [];
|
||||
}
|
||||
return response.data.results;
|
||||
});
|
||||
};
|
||||
|
||||
KairosDBDatasource.prototype.performListTagNames = function() {
|
||||
var options = {
|
||||
url : this.url + '/api/v1/tagnames',
|
||||
method : 'GET'
|
||||
};
|
||||
|
||||
return $http(options).then(function(response) {
|
||||
if (!response.data) {
|
||||
return [];
|
||||
}
|
||||
return response.data.results;
|
||||
});
|
||||
};
|
||||
|
||||
KairosDBDatasource.prototype.performListTagValues = function() {
|
||||
var options = {
|
||||
url : this.url + '/api/v1/tagvalues',
|
||||
method : 'GET'
|
||||
};
|
||||
|
||||
return $http(options).then(function(response) {
|
||||
if (!response.data) {
|
||||
return [];
|
||||
}
|
||||
return response.data.results;
|
||||
});
|
||||
};
|
||||
|
||||
KairosDBDatasource.prototype.performTagSuggestQuery = function(metricname) {
|
||||
var options = {
|
||||
url : this.url + '/api/v1/datapoints/query/tags',
|
||||
method : 'POST',
|
||||
data : {
|
||||
metrics : [{ name : metricname }],
|
||||
cache_time : 0,
|
||||
start_absolute: 0
|
||||
}
|
||||
};
|
||||
|
||||
return $http(options).then(function(response) {
|
||||
if (!response.data) {
|
||||
return [];
|
||||
}
|
||||
else {
|
||||
return response.data.queries[0].results[0];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
KairosDBDatasource.prototype.metricFindQuery = function(query) {
|
||||
function format(results, query) {
|
||||
return _.chain(results)
|
||||
.filter(function(result) {
|
||||
return result.indexOf(query) >= 0;
|
||||
})
|
||||
.map(function(result) {
|
||||
return {
|
||||
text: result,
|
||||
expandable: true
|
||||
};
|
||||
})
|
||||
.value();
|
||||
}
|
||||
|
||||
var interpolated;
|
||||
try {
|
||||
interpolated = templateSrv.replace(query);
|
||||
}
|
||||
catch (err) {
|
||||
return $q.reject(err);
|
||||
}
|
||||
|
||||
var metrics_regex = /metrics\((.*)\)/;
|
||||
var tag_names_regex = /tag_names\((.*)\)/;
|
||||
var tag_values_regex = /tag_values\((.*)\)/;
|
||||
|
||||
var metrics_query = interpolated.match(metrics_regex);
|
||||
if (metrics_query) {
|
||||
return this.performMetricSuggestQuery().then(function(metrics) {
|
||||
return format(metrics, metrics_query[1]);
|
||||
});
|
||||
}
|
||||
|
||||
var tag_names_query = interpolated.match(tag_names_regex);
|
||||
if (tag_names_query) {
|
||||
return this.performListTagNames().then(function(tag_names) {
|
||||
return format(tag_names, tag_names_query[1]);
|
||||
});
|
||||
}
|
||||
|
||||
var tag_values_query = interpolated.match(tag_values_regex);
|
||||
if (tag_values_query) {
|
||||
return this.performListTagValues().then(function(tag_values) {
|
||||
return format(tag_values, tag_values_query[1]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
/// Formatting methods
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Requires a verion of KairosDB with every CORS defects fixed
|
||||
* @param results
|
||||
* @returns {*}
|
||||
*/
|
||||
function handleQueryError(results) {
|
||||
if (results.data.errors && !_.isEmpty(results.data.errors)) {
|
||||
var errors = {
|
||||
message: results.data.errors[0]
|
||||
};
|
||||
return $q.reject(errors);
|
||||
}
|
||||
else {
|
||||
return $q.reject(results);
|
||||
}
|
||||
}
|
||||
|
||||
function handleKairosDBQueryResponse(plotParams, results) {
|
||||
var output = [];
|
||||
var index = 0;
|
||||
_.each(results.data.queries, function(series) {
|
||||
_.each(series.results, function(result) {
|
||||
var target = plotParams[index].alias;
|
||||
var details = " ( ";
|
||||
|
||||
_.each(result.group_by, function(element) {
|
||||
if (element.name === "tag") {
|
||||
_.each(element.group, function(value, key) {
|
||||
details += key + "=" + value + " ";
|
||||
});
|
||||
}
|
||||
else if (element.name === "value") {
|
||||
details += 'value_group=' + element.group.group_number + " ";
|
||||
}
|
||||
else if (element.name === "time") {
|
||||
details += 'time_group=' + element.group.group_number + " ";
|
||||
}
|
||||
});
|
||||
|
||||
details += ") ";
|
||||
|
||||
if (details !== " ( ) ") {
|
||||
target += details;
|
||||
}
|
||||
|
||||
var datapoints = [];
|
||||
|
||||
for (var i = 0; i < result.values.length; i++) {
|
||||
var t = Math.floor(result.values[i][0]);
|
||||
var v = result.values[i][1];
|
||||
datapoints[i] = [v, t];
|
||||
}
|
||||
if (plotParams[index].exouter) {
|
||||
datapoints = new PeakFilter(datapoints, 10);
|
||||
}
|
||||
output.push({ target: target, datapoints: datapoints });
|
||||
});
|
||||
|
||||
index++;
|
||||
});
|
||||
|
||||
return { data: _.flatten(output) };
|
||||
}
|
||||
|
||||
function convertTargetToQuery(options, target) {
|
||||
if (!target.metric || target.hide) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var query = {
|
||||
name: templateSrv.replace(target.metric)
|
||||
};
|
||||
|
||||
query.aggregators = [];
|
||||
|
||||
if (target.downsampling !== '(NONE)') {
|
||||
query.aggregators.push({
|
||||
name: target.downsampling,
|
||||
align_sampling: true,
|
||||
align_start_time: true,
|
||||
sampling: KairosDBDatasource.prototype.convertToKairosInterval(target.sampling || options.interval)
|
||||
});
|
||||
}
|
||||
|
||||
if (target.horizontalAggregators) {
|
||||
_.each(target.horizontalAggregators, function(chosenAggregator) {
|
||||
var returnedAggregator = {
|
||||
name:chosenAggregator.name
|
||||
};
|
||||
|
||||
if (chosenAggregator.sampling_rate) {
|
||||
returnedAggregator.sampling = KairosDBDatasource.prototype.convertToKairosInterval(chosenAggregator.sampling_rate);
|
||||
returnedAggregator.align_sampling = true;
|
||||
returnedAggregator.align_start_time =true;
|
||||
}
|
||||
|
||||
if (chosenAggregator.unit) {
|
||||
returnedAggregator.unit = chosenAggregator.unit + 's';
|
||||
}
|
||||
|
||||
if (chosenAggregator.factor && chosenAggregator.name === 'div') {
|
||||
returnedAggregator.divisor = chosenAggregator.factor;
|
||||
}
|
||||
else if (chosenAggregator.factor && chosenAggregator.name === 'scale') {
|
||||
returnedAggregator.factor = chosenAggregator.factor;
|
||||
}
|
||||
|
||||
if (chosenAggregator.percentile) {
|
||||
returnedAggregator.percentile = chosenAggregator.percentile;
|
||||
}
|
||||
query.aggregators.push(returnedAggregator);
|
||||
});
|
||||
}
|
||||
|
||||
if (_.isEmpty(query.aggregators)) {
|
||||
delete query.aggregators;
|
||||
}
|
||||
|
||||
if (target.tags) {
|
||||
query.tags = angular.copy(target.tags);
|
||||
_.forOwn(query.tags, function(value, key) {
|
||||
query.tags[key] = _.map(value, function(tag) { return templateSrv.replace(tag); });
|
||||
});
|
||||
}
|
||||
|
||||
if (target.groupByTags || target.nonTagGroupBys) {
|
||||
query.group_by = [];
|
||||
if (target.groupByTags) {
|
||||
query.group_by.push({
|
||||
name: "tag",
|
||||
tags: _.map(angular.copy(target.groupByTags), function(tag) { return templateSrv.replace(tag); })
|
||||
});
|
||||
}
|
||||
|
||||
if (target.nonTagGroupBys) {
|
||||
_.each(target.nonTagGroupBys, function(rawGroupBy) {
|
||||
var formattedGroupBy = angular.copy(rawGroupBy);
|
||||
if (formattedGroupBy.name === 'time') {
|
||||
formattedGroupBy.range_size = KairosDBDatasource.prototype.convertToKairosInterval(formattedGroupBy.range_size);
|
||||
}
|
||||
query.group_by.push(formattedGroupBy);
|
||||
});
|
||||
}
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
/// Time conversion functions specifics to KairosDB
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
KairosDBDatasource.prototype.convertToKairosInterval = function(intervalString) {
|
||||
intervalString = templateSrv.replace(intervalString);
|
||||
|
||||
var interval_regex = /(\d+(?:\.\d+)?)([Mwdhmsy])/;
|
||||
var interval_regex_ms = /(\d+(?:\.\d+)?)(ms)/;
|
||||
var matches = intervalString.match(interval_regex_ms);
|
||||
if (!matches) {
|
||||
matches = intervalString.match(interval_regex);
|
||||
}
|
||||
if (!matches) {
|
||||
throw new Error('Invalid interval string, expecting a number followed by one of "y M w d h m s ms"');
|
||||
}
|
||||
|
||||
var value = matches[1];
|
||||
var unit = matches[2];
|
||||
if (value%1 !== 0) {
|
||||
if (unit === 'ms') {
|
||||
throw new Error('Invalid interval value, cannot be smaller than the millisecond');
|
||||
}
|
||||
value = Math.round(kbn.intervals_in_seconds[unit] * value * 1000);
|
||||
unit = 'ms';
|
||||
}
|
||||
|
||||
return {
|
||||
value: value,
|
||||
unit: convertToKairosDBTimeUnit(unit)
|
||||
};
|
||||
};
|
||||
|
||||
function convertToKairosTime(date, response_obj, start_stop_name) {
|
||||
var name;
|
||||
|
||||
if (_.isString(date)) {
|
||||
if (date === 'now') {
|
||||
return;
|
||||
}
|
||||
else if (date.indexOf('now-') >= 0) {
|
||||
date = date.substring(4);
|
||||
name = start_stop_name + "_relative";
|
||||
var re_date = /(\d+)\s*(\D+)/;
|
||||
var result = re_date.exec(date);
|
||||
|
||||
if (result) {
|
||||
var value = result[1];
|
||||
var unit = result[2];
|
||||
|
||||
response_obj[name] = {
|
||||
value: value,
|
||||
unit: convertToKairosDBTimeUnit(unit)
|
||||
};
|
||||
return;
|
||||
}
|
||||
console.log("Unparseable date", date);
|
||||
return;
|
||||
}
|
||||
|
||||
date = kbn.parseDate(date);
|
||||
}
|
||||
|
||||
if (_.isDate(date)) {
|
||||
name = start_stop_name + "_absolute";
|
||||
response_obj[name] = date.getTime();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Date is neither string nor date");
|
||||
}
|
||||
|
||||
function convertToKairosDBTimeUnit(unit) {
|
||||
switch (unit) {
|
||||
case 'ms':
|
||||
return 'milliseconds';
|
||||
case 's':
|
||||
return 'seconds';
|
||||
case 'm':
|
||||
return 'minutes';
|
||||
case 'h':
|
||||
return 'hours';
|
||||
case 'd':
|
||||
return 'days';
|
||||
case 'w':
|
||||
return 'weeks';
|
||||
case 'M':
|
||||
return 'months';
|
||||
case 'y':
|
||||
return 'years';
|
||||
default:
|
||||
console.log("Unknown unit ", unit);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function PeakFilter(dataIn, limit) {
|
||||
var datapoints = dataIn;
|
||||
var arrLength = datapoints.length;
|
||||
if (arrLength <= 3) {
|
||||
return datapoints;
|
||||
}
|
||||
var LastIndx = arrLength - 1;
|
||||
|
||||
// Check first point
|
||||
var prvDelta = Math.abs((datapoints[1][0] - datapoints[0][0]) / datapoints[0][0]);
|
||||
var nxtDelta = Math.abs((datapoints[1][0] - datapoints[2][0]) / datapoints[2][0]);
|
||||
if (prvDelta >= limit && nxtDelta < limit) {
|
||||
datapoints[0][0] = datapoints[1][0];
|
||||
}
|
||||
|
||||
// Check last point
|
||||
prvDelta = Math.abs((datapoints[LastIndx - 1][0] - datapoints[LastIndx - 2][0]) / datapoints[LastIndx - 2][0]);
|
||||
nxtDelta = Math.abs((datapoints[LastIndx - 1][0] - datapoints[LastIndx][0]) / datapoints[LastIndx][0]);
|
||||
if (prvDelta >= limit && nxtDelta < limit) {
|
||||
datapoints[LastIndx][0] = datapoints[LastIndx - 1][0];
|
||||
}
|
||||
|
||||
for (var i = 1; i < arrLength - 1; i++) {
|
||||
prvDelta = Math.abs((datapoints[i][0] - datapoints[i - 1][0]) / datapoints[i - 1][0]);
|
||||
nxtDelta = Math.abs((datapoints[i][0] - datapoints[i + 1][0]) / datapoints[i + 1][0]);
|
||||
if (prvDelta >= limit && nxtDelta >= limit) {
|
||||
datapoints[i][0] = (datapoints[i - 1][0] + datapoints[i + 1][0]) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
return datapoints;
|
||||
}
|
||||
|
||||
return KairosDBDatasource;
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
<div ng-include="httpConfigPartialSrc"></div>
|
||||
@@ -0,0 +1,384 @@
|
||||
<div class="editor-row">
|
||||
<div ng-repeat="target in panel.targets"
|
||||
class="tight-form-container"
|
||||
ng-class="{'tight-form-disabled': target.hide}"
|
||||
ng-controller="KairosDBQueryCtrl"
|
||||
ng-init="init()">
|
||||
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list pull-right">
|
||||
<li class="tight-form-item">
|
||||
<div class="dropdown">
|
||||
<a class="pointer dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
tabindex="1">
|
||||
<i class="fa fa-bars"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu pull-right" role="menu">
|
||||
<li role="menuitem"><a tabindex="1" ng-click="duplicate()">Duplicate</a></li>
|
||||
<li role="menuitem"><a tabindex="1" ng-click="moveMetricQuery($index, $index-1)">Move up</a></li>
|
||||
<li role="menuitem"><a tabindex="1" ng-click="moveMetricQuery($index, $index+1)">Move down</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="tight-form-item last">
|
||||
<a class="pointer" tabindex="1" ng-click="removeDataQuery(target)">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="tight-form-list">
|
||||
<li>
|
||||
<a class="tight-form-item" ng-click="target.hide = !target.hide; targetBlur();" role="menuitem">
|
||||
<i class="fa fa-eye"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Metric
|
||||
</li>
|
||||
<li>
|
||||
<input type="text"
|
||||
class="input-large tight-form-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="fa fa-warning"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Alias
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="input-medium tight-form-input" ng-model="target.alias"
|
||||
spellcheck='false' placeholder="alias" ng-blur="targetBlur()">
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Peak filter
|
||||
<input class="input-medium" type="checkbox" ng-model="target.exOuter" ng-change="targetBlur()">
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<!-- TAGS -->
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list" role="menu">
|
||||
<li class="tight-form-item">
|
||||
<i class="fa fa-eye invisible"></i>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Tags
|
||||
</li>
|
||||
<li ng-repeat="(key, value) in target.tags track by $index" class="tight-form-item">
|
||||
{{key}} = {{value}}
|
||||
<a ng-click="removeFilterTag(key)">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="tight-form-item" ng-hide="addFilterTagMode">
|
||||
<a ng-click="addFilterTag()">
|
||||
<i class="fa fa-plus"></i>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li ng-show="addFilterTagMode">
|
||||
<input type="text"
|
||||
class="input-small tight-form-input"
|
||||
spellcheck='false'
|
||||
bs-typeahead="suggestTagKeys"
|
||||
ng-change="validateFilterTag()"
|
||||
data-min-length=0 data-items=100
|
||||
ng-model="target.currentTagKey"
|
||||
placeholder="key">
|
||||
</li>
|
||||
<li ng-show="addFilterTagMode">
|
||||
<input type="text"
|
||||
class="input-small tight-form-input"
|
||||
spellcheck='false'
|
||||
bs-typeahead="suggestTagValues"
|
||||
ng-change="validateFilterTag()"
|
||||
data-min-length=0 data-items=100
|
||||
ng-model="target.currentTagValue"
|
||||
placeholder="value">
|
||||
<a bs-tooltip="target.errors.tags"
|
||||
style="color: rgb(229, 189, 28)"
|
||||
ng-show="target.errors.tags">
|
||||
<i class="fa fa-warning"></i>
|
||||
</a>
|
||||
<li class="tight-form-item" ng-show="addFilterTagMode">
|
||||
<a ng-click="addFilterTag()">
|
||||
<i ng-show="target.errors.tags" class="fa fa-remove"></i>
|
||||
<i ng-hide="target.errors.tags" class="fa fa-plus-circle"></i>
|
||||
</a>
|
||||
</li>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<!-- GROUP BY -->
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list" role="menu">
|
||||
<li class="tight-form-item">
|
||||
<i class="fa fa-eye invisible"></i>
|
||||
</li>
|
||||
|
||||
<li class="tight-form-item">
|
||||
Group By
|
||||
</li>
|
||||
|
||||
<li class="tight-form-item" ng-show="target.groupByTags">
|
||||
tags:
|
||||
</li>
|
||||
|
||||
<li ng-repeat="key in target.groupByTags track by $index" class="tight-form-item">
|
||||
{{key}}
|
||||
<a ng-click="removeGroupByTag($index)">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="tight-form-item" ng-show="target.groupByTags && target.nonTagGroupBys">
|
||||
and by:
|
||||
</li>
|
||||
|
||||
<li ng-repeat="groupByObject in target.nonTagGroupBys track by $index" class="tight-form-item">
|
||||
{{_.values(groupByObject)}}
|
||||
<a ng-click="removeNonTagGroupBy($index)">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="tight-form-item" ng-hide="addGroupByMode">
|
||||
<a ng-click="addGroupBy()">
|
||||
<i class="fa fa-plus"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li ng-show="addGroupByMode">
|
||||
<select class="input-small tight-form-input"
|
||||
ng-change="changeGroupByInput()"
|
||||
ng-model="target.currentGroupByType"
|
||||
ng-options="f for f in ['tag','value','time']"></select>
|
||||
</li>
|
||||
<li ng-show="isTagGroupBy">
|
||||
<input type="text"
|
||||
class="input-small tight-form-input"
|
||||
spellcheck='false'
|
||||
bs-typeahead="suggestTagKeys"
|
||||
ng-change = "validateGroupBy()"
|
||||
data-min-length=0 data-items=100
|
||||
ng-model="target.groupBy.tagKey"
|
||||
placeholder="key">
|
||||
<a bs-tooltip="target.errors.groupBy.tagKey"
|
||||
style="color: rgb(229, 189, 28)"
|
||||
ng-show="target.errors.groupBy.tagKey">
|
||||
<i class="fa fa-warning"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li ng-show="isValueGroupBy">
|
||||
<input type="text"
|
||||
class="input-mini tight-form-input"
|
||||
spellcheck='false'
|
||||
ng-model="target.groupBy.valueRange"
|
||||
placeholder="range"
|
||||
bs-tooltip="'Range on which values are considered in the same group'"
|
||||
ng-change = "validateGroupBy()" >
|
||||
<a bs-tooltip="target.errors.groupBy.valueRange"
|
||||
style="color: rgb(229, 189, 28)"
|
||||
ng-show="target.errors.groupBy.valueRange">
|
||||
<i class="fa fa-warning"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li ng-show="isTimeGroupBy">
|
||||
<input type="text"
|
||||
class="input-mini tight-form-input"
|
||||
ng-model="target.groupBy.timeInterval"
|
||||
ng-init="target.groupBy.timeInterval='1s'"
|
||||
placeholder="interval"
|
||||
bs-tooltip="'Duration of time groups'"
|
||||
spellcheck='false'
|
||||
ng-change="validateGroupBy()">
|
||||
<a bs-tooltip="target.errors.groupBy.timeInterval"
|
||||
style="color: rgb(229, 189, 28)"
|
||||
ng-show="target.errors.groupBy.timeInterval">
|
||||
<i class="fa fa-warning"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li ng-show="isTimeGroupBy">
|
||||
<input type="text"
|
||||
class="input-mini tight-form-input"
|
||||
ng-model="target.groupBy.groupCount"
|
||||
placeholder="Count"
|
||||
bs-tooltip="'Number of time groups to be formed'"
|
||||
spellcheck='false'
|
||||
ng-change="validateGroupBy()">
|
||||
<a bs-tooltip="target.errors.groupBy.groupCount"
|
||||
style="color: rgb(229, 189, 28)"
|
||||
ng-show="target.errors.groupBy.groupCount">
|
||||
<i class="fa fa-warning"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="tight-form-item" ng-show="addGroupByMode">
|
||||
<a ng-click="addGroupBy()">
|
||||
<i ng-hide="isGroupByValid" class="fa fa-remove"></i>
|
||||
<i ng-show="isGroupByValid" class="fa fa-plus-circle"></i>
|
||||
</a>
|
||||
</li>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<!-- HORIZONTAL AGGREGATION -->
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list" role="menu">
|
||||
<li class="tight-form-item">
|
||||
<i class="fa fa-eye invisible"></i>
|
||||
</li>
|
||||
|
||||
<li class="tight-form-item">
|
||||
Aggregators
|
||||
</li>
|
||||
<li ng-repeat="aggregatorObject in target.horizontalAggregators track by $index" class="tight-form-item">
|
||||
{{aggregatorObject.name}}(
|
||||
<span ng-repeat="aggKey in _.keys(_.omit(aggregatorObject,'name'))" bs-tooltip="aggKey">
|
||||
{{$last?aggregatorObject[aggKey]:aggregatorObject[aggKey]+","}}
|
||||
</span>
|
||||
)
|
||||
<a ng-click="removeHorizontalAggregator($index)">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="tight-form-item" ng-hide="addHorizontalAggregatorMode">
|
||||
<a ng-click="addHorizontalAggregator()">
|
||||
<i class="fa fa-plus"></i>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li ng-show="addHorizontalAggregatorMode">
|
||||
<select class="input-medium tight-form-input"
|
||||
ng-change="changeHorAggregationInput()"
|
||||
ng-model="target.currentHorizontalAggregatorName"
|
||||
ng-options="f for f in ['avg','dev','max','min','rate','sampler','count','sum','least_squares','percentile','scale','div']"></select>
|
||||
</li>
|
||||
|
||||
<!-- Different parameters -->
|
||||
<li ng-show="hasSamplingRate" class="tight-form-item">
|
||||
every
|
||||
</li>
|
||||
<li ng-show="hasSamplingRate">
|
||||
<input type="text"
|
||||
class="input-mini tight-form-input"
|
||||
ng-model="target.horAggregator.samplingRate"
|
||||
ng-init="target.horAggregator.samplingRate='1s'"
|
||||
spellcheck='false'
|
||||
ng-change="validateHorizontalAggregator()" >
|
||||
<a bs-tooltip="target.errors.horAggregator.samplingRate"
|
||||
style="color: rgb(229, 189, 28)"
|
||||
ng-show="target.errors.horAggregator.samplingRate">
|
||||
<i class="fa fa-warning"></i>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li ng-show="hasUnit" class="tight-form-item">
|
||||
every
|
||||
</li>
|
||||
<li ng-show="hasUnit">
|
||||
<select class="input-medium tight-form-input"
|
||||
ng-model="target.horAggregator.unit"
|
||||
ng-init="target.horAggregator.unit='millisecond'"
|
||||
ng-options="f for f in ['millisecond','second','minute','hour','day','week','month','year']"></select>
|
||||
</li>
|
||||
|
||||
<li ng-show="hasFactor" class="tight-form-item">
|
||||
by
|
||||
</li>
|
||||
<li ng-show="hasFactor">
|
||||
<input type="text"
|
||||
class="input-mini tight-form-input"
|
||||
ng-model="target.horAggregator.factor"
|
||||
ng-init="target.horAggregator.factor='1'"
|
||||
spellcheck='false'
|
||||
ng-change="validateHorizontalAggregator()" >
|
||||
<a bs-tooltip="target.errors.horAggregator.factor"
|
||||
style="color: rgb(229, 189, 28)"
|
||||
ng-show="target.errors.horAggregator.factor">
|
||||
<i class="fa fa-warning"></i>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li ng-show="hasPercentile" class="tight-form-item">
|
||||
percentile
|
||||
</li>
|
||||
<li ng-show="hasPercentile">
|
||||
<input type="text"
|
||||
class="input-mini tight-form-input"
|
||||
ng-model="target.horAggregator.percentile"
|
||||
ng-init="target.horAggregator.percentile='0.75'"
|
||||
spellcheck='false'
|
||||
ng-change="validateHorizontalAggregator()" >
|
||||
<a bs-tooltip="target.errors.horAggregator.percentile"
|
||||
style="color: rgb(229, 189, 28)"
|
||||
ng-show="target.errors.horAggregator.percentile">
|
||||
<i class="fa fa-warning"></i>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="tight-form-item" ng-show="addHorizontalAggregatorMode">
|
||||
<a ng-click="addHorizontalAggregator()">
|
||||
<i ng-hide="isAggregatorValid" class="fa fa-remove"></i>
|
||||
<i ng-show="isAggregatorValid" class="fa fa-plus-circle"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="grafana-metric-options" ng-controller="KairosDBQueryCtrl">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item tight-form-item-icon">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</li>
|
||||
|
||||
<li class="tight-form-item">
|
||||
Downsampling with
|
||||
</li>
|
||||
<li>
|
||||
<select class="input-medium tight-form-input" ng-change="panelBlur()" ng-model="panel.downsampling" ng-options="f for f in ['(NONE)','avg', 'sum', 'min', 'max', 'dev']" ></select>
|
||||
</li>
|
||||
|
||||
<!-- SAMPLING RATE -->
|
||||
<li ng-hide="panel.downsampling=='(NONE)'" class="tight-form-item">
|
||||
every
|
||||
</li>
|
||||
<li>
|
||||
<input type="text"
|
||||
ng-hide="panel.downsampling=='(NONE)'"
|
||||
class="input-mini tight-form-input"
|
||||
ng-model="panel.sampling"
|
||||
placeholder="{{interval}}"
|
||||
bs-tooltip="'Leave blank for auto handling based on time range and panel width'"
|
||||
spellcheck='false'
|
||||
ng-blur="panelBlur()" >
|
||||
<a bs-tooltip="target.errors.sampling"
|
||||
style="color: rgb(229, 189, 28)"
|
||||
ng-show="target.errors.sampling">
|
||||
<i class="fa fa-warning"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</section>
|
||||
17
public/app/plugins/datasource/kairosdb/plugin.json
Normal file
17
public/app/plugins/datasource/kairosdb/plugin.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"pluginType": "datasource",
|
||||
"name": "KairosDB",
|
||||
|
||||
"type": "kairosdb",
|
||||
"serviceName": "KairosDBDatasource",
|
||||
|
||||
"module": "plugins/datasource/kairosdb/datasource",
|
||||
|
||||
"partials": {
|
||||
"config": "app/plugins/datasource/kairosdb/partials/config.html",
|
||||
"query": "app/plugins/datasource/kairosdb/partials/query.editor.html"
|
||||
},
|
||||
|
||||
"metrics": true,
|
||||
"annotations": false
|
||||
}
|
||||
367
public/app/plugins/datasource/kairosdb/queryCtrl.js
Normal file
367
public/app/plugins/datasource/kairosdb/queryCtrl.js
Normal file
@@ -0,0 +1,367 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash'
|
||||
],
|
||||
function (angular, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
var metricList = [];
|
||||
var tagList = [];
|
||||
|
||||
module.controller('KairosDBQueryCtrl', function($scope) {
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.panel.stack = false;
|
||||
if (!$scope.panel.downsampling) {
|
||||
$scope.panel.downsampling = 'avg';
|
||||
}
|
||||
if (!$scope.target.downsampling) {
|
||||
$scope.target.downsampling = $scope.panel.downsampling;
|
||||
$scope.target.sampling = $scope.panel.sampling;
|
||||
}
|
||||
$scope.target.errors = validateTarget($scope.target);
|
||||
};
|
||||
|
||||
$scope.targetBlur = function() {
|
||||
$scope.target.errors = validateTarget($scope.target);
|
||||
if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) {
|
||||
$scope.oldTarget = angular.copy($scope.target);
|
||||
$scope.get_data();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.panelBlur = function() {
|
||||
_.each($scope.panel.targets, function(target) {
|
||||
target.downsampling = $scope.panel.downsampling;
|
||||
target.sampling = $scope.panel.sampling;
|
||||
});
|
||||
$scope.get_data();
|
||||
};
|
||||
|
||||
$scope.duplicate = function() {
|
||||
var clone = angular.copy($scope.target);
|
||||
$scope.panel.targets.push(clone);
|
||||
};
|
||||
|
||||
$scope.moveMetricQuery = function(fromIndex, toIndex) {
|
||||
_.move($scope.panel.targets, fromIndex, toIndex);
|
||||
};
|
||||
|
||||
$scope.suggestMetrics = function(query, callback) {
|
||||
if (!_.isEmpty(metricList)) {
|
||||
return metricList;
|
||||
}
|
||||
else {
|
||||
$scope.datasource.performMetricSuggestQuery().then(function(result) {
|
||||
metricList = result;
|
||||
callback(metricList);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.suggestTagKeys = function(query, callback) {
|
||||
if (!_.isEmpty(tagList)) {
|
||||
var result = _.find(tagList, { name : $scope.target.metric });
|
||||
|
||||
if (!_.isEmpty(result)) {
|
||||
return _.keys(result.tags);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.datasource.performTagSuggestQuery($scope.target.metric).then(function(result) {
|
||||
if (!_.isEmpty(result)) {
|
||||
tagList.push(result);
|
||||
callback(_.keys(result.tags));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.suggestTagValues = function(query, callback) {
|
||||
if (!_.isEmpty(tagList)) {
|
||||
var result = _.find(tagList, { name : $scope.target.metric });
|
||||
|
||||
if (!_.isEmpty(result)) {
|
||||
return result.tags[$scope.target.currentTagKey];
|
||||
}
|
||||
}
|
||||
|
||||
$scope.datasource.performTagSuggestQuery($scope.target.metric).then(function(result) {
|
||||
if (!_.isEmpty(result)) {
|
||||
tagList.push(result);
|
||||
callback(result.tags[$scope.target.currentTagKey]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Filter metric by tag
|
||||
$scope.addFilterTag = function() {
|
||||
if (!$scope.addFilterTagMode) {
|
||||
$scope.addFilterTagMode = true;
|
||||
$scope.validateFilterTag();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$scope.target.tags) {
|
||||
$scope.target.tags = {};
|
||||
}
|
||||
|
||||
$scope.validateFilterTag();
|
||||
if (!$scope.target.errors.tags) {
|
||||
if (!_.has($scope.target.tags, $scope.target.currentTagKey)) {
|
||||
$scope.target.tags[$scope.target.currentTagKey] = [];
|
||||
}
|
||||
$scope.target.tags[$scope.target.currentTagKey].push($scope.target.currentTagValue);
|
||||
$scope.target.currentTagKey = '';
|
||||
$scope.target.currentTagValue = '';
|
||||
$scope.targetBlur();
|
||||
}
|
||||
|
||||
$scope.addFilterTagMode = false;
|
||||
};
|
||||
|
||||
$scope.removeFilterTag = function(key) {
|
||||
delete $scope.target.tags[key];
|
||||
if (_.size($scope.target.tags) === 0) {
|
||||
$scope.target.tags = null;
|
||||
}
|
||||
$scope.targetBlur();
|
||||
};
|
||||
|
||||
$scope.validateFilterTag = function() {
|
||||
$scope.target.errors.tags = null;
|
||||
if (!$scope.target.currentTagKey || !$scope.target.currentTagValue) {
|
||||
$scope.target.errors.tags = "You must specify a tag name and value.";
|
||||
}
|
||||
};
|
||||
|
||||
//////////////////////////////
|
||||
// GROUP BY
|
||||
//////////////////////////////
|
||||
|
||||
$scope.addGroupBy = function() {
|
||||
if (!$scope.addGroupByMode) {
|
||||
$scope.addGroupByMode = true;
|
||||
$scope.target.currentGroupByType = 'tag';
|
||||
$scope.isTagGroupBy = true;
|
||||
$scope.validateGroupBy();
|
||||
return;
|
||||
}
|
||||
$scope.validateGroupBy();
|
||||
// nb: if error is found, means that user clicked on cross : cancels input
|
||||
if (_.isEmpty($scope.target.errors.groupBy)) {
|
||||
if ($scope.isTagGroupBy) {
|
||||
if (!$scope.target.groupByTags) {
|
||||
$scope.target.groupByTags = [];
|
||||
}
|
||||
if (!_.contains($scope.target.groupByTags, $scope.target.groupBy.tagKey)) {
|
||||
$scope.target.groupByTags.push($scope.target.groupBy.tagKey);
|
||||
$scope.targetBlur();
|
||||
}
|
||||
$scope.target.groupBy.tagKey = '';
|
||||
}
|
||||
else {
|
||||
if (!$scope.target.nonTagGroupBys) {
|
||||
$scope.target.nonTagGroupBys = [];
|
||||
}
|
||||
var groupBy = {
|
||||
name: $scope.target.currentGroupByType
|
||||
};
|
||||
if ($scope.isValueGroupBy) {groupBy.range_size = $scope.target.groupBy.valueRange;}
|
||||
else if ($scope.isTimeGroupBy) {
|
||||
groupBy.range_size = $scope.target.groupBy.timeInterval;
|
||||
groupBy.group_count = $scope.target.groupBy.groupCount;
|
||||
}
|
||||
$scope.target.nonTagGroupBys.push(groupBy);
|
||||
}
|
||||
$scope.targetBlur();
|
||||
}
|
||||
$scope.isTagGroupBy = false;
|
||||
$scope.isValueGroupBy = false;
|
||||
$scope.isTimeGroupBy = false;
|
||||
$scope.addGroupByMode = false;
|
||||
};
|
||||
|
||||
$scope.removeGroupByTag = function(index) {
|
||||
$scope.target.groupByTags.splice(index, 1);
|
||||
if (_.size($scope.target.groupByTags) === 0) {
|
||||
$scope.target.groupByTags = null;
|
||||
}
|
||||
$scope.targetBlur();
|
||||
};
|
||||
|
||||
$scope.removeNonTagGroupBy = function(index) {
|
||||
$scope.target.nonTagGroupBys.splice(index, 1);
|
||||
if (_.size($scope.target.nonTagGroupBys) === 0) {
|
||||
$scope.target.nonTagGroupBys = null;
|
||||
}
|
||||
$scope.targetBlur();
|
||||
};
|
||||
|
||||
$scope.changeGroupByInput = function() {
|
||||
$scope.isTagGroupBy = $scope.target.currentGroupByType === 'tag';
|
||||
$scope.isValueGroupBy = $scope.target.currentGroupByType === 'value';
|
||||
$scope.isTimeGroupBy = $scope.target.currentGroupByType === 'time';
|
||||
$scope.validateGroupBy();
|
||||
};
|
||||
|
||||
$scope.validateGroupBy = function() {
|
||||
delete $scope.target.errors.groupBy;
|
||||
var errors = {};
|
||||
$scope.isGroupByValid = true;
|
||||
if ($scope.isTagGroupBy) {
|
||||
if (!$scope.target.groupBy.tagKey) {
|
||||
$scope.isGroupByValid = false;
|
||||
errors.tagKey = 'You must supply a tag name';
|
||||
}
|
||||
}
|
||||
|
||||
if ($scope.isValueGroupBy) {
|
||||
if (!$scope.target.groupBy.valueRange || !isInt($scope.target.groupBy.valueRange)) {
|
||||
errors.valueRange = "Range must be an integer";
|
||||
$scope.isGroupByValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($scope.isTimeGroupBy) {
|
||||
try {
|
||||
$scope.datasource.convertToKairosInterval($scope.target.groupBy.timeInterval);
|
||||
} catch (err) {
|
||||
errors.timeInterval = err.message;
|
||||
$scope.isGroupByValid = false;
|
||||
}
|
||||
if (!$scope.target.groupBy.groupCount || !isInt($scope.target.groupBy.groupCount)) {
|
||||
errors.groupCount = "Group count must be an integer";
|
||||
$scope.isGroupByValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_.isEmpty(errors)) {
|
||||
$scope.target.errors.groupBy = errors;
|
||||
}
|
||||
};
|
||||
|
||||
function isInt(n) {
|
||||
return parseInt(n) % 1 === 0;
|
||||
}
|
||||
|
||||
//////////////////////////////
|
||||
// HORIZONTAL AGGREGATION
|
||||
//////////////////////////////
|
||||
|
||||
$scope.addHorizontalAggregator = function() {
|
||||
if (!$scope.addHorizontalAggregatorMode) {
|
||||
$scope.addHorizontalAggregatorMode = true;
|
||||
$scope.target.currentHorizontalAggregatorName = 'avg';
|
||||
$scope.hasSamplingRate = true;
|
||||
$scope.validateHorizontalAggregator();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.validateHorizontalAggregator();
|
||||
// nb: if error is found, means that user clicked on cross : cancels input
|
||||
if (_.isEmpty($scope.target.errors.horAggregator)) {
|
||||
if (!$scope.target.horizontalAggregators) {
|
||||
$scope.target.horizontalAggregators = [];
|
||||
}
|
||||
var aggregator = {
|
||||
name:$scope.target.currentHorizontalAggregatorName
|
||||
};
|
||||
if ($scope.hasSamplingRate) {aggregator.sampling_rate = $scope.target.horAggregator.samplingRate;}
|
||||
if ($scope.hasUnit) {aggregator.unit = $scope.target.horAggregator.unit;}
|
||||
if ($scope.hasFactor) {aggregator.factor = $scope.target.horAggregator.factor;}
|
||||
if ($scope.hasPercentile) {aggregator.percentile = $scope.target.horAggregator.percentile;}
|
||||
$scope.target.horizontalAggregators.push(aggregator);
|
||||
$scope.targetBlur();
|
||||
}
|
||||
|
||||
$scope.addHorizontalAggregatorMode = false;
|
||||
$scope.hasSamplingRate = false;
|
||||
$scope.hasUnit = false;
|
||||
$scope.hasFactor = false;
|
||||
$scope.hasPercentile = false;
|
||||
};
|
||||
|
||||
$scope.removeHorizontalAggregator = function(index) {
|
||||
$scope.target.horizontalAggregators.splice(index, 1);
|
||||
if (_.size($scope.target.horizontalAggregators) === 0) {
|
||||
$scope.target.horizontalAggregators = null;
|
||||
}
|
||||
|
||||
$scope.targetBlur();
|
||||
};
|
||||
|
||||
$scope.changeHorAggregationInput = function() {
|
||||
$scope.hasSamplingRate = _.contains(['avg','dev','max','min','sum','least_squares','count','percentile'],
|
||||
$scope.target.currentHorizontalAggregatorName);
|
||||
$scope.hasUnit = _.contains(['sampler','rate'], $scope.target.currentHorizontalAggregatorName);
|
||||
$scope.hasFactor = _.contains(['div','scale'], $scope.target.currentHorizontalAggregatorName);
|
||||
$scope.hasPercentile = 'percentile' === $scope.target.currentHorizontalAggregatorName;
|
||||
$scope.validateHorizontalAggregator();
|
||||
};
|
||||
|
||||
$scope.validateHorizontalAggregator = function() {
|
||||
delete $scope.target.errors.horAggregator;
|
||||
var errors = {};
|
||||
$scope.isAggregatorValid = true;
|
||||
|
||||
if ($scope.hasSamplingRate) {
|
||||
try {
|
||||
$scope.datasource.convertToKairosInterval($scope.target.horAggregator.samplingRate);
|
||||
} catch (err) {
|
||||
errors.samplingRate = err.message;
|
||||
$scope.isAggregatorValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($scope.hasFactor) {
|
||||
if (!$scope.target.horAggregator.factor) {
|
||||
errors.factor = 'You must supply a numeric value for this aggregator';
|
||||
$scope.isAggregatorValid = false;
|
||||
}
|
||||
else if (parseInt($scope.target.horAggregator.factor) === 0 && $scope.target.currentHorizontalAggregatorName === 'div') {
|
||||
errors.factor = 'Cannot divide by 0';
|
||||
$scope.isAggregatorValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($scope.hasPercentile) {
|
||||
if (!$scope.target.horAggregator.percentile ||
|
||||
$scope.target.horAggregator.percentile<=0 ||
|
||||
$scope.target.horAggregator.percentile>1) {
|
||||
errors.percentile = 'Percentile must be between 0 and 1';
|
||||
$scope.isAggregatorValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_.isEmpty(errors)) {
|
||||
$scope.target.errors.horAggregator = errors;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.alert = function(message) {
|
||||
alert(message);
|
||||
};
|
||||
|
||||
// Validation
|
||||
function validateTarget(target) {
|
||||
var errs = {};
|
||||
|
||||
if (!target.metric) {
|
||||
errs.metric = "You must supply a metric name.";
|
||||
}
|
||||
|
||||
try {
|
||||
if (target.sampling) {
|
||||
$scope.datasource.convertToKairosInterval(target.sampling);
|
||||
}
|
||||
} catch (err) {
|
||||
errs.sampling = err.message;
|
||||
}
|
||||
|
||||
return errs;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -14,7 +14,6 @@ function (angular, _, kbn) {
|
||||
|
||||
function OpenTSDBDatasource(datasource) {
|
||||
this.type = 'opentsdb';
|
||||
this.editorSrc = 'app/features/opentsdb/partials/query.editor.html';
|
||||
this.url = datasource.url;
|
||||
this.name = datasource.name;
|
||||
this.supportMetrics = true;
|
||||
@@ -27,7 +26,8 @@ function (angular, _, kbn) {
|
||||
var qs = [];
|
||||
|
||||
_.each(options.targets, function(target) {
|
||||
qs.push(convertTargetToQuery(target, options.interval));
|
||||
if (!target.metric) { return; }
|
||||
qs.push(convertTargetToQuery(target, options));
|
||||
});
|
||||
|
||||
var queries = _.compact(qs);
|
||||
@@ -47,10 +47,13 @@ function (angular, _, kbn) {
|
||||
});
|
||||
|
||||
return this.performTimeSeriesQuery(queries, start, end).then(function(response) {
|
||||
var metricToTargetMapping = mapMetricsToTargets(response.data, options.targets);
|
||||
var metricToTargetMapping = mapMetricsToTargets(response.data, options);
|
||||
var result = _.map(response.data, function(metricData, index) {
|
||||
index = metricToTargetMapping[index];
|
||||
return transformMetricData(metricData, groupByTags, options.targets[index]);
|
||||
if (index === -1) {
|
||||
index = 0;
|
||||
}
|
||||
return transformMetricData(metricData, groupByTags, options.targets[index], options);
|
||||
});
|
||||
return { data: result };
|
||||
});
|
||||
@@ -76,20 +79,99 @@ function (angular, _, kbn) {
|
||||
return backendSrv.datasourceRequest(options);
|
||||
};
|
||||
|
||||
OpenTSDBDatasource.prototype.performSuggestQuery = function(query, type) {
|
||||
var options = {
|
||||
method: 'GET',
|
||||
url: this.url + '/api/suggest',
|
||||
params: {
|
||||
type: type,
|
||||
q: query
|
||||
}
|
||||
};
|
||||
return backendSrv.datasourceRequest(options).then(function(result) {
|
||||
OpenTSDBDatasource.prototype._performSuggestQuery = function(query) {
|
||||
return this._get('/api/suggest', {type: 'metrics', q: query, max: 1000}).then(function(result) {
|
||||
return result.data;
|
||||
});
|
||||
};
|
||||
|
||||
OpenTSDBDatasource.prototype._performMetricKeyValueLookup = function(metric, key) {
|
||||
if(!metric || !key) {
|
||||
return $q.when([]);
|
||||
}
|
||||
|
||||
var m = metric + "{" + key + "=*}";
|
||||
|
||||
return this._get('/api/search/lookup', {m: m}).then(function(result) {
|
||||
result = result.data.results;
|
||||
var tagvs = [];
|
||||
_.each(result, function(r) {
|
||||
tagvs.push(r.tags[key]);
|
||||
});
|
||||
return tagvs;
|
||||
});
|
||||
};
|
||||
|
||||
OpenTSDBDatasource.prototype._performMetricKeyLookup = function(metric) {
|
||||
if(!metric) { return $q.when([]); }
|
||||
|
||||
return this._get('/api/search/lookup', {m: metric}).then(function(result) {
|
||||
result = result.data.results;
|
||||
var tagks = [];
|
||||
_.each(result, function(r) {
|
||||
_.each(r.tags, function(tagv, tagk) {
|
||||
if(tagks.indexOf(tagk) === -1) {
|
||||
tagks.push(tagk);
|
||||
}
|
||||
});
|
||||
});
|
||||
return tagks;
|
||||
});
|
||||
};
|
||||
|
||||
OpenTSDBDatasource.prototype._get = function(relativeUrl, params) {
|
||||
return backendSrv.datasourceRequest({
|
||||
method: 'GET',
|
||||
url: this.url + relativeUrl,
|
||||
params: params,
|
||||
});
|
||||
};
|
||||
|
||||
OpenTSDBDatasource.prototype.metricFindQuery = function(query) {
|
||||
if (!query) { return $q.when([]); }
|
||||
|
||||
var interpolated;
|
||||
try {
|
||||
interpolated = templateSrv.replace(query);
|
||||
}
|
||||
catch (err) {
|
||||
return $q.reject(err);
|
||||
}
|
||||
|
||||
var responseTransform = function(result) {
|
||||
return _.map(result, function(value) {
|
||||
return {text: value};
|
||||
});
|
||||
};
|
||||
|
||||
var metrics_regex = /metrics\((.*)\)/;
|
||||
var tag_names_regex = /tag_names\((.*)\)/;
|
||||
var tag_values_regex = /tag_values\((.*),\s?(.*)\)/;
|
||||
|
||||
var metrics_query = interpolated.match(metrics_regex);
|
||||
if (metrics_query) {
|
||||
return this._performSuggestQuery(metrics_query[1]).then(responseTransform);
|
||||
}
|
||||
|
||||
var tag_names_query = interpolated.match(tag_names_regex);
|
||||
if (tag_names_query) {
|
||||
return this._performMetricKeyLookup(tag_names_query[1]).then(responseTransform);
|
||||
}
|
||||
|
||||
var tag_values_query = interpolated.match(tag_values_regex);
|
||||
if (tag_values_query) {
|
||||
return this._performMetricKeyValueLookup(tag_values_query[1], tag_values_query[2]).then(responseTransform);
|
||||
}
|
||||
|
||||
return $q.when([]);
|
||||
};
|
||||
|
||||
OpenTSDBDatasource.prototype.testDatasource = function() {
|
||||
return this.performSuggestQuery('cpu', 'metrics').then(function () {
|
||||
return { status: "success", message: "Data source is working", title: "Success" };
|
||||
});
|
||||
};
|
||||
|
||||
OpenTSDBDatasource.prototype.performAggregatorsQuery = function() {
|
||||
var options = {
|
||||
method: 'GET',
|
||||
@@ -104,8 +186,8 @@ function (angular, _, kbn) {
|
||||
});
|
||||
};
|
||||
|
||||
function transformMetricData(md, groupByTags, options) {
|
||||
var metricLabel = createMetricLabel(md, options, groupByTags);
|
||||
function transformMetricData(md, groupByTags, target, options) {
|
||||
var metricLabel = createMetricLabel(md, target, groupByTags, options);
|
||||
var dps = [];
|
||||
|
||||
// TSDB returns datapoints has a hash of ts => value.
|
||||
@@ -117,13 +199,13 @@ function (angular, _, kbn) {
|
||||
return { target: metricLabel, datapoints: dps };
|
||||
}
|
||||
|
||||
function createMetricLabel(md, options, groupByTags) {
|
||||
if (!_.isUndefined(options) && options.alias) {
|
||||
var scopedVars = {};
|
||||
function createMetricLabel(md, target, groupByTags, options) {
|
||||
if (target.alias) {
|
||||
var scopedVars = _.clone(options.scopedVars || {});
|
||||
_.each(md.tags, function(value, key) {
|
||||
scopedVars['tag_' + key] = {value: value};
|
||||
});
|
||||
return templateSrv.replace(options.alias, scopedVars);
|
||||
return templateSrv.replace(target.alias, scopedVars);
|
||||
}
|
||||
|
||||
var label = md.metric;
|
||||
@@ -144,13 +226,13 @@ function (angular, _, kbn) {
|
||||
return label;
|
||||
}
|
||||
|
||||
function convertTargetToQuery(target, interval) {
|
||||
function convertTargetToQuery(target, options) {
|
||||
if (!target.metric || target.hide) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var query = {
|
||||
metric: templateSrv.replace(target.metric),
|
||||
metric: templateSrv.replace(target.metric, options.scopedVars),
|
||||
aggregator: "avg"
|
||||
};
|
||||
|
||||
@@ -174,7 +256,7 @@ function (angular, _, kbn) {
|
||||
}
|
||||
|
||||
if (!target.disableDownsampling) {
|
||||
interval = templateSrv.replace(target.downsampleInterval || interval);
|
||||
var interval = templateSrv.replace(target.downsampleInterval || options.interval);
|
||||
|
||||
if (interval.match(/\.[0-9]+s/)) {
|
||||
interval = parseFloat(interval)*1000 + "ms";
|
||||
@@ -186,20 +268,20 @@ function (angular, _, kbn) {
|
||||
query.tags = angular.copy(target.tags);
|
||||
if(query.tags){
|
||||
for(var key in query.tags){
|
||||
query.tags[key] = templateSrv.replace(query.tags[key]);
|
||||
query.tags[key] = templateSrv.replace(query.tags[key], options.scopedVars);
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
function mapMetricsToTargets(metrics, targets) {
|
||||
function mapMetricsToTargets(metrics, options) {
|
||||
var interpolatedTagValue;
|
||||
return _.map(metrics, function(metricData) {
|
||||
return _.findIndex(targets, function(target) {
|
||||
return _.findIndex(options.targets, function(target) {
|
||||
return target.metric === metricData.metric &&
|
||||
_.all(target.tags, function(tagV, tagK) {
|
||||
interpolatedTagValue = templateSrv.replace(tagV);
|
||||
interpolatedTagValue = templateSrv.replace(tagV, options.scopedVars);
|
||||
return metricData.tags[tagK] === interpolatedTagValue || interpolatedTagValue === "*";
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
placeholder="metric name"
|
||||
data-min-length=0 data-items=100
|
||||
ng-model-onblur
|
||||
ng-blur="targetBlur()"
|
||||
ng-change="targetBlur()"
|
||||
>
|
||||
<a bs-tooltip="target.errors.metric"
|
||||
style="color: rgb(229, 189, 28)"
|
||||
|
||||
@@ -48,21 +48,25 @@ function (angular, _, kbn) {
|
||||
$scope.panel.targets.push(clone);
|
||||
};
|
||||
|
||||
$scope.getTextValues = function(metricFindResult) {
|
||||
return _.map(metricFindResult, function(value) { return value.text; });
|
||||
};
|
||||
|
||||
$scope.suggestMetrics = function(query, callback) {
|
||||
$scope.datasource
|
||||
.performSuggestQuery(query, 'metrics')
|
||||
$scope.datasource.metricFindQuery('metrics(' + query + ')')
|
||||
.then($scope.getTextValues)
|
||||
.then(callback);
|
||||
};
|
||||
|
||||
$scope.suggestTagKeys = function(query, callback) {
|
||||
$scope.datasource
|
||||
.performSuggestQuery(query, 'tagk')
|
||||
$scope.datasource.metricFindQuery('tag_names(' + $scope.target.metric + ')')
|
||||
.then($scope.getTextValues)
|
||||
.then(callback);
|
||||
};
|
||||
|
||||
$scope.suggestTagValues = function(query, callback) {
|
||||
$scope.datasource
|
||||
.performSuggestQuery(query, 'tagv')
|
||||
$scope.datasource.metricFindQuery('tag_values(' + $scope.target.metric + ',' + $scope.target.currentTagKey + ')')
|
||||
.then($scope.getTextValues)
|
||||
.then(callback);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user