Commit 2c05237d by Torkel Ödegaard

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

parents c0459078 bc8d2773
...@@ -15,11 +15,13 @@ ...@@ -15,11 +15,13 @@
* **LDAP**: Basic Auth now supports LDAP username and password, [#6940](https://github.com/grafana/grafana/pull/6940), thx [@utkarshcmu](https://github.com/utkarshcmu) * **LDAP**: Basic Auth now supports LDAP username and password, [#6940](https://github.com/grafana/grafana/pull/6940), thx [@utkarshcmu](https://github.com/utkarshcmu)
* **LDAP**: Now works with Auth Proxy, role and organisation mapping & sync will regularly be performed. [#6895](https://github.com/grafana/grafana/pull/6895), thx [@Seuf](https://github.com/seuf) * **LDAP**: Now works with Auth Proxy, role and organisation mapping & sync will regularly be performed. [#6895](https://github.com/grafana/grafana/pull/6895), thx [@Seuf](https://github.com/seuf)
* **Alerting**: Adds OK as no data option. [#6866](https://github.com/grafana/grafana/issues/6866) * **Alerting**: Adds OK as no data option. [#6866](https://github.com/grafana/grafana/issues/6866)
* **Alert list**: Order alerts based on state. [#6676](https://github.com/grafana/grafana/issues/6676)
### Bugfixes ### Bugfixes
* **API**: HTTP API for deleting org returning incorrect message for a non-existing org [#6679](https://github.com/grafana/grafana/issues/6679) * **API**: HTTP API for deleting org returning incorrect message for a non-existing org [#6679](https://github.com/grafana/grafana/issues/6679)
* **Dashboard**: Posting empty dashboard result in corrupted dashboard [#5443](https://github.com/grafana/grafana/issues/5443) * **Dashboard**: Posting empty dashboard result in corrupted dashboard [#5443](https://github.com/grafana/grafana/issues/5443)
* **Logging**: Fixed logging level confing issue [#6978](https://github.com/grafana/grafana/issues/6978) * **Logging**: Fixed logging level confing issue [#6978](https://github.com/grafana/grafana/issues/6978)
* **Notifications**: Remove html escaping the email subject. [#6905](https://github.com/grafana/grafana/issues/6905)
# 4.0.2 (2016-12-08) # 4.0.2 (2016-12-08)
......
...@@ -71,7 +71,7 @@ Name | Description ...@@ -71,7 +71,7 @@ Name | Description
------- | -------- ------- | --------
`regions()` | Returns a list of regions AWS provides their service. `regions()` | Returns a list of regions AWS provides their service.
`namespaces()` | Returns a list of namespaces CloudWatch support. `namespaces()` | Returns a list of namespaces CloudWatch support.
`metrics(namespace)` | Returns a list of metrics in the namespace. `metrics(namespace, [region])` | Returns a list of metrics in the namespace. (specify region for custom metrics)
`dimension_keys(namespace)` | Returns a list of dimension keys in the namespace. `dimension_keys(namespace)` | Returns a list of dimension keys in the namespace.
`dimension_values(region, namespace, metric, dimension_key)` | Returns a list of dimension values matching the specified `region`, `namespace`, `metric` and `dimension_key`. `dimension_values(region, namespace, metric, dimension_key)` | Returns a list of dimension values matching the specified `region`, `namespace`, `metric` and `dimension_key`.
`ebs_volume_ids(region, instance_id)` | Returns a list of volume id matching the specified `region`, `instance_id`. `ebs_volume_ids(region, instance_id)` | Returns a list of volume id matching the specified `region`, `instance_id`.
......
...@@ -18,6 +18,7 @@ import ( ...@@ -18,6 +18,7 @@ import (
"github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/sts" "github.com/aws/aws-sdk-go/service/sts"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
) )
...@@ -197,8 +198,6 @@ func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) { ...@@ -197,8 +198,6 @@ func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
Namespace: aws.String(reqParam.Parameters.Namespace), Namespace: aws.String(reqParam.Parameters.Namespace),
MetricName: aws.String(reqParam.Parameters.MetricName), MetricName: aws.String(reqParam.Parameters.MetricName),
Dimensions: reqParam.Parameters.Dimensions, Dimensions: reqParam.Parameters.Dimensions,
Statistics: reqParam.Parameters.Statistics,
ExtendedStatistics: reqParam.Parameters.ExtendedStatistics,
StartTime: aws.Time(time.Unix(reqParam.Parameters.StartTime, 0)), StartTime: aws.Time(time.Unix(reqParam.Parameters.StartTime, 0)),
EndTime: aws.Time(time.Unix(reqParam.Parameters.EndTime, 0)), EndTime: aws.Time(time.Unix(reqParam.Parameters.EndTime, 0)),
Period: aws.Int64(reqParam.Parameters.Period), Period: aws.Int64(reqParam.Parameters.Period),
...@@ -215,6 +214,7 @@ func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) { ...@@ -215,6 +214,7 @@ func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
c.JsonApiErr(500, "Unable to call AWS API", err) c.JsonApiErr(500, "Unable to call AWS API", err)
return return
} }
metrics.M_Aws_CloudWatch_GetMetricStatistics.Inc(1)
c.JSON(200, resp) c.JSON(200, resp)
} }
...@@ -241,6 +241,7 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) { ...@@ -241,6 +241,7 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
var resp cloudwatch.ListMetricsOutput var resp cloudwatch.ListMetricsOutput
err := svc.ListMetricsPages(params, err := svc.ListMetricsPages(params,
func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool { func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
metrics.M_Aws_CloudWatch_ListMetrics.Inc(1)
metrics, _ := awsutil.ValuesAtPath(page, "Metrics") metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
for _, metric := range metrics { for _, metric := range metrics {
resp.Metrics = append(resp.Metrics, metric.(*cloudwatch.Metric)) resp.Metrics = append(resp.Metrics, metric.(*cloudwatch.Metric))
......
...@@ -11,6 +11,7 @@ import ( ...@@ -11,6 +11,7 @@ import (
"github.com/aws/aws-sdk-go/aws/awsutil" "github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
...@@ -261,6 +262,7 @@ func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error) ...@@ -261,6 +262,7 @@ func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error)
var resp cloudwatch.ListMetricsOutput var resp cloudwatch.ListMetricsOutput
err := svc.ListMetricsPages(params, err := svc.ListMetricsPages(params,
func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool { func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
metrics.M_Aws_CloudWatch_ListMetrics.Inc(1)
metrics, _ := awsutil.ValuesAtPath(page, "Metrics") metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
for _, metric := range metrics { for _, metric := range metrics {
resp.Metrics = append(resp.Metrics, metric.(*cloudwatch.Metric)) resp.Metrics = append(resp.Metrics, metric.(*cloudwatch.Metric))
......
...@@ -47,6 +47,8 @@ var ( ...@@ -47,6 +47,8 @@ var (
M_Alerting_Notification_Sent_PagerDuty Counter M_Alerting_Notification_Sent_PagerDuty Counter
M_Alerting_Notification_Sent_Victorops Counter M_Alerting_Notification_Sent_Victorops Counter
M_Alerting_Notification_Sent_OpsGenie Counter M_Alerting_Notification_Sent_OpsGenie Counter
M_Aws_CloudWatch_GetMetricStatistics Counter
M_Aws_CloudWatch_ListMetrics Counter
// Timers // Timers
M_DataSource_ProxyReq_Timer Timer M_DataSource_ProxyReq_Timer Timer
...@@ -113,6 +115,9 @@ func initMetricVars(settings *MetricSettings) { ...@@ -113,6 +115,9 @@ func initMetricVars(settings *MetricSettings) {
M_Alerting_Notification_Sent_Victorops = RegCounter("alerting.notifications_sent", "type", "victorops") M_Alerting_Notification_Sent_Victorops = RegCounter("alerting.notifications_sent", "type", "victorops")
M_Alerting_Notification_Sent_OpsGenie = RegCounter("alerting.notifications_sent", "type", "opsgenie") M_Alerting_Notification_Sent_OpsGenie = RegCounter("alerting.notifications_sent", "type", "opsgenie")
M_Aws_CloudWatch_GetMetricStatistics = RegCounter("aws.cloudwatch.get_metric_statistics")
M_Aws_CloudWatch_ListMetrics = RegCounter("aws.cloudwatch.list_metrics")
// Timers // Timers
M_DataSource_ProxyReq_Timer = RegTimer("api.dataproxy.request.all") M_DataSource_ProxyReq_Timer = RegTimer("api.dataproxy.request.all")
M_Alerting_Execution_Time = RegTimer("alerting.execution_time") M_Alerting_Execution_Time = RegTimer("alerting.execution_time")
......
...@@ -7,6 +7,7 @@ var ErrInvalidEmailCode = errors.New("Invalid or expired email code") ...@@ -7,6 +7,7 @@ var ErrInvalidEmailCode = errors.New("Invalid or expired email code")
type SendEmailCommand struct { type SendEmailCommand struct {
To []string To []string
Template string Template string
Subject string
Data map[string]interface{} Data map[string]interface{}
Info string Info string
EmbededFiles []string EmbededFiles []string
......
...@@ -57,6 +57,7 @@ func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error { ...@@ -57,6 +57,7 @@ func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error {
cmd := &m.SendEmailCommandSync{ cmd := &m.SendEmailCommandSync{
SendEmailCommand: m.SendEmailCommand{ SendEmailCommand: m.SendEmailCommand{
Subject: evalContext.GetNotificationTitle(),
Data: map[string]interface{}{ Data: map[string]interface{}{
"Title": evalContext.GetNotificationTitle(), "Title": evalContext.GetNotificationTitle(),
"State": evalContext.Rule.State, "State": evalContext.Rule.State,
......
...@@ -111,7 +111,6 @@ func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) { ...@@ -111,7 +111,6 @@ func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
var buffer bytes.Buffer var buffer bytes.Buffer
var err error var err error
var subjectText interface{}
data := cmd.Data data := cmd.Data
if data == nil { if data == nil {
...@@ -124,6 +123,9 @@ func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) { ...@@ -124,6 +123,9 @@ func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
return nil, err return nil, err
} }
subject := cmd.Subject
if cmd.Subject == "" {
var subjectText interface{}
subjectData := data["Subject"].(map[string]interface{}) subjectData := data["Subject"].(map[string]interface{})
subjectText, hasSubject := subjectData["value"] subjectText, hasSubject := subjectData["value"]
...@@ -142,10 +144,13 @@ func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) { ...@@ -142,10 +144,13 @@ func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
return nil, err return nil, err
} }
subject = subjectBuffer.String()
}
return &Message{ return &Message{
To: cmd.To, To: cmd.To,
From: setting.Smtp.FromAddress, From: setting.Smtp.FromAddress,
Subject: subjectBuffer.String(), Subject: subject,
Body: buffer.String(), Body: buffer.String(),
EmbededFiles: cmd.EmbededFiles, EmbededFiles: cmd.EmbededFiles,
}, nil }, nil
......
...@@ -80,6 +80,7 @@ func sendEmailCommandHandlerSync(ctx context.Context, cmd *m.SendEmailCommandSyn ...@@ -80,6 +80,7 @@ func sendEmailCommandHandlerSync(ctx context.Context, cmd *m.SendEmailCommandSyn
Template: cmd.Template, Template: cmd.Template,
To: cmd.To, To: cmd.To,
EmbededFiles: cmd.EmbededFiles, EmbededFiles: cmd.EmbededFiles,
Subject: cmd.Subject,
}) })
if err != nil { if err != nil {
......
...@@ -42,29 +42,20 @@ func (q *Query) Build(availableSeries []string) ([]QueryToSend, error) { ...@@ -42,29 +42,20 @@ func (q *Query) Build(availableSeries []string) ([]QueryToSend, error) {
where := q.buildWhereClause() where := q.buildWhereClause()
functions := q.buildFunctionList() functions := q.buildFunctionList()
for _, v := range q.Metrics { for _, metric := range q.Metrics {
if !containsWildcardPattern.Match([]byte(v.Metric)) {
alias := "" alias := ""
if v.Alias != "" { if metric.Alias != "" {
alias = fmt.Sprintf(" {%s}", v.Alias) alias = fmt.Sprintf(" {%s}", metric.Alias)
} }
rawQuery := fmt.Sprintf( if !containsWildcardPattern.Match([]byte(metric.Metric)) {
"`%s`%s%s %s from %v to %v", rawQuery := q.renderQuerystring(metric.Metric, functions, alias, where, q.TimeRange)
v.Metric,
functions,
alias,
where,
q.TimeRange.GetFromAsMsEpoch(),
q.TimeRange.GetToAsMsEpoch())
queriesToSend = append(queriesToSend, QueryToSend{ queriesToSend = append(queriesToSend, QueryToSend{
RawQuery: rawQuery, RawQuery: rawQuery,
QueryRef: q, QueryRef: q,
}) })
continue } else {
} m := strings.Replace(metric.Metric, "*", ".*", -1)
m := strings.Replace(v.Metric, "*", ".*", -1)
mp, err := regexp.Compile(m) mp, err := regexp.Compile(m)
if err != nil { if err != nil {
...@@ -73,22 +64,9 @@ func (q *Query) Build(availableSeries []string) ([]QueryToSend, error) { ...@@ -73,22 +64,9 @@ func (q *Query) Build(availableSeries []string) ([]QueryToSend, error) {
} }
//TODO: this lookup should be cached //TODO: this lookup should be cached
for _, a := range availableSeries { for _, wildcardMatch := range availableSeries {
if mp.Match([]byte(a)) { if mp.Match([]byte(wildcardMatch)) {
alias := "" rawQuery := q.renderQuerystring(wildcardMatch, functions, alias, where, q.TimeRange)
if v.Alias != "" {
alias = fmt.Sprintf(" {%s}", v.Alias)
}
rawQuery := fmt.Sprintf(
"`%s`%s%s %s from %v to %v",
a,
functions,
alias,
where,
q.TimeRange.GetFromAsMsEpoch(),
q.TimeRange.GetToAsMsEpoch())
queriesToSend = append(queriesToSend, QueryToSend{ queriesToSend = append(queriesToSend, QueryToSend{
RawQuery: rawQuery, RawQuery: rawQuery,
QueryRef: q, QueryRef: q,
...@@ -96,9 +74,22 @@ func (q *Query) Build(availableSeries []string) ([]QueryToSend, error) { ...@@ -96,9 +74,22 @@ func (q *Query) Build(availableSeries []string) ([]QueryToSend, error) {
} }
} }
} }
}
return queriesToSend, nil return queriesToSend, nil
} }
func (q *Query) renderQuerystring(path, functions, alias, where string, timerange *tsdb.TimeRange) string {
return fmt.Sprintf(
"`%s`%s%s %s from %v to %v",
path,
functions,
alias,
where,
q.TimeRange.GetFromAsMsEpoch(),
q.TimeRange.GetToAsMsEpoch())
}
func (q *Query) buildFunctionList() string { func (q *Query) buildFunctionList() string {
functions := "" functions := ""
for _, v := range q.FunctionList { for _, v := range q.FunctionList {
......
...@@ -20,6 +20,14 @@ var conditionTypes = [ ...@@ -20,6 +20,14 @@ var conditionTypes = [
{text: 'Query', value: 'query'}, {text: 'Query', value: 'query'},
]; ];
var alertStateSortScore = {
alerting: 1,
no_data: 2,
pending: 3,
ok: 4,
paused: 5,
};
var evalFunctions = [ var evalFunctions = [
{text: 'IS ABOVE', value: 'gt'}, {text: 'IS ABOVE', value: 'gt'},
{text: 'IS BELOW', value: 'lt'}, {text: 'IS BELOW', value: 'lt'},
...@@ -129,4 +137,5 @@ export default { ...@@ -129,4 +137,5 @@ export default {
reducerTypes: reducerTypes, reducerTypes: reducerTypes,
createReducerPart: createReducerPart, createReducerPart: createReducerPart,
joinEvalMatches: joinEvalMatches, joinEvalMatches: joinEvalMatches,
alertStateSortScore: alertStateSortScore,
}; };
...@@ -11,6 +11,12 @@ ...@@ -11,6 +11,12 @@
<span class="gf-form-label width-8">Max items</span> <span class="gf-form-label width-8">Max items</span>
<input type="text" class="gf-form-input max-width-15" ng-model="ctrl.panel.limit" ng-change="ctrl.onRender()" /> <input type="text" class="gf-form-input max-width-15" ng-model="ctrl.panel.limit" ng-change="ctrl.onRender()" />
</div> </div>
<div class="gf-form" ng-show="ctrl.panel.show === 'current'">
<span class="gf-form-label width-8">Sort order</span>
<div class="gf-form-select-wrapper max-width-15">
<select class="gf-form-input" ng-model="ctrl.panel.sortOrder" ng-options="f.value as f.text for f in ctrl.sortOrderOptions" ng-change="ctrl.onRender()"></select>
</div>
</div>
<gf-form-switch class="gf-form" label="Alerts from this dashboard" label-class="width-18" checked="ctrl.panel.onlyAlertsOnDashboard" on-change="ctrl.updateStateFilter()"></gf-form-switch> <gf-form-switch class="gf-form" label="Alerts from this dashboard" label-class="width-18" checked="ctrl.panel.onlyAlertsOnDashboard" on-change="ctrl.updateStateFilter()"></gf-form-switch>
</div> </div>
<div class="section gf-form-group"> <div class="section gf-form-group">
......
...@@ -17,6 +17,12 @@ class AlertListPanel extends PanelCtrl { ...@@ -17,6 +17,12 @@ class AlertListPanel extends PanelCtrl {
{text: 'Recent state changes', value: 'changes'} {text: 'Recent state changes', value: 'changes'}
]; ];
sortOrderOptions = [
{text: 'Alphabetical (asc)', value: 1},
{text: 'Alphabetical (desc)', value: 2},
{text: 'Importance', value: 3},
];
contentHeight: string; contentHeight: string;
stateFilter: any = {}; stateFilter: any = {};
currentAlerts: any = []; currentAlerts: any = [];
...@@ -26,10 +32,10 @@ class AlertListPanel extends PanelCtrl { ...@@ -26,10 +32,10 @@ class AlertListPanel extends PanelCtrl {
show: 'current', show: 'current',
limit: 10, limit: 10,
stateFilter: [], stateFilter: [],
onlyAlertsOnDashboard: false onlyAlertsOnDashboard: false,
sortOrder: 1
}; };
/** @ngInject */ /** @ngInject */
constructor($scope, $injector, private $location, private backendSrv, private timeSrv, private templateSrv) { constructor($scope, $injector, private $location, private backendSrv, private timeSrv, private templateSrv) {
super($scope, $injector); super($scope, $injector);
...@@ -44,6 +50,19 @@ class AlertListPanel extends PanelCtrl { ...@@ -44,6 +50,19 @@ class AlertListPanel extends PanelCtrl {
} }
} }
sortResult(alerts) {
if (this.panel.sortOrder === 3) {
return _.sortBy(alerts, a => { return alertDef.alertStateSortScore[a.state]; });
}
var result = _.sortBy(alerts, a => { return a.name.toLowerCase();});
if (this.panel.sortOrder === 2) {
result.reverse();
}
return result;
}
updateStateFilter() { updateStateFilter() {
var result = []; var result = [];
...@@ -104,11 +123,11 @@ class AlertListPanel extends PanelCtrl { ...@@ -104,11 +123,11 @@ class AlertListPanel extends PanelCtrl {
this.backendSrv.get(`/api/alerts`, params) this.backendSrv.get(`/api/alerts`, params)
.then(res => { .then(res => {
this.currentAlerts = _.map(res, al => { this.currentAlerts = this.sortResult(_.map(res, al => {
al.stateModel = alertDef.getStateDisplayModel(al.state); al.stateModel = alertDef.getStateDisplayModel(al.state);
al.newStateDateAgo = moment(al.newStateDate).fromNow().replace(" ago", ""); al.newStateDateAgo = moment(al.newStateDate).fromNow().replace(" ago", "");
return al; return al;
}); }));
}); });
} }
......
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