Explorar o código

Alerting: Add tags to alert rules (#10989)

Ref #6552
Thibault Chataigner %!s(int64=6) %!d(string=hai) anos
pai
achega
e06abb30aa

+ 27 - 19
docs/sources/alerting/notifications.md

@@ -167,26 +167,26 @@ Notifications can be sent by setting up an incoming webhook in Google Hangouts c
 
 ### All supported notifiers
 
-Name | Type | Supports images
+Name | Type | Supports images |Support alert rule tags
 -----|------------ | ------
-DingDing | `dingding` | yes, external only
-Discord | `discord` | yes
-Email | `email` | yes
-Google Hangouts Chat | `googlechat` | yes, external only
-Hipchat | `hipchat` | yes, external only
-Kafka | `kafka` | yes, external only
-Line | `line` | yes, external only
-Microsoft Teams | `teams` | yes, external only
-OpsGenie | `opsgenie` | yes, external only
-Pagerduty | `pagerduty` | yes, external only
-Prometheus Alertmanager | `prometheus-alertmanager` | yes, external only
-Pushover | `pushover` | yes
-Sensu | `sensu` | yes, external only
-Slack | `slack` | yes
-Telegram | `telegram` | yes
-Threema | `threema` | yes, external only
-VictorOps | `victorops` | yes, external only
-Webhook | `webhook` | yes, external only
+DingDing | `dingding` | yes, external only | no
+Discord | `discord` | yes | no
+Email | `email` | yes | no
+Google Hangouts Chat | `googlechat` | yes, external only | no
+Hipchat | `hipchat` | yes, external only | no
+Kafka | `kafka` | yes, external only | no
+Line | `line` | yes, external only | no
+Microsoft Teams | `teams` | yes, external only | no
+OpsGenie | `opsgenie` | yes, external only | no
+Pagerduty | `pagerduty` | yes, external only | no
+Prometheus Alertmanager | `prometheus-alertmanager` | yes, external only | yes
+Pushover | `pushover` | yes | no
+Sensu | `sensu` | yes, external only | no
+Slack | `slack` | yes | no
+Telegram | `telegram` | yes | no
+Threema | `threema` | yes, external only | no
+VictorOps | `victorops` | yes, external only | no
+Webhook | `webhook` | yes, external only | no
 
 # Enable images in notifications {#external-image-store}
 
@@ -197,6 +197,14 @@ Be aware that some notifiers requires public access to the image to be able to i
 
 Notification services which need public image access are marked as 'external only'.
 
+# Use alert rule tags in notifications {#alert-rule-tags}
+
+Grafana can include a list of tags (key/value) in the notification.
+It's called alert rule tags to contrast with tags parsed from timeseries.
+It currently supports only the Prometheus Alertmanager notifier.
+
+ This is an optional feature. You can get notifications without using alert rule tags.
+
 # Configure the link back to Grafana from alert notifications
 
 All alert notifications contain a link back to the triggered alert in the Grafana instance.

+ 15 - 0
pkg/models/alert.go

@@ -117,6 +117,21 @@ func (this *Alert) ContainsUpdates(other *Alert) bool {
 	return result
 }
 
+func (alert *Alert) GetTagsFromSettings() []*Tag {
+	tags := []*Tag{}
+	if alert.Settings != nil {
+		if data, ok := alert.Settings.CheckGet("alertRuleTags"); ok {
+			for tagNameString, tagValue := range data.MustMap() {
+				// MustMap() already guarantees the return of a `map[string]interface{}`.
+				// Therefore we only need to verify that tagValue is a String.
+				tagValueString := simplejson.NewFromAny(tagValue).MustString()
+				tags = append(tags, &Tag{Key: tagNameString, Value: tagValueString})
+			}
+		}
+	}
+	return tags
+}
+
 type AlertingClusterInfo struct {
 	ServerId       string
 	ClusterSize    int

+ 23 - 0
pkg/models/alert_test.go

@@ -35,5 +35,28 @@ func TestAlertingModelTest(t *testing.T) {
 			rule1.Settings = json2
 			So(rule1.ContainsUpdates(rule2), ShouldBeTrue)
 		})
+
+		Convey("Should parse alertRule tags correctly", func() {
+			json2, _ := simplejson.NewJson([]byte(`{
+				"field": "value",
+				"alertRuleTags": {
+					"foo": "bar",
+					"waldo": "fred",
+					"tagMap": { "mapValue": "value" }
+				}
+			}`))
+			rule1.Settings = json2
+			expectedTags := []*Tag{
+				{Id: 0, Key: "foo", Value: "bar"},
+				{Id: 0, Key: "waldo", Value: "fred"},
+				{Id: 0, Key: "tagMap", Value: ""},
+			}
+			actualTags := rule1.GetTagsFromSettings()
+
+			So(len(actualTags), ShouldEqual, len(expectedTags))
+			for _, tag := range expectedTags {
+				So(ContainsTag(actualTags, tag), ShouldBeTrue)
+			}
+		})
 	})
 }

+ 4 - 1
pkg/services/alerting/notifiers/alertmanager.go

@@ -93,7 +93,7 @@ func (am *AlertmanagerNotifier) createAlert(evalContext *alerting.EvalContext, m
 		alertJSON.SetPath([]string{"annotations", "image"}, evalContext.ImagePublicURL)
 	}
 
-	// Labels (from metrics tags + mandatory alertname).
+	// Labels (from metrics tags + AlertRuleTags + mandatory alertname).
 	tags := make(map[string]string)
 	if match != nil {
 		if len(match.Tags) == 0 {
@@ -104,6 +104,9 @@ func (am *AlertmanagerNotifier) createAlert(evalContext *alerting.EvalContext, m
 			}
 		}
 	}
+	for _, tag := range evalContext.Rule.AlertRuleTags {
+		tags[tag.Key] = tag.Value
+	}
 	tags["alertname"] = evalContext.Rule.Name
 	alertJSON.Set("labels", tags)
 	return alertJSON

+ 2 - 0
pkg/services/alerting/rule.go

@@ -35,6 +35,7 @@ type Rule struct {
 	State               models.AlertStateType
 	Conditions          []Condition
 	Notifications       []string
+	AlertRuleTags       []*models.Tag
 
 	StateChanges int64
 }
@@ -145,6 +146,7 @@ func NewRuleFromDBAlert(ruleDef *models.Alert) (*Rule, error) {
 			model.Notifications = append(model.Notifications, uid)
 		}
 	}
+	model.AlertRuleTags = ruleDef.GetTagsFromSettings()
 
 	for index, condition := range ruleDef.Settings.Get("conditions").MustArray() {
 		conditionModel := simplejson.NewFromAny(condition)

+ 19 - 0
pkg/services/sqlstore/alert.go

@@ -64,6 +64,10 @@ func deleteAlertByIdInternal(alertId int64, reason string, sess *DBSession) erro
 		return err
 	}
 
+	if _, err := sess.Exec("DELETE FROM alert_rule_tag WHERE alert_id = ?", alertId); err != nil {
+		return err
+	}
+
 	return nil
 }
 
@@ -215,6 +219,21 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS
 
 			sqlog.Debug("Alert inserted", "name", alert.Name, "id", alert.Id)
 		}
+		tags := alert.GetTagsFromSettings()
+		if _, err := sess.Exec("DELETE FROM alert_rule_tag WHERE alert_id = ?", alert.Id); err != nil {
+			return err
+		}
+		if tags != nil {
+			tags, err := EnsureTagsExist(sess, tags)
+			if err != nil {
+				return err
+			}
+			for _, tag := range tags {
+				if _, err := sess.Exec("INSERT INTO alert_rule_tag (alert_id, tag_id) VALUES(?,?)", alert.Id, tag.Id); err != nil {
+					return err
+				}
+			}
+		}
 	}
 
 	return nil

+ 2 - 22
pkg/services/sqlstore/annotation.go

@@ -29,7 +29,7 @@ func (r *SqlAnnotationRepo) Save(item *annotations.Item) error {
 		}
 
 		if item.Tags != nil {
-			tags, err := r.ensureTagsExist(sess, tags)
+			tags, err := EnsureTagsExist(sess, tags)
 			if err != nil {
 				return err
 			}
@@ -44,26 +44,6 @@ func (r *SqlAnnotationRepo) Save(item *annotations.Item) error {
 	})
 }
 
-// Will insert if needed any new key/value pars and return ids
-func (r *SqlAnnotationRepo) ensureTagsExist(sess *DBSession, tags []*models.Tag) ([]*models.Tag, error) {
-	for _, tag := range tags {
-		var existingTag models.Tag
-
-		// check if it exists
-		if exists, err := sess.Table("tag").Where(dialect.Quote("key")+"=? AND "+dialect.Quote("value")+"=?", tag.Key, tag.Value).Get(&existingTag); err != nil {
-			return nil, err
-		} else if exists {
-			tag.Id = existingTag.Id
-		} else {
-			if _, err := sess.Table("tag").Insert(tag); err != nil {
-				return nil, err
-			}
-		}
-	}
-
-	return tags, nil
-}
-
 func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
 	return inTransaction(func(sess *DBSession) error {
 		var (
@@ -94,7 +74,7 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
 		}
 
 		if item.Tags != nil {
-			tags, err := r.ensureTagsExist(sess, models.ParseTagPairs(item.Tags))
+			tags, err := EnsureTagsExist(sess, models.ParseTagPairs(item.Tags))
 			if err != nil {
 				return err
 			}

+ 0 - 28
pkg/services/sqlstore/annotation_test.go

@@ -5,37 +5,9 @@ import (
 
 	. "github.com/smartystreets/goconvey/convey"
 
-	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/annotations"
 )
 
-func TestSavingTags(t *testing.T) {
-	InitTestDB(t)
-
-	Convey("Testing annotation saving/loading", t, func() {
-
-		repo := SqlAnnotationRepo{}
-
-		Convey("Can save tags", func() {
-			Reset(func() {
-				_, err := x.Exec("DELETE FROM annotation_tag WHERE 1=1")
-				So(err, ShouldBeNil)
-			})
-
-			tagPairs := []*models.Tag{
-				{Key: "outage"},
-				{Key: "type", Value: "outage"},
-				{Key: "server", Value: "server-1"},
-				{Key: "error"},
-			}
-			tags, err := repo.ensureTagsExist(newSession(), tagPairs)
-
-			So(err, ShouldBeNil)
-			So(len(tags), ShouldEqual, 4)
-		})
-	})
-}
-
 func TestAnnotations(t *testing.T) {
 	InitTestDB(t)
 

+ 14 - 0
pkg/services/sqlstore/migrations/alert_mig.go

@@ -45,6 +45,20 @@ func addAlertMigrations(mg *Migrator) {
 	mg.AddMigration("add index alert state", NewAddIndexMigration(alertV1, alertV1.Indices[1]))
 	mg.AddMigration("add index alert dashboard_id", NewAddIndexMigration(alertV1, alertV1.Indices[2]))
 
+	alertRuleTagTable := Table{
+		Name: "alert_rule_tag",
+		Columns: []*Column{
+			{Name: "alert_id", Type: DB_BigInt, Nullable: false},
+			{Name: "tag_id", Type: DB_BigInt, Nullable: false},
+		},
+		Indices: []*Index{
+			{Cols: []string{"alert_id", "tag_id"}, Type: UniqueIndex},
+		},
+	}
+
+	mg.AddMigration("Create alert_rule_tag table v1", NewAddTableMigration(alertRuleTagTable))
+	mg.AddMigration("Add unique index alert_rule_tag.alert_id_tag_id", NewAddIndexMigration(alertRuleTagTable, alertRuleTagTable.Indices[0]))
+
 	alert_notification := Table{
 		Name: "alert_notification",
 		Columns: []*Column{

+ 23 - 0
pkg/services/sqlstore/tags.go

@@ -0,0 +1,23 @@
+package sqlstore
+
+import "github.com/grafana/grafana/pkg/models"
+
+// Will insert if needed any new key/value pars and return ids
+func EnsureTagsExist(sess *DBSession, tags []*models.Tag) ([]*models.Tag, error) {
+	for _, tag := range tags {
+		var existingTag models.Tag
+
+		// check if it exists
+		if exists, err := sess.Table("tag").Where("`key`=? AND `value`=?", tag.Key, tag.Value).Get(&existingTag); err != nil {
+			return nil, err
+		} else if exists {
+			tag.Id = existingTag.Id
+		} else {
+			if _, err := sess.Table("tag").Insert(tag); err != nil {
+				return nil, err
+			}
+		}
+	}
+
+	return tags, nil
+}

+ 26 - 0
pkg/services/sqlstore/tags_test.go

@@ -0,0 +1,26 @@
+package sqlstore
+
+import (
+	"testing"
+
+	. "github.com/smartystreets/goconvey/convey"
+
+	"github.com/grafana/grafana/pkg/models"
+)
+
+func TestSavingTags(t *testing.T) {
+	Convey("Testing tags saving", t, func() {
+		InitTestDB(t)
+
+		tagPairs := []*models.Tag{
+			{Key: "outage"},
+			{Key: "type", Value: "outage"},
+			{Key: "server", Value: "server-1"},
+			{Key: "error"},
+		}
+		tags, err := EnsureTagsExist(newSession(), tagPairs)
+
+		So(err, ShouldBeNil)
+		So(len(tags), ShouldEqual, 4)
+	})
+}

+ 14 - 0
public/app/features/alerting/AlertTabCtrl.ts

@@ -28,6 +28,7 @@ export class AlertTabCtrl {
   error: string;
   appSubUrl: string;
   alertHistory: any;
+  newAlertRuleTag: any;
 
   /** @ngInject */
   constructor(
@@ -158,6 +159,18 @@ export class AlertTabCtrl {
     _.remove(this.alertNotifications, (n: any) => n.uid === an.uid || n.id === an.id);
   }
 
+  addAlertRuleTag() {
+    if (this.newAlertRuleTag.name) {
+      this.alert.alertRuleTags[this.newAlertRuleTag.name] = this.newAlertRuleTag.value;
+    }
+    this.newAlertRuleTag.name = '';
+    this.newAlertRuleTag.value = '';
+  }
+
+  removeAlertRuleTag(tagName) {
+    delete this.alert.alertRuleTags[tagName];
+  }
+
   initModel() {
     const alert = (this.alert = this.panel.alert);
     if (!alert) {
@@ -175,6 +188,7 @@ export class AlertTabCtrl {
     alert.handler = alert.handler || 1;
     alert.notifications = alert.notifications || [];
     alert.for = alert.for || '0m';
+    alert.alertRuleTags = alert.alertRuleTags || {};
 
     const defaultName = this.panel.title + ' alert';
     alert.name = alert.name || defaultName;

+ 32 - 0
public/app/features/alerting/partials/alert_tab.html

@@ -149,6 +149,38 @@
         <textarea class="gf-form-input" rows="10" ng-model="ctrl.alert.message"
                   placeholder="Notification message details..."></textarea>
       </div>
+      <div class="gf-form">
+        <span class="gf-form-label width-8">Tags</span>
+        <div class="gf-form-group">
+          <div class="gf-form-inline" ng-repeat="(name, value) in ctrl.alert.alertRuleTags">
+            <label class="gf-form-label width-15">{{ name }}</label>
+            <input class="gf-form-input width-15" placeholder="Tag value..."
+                   ng-model="ctrl.alert.alertRuleTags[name]" type="text"/>
+            <label class="gf-form-label">
+              <a class="pointer" tabindex="1" ng-click="ctrl.removeAlertRuleTag(name)">
+                <i class="fa fa-trash"></i>
+              </a>
+            </label>
+          </div>
+          <div class="gf-form-group">
+            <div class="gf-form-inline">
+              <div class="gf-form">
+                <input class="gf-form-input width-15" placeholder="New tag name..."
+                       ng-model="ctrl.newAlertRuleTag.name" type="text">
+                <input class="gf-form-input width-15" placeholder="New tag value..."
+                       ng-model="ctrl.newAlertRuleTag.value" type="text">
+              </div>
+            </div>
+            <div class="gf-form">
+              <label class="gf-form-label">
+                <a class="pointer" tabindex="1" ng-click="ctrl.addAlertRuleTag()">
+                  <i class="fa fa-plus"></i>&nbsp;Add Tag
+                </a>
+              </label>
+            </div>
+          </div>
+        </div>
+      </div>
     </div>
   </div>
 </div>