From 550952eb51be17ab9cb5c45b57191efd9a0da8cb Mon Sep 17 00:00:00 2001 From: Matthias Loibl Date: Fri, 10 Jun 2022 16:17:14 +0200 Subject: [PATCH] notify/discord: Create Discord integration Signed-off-by: Matthias Loibl --- asset/assets_vfsdata.go | 4 +- cmd/alertmanager/main.go | 5 ++ config/config.go | 12 +++ config/notifiers.go | 27 +++++++ notify/discord/discord.go | 133 +++++++++++++++++++++++++++++++++ notify/discord/discord_test.go | 129 ++++++++++++++++++++++++++++++++ template/default.tmpl | 14 +++- 7 files changed, 321 insertions(+), 3 deletions(-) create mode 100644 notify/discord/discord.go create mode 100644 notify/discord/discord_test.go diff --git a/asset/assets_vfsdata.go b/asset/assets_vfsdata.go index ff127179..2e60f700 100644 --- a/asset/assets_vfsdata.go +++ b/asset/assets_vfsdata.go @@ -163,9 +163,9 @@ var Assets = func() http.FileSystem { "/templates/default.tmpl": &vfsgen۰CompressedFileInfo{ name: "default.tmpl", modTime: time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC), - uncompressedSize: 4554, + uncompressedSize: 4905, - compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\xec\x57\xc1\x6e\xe3\x38\x0c\xbd\xe7\x2b\x08\xef\xa5\x39\xc4\xdd\x73\x81\x62\x51\x2c\x76\xe7\x52\x0c\x06\x29\x32\x97\xc1\xc0\x50\x6d\xc6\x55\x2b\x4b\xae\x44\xa7\x0d\x1c\xff\xfb\x40\xb6\x9b\x58\x91\x9d\xda\x41\xe7\x34\xbd\xd5\x2c\xf9\x48\x3d\x3e\x91\x4a\x59\x42\x82\x6b\x2e\x11\x82\x28\x62\x02\x35\x65\x4c\xb2\x14\x75\x00\x55\x75\xd3\xf9\x2e\x4b\x40\x99\x40\x55\xcd\x06\x43\x56\xcb\x5b\x1b\x55\x96\x10\xfe\xf7\x4a\xa8\x25\x13\xab\xe5\x2d\x54\xd5\xe5\x5f\x97\xb5\x9f\xf9\x47\x63\x8c\x7c\x83\xfa\xda\x3a\x2d\xdb\x0f\xd8\x41\xa1\xc5\x73\x81\x7a\xdb\x84\xb7\x89\xdc\x4c\xa6\xb8\x7f\xc4\x98\x6c\x86\x1f\x36\xfa\x8e\x18\x15\x06\x76\x40\x6a\x95\xe7\xa8\x9b\x50\xbe\x06\x7c\xde\xff\x33\x58\x73\xcd\x65\x6a\x63\xae\x6c\x4c\x7d\x20\x13\xfe\x5f\x5b\x61\x07\x02\x65\x37\xe3\x4f\xb0\x4e\x5f\xb4\x2a\xf2\x5b\x76\x8f\xc2\x84\x77\x4a\x13\x26\xdf\x18\xd7\x26\xfc\xce\x44\x81\x36\xe1\xa3\xe2\x12\x02\xb0\xa8\xd0\xa4\x4c\x09\x2e\x2c\x56\xf8\xaf\xca\x32\x25\x9b\xe0\x79\x6b\xeb\xe0\xcd\xa1\xaa\x2e\xca\x12\x5e\x38\x3d\xb8\xce\xe1\x12\x33\xb5\x41\x37\xfb\x57\x96\xa1\x69\x19\xed\xcb\xbe\x2f\x7c\xbe\xff\x6b\xa0\x4d\x09\x9a\x58\xf3\x9c\xb8\x92\xc1\x09\x8e\x09\x5f\xa9\x69\x69\x24\xb8\xa1\xd6\x55\x33\x99\x22\x84\x50\x55\x4d\x5d\x57\xb3\x83\xd1\xe7\xc9\xb2\xb2\xa8\x89\xb4\xe5\xdb\xaf\x6b\xd8\x1f\xa0\x2d\xac\x49\x7e\x23\xa5\x22\x66\x6b\x72\x20\x3b\xe6\xf3\x70\xef\x54\xa1\x63\xbc\x6a\x9a\x89\x12\x35\x23\xa5\x1b\x25\xce\x7a\x88\x72\x38\x30\x82\xc5\x4f\x61\x82\x6b\x56\x08\x0a\x89\x93\xc0\x96\x05\xc2\x2c\x17\x8c\x5c\x2d\x86\x43\x94\xbb\x38\x85\xb1\xb7\x21\xeb\x83\x72\xef\xdc\x48\xbc\x35\x13\xe2\x9e\xc5\x4f\x1e\x5e\x6f\xf9\x16\x14\x76\xf0\x9e\xa3\xe0\xf2\x69\x74\x05\x71\x5b\x01\x4f\x82\x71\x01\xb9\x46\xab\xae\x91\xde\x9d\x82\x4e\x32\x56\x8f\x9c\x91\x25\xf3\x58\x49\xcc\xd4\x23\x0f\xc6\xfb\x17\x5a\x8c\xad\x78\xfc\xe1\xd6\x4a\x51\x33\x60\x07\x44\x98\xdb\xa3\x25\x05\x6d\xf7\x21\xfe\xfd\x9d\x26\x47\x1f\x31\x16\x1c\x25\x9d\x2f\xc8\x21\xc4\xc3\x12\x38\xaf\x67\x3e\x2e\x97\x86\x98\x8c\xd1\xf4\xe0\x7a\x03\x2b\x1c\x66\x55\xe5\x26\x45\xc9\x71\x0f\x9c\xa1\x31\x2c\x3d\xef\x7e\x7b\x60\x7e\x87\xda\xf9\x3e\x30\xce\x7a\x07\xfa\xec\x68\x9d\x38\xfb\x6a\x0e\x7f\xc3\xa2\xaa\x66\x8d\x11\x1a\x63\x3d\x38\x4f\x33\xe2\x2e\xbd\x3a\xc9\xa2\x73\xa2\x9e\x7c\x4b\x34\x4a\x6c\x30\x39\xca\xf8\x66\x1e\x9f\xf3\x2d\xc2\xcb\xba\x18\x43\xa9\xa9\xe7\xf8\x74\x35\x39\x5d\x7f\xc1\xf8\x81\xd1\xd4\x9e\xcf\x3e\xfb\x77\xa2\x7f\xdd\x77\xe1\x4a\x0b\x0f\xaf\xb7\x3f\x03\x5d\x3f\xea\x0f\xa9\xc8\x2e\xcb\xc1\x49\xea\xbb\xe7\x4c\xd3\x76\x82\x3f\xb1\x74\xac\x37\x4b\x51\x52\x74\xbc\xe2\x5c\x7d\x6d\x78\x4c\x4a\xab\xdc\x1c\x64\x4b\x8c\x30\x72\x85\xf6\xa9\xa5\x69\xb3\xc0\x67\x15\x25\x71\xda\x46\x09\x37\xb9\x60\xdb\x68\xe0\x35\xf5\xfe\xe0\xf6\x91\x33\x25\x39\x29\x4b\x48\x44\x4a\x89\x89\x2b\xd1\xd9\x5d\x85\x79\x50\x1b\xd4\x1f\xf0\x7e\xf4\xa0\x7e\xbf\x9e\x3e\x46\x4e\xe3\xd5\xf4\x71\x62\xea\xe4\x1c\xc1\xe4\xe1\x4d\x37\x65\xa7\x74\x5f\x73\xb2\x73\xd9\x0f\xbf\x4a\xa7\xff\x46\xe8\xe0\x7c\xb6\x77\x4a\x7b\xbb\x2c\x12\x0a\x4c\x35\xcb\xfa\xa8\xfc\x93\x48\xf9\x15\x00\x00\xff\xff\xf5\xfe\x1f\x22\xca\x11\x00\x00"), + compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\xec\x57\x41\x6f\xdb\x3a\x0c\xbe\xe7\x57\x10\x79\x97\xe6\x10\xf7\x9d\x0b\x14\x0f\xc5\xc3\xb6\x4b\x31\x0c\x29\xb2\xcb\x30\x18\xaa\xcd\xb8\x6a\x65\xc9\x95\xe8\xb4\x81\xe3\xff\x3e\xc8\x76\x13\x2b\xb2\x53\x3b\xc8\x4e\xed\xad\x66\xc9\x8f\xd4\xc7\x4f\xa4\x52\x14\x10\xe3\x8a\x4b\x84\x69\x18\x32\x81\x9a\x52\x26\x59\x82\x7a\x0a\x65\x79\xd3\xfa\x2e\x0a\x40\x19\x43\x59\x4e\x7a\x43\x96\x8b\x5b\x1b\x55\x14\x10\x7c\x79\x25\xd4\x92\x89\xe5\xe2\x16\xca\xf2\xf2\x9f\xcb\xca\xcf\xfc\xa7\x31\x42\xbe\x46\x7d\x6d\x9d\x16\xcd\x07\x6c\x21\xd7\xe2\x39\x47\xbd\xa9\xc3\x9b\x44\x6e\x26\x93\xdf\x3f\x62\x44\x36\xc3\x2f\x1b\x7d\x47\x8c\x72\x03\x5b\x20\xb5\xcc\x32\xd4\x75\x28\x5f\x01\x3e\xef\xfe\x39\x5d\x71\xcd\x65\x62\x63\xae\x6c\x4c\x75\x20\x13\x7c\xad\xac\xb0\x05\x81\xb2\x9d\xf1\x37\x58\xa7\x6f\x5a\xe5\xd9\x2d\xbb\x47\x61\x82\x3b\xa5\x09\xe3\x1f\x8c\x6b\x13\xfc\x64\x22\x47\x9b\xf0\x51\x71\x09\x53\xb0\xa8\x50\xa7\x4c\x08\x2e\x2c\x56\xf0\xbf\x4a\x53\x25\xeb\xe0\x59\x63\x6b\xe1\xcd\xa0\x2c\x2f\x8a\x02\x5e\x38\x3d\xb8\xce\xc1\x02\x53\xb5\x46\x37\xfb\x77\x96\xa2\x69\x18\xed\xca\xbe\x2b\x7c\xb6\xfb\xab\xa7\x4d\x31\x9a\x48\xf3\x8c\xb8\x92\xd3\x23\x1c\x13\xbe\x52\xdd\xd2\x50\x70\x43\x8d\xab\x66\x32\x41\x08\xa0\x2c\xeb\xba\xae\x26\x7b\xa3\xcf\x93\x65\x65\x5e\x11\x69\xcb\xb7\x5f\xd7\xb0\x3b\x40\x53\x58\x9d\xfc\x46\x4a\x45\xcc\xd6\xe4\x40\xb6\xcc\xa7\xe1\xde\xa9\x5c\x47\x78\x55\x37\x13\x25\x6a\x46\x4a\xd7\x4a\x9c\x74\x10\xe5\x70\x60\x04\x8b\x9e\x82\x18\x57\x2c\x17\x14\x10\x27\x81\x0d\x0b\x84\x69\x26\x18\xb9\x5a\x0c\xfa\x28\x77\x71\x72\x63\x6f\x43\xda\x05\xe5\xde\xb9\x81\x78\x2b\x26\xc4\x3d\x8b\x9e\x3c\xbc\xce\xf2\x2d\x28\x6c\xe1\x3d\x47\xc1\xe5\xd3\xe0\x0a\xa2\xa6\x02\x1e\x4f\x87\x05\x64\x1a\xad\xba\x06\x7a\xb7\x0a\x3a\xca\x58\x35\x72\x06\x96\xcc\x23\x25\x31\x55\x8f\x7c\x3a\xdc\x3f\xd7\x62\x68\xc5\xc3\x0f\xb7\x52\x8a\xea\x01\xdb\x23\xc2\xcc\x1e\x2d\xce\x69\xb3\x0b\xf1\xef\xef\x38\x39\xfa\x88\x91\xe0\x28\xe9\x74\x41\xf6\x21\xee\x97\xc0\x69\x3d\xf3\x71\xb9\x34\xc4\x64\x84\xa6\x03\xd7\x1b\x58\x41\x3f\xab\x2a\x33\x09\x4a\x8e\x3b\xe0\x14\x8d\x61\xc9\x69\xf7\xdb\x03\xf3\x3b\xd4\xcc\xf7\x9e\x71\xd6\x39\xd0\x27\x07\xeb\xc4\xd9\x57\x33\xf8\x17\xe6\x65\x39\xa9\x8d\x50\x1b\xab\xc1\x79\x9c\x11\x77\xe9\x55\x49\xe6\xad\x13\x75\xe4\x5b\xa0\x51\x62\x8d\xf1\x41\xc6\x37\xf3\xf0\x9c\x6f\x11\x5e\xd6\xf9\x10\x4a\x4d\x35\xc7\xc7\xab\xc9\xe9\xfa\x0b\x46\x0f\x8c\xc6\xf6\x7c\xf2\xd9\xbf\x23\xfd\x6b\xbf\x0b\x97\x5a\x78\x78\x9d\xfd\xe9\xe9\xfa\x41\x7f\x48\x85\x76\x59\xf6\x4e\x52\xdf\x3d\x63\x9a\x36\x23\xfc\x89\x25\x43\xbd\x59\x82\x92\xc2\xc3\x15\xe7\xea\x6b\xcd\x23\x52\x5a\x65\x66\x2f\x5b\x62\x84\xa1\x2b\xb4\x4f\x2d\x8d\x9b\x05\x3e\xab\x28\x89\xd3\x26\x8c\xb9\xc9\x04\xdb\x84\x3d\xaf\xa9\xf7\x07\xb7\x8f\x9c\x2a\xc9\x49\x59\x42\x42\x52\x4a\x8c\x5c\x89\xce\xee\xca\xcd\x83\x5a\xa3\x3e\xc3\xfb\xd1\x83\xfa\xfb\x7a\x3a\x8f\x9c\x86\xab\xe9\x7c\x62\x6a\xe5\x1c\xc0\xe4\xfe\x4d\x37\x66\xa7\xb4\x5f\x73\xb2\x75\xd9\xf7\xbf\x4a\xc7\xff\x46\x68\xe1\x7c\xb6\x77\x4c\x7b\xdb\x2c\x12\x0a\x4c\x34\x4b\xbb\xa8\xfc\xb0\xa4\xc4\xdc\x44\x4a\xc7\x67\x18\x44\x87\x48\x1f\x95\xdd\x3f\x01\x00\x00\xff\xff\xcf\xc2\xdd\x36\x29\x13\x00\x00"), }, "/templates/email.tmpl": &vfsgen۰CompressedFileInfo{ name: "email.tmpl", diff --git a/cmd/alertmanager/main.go b/cmd/alertmanager/main.go index d8d3ddfe..2879ada8 100644 --- a/cmd/alertmanager/main.go +++ b/cmd/alertmanager/main.go @@ -49,6 +49,7 @@ import ( "github.com/prometheus/alertmanager/inhibit" "github.com/prometheus/alertmanager/nflog" "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/notify/discord" "github.com/prometheus/alertmanager/notify/email" "github.com/prometheus/alertmanager/notify/opsgenie" "github.com/prometheus/alertmanager/notify/pagerduty" @@ -173,6 +174,10 @@ func buildReceiverIntegrations(nc *config.Receiver, tmpl *template.Template, log for i, c := range nc.TelegramConfigs { add("telegram", i, c, func(l log.Logger) (notify.Notifier, error) { return telegram.New(c, tmpl, l) }) } + for i, c := range nc.DiscordConfigs { + add("discord", i, c, func(l log.Logger) (notify.Notifier, error) { return discord.New(c, tmpl, l) }) + } + if errs.Len() > 0 { return nil, &errs } diff --git a/config/config.go b/config/config.go index 4d1e76c7..c62366ae 100644 --- a/config/config.go +++ b/config/config.go @@ -248,6 +248,9 @@ func resolveFilepaths(baseDir string, cfg *Config) { for _, cfg := range receiver.TelegramConfigs { cfg.HTTPConfig.SetDirectory(baseDir) } + for _, cfg := range receiver.DiscordConfigs { + cfg.HTTPConfig.SetDirectory(baseDir) + } } } @@ -492,6 +495,14 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { telegram.APIUrl = c.Global.TelegramAPIUrl } } + for _, discord := range rcv.DiscordConfigs { + if discord.HTTPConfig == nil { + discord.HTTPConfig = c.Global.HTTPConfig + } + if discord.WebhookURL == nil { + return fmt.Errorf("no discord webhook URL provided") + } + } names[rcv.Name] = struct{}{} } @@ -844,6 +855,7 @@ type Receiver struct { // A unique identifier for this receiver. Name string `yaml:"name" json:"name"` + DiscordConfigs []*DiscordConfig `yaml:"discord_configs,omitempty" json:"discord_configs,omitempty"` EmailConfigs []*EmailConfig `yaml:"email_configs,omitempty" json:"email_configs,omitempty"` PagerdutyConfigs []*PagerdutyConfig `yaml:"pagerduty_configs,omitempty" json:"pagerduty_configs,omitempty"` SlackConfigs []*SlackConfig `yaml:"slack_configs,omitempty" json:"slack_configs,omitempty"` diff --git a/config/notifiers.go b/config/notifiers.go index 682b38df..c6299595 100644 --- a/config/notifiers.go +++ b/config/notifiers.go @@ -32,6 +32,15 @@ var ( }, } + // DefaultDiscordConfig defines default values for Discord configurations. + DefaultDiscordConfig = DiscordConfig{ + NotifierConfig: NotifierConfig{ + VSendResolved: true, + }, + Title: `{{ template "discord.default.title" . }}`, + Message: `{{ template "discord.default.message" . }}`, + } + // DefaultEmailConfig defines default values for Email configurations. DefaultEmailConfig = EmailConfig{ NotifierConfig: NotifierConfig{ @@ -156,6 +165,24 @@ func (nc *NotifierConfig) SendResolved() bool { return nc.VSendResolved } +// DiscordConfig configures notifications via Discord. +type DiscordConfig struct { + NotifierConfig `yaml:",inline" json:",inline"` + + HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` + WebhookURL *SecretURL `yaml:"webhook_url,omitempty" json:"webhook_url,omitempty"` + + Title string `yaml:"title,omitempty" json:"title,omitempty"` + Message string `yaml:"message,omitempty" json:"message,omitempty"` +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (c *DiscordConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + *c = DefaultDiscordConfig + type plain DiscordConfig + return unmarshal((*plain)(c)) +} + // EmailConfig configures notifications via mail. type EmailConfig struct { NotifierConfig `yaml:",inline" json:",inline"` diff --git a/notify/discord/discord.go b/notify/discord/discord.go new file mode 100644 index 00000000..31a0a7cf --- /dev/null +++ b/notify/discord/discord.go @@ -0,0 +1,133 @@ +// Copyright 2021 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package discord + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + + "github.com/go-kit/log" + "github.com/go-kit/log/level" + commoncfg "github.com/prometheus/common/config" + "github.com/prometheus/common/model" + + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/template" + "github.com/prometheus/alertmanager/types" +) + +const ( + colorRed = 0x992D22 + colorGreen = 0x2ECC71 + colorGrey = 0x95A5A6 +) + +// Notifier implements a Notifier for Discord notifications. +type Notifier struct { + conf *config.DiscordConfig + tmpl *template.Template + logger log.Logger + client *http.Client + retrier *notify.Retrier + webhookURL *config.SecretURL +} + +// New returns a new Discord notifier. +func New(c *config.DiscordConfig, t *template.Template, l log.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) { + client, err := commoncfg.NewClientFromConfig(*c.HTTPConfig, "discord", httpOpts...) + if err != nil { + return nil, err + } + n := &Notifier{ + conf: c, + tmpl: t, + logger: l, + client: client, + retrier: ¬ify.Retrier{}, + webhookURL: c.WebhookURL, + } + return n, nil +} + +type webhook struct { + Content string `json:"content"` + Embeds []webhookEmbed `json:"embeds"` +} + +type webhookEmbed struct { + Title string `json:"title"` + Description string `json:"description"` + Color int `json:"color"` +} + +// Notify implements the Notifier interface. +func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { + key, err := notify.ExtractGroupKey(ctx) + if err != nil { + return false, err + } + + level.Debug(n.logger).Log("incident", key) + + alerts := types.Alerts(as...) + data := notify.GetTemplateData(ctx, n.tmpl, as, n.logger) + tmpl := notify.TmplText(n.tmpl, data, &err) + if err != nil { + return false, err + } + + title := tmpl(n.conf.Title) + if err != nil { + return false, err + } + description := tmpl(n.conf.Message) + if err != nil { + return false, err + } + + color := colorGrey + if alerts.Status() == model.AlertFiring { + color = colorRed + } + if alerts.Status() == model.AlertResolved { + color = colorGreen + } + + w := webhook{ + Embeds: []webhookEmbed{{ + Title: title, + Description: description, + Color: color, + }}, + } + + var payload bytes.Buffer + if err = json.NewEncoder(&payload).Encode(w); err != nil { + return false, err + } + + resp, err := notify.PostJSON(ctx, n.client, n.webhookURL.String(), &payload) + if err != nil { + return true, notify.RedactURL(err) + } + + shouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body) + if err != nil { + return shouldRetry, err + } + return false, nil +} diff --git a/notify/discord/discord_test.go b/notify/discord/discord_test.go new file mode 100644 index 00000000..142a9a60 --- /dev/null +++ b/notify/discord/discord_test.go @@ -0,0 +1,129 @@ +// Copyright 2021 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package discord + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/go-kit/log" + commoncfg "github.com/prometheus/common/config" + "github.com/prometheus/common/model" + "github.com/stretchr/testify/require" + + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/notify/test" + "github.com/prometheus/alertmanager/types" +) + +// This is a test URL that has been modified to not be valid. +var testWebhookURL, _ = url.Parse("https://discord.com/api/webhooks/971139602272503183/78ZWZ4V3xwZUBKRFF-G9m1nRtDtNTChl_WzW6Q4kxShjSB02oLSiPTPa8TS2tTGO9EYf") + +func TestDiscordRetry(t *testing.T) { + notifier, err := New( + &config.DiscordConfig{ + WebhookURL: &config.SecretURL{URL: testWebhookURL}, + HTTPConfig: &commoncfg.HTTPClientConfig{}, + }, + test.CreateTmpl(t), + log.NewNopLogger(), + ) + require.NoError(t, err) + + for statusCode, expected := range test.RetryTests(test.DefaultRetryCodes()) { + actual, _ := notifier.retrier.Check(statusCode, nil) + require.Equal(t, expected, actual, fmt.Sprintf("retry - error on status %d", statusCode)) + } +} + +func TestDiscordTemplating(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + dec := json.NewDecoder(r.Body) + out := make(map[string]interface{}) + err := dec.Decode(&out) + if err != nil { + panic(err) + } + })) + defer srv.Close() + u, _ := url.Parse(srv.URL) + + for _, tc := range []struct { + title string + cfg *config.DiscordConfig + + retry bool + errMsg string + }{ + { + title: "full-blown message", + cfg: &config.DiscordConfig{ + Title: `{{ template "discord.default.title" . }}`, + Message: `{{ template "discord.default.message" . }}`, + }, + retry: false, + }, + { + title: "title with templating errors", + cfg: &config.DiscordConfig{ + Title: "{{ ", + }, + errMsg: "template: :1: unclosed action", + }, + { + title: "message with templating errors", + cfg: &config.DiscordConfig{ + Title: `{{ template "discord.default.title" . }}`, + Message: "{{ ", + }, + errMsg: "template: :1: unclosed action", + }, + } { + t.Run(tc.title, func(t *testing.T) { + tc.cfg.WebhookURL = &config.SecretURL{URL: u} + tc.cfg.HTTPConfig = &commoncfg.HTTPClientConfig{} + pd, err := New(tc.cfg, test.CreateTmpl(t), log.NewNopLogger()) + require.NoError(t, err) + + ctx := context.Background() + ctx = notify.WithGroupKey(ctx, "1") + + ok, err := pd.Notify(ctx, []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{ + "lbl1": "val1", + }, + StartsAt: time.Now(), + EndsAt: time.Now().Add(time.Hour), + }, + }, + }...) + if tc.errMsg == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errMsg) + } + require.Equal(t, tc.retry, ok) + }) + } +} diff --git a/template/default.tmpl b/template/default.tmpl index 4ce5de70..dd669743 100644 --- a/template/default.tmpl +++ b/template/default.tmpl @@ -111,4 +111,16 @@ Alerts Firing: Alerts Resolved: {{ template "__text_alert_list" .Alerts.Resolved }} {{ end }} -{{ end }} \ No newline at end of file +{{ end }} + +{{ define "discord.default.title" }}{{ template "__subject" . }}{{ end }} +{{ define "discord.default.message" }} +{{ if gt (len .Alerts.Firing) 0 }} +Alerts Firing: +{{ template "__text_alert_list" .Alerts.Firing }} +{{ end }} +{{ if gt (len .Alerts.Resolved) 0 }} +Alerts Resolved: +{{ template "__text_alert_list" .Alerts.Resolved }} +{{ end }} +{{ end }}