Merge remote-tracking branch 'origin/kariosdb'

Conflicts:
	public/test/test-main.js
This commit is contained in:
Torkel Ödegaard 2015-06-25 18:34:41 +02:00
commit 9266d3789b
10 changed files with 1357 additions and 2 deletions

View File

@ -60,6 +60,7 @@ pages:
- ['datasources/graphite.md', 'Data Sources', 'Graphite']
- ['datasources/influxdb.md', 'Data Sources', 'InfluxDB']
- ['datasources/opentsdb.md', 'Data Sources', 'OpenTSDB']
- ['datasources/kairosdb.md', 'Data Sources', 'KairosDB']
- ['project/building_from_source.md', 'Project', 'Building from source']
- ['project/cla.md', 'Project', 'Contributor License Agreement']

View File

@ -0,0 +1,47 @@
---
page_title: KairosDB Guide
page_description: KairosDB guide for Grafana
page_keywords: grafana, kairosdb, documentation
---
# KairosDB Guide
## Adding the data source to Grafana
Open the side menu by clicking the the Grafana icon in the top header. In the side menu under the `Dashboards` link you
should find a link named `Data Sources`. If this link is missing in the side menu it means that your current
user does not have the `Admin` role for the current organization.
<!-- ![](/img/v2/add_datasource_kairosdb.png) -->
Now click the `Add new` link in the top header.
Name | Description
------------ | -------------
Name | The data source name, important that this is the same as in Grafana v1.x if you plan to import old dashboards.
Default | Default data source means that it will be pre-selected for new panels.
Url | The http protocol, ip and port of your kairosdb server (default port is usually 8080)
Access | Proxy = access via Grafana backend, Direct = access directory from browser.
## Query editor
Open a graph in edit mode by click the title.
<!-- ![](/img/v2/kairosdb_query_editor.png) -->
For details on KairosDB metric queries checkout the offical.
- [Query Metrics - KairosDB 0.9.4 documentation](http://kairosdb.github.io/kairosdocs/restapi/QueryMetrics.html).
## Templated queries
KairosDB Datasource Plugin provides following functions in `Variables values query` field in Templating Editor to query `metric names`, `tag names`, and `tag values` to kairosdb server.
Name | Description
---- | ----
`metrics(query)` | Returns a list of metric names. If nothing is given, returns a list of all metric names.
`tag_names(query)` | Returns a list of tag names. If nothing is given, returns a list of all tag names.
`tag_values(query)` | Returns a list of tag values. If nothing is given, returns a list of all tag values.
For details of `metric names`, `tag names`, and `tag values`, please refer to the KairosDB documentations.
- [List Metric Names - KairosDB 0.9.4 documentation](http://kairosdb.github.io/kairosdocs/restapi/ListMetricNames.html)
- [List Tag Names - KairosDB 0.9.4 documentation](http://kairosdb.github.io/kairosdocs/restapi/ListTagNames.html)
- [List Tag Values - KairosDB 0.9.4 documentation](http://kairosdb.github.io/kairosdocs/restapi/ListTagValues.html)

View File

@ -10,7 +10,7 @@ It provides a powerful and elegant way to create, share, and explore data and da
Grafana is most commonly used for Internet infrastructure and application analytics, but many use it in other domains including industrial sensors, home automation, weather, and process control.
Grafana features pluggable panels and data sources allowing easy extensibility. There is currently rich support for [Graphite](http://graphite.readthedocs.org/en/latest/), [InfluxDB](http://influxdb.org) and [OpenTSDB](http://opentsdb.net). There is also experimental support for KairosDB, and SQL is on the roadmap. Grafana has a variety of panels, including a fully featured graph panel with rich visualization options.
Grafana features pluggable panels and data sources allowing easy extensibility. There is currently rich support for [Graphite](http://graphite.readthedocs.org/en/latest/), [InfluxDB](http://influxdb.org) and [OpenTSDB](http://opentsdb.net). There is also experimental support for [KairosDB](https://github.com/kairosdb/kairosdb), and SQL is on the roadmap. Grafana has a variety of panels, including a fully featured graph panel with rich visualization options.
Version 2.0 was released in April 2015: Grafana now ships with its own backend server that brings [many changes and features](../guides/whats-new-in-v2/).

View File

@ -0,0 +1,475 @@
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.editorSrc = 'plugins/datasources/kairosdb/kairosdb.editor.html';
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;
});
});

View File

@ -0,0 +1 @@
<div ng-include="httpConfigPartialSrc"></div>

View File

@ -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">
&nbsp;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}}&nbsp;=&nbsp;{{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}}&#40;
<span ng-repeat="aggKey in _.keys(_.omit(aggregatorObject,'name'))" bs-tooltip="aggKey">
{{$last?aggregatorObject[aggKey]:aggregatorObject[aggKey]+","}}
</span>
&#41;
<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>

View 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
}

View 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;
}
});
});

View File

@ -0,0 +1,63 @@
define([
'helpers',
'plugins/datasource/kairosdb/datasource'
], function(helpers) {
'use strict';
describe('KairosDBDatasource', function() {
var ctx = new helpers.ServiceTestContext();
beforeEach(module('grafana.services'));
beforeEach(ctx.providePhase(['templateSrv']));
beforeEach(ctx.createService('KairosDBDatasource'));
beforeEach(function() {
ctx.ds = new ctx.service({ url: ''});
});
describe('When querying kairosdb with one target using query editor target spec', function() {
var results;
var urlExpected = "/api/v1/datapoints/query";
var bodyExpected = {
metrics: [{ name: "test" }],
cache_time: 0,
start_relative: {
value: "1",
unit: "hours"
}
};
var query = {
range: { from: 'now-1h', to: 'now' },
targets: [{ metric: 'test', downsampling: '(NONE)'}]
};
var response = {
queries: [{
sample_size: 60,
results: [{
name: "test",
values: [[1420070400000, 1]]
}]
}]
};
beforeEach(function() {
ctx.$httpBackend.expect('POST', urlExpected, bodyExpected).respond(response);
ctx.ds.query(query).then(function(data) { results = data; });
ctx.$httpBackend.flush();
});
it('should generate the correct query', function() {
ctx.$httpBackend.verifyNoOutstandingExpectation();
});
it('should return series list', function() {
expect(results.data.length).to.be(1);
expect(results.data[0].target).to.be('test');
});
});
});
});

View File

@ -130,6 +130,7 @@ require([
'specs/influx09-querybuilder-specs',
'specs/influxdb-datasource-specs',
'specs/influxdbQueryCtrl-specs',
'specs/kairosdb-datasource-specs',
'specs/graph-ctrl-specs',
'specs/graph-specs',
'specs/graph-tooltip-specs',
@ -155,4 +156,3 @@ require([
window.__karma__.start();
});
});