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
This commit is contained in:
Yuri Tseretyan 2022-12-14 10:59:37 -05:00 committed by GitHub
parent 07b5043222
commit 7c3ab4a715
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 238 additions and 192 deletions

View File

@ -514,7 +514,7 @@ func (am *Alertmanager) buildReceiverIntegration(r *apimodels.PostableGrafanaRec
SecureSettings: secureSettings, 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 { if err != nil {
return nil, InvalidReceiverError{ return nil, InvalidReceiverError{
Receiver: r, Receiver: r,

View File

@ -12,7 +12,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/notifications"
) )
const defaultDingdingMsgType = "link" const defaultDingdingMsgType = "link"
@ -73,7 +72,7 @@ func newDingDingNotifier(fc FactoryConfig) (*DingDingNotifier, error) {
type DingDingNotifier struct { type DingDingNotifier struct {
*Base *Base
log log.Logger log log.Logger
ns notifications.WebhookSender ns WebhookSender
tmpl *template.Template tmpl *template.Template
settings dingDingSettings settings dingDingSettings
} }
@ -107,9 +106,9 @@ func (dd *DingDingNotifier) Notify(ctx context.Context, as ...*types.Alert) (boo
u = dd.settings.URL 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) return false, fmt.Errorf("send notification to dingding: %w", err)
} }

View File

@ -20,14 +20,13 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
type DiscordNotifier struct { type DiscordNotifier struct {
*Base *Base
log log.Logger log log.Logger
ns notifications.WebhookSender ns WebhookSender
images ImageStore images ImageStore
tmpl *template.Template tmpl *template.Template
settings discordSettings settings discordSettings
@ -179,7 +178,7 @@ func (d DiscordNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
return false, err 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) d.log.Error("failed to send notification to Discord", "error", err)
return false, err return false, err
} }
@ -236,8 +235,8 @@ func (d DiscordNotifier) constructAttachments(ctx context.Context, as []*types.A
return attachments return attachments
} }
func (d DiscordNotifier) buildRequest(url string, body []byte, attachments []discordAttachment) (*models.SendWebhookSync, error) { func (d DiscordNotifier) buildRequest(url string, body []byte, attachments []discordAttachment) (*SendWebhookSettings, error) {
cmd := &models.SendWebhookSync{ cmd := &SendWebhookSettings{
Url: url, Url: url,
HttpMethod: "POST", HttpMethod: "POST",
} }

View File

@ -14,7 +14,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
@ -27,7 +26,7 @@ type EmailNotifier struct {
Message string Message string
Subject string Subject string
log log.Logger log log.Logger
ns notifications.EmailSender ns EmailSender
images ImageStore images ImageStore
tmpl *template.Template tmpl *template.Template
} }
@ -69,7 +68,7 @@ func NewEmailConfig(config *NotificationChannelConfig) (*EmailConfig, error) {
// NewEmailNotifier is the constructor function // NewEmailNotifier is the constructor function
// for the EmailNotifier. // 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{ return &EmailNotifier{
Base: NewBase(&models.AlertNotification{ Base: NewBase(&models.AlertNotification{
Uid: config.UID, Uid: config.UID,
@ -126,33 +125,31 @@ func (en *EmailNotifier) Notify(ctx context.Context, alerts ...*types.Alert) (bo
return nil return nil
}, alerts...) }, alerts...)
cmd := &models.SendEmailCommandSync{ cmd := &SendEmailSettings{
SendEmailCommand: models.SendEmailCommand{ Subject: subject,
Subject: subject, Data: map[string]interface{}{
Data: map[string]interface{}{ "Title": subject,
"Title": subject, "Message": tmpl(en.Message),
"Message": tmpl(en.Message), "Status": data.Status,
"Status": data.Status, "Alerts": data.Alerts,
"Alerts": data.Alerts, "GroupLabels": data.GroupLabels,
"GroupLabels": data.GroupLabels, "CommonLabels": data.CommonLabels,
"CommonLabels": data.CommonLabels, "CommonAnnotations": data.CommonAnnotations,
"CommonAnnotations": data.CommonAnnotations, "ExternalURL": data.ExternalURL,
"ExternalURL": data.ExternalURL, "RuleUrl": ruleURL,
"RuleUrl": ruleURL, "AlertPageUrl": alertPageURL,
"AlertPageUrl": alertPageURL,
},
EmbeddedFiles: embeddedFiles,
To: en.Addresses,
SingleEmail: en.SingleEmail,
Template: "ng_alert_notification",
}, },
EmbeddedFiles: embeddedFiles,
To: en.Addresses,
SingleEmail: en.SingleEmail,
Template: "ng_alert_notification",
} }
if tmplErr != nil { if tmplErr != nil {
en.log.Warn("failed to template email message", "error", tmplErr.Error()) 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 return false, err
} }

View File

@ -104,7 +104,7 @@ func TestEmailNotifier(t *testing.T) {
} }
func TestEmailNotifierIntegration(t *testing.T) { func TestEmailNotifierIntegration(t *testing.T) {
ns := CreateNotificationService(t) ns := createEmailSender(t)
emailTmpl := templateForTests(t) emailTmpl := templateForTests(t)
externalURL, err := url.Parse("http://localhost/base") 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() t.Helper()
json := `{ json := `{
@ -295,10 +295,10 @@ func createSut(t *testing.T, messageTmpl string, subjectTmpl string, emailTmpl *
return emailNotifier return emailNotifier
} }
func getSingleSentMessage(t *testing.T, ns *notifications.NotificationService) *notifications.Message { func getSingleSentMessage(t *testing.T, ns *emailSender) *notifications.Message {
t.Helper() t.Helper()
mailer := ns.GetMailer().(*notifications.FakeMailer) mailer := ns.ns.GetMailer().(*notifications.FakeMailer)
require.Len(t, mailer.Sent, 1) require.Len(t, mailer.Sent, 1)
sent := mailer.Sent[0] sent := mailer.Sent[0]
mailer.Sent = []*notifications.Message{} mailer.Sent = []*notifications.Message{}

View File

@ -8,12 +8,11 @@ import (
"github.com/prometheus/alertmanager/template" "github.com/prometheus/alertmanager/template"
"github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/notifications"
) )
type FactoryConfig struct { type FactoryConfig struct {
Config *NotificationChannelConfig Config *NotificationChannelConfig
NotificationService notifications.Service NotificationService NotificationSender
DecryptFunc GetDecryptedValueFn DecryptFunc GetDecryptedValueFn
ImageStore ImageStore ImageStore ImageStore
// Used to retrieve image URLs for messages, or data for uploads. // 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) 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) { decryptFunc GetDecryptedValueFn, template *template.Template, imageStore ImageStore) (FactoryConfig, error) {
if config.Settings == nil { if config.Settings == nil {
return FactoryConfig{}, errors.New("no settings supplied") return FactoryConfig{}, errors.New("no settings supplied")

View File

@ -14,7 +14,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
@ -23,7 +22,7 @@ import (
type GoogleChatNotifier struct { type GoogleChatNotifier struct {
*Base *Base
log log.Logger log log.Logger
ns notifications.WebhookSender ns WebhookSender
images ImageStore images ImageStore
tmpl *template.Template tmpl *template.Template
settings googleChatSettings settings googleChatSettings
@ -160,7 +159,7 @@ func (gcn *GoogleChatNotifier) Notify(ctx context.Context, as ...*types.Alert) (
return false, fmt.Errorf("marshal json: %w", err) return false, fmt.Errorf("marshal json: %w", err)
} }
cmd := &models.SendWebhookSync{ cmd := &SendWebhookSettings{
Url: u, Url: u,
HttpMethod: "POST", HttpMethod: "POST",
HttpHeader: map[string]string{ HttpHeader: map[string]string{
@ -169,7 +168,7 @@ func (gcn *GoogleChatNotifier) Notify(ctx context.Context, as ...*types.Alert) (
Body: string(body), 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) gcn.log.Error("Failed to send Google Hangouts Chat alert", "error", err, "webhook", gcn.Name)
return false, err return false, err
} }

View File

@ -14,7 +14,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/notifications"
) )
// KafkaNotifier is responsible for sending // KafkaNotifier is responsible for sending
@ -23,7 +22,7 @@ type KafkaNotifier struct {
*Base *Base
log log.Logger log log.Logger
images ImageStore images ImageStore
ns notifications.WebhookSender ns WebhookSender
tmpl *template.Template tmpl *template.Template
settings kafkaSettings 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()) kn.log.Warn("failed to template Kafka message", "error", tmplErr.Error())
} }
cmd := &models.SendWebhookSync{ cmd := &SendWebhookSettings{
Url: topicURL, Url: topicURL,
Body: body, Body: body,
HttpMethod: "POST", 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) kn.log.Error("Failed to send notification to Kafka", "error", err, "body", body)
return false, err return false, err
} }

View File

@ -12,7 +12,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/notifications"
) )
var ( var (
@ -24,7 +23,7 @@ var (
type LineNotifier struct { type LineNotifier struct {
*Base *Base
log log.Logger log log.Logger
ns notifications.WebhookSender ns WebhookSender
tmpl *template.Template tmpl *template.Template
settings lineSettings settings lineSettings
} }
@ -79,7 +78,7 @@ func (ln *LineNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, e
form := url.Values{} form := url.Values{}
form.Add("message", body) form.Add("message", body)
cmd := &models.SendWebhookSync{ cmd := &SendWebhookSettings{
Url: LineNotifyURL, Url: LineNotifyURL,
HttpMethod: "POST", HttpMethod: "POST",
HttpHeader: map[string]string{ HttpHeader: map[string]string{
@ -89,7 +88,7 @@ func (ln *LineNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, e
Body: form.Encode(), 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) ln.log.Error("failed to send notification to LINE", "error", err, "body", body)
return false, err return false, err
} }

View File

@ -18,7 +18,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/notifications"
) )
const ( const (
@ -39,7 +38,7 @@ type OpsgenieNotifier struct {
*Base *Base
tmpl *template.Template tmpl *template.Template
log log.Logger log log.Logger
ns notifications.WebhookSender ns WebhookSender
images ImageStore images ImageStore
settings *opsgenieSettings settings *opsgenieSettings
} }
@ -163,7 +162,7 @@ func (on *OpsgenieNotifier) Notify(ctx context.Context, as ...*types.Alert) (boo
return true, nil return true, nil
} }
cmd := &models.SendWebhookSync{ cmd := &SendWebhookSettings{
Url: url, Url: url,
Body: string(body), Body: string(body),
HttpMethod: http.MethodPost, 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) return false, fmt.Errorf("send notification to Opsgenie: %w", err)
} }

View File

@ -16,7 +16,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/notifications"
) )
const ( const (
@ -45,7 +44,7 @@ type PagerdutyNotifier struct {
*Base *Base
tmpl *template.Template tmpl *template.Template
log log.Logger log log.Logger
ns notifications.WebhookSender ns WebhookSender
images ImageStore images ImageStore
settings *pagerdutySettings 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) pn.log.Info("notifying Pagerduty", "event_type", eventType)
cmd := &models.SendWebhookSync{ cmd := &SendWebhookSettings{
Url: PagerdutyEventAPIURL, Url: PagerdutyEventAPIURL,
Body: string(body), Body: string(body),
HttpMethod: "POST", HttpMethod: "POST",
@ -174,7 +173,7 @@ func (pn *PagerdutyNotifier) Notify(ctx context.Context, as ...*types.Alert) (bo
"Content-Type": "application/json", "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) return false, fmt.Errorf("send notification to Pagerduty: %w", err)
} }

View File

@ -20,7 +20,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/notifications"
) )
const ( const (
@ -44,7 +43,7 @@ type PushoverNotifier struct {
tmpl *template.Template tmpl *template.Template
log log.Logger log log.Logger
images ImageStore images ImageStore
ns notifications.WebhookSender ns WebhookSender
settings pushoverSettings settings pushoverSettings
} }
@ -173,14 +172,14 @@ func (pn *PushoverNotifier) Notify(ctx context.Context, as ...*types.Alert) (boo
return false, err return false, err
} }
cmd := &models.SendWebhookSync{ cmd := &SendWebhookSettings{
Url: PushoverEndpoint, Url: PushoverEndpoint,
HttpMethod: "POST", HttpMethod: "POST",
HttpHeader: headers, HttpHeader: headers,
Body: uploadBody.String(), 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) pn.log.Error("failed to send pushover notification", "error", err, "webhook", pn.Name)
return false, err return false, err
} }

View File

@ -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
}

View File

@ -14,14 +14,13 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/notifications"
) )
type SensuGoNotifier struct { type SensuGoNotifier struct {
*Base *Base
log log.Logger log log.Logger
images ImageStore images ImageStore
ns notifications.WebhookSender ns WebhookSender
tmpl *template.Template tmpl *template.Template
settings sensuGoSettings settings sensuGoSettings
} }
@ -172,7 +171,7 @@ func (sn *SensuGoNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool
return false, err return false, err
} }
cmd := &models.SendWebhookSync{ cmd := &SendWebhookSettings{
Url: fmt.Sprintf("%s/api/core/v2/namespaces/%s/events", strings.TrimSuffix(sn.settings.URL, "/"), namespace), Url: fmt.Sprintf("%s/api/core/v2/namespaces/%s/events", strings.TrimSuffix(sn.settings.URL, "/"), namespace),
Body: string(body), Body: string(body),
HttpMethod: "POST", 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), "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) sn.log.Error("failed to send Sensu Go event", "error", err, "sensugo", sn.Name)
return false, err return false, err
} }

View File

@ -25,7 +25,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
@ -67,7 +66,7 @@ type SlackNotifier struct {
log log.Logger log log.Logger
tmpl *template.Template tmpl *template.Template
images ImageStore images ImageStore
webhookSender notifications.WebhookSender webhookSender WebhookSender
sendFn sendFunc sendFn sendFunc
settings slackSettings settings slackSettings
} }

View File

@ -14,7 +14,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/notifications"
) )
const ( const (
@ -253,7 +252,7 @@ type TeamsNotifier struct {
*Base *Base
tmpl *template.Template tmpl *template.Template
log log.Logger log log.Logger
ns notifications.WebhookSender ns WebhookSender
images ImageStore images ImageStore
settings teamsSettings 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) 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 // 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) // 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 { cmd.Validation = validateResponse
// 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
}
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 false, errors.Wrap(err, "send notification to Teams")
} }
return true, nil 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 { func (tn *TeamsNotifier) SendResolved() bool {
return !tn.GetDisableResolveMessage() return !tn.GetDisableResolveMessage()
} }

View File

@ -3,11 +3,8 @@ package channels
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "math/rand"
"io"
"net/http"
"net/url" "net/url"
"strings"
"testing" "testing"
"github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/notify"
@ -16,7 +13,6 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/notifications"
) )
func TestTeamsNotifier(t *testing.T) { func TestTeamsNotifier(t *testing.T) {
@ -30,7 +26,6 @@ func TestTeamsNotifier(t *testing.T) {
name string name string
settings string settings string
alerts []*types.Alert alerts []*types.Alert
response *mockResponse
expMsg map[string]interface{} expMsg map[string]interface{}
expInitError string expInitError string
expMsgError error expMsgError error
@ -250,23 +245,6 @@ func TestTeamsNotifier(t *testing.T) {
name: "Error in initing", name: "Error in initing",
settings: `{}`, settings: `{}`,
expInitError: `could not find url property in 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 { for _, c := range cases {
@ -280,14 +258,7 @@ func TestTeamsNotifier(t *testing.T) {
Settings: settingsJSON, Settings: settingsJSON,
} }
webhookSender := CreateNotificationService(t) webhookSender := mockNotificationService()
originalClient := notifications.NetClient
defer func() {
notifications.SetWebhookClient(*originalClient)
}()
clientStub := newMockClient(c.response)
notifications.SetWebhookClient(clientStub)
fc := FactoryConfig{ fc := FactoryConfig{
Config: m, Config: m,
@ -317,54 +288,24 @@ func TestTeamsNotifier(t *testing.T) {
require.True(t, ok) require.True(t, ok)
require.NoError(t, err) 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) expBody, err := json.Marshal(c.expMsg)
require.NoError(t, err) require.NoError(t, err)
body, err := io.ReadAll(clientStub.lastRequest.Body) require.JSONEq(t, string(expBody), lastRequest.Body)
require.NoError(t, err)
require.JSONEq(t, string(expBody), string(body)) require.NotNil(t, lastRequest.Validation)
}) })
} }
} }
type mockClient struct { func Test_ValidateResponse(t *testing.T) {
response mockResponse require.NoError(t, validateResponse([]byte("1"), rand.Int()))
lastRequest *http.Request err := validateResponse([]byte("some error message"), rand.Int())
} require.Error(t, err)
require.Equal(t, "some error message", err.Error())
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)),
}
} }

View File

@ -17,7 +17,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/notifications"
) )
var ( var (
@ -38,7 +37,7 @@ type TelegramNotifier struct {
*Base *Base
log log.Logger log log.Logger
images ImageStore images ImageStore
ns notifications.WebhookSender ns WebhookSender
tmpl *template.Template tmpl *template.Template
settings telegramSettings settings telegramSettings
} }
@ -140,7 +139,7 @@ func (tn *TelegramNotifier) Notify(ctx context.Context, as ...*types.Alert) (boo
if err != nil { if err != nil {
return false, fmt.Errorf("failed to create telegram message: %w", err) 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) 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 { if err != nil {
return fmt.Errorf("failed to create image: %w", err) 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 fmt.Errorf("failed to upload image to telegram: %w", err)
} }
return nil return nil
@ -207,7 +206,7 @@ func (tn *TelegramNotifier) buildTelegramMessage(ctx context.Context, as []*type
return m, nil 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{} b := bytes.Buffer{}
w := multipart.NewWriter(&b) 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) return nil, fmt.Errorf("failed to close multipart: %w", err)
} }
cmd := &models.SendWebhookSync{ cmd := &SendWebhookSettings{
Url: fmt.Sprintf(TelegramAPIURL, tn.settings.BotToken, action), Url: fmt.Sprintf(TelegramAPIURL, tn.settings.BotToken, action),
Body: b.String(), Body: b.String(),
HttpMethod: "POST", HttpMethod: "POST",

View File

@ -129,28 +129,50 @@ func resetTimeNow() {
} }
type notificationServiceMock struct { type notificationServiceMock struct {
Webhook models.SendWebhookSync Webhook SendWebhookSettings
EmailSync models.SendEmailCommandSync EmailSync SendEmailSettings
Emailx models.SendEmailCommand
ShouldError error 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 ns.Webhook = *cmd
return ns.ShouldError 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 ns.EmailSync = *cmd
return ns.ShouldError return ns.ShouldError
} }
func (ns *notificationServiceMock) SendEmailCommandHandler(ctx context.Context, cmd *models.SendEmailCommand) error {
ns.Emailx = *cmd
return ns.ShouldError
}
func mockNotificationService() *notificationServiceMock { return &notificationServiceMock{} } func mockNotificationService() *notificationServiceMock { return &notificationServiceMock{} }
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() t.Helper()
tracer := tracing.InitializeTracerForTest() tracer := tracing.InitializeTracerForTest()
@ -170,5 +192,5 @@ func CreateNotificationService(t *testing.T) *notifications.NotificationService
ns, err := notifications.ProvideService(bus, cfg, mailer, nil) ns, err := notifications.ProvideService(bus, cfg, mailer, nil)
require.NoError(t, err) require.NoError(t, err)
return ns return &emailSender{ns: ns}
} }

View File

@ -15,7 +15,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/notifications"
) )
var ( var (
@ -28,7 +27,7 @@ type ThreemaNotifier struct {
*Base *Base
log log.Logger log log.Logger
images ImageStore images ImageStore
ns notifications.WebhookSender ns WebhookSender
tmpl *template.Template tmpl *template.Template
settings threemaSettings 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("secret", tn.settings.APISecret)
data.Set("text", tn.buildMessage(ctx, as...)) data.Set("text", tn.buildMessage(ctx, as...))
cmd := &models.SendWebhookSync{ cmd := &SendWebhookSettings{
Url: ThreemaGwBaseURL, Url: ThreemaGwBaseURL,
Body: data.Encode(), Body: data.Encode(),
HttpMethod: "POST", HttpMethod: "POST",
@ -131,7 +130,7 @@ func (tn *ThreemaNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool
"Content-Type": "application/x-www-form-urlencoded", "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) tn.log.Error("Failed to send threema notification", "error", err, "webhook", tn.Name)
return false, err return false, err
} }

View File

@ -16,7 +16,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
@ -100,7 +99,7 @@ type VictoropsNotifier struct {
*Base *Base
log log.Logger log log.Logger
images ImageStore images ImageStore
ns notifications.WebhookSender ns WebhookSender
tmpl *template.Template tmpl *template.Template
settings victorOpsSettings settings victorOpsSettings
} }
@ -161,12 +160,12 @@ func (vn *VictoropsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bo
if err != nil { if err != nil {
return false, err return false, err
} }
cmd := &models.SendWebhookSync{ cmd := &SendWebhookSettings{
Url: u, Url: u,
Body: string(b), 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) vn.log.Error("failed to send notification", "error", err, "webhook", vn.Name)
return false, err return false, err
} }

View File

@ -13,7 +13,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/notifications"
) )
const webexAPIURL = "https://webexapis.com/v1/messages" 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. // WebexNotifier is responsible for sending alert notifications as webex messages.
type WebexNotifier struct { type WebexNotifier struct {
*Base *Base
ns notifications.WebhookSender ns WebhookSender
log log.Logger log log.Logger
images ImageStore images ImageStore
tmpl *template.Template tmpl *template.Template
@ -152,7 +151,7 @@ func (wn *WebexNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
return false, tmplErr return false, tmplErr
} }
cmd := &models.SendWebhookSync{ cmd := &SendWebhookSettings{
Url: parsedURL, Url: parsedURL,
Body: string(body), Body: string(body),
HttpMethod: http.MethodPost, HttpMethod: http.MethodPost,
@ -164,7 +163,7 @@ func (wn *WebexNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
cmd.HttpHeader = headers cmd.HttpHeader = headers
} }
if err := wn.ns.SendWebhookSync(ctx, cmd); err != nil { if err := wn.ns.SendWebhook(ctx, cmd); err != nil {
return false, err return false, err
} }

View File

@ -16,7 +16,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/notifications"
) )
// WebhookNotifier is responsible for sending // WebhookNotifier is responsible for sending
@ -24,7 +23,7 @@ import (
type WebhookNotifier struct { type WebhookNotifier struct {
*Base *Base
log log.Logger log log.Logger
ns notifications.WebhookSender ns WebhookSender
images ImageStore images ImageStore
tmpl *template.Template tmpl *template.Template
orgID int64 orgID int64
@ -204,7 +203,7 @@ func (wn *WebhookNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool
return false, tmplErr return false, tmplErr
} }
cmd := &models.SendWebhookSync{ cmd := &SendWebhookSettings{
Url: parsedURL, Url: parsedURL,
User: wn.settings.User, User: wn.settings.User,
Password: wn.settings.Password, Password: wn.settings.Password,
@ -213,7 +212,7 @@ func (wn *WebhookNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool
HttpHeader: headers, HttpHeader: headers,
} }
if err := wn.ns.SendWebhookSync(ctx, cmd); err != nil { if err := wn.ns.SendWebhook(ctx, cmd); err != nil {
return false, err return false, err
} }

View File

@ -14,7 +14,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/notifications"
) )
var weComEndpoint = "https://qyapi.weixin.qq.com" var weComEndpoint = "https://qyapi.weixin.qq.com"
@ -130,7 +129,7 @@ type WeComNotifier struct {
*Base *Base
tmpl *template.Template tmpl *template.Template
log log.Logger log log.Logger
ns notifications.WebhookSender ns WebhookSender
settings wecomSettings settings wecomSettings
tok *WeComAccessToken tok *WeComAccessToken
tokExpireAt time.Time 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()) w.log.Warn("failed to template WeCom message", "error", tmplErr.Error())
} }
cmd := &models.SendWebhookSync{ cmd := &SendWebhookSettings{
Url: url, Url: url,
Body: string(body), 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) w.log.Error("failed to send WeCom webhook", "error", err, "notification", w.Name)
return false, err return false, err
} }

View File

@ -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}
}