Commit 6044b3ae by Marcus Efraimsson

Merge branch 'master' into mssql_datasource

parents 449a3075 ec007f53
......@@ -5,6 +5,7 @@
* **Graph**: Thresholds for Right Y axis [#7107](https://github.com/grafana/grafana/issues/7107), thx [@ilgizar](https://github.com/ilgizar)
* **Graph**: Support multiple series stacking in histogram mode [#8151](https://github.com/grafana/grafana/issues/8151), thx [@mtanda](https://github.com/mtanda)
* **Alerting**: Pausing/un alerts now updates new_state_date [#10942](https://github.com/grafana/grafana/pull/10942)
* **Alerting**: Support Pagerduty notification channel using Pagerduty V2 API [#10531](https://github.com/grafana/grafana/issues/10531), thx [@jbaublitz](https://github.com/jbaublitz)
* **Templating**: Add comma templating format [#10632](https://github.com/grafana/grafana/issues/10632), thx [@mtanda](https://github.com/mtanda)
* **Prometheus**: Support POST for query and query_range [#9859](https://github.com/grafana/grafana/pull/9859), thx [@mtanda](https://github.com/mtanda)
......
......@@ -6,18 +6,21 @@ But it will give you an idea of our current vision and plan.
### Short term (1-2 months)
- v5.1
- Crossplatform builds & build speed improvements
- Build speed improvements & integration test execution
- Kubernetes friendly docker container
- Enterprise LDAP
- Provisioning workflow
- First login registration view
- IFQL Initial support
- MSSQL datasource
### Mid term (2-4 months)
- v5.2
- Azure monitor backend rewrite
- Elasticsearch alerting
- First login registration view
- Backend plugins? (alert notifiers, auth)
- Crossplatform builds
- IFQL Initial support
### Long term (4 - 8 months)
......
......@@ -9,30 +9,38 @@ weight = 10
# How to setup Grafana for high availability
> Alerting does not support high availability yet.
Setting up Grafana for high availability is fairly simple. It comes down to two things:
* Use a shared database for multiple grafana instances.
* Consider how user sessions are stored.
1. Use a shared database for storing dashboard, users, and other persistent data
2. Decide how to store session data.
<div class="text-center">
<img src="/img/docs/tutorials/grafana-high-availability.png" max-width= "800px" class="center"></img>
</div>
## Configure multiple servers to use the same database
First you need to do is to setup mysql or postgres on another server and configure Grafana to use that database.
First, you need to do is to setup MySQL or Postgres on another server and configure Grafana to use that database.
You can find the configuration for doing that in the [[database]]({{< relref "configuration.md" >}}#database) section in the grafana config.
Grafana will now persist all long term data in the database.
It also worth considering how to setup the database for high availability but thats outside the scope of this guide.
Grafana will now persist all long term data in the database. How to configure the database for high availability is out of scope for this guide. We recommend finding an expert on for the database your using.
## User sessions
The second thing to consider is how to deal with user sessions and how to balance the load between servers.
By default Grafana stores user sessions on disk which works fine if you use `sticky sessions` in your load balancer.
Grafana also supports storing the session data in the database, redis or memcache which makes it possible to use round robin in your load balancer.
If you use mysql/postgres for session storage you first need a table to store the session data in. More details about that in [[sessions]]({{< relref "configuration.md" >}}#session)
The second thing to consider is how to deal with user sessions and how to configure your load balancer infront of Grafana.
Grafana support two says of storing session data locally on disk or in a database/cache-server.
If you want to store sessions on disk you can use `sticky sessions` in your load balanacer. If you prefer to store session data in a database/cache-server
you can use any stateless routing strategy in your load balancer (ex round robin or least connections).
### Sticky sessions
Using sticky sessions, all traffic for one user will always be sent to the same server. Which means that session related data can be
stored on disk rather than on a shared database. This is the default behavior for Grafana and if only want multiple servers for fail over this is a good solution since it requires the least amount of work.
### Stateless sessions
You can also choose to store session data in a Redis/Memcache/Postgres/MySQL which means that the load balancer can send a user to any Grafana server without having to log in on each server. This requires a little bit more work from the operator but enables you to remove/add grafana servers without impacting the user experience.
If you use MySQL/Postgres for session storage, you first need a table to store the session data in. More details about that in [[sessions]]({{< relref "configuration.md" >}}#session)
For Grafana itself it doesn't really matter if you store your sessions on disk or database/redis/memcache.
But we suggest that you store the session in redis/memcache since it makes it easier to add/remote instances from the group.
For Grafana itself it doesn't really matter if you store the session data on disk or database/redis/memcache. But we recommend using a database/redis/memcache since it makes it easier manage the grafana servers.
## Alerting
Currently alerting supports a limited form of high availability. Since v4.2.0 of Grafana, alert notifications are deduped when running multiple servers. This means all alerts are executed on every server but no duplicate alert notifications are sent due to the deduping logic. Proper load balancing of alerts will be introduced in the future.
Currently alerting supports a limited form of high availability. Since v4.2.0, alert notifications are deduped when running multiple servers. This means all alerts are executed on every server but alert notifications are only sent once per alert. Grafana does not support distributing the alert rule execution between servers. That might be added in the future but right now prefer to keep it simple.
define([
'angular',
'lodash',
'./query_def',
],
function (angular, _, queryDef) {
'use strict';
var module = angular.module('grafana.directives');
module.directive('elasticBucketAgg', function() {
return {
templateUrl: 'public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html',
controller: 'ElasticBucketAggCtrl',
restrict: 'E',
scope: {
target: "=",
index: "=",
onChange: "&",
getFields: "&",
}
};
});
module.controller('ElasticBucketAggCtrl', function($scope, uiSegmentSrv, $q, $rootScope) {
import angular from 'angular';
import _ from 'lodash';
import * as queryDef from './query_def';
export function elasticBucketAgg() {
return {
templateUrl: 'public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html',
controller: 'ElasticBucketAggCtrl',
restrict: 'E',
scope: {
target: '=',
index: '=',
onChange: '&',
getFields: '&',
},
};
}
export class ElasticBucketAggCtrl {
/** @nginject */
constructor($scope, uiSegmentSrv, $q, $rootScope) {
var bucketAggs = $scope.target.bucketAggs;
$scope.orderByOptions = [];
......@@ -39,9 +35,13 @@ function (angular, _, queryDef) {
return queryDef.sizeOptions;
};
$rootScope.onAppEvent('elastic-query-updated', function() {
$scope.validateModel();
}, $scope);
$rootScope.onAppEvent(
'elastic-query-updated',
function() {
$scope.validateModel();
},
$scope
);
$scope.init = function() {
$scope.agg = bucketAggs[$scope.index];
......@@ -56,10 +56,10 @@ function (angular, _, queryDef) {
$scope.agg.settings = {};
$scope.showOptions = false;
switch($scope.agg.type) {
switch ($scope.agg.type) {
case 'date_histogram':
case 'histogram':
case 'terms': {
case 'terms': {
delete $scope.agg.query;
$scope.agg.field = 'select field';
break;
......@@ -84,15 +84,15 @@ function (angular, _, queryDef) {
$scope.isFirst = $scope.index === 0;
$scope.bucketAggCount = bucketAggs.length;
var settingsLinkText = "";
var settingsLinkText = '';
var settings = $scope.agg.settings || {};
switch($scope.agg.type) {
switch ($scope.agg.type) {
case 'terms': {
settings.order = settings.order || "desc";
settings.size = settings.size || "10";
settings.order = settings.order || 'desc';
settings.size = settings.size || '10';
settings.min_doc_count = settings.min_doc_count || 1;
settings.orderBy = settings.orderBy || "_term";
settings.orderBy = settings.orderBy || '_term';
if (settings.size !== '0') {
settingsLinkText = queryDef.describeOrder(settings.order) + ' ' + settings.size + ', ';
......@@ -111,13 +111,17 @@ function (angular, _, queryDef) {
break;
}
case 'filters': {
settings.filters = settings.filters || [{query: '*'}];
settingsLinkText = _.reduce(settings.filters, function(memo, value, index) {
memo += 'Q' + (index + 1) + ' = ' + value.query + ' ';
return memo;
}, '');
settings.filters = settings.filters || [{ query: '*' }];
settingsLinkText = _.reduce(
settings.filters,
function(memo, value, index) {
memo += 'Q' + (index + 1) + ' = ' + value.query + ' ';
return memo;
},
''
);
if (settingsLinkText.length > 50) {
settingsLinkText = settingsLinkText.substr(0, 50) + "...";
settingsLinkText = settingsLinkText.substr(0, 50) + '...';
}
settingsLinkText = 'Filter Queries (' + settings.filters.length + ')';
break;
......@@ -165,7 +169,7 @@ function (angular, _, queryDef) {
};
$scope.addFiltersQuery = function() {
$scope.agg.settings.filters.push({query: '*'});
$scope.agg.settings.filters.push({ query: '*' });
};
$scope.removeFiltersQuery = function(filter) {
......@@ -182,7 +186,7 @@ function (angular, _, queryDef) {
$scope.getFieldsInternal = function() {
if ($scope.agg.type === 'date_histogram') {
return $scope.getFields({$fieldType: 'date'});
return $scope.getFields({ $fieldType: 'date' });
} else {
return $scope.getFields();
}
......@@ -198,14 +202,18 @@ function (angular, _, queryDef) {
var addIndex = bucketAggs.length - 1;
if (lastBucket && lastBucket.type === 'date_histogram') {
addIndex - 1;
addIndex -= 1;
}
var id = _.reduce($scope.target.bucketAggs.concat($scope.target.metrics), function(max, val) {
return parseInt(val.id) > max ? parseInt(val.id) : max;
}, 0);
var id = _.reduce(
$scope.target.bucketAggs.concat($scope.target.metrics),
function(max, val) {
return parseInt(val.id) > max ? parseInt(val.id) : max;
},
0
);
bucketAggs.splice(addIndex, 0, {type: "terms", field: "select field", id: (id+1).toString(), fake: true});
bucketAggs.splice(addIndex, 0, { type: 'terms', field: 'select field', id: (id + 1).toString(), fake: true });
$scope.onChange();
};
......@@ -215,7 +223,9 @@ function (angular, _, queryDef) {
};
$scope.init();
}
}
});
});
var module = angular.module('grafana.directives');
module.directive('elasticBucketAgg', elasticBucketAgg);
module.controller('ElasticBucketAggCtrl', ElasticBucketAggCtrl);
define([
'angular',
'lodash',
'./query_def'
],
function (angular, _, queryDef) {
'use strict';
var module = angular.module('grafana.directives');
module.directive('elasticMetricAgg', function() {
return {
templateUrl: 'public/app/plugins/datasource/elasticsearch/partials/metric_agg.html',
controller: 'ElasticMetricAggCtrl',
restrict: 'E',
scope: {
target: "=",
index: "=",
onChange: "&",
getFields: "&",
esVersion: '='
}
};
});
module.controller('ElasticMetricAggCtrl', function($scope, uiSegmentSrv, $q, $rootScope) {
import angular from 'angular';
import _ from 'lodash';
import * as queryDef from './query_def';
export function elasticMetricAgg() {
return {
templateUrl: 'public/app/plugins/datasource/elasticsearch/partials/metric_agg.html',
controller: 'ElasticMetricAggCtrl',
restrict: 'E',
scope: {
target: '=',
index: '=',
onChange: '&',
getFields: '&',
esVersion: '=',
},
};
}
export class ElasticMetricAggCtrl {
constructor($scope, uiSegmentSrv, $q, $rootScope) {
var metricAggs = $scope.target.metrics;
$scope.metricAggTypes = queryDef.getMetricAggTypes($scope.esVersion);
$scope.extendedStats = queryDef.extendedStats;
$scope.pipelineAggOptions = [];
......@@ -41,17 +35,21 @@ function (angular, _, queryDef) {
$scope.pipelineAggOptions = queryDef.getPipelineAggOptions($scope.target);
};
$rootScope.onAppEvent('elastic-query-updated', function() {
$scope.index = _.indexOf(metricAggs, $scope.agg);
$scope.updatePipelineAggOptions();
$scope.validateModel();
}, $scope);
$rootScope.onAppEvent(
'elastic-query-updated',
function() {
$scope.index = _.indexOf(metricAggs, $scope.agg);
$scope.updatePipelineAggOptions();
$scope.validateModel();
},
$scope
);
$scope.validateModel = function() {
$scope.isFirst = $scope.index === 0;
$scope.isSingle = metricAggs.length === 1;
$scope.settingsLinkText = '';
$scope.aggDef = _.find($scope.metricAggTypes, {value: $scope.agg.type});
$scope.aggDef = _.find($scope.metricAggTypes, { value: $scope.agg.type });
if (queryDef.isPipelineAgg($scope.agg.type)) {
$scope.agg.pipelineAgg = $scope.agg.pipelineAgg || 'select metric';
......@@ -67,30 +65,34 @@ function (angular, _, queryDef) {
} else if (!$scope.agg.field) {
$scope.agg.field = 'select field';
}
switch($scope.agg.type) {
switch ($scope.agg.type) {
case 'cardinality': {
var precision_threshold = $scope.agg.settings.precision_threshold || '';
$scope.settingsLinkText = 'Precision threshold: ' + precision_threshold;
break;
}
case 'percentiles': {
$scope.agg.settings.percents = $scope.agg.settings.percents || [25,50,75,95,99];
$scope.agg.settings.percents = $scope.agg.settings.percents || [25, 50, 75, 95, 99];
$scope.settingsLinkText = 'Values: ' + $scope.agg.settings.percents.join(',');
break;
}
case 'extended_stats': {
if (_.keys($scope.agg.meta).length === 0) {
if (_.keys($scope.agg.meta).length === 0) {
$scope.agg.meta.std_deviation_bounds_lower = true;
$scope.agg.meta.std_deviation_bounds_upper = true;
}
var stats = _.reduce($scope.agg.meta, function(memo, val, key) {
if (val) {
var def = _.find($scope.extendedStats, {value: key});
memo.push(def.text);
}
return memo;
}, []);
var stats = _.reduce(
$scope.agg.meta,
function(memo, val, key) {
if (val) {
var def = _.find($scope.extendedStats, { value: key });
memo.push(def.text);
}
return memo;
},
[]
);
$scope.settingsLinkText = 'Stats: ' + stats.join(', ');
break;
......@@ -103,8 +105,8 @@ function (angular, _, queryDef) {
}
case 'raw_document': {
$scope.agg.settings.size = $scope.agg.settings.size || 500;
$scope.settingsLinkText = 'Size: ' + $scope.agg.settings.size ;
$scope.target.metrics.splice(0,$scope.target.metrics.length, $scope.agg);
$scope.settingsLinkText = 'Size: ' + $scope.agg.settings.size;
$scope.target.metrics.splice(0, $scope.target.metrics.length, $scope.agg);
$scope.target.bucketAggs = [];
break;
......@@ -115,7 +117,7 @@ function (angular, _, queryDef) {
// but having it like this simplifes the query_builder
var inlineScript = $scope.agg.inlineScript;
if (inlineScript) {
$scope.agg.settings.script = {inline: inlineScript};
$scope.agg.settings.script = { inline: inlineScript };
} else {
delete $scope.agg.settings.script;
}
......@@ -135,15 +137,15 @@ function (angular, _, queryDef) {
$scope.onChange();
};
$scope.updateMovingAvgModelSettings = function () {
$scope.updateMovingAvgModelSettings = function() {
var modelSettingsKeys = [];
var modelSettings = queryDef.getMovingAvgSettings($scope.agg.settings.model, false);
for (var i=0; i < modelSettings.length; i++) {
for (var i = 0; i < modelSettings.length; i++) {
modelSettingsKeys.push(modelSettings[i].value);
}
for (var key in $scope.agg.settings.settings) {
if (($scope.agg.settings.settings[key] === null) || (modelSettingsKeys.indexOf(key) === -1)) {
if ($scope.agg.settings.settings[key] === null || modelSettingsKeys.indexOf(key) === -1) {
delete $scope.agg.settings.settings[key];
}
}
......@@ -166,17 +168,21 @@ function (angular, _, queryDef) {
if ($scope.agg.type === 'cardinality') {
return $scope.getFields();
}
return $scope.getFields({$fieldType: 'number'});
return $scope.getFields({ $fieldType: 'number' });
};
$scope.addMetricAgg = function() {
var addIndex = metricAggs.length;
var id = _.reduce($scope.target.bucketAggs.concat($scope.target.metrics), function(max, val) {
return parseInt(val.id) > max ? parseInt(val.id) : max;
}, 0);
var id = _.reduce(
$scope.target.bucketAggs.concat($scope.target.metrics),
function(max, val) {
return parseInt(val.id) > max ? parseInt(val.id) : max;
},
0
);
metricAggs.splice(addIndex, 0, {type: "count", field: "select field", id: (id+1).toString()});
metricAggs.splice(addIndex, 0, { type: 'count', field: 'select field', id: (id + 1).toString() });
$scope.onChange();
};
......@@ -194,7 +200,9 @@ function (angular, _, queryDef) {
};
$scope.init();
}
}
});
});
var module = angular.module('grafana.directives');
module.directive('elasticMetricAgg', elasticMetricAgg);
module.controller('ElasticMetricAggCtrl', ElasticMetricAggCtrl);
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