Commit 3d178c8e by Nick Christus

Merge branch 'master' of github.com:grafana/grafana

parents d86b2c4f db9c2880
node_modules
npm-debug.log
coverage/
.aws-config.json
awsconfig
......
......@@ -8,7 +8,6 @@
* **Elasticsearch**: Support for dynamic daily indices for annotations, closes [#3061](https://github.com/grafana/grafana/issues/3061)
* **Graph Panel**: Option to hide series with all zeroes from legend and tooltip, closes [#1381](https://github.com/grafana/grafana/issues/1381), [#3336](https://github.com/grafana/grafana/issues/3336)
### Bug Fixes
* **cloudwatch**: fix for handling of period for long time ranges, fixes [#3086](https://github.com/grafana/grafana/issues/3086)
* **dashboard**: fix for collapse row by clicking on row title, fixes [#3065](https://github.com/grafana/grafana/issues/3065)
......@@ -16,6 +15,9 @@
* **graph**: layout fix for color picker when right side legend was enabled, fixes [#3093](https://github.com/grafana/grafana/issues/3093)
* **elasticsearch**: disabling elastic query (via eye) caused error, fixes [#3300](https://github.com/grafana/grafana/issues/3300)
### Breaking changes
* **elasticsearch**: Manual json edited queries are not supported any more (They very barely worked in 2.5)
# 2.5 (2015-10-28)
**New Feature: Mix data sources**
......
......@@ -90,7 +90,7 @@ Replace X.Y.Z by actual version number.
cd $GOPATH/src/github.com/grafana/grafana
go run build.go setup (only needed once to install godep)
godep restore (will pull down all golang lib dependencies in your current GOPATH)
godep go run build.go build
go run build.go build
```
### Building frontend assets
......
......@@ -63,15 +63,10 @@ Name | Description
`namespaces()` | Returns a list of namespaces CloudWatch support.
`metrics(namespace)` | Returns a list of metrics in the namespace.
`dimension_keys(namespace)` | Returns a list of dimension keys in the namespace.
`dimension_values(region, namespace, metric)` | Returns a list of dimension values matching the specified `region`, `namespace` and `metric`.
`dimension_values(region, namespace, metric, dimension_key)` | Returns a list of dimension values matching the specified `region`, `namespace`, `metric` and `dimension_key`.
For details about the metrics CloudWatch provides, please refer to the [CloudWatch documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/CW_Support_For_AWS.html).
If you want to filter dimension values by other dimension key/value pair, you can specify optional parameter like this.
```sql
dimension_values(region, namespace, metric, dim_key1=dim_val1,dim_key2=dim_val2,...)
```
![](/img/v2/cloudwatch_templating.png)
## Cost
......
import {transformers} from './transformers';
export class TableModel {
class TableModel {
columns: any[];
rows: any[];
type: string;
constructor() {
this.columns = [];
this.rows = [];
this.type = 'table';
}
sort(options) {
......@@ -33,20 +34,6 @@ export class TableModel {
this.columns[options.col].desc = true;
}
}
static transform(data, panel) {
var model = new TableModel();
if (!data || data.length === 0) {
return model;
}
var transformer = transformers[panel.transform];
if (!transformer) {
throw {message: 'Transformer ' + panel.transformer + ' not found'};
}
transformer.transform(data, panel, model);
return model;
}
}
export = TableModel;
......@@ -13,7 +13,7 @@ function (angular, _, config) {
$scope.httpConfigPartialSrc = 'app/features/org/partials/datasourceHttpConfig.html';
var defaults = {name: '', type: 'graphite', url: '', access: 'proxy' };
var defaults = {name: '', type: 'graphite', url: '', access: 'proxy', jsonData: {}};
$scope.indexPatternTypes = [
{name: 'No pattern', value: undefined},
......@@ -24,6 +24,11 @@ function (angular, _, config) {
{name: 'Yearly', value: 'Yearly', example: '[logstash-]YYYY'},
];
$scope.esVersions = [
{name: '1.x', value: 1},
{name: '2.x', value: 2},
];
$scope.init = function() {
$scope.isNew = true;
$scope.datasources = [];
......
......@@ -5,7 +5,7 @@ import _ = require('lodash');
import moment = require('moment');
import PanelMeta = require('app/features/panel/panel_meta');
import {TableModel} from './table_model';
import {transformDataToTable} from './transformers';
export class TablePanelCtrl {
......@@ -104,7 +104,23 @@ export class TablePanelCtrl {
};
$scope.render = function() {
$scope.table = TableModel.transform($scope.dataRaw, $scope.panel);
// automatically correct transform mode
// based on data
if ($scope.dataRaw && $scope.dataRaw.length) {
if ($scope.dataRaw[0].type === 'table') {
$scope.panel.transform = 'table';
} else {
if ($scope.dataRaw[0].type === 'docs') {
$scope.panel.transform = 'json';
} else {
if ($scope.panel.transform === 'table' || $scope.panel.transform === 'json') {
$scope.panel.transform = 'timeseries_to_rows';
}
}
}
}
$scope.table = transformDataToTable($scope.dataRaw, $scope.panel);
$scope.table.sort($scope.panel.sort);
panelHelper.broadcastRender($scope, $scope.table, $scope.dataRaw);
};
......
import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
import {TableModel} from '../table_model';
import TableModel = require('app/core/table_model');
import {TableRenderer} from '../renderer';
describe('when rendering table', () => {
......
import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
import {TableModel} from '../table_model';
import {transformers} from '../transformers';
import {transformers, transformDataToTable} from '../transformers';
describe('when transforming time series table', () => {
var table;
......@@ -26,7 +25,7 @@ describe('when transforming time series table', () => {
};
beforeEach(() => {
table = TableModel.transform(timeSeries, panel);
table = transformDataToTable(timeSeries, panel);
});
it('should return 3 rows', () => {
......@@ -51,7 +50,7 @@ describe('when transforming time series table', () => {
};
beforeEach(() => {
table = TableModel.transform(timeSeries, panel);
table = transformDataToTable(timeSeries, panel);
});
it ('should return 3 columns', () => {
......@@ -80,7 +79,7 @@ describe('when transforming time series table', () => {
};
beforeEach(() => {
table = TableModel.transform(timeSeries, panel);
table = transformDataToTable(timeSeries, panel);
});
it('should return 2 rows', () => {
......@@ -133,7 +132,7 @@ describe('when transforming time series table', () => {
describe('transform', function() {
beforeEach(() => {
table = TableModel.transform(rawData, panel);
table = transformDataToTable(rawData, panel);
});
it ('should return 2 columns', () => {
......@@ -164,7 +163,7 @@ describe('when transforming time series table', () => {
];
beforeEach(() => {
table = TableModel.transform(rawData, panel);
table = transformDataToTable(rawData, panel);
});
it ('should return 4 columns', () => {
......
......@@ -4,6 +4,7 @@ import moment = require('moment');
import _ = require('lodash');
import flatten = require('app/core/utils/flatten');
import TimeSeries = require('app/core/time_series');
import TableModel = require('app/core/table_model');
var transformers = {};
......@@ -136,6 +137,27 @@ transformers['annotations'] = {
}
};
transformers['table'] = {
description: 'Table',
getColumns: function(data) {
if (!data || data.length === 0) {
return [];
}
},
transform: function(data, panel, model) {
if (!data || data.length === 0) {
return;
}
if (data[0].type !== 'table') {
throw {message: 'Query result is not in table format, try using another transform.'};
}
model.columns = data[0].columns;
model.rows = data[0].rows;
}
};
transformers['json'] = {
description: 'JSON Data',
getColumns: function(data) {
......@@ -197,4 +219,20 @@ transformers['json'] = {
}
};
export {transformers}
function transformDataToTable(data, panel) {
var model = new TableModel();
if (!data || data.length === 0) {
return model;
}
var transformer = transformers[panel.transform];
if (!transformer) {
throw {message: 'Transformer ' + panel.transformer + ' not found'};
}
transformer.transform(data, panel, model);
return model;
}
export {transformers, transformDataToTable}
......@@ -113,23 +113,28 @@ function (angular, _) {
});
};
CloudWatchDatasource.prototype.getDimensionValues = function(region, namespace, metricName, dimensions) {
CloudWatchDatasource.prototype.getDimensionValues = function(region, namespace, metricName, dimensionKey, filterDimensions) {
var request = {
region: templateSrv.replace(region),
action: 'ListMetrics',
parameters: {
namespace: templateSrv.replace(namespace),
metricName: templateSrv.replace(metricName),
dimensions: convertDimensionFormat(dimensions, {}),
dimensions: convertDimensionFormat(filterDimensions, {}),
}
};
return this.awsRequest(request).then(function(result) {
return _.chain(result.Metrics).map(function(metric) {
return _.pluck(metric.Dimensions, 'Value');
}).flatten().uniq().sortBy(function(name) {
return name;
}).map(function(value) {
return _.chain(result.Metrics)
.pluck('Dimensions')
.flatten()
.filter(function(dimension) {
return dimension.Name === dimensionKey;
})
.pluck('Value')
.uniq()
.sortBy()
.map(function(value) {
return {value: value, text: value};
}).value();
});
......@@ -174,25 +179,14 @@ function (angular, _) {
return this.getDimensionKeys(dimensionKeysQuery[1]);
}
var dimensionValuesQuery = query.match(/^dimension_values\(([^,]+?),\s?([^,]+?),\s?([^,]+?)(,\s?([^)]*))?\)/);
var dimensionValuesQuery = query.match(/^dimension_values\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/);
if (dimensionValuesQuery) {
region = templateSrv.replace(dimensionValuesQuery[1]);
namespace = templateSrv.replace(dimensionValuesQuery[2]);
metricName = templateSrv.replace(dimensionValuesQuery[3]);
var dimensionPart = templateSrv.replace(dimensionValuesQuery[5]);
var dimensions = {};
if (!_.isEmpty(dimensionPart)) {
_.each(dimensionPart.split(','), function(v) {
var t = v.split('=');
if (t.length !== 2) {
throw new Error('Invalid query format');
}
dimensions[t[0]] = t[1];
});
}
var dimensionKey = templateSrv.replace(dimensionValuesQuery[4]);
return this.getDimensionValues(region, namespace, metricName, dimensions);
return this.getDimensionValues(region, namespace, metricName, dimensionKey, {});
}
var ebsVolumeIdsQuery = query.match(/^ebs_volume_ids\(([^,]+?),\s?([^,]+?)\)/);
......@@ -222,7 +216,7 @@ function (angular, _) {
var metricName = 'EstimatedCharges';
var dimensions = {};
return this.getDimensionValues(region, namespace, metricName, dimensions).then(function () {
return this.getDimensionValues(region, namespace, metricName, 'ServiceName', dimensions).then(function () {
return { status: 'success', message: 'Data source is working', title: 'Success' };
});
};
......
......@@ -76,7 +76,7 @@ function (angular, _) {
}
};
$scope.getDimSegments = function(segment) {
$scope.getDimSegments = function(segment, $index) {
if (segment.type === 'operator') { return $q.when([]); }
var target = $scope.target;
......@@ -85,7 +85,8 @@ function (angular, _) {
if (segment.type === 'key' || segment.type === 'plus-button') {
query = $scope.datasource.getDimensionKeys($scope.target.namespace);
} else if (segment.type === 'value') {
query = $scope.datasource.getDimensionValues(target.region, target.namespace, target.metricName, {});
var dimensionKey = $scope.dimSegments[$index-2].value;
query = $scope.datasource.getDimensionValues(target.region, target.namespace, target.metricName, dimensionKey, {});
}
return query.then($scope.transformToSegments(true)).then(function(results) {
......
......@@ -165,7 +165,7 @@ describe('CloudWatchDatasource', function() {
});
});
describeMetricFindQuery('dimension_values(us-east-1,AWS/EC2,CPUUtilization)', scenario => {
describeMetricFindQuery('dimension_values(us-east-1,AWS/EC2,CPUUtilization,InstanceId)', scenario => {
scenario.setup(() => {
scenario.requestResponse = {
Metrics: [
......
......@@ -23,9 +23,11 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
this.name = datasource.name;
this.index = datasource.index;
this.timeField = datasource.jsonData.timeField;
this.esVersion = datasource.jsonData.esVersion;
this.indexPattern = new IndexPattern(datasource.index, datasource.jsonData.interval);
this.queryBuilder = new ElasticQueryBuilder({
timeField: this.timeField
timeField: this.timeField,
esVersion: this.esVersion,
});
}
......@@ -94,7 +96,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
var payload = angular.toJson(header) + '\n' + angular.toJson(data) + '\n';
return this._post('/_msearch', payload).then(function(res) {
return this._post('_msearch', payload).then(function(res) {
var list = [];
var hits = res.responses[0].hits.hits;
......@@ -183,12 +185,16 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
sentTargets.push(target);
}
if (sentTargets.length === 0) {
return $q.when([]);
}
payload = payload.replace(/\$interval/g, options.interval);
payload = payload.replace(/\$timeFrom/g, options.range.from.valueOf());
payload = payload.replace(/\$timeTo/g, options.range.to.valueOf());
payload = templateSrv.replace(payload, options.scopedVars);
return this._post('/_msearch', payload).then(function(res) {
return this._post('_msearch', payload).then(function(res) {
return new ElasticResponse(sentTargets, res).getTimeSeries();
});
};
......
......@@ -20,7 +20,7 @@
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form last">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 144px">
Time field name
......@@ -31,3 +31,14 @@
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 144px">
Version
</li>
<li>
<select class="input-medium tight-form-input" ng-model="current.jsonData.esVersion" ng-options="f.value as f.name for f in esVersions"></select>
</li>
</ul>
<div class="clearfix"></div>
</div>
......@@ -14,7 +14,6 @@
<i class="fa fa-bars"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem"><a tabindex="1" ng-click="toggleQueryMode()">Switch editor mode</a></li>
<li role="menuitem"><a tabindex="1" ng-click="duplicateDataQuery(target)">Duplicate</a></li>
<li role="menuitem"><a tabindex="1" ng-click="moveDataQuery($index, $index-1)">Move up</a></li>
<li role="menuitem"><a tabindex="1" ng-click="moveDataQuery($index, $index+1)">Move down</a></li>
......
define([
"angular"
],
function (angular) {
function () {
'use strict';
function ElasticQueryBuilder(options) {
this.timeField = options.timeField;
this.esVersion = options.esVersion;
}
ElasticQueryBuilder.prototype.getRangeFilter = function() {
var filter = {};
filter[this.timeField] = {"gte": "$timeFrom", "lte": "$timeTo"};
if (this.esVersion >= 2) {
filter[this.timeField]["format"] = "epoch_millis";
}
return filter;
};
......@@ -82,9 +87,11 @@ function (angular) {
};
ElasticQueryBuilder.prototype.build = function(target) {
if (target.rawQuery) {
return angular.fromJson(target.rawQuery);
}
// make sure query has defaults;
target.metrics = target.metrics || [{ type: 'count', id: '1' }];
target.dsType = 'elasticsearch';
target.bucketAggs = target.bucketAggs || [{type: 'date_histogram', id: '2', settings: {interval: 'auto'}}];
target.timeField = this.timeField;
var i, nestedAggs, metric;
var query = {
......@@ -129,6 +136,9 @@ function (angular) {
"min_doc_count": 0,
"extended_bounds": { "min": "$timeFrom", "max": "$timeTo" }
};
if (this.esVersion >= 2) {
esAgg["date_histogram"]["format"] = "epoch_millis";
}
break;
}
case 'filters': {
......
......@@ -12,9 +12,7 @@ function (angular) {
var target = $scope.target;
if (!target) { return; }
target.metrics = target.metrics || [{ type: 'count', id: '1' }];
target.bucketAggs = target.bucketAggs || [{type: 'date_histogram', id: '2', settings: {interval: 'auto'}}];
target.timeField = $scope.datasource.timeField;
$scope.queryUpdated();
};
$scope.getFields = function(type) {
......@@ -39,14 +37,6 @@ function (angular) {
return [];
};
$scope.toggleQueryMode = function () {
if ($scope.target.rawQuery) {
delete $scope.target.rawQuery;
} else {
$scope.target.rawQuery = angular.toJson($scope.datasource.queryBuilder.build($scope.target), true);
}
};
$scope.init();
});
......
......@@ -22,14 +22,6 @@ describe('ElasticQueryBuilder', function() {
expect(query.aggs["1"].date_histogram.extended_bounds.min).to.be("$timeFrom");
});
it('with raw query', function() {
var query = builder.build({
rawQuery: '{"query": "$lucene_query"}',
});
expect(query.query).to.be("$lucene_query");
});
it('with multiple bucket aggs', function() {
var query = builder.build({
metrics: [{type: 'count', id: '1'}],
......@@ -44,6 +36,39 @@ describe('ElasticQueryBuilder', function() {
expect(query.aggs["2"].aggs["3"].date_histogram.field).to.be("@timestamp");
});
it('with es1.x and es2.x date histogram queries check time format', function() {
var builder_2x = new ElasticQueryBuilder({
timeField: '@timestamp',
esVersion: 2
});
var query_params = {
metrics: [],
bucketAggs: [
{type: 'date_histogram', field: '@timestamp', id: '1'}
],
};
// format should not be specified in 1.x queries
expect("format" in builder.build(query_params)["aggs"]["1"]["date_histogram"]).to.be(false);
// 2.x query should specify format to be "epoch_millis"
expect(builder_2x.build(query_params)["aggs"]["1"]["date_histogram"]["format"]).to.be("epoch_millis");
});
it('with es1.x and es2.x range filter check time format', function() {
var builder_2x = new ElasticQueryBuilder({
timeField: '@timestamp',
esVersion: 2
});
// format should not be specified in 1.x queries
expect("format" in builder.getRangeFilter()["@timestamp"]).to.be(false);
// 2.x query should specify format to be "epoch_millis"
expect(builder_2x.getRangeFilter()["@timestamp"]["format"]).to.be("epoch_millis");
});
it('with select field', function() {
var query = builder.build({
metrics: [{type: 'avg', field: '@value', id: '1'}],
......
......@@ -53,6 +53,7 @@ function (angular, _, dateMath, InfluxSeries, InfluxQuery) {
// replace templated variables
allQueries = templateSrv.replace(allQueries, options.scopedVars);
return this._seriesQuery(allQueries).then(function(data) {
if (!data || !data.results) {
return [];
......@@ -63,13 +64,26 @@ function (angular, _, dateMath, InfluxSeries, InfluxQuery) {
var result = data.results[i];
if (!result || !result.series) { continue; }
var alias = (queryTargets[i] || {}).alias;
var target = queryTargets[i];
var alias = target.alias;
if (alias) {
alias = templateSrv.replace(alias, options.scopedVars);
alias = templateSrv.replace(target.alias, options.scopedVars);
}
var influxSeries = new InfluxSeries({ series: data.results[i].series, alias: alias });
switch(target.resultFormat) {
case 'table': {
seriesList.push(influxSeries.getTable());
break;
}
default: {
var timeSeries = influxSeries.getTimeSeries();
for (y = 0; y < timeSeries.length; y++) {
seriesList.push(timeSeries[y]);
}
break;
}
var targetSeries = new InfluxSeries({ series: data.results[i].series, alias: alias }).getTimeSeries();
for (y = 0; y < targetSeries.length; y++) {
seriesList.push(targetSeries[y]);
}
}
......
......@@ -12,6 +12,8 @@ class InfluxQuery {
constructor(target) {
this.target = target;
target.dsType = 'influxdb';
target.resultFormat = target.resultFormat || 'time_series';
target.tags = target.tags || [];
target.groupBy = target.groupBy || [
{type: 'time', params: ['$interval']},
......
define([
'lodash',
'app/core/table_model',
],
function (_) {
function (_, TableModel) {
'use strict';
function InfluxSeries(options) {
......@@ -108,5 +109,44 @@ function (_) {
return list;
};
p.getTable = function() {
var table = new TableModel();
var self = this;
var i, j;
if (self.series.length === 0) {
return table;
}
_.each(self.series, function(series, seriesIndex) {
if (seriesIndex === 0) {
table.columns.push({text: 'Time', type: 'time'});
_.each(_.keys(series.tags), function(key) {
table.columns.push({text: key});
});
for (j = 1; j < series.columns.length; j++) {
table.columns.push({text: series.columns[j]});
}
}
if (series.values) {
for (i = 0; i < series.values.length; i++) {
var values = series.values[i];
if (series.tags) {
for (var key in series.tags) {
if (series.tags.hasOwnProperty(key)) {
values.splice(1, 0, series.tags[key]);
}
}
}
table.rows.push(values);
}
}
});
return table;
};
return InfluxSeries;
});
......@@ -103,6 +103,12 @@
<li>
<input type="text" class="tight-form-clear-input input-xlarge" ng-model="target.alias" spellcheck='false' placeholder="Naming pattern" ng-blur="get_data()">
</li>
<li class="tight-form-item">
Format as
</li>
<li>
<select class="input-small tight-form-input" style="width: 104px" ng-model="target.resultFormat" ng-options="f.value as f.text for f in resultFormats" ng-change="get_data()"></select>
</li>
</ul>
<div class="clearfix"></div>
</div>
......
......@@ -20,6 +20,11 @@ function (angular, _, InfluxQueryBuilder, InfluxQuery, queryPart) {
$scope.queryModel = new InfluxQuery($scope.target);
$scope.queryBuilder = new InfluxQueryBuilder($scope.target);
$scope.groupBySegment = uiSegmentSrv.newPlusButton();
$scope.resultFormats = [
{text: 'Time series', value: 'time_series'},
{text: 'Table', value: 'table'},
{text: 'JSON field', value: 'json_field'},
];
if (!$scope.target.measurement) {
$scope.measurementSegment = uiSegmentSrv.newSelectMeasurement();
......
......@@ -186,5 +186,28 @@ describe('when generating timeseries from influxdb response', function() {
});
});
describe('given table response', function() {
var options = {
alias: '',
series: [
{
name: 'app.prod.server1.count',
tags: {},
columns: ['time', 'datacenter', 'value'],
values: [[1431946625000, 'America', 10], [1431946626000, 'EU', 12]]
}
]
};
it('should return table', function() {
var series = new InfluxSeries(options);
var table = series.getTable();
expect(table.type).to.be('table');
expect(table.columns.length).to.be(3);
expect(table.rows[0]).to.eql([1431946625000, 'America', 10]);;
});
});
});
import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
import {TableModel} from '../table_model';
import TableModel = require('app/core/table_model');
describe('when sorting table desc', () => {
var table;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment