diff --git a/pkg/services/ngalert/notifier/channels/discord.go b/pkg/services/ngalert/notifier/channels/discord.go index c04f81f4cab..fc471d165aa 100644 --- a/pkg/services/ngalert/notifier/channels/discord.go +++ b/pkg/services/ngalert/notifier/channels/discord.go @@ -26,18 +26,15 @@ import ( type DiscordNotifier struct { *Base - log log.Logger - ns notifications.WebhookSender - images ImageStore - tmpl *template.Template - Content string - AvatarURL string - WebhookURL string - UseDiscordUsername bool + log log.Logger + ns notifications.WebhookSender + images ImageStore + tmpl *template.Template + settings discordSettings } -type DiscordConfig struct { - *NotificationChannelConfig +type discordSettings struct { + Title string Content string AvatarURL string WebhookURL string @@ -54,50 +51,44 @@ type discordAttachment struct { const DiscordMaxEmbeds = 10 -func NewDiscordConfig(config *NotificationChannelConfig) (*DiscordConfig, error) { - discordURL := config.Settings.Get("url").MustString() - if discordURL == "" { - return nil, errors.New("could not find webhook url property in settings") - } - return &DiscordConfig{ - NotificationChannelConfig: config, - Content: config.Settings.Get("message").MustString(DefaultMessageEmbed), - AvatarURL: config.Settings.Get("avatar_url").MustString(), - WebhookURL: discordURL, - UseDiscordUsername: config.Settings.Get("use_discord_username").MustBool(false), - }, nil -} - func DiscordFactory(fc FactoryConfig) (NotificationChannel, error) { - cfg, err := NewDiscordConfig(fc.Config) + dn, err := newDiscordNotifier(fc) if err != nil { return nil, receiverInitError{ Reason: err.Error(), Cfg: *fc.Config, } } - return NewDiscordNotifier(cfg, fc.NotificationService, fc.ImageStore, fc.Template), nil + return dn, nil } -func NewDiscordNotifier(config *DiscordConfig, ns notifications.WebhookSender, images ImageStore, t *template.Template) *DiscordNotifier { +func newDiscordNotifier(fc FactoryConfig) (*DiscordNotifier, error) { + dUrl := fc.Config.Settings.Get("url").MustString() + if dUrl == "" { + return nil, errors.New("could not find webhook url property in settings") + } + return &DiscordNotifier{ Base: NewBase(&models.AlertNotification{ - Uid: config.UID, - Name: config.Name, - Type: config.Type, - DisableResolveMessage: config.DisableResolveMessage, - Settings: config.Settings, - SecureSettings: config.SecureSettings, + Uid: fc.Config.UID, + Name: fc.Config.Name, + Type: fc.Config.Type, + DisableResolveMessage: fc.Config.DisableResolveMessage, + Settings: fc.Config.Settings, + SecureSettings: fc.Config.SecureSettings, }), - Content: config.Content, - AvatarURL: config.AvatarURL, - WebhookURL: config.WebhookURL, - log: log.New("alerting.notifier.discord"), - ns: ns, - images: images, - tmpl: t, - UseDiscordUsername: config.UseDiscordUsername, - } + log: log.New("alerting.notifier.discord"), + ns: fc.NotificationService, + images: fc.ImageStore, + tmpl: fc.Template, + settings: discordSettings{ + Title: fc.Config.Settings.Get("title").MustString(DefaultMessageTitleEmbed), + Content: fc.Config.Settings.Get("message").MustString(DefaultMessageEmbed), + AvatarURL: fc.Config.Settings.Get("avatar_url").MustString(), + WebhookURL: dUrl, + UseDiscordUsername: fc.Config.Settings.Get("use_discord_username").MustBool(false), + }, + }, nil } func (d DiscordNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { @@ -105,27 +96,25 @@ func (d DiscordNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, bodyJSON := simplejson.New() - if !d.UseDiscordUsername { + if !d.settings.UseDiscordUsername { bodyJSON.Set("username", "Grafana") } var tmplErr error tmpl, _ := TmplText(ctx, d.tmpl, as, d.log, &tmplErr) - if d.Content != "" { - bodyJSON.Set("content", tmpl(d.Content)) - if tmplErr != nil { - d.log.Warn("failed to template Discord notification content", "error", tmplErr.Error()) - // Reset tmplErr for templating other fields. - tmplErr = nil - } + bodyJSON.Set("content", tmpl(d.settings.Content)) + if tmplErr != nil { + d.log.Warn("failed to template Discord notification content", "error", tmplErr.Error()) + // Reset tmplErr for templating other fields. + tmplErr = nil } - if d.AvatarURL != "" { - bodyJSON.Set("avatar_url", tmpl(d.AvatarURL)) + if d.settings.AvatarURL != "" { + bodyJSON.Set("avatar_url", tmpl(d.settings.AvatarURL)) if tmplErr != nil { - d.log.Warn("failed to template Discord Avatar URL", "error", tmplErr.Error(), "fallback", d.AvatarURL) - bodyJSON.Set("avatar_url", d.AvatarURL) + d.log.Warn("failed to template Discord Avatar URL", "error", tmplErr.Error(), "fallback", d.settings.AvatarURL) + bodyJSON.Set("avatar_url", d.settings.AvatarURL) tmplErr = nil } } @@ -136,7 +125,13 @@ func (d DiscordNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, } linkEmbed := simplejson.New() - linkEmbed.Set("title", tmpl(DefaultMessageTitleEmbed)) + + linkEmbed.Set("title", tmpl(d.settings.Title)) + if tmplErr != nil { + d.log.Warn("failed to template Discord notification title", "error", tmplErr.Error()) + // Reset tmplErr for templating other fields. + tmplErr = nil + } linkEmbed.Set("footer", footer) linkEmbed.Set("type", "rich") @@ -168,10 +163,10 @@ func (d DiscordNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, tmplErr = nil } - u := tmpl(d.WebhookURL) + u := tmpl(d.settings.WebhookURL) if tmplErr != nil { - d.log.Warn("failed to template Discord URL", "error", tmplErr.Error(), "fallback", d.WebhookURL) - u = d.WebhookURL + d.log.Warn("failed to template Discord URL", "error", tmplErr.Error(), "fallback", d.settings.WebhookURL) + u = d.settings.WebhookURL } body, err := json.Marshal(bodyJSON) @@ -179,7 +174,7 @@ func (d DiscordNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, return false, err } - cmd, err := d.buildRequest(ctx, u, body, attachments) + cmd, err := d.buildRequest(u, body, attachments) if err != nil { return false, err } @@ -241,7 +236,7 @@ func (d DiscordNotifier) constructAttachments(ctx context.Context, as []*types.A return attachments } -func (d DiscordNotifier) buildRequest(ctx context.Context, url string, body []byte, attachments []discordAttachment) (*models.SendWebhookSync, error) { +func (d DiscordNotifier) buildRequest(url string, body []byte, attachments []discordAttachment) (*models.SendWebhookSync, error) { cmd := &models.SendWebhookSync{ Url: url, HttpMethod: "POST", diff --git a/pkg/services/ngalert/notifier/channels/discord_test.go b/pkg/services/ngalert/notifier/channels/discord_test.go index e2216a60029..e6a7fc3a681 100644 --- a/pkg/services/ngalert/notifier/channels/discord_test.go +++ b/pkg/services/ngalert/notifier/channels/discord_test.go @@ -57,6 +57,33 @@ func TestDiscordNotifier(t *testing.T) { }, expMsgError: nil, }, + { + name: "Default config with one alert and custom title", + settings: `{"url": "http://localhost", "title": "Alerts firing: {{ len .Alerts.Firing }}"}`, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh"}, + }, + }, + }, + expMsg: map[string]interface{}{ + "content": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "embeds": []interface{}{map[string]interface{}{ + "color": 1.4037554e+07, + "footer": map[string]interface{}{ + "icon_url": "https://grafana.com/assets/img/fav32.png", + "text": "Grafana v" + setting.BuildVersion, + }, + "title": "Alerts firing: 1", + "url": "http://localhost/alerting/list", + "type": "rich", + }}, + "username": "Grafana", + }, + expMsgError: nil, + }, { name: "Missing field in template", settings: `{ @@ -262,25 +289,30 @@ func TestDiscordNotifier(t *testing.T) { t.Run(c.name, func(t *testing.T) { settingsJson, err := simplejson.NewJson([]byte(c.settings)) require.NoError(t, err) + webhookSender := mockNotificationService() + imageStore := &UnavailableImageStore{} - m := &NotificationChannelConfig{ - Name: "discord_testing", - Type: "discord", - Settings: settingsJson, + fc := FactoryConfig{ + Config: &NotificationChannelConfig{ + Name: "discord_testing", + Type: "discord", + Settings: settingsJson, + }, + ImageStore: imageStore, + // TODO: allow changing the associated values for different tests. + NotificationService: webhookSender, + Template: tmpl, } - webhookSender := mockNotificationService() - cfg, err := NewDiscordConfig(m) + dn, err := newDiscordNotifier(fc) if c.expInitError != "" { require.Equal(t, c.expInitError, err.Error()) return } require.NoError(t, err) - imageStore := &UnavailableImageStore{} ctx := notify.WithGroupKey(context.Background(), "alertname") ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) - dn := NewDiscordNotifier(cfg, webhookSender, imageStore, tmpl) ok, err := dn.Notify(ctx, c.alerts...) if c.expMsgError != nil { require.False(t, ok) diff --git a/pkg/services/ngalert/notifier/channels_config/available_channels.go b/pkg/services/ngalert/notifier/channels_config/available_channels.go index 5741f6bccf6..9734d6c0053 100644 --- a/pkg/services/ngalert/notifier/channels_config/available_channels.go +++ b/pkg/services/ngalert/notifier/channels_config/available_channels.go @@ -852,6 +852,14 @@ func GetAvailableNotifiers() []*NotifierPlugin { Heading: "Discord settings", Description: "Sends notifications to Discord", Options: []NotifierOption{ + { + Label: "Title", + Description: "Templated title of the message", + Element: ElementTypeInput, + InputType: InputTypeText, + Placeholder: channels.DefaultMessageTitleEmbed, + PropertyName: "title", + }, { Label: "Message Content", Description: "Mention a group using @ or a user using <@ID> when notifying in a channel",