diff --git a/asset/assets_vfsdata.go b/asset/assets_vfsdata.go index 9eb94a83c0..3a33fa0308 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: 4905, + uncompressedSize: 5233, - 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"), + compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\xec\x57\x41\x6f\xdb\x3c\x0c\xbd\xe7\x57\x10\xfe\x2e\xcd\x21\xee\x77\x2e\x50\x0c\xc5\xb0\xed\x52\x0c\x43\x8a\xec\x32\x0c\x86\x62\x33\xae\x5a\x59\x72\x25\x3a\x6d\xe0\xf8\xbf\x0f\xb2\xdd\xc4\x8e\xec\xd4\x0e\xb2\xd3\x72\xab\x59\xf2\x91\x7a\x7c\x22\x95\x3c\x87\x08\x57\x5c\x22\x78\x41\xc0\x04\x6a\x4a\x98\x64\x31\x6a\x0f\x8a\xe2\xae\xf1\x9d\xe7\x80\x32\x82\xa2\x98\xf4\x86\x2c\xe6\xf7\x36\x2a\xcf\xc1\xff\xf2\x46\xa8\x25\x13\x8b\xf9\x3d\x14\xc5\xf5\x7f\xd7\xa5\x9f\xf9\xa4\x31\x44\xbe\x46\x7d\x6b\x9d\xe6\xf5\x07\x6c\x21\xd3\xe2\x25\x43\xbd\xa9\xc2\xeb\x44\xed\x4c\x26\x5b\x3e\x61\x48\x36\xc3\x2f\x1b\xfd\x40\x8c\x32\x03\x5b\x20\xb5\x48\x53\xd4\x55\x28\x5f\x01\xbe\xec\xfe\xe9\xad\xb8\xe6\x32\xb6\x31\x37\x36\xa6\x3c\x90\xf1\xbf\x96\x56\xd8\x82\x40\xd9\xcc\xf8\x1b\xac\xd3\x37\xad\xb2\xf4\x9e\x2d\x51\x18\xff\x41\x69\xc2\xe8\x07\xe3\xda\xf8\x3f\x99\xc8\xd0\x26\x7c\x52\x5c\x82\x07\x16\x15\xaa\x94\x31\xc1\x95\xc5\xf2\x3f\xab\x24\x51\xb2\x0a\x9e\xd6\xb6\x06\xde\x14\x8a\xe2\x2a\xcf\xe1\x95\xd3\x63\xdb\xd9\x9f\x63\xa2\xd6\xd8\xce\xfe\x9d\x25\x68\x6a\x46\xbb\xb2\xef\x0a\x9f\xee\xfe\xea\x69\x53\x84\x26\xd4\x3c\x25\xae\xa4\x77\x84\x63\xc2\x37\xaa\x5a\x1a\x08\x6e\xa8\x76\xd5\x4c\xc6\x08\x3e\x14\x45\x55\xd7\xcd\x64\x6f\x74\x79\xb2\xac\xcc\x4a\x22\x6d\xf9\xf6\xeb\x16\x76\x07\xa8\x0b\xab\x92\xdf\x49\xa9\x88\xd9\x9a\x5a\x90\x0d\xf3\x69\xb8\x0f\x2a\xd3\x21\xde\x54\xcd\x44\x89\x9a\x91\xd2\x95\x12\x27\x1d\x44\xb5\x38\x30\x82\x85\xcf\x7e\x84\x2b\x96\x09\xf2\x89\x93\xc0\x9a\x05\xc2\x24\x15\x8c\xda\x5a\xf4\xfb\x28\x6f\xe3\x64\xc6\xde\x86\xa4\x0b\xaa\x7d\xe7\x06\xe2\xad\x98\x10\x4b\x16\x3e\x3b\x78\x9d\xe5\x5b\x50\xd8\xc2\x47\x8e\x82\xcb\xe7\xc1\x15\x84\x75\x05\x3c\xf2\x86\x05\xa4\x1a\xad\xba\x06\x7a\x37\x0a\x3a\xca\x58\x39\x72\x06\x96\xcc\x43\x25\x31\x51\x4f\xdc\x1b\xee\x9f\x69\x31\xb4\xe2\xe1\x87\x5b\x29\x45\xd5\x80\xed\x11\x61\x6a\x8f\x16\x65\xb4\xd9\x85\xb8\xf7\x77\x9c\x1c\x5d\xc4\x50\x70\x94\x74\xba\x20\xfb\x10\xf7\x4b\xe0\xb4\x9e\xb9\xb8\x5c\x1a\x62\x32\x44\xd3\x81\xeb\x0c\x2c\xbf\x9f\x55\x95\x9a\x18\x25\xc7\x1d\x70\x82\xc6\xb0\xf8\xb4\xfb\xed\x80\xb9\x1d\xaa\xe7\x7b\xcf\x38\xeb\x1c\xe8\x93\x83\x75\xd2\xda\x57\x53\xf8\x1f\x66\x45\x31\xa9\x8c\x50\x19\xcb\xc1\x79\x9c\x91\xf6\xd2\x2b\x93\xcc\x1a\x27\xea\xc8\x37\x47\xa3\xc4\x1a\xa3\x83\x8c\xef\xe6\xe1\x39\xdf\x23\x9c\xac\xb3\x21\x94\x9a\x72\x8e\x8f\x57\x53\xab\xeb\xaf\x18\x3e\x32\x1a\xdb\xf3\xc9\xa5\x7f\x47\xfa\xd7\x7c\x17\x2e\xb4\x70\xf0\x3a\xfb\xd3\xd3\xf5\x83\xfe\x90\x0a\xec\xb2\xec\x9d\xa4\xae\x7b\xca\x34\x6d\x46\xf8\x13\x8b\x87\x7a\xb3\x18\x25\x05\x87\x2b\xae\xad\xaf\x35\x0f\x49\x69\x95\x9a\xbd\x6c\x89\x11\x06\x6d\xa1\x5d\xb4\x34\x6e\x16\xb8\xac\xa2\x24\x4e\x9b\x20\xe2\x26\x15\x6c\x13\xf4\xbc\xa6\x3e\x1e\xdc\x2e\x72\xa2\x24\x27\x65\x09\x09\x48\x29\x31\x72\x25\xb6\x76\x57\x66\x1e\xd5\x1a\xf5\x19\xde\x8f\x0e\xd4\xdf\xd7\xd3\x79\xe4\x34\x5c\x4d\xe7\x13\x53\x23\xe7\x00\x26\xf7\x6f\xba\x31\x3b\xa5\xf9\x9a\x93\x8d\xcb\xbe\xff\x55\x3a\xfe\x37\x42\x03\xe7\xd2\xde\x31\xed\x6d\xb2\x48\x28\x30\xd6\x2c\xe9\xa2\xf2\x9f\x25\x25\xe2\x26\x54\x3a\x3a\xc3\x20\x3a\x44\xba\xb0\x6b\x9f\x09\x4b\x7c\xbb\x5c\xdd\x93\x78\xfc\x13\x00\x00\xff\xff\x40\x8b\x18\x6d\x71\x14\x00\x00"), }, "/templates/email.tmpl": &vfsgen۰CompressedFileInfo{ name: "email.tmpl", diff --git a/cmd/alertmanager/main.go b/cmd/alertmanager/main.go index 2879ada888..d915c8b4f0 100644 --- a/cmd/alertmanager/main.go +++ b/cmd/alertmanager/main.go @@ -58,6 +58,7 @@ import ( "github.com/prometheus/alertmanager/notify/sns" "github.com/prometheus/alertmanager/notify/telegram" "github.com/prometheus/alertmanager/notify/victorops" + "github.com/prometheus/alertmanager/notify/webex" "github.com/prometheus/alertmanager/notify/webhook" "github.com/prometheus/alertmanager/notify/wechat" "github.com/prometheus/alertmanager/provider/mem" @@ -177,6 +178,9 @@ func buildReceiverIntegrations(nc *config.Receiver, tmpl *template.Template, log for i, c := range nc.DiscordConfigs { add("discord", i, c, func(l log.Logger) (notify.Notifier, error) { return discord.New(c, tmpl, l) }) } + for i, c := range nc.WebexConfigs { + add("webex", i, c, func(l log.Logger) (notify.Notifier, error) { return webex.New(c, tmpl, l) }) + } if errs.Len() > 0 { return nil, &errs diff --git a/config/config.go b/config/config.go index e91db5c6fd..21698681ce 100644 --- a/config/config.go +++ b/config/config.go @@ -251,6 +251,9 @@ func resolveFilepaths(baseDir string, cfg *Config) { for _, cfg := range receiver.DiscordConfigs { cfg.HTTPConfig.SetDirectory(baseDir) } + for _, cfg := range receiver.WebexConfigs { + cfg.HTTPConfig.SetDirectory(baseDir) + } } } @@ -513,6 +516,18 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { return fmt.Errorf("no discord webhook URL provided") } } + for _, webex := range rcv.WebexConfigs { + if webex.HTTPConfig == nil { + webex.HTTPConfig = c.Global.HTTPConfig + } + if webex.APIURL == nil { + if c.Global.WebexAPIURL == nil { + return fmt.Errorf("no global Webex URL set") + } + + webex.APIURL = c.Global.WebexAPIURL + } + } names[rcv.Name] = struct{}{} } @@ -613,6 +628,7 @@ func DefaultGlobalConfig() GlobalConfig { WeChatAPIURL: mustParseURL("https://qyapi.weixin.qq.com/cgi-bin/"), VictorOpsAPIURL: mustParseURL("https://alert.victorops.com/integrations/generic/20131114/alert/"), TelegramAPIUrl: mustParseURL("https://api.telegram.org"), + WebexAPIURL: mustParseURL("https://webexapis.com/v1/messages"), } } @@ -736,6 +752,7 @@ type GlobalConfig struct { VictorOpsAPIKey Secret `yaml:"victorops_api_key,omitempty" json:"victorops_api_key,omitempty"` VictorOpsAPIKeyFile string `yaml:"victorops_api_key_file,omitempty" json:"victorops_api_key_file,omitempty"` TelegramAPIUrl *URL `yaml:"telegram_api_url,omitempty" json:"telegram_api_url,omitempty"` + WebexAPIURL *URL `yaml:"webex_api_url,omitempty" json:"webex_api_url,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface for GlobalConfig. @@ -878,6 +895,7 @@ type Receiver struct { VictorOpsConfigs []*VictorOpsConfig `yaml:"victorops_configs,omitempty" json:"victorops_configs,omitempty"` SNSConfigs []*SNSConfig `yaml:"sns_configs,omitempty" json:"sns_configs,omitempty"` TelegramConfigs []*TelegramConfig `yaml:"telegram_configs,omitempty" json:"telegram_configs,omitempty"` + WebexConfigs []*WebexConfig `yaml:"webex_configs,omitempty" json:"webex_configs,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface for Receiver. diff --git a/config/config_test.go b/config/config_test.go index c7a6b3941e..5855eac729 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -867,6 +867,7 @@ func TestEmptyFieldsAndRegex(t *testing.T) { WeChatAPIURL: mustParseURL("https://qyapi.weixin.qq.com/cgi-bin/"), VictorOpsAPIURL: mustParseURL("https://alert.victorops.com/integrations/generic/20131114/alert/"), TelegramAPIUrl: mustParseURL("https://api.telegram.org"), + WebexAPIURL: mustParseURL("https://webexapis.com/v1/messages"), }, Templates: []string{ diff --git a/config/notifiers.go b/config/notifiers.go index f344756102..d67a7236c7 100644 --- a/config/notifiers.go +++ b/config/notifiers.go @@ -33,6 +33,14 @@ var ( }, } + // DefaultWebexConfig defines default values for Webex configurations. + DefaultWebexConfig = WebexConfig{ + NotifierConfig: NotifierConfig{ + VSendResolved: true, + }, + Message: `{{ template "webex.default.message" . }}`, + } + // DefaultDiscordConfig defines default values for Discord configurations. DefaultDiscordConfig = DiscordConfig{ NotifierConfig: NotifierConfig{ @@ -166,6 +174,35 @@ func (nc *NotifierConfig) SendResolved() bool { return nc.VSendResolved } +// WebexConfig configures notifications via Webex. +type WebexConfig struct { + NotifierConfig `yaml:",inline" json:",inline"` + HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` + APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"` + + Message string `yaml:"message,omitempty" json:"message,omitempty"` + RoomID string `yaml:"room_id" json:"room_id"` +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (c *WebexConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + *c = DefaultWebexConfig + type plain WebexConfig + if err := unmarshal((*plain)(c)); err != nil { + return err + } + + if c.RoomID == "" { + return fmt.Errorf("missing room_id on webex_config") + } + + if c.HTTPConfig == nil || c.HTTPConfig.Authorization == nil { + return fmt.Errorf("missing webex_configs.http_config.authorization") + } + + return nil +} + // DiscordConfig configures notifications via Discord. type DiscordConfig struct { NotifierConfig `yaml:",inline" json:",inline"` diff --git a/config/notifiers_test.go b/config/notifiers_test.go index 602cd1826f..a58f8d95be 100644 --- a/config/notifiers_test.go +++ b/config/notifiers_test.go @@ -14,6 +14,7 @@ package config import ( + "errors" "strings" "testing" @@ -866,6 +867,41 @@ func TestWeChatTypeMatcher(t *testing.T) { } } +func TestWebexConfiguration(t *testing.T) { + tc := []struct { + name string + + in string + expected error + }{ + { + name: "with no room_id - it fails", + in: ` +message: xyz123 +`, + expected: errors.New("missing room_id on webex_config"), + }, + { + name: "with room_id and http_config.authorization set - it succeeds", + in: ` +room_id: 2 +http_config: + authorization: + credentials: "xxxyyyzz" +`, + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + var cfg WebexConfig + err := yaml.UnmarshalStrict([]byte(tt.in), &cfg) + + require.Equal(t, tt.expected, err) + }) + } +} + func newBoolPointer(b bool) *bool { return &b } diff --git a/docs/configuration.md b/docs/configuration.md index d717914c91..bc4c570c40 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -95,6 +95,7 @@ global: [ wechat_api_secret: ] [ wechat_api_corp_id: ] [ telegram_api_url: | default = "https://api.telegram.org" ] + [ webex_api_url: | default = "https://webexapis.com/v1/messages" ] # The default HTTP client configuration [ http_config: ] @@ -516,6 +517,8 @@ wechat_configs: [ - , ... ] telegram_configs: [ - , ... ] +webex_configs: + [ - , ... ] ``` ## `` @@ -1112,3 +1115,22 @@ API](http://admin.wechat.com/wiki/index.php?title=Customer_Service_Messages). # The HTTP client's configuration. [ http_config: | default = global.http_config ] ``` + +## `` +```yaml +# Whether to notify about resolved alerts. +[ send_resolved: | default = true ] + +# The Webex Teams API URL i.e. https://webexapis.com/v1/messages +# If not specified, default API URL will be used. +[ api_url: | default = global.webex_api_url ] + +# ID of the Webex Teams room where to send the messages. +room_id: + +# Message template +[ message: default = '{{ template "webex.default.message" .}}' ] + +# The HTTP client's configuration. You must use this configuration to supply the bot token as part of the HTTP `Authorization` header. +[ http_config: | default = global.http_config ] +``` diff --git a/notify/util.go b/notify/util.go index c127913ea2..6b71ac7b29 100644 --- a/notify/util.go +++ b/notify/util.go @@ -20,6 +20,7 @@ import ( "io" "net/http" "net/url" + "strings" "github.com/go-kit/log" "github.com/go-kit/log/level" @@ -100,15 +101,32 @@ func TruncateInRunes(s string, n int) (string, bool) { // TruncateInBytes truncates a string to fit the given size in Bytes. func TruncateInBytes(s string, n int) (string, bool) { + // First, measure the string the w/o a to-rune conversion. if len(s) <= n { return s, false } + // The truncationMarker itself is 3 bytes, we can't return any part of the string when it's less than 3. if n <= 3 { - return string(s[:n]), true + switch n { + case 3: + return truncationMarker, true + default: + return strings.Repeat(".", n), true + } + } + + // Now, to ensure we don't butcher the string we need to remove using runes. + r := []rune(s) + truncationTarget := n - 3 + + // Next, let's truncate the runes to the lower possible number. + truncatedRunes := r[:truncationTarget] + for len(string(truncatedRunes)) > truncationTarget { + truncatedRunes = r[:len(truncatedRunes)-1] } - return string(s[:n-3]) + truncationMarker, true // In bytes, the truncation marker is 3 bytes. + return string(truncatedRunes) + truncationMarker, true } // TmplText is using monadic error handling in order to make string templating diff --git a/notify/util_test.go b/notify/util_test.go index 434e8c558a..032fc51508 100644 --- a/notify/util_test.go +++ b/notify/util_test.go @@ -49,7 +49,7 @@ func TestTruncate(t *testing.T) { in: "abcde", n: 2, runes: expect{out: "ab", trunc: true}, - bytes: expect{out: "ab", trunc: true}, + bytes: expect{out: "..", trunc: true}, }, { in: "abcde", @@ -73,25 +73,25 @@ func TestTruncate(t *testing.T) { in: "a⌘cde", n: 5, runes: expect{out: "a⌘cde", trunc: false}, - bytes: expect{out: "a\xe2…", trunc: true}, + bytes: expect{out: "a…", trunc: true}, }, { in: "a⌘cdef", n: 5, runes: expect{out: "a⌘cd…", trunc: true}, - bytes: expect{out: "a\xe2…", trunc: true}, + bytes: expect{out: "a…", trunc: true}, }, { in: "世界cdef", n: 3, runes: expect{out: "世界c", trunc: true}, - bytes: expect{out: "世", trunc: true}, + bytes: expect{out: "…", trunc: true}, }, { - in: "❤️✅🚀🔥❌", - n: 4, - runes: expect{out: "❤️✅…", trunc: true}, - bytes: expect{out: "\xe2…", trunc: true}, + in: "❤️✅🚀🔥❌❤️✅🚀🔥❌❤️✅🚀🔥❌❤️✅🚀🔥❌", + n: 19, + runes: expect{out: "❤️✅🚀🔥❌❤️✅🚀🔥❌❤️✅🚀🔥❌…", trunc: true}, + bytes: expect{out: "❤️✅🚀…", trunc: true}, }, } @@ -117,8 +117,8 @@ func TestTruncate(t *testing.T) { t.Run(fmt.Sprintf("%s(%s,%d)", fnName, tc.in, tc.n), func(t *testing.T) { s, trunc := fn(tc.in, tc.n) - require.Equal(t, truncated, trunc) require.Equal(t, out, s) + require.Equal(t, truncated, trunc) }) } } diff --git a/notify/webex/webex.go b/notify/webex/webex.go new file mode 100644 index 0000000000..9e95e3ed9b --- /dev/null +++ b/notify/webex/webex.go @@ -0,0 +1,114 @@ +// Copyright 2022 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 webex + +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/alertmanager/config" + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/template" + "github.com/prometheus/alertmanager/types" +) + +const ( + // maxMessageSize represents the maximum message length that Webex supports. + maxMessageSize = 7439 +) + +type Notifier struct { + conf *config.WebexConfig + tmpl *template.Template + logger log.Logger + client *http.Client + retrier *notify.Retrier +} + +// New returns a new Webex notifier. +func New(c *config.WebexConfig, t *template.Template, l log.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) { + client, err := commoncfg.NewClientFromConfig(*c.HTTPConfig, "webex", httpOpts...) + if err != nil { + return nil, err + } + + n := &Notifier{ + conf: c, + tmpl: t, + logger: l, + client: client, + retrier: ¬ify.Retrier{}, + } + + return n, nil +} + +type webhook struct { + Markdown string `json:"markdown"` + RoomID string `json:"roomId,omitempty"` +} + +// 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) + + data := notify.GetTemplateData(ctx, n.tmpl, as, n.logger) + tmpl := notify.TmplText(n.tmpl, data, &err) + if err != nil { + return false, err + } + + message := tmpl(n.conf.Message) + if err != nil { + return false, err + } + + message, truncated := notify.TruncateInBytes(message, maxMessageSize) + if truncated { + level.Debug(n.logger).Log("msg", "message truncated due to exceeding maximum allowed length by webex", "truncated_message", message) + } + + w := webhook{ + Markdown: message, + RoomID: n.conf.RoomID, + } + + var payload bytes.Buffer + if err = json.NewEncoder(&payload).Encode(w); err != nil { + return false, err + } + + resp, err := notify.PostJSON(ctx, n.client, n.conf.APIURL.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/webex/webex_test.go b/notify/webex/webex_test.go new file mode 100644 index 0000000000..161751fb50 --- /dev/null +++ b/notify/webex/webex_test.go @@ -0,0 +1,150 @@ +// Copyright 2022 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 webex + +import ( + "context" + "fmt" + "io" + "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" +) + +func TestWebexRetry(t *testing.T) { + testWebhookURL, err := url.Parse("https://api.ciscospark.com/v1/message") + require.NoError(t, err) + + notifier, err := New( + &config.WebexConfig{ + HTTPConfig: &commoncfg.HTTPClientConfig{}, + APIURL: &config.URL{URL: testWebhookURL}, + }, + 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("error on status %d", statusCode)) + } +} + +func TestWebexTemplating(t *testing.T) { + tc := []struct { + name string + + cfg *config.WebexConfig + Message string + expJSON string + commonCfg *commoncfg.HTTPClientConfig + + retry bool + errMsg string + expHeader string + }{ + { + name: "with a valid message and a set http_config.authorization, it is formatted as expected", + cfg: &config.WebexConfig{ + Message: `{{ template "webex.default.message" . }}`, + }, + commonCfg: &commoncfg.HTTPClientConfig{ + Authorization: &commoncfg.Authorization{Type: "Bearer", Credentials: "anewsecret"}, + }, + + expJSON: `{"markdown":"\n\nAlerts Firing:\nLabels:\n - lbl1 = val1\n - lbl3 = val3\nAnnotations:\nSource: \nLabels:\n - lbl1 = val1\n - lbl2 = val2\nAnnotations:\nSource: \n\n\n\n"}`, + retry: false, + expHeader: "Bearer anewsecret", + }, + { + name: "with templating errors, it fails.", + cfg: &config.WebexConfig{ + Message: "{{ ", + }, + commonCfg: &commoncfg.HTTPClientConfig{}, + errMsg: "template: :1: unclosed action", + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + var out []byte + var header http.Header + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var err error + out, err = io.ReadAll(r.Body) + header = r.Header.Clone() + require.NoError(t, err) + })) + defer srv.Close() + u, _ := url.Parse(srv.URL) + + tt.cfg.APIURL = &config.URL{URL: u} + tt.cfg.HTTPConfig = tt.commonCfg + notifierWebex, err := New(tt.cfg, test.CreateTmpl(t), log.NewNopLogger()) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + ctx = notify.WithGroupKey(ctx, "1") + + ok, err := notifierWebex.Notify(ctx, []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{ + "lbl1": "val1", + "lbl3": "val3", + }, + StartsAt: time.Now(), + EndsAt: time.Now().Add(time.Hour), + }, + }, + { + Alert: model.Alert{ + Labels: model.LabelSet{ + "lbl1": "val1", + "lbl2": "val2", + }, + StartsAt: time.Now(), + EndsAt: time.Now().Add(time.Hour), + }, + }, + }...) + + if tt.errMsg == "" { + require.NoError(t, err) + require.Equal(t, tt.expHeader, header.Get("Authorization")) + require.JSONEq(t, tt.expJSON, string(out)) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), tt.errMsg) + } + + require.Equal(t, tt.retry, ok) + }) + } +} diff --git a/template/default.tmpl b/template/default.tmpl index dd6697438f..82626b1a45 100644 --- a/template/default.tmpl +++ b/template/default.tmpl @@ -124,3 +124,14 @@ Alerts Resolved: {{ template "__text_alert_list" .Alerts.Resolved }} {{ end }} {{ end }} + +{{ define "webex.default.message" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }} +{{ 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 }}