From d66a5e65a42e675b33e56cc7930402283e677216 Mon Sep 17 00:00:00 2001 From: Ganesh Vernekar <15064823+codesome@users.noreply.github.com> Date: Fri, 23 Apr 2021 18:59:28 +0530 Subject: [PATCH] AlertingNG: Add webhook notification channel (#33229) Signed-off-by: Ganesh Vernekar --- pkg/services/ngalert/notifier/alertmanager.go | 23 +- .../ngalert/notifier/channels/dingding.go | 18 +- .../notifier/channels/dingding_test.go | 8 +- .../ngalert/notifier/channels/email.go | 8 +- .../ngalert/notifier/channels/email_test.go | 8 +- .../ngalert/notifier/channels/pagerduty.go | 11 +- .../notifier/channels/pagerduty_test.go | 8 +- .../ngalert/notifier/channels/slack.go | 10 +- .../ngalert/notifier/channels/slack_test.go | 8 +- .../ngalert/notifier/channels/teams.go | 24 +- .../ngalert/notifier/channels/teams_test.go | 8 +- .../ngalert/notifier/channels/telegram.go | 17 +- .../notifier/channels/telegram_test.go | 8 +- .../ngalert/notifier/channels/webhook.go | 130 ++++++++++ .../ngalert/notifier/channels/webhook_test.go | 222 ++++++++++++++++++ 15 files changed, 431 insertions(+), 80 deletions(-) create mode 100644 pkg/services/ngalert/notifier/channels/webhook.go create mode 100644 pkg/services/ngalert/notifier/channels/webhook_test.go diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index 55e76356940..84128ee402e 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -274,6 +274,11 @@ func (am *Alertmanager) applyConfig(cfg *apimodels.PostableUserConfig) error { if err != nil { return err } + externalURL, err := url.Parse(am.Settings.AppURL) + if err != nil { + return err + } + tmpl.ExternalURL = externalURL // Finally, build the integrations map using the receiver configuration and templates. integrationsMap, err := am.buildIntegrationsMap(cfg.AlertmanagerConfig.Receivers, tmpl) @@ -355,23 +360,21 @@ func (am *Alertmanager) buildReceiverIntegrations(receiver *apimodels.PostableAp n NotificationChannel err error ) - externalURL, err := url.Parse(am.Settings.AppURL) - if err != nil { - return nil, err - } switch r.Type { case "email": - n, err = channels.NewEmailNotifier(cfg, externalURL, am.Settings.AppURL) + n, err = channels.NewEmailNotifier(cfg, tmpl.ExternalURL) // Email notifier already has a default template. case "pagerduty": - n, err = channels.NewPagerdutyNotifier(cfg, tmpl, externalURL) + n, err = channels.NewPagerdutyNotifier(cfg, tmpl) case "slack": - n, err = channels.NewSlackNotifier(cfg, tmpl, externalURL) + n, err = channels.NewSlackNotifier(cfg, tmpl) case "telegram": - n, err = channels.NewTelegramNotifier(cfg, tmpl, externalURL) + n, err = channels.NewTelegramNotifier(cfg, tmpl) case "teams": - n, err = channels.NewTeamsNotifier(cfg, tmpl, externalURL) + n, err = channels.NewTeamsNotifier(cfg, tmpl) case "dingding": - n, err = channels.NewDingDingNotifier(cfg, tmpl, externalURL) + n, err = channels.NewDingDingNotifier(cfg, tmpl) + case "webhook": + n, err = channels.NewWebHookNotifier(cfg, tmpl) } if err != nil { return nil, err diff --git a/pkg/services/ngalert/notifier/channels/dingding.go b/pkg/services/ngalert/notifier/channels/dingding.go index df60d3c6f80..5a5f86df3ff 100644 --- a/pkg/services/ngalert/notifier/channels/dingding.go +++ b/pkg/services/ngalert/notifier/channels/dingding.go @@ -21,7 +21,7 @@ import ( const defaultDingdingMsgType = "link" // NewDingDingNotifier is the constructor for the Dingding notifier -func NewDingDingNotifier(model *models.AlertNotification, t *template.Template, externalUrl *url.URL) (*DingDingNotifier, error) { +func NewDingDingNotifier(model *models.AlertNotification, t *template.Template) (*DingDingNotifier, error) { if model.Settings == nil { return nil, alerting.ValidationError{Reason: "No Settings Supplied"} } @@ -40,19 +40,17 @@ func NewDingDingNotifier(model *models.AlertNotification, t *template.Template, Message: model.Settings.Get("message").MustString(`{{ template "default.message" .}}`), log: log.New("alerting.notifier.dingding"), tmpl: t, - externalUrl: externalUrl, }, nil } // DingDingNotifier is responsible for sending alert notifications to ding ding. type DingDingNotifier struct { old_notifiers.NotifierBase - MsgType string - URL string - Message string - tmpl *template.Template - log log.Logger - externalUrl *url.URL + MsgType string + URL string + Message string + tmpl *template.Template + log log.Logger } // Notify sends the alert notification to dingding. @@ -61,14 +59,14 @@ func (dd *DingDingNotifier) Notify(ctx context.Context, as ...*types.Alert) (boo q := url.Values{ "pc_slide": {"false"}, - "url": {dd.externalUrl.String()}, // TODO: should this be rule URL according to original? + "url": {dd.tmpl.ExternalURL.String()}, // TODO: should this be rule URL according to original? } // Use special link to auto open the message url outside of Dingding // Refer: https://open-doc.dingtalk.com/docs/doc.htm?treeId=385&articleId=104972&docType=1#s9 messageURL := "dingtalk://dingtalkclient/page/link?" + q.Encode() - data := notify.GetTemplateData(ctx, &template.Template{ExternalURL: dd.externalUrl}, as, gokit_log.NewNopLogger()) + data := notify.GetTemplateData(ctx, dd.tmpl, as, gokit_log.NewNopLogger()) var tmplErr error tmpl := notify.TmplText(dd.tmpl, data, &tmplErr) diff --git a/pkg/services/ngalert/notifier/channels/dingding_test.go b/pkg/services/ngalert/notifier/channels/dingding_test.go index b9ad065e537..7b6f57520de 100644 --- a/pkg/services/ngalert/notifier/channels/dingding_test.go +++ b/pkg/services/ngalert/notifier/channels/dingding_test.go @@ -23,6 +23,10 @@ func TestDingdingNotifier(t *testing.T) { tmpl, err := template.FromGlobs("templates/default.tmpl") require.NoError(t, err) + externalURL, err := url.Parse("http://localhost") + require.NoError(t, err) + tmpl.ExternalURL = externalURL + cases := []struct { name string settings string @@ -108,9 +112,7 @@ func TestDingdingNotifier(t *testing.T) { Settings: settingsJSON, } - externalURL, err := url.Parse("http://localhost") - require.NoError(t, err) - pn, err := NewDingDingNotifier(m, tmpl, externalURL) + pn, err := NewDingDingNotifier(m, tmpl) if c.expInitError != nil { require.Error(t, err) require.Equal(t, c.expInitError.Error(), err.Error()) diff --git a/pkg/services/ngalert/notifier/channels/email.go b/pkg/services/ngalert/notifier/channels/email.go index a6c36232b9c..7d4eab11487 100644 --- a/pkg/services/ngalert/notifier/channels/email.go +++ b/pkg/services/ngalert/notifier/channels/email.go @@ -26,12 +26,11 @@ type EmailNotifier struct { SingleEmail bool log log.Logger externalUrl *url.URL - appURL string } // NewEmailNotifier is the constructor function // for the EmailNotifier. -func NewEmailNotifier(model *models.AlertNotification, externalUrl *url.URL, appURL string) (*EmailNotifier, error) { +func NewEmailNotifier(model *models.AlertNotification, externalUrl *url.URL) (*EmailNotifier, error) { if model.Settings == nil { return nil, alerting.ValidationError{Reason: "No Settings Supplied"} } @@ -50,7 +49,6 @@ func NewEmailNotifier(model *models.AlertNotification, externalUrl *url.URL, app NotifierBase: old_notifiers.NewNotifierBase(model), Addresses: addresses, SingleEmail: singleEmail, - appURL: appURL, log: log.New("alerting.notifier.email"), externalUrl: externalUrl, }, nil @@ -74,8 +72,8 @@ func (en *EmailNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, "CommonLabels": data.CommonLabels, "CommonAnnotations": data.CommonAnnotations, "ExternalURL": data.ExternalURL, - "RuleUrl": path.Join(en.appURL, "/alerting/list"), - "AlertPageUrl": path.Join(en.appURL, "/alerting/list?alertState=firing&view=state"), + "RuleUrl": path.Join(en.externalUrl.String(), "/alerting/list"), + "AlertPageUrl": path.Join(en.externalUrl.String(), "/alerting/list?alertState=firing&view=state"), }, To: en.Addresses, SingleEmail: en.SingleEmail, diff --git a/pkg/services/ngalert/notifier/channels/email_test.go b/pkg/services/ngalert/notifier/channels/email_test.go index e63bcdf0d18..0847a450cea 100644 --- a/pkg/services/ngalert/notifier/channels/email_test.go +++ b/pkg/services/ngalert/notifier/channels/email_test.go @@ -29,7 +29,7 @@ func TestEmailNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewEmailNotifier(model, externalURL, "") + _, err := NewEmailNotifier(model, externalURL) require.Error(t, err) }) @@ -43,7 +43,7 @@ func TestEmailNotifier(t *testing.T) { Type: "email", Settings: settingsJSON, - }, externalURL, "") + }, externalURL) require.NoError(t, err) @@ -91,8 +91,8 @@ func TestEmailNotifier(t *testing.T) { "CommonLabels": template.KV{"alertname": "AlwaysFiring", "severity": "warning"}, "CommonAnnotations": template.KV{"runbook_url": "http://fix.me"}, "ExternalURL": "http://localhost", - "RuleUrl": "/alerting/list", - "AlertPageUrl": "/alerting/list?alertState=firing&view=state", + "RuleUrl": "http:/localhost/alerting/list", + "AlertPageUrl": "http:/localhost/alerting/list?alertState=firing&view=state", }, }, expected) }) diff --git a/pkg/services/ngalert/notifier/channels/pagerduty.go b/pkg/services/ngalert/notifier/channels/pagerduty.go index e243a562f1a..14c12aac68e 100644 --- a/pkg/services/ngalert/notifier/channels/pagerduty.go +++ b/pkg/services/ngalert/notifier/channels/pagerduty.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "net/url" "os" gokit_log "github.com/go-kit/kit/log" @@ -42,11 +41,10 @@ type PagerdutyNotifier struct { Summary string tmpl *template.Template log log.Logger - externalUrl *url.URL } // NewPagerdutyNotifier is the constructor for the PagerDuty notifier -func NewPagerdutyNotifier(model *models.AlertNotification, t *template.Template, externalUrl *url.URL) (*PagerdutyNotifier, error) { +func NewPagerdutyNotifier(model *models.AlertNotification, t *template.Template) (*PagerdutyNotifier, error) { if model.Settings == nil { return nil, alerting.ValidationError{Reason: "No Settings Supplied"} } @@ -80,7 +78,6 @@ func NewPagerdutyNotifier(model *models.AlertNotification, t *template.Template, Group: model.Settings.Get("group").MustString("todo_group"), // TODO Summary: model.Settings.Get("summary").MustString(`{{ template "pagerduty.default.description" .}}`), tmpl: t, - externalUrl: externalUrl, log: log.New("alerting.notifier." + model.Name), }, nil } @@ -130,7 +127,7 @@ func (pn *PagerdutyNotifier) buildPagerdutyMessage(ctx context.Context, alerts m eventType = pagerDutyEventResolve } - data := notify.GetTemplateData(ctx, &template.Template{ExternalURL: pn.externalUrl}, as, gokit_log.NewNopLogger()) + data := notify.GetTemplateData(ctx, pn.tmpl, as, gokit_log.NewNopLogger()) var tmplErr error tmpl := notify.TmplText(pn.tmpl, data, &tmplErr) @@ -145,12 +142,12 @@ func (pn *PagerdutyNotifier) buildPagerdutyMessage(ctx context.Context, alerts m msg := &pagerDutyMessage{ Client: "Grafana", - ClientURL: pn.externalUrl.String(), + ClientURL: pn.tmpl.ExternalURL.String(), RoutingKey: pn.Key, EventAction: eventType, DedupKey: key.Hash(), Links: []pagerDutyLink{{ - HRef: pn.externalUrl.String(), + HRef: pn.tmpl.ExternalURL.String(), Text: "External URL", }}, Description: getTitleFromTemplateData(data), // TODO: this can be configurable template. diff --git a/pkg/services/ngalert/notifier/channels/pagerduty_test.go b/pkg/services/ngalert/notifier/channels/pagerduty_test.go index 98e730c1db4..5f11d9189aa 100644 --- a/pkg/services/ngalert/notifier/channels/pagerduty_test.go +++ b/pkg/services/ngalert/notifier/channels/pagerduty_test.go @@ -24,6 +24,10 @@ func TestPagerdutyNotifier(t *testing.T) { tmpl, err := template.FromGlobs("templates/default.tmpl") require.NoError(t, err) + externalURL, err := url.Parse("http://localhost") + require.NoError(t, err) + tmpl.ExternalURL = externalURL + hostname, err := os.Hostname() require.NoError(t, err) @@ -143,9 +147,7 @@ func TestPagerdutyNotifier(t *testing.T) { Settings: settingsJSON, } - externalURL, err := url.Parse("http://localhost") - require.NoError(t, err) - pn, err := NewPagerdutyNotifier(m, tmpl, externalURL) + pn, err := NewPagerdutyNotifier(m, tmpl) if c.expInitError != nil { require.Error(t, err) require.Equal(t, c.expInitError.Error(), err.Error()) diff --git a/pkg/services/ngalert/notifier/channels/slack.go b/pkg/services/ngalert/notifier/channels/slack.go index 0beaf44c194..8faea0d6d89 100644 --- a/pkg/services/ngalert/notifier/channels/slack.go +++ b/pkg/services/ngalert/notifier/channels/slack.go @@ -31,9 +31,8 @@ import ( // alert notification to Slack. type SlackNotifier struct { old_notifiers.NotifierBase - log log.Logger - tmpl *template.Template - externalUrl *url.URL + log log.Logger + tmpl *template.Template URL *url.URL Username string @@ -54,7 +53,7 @@ var reRecipient *regexp.Regexp = regexp.MustCompile("^((@[a-z0-9][a-zA-Z0-9._-]* const slackAPIEndpoint = "https://slack.com/api/chat.postMessage" // NewSlackNotifier is the constructor for the Slack notifier -func NewSlackNotifier(model *models.AlertNotification, t *template.Template, externalUrl *url.URL) (*SlackNotifier, error) { +func NewSlackNotifier(model *models.AlertNotification, t *template.Template) (*SlackNotifier, error) { if model.Settings == nil { return nil, alerting.ValidationError{Reason: "No Settings Supplied"} } @@ -127,7 +126,6 @@ func NewSlackNotifier(model *models.AlertNotification, t *template.Template, ext Fallback: model.Settings.Get("fallback").MustString(`{{ template "slack.default.title" . }}`), log: log.New("alerting.notifier.slack"), tmpl: t, - externalUrl: externalUrl, }, nil } @@ -242,7 +240,7 @@ var sendSlackRequest = func(request *http.Request, logger log.Logger) error { } func (sn *SlackNotifier) buildSlackMessage(ctx context.Context, as []*types.Alert) (*slackMessage, error) { - data := notify.GetTemplateData(ctx, &template.Template{ExternalURL: sn.externalUrl}, as, gokit_log.NewNopLogger()) + data := notify.GetTemplateData(ctx, sn.tmpl, as, gokit_log.NewNopLogger()) alerts := types.Alerts(as...) var tmplErr error tmpl := notify.TmplText(sn.tmpl, data, &tmplErr) diff --git a/pkg/services/ngalert/notifier/channels/slack_test.go b/pkg/services/ngalert/notifier/channels/slack_test.go index 0baec37964a..26d21aa61b5 100644 --- a/pkg/services/ngalert/notifier/channels/slack_test.go +++ b/pkg/services/ngalert/notifier/channels/slack_test.go @@ -25,6 +25,10 @@ func TestSlackNotifier(t *testing.T) { tmpl, err := template.FromGlobs("templates/default.tmpl") require.NoError(t, err) + externalURL, err := url.Parse("http://localhost") + require.NoError(t, err) + tmpl.ExternalURL = externalURL + cases := []struct { name string settings string @@ -180,9 +184,7 @@ func TestSlackNotifier(t *testing.T) { Settings: settingsJSON, } - externalURL, err := url.Parse("http://localhost") - require.NoError(t, err) - pn, err := NewSlackNotifier(m, tmpl, externalURL) + pn, err := NewSlackNotifier(m, tmpl) if c.expInitError != nil { require.Error(t, err) require.Equal(t, c.expInitError.Error(), err.Error()) diff --git a/pkg/services/ngalert/notifier/channels/teams.go b/pkg/services/ngalert/notifier/channels/teams.go index 1a0b981fcbe..6f376f01b9b 100644 --- a/pkg/services/ngalert/notifier/channels/teams.go +++ b/pkg/services/ngalert/notifier/channels/teams.go @@ -3,33 +3,32 @@ package channels import ( "context" "encoding/json" - "net/url" gokit_log "github.com/go-kit/kit/log" + "github.com/pkg/errors" + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/template" + "github.com/prometheus/alertmanager/types" + "github.com/grafana/grafana/pkg/bus" "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/pkg/errors" - "github.com/prometheus/alertmanager/notify" - "github.com/prometheus/alertmanager/template" - "github.com/prometheus/alertmanager/types" ) // TeamsNotifier is responsible for sending // alert notifications to Microsoft teams. type TeamsNotifier struct { old_notifiers.NotifierBase - URL string - Message string - tmpl *template.Template - log log.Logger - externalUrl *url.URL + URL string + Message string + tmpl *template.Template + log log.Logger } // NewTeamsNotifier is the constructor for Teams notifier. -func NewTeamsNotifier(model *models.AlertNotification, t *template.Template, externalUrl *url.URL) (*TeamsNotifier, error) { +func NewTeamsNotifier(model *models.AlertNotification, t *template.Template) (*TeamsNotifier, error) { if model.Settings == nil { return nil, alerting.ValidationError{Reason: "No Settings Supplied"} } @@ -44,14 +43,13 @@ func NewTeamsNotifier(model *models.AlertNotification, t *template.Template, ext URL: u, Message: model.Settings.Get("message").MustString(`{{ template "default.message" .}}`), log: log.New("alerting.notifier.teams"), - externalUrl: externalUrl, tmpl: t, }, nil } // Notify send an alert notification to Microsoft teams. func (tn *TeamsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { - data := notify.GetTemplateData(ctx, &template.Template{ExternalURL: tn.externalUrl}, as, gokit_log.NewNopLogger()) + data := notify.GetTemplateData(ctx, tn.tmpl, as, gokit_log.NewNopLogger()) var tmplErr error tmpl := notify.TmplText(tn.tmpl, data, &tmplErr) diff --git a/pkg/services/ngalert/notifier/channels/teams_test.go b/pkg/services/ngalert/notifier/channels/teams_test.go index 245e2542f45..d6b063337e8 100644 --- a/pkg/services/ngalert/notifier/channels/teams_test.go +++ b/pkg/services/ngalert/notifier/channels/teams_test.go @@ -23,6 +23,10 @@ func TestTeamsNotifier(t *testing.T) { tmpl, err := template.FromGlobs("templates/default.tmpl") require.NoError(t, err) + externalURL, err := url.Parse("http://localhost") + require.NoError(t, err) + tmpl.ExternalURL = externalURL + cases := []struct { name string settings string @@ -132,9 +136,7 @@ func TestTeamsNotifier(t *testing.T) { Settings: settingsJSON, } - externalURL, err := url.Parse("http://localhost") - require.NoError(t, err) - pn, err := NewTeamsNotifier(m, tmpl, externalURL) + pn, err := NewTeamsNotifier(m, tmpl) if c.expInitError != nil { require.Error(t, err) require.Equal(t, c.expInitError.Error(), err.Error()) diff --git a/pkg/services/ngalert/notifier/channels/telegram.go b/pkg/services/ngalert/notifier/channels/telegram.go index 3723f46345e..140f02946fd 100644 --- a/pkg/services/ngalert/notifier/channels/telegram.go +++ b/pkg/services/ngalert/notifier/channels/telegram.go @@ -5,7 +5,6 @@ import ( "context" "fmt" "mime/multipart" - "net/url" gokit_log "github.com/go-kit/kit/log" "github.com/prometheus/alertmanager/notify" @@ -27,16 +26,15 @@ const ( // alert notifications to Telegram. type TelegramNotifier struct { old_notifiers.NotifierBase - BotToken string - ChatID string - Message string - log log.Logger - tmpl *template.Template - externalUrl *url.URL + BotToken string + ChatID string + Message string + log log.Logger + tmpl *template.Template } // NewTelegramNotifier is the constructor for the Telegram notifier -func NewTelegramNotifier(model *models.AlertNotification, t *template.Template, externalUrl *url.URL) (*TelegramNotifier, error) { +func NewTelegramNotifier(model *models.AlertNotification, t *template.Template) (*TelegramNotifier, error) { if model.Settings == nil { return nil, alerting.ValidationError{Reason: "No Settings Supplied"} } @@ -60,7 +58,6 @@ func NewTelegramNotifier(model *models.AlertNotification, t *template.Template, Message: message, tmpl: t, log: log.New("alerting.notifier.telegram"), - externalUrl: externalUrl, }, nil } @@ -114,7 +111,7 @@ func (tn *TelegramNotifier) buildTelegramMessage(ctx context.Context, as []*type msg["chat_id"] = tn.ChatID msg["parse_mode"] = "html" - data := notify.GetTemplateData(ctx, &template.Template{ExternalURL: tn.externalUrl}, as, gokit_log.NewNopLogger()) + data := notify.GetTemplateData(ctx, &template.Template{ExternalURL: tn.tmpl.ExternalURL}, as, gokit_log.NewNopLogger()) var tmplErr error tmpl := notify.TmplText(tn.tmpl, data, &tmplErr) diff --git a/pkg/services/ngalert/notifier/channels/telegram_test.go b/pkg/services/ngalert/notifier/channels/telegram_test.go index df4f161157f..0419224419a 100644 --- a/pkg/services/ngalert/notifier/channels/telegram_test.go +++ b/pkg/services/ngalert/notifier/channels/telegram_test.go @@ -21,6 +21,10 @@ func TestTelegramNotifier(t *testing.T) { tmpl, err := template.FromGlobs("templates/default.tmpl") require.NoError(t, err) + externalURL, err := url.Parse("http://localhost") + require.NoError(t, err) + tmpl.ExternalURL = externalURL + cases := []struct { name string settings string @@ -105,9 +109,7 @@ func TestTelegramNotifier(t *testing.T) { Settings: settingsJSON, } - externalURL, err := url.Parse("http://localhost") - require.NoError(t, err) - pn, err := NewTelegramNotifier(m, tmpl, externalURL) + pn, err := NewTelegramNotifier(m, tmpl) if c.expInitError != nil { require.Error(t, err) require.Equal(t, c.expInitError.Error(), err.Error()) diff --git a/pkg/services/ngalert/notifier/channels/webhook.go b/pkg/services/ngalert/notifier/channels/webhook.go new file mode 100644 index 00000000000..e6ceb67a37f --- /dev/null +++ b/pkg/services/ngalert/notifier/channels/webhook.go @@ -0,0 +1,130 @@ +package channels + +import ( + "context" + "encoding/json" + "fmt" + + gokit_log "github.com/go-kit/kit/log" + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/template" + "github.com/prometheus/alertmanager/types" + "github.com/prometheus/common/model" + + "github.com/grafana/grafana/pkg/bus" + "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" +) + +// WebhookNotifier is responsible for sending +// alert notifications as webhooks. +type WebhookNotifier struct { + old_notifiers.NotifierBase + URL string + User string + Password string + HTTPMethod string + MaxAlerts int + log log.Logger + tmpl *template.Template +} + +// NewWebHookNotifier is the constructor for +// the WebHook notifier. +func NewWebHookNotifier(model *models.AlertNotification, t *template.Template) (*WebhookNotifier, error) { + url := model.Settings.Get("url").MustString() + if url == "" { + return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} + } + return &WebhookNotifier{ + NotifierBase: old_notifiers.NewNotifierBase(model), + URL: url, + User: model.Settings.Get("username").MustString(), + Password: model.DecryptedValue("password", model.Settings.Get("password").MustString()), + HTTPMethod: model.Settings.Get("httpMethod").MustString("POST"), + MaxAlerts: model.Settings.Get("maxAlerts").MustInt(0), + log: log.New("alerting.notifier.webhook"), + tmpl: t, + }, nil +} + +// webhookMessage defines the JSON object send to webhook endpoints. +type webhookMessage struct { + *template.Data + + // The protocol version. + Version string `json:"version"` + GroupKey string `json:"groupKey"` + TruncatedAlerts int `json:"truncatedAlerts"` + + // Deprecated, to be removed in 8.1. + // These are present to make migration a little less disruptive. + Title string `json:"title"` + State string `json:"state"` + Message string `json:"message"` +} + +// Notify implements the Notifier interface. +func (wn *WebhookNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { + groupKey, err := notify.ExtractGroupKey(ctx) + if err != nil { + return false, err + } + + as, numTruncated := truncateAlerts(wn.MaxAlerts, as) + data := notify.GetTemplateData(ctx, wn.tmpl, as, gokit_log.NewNopLogger()) + + var tmplErr error + tmpl := notify.TmplText(wn.tmpl, data, &tmplErr) + msg := &webhookMessage{ + Version: "1", + Data: data, + GroupKey: groupKey.String(), + TruncatedAlerts: numTruncated, + Title: tmpl(`{{ template "default.title" . }}`), + Message: tmpl(`{{ template "default.message" . }}`), + } + + if types.Alerts(as...).Status() == model.AlertFiring { + msg.State = string(models.AlertStateAlerting) + } else { + msg.State = string(models.AlertStateOK) + } + + if tmplErr != nil { + return false, fmt.Errorf("failed to template webhook message: %w", tmplErr) + } + + body, err := json.Marshal(msg) + if err != nil { + return false, err + } + + cmd := &models.SendWebhookSync{ + Url: wn.URL, + User: wn.User, + Password: wn.Password, + Body: string(body), + HttpMethod: wn.HTTPMethod, + } + + if err := bus.DispatchCtx(ctx, cmd); err != nil { + return false, err + } + + return true, nil +} + +func truncateAlerts(maxAlerts int, alerts []*types.Alert) ([]*types.Alert, int) { + if maxAlerts > 0 && len(alerts) > maxAlerts { + return alerts[:maxAlerts], len(alerts) - maxAlerts + } + + return alerts, 0 +} + +func (wn *WebhookNotifier) SendResolved() bool { + return !wn.GetDisableResolveMessage() +} diff --git a/pkg/services/ngalert/notifier/channels/webhook_test.go b/pkg/services/ngalert/notifier/channels/webhook_test.go new file mode 100644 index 00000000000..ee8648c0fd8 --- /dev/null +++ b/pkg/services/ngalert/notifier/channels/webhook_test.go @@ -0,0 +1,222 @@ +package channels + +import ( + "context" + "encoding/json" + "net/url" + "testing" + + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/template" + "github.com/prometheus/alertmanager/types" + "github.com/prometheus/common/model" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" +) + +func TestWebhookNotifier(t *testing.T) { + tmpl, err := template.FromGlobs("templates/default.tmpl") + require.NoError(t, err) + + externalURL, err := url.Parse("http://localhost") + require.NoError(t, err) + tmpl.ExternalURL = externalURL + + cases := []struct { + name string + settings string + alerts []*types.Alert + expMsg *webhookMessage + expUrl string + expUsername string + expPassword string + expHttpMethod string + expInitError error + expMsgError error + }{ + { + name: "Default config with one alert", + settings: `{"url": "http://localhost/test"}`, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1"}, + }, + }, + }, + expUrl: "http://localhost/test", + expHttpMethod: "POST", + expMsg: &webhookMessage{ + Data: &template.Data{ + Receiver: "my_receiver", + Status: "firing", + Alerts: template.Alerts{ + { + Status: "firing", + Labels: template.KV{ + "alertname": "alert1", + "lbl1": "val1", + }, + Annotations: template.KV{ + "ann1": "annv1", + }, + Fingerprint: "fac0861a85de433a", + }, + }, + GroupLabels: template.KV{ + "alertname": "", + }, + CommonLabels: template.KV{ + "alertname": "alert1", + "lbl1": "val1", + }, + CommonAnnotations: template.KV{ + "ann1": "annv1", + }, + ExternalURL: "http://localhost", + }, + Version: "1", + GroupKey: "alertname", + Title: "[FIRING:1] (val1)", + State: "alerting", + Message: "\n**Firing**\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: \n\n\n\n\n", + }, + expInitError: nil, + expMsgError: nil, + }, { + name: "Custom config with multiple alerts", + settings: `{ + "url": "http://localhost/test1", + "username": "user1", + "password": "mysecret", + "httpMethod": "PUT", + "maxAlerts": 2 + }`, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1"}, + }, + }, { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val2"}, + Annotations: model.LabelSet{"ann1": "annv2"}, + }, + }, { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val3"}, + Annotations: model.LabelSet{"ann1": "annv3"}, + }, + }, + }, + expUrl: "http://localhost/test1", + expHttpMethod: "PUT", + expUsername: "user1", + expPassword: "mysecret", + expMsg: &webhookMessage{ + Data: &template.Data{ + Receiver: "my_receiver", + Status: "firing", + Alerts: template.Alerts{ + { + Status: "firing", + Labels: template.KV{ + "alertname": "alert1", + "lbl1": "val1", + }, + Annotations: template.KV{ + "ann1": "annv1", + }, + Fingerprint: "fac0861a85de433a", + }, { + Status: "firing", + Labels: template.KV{ + "alertname": "alert1", + "lbl1": "val2", + }, + Annotations: template.KV{ + "ann1": "annv2", + }, + Fingerprint: "fab6861a85d5eeb5", + }, + }, + GroupLabels: template.KV{ + "alertname": "", + }, + CommonLabels: template.KV{ + "alertname": "alert1", + }, + CommonAnnotations: template.KV{}, + ExternalURL: "http://localhost", + }, + Version: "1", + GroupKey: "alertname", + TruncatedAlerts: 1, + Title: "[FIRING:2] ", + State: "alerting", + Message: "\n**Firing**\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: \nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSource: \n\n\n\n\n", + }, + expInitError: nil, + expMsgError: nil, + }, { + name: "Error in initing", + settings: `{}`, + expInitError: alerting.ValidationError{Reason: "Could not find url property in settings"}, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + settingsJSON, err := simplejson.NewJson([]byte(c.settings)) + require.NoError(t, err) + + m := &models.AlertNotification{ + Name: "webhook_testing", + Type: "webhook", + Settings: settingsJSON, + } + + pn, err := NewWebHookNotifier(m, tmpl) + if c.expInitError != nil { + require.Error(t, err) + require.Equal(t, c.expInitError.Error(), err.Error()) + return + } + require.NoError(t, err) + + var payload *models.SendWebhookSync + bus.AddHandlerCtx("test", func(ctx context.Context, webhook *models.SendWebhookSync) error { + payload = webhook + return nil + }) + + ctx := notify.WithGroupKey(context.Background(), "alertname") + ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) + ctx = notify.WithReceiverName(ctx, "my_receiver") + ok, err := pn.Notify(ctx, c.alerts...) + if c.expMsgError != nil { + require.False(t, ok) + require.Error(t, err) + require.Equal(t, c.expMsgError.Error(), err.Error()) + return + } + require.NoError(t, err) + require.True(t, ok) + + expBody, err := json.Marshal(c.expMsg) + require.NoError(t, err) + + require.JSONEq(t, string(expBody), payload.Body) + require.Equal(t, c.expUrl, payload.Url) + require.Equal(t, c.expUsername, payload.User) + require.Equal(t, c.expPassword, payload.Password) + require.Equal(t, c.expHttpMethod, payload.HttpMethod) + }) + } +}