mirror of
https://github.com/grafana/grafana.git
synced 2024-11-22 17:06:57 -06:00
Fix mentioning Slack users/groups (#21734)
* alerting/slack: Allow mentioning users, groups, and channels separately
This commit is contained in:
parent
717e1a3b2b
commit
6f09bc9fb4
@ -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
|
||||
|
||||
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user