Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions receivers/discord/v1/discord.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ type discordEmbedType string
const (
discordRichEmbed discordEmbedType = "rich"

discordMaxEmbeds = 10
discordMaxMessageLen = 2000
discordMaxEmbeds = 10
// https://discord.com/developers/docs/resources/message#embed-object-embed-limits
discordMaxTitleLen = 256
// Note: Discord's 6000-char total embed limit is not enforced here because
// title (256) + description (4096) + footer (~30) stays well under 6000.
discordMaxTitleLen = 256
discordMaxDescriptionLen = 4096
)

type discordMessage struct {
Expand All @@ -44,10 +46,11 @@ type discordMessage struct {

// 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"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Type discordEmbedType `json:"type,omitempty"`
URL string `json:"url,omitempty"`
Color int64 `json:"color,omitempty"`

Footer *discordFooter `json:"footer,omitempty"`

Expand Down Expand Up @@ -117,16 +120,15 @@ func (d Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
var tmplErr error
tmpl, _ := templates.TmplText(ctx, d.tmpl, as, l, &tmplErr)

msg.Content = tmpl(d.settings.Message)
messageText := tmpl(d.settings.Message)
if tmplErr != nil {
level.Warn(l).Log("msg", "failed to template Discord notification content", "err", tmplErr.Error())
// Reset tmplErr for templating other fields.
tmplErr = nil
}
truncatedMsg, truncated := receivers.TruncateInRunes(msg.Content, discordMaxMessageLen)
messageText, truncated := receivers.TruncateInRunes(messageText, discordMaxDescriptionLen)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Embed total may exceed Discord's 6000 character limit

Medium Severity

Moving the message from the top-level content field (which doesn't count toward embed limits) into the embed Description field (which does) means the message text now contributes to Discord's 6000-character total across all embeds. With discordMaxDescriptionLen at 4096, plus discordMaxTitleLen at 256, footer text, and up to 9 image embed titles (each up to 256 chars), the theoretical maximum is ~6681 characters — exceeding 6000. Discord will reject such messages. Previously, the content field was outside embeds so this limit couldn't be hit.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c50833f. Configure here.

if truncated {
level.Warn(l).Log("msg", "Truncated content", "key", key, "max_runes", discordMaxMessageLen)
msg.Content = truncatedMsg
level.Warn(l).Log("msg", "Truncated content", "key", key, "max_runes", discordMaxDescriptionLen)
}

if d.settings.AvatarURL != "" {
Expand Down Expand Up @@ -156,6 +158,7 @@ func (d Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
level.Warn(l).Log("msg", "Truncated title", "key", key, "max_runes", discordMaxTitleLen)
}

linkEmbed.Description = messageText
linkEmbed.Footer = footer
linkEmbed.Type = discordRichEmbed

Expand Down
91 changes: 58 additions & 33 deletions receivers/discord/v1/discord_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,10 @@ func TestNotify(t *testing.T) {
},
},
expMsg: map[string]interface{}{
"content": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
"content": "",
"embeds": []interface{}{map[string]interface{}{
"color": 1.4037554e+07,
"color": 1.4037554e+07,
"description": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
"footer": map[string]interface{}{
"icon_url": "https://grafana.com/static/assets/img/fav32.png",
"text": "Grafana v" + appVersion,
Expand Down Expand Up @@ -94,9 +95,10 @@ func TestNotify(t *testing.T) {
},
},
expMsg: map[string]interface{}{
"content": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
"content": "",
"embeds": []interface{}{map[string]interface{}{
"color": 1.4037554e+07,
"color": 1.4037554e+07,
"description": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
"footer": map[string]interface{}{
"icon_url": "https://grafana.com/static/assets/img/fav32.png",
"text": "Grafana v" + appVersion,
Expand Down Expand Up @@ -127,9 +129,10 @@ func TestNotify(t *testing.T) {
},
},
expMsg: map[string]any{
"content": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
"content": "",
"embeds": []any{map[string]any{
"color": 1.4037554e+07,
"color": 1.4037554e+07,
"description": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
"footer": map[string]any{
"icon_url": "https://grafana.com/static/assets/img/fav32.png",
"text": "Grafana v" + appVersion,
Expand Down Expand Up @@ -161,9 +164,10 @@ func TestNotify(t *testing.T) {
},
expMsg: map[string]interface{}{
"avatar_url": "https://grafana.com/static/assets/img/fav32.png",
"content": "I'm a custom template ",
"content": "",
"embeds": []interface{}{map[string]interface{}{
"color": 1.4037554e+07,
"color": 1.4037554e+07,
"description": "I'm a custom template ",
"footer": map[string]interface{}{
"icon_url": "https://grafana.com/static/assets/img/fav32.png",
"text": "Grafana v" + appVersion,
Expand Down Expand Up @@ -229,9 +233,10 @@ func TestNotify(t *testing.T) {
},
expMsg: map[string]interface{}{
"avatar_url": "{{ invalid } }}",
"content": "valid message",
"content": "",
"embeds": []interface{}{map[string]interface{}{
"color": 1.4037554e+07,
"color": 1.4037554e+07,
"description": "valid message",
"footer": map[string]interface{}{
"icon_url": "https://grafana.com/static/assets/img/fav32.png",
"text": "Grafana v" + appVersion,
Expand Down Expand Up @@ -263,9 +268,10 @@ func TestNotify(t *testing.T) {
},
expMsg: map[string]interface{}{
"avatar_url": "https://grafana.com/static/assets/img/fav32.png",
"content": "valid message",
"content": "",
"embeds": []interface{}{map[string]interface{}{
"color": 1.4037554e+07,
"color": 1.4037554e+07,
"description": "valid message",
"footer": map[string]interface{}{
"icon_url": "https://grafana.com/static/assets/img/fav32.png",
"text": "Grafana v" + appVersion,
Expand Down Expand Up @@ -302,9 +308,10 @@ func TestNotify(t *testing.T) {
},
expMsg: map[string]interface{}{
"avatar_url": "https://grafana.com/static/assets/img/fav32.png",
"content": "2 alerts are firing, 0 are resolved",
"content": "",
"embeds": []interface{}{map[string]interface{}{
"color": 1.4037554e+07,
"color": 1.4037554e+07,
"description": "2 alerts are firing, 0 are resolved",
"footer": map[string]interface{}{
"icon_url": "https://grafana.com/static/assets/img/fav32.png",
"text": "Grafana v" + appVersion,
Expand Down Expand Up @@ -335,9 +342,10 @@ func TestNotify(t *testing.T) {
},
},
expMsg: map[string]interface{}{
"content": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
"content": "",
"embeds": []interface{}{map[string]interface{}{
"color": 1.4037554e+07,
"color": 1.4037554e+07,
"description": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
"footer": map[string]interface{}{
"icon_url": "https://grafana.com/static/assets/img/fav32.png",
"text": "Grafana v" + appVersion,
Expand All @@ -353,7 +361,7 @@ func TestNotify(t *testing.T) {
name: "Should truncate too long messages",
settings: Config{
Title: templates.DefaultMessageTitleEmbed,
Message: strings.Repeat("Y", discordMaxMessageLen+rand.Intn(100)+1),
Message: strings.Repeat("Y", discordMaxDescriptionLen+rand.Intn(100)+1),
AvatarURL: "",
WebhookURL: "http://localhost",
UseDiscordUsername: true,
Expand All @@ -367,9 +375,10 @@ func TestNotify(t *testing.T) {
},
},
expMsg: map[string]interface{}{
"content": strings.Repeat("Y", discordMaxMessageLen-1) + "…",
"content": "",
"embeds": []interface{}{map[string]interface{}{
"color": 1.4037554e+07,
"color": 1.4037554e+07,
"description": strings.Repeat("Y", discordMaxDescriptionLen-1) + "…",
"footer": map[string]interface{}{
"icon_url": "https://grafana.com/static/assets/img/fav32.png",
"text": "Grafana v" + appVersion,
Expand Down Expand Up @@ -471,9 +480,10 @@ func TestNotify_WithImages(t *testing.T) {
},
},
expMsg: map[string]interface{}{
"content": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
"content": "",
"embeds": []interface{}{map[string]interface{}{
"color": 1.4037554e+07,
"color": 1.4037554e+07,
"description": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
"footer": map[string]interface{}{
"icon_url": "https://grafana.com/static/assets/img/fav32.png",
"text": "Grafana v" + appVersion,
Expand Down Expand Up @@ -511,9 +521,10 @@ func TestNotify_WithImages(t *testing.T) {
},
},
expMsg: map[string]interface{}{
"content": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
"content": "",
"embeds": []interface{}{map[string]interface{}{
"color": 1.4037554e+07,
"color": 1.4037554e+07,
"description": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
"footer": map[string]interface{}{
"icon_url": "https://grafana.com/static/assets/img/fav32.png",
"text": "Grafana v" + appVersion,
Expand Down Expand Up @@ -559,7 +570,11 @@ func TestNotify_WithImages(t *testing.T) {
},
},
expMsg: map[string]interface{}{
"content": `**Firing**
"content": "",
"embeds": []interface{}{
map[string]interface{}{
"color": 1.4037554e+07,
"description": `**Firing**

Value: [no value]
Labels:
Expand All @@ -578,9 +593,6 @@ Labels:
Annotations:
Silence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1
`,
"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,
Expand Down Expand Up @@ -625,7 +637,11 @@ Silence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=aler
},
},
expMsg: map[string]interface{}{
"content": `**Firing**
"content": "",
"embeds": []interface{}{
map[string]interface{}{
"color": 1.4037554e+07,
"description": `**Firing**

Value: [no value]
Labels:
Expand All @@ -642,9 +658,6 @@ Labels:
Annotations:
Silence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert2&matcher=lbl1%3Dval2
`,
"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,
Expand Down Expand Up @@ -690,10 +703,11 @@ Silence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=aler
},
},
expMsg: map[string]interface{}{
"content": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = " + strings.Repeat("B", discordMaxTitleLen+1) + "\n - lbl1 = val1\nAnnotations:\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3D" + strings.Repeat("B", discordMaxTitleLen+1) + "&matcher=lbl1%3Dval1\n",
"content": "",
"embeds": []interface{}{
map[string]interface{}{
"color": 1.4037554e+07,
"color": 1.4037554e+07,
"description": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = " + strings.Repeat("B", discordMaxTitleLen+1) + "\n - lbl1 = val1\nAnnotations:\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3D" + strings.Repeat("B", discordMaxTitleLen+1) + "&matcher=lbl1%3Dval1\n",
"footer": map[string]interface{}{
"icon_url": "https://grafana.com/static/assets/img/fav32.png",
"text": "Grafana v" + appVersion,
Expand Down Expand Up @@ -797,6 +811,9 @@ Silence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=aler
return strings.Compare(a.Name(), b.Name())
})

// The description will contain the rendered default message template for all 15 alerts.
// We don't check the exact content here, just that the embed structure is correct.
// Extract the description from the actual payload later for comparison.
expEmbeds := []interface{}{
map[string]interface{}{
"color": 1.4037554e+07,
Expand Down Expand Up @@ -854,6 +871,14 @@ Silence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=aler
embeds, ok := payloadMap["embeds"]
require.True(tt, ok)
require.Len(tt, embeds, 10)

// The first embed contains the description (message text). Remove it for structural comparison
// since the exact content depends on dynamically generated alert data.
embedsList := embeds.([]interface{})
firstEmbed := embedsList[0].(map[string]interface{})
require.NotEmpty(tt, firstEmbed["description"], "first embed should have a description")
delete(firstEmbed, "description")

require.Equal(tt, expEmbeds, embeds)
})
}