mirror of
https://github.com/grafana/grafana.git
synced 2025-01-15 19:22:34 -06:00
parent
34f314552d
commit
e06abb30aa
@ -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{
|
||||
|
23
pkg/services/sqlstore/tags.go
Normal file
23
pkg/services/sqlstore/tags.go
Normal file
@ -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
pkg/services/sqlstore/tags_test.go
Normal file
26
pkg/services/sqlstore/tags_test.go
Normal file
@ -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)
|
||||
})
|
||||
}
|
@ -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> Add Tag
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user