@ -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"
)
// Constants and models are set according to the official documentation https://discord.com/developers/docs/resources/webhook#execute-webhook-jsonform-params
type discordEmbedType string
"github.com/grafana/grafana/pkg/components/simplejson"
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 , D iscordMaxEmbeds- 1 )
attachments := d . constructAttachments ( ctx , as , d iscordMaxEmbeds- 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
}