Bläddra i källkod

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

Torkel Ödegaard 9 år sedan
förälder
incheckning
2c05237d90

+ 2 - 0
CHANGELOG.md

@@ -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**: 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)
+* **Alert list**: Order alerts based on state. [#6676](https://github.com/grafana/grafana/issues/6676)
 
 ### Bugfixes
 * **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)
 * **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)
 

+ 1 - 1
docs/sources/datasources/cloudwatch.md

@@ -71,7 +71,7 @@ Name | Description
 ------- | --------
 `regions()` | Returns a list of regions AWS provides their service.
 `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_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`.

+ 9 - 8
pkg/api/cloudwatch/cloudwatch.go

@@ -18,6 +18,7 @@ import (
 	"github.com/aws/aws-sdk-go/service/ec2"
 	"github.com/aws/aws-sdk-go/service/sts"
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
 )
@@ -194,14 +195,12 @@ func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
 	json.Unmarshal(req.Body, reqParam)
 
 	params := &cloudwatch.GetMetricStatisticsInput{
-		Namespace:          aws.String(reqParam.Parameters.Namespace),
-		MetricName:         aws.String(reqParam.Parameters.MetricName),
-		Dimensions:         reqParam.Parameters.Dimensions,
-		Statistics:         reqParam.Parameters.Statistics,
-		ExtendedStatistics: reqParam.Parameters.ExtendedStatistics,
-		StartTime:          aws.Time(time.Unix(reqParam.Parameters.StartTime, 0)),
-		EndTime:            aws.Time(time.Unix(reqParam.Parameters.EndTime, 0)),
-		Period:             aws.Int64(reqParam.Parameters.Period),
+		Namespace:  aws.String(reqParam.Parameters.Namespace),
+		MetricName: aws.String(reqParam.Parameters.MetricName),
+		Dimensions: reqParam.Parameters.Dimensions,
+		StartTime:  aws.Time(time.Unix(reqParam.Parameters.StartTime, 0)),
+		EndTime:    aws.Time(time.Unix(reqParam.Parameters.EndTime, 0)),
+		Period:     aws.Int64(reqParam.Parameters.Period),
 	}
 	if len(reqParam.Parameters.Statistics) != 0 {
 		params.Statistics = reqParam.Parameters.Statistics
@@ -215,6 +214,7 @@ func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
 		c.JsonApiErr(500, "Unable to call AWS API", err)
 		return
 	}
+	metrics.M_Aws_CloudWatch_GetMetricStatistics.Inc(1)
 
 	c.JSON(200, resp)
 }
@@ -241,6 +241,7 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
 	var resp cloudwatch.ListMetricsOutput
 	err := svc.ListMetricsPages(params,
 		func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
+			metrics.M_Aws_CloudWatch_ListMetrics.Inc(1)
 			metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
 			for _, metric := range metrics {
 				resp.Metrics = append(resp.Metrics, metric.(*cloudwatch.Metric))

+ 2 - 0
pkg/api/cloudwatch/metrics.go

@@ -11,6 +11,7 @@ import (
 	"github.com/aws/aws-sdk-go/aws/awsutil"
 	"github.com/aws/aws-sdk-go/aws/session"
 	"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/util"
 )
@@ -261,6 +262,7 @@ func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error)
 	var resp cloudwatch.ListMetricsOutput
 	err := svc.ListMetricsPages(params,
 		func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
+			metrics.M_Aws_CloudWatch_ListMetrics.Inc(1)
 			metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
 			for _, metric := range metrics {
 				resp.Metrics = append(resp.Metrics, metric.(*cloudwatch.Metric))

+ 5 - 0
pkg/metrics/metrics.go

@@ -47,6 +47,8 @@ var (
 	M_Alerting_Notification_Sent_PagerDuty Counter
 	M_Alerting_Notification_Sent_Victorops Counter
 	M_Alerting_Notification_Sent_OpsGenie  Counter
+	M_Aws_CloudWatch_GetMetricStatistics   Counter
+	M_Aws_CloudWatch_ListMetrics           Counter
 
 	// Timers
 	M_DataSource_ProxyReq_Timer Timer
@@ -113,6 +115,9 @@ func initMetricVars(settings *MetricSettings) {
 	M_Alerting_Notification_Sent_Victorops = RegCounter("alerting.notifications_sent", "type", "victorops")
 	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
 	M_DataSource_ProxyReq_Timer = RegTimer("api.dataproxy.request.all")
 	M_Alerting_Execution_Time = RegTimer("alerting.execution_time")

+ 1 - 0
pkg/models/notifications.go

@@ -7,6 +7,7 @@ var ErrInvalidEmailCode = errors.New("Invalid or expired email code")
 type SendEmailCommand struct {
 	To           []string
 	Template     string
+	Subject      string
 	Data         map[string]interface{}
 	Info         string
 	EmbededFiles []string

+ 1 - 0
pkg/services/alerting/notifiers/email.go

@@ -57,6 +57,7 @@ func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error {
 
 	cmd := &m.SendEmailCommandSync{
 		SendEmailCommand: m.SendEmailCommand{
+			Subject: evalContext.GetNotificationTitle(),
 			Data: map[string]interface{}{
 				"Title":        evalContext.GetNotificationTitle(),
 				"State":        evalContext.Rule.State,

+ 20 - 15
pkg/services/notifications/mailer.go

@@ -111,7 +111,6 @@ func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
 
 	var buffer bytes.Buffer
 	var err error
-	var subjectText interface{}
 
 	data := cmd.Data
 	if data == nil {
@@ -124,28 +123,34 @@ func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
 		return nil, err
 	}
 
-	subjectData := data["Subject"].(map[string]interface{})
-	subjectText, hasSubject := subjectData["value"]
+	subject := cmd.Subject
+	if cmd.Subject == "" {
+		var subjectText interface{}
+		subjectData := data["Subject"].(map[string]interface{})
+		subjectText, hasSubject := subjectData["value"]
 
-	if !hasSubject {
-		return nil, errors.New(fmt.Sprintf("Missing subject in Template %s", cmd.Template))
-	}
+		if !hasSubject {
+			return nil, errors.New(fmt.Sprintf("Missing subject in Template %s", cmd.Template))
+		}
 
-	subjectTmpl, err := template.New("subject").Parse(subjectText.(string))
-	if err != nil {
-		return nil, err
-	}
+		subjectTmpl, err := template.New("subject").Parse(subjectText.(string))
+		if err != nil {
+			return nil, err
+		}
 
-	var subjectBuffer bytes.Buffer
-	err = subjectTmpl.ExecuteTemplate(&subjectBuffer, "subject", data)
-	if err != nil {
-		return nil, err
+		var subjectBuffer bytes.Buffer
+		err = subjectTmpl.ExecuteTemplate(&subjectBuffer, "subject", data)
+		if err != nil {
+			return nil, err
+		}
+
+		subject = subjectBuffer.String()
 	}
 
 	return &Message{
 		To:           cmd.To,
 		From:         setting.Smtp.FromAddress,
-		Subject:      subjectBuffer.String(),
+		Subject:      subject,
 		Body:         buffer.String(),
 		EmbededFiles: cmd.EmbededFiles,
 	}, nil

+ 1 - 0
pkg/services/notifications/notifications.go

@@ -80,6 +80,7 @@ func sendEmailCommandHandlerSync(ctx context.Context, cmd *m.SendEmailCommandSyn
 		Template:     cmd.Template,
 		To:           cmd.To,
 		EmbededFiles: cmd.EmbededFiles,
+		Subject:      cmd.Subject,
 	})
 
 	if err != nil {

+ 34 - 43
pkg/tsdb/mqe/types.go

@@ -42,63 +42,54 @@ func (q *Query) Build(availableSeries []string) ([]QueryToSend, error) {
 	where := q.buildWhereClause()
 	functions := q.buildFunctionList()
 
-	for _, v := range q.Metrics {
-		if !containsWildcardPattern.Match([]byte(v.Metric)) {
-			alias := ""
-			if v.Alias != "" {
-				alias = fmt.Sprintf(" {%s}", v.Alias)
-			}
+	for _, metric := range q.Metrics {
+		alias := ""
+		if metric.Alias != "" {
+			alias = fmt.Sprintf(" {%s}", metric.Alias)
+		}
 
-			rawQuery := fmt.Sprintf(
-				"`%s`%s%s %s from %v to %v",
-				v.Metric,
-				functions,
-				alias,
-				where,
-				q.TimeRange.GetFromAsMsEpoch(),
-				q.TimeRange.GetToAsMsEpoch())
+		if !containsWildcardPattern.Match([]byte(metric.Metric)) {
+			rawQuery := q.renderQuerystring(metric.Metric, functions, alias, where, q.TimeRange)
 			queriesToSend = append(queriesToSend, QueryToSend{
 				RawQuery: rawQuery,
 				QueryRef: q,
 			})
-			continue
-		}
+		} else {
+			m := strings.Replace(metric.Metric, "*", ".*", -1)
+			mp, err := regexp.Compile(m)
 
-		m := strings.Replace(v.Metric, "*", ".*", -1)
-		mp, err := regexp.Compile(m)
-
-		if err != nil {
-			log.Error2("failed to compile regex for ", "metric", m)
-			continue
-		}
+			if err != nil {
+				log.Error2("failed to compile regex for ", "metric", m)
+				continue
+			}
 
-		//TODO: this lookup should be cached
-		for _, a := range availableSeries {
-			if mp.Match([]byte(a)) {
-				alias := ""
-				if v.Alias != "" {
-					alias = fmt.Sprintf(" {%s}", v.Alias)
+			//TODO: this lookup should be cached
+			for _, wildcardMatch := range availableSeries {
+				if mp.Match([]byte(wildcardMatch)) {
+					rawQuery := q.renderQuerystring(wildcardMatch, functions, alias, where, q.TimeRange)
+					queriesToSend = append(queriesToSend, QueryToSend{
+						RawQuery: rawQuery,
+						QueryRef: q,
+					})
 				}
-
-				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{
-					RawQuery: rawQuery,
-					QueryRef: q,
-				})
 			}
 		}
 	}
+
 	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 {
 	functions := ""
 	for _, v := range q.FunctionList {

+ 9 - 0
public/app/features/alerting/alert_def.ts

@@ -20,6 +20,14 @@ var conditionTypes = [
   {text: 'Query', value: 'query'},
 ];
 
+var alertStateSortScore = {
+  alerting: 1,
+  no_data: 2,
+  pending: 3,
+  ok: 4,
+  paused: 5,
+};
+
 var evalFunctions = [
   {text: 'IS ABOVE', value: 'gt'},
   {text: 'IS BELOW', value: 'lt'},
@@ -129,4 +137,5 @@ export default {
   reducerTypes: reducerTypes,
   createReducerPart: createReducerPart,
   joinEvalMatches: joinEvalMatches,
+  alertStateSortScore: alertStateSortScore,
 };

+ 6 - 0
public/app/plugins/panel/alertlist/editor.html

@@ -11,6 +11,12 @@
       <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()" />
     </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>
   </div>
   <div class="section gf-form-group">

+ 23 - 4
public/app/plugins/panel/alertlist/module.ts

@@ -17,6 +17,12 @@ class AlertListPanel extends PanelCtrl {
     {text: 'Recent state changes', value: 'changes'}
   ];
 
+  sortOrderOptions = [
+    {text: 'Alphabetical (asc)', value: 1},
+    {text: 'Alphabetical (desc)', value: 2},
+    {text: 'Importance', value: 3},
+  ];
+
   contentHeight: string;
   stateFilter: any = {};
   currentAlerts: any = [];
@@ -26,10 +32,10 @@ class AlertListPanel extends PanelCtrl {
     show: 'current',
     limit: 10,
     stateFilter: [],
-    onlyAlertsOnDashboard: false
+    onlyAlertsOnDashboard: false,
+    sortOrder: 1
   };
 
-
   /** @ngInject */
   constructor($scope, $injector, private $location, private backendSrv, private timeSrv, private templateSrv) {
     super($scope, $injector);
@@ -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() {
     var result = [];
 
@@ -104,11 +123,11 @@ class AlertListPanel extends PanelCtrl {
 
     this.backendSrv.get(`/api/alerts`, params)
       .then(res => {
-        this.currentAlerts = _.map(res, al => {
+        this.currentAlerts = this.sortResult(_.map(res, al => {
           al.stateModel = alertDef.getStateDisplayModel(al.state);
           al.newStateDateAgo = moment(al.newStateDate).fromNow().replace(" ago", "");
           return al;
-        });
+        }));
       });
   }