Alerting: Update Discord receiver to use encoding/json to build a webhook message + truncate long message (#60592)

* replace simplejson with models
* truncate too long messages

Co-authored-by: Santiago <santiagohernandez.1997@gmail.com>
This commit is contained in:
Yuri Tseretyan 2022-12-20 14:20:42 -05:00 committed by GitHub
parent aaa55b4252
commit 4a3097f52a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 105 additions and 27 deletions

View File

@ -13,13 +13,53 @@ import (
"strings"
"github.com/grafana/alerting/alerting/notifier/channels"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
"github.com/grafana/grafana/pkg/components/simplejson"
)
// Constants and models are set according to the official documentation https://discord.com/developers/docs/resources/webhook#execute-webhook-jsonform-params
type discordEmbedType string
const (
discordRichEmbed discordEmbedType = "rich"
discordMaxEmbeds = 10
discordMaxMessageLen = 2000
)
type discordMessage struct {
Username string `json:"username,omitempty"`
Content string `json:"content"`
AvatarURL string `json:"avatar_url,omitempty"`
Embeds []discordLinkEmbed `json:"embeds,omitempty"`
}
// discordLinkEmbed implements https://discord.com/developers/docs/resources/channel#embed-object
type discordLinkEmbed struct {
Title string `json:"title,omitempty"`
Type discordEmbedType `json:"type,omitempty"`
URL string `json:"url,omitempty"`
Color int64 `json:"color,omitempty"`
Footer *discordFooter `json:"footer,omitempty"`
Image *discordImage `json:"image,omitempty"`
}
// discordFooter implements https://discord.com/developers/docs/resources/channel#embed-object-embed-footer-structure
type discordFooter struct {
Text string `json:"text"`
IconURL string `json:"icon_url,omitempty"`
}
// discordImage implements https://discord.com/developers/docs/resources/channel#embed-object-embed-footer-structure
type discordImage struct {
URL string `json:"url"`
}
type DiscordNotifier struct {
*channels.Base
log channels.Logger
@ -64,8 +104,6 @@ type discordAttachment struct {
state model.AlertStatus
}
const DiscordMaxEmbeds = 10
func DiscordFactory(fc channels.FactoryConfig) (channels.NotificationChannel, error) {
dn, err := newDiscordNotifier(fc)
if err != nil {
@ -96,69 +134,78 @@ func newDiscordNotifier(fc channels.FactoryConfig) (*DiscordNotifier, error) {
func (d DiscordNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
alerts := types.Alerts(as...)
bodyJSON := simplejson.New()
var msg discordMessage
if !d.settings.UseDiscordUsername {
bodyJSON.Set("username", "Grafana")
msg.Username = "Grafana"
}
var tmplErr error
tmpl, _ := channels.TmplText(ctx, d.tmpl, as, d.log, &tmplErr)
bodyJSON.Set("content", tmpl(d.settings.Message))
msg.Content = tmpl(d.settings.Message)
if tmplErr != nil {
d.log.Warn("failed to template Discord notification content", "error", tmplErr.Error())
// Reset tmplErr for templating other fields.
tmplErr = nil
}
truncatedMsg, truncated := channels.TruncateInRunes(msg.Content, discordMaxMessageLen)
if truncated {
key, err := notify.ExtractGroupKey(ctx)
if err != nil {
return false, err
}
d.log.Warn("Truncated content", "key", key, "max_runes", discordMaxMessageLen)
msg.Content = truncatedMsg
}
if d.settings.AvatarURL != "" {
bodyJSON.Set("avatar_url", tmpl(d.settings.AvatarURL))
msg.AvatarURL = tmpl(d.settings.AvatarURL)
if tmplErr != nil {
d.log.Warn("failed to template Discord Avatar URL", "error", tmplErr.Error(), "fallback", d.settings.AvatarURL)
bodyJSON.Set("avatar_url", d.settings.AvatarURL)
msg.AvatarURL = d.settings.AvatarURL
tmplErr = nil
}
}
footer := map[string]interface{}{
"text": "Grafana v" + d.appVersion,
"icon_url": "https://grafana.com/static/assets/img/fav32.png",
footer := &discordFooter{
Text: "Grafana v" + d.appVersion,
IconURL: "https://grafana.com/static/assets/img/fav32.png",
}
linkEmbed := simplejson.New()
var linkEmbed discordLinkEmbed
linkEmbed.Set("title", tmpl(d.settings.Title))
linkEmbed.Title = tmpl(d.settings.Title)
if tmplErr != nil {
d.log.Warn("failed to template Discord notification title", "error", tmplErr.Error())
// Reset tmplErr for templating other fields.
tmplErr = nil
}
linkEmbed.Set("footer", footer)
linkEmbed.Set("type", "rich")
linkEmbed.Footer = footer
linkEmbed.Type = discordRichEmbed
color, _ := strconv.ParseInt(strings.TrimLeft(getAlertStatusColor(alerts.Status()), "#"), 16, 0)
linkEmbed.Set("color", color)
linkEmbed.Color = color
ruleURL := joinUrlPath(d.tmpl.ExternalURL.String(), "/alerting/list", d.log)
linkEmbed.Set("url", ruleURL)
linkEmbed.URL = ruleURL
embeds := []interface{}{linkEmbed}
embeds := []discordLinkEmbed{linkEmbed}
attachments := d.constructAttachments(ctx, as, DiscordMaxEmbeds-1)
attachments := d.constructAttachments(ctx, as, discordMaxEmbeds-1)
for _, a := range attachments {
color, _ := strconv.ParseInt(strings.TrimLeft(getAlertStatusColor(alerts.Status()), "#"), 16, 0)
embed := map[string]interface{}{
"image": map[string]interface{}{
"url": a.url,
embed := discordLinkEmbed{
Image: &discordImage{
URL: a.url,
},
"color": color,
"title": a.alertName,
Color: color,
Title: a.alertName,
}
embeds = append(embeds, embed)
}
bodyJSON.Set("embeds", embeds)
msg.Embeds = embeds
if tmplErr != nil {
d.log.Warn("failed to template Discord message", "error", tmplErr.Error())
@ -171,7 +218,7 @@ func (d DiscordNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
u = d.settings.WebhookURL
}
body, err := json.Marshal(bodyJSON)
body, err := json.Marshal(msg)
if err != nil {
return false, err
}

View File

@ -6,6 +6,7 @@ import (
"fmt"
"math/rand"
"net/url"
"strings"
"testing"
"github.com/grafana/alerting/alerting/notifier/channels"
@ -283,6 +284,36 @@ func TestDiscordNotifier(t *testing.T) {
},
expMsgError: nil,
},
{
name: "Should truncate too long messages",
settings: fmt.Sprintf(`{
"url": "http://localhost",
"use_discord_username": true,
"message": "%s"
}`, strings.Repeat("Y", discordMaxMessageLen+rand.Intn(100)+1)),
alerts: []*types.Alert{
{
Alert: model.Alert{
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"},
Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh"},
},
},
},
expMsg: map[string]interface{}{
"content": strings.Repeat("Y", discordMaxMessageLen-1) + "…",
"embeds": []interface{}{map[string]interface{}{
"color": 1.4037554e+07,
"footer": map[string]interface{}{
"icon_url": "https://grafana.com/static/assets/img/fav32.png",
"text": "Grafana v" + appVersion,
},
"title": "[FIRING:1] (val1)",
"url": "http://localhost/alerting/list",
"type": "rich",
}},
},
expMsgError: nil,
},
}
for _, c := range cases {