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
### 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.
......
......@@ -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
......
......@@ -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)
}
})
})
}
......@@ -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
......
......@@ -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)
......
......@@ -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
......
......@@ -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
}
......
......@@ -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)
......
......@@ -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{
......
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 {
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;
......
......@@ -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>
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