diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index 28d1c880216..b3a7e10b2a4 100644 --- a/docs/sources/alerting/notifications.md +++ b/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. diff --git a/pkg/models/alert.go b/pkg/models/alert.go index b9bcd3f0615..1deb74f9bf0 100644 --- a/pkg/models/alert.go +++ b/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 diff --git a/pkg/models/alert_test.go b/pkg/models/alert_test.go index 421b90f1b21..a1e47a71008 100644 --- a/pkg/models/alert_test.go +++ b/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) + } + }) }) } diff --git a/pkg/services/alerting/notifiers/alertmanager.go b/pkg/services/alerting/notifiers/alertmanager.go index a8fd7db2f5e..63f34f579ab 100644 --- a/pkg/services/alerting/notifiers/alertmanager.go +++ b/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 diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index 9a4065e279d..af33078b209 100644 --- a/pkg/services/alerting/rule.go +++ b/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) diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 7796cfd0dc7..48096df7d57 100644 --- a/pkg/services/sqlstore/alert.go +++ b/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 diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go index a285b231aae..8d1f8ce944d 100644 --- a/pkg/services/sqlstore/annotation.go +++ b/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 } diff --git a/pkg/services/sqlstore/annotation_test.go b/pkg/services/sqlstore/annotation_test.go index d3459527e7d..87eba6cab08 100644 --- a/pkg/services/sqlstore/annotation_test.go +++ b/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) diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index 86e7b3933b4..97aa3cdf60d 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/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{ diff --git a/pkg/services/sqlstore/tags.go b/pkg/services/sqlstore/tags.go new file mode 100644 index 00000000000..ac731cf5126 --- /dev/null +++ b/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 +} diff --git a/pkg/services/sqlstore/tags_test.go b/pkg/services/sqlstore/tags_test.go new file mode 100644 index 00000000000..67ca2479e41 --- /dev/null +++ b/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) + }) +} diff --git a/public/app/features/alerting/AlertTabCtrl.ts b/public/app/features/alerting/AlertTabCtrl.ts index 0b9e5d639b7..681e9938f95 100644 --- a/public/app/features/alerting/AlertTabCtrl.ts +++ b/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; diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index 4b31edebe8e..5b9f241d8d0 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -149,6 +149,38 @@ +