From 6f09bc9fb45d1beecf2bdb5b1c7359b9b1a4d5ec Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Tue, 11 Feb 2020 21:43:28 +0100 Subject: [PATCH] Fix mentioning Slack users/groups (#21734) * alerting/slack: Allow mentioning users, groups, and channels separately --- CHANGELOG.md | 1 + docs/sources/administration/provisioning.md | 4 +- docs/sources/alerting/notifications.md | 16 +- pkg/services/alerting/notifiers/slack.go | 160 ++++++++++++++---- pkg/services/alerting/notifiers/slack_test.go | 30 ++-- 5 files changed, 156 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd78af1d4fa..5eb97119490 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # 6.7.0 (unreleased) ## Breaking changes +* **Slack**: Removed _Mention_ setting and instead introduce _Mention Users_, _Mention Groups_, and _Mention Channel_. The first two settings require user and group IDs, respectively. This change was necessary because the way of mentioning via the Slack API [changed](https://api.slack.com/changelog/2017-09-the-one-about-usernames) and mentions in Slack notifications no longer worked. ### Notice about changes in backendSrv for plugin authors diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index 151683dc5c0..f7b740fb315 100755 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -351,7 +351,9 @@ The following sections detail the supported settings for each alert notification | icon_emoji | | icon_url | | uploadImage | -| mention | +| mentionUsers | +| mentionGroups | +| mentionChannel | | token | #### Alert notification `victorops` diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index 69708e917b3..5ea13a685aa 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -75,20 +75,22 @@ able to access the image. {{< imgbox max-width="40%" img="/img/docs/v4/slack_notification.png" caption="Alerting Slack Notification" >}} -To set up slack you need to configure an incoming webhook url at slack. You can follow their guide on how -to do that [here](https://api.slack.com/incoming-webhooks). If you want to include screenshots of the firing alerts -in the Slack messages you have to configure either the [external image destination](#external-image-store) in Grafana, -or a bot integration via Slack Apps. Follow Slack's guide to set up a bot integration and use the token provided -(https://api.slack.com/bot-users), which starts with "xoxb". +To set up Slack, you need to configure an incoming Slack webhook URL. You can follow +[their guide](https://api.slack.com/incoming-webhooks) on how to do that. If you want to include screenshots of the +firing alerts in the Slack messages you have to configure either the [external image destination](#external-image-store) +in Grafana, or a bot integration via Slack Apps. Follow Slack's guide to set up a bot integration and use the token +provided (https://api.slack.com/bot-users), which starts with "xoxb". Setting | Description ---------- | ----------- -Url | Slack incoming webhook url. +Url | Slack incoming webhook URL. Username | Set the username for the bot's message. Recipient | Allows you to override the Slack recipient. Icon emoji | Provide an emoji to use as the icon for the bot's message. Ex :smile: Icon URL | Provide a url to an image to use as the icon for the bot's message. -Mention | make it possible to include a mention in the Slack notification sent by Grafana. Ex @here or @channel +Mention Users | Optionally mention one or more users in the Slack notification sent by Grafana. You have to refer to users, comma-separated, via their corresponding Slack IDs (which you can find by clicking the overflow button on each user's Slack profile). +Mention Groups | Optionally mention one or more groups in the Slack notification sent by Grafana. You have to refer to groups, comma-separated, via their corresponding Slack IDs (which you can get from each group's Slack profile URL). +Mention Channel | Optionally mention either all channel members or just active ones. Token | If provided, Grafana will upload the generated image via Slack's file.upload API method, not the external image destination. If you are using the token for a slack bot, then you have to invite the bot to the channel you want to send notifications and add the channel to the recipient field. diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index f86314d0726..199ac565d88 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -3,10 +3,12 @@ package notifiers import ( "bytes" "encoding/json" + "fmt" "io" "mime/multipart" "os" "path/filepath" + "strings" "time" "github.com/grafana/grafana/pkg/bus" @@ -25,11 +27,11 @@ func init() { OptionsTemplate: `

Slack settings

- Url + Url
- Recipient + Recipient
- Username + Username
- Icon emoji + Icon emoji
- Icon URL + Icon URL
- Mention + Mention Users - Mention a user or a group using @ when notifying in a channel + Mention one or more users (comma separated) when notifying in a channel, by ID (you can copy this from the user's Slack profile)
- Token + Mention Groups + + + + Mention one or more groups (comma separated) when notifying in a channel (you can copy this from the group's Slack profile URL) + +
+
+ Mention Channel + + + Mention whole channel or just active members when notifying + +
+
+ Token - Provide a bot token to use the Slack file.upload API (starts with "xoxb"). Specify #channel-name or @username in Recipient for this to work + Provide a bot token to use the Slack file.upload API (starts with "xoxb"). Specify #channel-name or @username in Recipient for this to work
`, @@ -110,21 +137,43 @@ func NewSlackNotifier(model *models.AlertNotification) (alerting.Notifier, error username := model.Settings.Get("username").MustString() iconEmoji := model.Settings.Get("icon_emoji").MustString() iconURL := model.Settings.Get("icon_url").MustString() - mention := model.Settings.Get("mention").MustString() + mentionUsersStr := model.Settings.Get("mentionUsers").MustString() + mentionGroupsStr := model.Settings.Get("mentionGroups").MustString() + mentionChannel := model.Settings.Get("mentionChannel").MustString() token := model.Settings.Get("token").MustString() uploadImage := model.Settings.Get("uploadImage").MustBool(true) + if mentionChannel != "" && mentionChannel != "here" && mentionChannel != "channel" { + return nil, fmt.Errorf(fmt.Sprintf("invalid value for mentionChannel: %q", mentionChannel)) + } + mentionUsers := []string{} + for _, u := range strings.Split(mentionUsersStr, ",") { + u = strings.TrimSpace(u) + if u != "" { + mentionUsers = append(mentionUsers, u) + } + } + mentionGroups := []string{} + for _, g := range strings.Split(mentionGroupsStr, ",") { + g = strings.TrimSpace(g) + if g != "" { + mentionGroups = append(mentionGroups, g) + } + } + return &SlackNotifier{ - NotifierBase: NewNotifierBase(model), - URL: url, - Recipient: recipient, - Username: username, - IconEmoji: iconEmoji, - IconURL: iconURL, - Mention: mention, - Token: token, - Upload: uploadImage, - log: log.New("alerting.notifier.slack"), + NotifierBase: NewNotifierBase(model), + URL: url, + Recipient: recipient, + Username: username, + IconEmoji: iconEmoji, + IconURL: iconURL, + MentionUsers: mentionUsers, + MentionGroups: mentionGroups, + MentionChannel: mentionChannel, + Token: token, + Upload: uploadImage, + log: log.New("alerting.notifier.slack"), }, nil } @@ -132,15 +181,17 @@ func NewSlackNotifier(model *models.AlertNotification) (alerting.Notifier, error // alert notification to Slack. type SlackNotifier struct { NotifierBase - URL string - Recipient string - Username string - IconEmoji string - IconURL string - Mention string - Token string - Upload bool - log log.Logger + URL string + Recipient string + Username string + IconEmoji string + IconURL string + MentionUsers []string + MentionGroups []string + MentionChannel string + Token string + Upload bool + log log.Logger } // Notify send alert notification to Slack. @@ -174,9 +225,31 @@ func (sn *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { }) } - message := sn.Mention + mentionsBuilder := strings.Builder{} + appendSpace := func() { + if mentionsBuilder.Len() > 0 { + mentionsBuilder.WriteString(" ") + } + } + mentionChannel := strings.TrimSpace(sn.MentionChannel) + if mentionChannel != "" { + mentionsBuilder.WriteString(fmt.Sprintf("", mentionChannel, mentionChannel)) + } + if len(sn.MentionGroups) > 0 { + appendSpace() + for _, g := range sn.MentionGroups { + mentionsBuilder.WriteString(fmt.Sprintf("", g)) + } + } + if len(sn.MentionUsers) > 0 { + appendSpace() + for _, u := range sn.MentionUsers { + mentionsBuilder.WriteString(fmt.Sprintf("<@%s>", u)) + } + } + msg := "" if evalContext.Rule.State != models.AlertStateOK { //don't add message when going back to alert state ok. - message += " " + evalContext.Rule.Message + msg = evalContext.Rule.Message } imageURL := "" // default to file.upload API method if a token is provided @@ -184,14 +257,28 @@ func (sn *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { imageURL = evalContext.ImagePublicURL } + var blocks []map[string]interface{} + if mentionsBuilder.Len() > 0 { + blocks = []map[string]interface{}{ + { + "type": "section", + "text": map[string]interface{}{ + "type": "mrkdwn", + "text": mentionsBuilder.String(), + }, + }, + } + } body := map[string]interface{}{ + "text": evalContext.GetNotificationTitle(), + "blocks": blocks, "attachments": []map[string]interface{}{ { - "fallback": evalContext.GetNotificationTitle(), "color": evalContext.GetStateModel().Color, "title": evalContext.GetNotificationTitle(), "title_link": ruleURL, - "text": message, + "text": msg, + "fallback": evalContext.GetNotificationTitle(), "fields": fields, "image_url": imageURL, "footer": "Grafana v" + setting.BuildVersion, @@ -215,7 +302,10 @@ func (sn *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { if sn.IconURL != "" { body["icon_url"] = sn.IconURL } - data, _ := json.Marshal(&body) + data, err := json.Marshal(&body) + if err != nil { + return err + } cmd := &models.SendWebhookSync{Url: sn.URL, Body: string(data)} if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { sn.log.Error("Failed to send slack notification", "error", err, "webhook", sn.Name) diff --git a/pkg/services/alerting/notifiers/slack_test.go b/pkg/services/alerting/notifiers/slack_test.go index ca00157590e..e14469ed9d9 100644 --- a/pkg/services/alerting/notifiers/slack_test.go +++ b/pkg/services/alerting/notifiers/slack_test.go @@ -51,21 +51,25 @@ func TestSlackNotifier(t *testing.T) { So(slackNotifier.Username, ShouldEqual, "") So(slackNotifier.IconEmoji, ShouldEqual, "") So(slackNotifier.IconURL, ShouldEqual, "") - So(slackNotifier.Mention, ShouldEqual, "") + So(slackNotifier.MentionUsers, ShouldResemble, []string{}) + So(slackNotifier.MentionGroups, ShouldResemble, []string{}) + So(slackNotifier.MentionChannel, ShouldEqual, "") So(slackNotifier.Token, ShouldEqual, "") }) - Convey("from settings with Recipient, Username, IconEmoji, IconUrl, Mention, and Token", func() { + Convey("from settings with Recipient, Username, IconEmoji, IconUrl, MentionUsers, MentionGroups, MentionChannel, and Token", func() { json := ` - { - "url": "http://google.com", - "recipient": "#ds-opentsdb", - "username": "Grafana Alerts", - "icon_emoji": ":smile:", - "icon_url": "https://grafana.com/img/fav32.png", - "mention": "@carl", - "token": "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX" - }` + { + "url": "http://google.com", + "recipient": "#ds-opentsdb", + "username": "Grafana Alerts", + "icon_emoji": ":smile:", + "icon_url": "https://grafana.com/img/fav32.png", + "mentionUsers": "user1, user2", + "mentionGroups": "group1, group2", + "mentionChannel": "here", + "token": "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX" + }` settingsJSON, _ := simplejson.NewJson([]byte(json)) model := &models.AlertNotification{ @@ -85,7 +89,9 @@ func TestSlackNotifier(t *testing.T) { So(slackNotifier.Username, ShouldEqual, "Grafana Alerts") So(slackNotifier.IconEmoji, ShouldEqual, ":smile:") So(slackNotifier.IconURL, ShouldEqual, "https://grafana.com/img/fav32.png") - So(slackNotifier.Mention, ShouldEqual, "@carl") + So(slackNotifier.MentionUsers, ShouldResemble, []string{"user1", "user2"}) + So(slackNotifier.MentionGroups, ShouldResemble, []string{"group1", "group2"}) + So(slackNotifier.MentionChannel, ShouldEqual, "here") So(slackNotifier.Token, ShouldEqual, "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX") }) })