Commit 6f09bc9f by Arve Knudsen Committed by GitHub

Fix mentioning Slack users/groups (#21734)

* alerting/slack: Allow mentioning users, groups, and channels separately
parent 717e1a3b
# 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
......
......@@ -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`
......
......@@ -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.
......
......@@ -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: `
<h3 class="page-heading">Slack settings</h3>
<div class="gf-form max-width-30">
<span class="gf-form-label width-6">Url</span>
<span class="gf-form-label width-8">Url</span>
<input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.url" placeholder="Slack incoming webhook url"></input>
</div>
<div class="gf-form max-width-30">
<span class="gf-form-label width-6">Recipient</span>
<span class="gf-form-label width-8">Recipient</span>
<input type="text"
class="gf-form-input max-width-30"
ng-model="ctrl.model.settings.recipient"
......@@ -40,7 +42,7 @@ func init() {
</info-popover>
</div>
<div class="gf-form max-width-30">
<span class="gf-form-label width-6">Username</span>
<span class="gf-form-label width-8">Username</span>
<input type="text"
class="gf-form-input max-width-30"
ng-model="ctrl.model.settings.username"
......@@ -51,7 +53,7 @@ func init() {
</info-popover>
</div>
<div class="gf-form max-width-30">
<span class="gf-form-label width-6">Icon emoji</span>
<span class="gf-form-label width-8">Icon emoji</span>
<input type="text"
class="gf-form-input max-width-30"
ng-model="ctrl.model.settings.icon_emoji"
......@@ -62,7 +64,7 @@ func init() {
</info-popover>
</div>
<div class="gf-form max-width-30">
<span class="gf-form-label width-6">Icon URL</span>
<span class="gf-form-label width-8">Icon URL</span>
<input type="text"
class="gf-form-input max-width-30"
ng-model="ctrl.model.settings.icon_url"
......@@ -73,25 +75,50 @@ func init() {
</info-popover>
</div>
<div class="gf-form max-width-30">
<span class="gf-form-label width-6">Mention</span>
<span class="gf-form-label width-8">Mention Users</span>
<input type="text"
class="gf-form-input max-width-30"
ng-model="ctrl.model.settings.mention"
ng-model="ctrl.model.settings.mentionUsers"
data-placement="right">
</input>
<info-popover mode="right-absolute">
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)
</info-popover>
</div>
<div class="gf-form max-width-30">
<span class="gf-form-label width-6">Token</span>
<span class="gf-form-label width-8">Mention Groups</span>
<input type="text"
class="gf-form-input max-width-30"
ng-model="ctrl.model.settings.mentionGroups"
data-placement="right">
</input>
<info-popover mode="right-absolute">
Mention one or more groups (comma separated) when notifying in a channel (you can copy this from the group's Slack profile URL)
</info-popover>
</div>
<div class="gf-form max-width-30">
<span class="gf-form-label width-8">Mention Channel</span>
<select
class="gf-form-input max-width-30"
ng-model="ctrl.model.settings.mentionChannel"
data-placement="right">
<option value="">Disabled</option>
<option value="here">Every active channel member</option>
<option value="channel">Every channel member</option>
</select>
<info-popover mode="right-absolute">
Mention whole channel or just active members when notifying
</info-popover>
</div>
<div class="gf-form max-width-30">
<span class="gf-form-label width-8">Token</span>
<input type="text"
class="gf-form-input max-width-30"
ng-model="ctrl.model.settings.token"
data-placement="right">
</input>
<info-popover mode="right-absolute">
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
</info-popover>
</div>
`,
......@@ -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("<!%s|%s>", mentionChannel, mentionChannel))
}
if len(sn.MentionGroups) > 0 {
appendSpace()
for _, g := range sn.MentionGroups {
mentionsBuilder.WriteString(fmt.Sprintf("<!subteam^%s>", 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)
......
......@@ -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")
})
})
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment