mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Fix mentioning Slack users/groups (#21734)
* alerting/slack: Allow mentioning users, groups, and channels separately
This commit is contained in:
@@ -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")
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user