Commit e06abb30 by Thibault Chataigner Committed by Carl Bergquist

Alerting: Add tags to alert rules (#10989)

Ref #6552
parent 34f31455
...@@ -167,26 +167,26 @@ Notifications can be sent by setting up an incoming webhook in Google Hangouts c ...@@ -167,26 +167,26 @@ Notifications can be sent by setting up an incoming webhook in Google Hangouts c
### All supported notifiers ### All supported notifiers
Name | Type | Supports images Name | Type | Supports images |Support alert rule tags
-----|------------ | ------ -----|------------ | ------
DingDing | `dingding` | yes, external only DingDing | `dingding` | yes, external only | no
Discord | `discord` | yes Discord | `discord` | yes | no
Email | `email` | yes Email | `email` | yes | no
Google Hangouts Chat | `googlechat` | yes, external only Google Hangouts Chat | `googlechat` | yes, external only | no
Hipchat | `hipchat` | yes, external only Hipchat | `hipchat` | yes, external only | no
Kafka | `kafka` | yes, external only Kafka | `kafka` | yes, external only | no
Line | `line` | yes, external only Line | `line` | yes, external only | no
Microsoft Teams | `teams` | yes, external only Microsoft Teams | `teams` | yes, external only | no
OpsGenie | `opsgenie` | yes, external only OpsGenie | `opsgenie` | yes, external only | no
Pagerduty | `pagerduty` | yes, external only Pagerduty | `pagerduty` | yes, external only | no
Prometheus Alertmanager | `prometheus-alertmanager` | yes, external only Prometheus Alertmanager | `prometheus-alertmanager` | yes, external only | yes
Pushover | `pushover` | yes Pushover | `pushover` | yes | no
Sensu | `sensu` | yes, external only Sensu | `sensu` | yes, external only | no
Slack | `slack` | yes Slack | `slack` | yes | no
Telegram | `telegram` | yes Telegram | `telegram` | yes | no
Threema | `threema` | yes, external only Threema | `threema` | yes, external only | no
VictorOps | `victorops` | yes, external only VictorOps | `victorops` | yes, external only | no
Webhook | `webhook` | yes, external only Webhook | `webhook` | yes, external only | no
# Enable images in notifications {#external-image-store} # 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 ...@@ -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'. 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 # Configure the link back to Grafana from alert notifications
All alert notifications contain a link back to the triggered alert in the Grafana instance. All alert notifications contain a link back to the triggered alert in the Grafana instance.
......
...@@ -117,6 +117,21 @@ func (this *Alert) ContainsUpdates(other *Alert) bool { ...@@ -117,6 +117,21 @@ func (this *Alert) ContainsUpdates(other *Alert) bool {
return result 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 { type AlertingClusterInfo struct {
ServerId string ServerId string
ClusterSize int ClusterSize int
......
...@@ -35,5 +35,28 @@ func TestAlertingModelTest(t *testing.T) { ...@@ -35,5 +35,28 @@ func TestAlertingModelTest(t *testing.T) {
rule1.Settings = json2 rule1.Settings = json2
So(rule1.ContainsUpdates(rule2), ShouldBeTrue) 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)
}
})
}) })
} }
...@@ -93,7 +93,7 @@ func (am *AlertmanagerNotifier) createAlert(evalContext *alerting.EvalContext, m ...@@ -93,7 +93,7 @@ func (am *AlertmanagerNotifier) createAlert(evalContext *alerting.EvalContext, m
alertJSON.SetPath([]string{"annotations", "image"}, evalContext.ImagePublicURL) 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) tags := make(map[string]string)
if match != nil { if match != nil {
if len(match.Tags) == 0 { if len(match.Tags) == 0 {
...@@ -104,6 +104,9 @@ func (am *AlertmanagerNotifier) createAlert(evalContext *alerting.EvalContext, m ...@@ -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 tags["alertname"] = evalContext.Rule.Name
alertJSON.Set("labels", tags) alertJSON.Set("labels", tags)
return alertJSON return alertJSON
......
...@@ -35,6 +35,7 @@ type Rule struct { ...@@ -35,6 +35,7 @@ type Rule struct {
State models.AlertStateType State models.AlertStateType
Conditions []Condition Conditions []Condition
Notifications []string Notifications []string
AlertRuleTags []*models.Tag
StateChanges int64 StateChanges int64
} }
...@@ -145,6 +146,7 @@ func NewRuleFromDBAlert(ruleDef *models.Alert) (*Rule, error) { ...@@ -145,6 +146,7 @@ func NewRuleFromDBAlert(ruleDef *models.Alert) (*Rule, error) {
model.Notifications = append(model.Notifications, uid) model.Notifications = append(model.Notifications, uid)
} }
} }
model.AlertRuleTags = ruleDef.GetTagsFromSettings()
for index, condition := range ruleDef.Settings.Get("conditions").MustArray() { for index, condition := range ruleDef.Settings.Get("conditions").MustArray() {
conditionModel := simplejson.NewFromAny(condition) conditionModel := simplejson.NewFromAny(condition)
......
...@@ -64,6 +64,10 @@ func deleteAlertByIdInternal(alertId int64, reason string, sess *DBSession) erro ...@@ -64,6 +64,10 @@ func deleteAlertByIdInternal(alertId int64, reason string, sess *DBSession) erro
return err return err
} }
if _, err := sess.Exec("DELETE FROM alert_rule_tag WHERE alert_id = ?", alertId); err != nil {
return err
}
return nil return nil
} }
...@@ -215,6 +219,21 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS ...@@ -215,6 +219,21 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS
sqlog.Debug("Alert inserted", "name", alert.Name, "id", alert.Id) 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 return nil
......
...@@ -29,7 +29,7 @@ func (r *SqlAnnotationRepo) Save(item *annotations.Item) error { ...@@ -29,7 +29,7 @@ func (r *SqlAnnotationRepo) Save(item *annotations.Item) error {
} }
if item.Tags != nil { if item.Tags != nil {
tags, err := r.ensureTagsExist(sess, tags) tags, err := EnsureTagsExist(sess, tags)
if err != nil { if err != nil {
return err return err
} }
...@@ -44,26 +44,6 @@ func (r *SqlAnnotationRepo) Save(item *annotations.Item) error { ...@@ -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 { func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
return inTransaction(func(sess *DBSession) error { return inTransaction(func(sess *DBSession) error {
var ( var (
...@@ -94,7 +74,7 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error { ...@@ -94,7 +74,7 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
} }
if item.Tags != nil { if item.Tags != nil {
tags, err := r.ensureTagsExist(sess, models.ParseTagPairs(item.Tags)) tags, err := EnsureTagsExist(sess, models.ParseTagPairs(item.Tags))
if err != nil { if err != nil {
return err return err
} }
......
...@@ -5,37 +5,9 @@ import ( ...@@ -5,37 +5,9 @@ import (
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/annotations" "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) { func TestAnnotations(t *testing.T) {
InitTestDB(t) InitTestDB(t)
......
...@@ -45,6 +45,20 @@ func addAlertMigrations(mg *Migrator) { ...@@ -45,6 +45,20 @@ func addAlertMigrations(mg *Migrator) {
mg.AddMigration("add index alert state", NewAddIndexMigration(alertV1, alertV1.Indices[1])) mg.AddMigration("add index alert state", NewAddIndexMigration(alertV1, alertV1.Indices[1]))
mg.AddMigration("add index alert dashboard_id", NewAddIndexMigration(alertV1, alertV1.Indices[2])) 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{ alert_notification := Table{
Name: "alert_notification", Name: "alert_notification",
Columns: []*Column{ Columns: []*Column{
......
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
}
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)
})
}
...@@ -28,6 +28,7 @@ export class AlertTabCtrl { ...@@ -28,6 +28,7 @@ export class AlertTabCtrl {
error: string; error: string;
appSubUrl: string; appSubUrl: string;
alertHistory: any; alertHistory: any;
newAlertRuleTag: any;
/** @ngInject */ /** @ngInject */
constructor( constructor(
...@@ -158,6 +159,18 @@ export class AlertTabCtrl { ...@@ -158,6 +159,18 @@ export class AlertTabCtrl {
_.remove(this.alertNotifications, (n: any) => n.uid === an.uid || n.id === an.id); _.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() { initModel() {
const alert = (this.alert = this.panel.alert); const alert = (this.alert = this.panel.alert);
if (!alert) { if (!alert) {
...@@ -175,6 +188,7 @@ export class AlertTabCtrl { ...@@ -175,6 +188,7 @@ export class AlertTabCtrl {
alert.handler = alert.handler || 1; alert.handler = alert.handler || 1;
alert.notifications = alert.notifications || []; alert.notifications = alert.notifications || [];
alert.for = alert.for || '0m'; alert.for = alert.for || '0m';
alert.alertRuleTags = alert.alertRuleTags || {};
const defaultName = this.panel.title + ' alert'; const defaultName = this.panel.title + ' alert';
alert.name = alert.name || defaultName; alert.name = alert.name || defaultName;
......
...@@ -149,6 +149,38 @@ ...@@ -149,6 +149,38 @@
<textarea class="gf-form-input" rows="10" ng-model="ctrl.alert.message" <textarea class="gf-form-input" rows="10" ng-model="ctrl.alert.message"
placeholder="Notification message details..."></textarea> placeholder="Notification message details..."></textarea>
</div> </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> </div>
</div> </div>
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