From 7c3ab4a7152e2287917d0b282f05350bba5fe8db Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Wed, 14 Dec 2022 10:59:37 -0500 Subject: [PATCH] Alerting: Remove dependency on Grafana notifications package in alerting notifiers (#60271) * create sender service interface and bridge to grafana notifier service * update notifiers to use local sender interface --- pkg/services/ngalert/notifier/alertmanager.go | 2 +- .../ngalert/notifier/channels/dingding.go | 7 +- .../ngalert/notifier/channels/discord.go | 9 +- .../ngalert/notifier/channels/email.go | 43 +++++---- .../ngalert/notifier/channels/email_test.go | 8 +- .../ngalert/notifier/channels/factory.go | 5 +- .../ngalert/notifier/channels/googlechat.go | 7 +- .../ngalert/notifier/channels/kafka.go | 7 +- .../ngalert/notifier/channels/line.go | 7 +- .../ngalert/notifier/channels/opsgenie.go | 7 +- .../ngalert/notifier/channels/pagerduty.go | 7 +- .../ngalert/notifier/channels/pushover.go | 7 +- .../ngalert/notifier/channels/sender.go | 46 ++++++++++ .../ngalert/notifier/channels/sensugo.go | 7 +- .../ngalert/notifier/channels/slack.go | 3 +- .../ngalert/notifier/channels/teams.go | 25 +++--- .../ngalert/notifier/channels/teams_test.go | 87 +++---------------- .../ngalert/notifier/channels/telegram.go | 11 ++- .../ngalert/notifier/channels/testing.go | 44 +++++++--- .../ngalert/notifier/channels/threema.go | 7 +- .../ngalert/notifier/channels/victorops.go | 7 +- .../ngalert/notifier/channels/webex.go | 7 +- .../ngalert/notifier/channels/webhook.go | 7 +- .../ngalert/notifier/channels/wecom.go | 7 +- pkg/services/ngalert/notifier/sender.go | 56 ++++++++++++ 25 files changed, 238 insertions(+), 192 deletions(-) create mode 100644 pkg/services/ngalert/notifier/channels/sender.go create mode 100644 pkg/services/ngalert/notifier/sender.go diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index 3dbdaf5f125..c107de17cf5 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -514,7 +514,7 @@ func (am *Alertmanager) buildReceiverIntegration(r *apimodels.PostableGrafanaRec SecureSettings: secureSettings, } ) - factoryConfig, err := channels.NewFactoryConfig(cfg, am.NotificationService, am.decryptFn, tmpl, am.Store) + factoryConfig, err := channels.NewFactoryConfig(cfg, NewNotificationSender(am.NotificationService), am.decryptFn, tmpl, am.Store) if err != nil { return nil, InvalidReceiverError{ Receiver: r, diff --git a/pkg/services/ngalert/notifier/channels/dingding.go b/pkg/services/ngalert/notifier/channels/dingding.go index bdf6a0d8b0f..f5ce6f90d10 100644 --- a/pkg/services/ngalert/notifier/channels/dingding.go +++ b/pkg/services/ngalert/notifier/channels/dingding.go @@ -12,7 +12,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/notifications" ) const defaultDingdingMsgType = "link" @@ -73,7 +72,7 @@ func newDingDingNotifier(fc FactoryConfig) (*DingDingNotifier, error) { type DingDingNotifier struct { *Base log log.Logger - ns notifications.WebhookSender + ns WebhookSender tmpl *template.Template settings dingDingSettings } @@ -107,9 +106,9 @@ func (dd *DingDingNotifier) Notify(ctx context.Context, as ...*types.Alert) (boo u = dd.settings.URL } - cmd := &models.SendWebhookSync{Url: u, Body: b} + cmd := &SendWebhookSettings{Url: u, Body: b} - if err := dd.ns.SendWebhookSync(ctx, cmd); err != nil { + if err := dd.ns.SendWebhook(ctx, cmd); err != nil { return false, fmt.Errorf("send notification to dingding: %w", err) } diff --git a/pkg/services/ngalert/notifier/channels/discord.go b/pkg/services/ngalert/notifier/channels/discord.go index e074337815c..3d7fb1532e0 100644 --- a/pkg/services/ngalert/notifier/channels/discord.go +++ b/pkg/services/ngalert/notifier/channels/discord.go @@ -20,14 +20,13 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) type DiscordNotifier struct { *Base log log.Logger - ns notifications.WebhookSender + ns WebhookSender images ImageStore tmpl *template.Template settings discordSettings @@ -179,7 +178,7 @@ func (d DiscordNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, return false, err } - if err := d.ns.SendWebhookSync(ctx, cmd); err != nil { + if err := d.ns.SendWebhook(ctx, cmd); err != nil { d.log.Error("failed to send notification to Discord", "error", err) return false, err } @@ -236,8 +235,8 @@ func (d DiscordNotifier) constructAttachments(ctx context.Context, as []*types.A return attachments } -func (d DiscordNotifier) buildRequest(url string, body []byte, attachments []discordAttachment) (*models.SendWebhookSync, error) { - cmd := &models.SendWebhookSync{ +func (d DiscordNotifier) buildRequest(url string, body []byte, attachments []discordAttachment) (*SendWebhookSettings, error) { + cmd := &SendWebhookSettings{ Url: url, HttpMethod: "POST", } diff --git a/pkg/services/ngalert/notifier/channels/email.go b/pkg/services/ngalert/notifier/channels/email.go index 16b2f01aac0..2ff07d12cb4 100644 --- a/pkg/services/ngalert/notifier/channels/email.go +++ b/pkg/services/ngalert/notifier/channels/email.go @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/util" ) @@ -27,7 +26,7 @@ type EmailNotifier struct { Message string Subject string log log.Logger - ns notifications.EmailSender + ns EmailSender images ImageStore tmpl *template.Template } @@ -69,7 +68,7 @@ func NewEmailConfig(config *NotificationChannelConfig) (*EmailConfig, error) { // NewEmailNotifier is the constructor function // for the EmailNotifier. -func NewEmailNotifier(config *EmailConfig, ns notifications.EmailSender, images ImageStore, t *template.Template) *EmailNotifier { +func NewEmailNotifier(config *EmailConfig, ns EmailSender, images ImageStore, t *template.Template) *EmailNotifier { return &EmailNotifier{ Base: NewBase(&models.AlertNotification{ Uid: config.UID, @@ -126,33 +125,31 @@ func (en *EmailNotifier) Notify(ctx context.Context, alerts ...*types.Alert) (bo return nil }, alerts...) - cmd := &models.SendEmailCommandSync{ - SendEmailCommand: models.SendEmailCommand{ - Subject: subject, - Data: map[string]interface{}{ - "Title": subject, - "Message": tmpl(en.Message), - "Status": data.Status, - "Alerts": data.Alerts, - "GroupLabels": data.GroupLabels, - "CommonLabels": data.CommonLabels, - "CommonAnnotations": data.CommonAnnotations, - "ExternalURL": data.ExternalURL, - "RuleUrl": ruleURL, - "AlertPageUrl": alertPageURL, - }, - EmbeddedFiles: embeddedFiles, - To: en.Addresses, - SingleEmail: en.SingleEmail, - Template: "ng_alert_notification", + cmd := &SendEmailSettings{ + Subject: subject, + Data: map[string]interface{}{ + "Title": subject, + "Message": tmpl(en.Message), + "Status": data.Status, + "Alerts": data.Alerts, + "GroupLabels": data.GroupLabels, + "CommonLabels": data.CommonLabels, + "CommonAnnotations": data.CommonAnnotations, + "ExternalURL": data.ExternalURL, + "RuleUrl": ruleURL, + "AlertPageUrl": alertPageURL, }, + EmbeddedFiles: embeddedFiles, + To: en.Addresses, + SingleEmail: en.SingleEmail, + Template: "ng_alert_notification", } if tmplErr != nil { en.log.Warn("failed to template email message", "error", tmplErr.Error()) } - if err := en.ns.SendEmailCommandHandlerSync(ctx, cmd); err != nil { + if err := en.ns.SendEmail(ctx, cmd); err != nil { return false, err } diff --git a/pkg/services/ngalert/notifier/channels/email_test.go b/pkg/services/ngalert/notifier/channels/email_test.go index d29bc1273f0..149839d92da 100644 --- a/pkg/services/ngalert/notifier/channels/email_test.go +++ b/pkg/services/ngalert/notifier/channels/email_test.go @@ -104,7 +104,7 @@ func TestEmailNotifier(t *testing.T) { } func TestEmailNotifierIntegration(t *testing.T) { - ns := CreateNotificationService(t) + ns := createEmailSender(t) emailTmpl := templateForTests(t) externalURL, err := url.Parse("http://localhost/base") @@ -267,7 +267,7 @@ func TestEmailNotifierIntegration(t *testing.T) { } } -func createSut(t *testing.T, messageTmpl string, subjectTmpl string, emailTmpl *template.Template, ns notifications.EmailSender) *EmailNotifier { +func createSut(t *testing.T, messageTmpl string, subjectTmpl string, emailTmpl *template.Template, ns *emailSender) *EmailNotifier { t.Helper() json := `{ @@ -295,10 +295,10 @@ func createSut(t *testing.T, messageTmpl string, subjectTmpl string, emailTmpl * return emailNotifier } -func getSingleSentMessage(t *testing.T, ns *notifications.NotificationService) *notifications.Message { +func getSingleSentMessage(t *testing.T, ns *emailSender) *notifications.Message { t.Helper() - mailer := ns.GetMailer().(*notifications.FakeMailer) + mailer := ns.ns.GetMailer().(*notifications.FakeMailer) require.Len(t, mailer.Sent, 1) sent := mailer.Sent[0] mailer.Sent = []*notifications.Message{} diff --git a/pkg/services/ngalert/notifier/channels/factory.go b/pkg/services/ngalert/notifier/channels/factory.go index f8eb66286e2..5f5c08ce591 100644 --- a/pkg/services/ngalert/notifier/channels/factory.go +++ b/pkg/services/ngalert/notifier/channels/factory.go @@ -8,12 +8,11 @@ import ( "github.com/prometheus/alertmanager/template" "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/notifications" ) type FactoryConfig struct { Config *NotificationChannelConfig - NotificationService notifications.Service + NotificationService NotificationSender DecryptFunc GetDecryptedValueFn ImageStore ImageStore // Used to retrieve image URLs for messages, or data for uploads. @@ -24,7 +23,7 @@ type ImageStore interface { GetImage(ctx context.Context, token string) (*models.Image, error) } -func NewFactoryConfig(config *NotificationChannelConfig, notificationService notifications.Service, +func NewFactoryConfig(config *NotificationChannelConfig, notificationService NotificationSender, decryptFunc GetDecryptedValueFn, template *template.Template, imageStore ImageStore) (FactoryConfig, error) { if config.Settings == nil { return FactoryConfig{}, errors.New("no settings supplied") diff --git a/pkg/services/ngalert/notifier/channels/googlechat.go b/pkg/services/ngalert/notifier/channels/googlechat.go index 544daf92fbd..cdc5b6b31ef 100644 --- a/pkg/services/ngalert/notifier/channels/googlechat.go +++ b/pkg/services/ngalert/notifier/channels/googlechat.go @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -23,7 +22,7 @@ import ( type GoogleChatNotifier struct { *Base log log.Logger - ns notifications.WebhookSender + ns WebhookSender images ImageStore tmpl *template.Template settings googleChatSettings @@ -160,7 +159,7 @@ func (gcn *GoogleChatNotifier) Notify(ctx context.Context, as ...*types.Alert) ( return false, fmt.Errorf("marshal json: %w", err) } - cmd := &models.SendWebhookSync{ + cmd := &SendWebhookSettings{ Url: u, HttpMethod: "POST", HttpHeader: map[string]string{ @@ -169,7 +168,7 @@ func (gcn *GoogleChatNotifier) Notify(ctx context.Context, as ...*types.Alert) ( Body: string(body), } - if err := gcn.ns.SendWebhookSync(ctx, cmd); err != nil { + if err := gcn.ns.SendWebhook(ctx, cmd); err != nil { gcn.log.Error("Failed to send Google Hangouts Chat alert", "error", err, "webhook", gcn.Name) return false, err } diff --git a/pkg/services/ngalert/notifier/channels/kafka.go b/pkg/services/ngalert/notifier/channels/kafka.go index 7d8b2f777e2..bc0aff3fff2 100644 --- a/pkg/services/ngalert/notifier/channels/kafka.go +++ b/pkg/services/ngalert/notifier/channels/kafka.go @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/notifications" ) // KafkaNotifier is responsible for sending @@ -23,7 +22,7 @@ type KafkaNotifier struct { *Base log log.Logger images ImageStore - ns notifications.WebhookSender + ns WebhookSender tmpl *template.Template settings kafkaSettings } @@ -91,7 +90,7 @@ func (kn *KafkaNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, kn.log.Warn("failed to template Kafka message", "error", tmplErr.Error()) } - cmd := &models.SendWebhookSync{ + cmd := &SendWebhookSettings{ Url: topicURL, Body: body, HttpMethod: "POST", @@ -101,7 +100,7 @@ func (kn *KafkaNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, }, } - if err = kn.ns.SendWebhookSync(ctx, cmd); err != nil { + if err = kn.ns.SendWebhook(ctx, cmd); err != nil { kn.log.Error("Failed to send notification to Kafka", "error", err, "body", body) return false, err } diff --git a/pkg/services/ngalert/notifier/channels/line.go b/pkg/services/ngalert/notifier/channels/line.go index d46703f9280..99231acb773 100644 --- a/pkg/services/ngalert/notifier/channels/line.go +++ b/pkg/services/ngalert/notifier/channels/line.go @@ -12,7 +12,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/notifications" ) var ( @@ -24,7 +23,7 @@ var ( type LineNotifier struct { *Base log log.Logger - ns notifications.WebhookSender + ns WebhookSender tmpl *template.Template settings lineSettings } @@ -79,7 +78,7 @@ func (ln *LineNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, e form := url.Values{} form.Add("message", body) - cmd := &models.SendWebhookSync{ + cmd := &SendWebhookSettings{ Url: LineNotifyURL, HttpMethod: "POST", HttpHeader: map[string]string{ @@ -89,7 +88,7 @@ func (ln *LineNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, e Body: form.Encode(), } - if err := ln.ns.SendWebhookSync(ctx, cmd); err != nil { + if err := ln.ns.SendWebhook(ctx, cmd); err != nil { ln.log.Error("failed to send notification to LINE", "error", err, "body", body) return false, err } diff --git a/pkg/services/ngalert/notifier/channels/opsgenie.go b/pkg/services/ngalert/notifier/channels/opsgenie.go index f88a290217b..13b88378eaf 100644 --- a/pkg/services/ngalert/notifier/channels/opsgenie.go +++ b/pkg/services/ngalert/notifier/channels/opsgenie.go @@ -18,7 +18,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/notifications" ) const ( @@ -39,7 +38,7 @@ type OpsgenieNotifier struct { *Base tmpl *template.Template log log.Logger - ns notifications.WebhookSender + ns WebhookSender images ImageStore settings *opsgenieSettings } @@ -163,7 +162,7 @@ func (on *OpsgenieNotifier) Notify(ctx context.Context, as ...*types.Alert) (boo return true, nil } - cmd := &models.SendWebhookSync{ + cmd := &SendWebhookSettings{ Url: url, Body: string(body), HttpMethod: http.MethodPost, @@ -173,7 +172,7 @@ func (on *OpsgenieNotifier) Notify(ctx context.Context, as ...*types.Alert) (boo }, } - if err := on.ns.SendWebhookSync(ctx, cmd); err != nil { + if err := on.ns.SendWebhook(ctx, cmd); err != nil { return false, fmt.Errorf("send notification to Opsgenie: %w", err) } diff --git a/pkg/services/ngalert/notifier/channels/pagerduty.go b/pkg/services/ngalert/notifier/channels/pagerduty.go index ee90a20726b..95c1fd60d69 100644 --- a/pkg/services/ngalert/notifier/channels/pagerduty.go +++ b/pkg/services/ngalert/notifier/channels/pagerduty.go @@ -16,7 +16,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/notifications" ) const ( @@ -45,7 +44,7 @@ type PagerdutyNotifier struct { *Base tmpl *template.Template log log.Logger - ns notifications.WebhookSender + ns WebhookSender images ImageStore settings *pagerdutySettings } @@ -166,7 +165,7 @@ func (pn *PagerdutyNotifier) Notify(ctx context.Context, as ...*types.Alert) (bo } pn.log.Info("notifying Pagerduty", "event_type", eventType) - cmd := &models.SendWebhookSync{ + cmd := &SendWebhookSettings{ Url: PagerdutyEventAPIURL, Body: string(body), HttpMethod: "POST", @@ -174,7 +173,7 @@ func (pn *PagerdutyNotifier) Notify(ctx context.Context, as ...*types.Alert) (bo "Content-Type": "application/json", }, } - if err := pn.ns.SendWebhookSync(ctx, cmd); err != nil { + if err := pn.ns.SendWebhook(ctx, cmd); err != nil { return false, fmt.Errorf("send notification to Pagerduty: %w", err) } diff --git a/pkg/services/ngalert/notifier/channels/pushover.go b/pkg/services/ngalert/notifier/channels/pushover.go index 06b4b924cf8..1b1819200bd 100644 --- a/pkg/services/ngalert/notifier/channels/pushover.go +++ b/pkg/services/ngalert/notifier/channels/pushover.go @@ -20,7 +20,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/notifications" ) const ( @@ -44,7 +43,7 @@ type PushoverNotifier struct { tmpl *template.Template log log.Logger images ImageStore - ns notifications.WebhookSender + ns WebhookSender settings pushoverSettings } @@ -173,14 +172,14 @@ func (pn *PushoverNotifier) Notify(ctx context.Context, as ...*types.Alert) (boo return false, err } - cmd := &models.SendWebhookSync{ + cmd := &SendWebhookSettings{ Url: PushoverEndpoint, HttpMethod: "POST", HttpHeader: headers, Body: uploadBody.String(), } - if err := pn.ns.SendWebhookSync(ctx, cmd); err != nil { + if err := pn.ns.SendWebhook(ctx, cmd); err != nil { pn.log.Error("failed to send pushover notification", "error", err, "webhook", pn.Name) return false, err } diff --git a/pkg/services/ngalert/notifier/channels/sender.go b/pkg/services/ngalert/notifier/channels/sender.go new file mode 100644 index 00000000000..c3a682cf1d0 --- /dev/null +++ b/pkg/services/ngalert/notifier/channels/sender.go @@ -0,0 +1,46 @@ +package channels + +import "context" + +type SendWebhookSettings struct { + Url string + User string + Password string + Body string + HttpMethod string + HttpHeader map[string]string + ContentType string + Validation func(body []byte, statusCode int) error +} + +// SendEmailSettings is the command for sending emails +type SendEmailSettings struct { + To []string + SingleEmail bool + Template string + Subject string + Data map[string]interface{} + Info string + ReplyTo []string + EmbeddedFiles []string + AttachedFiles []*SendEmailAttachFile +} + +// SendEmailAttachFile is a definition of the attached files without path +type SendEmailAttachFile struct { + Name string + Content []byte +} + +type WebhookSender interface { + SendWebhook(ctx context.Context, cmd *SendWebhookSettings) error +} + +type EmailSender interface { + SendEmail(ctx context.Context, cmd *SendEmailSettings) error +} + +type NotificationSender interface { + WebhookSender + EmailSender +} diff --git a/pkg/services/ngalert/notifier/channels/sensugo.go b/pkg/services/ngalert/notifier/channels/sensugo.go index ead78de5a36..ac481258015 100644 --- a/pkg/services/ngalert/notifier/channels/sensugo.go +++ b/pkg/services/ngalert/notifier/channels/sensugo.go @@ -14,14 +14,13 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/notifications" ) type SensuGoNotifier struct { *Base log log.Logger images ImageStore - ns notifications.WebhookSender + ns WebhookSender tmpl *template.Template settings sensuGoSettings } @@ -172,7 +171,7 @@ func (sn *SensuGoNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool return false, err } - cmd := &models.SendWebhookSync{ + cmd := &SendWebhookSettings{ Url: fmt.Sprintf("%s/api/core/v2/namespaces/%s/events", strings.TrimSuffix(sn.settings.URL, "/"), namespace), Body: string(body), HttpMethod: "POST", @@ -181,7 +180,7 @@ func (sn *SensuGoNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool "Authorization": fmt.Sprintf("Key %s", sn.settings.APIKey), }, } - if err := sn.ns.SendWebhookSync(ctx, cmd); err != nil { + if err := sn.ns.SendWebhook(ctx, cmd); err != nil { sn.log.Error("failed to send Sensu Go event", "error", err, "sensugo", sn.Name) return false, err } diff --git a/pkg/services/ngalert/notifier/channels/slack.go b/pkg/services/ngalert/notifier/channels/slack.go index df753ec6836..d2745292c93 100644 --- a/pkg/services/ngalert/notifier/channels/slack.go +++ b/pkg/services/ngalert/notifier/channels/slack.go @@ -25,7 +25,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -67,7 +66,7 @@ type SlackNotifier struct { log log.Logger tmpl *template.Template images ImageStore - webhookSender notifications.WebhookSender + webhookSender WebhookSender sendFn sendFunc settings slackSettings } diff --git a/pkg/services/ngalert/notifier/channels/teams.go b/pkg/services/ngalert/notifier/channels/teams.go index 690b9f7919a..0292254f053 100644 --- a/pkg/services/ngalert/notifier/channels/teams.go +++ b/pkg/services/ngalert/notifier/channels/teams.go @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/notifications" ) const ( @@ -253,7 +252,7 @@ type TeamsNotifier struct { *Base tmpl *template.Template log log.Logger - ns notifications.WebhookSender + ns WebhookSender images ImageStore settings teamsSettings } @@ -355,25 +354,27 @@ func (tn *TeamsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, return false, fmt.Errorf("failed to marshal JSON: %w", err) } - cmd := &models.SendWebhookSync{Url: u, Body: string(b)} + cmd := &SendWebhookSettings{Url: u, Body: string(b)} // Teams sometimes does not use status codes to show when a request has failed. Instead, the // response can contain an error message, irrespective of status code (i.e. https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?tabs=cURL#rate-limiting-for-connectors) - cmd.Validation = func(b []byte, statusCode int) error { - // The request succeeded if the response is "1" - // https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?tabs=cURL#send-messages-using-curl-and-powershell - if !bytes.Equal(b, []byte("1")) { - return errors.New(string(b)) - } - return nil - } + cmd.Validation = validateResponse - if err := tn.ns.SendWebhookSync(ctx, cmd); err != nil { + if err := tn.ns.SendWebhook(ctx, cmd); err != nil { return false, errors.Wrap(err, "send notification to Teams") } return true, nil } +func validateResponse(b []byte, statusCode int) error { + // The request succeeded if the response is "1" + // https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?tabs=cURL#send-messages-using-curl-and-powershell + if !bytes.Equal(b, []byte("1")) { + return errors.New(string(b)) + } + return nil +} + func (tn *TeamsNotifier) SendResolved() bool { return !tn.GetDisableResolveMessage() } diff --git a/pkg/services/ngalert/notifier/channels/teams_test.go b/pkg/services/ngalert/notifier/channels/teams_test.go index b704fbb70f3..70fc238bcc6 100644 --- a/pkg/services/ngalert/notifier/channels/teams_test.go +++ b/pkg/services/ngalert/notifier/channels/teams_test.go @@ -3,11 +3,8 @@ package channels import ( "context" "encoding/json" - "errors" - "io" - "net/http" + "math/rand" "net/url" - "strings" "testing" "github.com/prometheus/alertmanager/notify" @@ -16,7 +13,6 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/services/notifications" ) func TestTeamsNotifier(t *testing.T) { @@ -30,7 +26,6 @@ func TestTeamsNotifier(t *testing.T) { name string settings string alerts []*types.Alert - response *mockResponse expMsg map[string]interface{} expInitError string expMsgError error @@ -250,23 +245,6 @@ func TestTeamsNotifier(t *testing.T) { name: "Error in initing", settings: `{}`, expInitError: `could not find url property in settings`, - }, { - name: "webhook returns error message in body with 200", - settings: `{"url": "http://localhost"}`, - alerts: []*types.Alert{ - { - Alert: model.Alert{ - Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, - Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh"}, - }, - }, - }, - response: &mockResponse{ - status: 200, - body: "some error message", - error: nil, - }, - expMsgError: errors.New("send notification to Teams: webhook failed validation: some error message"), }} for _, c := range cases { @@ -280,14 +258,7 @@ func TestTeamsNotifier(t *testing.T) { Settings: settingsJSON, } - webhookSender := CreateNotificationService(t) - - originalClient := notifications.NetClient - defer func() { - notifications.SetWebhookClient(*originalClient) - }() - clientStub := newMockClient(c.response) - notifications.SetWebhookClient(clientStub) + webhookSender := mockNotificationService() fc := FactoryConfig{ Config: m, @@ -317,54 +288,24 @@ func TestTeamsNotifier(t *testing.T) { require.True(t, ok) require.NoError(t, err) - require.NotEmpty(t, clientStub.lastRequest.URL.String()) + require.NotNil(t, webhookSender.Webhook) + lastRequest := webhookSender.Webhook + + require.NotEmpty(t, lastRequest.Url) expBody, err := json.Marshal(c.expMsg) require.NoError(t, err) - body, err := io.ReadAll(clientStub.lastRequest.Body) - require.NoError(t, err) - require.JSONEq(t, string(expBody), string(body)) + require.JSONEq(t, string(expBody), lastRequest.Body) + + require.NotNil(t, lastRequest.Validation) }) } } -type mockClient struct { - response mockResponse - lastRequest *http.Request -} - -type mockResponse struct { - status int - body string - error error -} - -func (c *mockClient) Do(req *http.Request) (*http.Response, error) { - // Do Nothing - c.lastRequest = req - return makeResponse(c.response.status, c.response.body), c.response.error -} - -func newMockClient(resp *mockResponse) *mockClient { - client := &mockClient{} - - if resp != nil { - client.response = *resp - } else { - client.response = mockResponse{ - status: 200, - body: "1", - error: nil, - } - } - - return client -} - -func makeResponse(status int, body string) *http.Response { - return &http.Response{ - StatusCode: status, - Body: io.NopCloser(strings.NewReader(body)), - } +func Test_ValidateResponse(t *testing.T) { + require.NoError(t, validateResponse([]byte("1"), rand.Int())) + err := validateResponse([]byte("some error message"), rand.Int()) + require.Error(t, err) + require.Equal(t, "some error message", err.Error()) } diff --git a/pkg/services/ngalert/notifier/channels/telegram.go b/pkg/services/ngalert/notifier/channels/telegram.go index 8afc091ebae..b9c78dd01db 100644 --- a/pkg/services/ngalert/notifier/channels/telegram.go +++ b/pkg/services/ngalert/notifier/channels/telegram.go @@ -17,7 +17,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/notifications" ) var ( @@ -38,7 +37,7 @@ type TelegramNotifier struct { *Base log log.Logger images ImageStore - ns notifications.WebhookSender + ns WebhookSender tmpl *template.Template settings telegramSettings } @@ -140,7 +139,7 @@ func (tn *TelegramNotifier) Notify(ctx context.Context, as ...*types.Alert) (boo if err != nil { return false, fmt.Errorf("failed to create telegram message: %w", err) } - if err := tn.ns.SendWebhookSync(ctx, cmd); err != nil { + if err := tn.ns.SendWebhook(ctx, cmd); err != nil { return false, fmt.Errorf("failed to send telegram message: %w", err) } @@ -168,7 +167,7 @@ func (tn *TelegramNotifier) Notify(ctx context.Context, as ...*types.Alert) (boo if err != nil { return fmt.Errorf("failed to create image: %w", err) } - if err := tn.ns.SendWebhookSync(ctx, cmd); err != nil { + if err := tn.ns.SendWebhook(ctx, cmd); err != nil { return fmt.Errorf("failed to upload image to telegram: %w", err) } return nil @@ -207,7 +206,7 @@ func (tn *TelegramNotifier) buildTelegramMessage(ctx context.Context, as []*type return m, nil } -func (tn *TelegramNotifier) newWebhookSyncCmd(action string, fn func(writer *multipart.Writer) error) (*models.SendWebhookSync, error) { +func (tn *TelegramNotifier) newWebhookSyncCmd(action string, fn func(writer *multipart.Writer) error) (*SendWebhookSettings, error) { b := bytes.Buffer{} w := multipart.NewWriter(&b) @@ -234,7 +233,7 @@ func (tn *TelegramNotifier) newWebhookSyncCmd(action string, fn func(writer *mul return nil, fmt.Errorf("failed to close multipart: %w", err) } - cmd := &models.SendWebhookSync{ + cmd := &SendWebhookSettings{ Url: fmt.Sprintf(TelegramAPIURL, tn.settings.BotToken, action), Body: b.String(), HttpMethod: "POST", diff --git a/pkg/services/ngalert/notifier/channels/testing.go b/pkg/services/ngalert/notifier/channels/testing.go index 4b88ad99ce4..2a113a11a46 100644 --- a/pkg/services/ngalert/notifier/channels/testing.go +++ b/pkg/services/ngalert/notifier/channels/testing.go @@ -129,28 +129,50 @@ func resetTimeNow() { } type notificationServiceMock struct { - Webhook models.SendWebhookSync - EmailSync models.SendEmailCommandSync - Emailx models.SendEmailCommand + Webhook SendWebhookSettings + EmailSync SendEmailSettings ShouldError error } -func (ns *notificationServiceMock) SendWebhookSync(ctx context.Context, cmd *models.SendWebhookSync) error { +func (ns *notificationServiceMock) SendWebhook(ctx context.Context, cmd *SendWebhookSettings) error { ns.Webhook = *cmd return ns.ShouldError } -func (ns *notificationServiceMock) SendEmailCommandHandlerSync(ctx context.Context, cmd *models.SendEmailCommandSync) error { +func (ns *notificationServiceMock) SendEmail(ctx context.Context, cmd *SendEmailSettings) error { ns.EmailSync = *cmd return ns.ShouldError } -func (ns *notificationServiceMock) SendEmailCommandHandler(ctx context.Context, cmd *models.SendEmailCommand) error { - ns.Emailx = *cmd - return ns.ShouldError -} func mockNotificationService() *notificationServiceMock { return ¬ificationServiceMock{} } -func CreateNotificationService(t *testing.T) *notifications.NotificationService { +type emailSender struct { + ns *notifications.NotificationService +} + +func (e emailSender) SendEmail(ctx context.Context, cmd *SendEmailSettings) error { + attached := make([]*models.SendEmailAttachFile, 0, len(cmd.AttachedFiles)) + for _, file := range cmd.AttachedFiles { + attached = append(attached, &models.SendEmailAttachFile{ + Name: file.Name, + Content: file.Content, + }) + } + return e.ns.SendEmailCommandHandlerSync(ctx, &models.SendEmailCommandSync{ + SendEmailCommand: models.SendEmailCommand{ + To: cmd.To, + SingleEmail: cmd.SingleEmail, + Template: cmd.Template, + Subject: cmd.Subject, + Data: cmd.Data, + Info: cmd.Info, + ReplyTo: cmd.ReplyTo, + EmbeddedFiles: cmd.EmbeddedFiles, + AttachedFiles: attached, + }, + }) +} + +func createEmailSender(t *testing.T) *emailSender { t.Helper() tracer := tracing.InitializeTracerForTest() @@ -170,5 +192,5 @@ func CreateNotificationService(t *testing.T) *notifications.NotificationService ns, err := notifications.ProvideService(bus, cfg, mailer, nil) require.NoError(t, err) - return ns + return &emailSender{ns: ns} } diff --git a/pkg/services/ngalert/notifier/channels/threema.go b/pkg/services/ngalert/notifier/channels/threema.go index 4011f18b4e5..0a96bd7c75d 100644 --- a/pkg/services/ngalert/notifier/channels/threema.go +++ b/pkg/services/ngalert/notifier/channels/threema.go @@ -15,7 +15,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/notifications" ) var ( @@ -28,7 +27,7 @@ type ThreemaNotifier struct { *Base log log.Logger images ImageStore - ns notifications.WebhookSender + ns WebhookSender tmpl *template.Template settings threemaSettings } @@ -123,7 +122,7 @@ func (tn *ThreemaNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool data.Set("secret", tn.settings.APISecret) data.Set("text", tn.buildMessage(ctx, as...)) - cmd := &models.SendWebhookSync{ + cmd := &SendWebhookSettings{ Url: ThreemaGwBaseURL, Body: data.Encode(), HttpMethod: "POST", @@ -131,7 +130,7 @@ func (tn *ThreemaNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool "Content-Type": "application/x-www-form-urlencoded", }, } - if err := tn.ns.SendWebhookSync(ctx, cmd); err != nil { + if err := tn.ns.SendWebhook(ctx, cmd); err != nil { tn.log.Error("Failed to send threema notification", "error", err, "webhook", tn.Name) return false, err } diff --git a/pkg/services/ngalert/notifier/channels/victorops.go b/pkg/services/ngalert/notifier/channels/victorops.go index d8c11899ade..cbcb4e78711 100644 --- a/pkg/services/ngalert/notifier/channels/victorops.go +++ b/pkg/services/ngalert/notifier/channels/victorops.go @@ -16,7 +16,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -100,7 +99,7 @@ type VictoropsNotifier struct { *Base log log.Logger images ImageStore - ns notifications.WebhookSender + ns WebhookSender tmpl *template.Template settings victorOpsSettings } @@ -161,12 +160,12 @@ func (vn *VictoropsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bo if err != nil { return false, err } - cmd := &models.SendWebhookSync{ + cmd := &SendWebhookSettings{ Url: u, Body: string(b), } - if err := vn.ns.SendWebhookSync(ctx, cmd); err != nil { + if err := vn.ns.SendWebhook(ctx, cmd); err != nil { vn.log.Error("failed to send notification", "error", err, "webhook", vn.Name) return false, err } diff --git a/pkg/services/ngalert/notifier/channels/webex.go b/pkg/services/ngalert/notifier/channels/webex.go index bc33301c71f..67851bc4b74 100644 --- a/pkg/services/ngalert/notifier/channels/webex.go +++ b/pkg/services/ngalert/notifier/channels/webex.go @@ -13,7 +13,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/notifications" ) const webexAPIURL = "https://webexapis.com/v1/messages" @@ -21,7 +20,7 @@ const webexAPIURL = "https://webexapis.com/v1/messages" // WebexNotifier is responsible for sending alert notifications as webex messages. type WebexNotifier struct { *Base - ns notifications.WebhookSender + ns WebhookSender log log.Logger images ImageStore tmpl *template.Template @@ -152,7 +151,7 @@ func (wn *WebexNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, return false, tmplErr } - cmd := &models.SendWebhookSync{ + cmd := &SendWebhookSettings{ Url: parsedURL, Body: string(body), HttpMethod: http.MethodPost, @@ -164,7 +163,7 @@ func (wn *WebexNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, cmd.HttpHeader = headers } - if err := wn.ns.SendWebhookSync(ctx, cmd); err != nil { + if err := wn.ns.SendWebhook(ctx, cmd); err != nil { return false, err } diff --git a/pkg/services/ngalert/notifier/channels/webhook.go b/pkg/services/ngalert/notifier/channels/webhook.go index e1e7cc0d0d4..65d1d32103e 100644 --- a/pkg/services/ngalert/notifier/channels/webhook.go +++ b/pkg/services/ngalert/notifier/channels/webhook.go @@ -16,7 +16,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/notifications" ) // WebhookNotifier is responsible for sending @@ -24,7 +23,7 @@ import ( type WebhookNotifier struct { *Base log log.Logger - ns notifications.WebhookSender + ns WebhookSender images ImageStore tmpl *template.Template orgID int64 @@ -204,7 +203,7 @@ func (wn *WebhookNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool return false, tmplErr } - cmd := &models.SendWebhookSync{ + cmd := &SendWebhookSettings{ Url: parsedURL, User: wn.settings.User, Password: wn.settings.Password, @@ -213,7 +212,7 @@ func (wn *WebhookNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool HttpHeader: headers, } - if err := wn.ns.SendWebhookSync(ctx, cmd); err != nil { + if err := wn.ns.SendWebhook(ctx, cmd); err != nil { return false, err } diff --git a/pkg/services/ngalert/notifier/channels/wecom.go b/pkg/services/ngalert/notifier/channels/wecom.go index af86af83379..c841d2cc64e 100644 --- a/pkg/services/ngalert/notifier/channels/wecom.go +++ b/pkg/services/ngalert/notifier/channels/wecom.go @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/notifications" ) var weComEndpoint = "https://qyapi.weixin.qq.com" @@ -130,7 +129,7 @@ type WeComNotifier struct { *Base tmpl *template.Template log log.Logger - ns notifications.WebhookSender + ns WebhookSender settings wecomSettings tok *WeComAccessToken tokExpireAt time.Time @@ -183,12 +182,12 @@ func (w *WeComNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, e w.log.Warn("failed to template WeCom message", "error", tmplErr.Error()) } - cmd := &models.SendWebhookSync{ + cmd := &SendWebhookSettings{ Url: url, Body: string(body), } - if err = w.ns.SendWebhookSync(ctx, cmd); err != nil { + if err = w.ns.SendWebhook(ctx, cmd); err != nil { w.log.Error("failed to send WeCom webhook", "error", err, "notification", w.Name) return false, err } diff --git a/pkg/services/ngalert/notifier/sender.go b/pkg/services/ngalert/notifier/sender.go new file mode 100644 index 00000000000..976ccc4cbe7 --- /dev/null +++ b/pkg/services/ngalert/notifier/sender.go @@ -0,0 +1,56 @@ +package notifier + +import ( + "context" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/ngalert/notifier/channels" + "github.com/grafana/grafana/pkg/services/notifications" +) + +type sender struct { + ns notifications.Service +} + +func (s sender) SendWebhook(ctx context.Context, cmd *channels.SendWebhookSettings) error { + return s.ns.SendWebhookSync(ctx, &models.SendWebhookSync{ + Url: cmd.Url, + User: cmd.User, + Password: cmd.Password, + Body: cmd.Body, + HttpMethod: cmd.HttpMethod, + HttpHeader: cmd.HttpHeader, + ContentType: cmd.ContentType, + Validation: cmd.Validation, + }) +} + +func (s sender) SendEmail(ctx context.Context, cmd *channels.SendEmailSettings) error { + var attached []*models.SendEmailAttachFile + if cmd.AttachedFiles != nil { + attached = make([]*models.SendEmailAttachFile, 0, len(cmd.AttachedFiles)) + for _, file := range cmd.AttachedFiles { + attached = append(attached, &models.SendEmailAttachFile{ + Name: file.Name, + Content: file.Content, + }) + } + } + return s.ns.SendEmailCommandHandlerSync(ctx, &models.SendEmailCommandSync{ + SendEmailCommand: models.SendEmailCommand{ + To: cmd.To, + SingleEmail: cmd.SingleEmail, + Template: cmd.Template, + Subject: cmd.Subject, + Data: cmd.Data, + Info: cmd.Info, + ReplyTo: cmd.ReplyTo, + EmbeddedFiles: cmd.EmbeddedFiles, + AttachedFiles: attached, + }, + }) +} + +func NewNotificationSender(ns notifications.Service) channels.NotificationSender { + return &sender{ns: ns} +}