diff --git a/emails/templates/ng_alert_notification.html b/emails/templates/ng_alert_notification.html new file mode 100644 index 00000000000..71d0542640a --- /dev/null +++ b/emails/templates/ng_alert_notification.html @@ -0,0 +1,130 @@ +[[Subject .Subject "[[.Title]]"]] + + + + + + + +
+ + + + +
+

[[.Title]]

+
+
+ + + + + +
+ + + [[ if gt (len .Alerts.Firing) 0 ]] + + + + + +
+ [[ else ]] + + [[ end ]]. + [[ .Alerts | len ]] alert[[ if gt (len .Alerts) 1 ]]s[[ end ]] for + [[ range .GroupLabels.SortedPairs ]] + [[ .Name ]]=[[ .Value ]] + [[ end ]] +
+ + [[ if gt (len .Alerts.Firing) 0 ]] + + + + [[ end ]] + [[ range .Alerts.Firing ]] + + + + [[ end ]] + + [[ if gt (len .Alerts.Resolved) 0 ]] + [[ if gt (len .Alerts.Firing) 0 ]] + + + + [[ end ]] + + + + [[ end ]] + [[ range .Alerts.Resolved ]] + + + + [[ end ]] +
+ ([[ .Alerts.Firing | len ]]) Firing +
+ Labels
+ [[ range .Labels.SortedPairs ]][[ .Name ]] = [[ .Value ]]
[[ end ]] + [[ if gt (len .Annotations) 0 ]]Annotations
[[ end ]] + [[ range .Annotations.SortedPairs ]][[ .Name ]] = [[ .Value ]]
[[ end ]] + Source
+
+
+
+
+
+ ([[ .Alerts.Resolved | len ]]) Resolved +
+ Labels
+ [[ range .Labels.SortedPairs ]][[ .Name ]] = [[ .Value ]]
[[ end ]] + [[ if gt (len .Annotations) 0 ]]Annotations
[[ end ]] + [[ range .Annotations.SortedPairs ]][[ .Name ]] = [[ .Value ]]
[[ end ]] + Source
+
+
+
+ + + + + + + +
+ + + + + +
+ + + + +
+ View your Alert rule +
+
+ + + + +
+ Go to the Alerts page +
+
+
+ + diff --git a/go.sum b/go.sum index fc6fdafa73e..90912aaf986 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,7 @@ cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUM cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/bigtable v1.1.0/go.mod h1:B6ByKcIdYmhoyDzmOnQxyOhN6r05qnewYIxxG6L0/b4= +cloud.google.com/go/bigtable v1.2.0 h1:F4cCmA4nuV84V5zYQ3MKY+M1Cw1avHDuf3S/LcZPA9c= cloud.google.com/go/bigtable v1.2.0/go.mod h1:JcVAOl45lrTmQfLj7T6TxyMzIN/3FGGcFm+2xVAli2o= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= @@ -183,7 +184,6 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkY github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= @@ -233,14 +233,12 @@ github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pO github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/bsm/sarama-cluster v2.1.13+incompatible/go.mod h1:r7ao+4tTNXvWm+VRpRJchr2kQhqxgmAp2iEX5W96gMM= github.com/c-bata/go-prompt v0.2.2/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34= -github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee h1:BnPxIde0gjtTnc9Er7cxvBk8DHLWhEux0SxayC8dP6I= github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v0.0.0-20181003080854-62661b46c409/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff v1.0.0/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -github.com/cenkalti/backoff/v4 v4.0.2 h1:JIufpQLbh4DkbQoii76ItQIUFzevQSqOLZca4eamEDs= github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= github.com/cenkalti/backoff/v4 v4.1.0 h1:c8LkOFQTzuO0WBM/ae5HdGQuZPfPxp7lqBRwQRm4fSc= github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= @@ -482,7 +480,6 @@ github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpR github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= github.com/go-openapi/analysis v0.19.4/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= -github.com/go-openapi/analysis v0.19.10 h1:5BHISBAXOc/aJK25irLZnx2D3s6WyYaY9D4gmuz9fdE= github.com/go-openapi/analysis v0.19.10/go.mod h1:qmhS3VNFxBlquFJ0RGoDtylO9y4pgTAUNE9AEEMdlJQ= github.com/go-openapi/analysis v0.19.16/go.mod h1:GLInF007N83Ad3m8a/CbQ5TPzdnGT7workfHwuVjNVk= github.com/go-openapi/analysis v0.20.0 h1:UN09o0kNhleunxW7LR+KnltD0YrJ8FF03pSqvAN3Vro= @@ -492,12 +489,10 @@ github.com/go-openapi/errors v0.17.2/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQH github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= github.com/go-openapi/errors v0.19.3/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= -github.com/go-openapi/errors v0.19.4 h1:fSGwO1tSYHFu70NKaWJt5Qh0qoBRtCm/mXS1yhf+0W0= github.com/go-openapi/errors v0.19.4/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= github.com/go-openapi/errors v0.19.6/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= github.com/go-openapi/errors v0.19.7/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= -github.com/go-openapi/errors v0.19.9 h1:9SnKdGhiPZHF3ttwFMiCBEb8jQ4IDdrK+5+a0oTygA4= github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= github.com/go-openapi/errors v0.20.0 h1:Sxpo9PjEHDzhs3FbnGNonvDgWcMW2U7wGTcDDSFSceM= github.com/go-openapi/errors v0.20.0/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= @@ -524,7 +519,6 @@ github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= github.com/go-openapi/loads v0.19.3/go.mod h1:YVfqhUCdahYwR3f3iiwQLhicVRvLlU/WO5WPaZvcvSI= github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk= -github.com/go-openapi/loads v0.19.5 h1:jZVYWawIQiA1NBnHla28ktg6hrcfTHsCE+3QLVRBIls= github.com/go-openapi/loads v0.19.5/go.mod h1:dswLCAdonkRufe/gSUC3gN8nTSaB9uaS2es0x5/IbjY= github.com/go-openapi/loads v0.19.6/go.mod h1:brCsvE6j8mnbmGBh103PT/QLHfbyDxA4hsKvYBNEGVc= github.com/go-openapi/loads v0.19.7/go.mod h1:brCsvE6j8mnbmGBh103PT/QLHfbyDxA4hsKvYBNEGVc= @@ -536,10 +530,8 @@ github.com/go-openapi/runtime v0.18.0/go.mod h1:uI6pHuxWYTy94zZxgcwJkUWa9wbIlhte github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= github.com/go-openapi/runtime v0.19.3/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= -github.com/go-openapi/runtime v0.19.15 h1:2GIefxs9Rx1vCDNghRtypRq+ig8KSLrjHbAYI/gCLCM= github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo= github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiSjahULvYmlv98= -github.com/go-openapi/runtime v0.19.24 h1:TqagMVlRAOTwllE/7hNKx6rQ10O6T8ZzeJdMjSTKaD4= github.com/go-openapi/runtime v0.19.24/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk= github.com/go-openapi/runtime v0.19.26 h1:K/6PoVNj5WJXUnMk+VEbELeXjtBkCS1UxTDa04tdXE0= github.com/go-openapi/runtime v0.19.26/go.mod h1:BvrQtn6iVb2QmiVXRsFAm6ZCAZBpbVKFfN6QWCp582M= @@ -1457,7 +1449,6 @@ github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4= github.com/satori/go.uuid v0.0.0-20160603004225-b111a074d5ef/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM= github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/schollz/progressbar/v3 v3.3.4/go.mod h1:Rp5lZwpgtYmlvmGo1FyDwXMqagyRBQYSDwzlP9QDu84= @@ -1839,7 +1830,6 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= diff --git a/pkg/services/ngalert/notifier/channels/email.go b/pkg/services/ngalert/notifier/channels/email.go new file mode 100644 index 00000000000..b2cf32e821b --- /dev/null +++ b/pkg/services/ngalert/notifier/channels/email.go @@ -0,0 +1,92 @@ +package channels + +import ( + "context" + "net/url" + + gokit_log "github.com/go-kit/kit/log" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" + old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers" + "github.com/grafana/grafana/pkg/util" + + "github.com/grafana/grafana/pkg/bus" + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/template" + "github.com/prometheus/alertmanager/types" + "github.com/prometheus/common/model" +) + +// EmailNotifier is responsible for sending +// alert notifications over email. +type EmailNotifier struct { + old_notifiers.NotifierBase + Addresses []string + SingleEmail bool + log log.Logger + externalUrl *url.URL +} + +// NewEmailNotifier is the constructor function +// for the EmailNotifier. +func NewEmailNotifier(model *models.AlertNotification) (*EmailNotifier, error) { + addressesString := model.Settings.Get("addresses").MustString() + singleEmail := model.Settings.Get("singleEmail").MustBool(false) + + if addressesString == "" { + return nil, alerting.ValidationError{Reason: "Could not find addresses in settings"} + } + + // split addresses with a few different ways + addresses := util.SplitEmails(addressesString) + + // TODO: remove this URL hack and add an actual external URL. + u, err := url.Parse("http://localhost") + if err != nil { + return nil, err + } + + return &EmailNotifier{ + NotifierBase: old_notifiers.NewNotifierBase(model), + Addresses: addresses, + SingleEmail: singleEmail, + log: log.New("alerting.notifier.email"), + externalUrl: u, + }, nil +} + +// Notify sends the alert notification. +func (en *EmailNotifier) Notify(ctx context.Context, as ...*types.Alert) error { + // TODO(codesome): make sure the receiver name is added in the ctx before calling this. + ctx = notify.WithReceiverName(ctx, "email-notification-channel") // Dummy. + // TODO(codesome): make sure the group labels is added in the ctx before calling this. + ctx = notify.WithGroupLabels(ctx, model.LabelSet{}) // Dummy. + + // We only need ExternalURL from this template object. This hack should go away with https://github.com/prometheus/alertmanager/pull/2508. + data := notify.GetTemplateData(ctx, &template.Template{ExternalURL: en.externalUrl}, as, gokit_log.NewNopLogger()) + + cmd := &models.SendEmailCommandSync{ + SendEmailCommand: models.SendEmailCommand{ + Subject: "TODO", + Data: map[string]interface{}{ + "Title": "TODO", + "Subject": "TODO", + "Receiver": data.Receiver, + "Status": data.Status, + "Alerts": data.Alerts, + "GroupLabels": data.GroupLabels, + "CommonLabels": data.CommonLabels, + "CommonAnnotations": data.CommonAnnotations, + "ExternalURL": data.ExternalURL, + "RuleUrl": "TODO", + "AlertPageUrl": "TODO", + }, + To: en.Addresses, + SingleEmail: en.SingleEmail, + Template: "ng_alert_notification.html", + }, + } + + return bus.DispatchCtx(ctx, cmd) +} diff --git a/pkg/services/ngalert/notifier/channels/email_test.go b/pkg/services/ngalert/notifier/channels/email_test.go new file mode 100644 index 00000000000..47f154fc6fe --- /dev/null +++ b/pkg/services/ngalert/notifier/channels/email_test.go @@ -0,0 +1,59 @@ +package channels + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/models" + "github.com/stretchr/testify/require" +) + +func TestEmailNotifier(t *testing.T) { + t.Run("empty settings should return error", func(t *testing.T) { + json := `{ }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &models.AlertNotification{ + Name: "ops", + Type: "email", + Settings: settingsJSON, + } + + _, err := NewEmailNotifier(model) + require.Error(t, err) + }) + + t.Run("from settings", func(t *testing.T) { + json := `{"addresses": "ops@grafana.org"}` + settingsJSON, err := simplejson.NewJson([]byte(json)) + require.NoError(t, err) + + emailNotifier, err := NewEmailNotifier(&models.AlertNotification{ + Name: "ops", + Type: "email", + Settings: settingsJSON, + }) + + require.NoError(t, err) + require.Equal(t, "ops", emailNotifier.Name) + require.Equal(t, "email", emailNotifier.Type) + require.Equal(t, []string{"ops@grafana.org"}, emailNotifier.Addresses) + }) + + t.Run("from settings with two emails", func(t *testing.T) { + json := `{"addresses": "ops@grafana.org;dev@grafana.org"}` + settingsJSON, err := simplejson.NewJson([]byte(json)) + require.NoError(t, err) + + emailNotifier, err := NewEmailNotifier(&models.AlertNotification{ + Name: "ops", + Type: "email", + Settings: settingsJSON, + }) + + require.NoError(t, err) + require.Equal(t, "ops", emailNotifier.Name) + require.Equal(t, "email", emailNotifier.Type) + require.Equal(t, []string{"ops@grafana.org", "dev@grafana.org"}, emailNotifier.Addresses) + }) +} diff --git a/pkg/services/notifications/mailer.go b/pkg/services/notifications/mailer.go index 92c8eb6c904..c44b0df2383 100644 --- a/pkg/services/notifications/mailer.go +++ b/pkg/services/notifications/mailer.go @@ -22,7 +22,7 @@ import ( "github.com/grafana/grafana/pkg/util/errutil" ) -func (ns *NotificationService) send(msg *Message) (int, error) { +func (ns *NotificationService) Send(msg *Message) (int, error) { messages := []*Message{} if msg.SingleEmail { diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go index a6d709ac338..797424c5e4d 100644 --- a/pkg/services/notifications/notifications.go +++ b/pkg/services/notifications/notifications.go @@ -83,7 +83,7 @@ func (ns *NotificationService) Run(ctx context.Context) error { ns.log.Error("Failed to send webrequest ", "error", err) } case msg := <-ns.mailQueue: - num, err := ns.send(msg) + num, err := ns.Send(msg) tos := strings.Join(msg.To, "; ") info := "" if err != nil { @@ -133,7 +133,7 @@ func (ns *NotificationService) sendEmailCommandHandlerSync(ctx context.Context, return err } - _, err = ns.send(message) + _, err = ns.Send(message) return err }