diff --git a/pkg/services/ngalert/notifier/channels/dingding.go b/pkg/services/ngalert/notifier/channels/dingding.go index 70e5772f56b..bdf6a0d8b0f 100644 --- a/pkg/services/ngalert/notifier/channels/dingding.go +++ b/pkg/services/ngalert/notifier/channels/dingding.go @@ -17,109 +17,83 @@ import ( const defaultDingdingMsgType = "link" -type DingDingConfig struct { - *NotificationChannelConfig - MsgType string - Message string - URL string +type dingDingSettings struct { + URL string + MessageType string + Title string + Message string } -func NewDingDingConfig(config *NotificationChannelConfig) (*DingDingConfig, error) { - url := config.Settings.Get("url").MustString() - if url == "" { +func buildDingDingSettings(fc FactoryConfig) (*dingDingSettings, error) { + URL := fc.Config.Settings.Get("url").MustString() + if URL == "" { return nil, errors.New("could not find url property in settings") } - return &DingDingConfig{ - NotificationChannelConfig: config, - MsgType: config.Settings.Get("msgType").MustString(defaultDingdingMsgType), - Message: config.Settings.Get("message").MustString(DefaultMessageEmbed), - URL: config.Settings.Get("url").MustString(), + return &dingDingSettings{ + URL: URL, + MessageType: fc.Config.Settings.Get("msgType").MustString(defaultDingdingMsgType), + Title: fc.Config.Settings.Get("title").MustString(DefaultMessageTitleEmbed), + Message: fc.Config.Settings.Get("message").MustString(DefaultMessageEmbed), }, nil } + func DingDingFactory(fc FactoryConfig) (NotificationChannel, error) { - cfg, err := NewDingDingConfig(fc.Config) + n, err := newDingDingNotifier(fc) if err != nil { return nil, receiverInitError{ Reason: err.Error(), Cfg: *fc.Config, } } - return NewDingDingNotifier(cfg, fc.NotificationService, fc.Template), nil + return n, nil } -// NewDingDingNotifier is the constructor for the Dingding notifier -func NewDingDingNotifier(config *DingDingConfig, ns notifications.WebhookSender, t *template.Template) *DingDingNotifier { +// newDingDingNotifier is the constructor for the Dingding notifier +func newDingDingNotifier(fc FactoryConfig) (*DingDingNotifier, error) { + settings, err := buildDingDingSettings(fc) + if err != nil { + return nil, err + } return &DingDingNotifier{ Base: NewBase(&models.AlertNotification{ - Uid: config.UID, - Name: config.Name, - Type: config.Type, - DisableResolveMessage: config.DisableResolveMessage, - Settings: config.Settings, + Uid: fc.Config.UID, + Name: fc.Config.Name, + Type: fc.Config.Type, + DisableResolveMessage: fc.Config.DisableResolveMessage, + Settings: fc.Config.Settings, }), - MsgType: config.MsgType, - Message: config.Message, - URL: config.URL, - log: log.New("alerting.notifier.dingding"), - tmpl: t, - ns: ns, - } + log: log.New("alerting.notifier.dingding"), + ns: fc.NotificationService, + tmpl: fc.Template, + settings: *settings, + }, nil } // DingDingNotifier is responsible for sending alert notifications to ding ding. type DingDingNotifier struct { *Base - MsgType string - URL string - Message string - tmpl *template.Template - ns notifications.WebhookSender - log log.Logger + log log.Logger + ns notifications.WebhookSender + tmpl *template.Template + settings dingDingSettings } // Notify sends the alert notification to dingding. func (dd *DingDingNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { dd.log.Info("sending dingding") - ruleURL := joinUrlPath(dd.tmpl.ExternalURL.String(), "/alerting/list", dd.log) - - q := url.Values{ - "pc_slide": {"false"}, - "url": {ruleURL}, - } - - // 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() + msgUrl := buildDingDingURL(dd) var tmplErr error tmpl, _ := TmplText(ctx, dd.tmpl, as, dd.log, &tmplErr) - message := tmpl(dd.Message) - title := tmpl(DefaultMessageTitleEmbed) + message := tmpl(dd.settings.Message) + title := tmpl(dd.settings.Title) - var bodyMsg map[string]interface{} - if tmpl(dd.MsgType) == "actionCard" { - bodyMsg = map[string]interface{}{ - "msgtype": "actionCard", - "actionCard": map[string]string{ - "text": message, - "title": title, - "singleTitle": "More", - "singleURL": messageURL, - }, - } - } else { - link := map[string]string{ - "text": message, - "title": title, - "messageUrl": messageURL, - } - - bodyMsg = map[string]interface{}{ - "msgtype": "link", - "link": link, - } + msgType := tmpl(dd.settings.MessageType) + b, err := buildBody(msgUrl, msgType, title, message) + if err != nil { + return false, err } if tmplErr != nil { @@ -127,21 +101,13 @@ func (dd *DingDingNotifier) Notify(ctx context.Context, as ...*types.Alert) (boo tmplErr = nil } - u := tmpl(dd.URL) + u := tmpl(dd.settings.URL) if tmplErr != nil { - dd.log.Warn("failed to template DingDing URL", "error", tmplErr.Error(), "fallback", dd.URL) - u = dd.URL + dd.log.Warn("failed to template DingDing URL", "error", tmplErr.Error(), "fallback", dd.settings.URL) + u = dd.settings.URL } - body, err := json.Marshal(bodyMsg) - if err != nil { - return false, err - } - - cmd := &models.SendWebhookSync{ - Url: u, - Body: string(body), - } + cmd := &models.SendWebhookSync{Url: u, Body: b} if err := dd.ns.SendWebhookSync(ctx, cmd); err != nil { return false, fmt.Errorf("send notification to dingding: %w", err) @@ -153,3 +119,43 @@ func (dd *DingDingNotifier) Notify(ctx context.Context, as ...*types.Alert) (boo func (dd *DingDingNotifier) SendResolved() bool { return !dd.GetDisableResolveMessage() } + +func buildDingDingURL(dd *DingDingNotifier) string { + q := url.Values{ + "pc_slide": {"false"}, + "url": {joinUrlPath(dd.tmpl.ExternalURL.String(), "/alerting/list", dd.log)}, + } + + // Use special link to auto open the message url outside Dingding + // Refer: https://open-doc.dingtalk.com/docs/doc.htm?treeId=385&articleId=104972&docType=1#s9 + return "dingtalk://dingtalkclient/page/link?" + q.Encode() +} + +func buildBody(msgUrl string, msgType string, title string, msg string) (string, error) { + var bodyMsg map[string]interface{} + if msgType == "actionCard" { + bodyMsg = map[string]interface{}{ + "msgtype": "actionCard", + "actionCard": map[string]string{ + "text": msg, + "title": title, + "singleTitle": "More", + "singleURL": msgUrl, + }, + } + } else { + bodyMsg = map[string]interface{}{ + "msgtype": "link", + "link": map[string]string{ + "text": msg, + "title": title, + "messageUrl": msgUrl, + }, + } + } + body, err := json.Marshal(bodyMsg) + if err != nil { + return "", err + } + return string(body), nil +} diff --git a/pkg/services/ngalert/notifier/channels/dingding_test.go b/pkg/services/ngalert/notifier/channels/dingding_test.go index 573cbcdaa5f..42aa15e505f 100644 --- a/pkg/services/ngalert/notifier/channels/dingding_test.go +++ b/pkg/services/ngalert/notifier/channels/dingding_test.go @@ -79,6 +79,26 @@ func TestDingdingNotifier(t *testing.T) { "msgtype": "actionCard", }, expMsgError: nil, + }, { + name: "Default config with one alert and custom title and description", + settings: `{"url": "http://localhost", "title": "Alerts firing: {{ len .Alerts.Firing }}", "message": "customMessage"}}`, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh", "__values__": "{\"A\": 1234}", "__value_string__": "1234"}, + }, + }, + }, + expMsg: map[string]interface{}{ + "msgtype": "link", + "link": map[string]interface{}{ + "messageUrl": "dingtalk://dingtalkclient/page/link?pc_slide=false&url=http%3A%2F%2Flocalhost%2Falerting%2Flist", + "text": "customMessage", + "title": "Alerts firing: 1", + }, + }, + expMsgError: nil, }, { name: "Missing field in template", settings: `{ @@ -149,14 +169,18 @@ func TestDingdingNotifier(t *testing.T) { settingsJSON, err := simplejson.NewJson([]byte(c.settings)) require.NoError(t, err) - m := &NotificationChannelConfig{ - Name: "dingding_testing", - Type: "dingding", - Settings: settingsJSON, - } - webhookSender := mockNotificationService() - cfg, err := NewDingDingConfig(m) + fc := FactoryConfig{ + Config: &NotificationChannelConfig{ + Name: "dingding_testing", + Type: "dingding", + Settings: settingsJSON, + }, + // TODO: allow changing the associated values for different tests. + NotificationService: webhookSender, + Template: tmpl, + } + pn, err := newDingDingNotifier(fc) if c.expInitError != "" { require.Equal(t, c.expInitError, err.Error()) return @@ -165,7 +189,6 @@ func TestDingdingNotifier(t *testing.T) { ctx := notify.WithGroupKey(context.Background(), "alertname") ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) - pn := NewDingDingNotifier(cfg, webhookSender, tmpl) ok, err := pn.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 622dc0b0ae2..5741f6bccf6 100644 --- a/pkg/services/ngalert/notifier/channels_config/available_channels.go +++ b/pkg/services/ngalert/notifier/channels_config/available_channels.go @@ -132,9 +132,18 @@ func GetAvailableNotifiers() []*NotifierPlugin { }, }, }, + { // New in 9.3. + Label: "Title", + Element: ElementTypeInput, + InputType: InputTypeText, + Description: "Templated title of the message", + Placeholder: channels.DefaultMessageTitleEmbed, + PropertyName: "title", + }, { // New in 8.0. Label: "Message", Element: ElementTypeTextArea, + Description: "Custom DingDing message. You can use template variables.", Placeholder: channels.DefaultMessageEmbed, PropertyName: "message", },