mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
aaa55b4252
commit
4a3097f52a
@ -13,13 +13,53 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/alerting/alerting/notifier/channels"
|
"github.com/grafana/alerting/alerting/notifier/channels"
|
||||||
|
"github.com/prometheus/alertmanager/notify"
|
||||||
"github.com/prometheus/alertmanager/template"
|
"github.com/prometheus/alertmanager/template"
|
||||||
"github.com/prometheus/alertmanager/types"
|
"github.com/prometheus/alertmanager/types"
|
||||||
"github.com/prometheus/common/model"
|
"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 {
|
type DiscordNotifier struct {
|
||||||
*channels.Base
|
*channels.Base
|
||||||
log channels.Logger
|
log channels.Logger
|
||||||
@ -64,8 +104,6 @@ type discordAttachment struct {
|
|||||||
state model.AlertStatus
|
state model.AlertStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
const DiscordMaxEmbeds = 10
|
|
||||||
|
|
||||||
func DiscordFactory(fc channels.FactoryConfig) (channels.NotificationChannel, error) {
|
func DiscordFactory(fc channels.FactoryConfig) (channels.NotificationChannel, error) {
|
||||||
dn, err := newDiscordNotifier(fc)
|
dn, err := newDiscordNotifier(fc)
|
||||||
if err != nil {
|
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) {
|
func (d DiscordNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
|
||||||
alerts := types.Alerts(as...)
|
alerts := types.Alerts(as...)
|
||||||
|
|
||||||
bodyJSON := simplejson.New()
|
var msg discordMessage
|
||||||
|
|
||||||
if !d.settings.UseDiscordUsername {
|
if !d.settings.UseDiscordUsername {
|
||||||
bodyJSON.Set("username", "Grafana")
|
msg.Username = "Grafana"
|
||||||
}
|
}
|
||||||
|
|
||||||
var tmplErr error
|
var tmplErr error
|
||||||
tmpl, _ := channels.TmplText(ctx, d.tmpl, as, d.log, &tmplErr)
|
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 {
|
if tmplErr != nil {
|
||||||
d.log.Warn("failed to template Discord notification content", "error", tmplErr.Error())
|
d.log.Warn("failed to template Discord notification content", "error", tmplErr.Error())
|
||||||
// Reset tmplErr for templating other fields.
|
// Reset tmplErr for templating other fields.
|
||||||
tmplErr = nil
|
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 != "" {
|
if d.settings.AvatarURL != "" {
|
||||||
bodyJSON.Set("avatar_url", tmpl(d.settings.AvatarURL))
|
msg.AvatarURL = tmpl(d.settings.AvatarURL)
|
||||||
if tmplErr != nil {
|
if tmplErr != nil {
|
||||||
d.log.Warn("failed to template Discord Avatar URL", "error", tmplErr.Error(), "fallback", d.settings.AvatarURL)
|
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
|
tmplErr = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
footer := map[string]interface{}{
|
footer := &discordFooter{
|
||||||
"text": "Grafana v" + d.appVersion,
|
Text: "Grafana v" + d.appVersion,
|
||||||
"icon_url": "https://grafana.com/static/assets/img/fav32.png",
|
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 {
|
if tmplErr != nil {
|
||||||
d.log.Warn("failed to template Discord notification title", "error", tmplErr.Error())
|
d.log.Warn("failed to template Discord notification title", "error", tmplErr.Error())
|
||||||
// Reset tmplErr for templating other fields.
|
// Reset tmplErr for templating other fields.
|
||||||
tmplErr = nil
|
tmplErr = nil
|
||||||
}
|
}
|
||||||
linkEmbed.Set("footer", footer)
|
linkEmbed.Footer = footer
|
||||||
linkEmbed.Set("type", "rich")
|
linkEmbed.Type = discordRichEmbed
|
||||||
|
|
||||||
color, _ := strconv.ParseInt(strings.TrimLeft(getAlertStatusColor(alerts.Status()), "#"), 16, 0)
|
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)
|
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 {
|
for _, a := range attachments {
|
||||||
color, _ := strconv.ParseInt(strings.TrimLeft(getAlertStatusColor(alerts.Status()), "#"), 16, 0)
|
color, _ := strconv.ParseInt(strings.TrimLeft(getAlertStatusColor(alerts.Status()), "#"), 16, 0)
|
||||||
embed := map[string]interface{}{
|
embed := discordLinkEmbed{
|
||||||
"image": map[string]interface{}{
|
Image: &discordImage{
|
||||||
"url": a.url,
|
URL: a.url,
|
||||||
},
|
},
|
||||||
"color": color,
|
Color: color,
|
||||||
"title": a.alertName,
|
Title: a.alertName,
|
||||||
}
|
}
|
||||||
embeds = append(embeds, embed)
|
embeds = append(embeds, embed)
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyJSON.Set("embeds", embeds)
|
msg.Embeds = embeds
|
||||||
|
|
||||||
if tmplErr != nil {
|
if tmplErr != nil {
|
||||||
d.log.Warn("failed to template Discord message", "error", tmplErr.Error())
|
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
|
u = d.settings.WebhookURL
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := json.Marshal(bodyJSON)
|
body, err := json.Marshal(msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/grafana/alerting/alerting/notifier/channels"
|
"github.com/grafana/alerting/alerting/notifier/channels"
|
||||||
@ -283,6 +284,36 @@ func TestDiscordNotifier(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expMsgError: nil,
|
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 {
|
for _, c := range cases {
|
||||||
|
Loading…
Reference in New Issue
Block a user