feat(3920): add msteamsv2 receiver (#4024)

* feat(3920): add msteamsv2 receiver

Signed-off-by: Simon Schneider <github@simon-schneider.eu>

* Don't use `fmt.Errorf` when there's no formatting required on `config/config.go`

Signed-off-by: gotjosh <josue.abreu@gmail.com>

* Don't use `fmt.Errorf` when there's no formatting required on `config/notifiers.go`

Signed-off-by: gotjosh <josue.abreu@gmail.com>

* Remove additional documentation steps

Signed-off-by: gotjosh <josue.abreu@gmail.com>

* add more info to the documentation

Signed-off-by: gotjosh <josue.abreu@gmail.com>

* Change documentation links to convey the message better

Signed-off-by: gotjosh <josue.abreu@gmail.com>

---------

Signed-off-by: Simon Schneider <github@simon-schneider.eu>
Signed-off-by: gotjosh <josue.abreu@gmail.com>
Co-authored-by: gotjosh <josue.abreu@gmail.com>
This commit is contained in:
Simon Schneider 2024-10-23 12:31:27 +02:00 committed by GitHub
parent 3d66826261
commit d4009f5e93
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 577 additions and 66 deletions

View File

@ -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: 7283,
uncompressedSize: 7656,
compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\xec\x59\xcd\x6e\xeb\x36\x13\xdd\xfb\x29\x06\xba\xdf\x22\x5e\x44\xf7\x5b\x07\x08\x8a\x8b\xa2\x3f\x8b\xb4\x28\x1c\xa4\x9b\xa2\x10\x18\x69\xac\x30\xa1\x48\x85\x1c\xd9\x31\x1c\xbd\x7b\x41\x49\x96\x45\x49\xb6\x29\x5f\x77\x55\xef\x62\x7a\xe6\x9c\xe1\x39\xa3\x21\xe5\x6c\xb7\x90\xe0\x92\x4b\x84\x20\x8a\x98\x40\x4d\x19\x93\x2c\x45\x1d\x40\x59\x7e\xeb\x7c\xde\x6e\x01\x65\x02\x65\x39\x3b\x98\xf2\xb4\x78\xb0\x59\xdb\x2d\x84\x3f\x7d\x10\x6a\xc9\xc4\xd3\xe2\x01\xca\xf2\xeb\x97\xaf\x55\x9c\xf9\x41\x63\x8c\x7c\x85\xfa\xde\x06\x2d\x9a\x0f\xf0\x09\x85\x16\xef\x05\xea\x4d\x9d\xde\x10\xb9\x4c\xa6\x78\x7e\xc5\x98\x2c\xc3\x5f\x36\xfb\x91\x18\x15\x06\x3e\x81\xd4\x53\x9e\xa3\xae\x53\xf9\x12\xf0\xbd\xfd\x32\x58\x72\xcd\x65\x6a\x73\xee\x6c\x4e\xb5\x21\x13\xfe\x5c\xad\xc2\x27\x08\x94\x5d\xc6\xbf\xc1\x06\xfd\xa2\x55\x91\x3f\xb0\x67\x14\x26\x7c\x54\x9a\x30\xf9\x83\x71\x6d\xc2\x3f\x99\x28\xd0\x12\xbe\x2a\x2e\x21\x00\x8b\x0a\x35\x65\x4a\x70\x63\xb1\xc2\x1f\x55\x96\x29\x59\x27\xcf\x9b\xb5\x0e\xde\x1c\xca\xf2\x66\xbb\x85\x35\xa7\x17\x37\x38\x5c\x60\xa6\x56\xe8\xb2\xff\xce\x32\x34\x8d\xa2\x63\xec\x6d\xe1\xf3\xf6\xaf\x03\x36\x25\x68\x62\xcd\x73\xe2\x4a\x06\x47\x34\x26\xfc\xa0\xda\xd2\x48\x70\x43\x4d\xa8\x66\x32\x45\x08\xa1\x2c\xeb\xba\xee\x66\xfb\xc5\xa1\x4e\x56\x95\xdb\x4a\x48\x5b\xbe\xfd\x74\x0f\xed\x06\x9a\xc2\x6a\xf2\x6f\x52\x2a\x62\xb6\x26\x07\xb2\xb3\x7c\x1e\xee\xa3\x2a\x74\x8c\x77\xb5\x99\x28\x51\x33\x52\xba\xee\xc4\xd9\x88\x50\x47\x25\x88\x32\xa6\xdf\x12\xb5\x96\x03\x2d\x66\xbe\x62\x78\x56\x3d\x9b\x2e\x87\x2f\xb2\x97\x20\xb3\x71\x45\x8c\x60\xf1\x5b\x98\xe0\x92\x15\x82\x42\xe2\x24\xb0\x91\x82\x30\xcb\x05\x23\xf7\xe1\x0c\x0f\xf5\xa0\x8b\x53\x18\x3b\x1e\xb2\x31\x28\x77\x08\x79\xe2\x2d\x99\x10\xcf\x2c\x7e\x1b\xe0\x8d\x96\x6f\x41\xe1\x13\x4e\x05\x0a\x2e\xdf\xbc\x2b\x88\x9b\x0a\x78\x12\xf8\x25\xe4\x1a\x6d\xaf\x79\x46\x77\x0a\x3a\xaa\x58\x35\x83\x3d\x4b\xe6\xb1\x92\x98\xa9\x57\x1e\xf8\xc7\x17\x5a\xf8\x56\xec\xbf\xb9\xa5\x52\x54\x9f\x38\x9d\x1e\xec\x86\xe7\x76\x6b\x49\x41\x9b\x36\x65\x38\xd0\xa6\xb5\xe3\x10\x31\x16\x1c\x25\x9d\xdf\x90\x87\x10\xf7\xa7\xe2\x79\x9e\x0d\x71\xb9\x34\xc4\x64\x8c\x66\x04\x77\x30\xc1\xc3\xc3\xaa\xaa\xdc\xa4\x28\x39\xb6\xc0\x19\x1a\xc3\xd2\xf3\x9e\xef\x01\xd8\xd0\xa1\xe6\xc0\x3b\x30\xd0\x46\x4f\xb8\x59\xef\x7c\x75\x0e\xf0\x39\xfc\x1f\x6e\xed\xe0\xac\x16\xa1\x5e\xac\x46\xe7\x71\x45\xdc\x5b\x40\x45\x72\xdb\xd9\xd1\x08\xdf\x02\x8d\x12\x2b\x4c\x7a\x8c\xbb\x65\x7f\xce\x5d\xc6\x80\xf5\xd6\x47\x52\x53\xcd\xf1\xe9\xdd\xe4\xb8\xbe\xc6\xf8\x85\xd1\x54\xcf\x67\x57\xff\x8e\xf8\xd7\xbd\x28\x3f\x69\x31\xc0\x1b\xf5\xe7\x80\xeb\x3d\x7f\x48\x45\xf6\xb0\x3c\x38\x49\x87\xe1\x39\xd3\xb4\x99\x10\x4f\x2c\xf5\x8d\x66\x29\x4a\x8a\xfa\x47\x9c\xdb\x5f\x2b\x1e\x93\xd2\x2a\x37\xfb\xb6\x25\x46\x18\xb9\x8d\x76\xed\xa5\x69\xb3\x60\xa8\x2a\x4a\xe2\xb4\x89\x12\x6e\x72\xc1\x36\xd1\x81\xdb\xd4\xe9\xc1\x3d\x44\xce\x94\xe4\xa4\xac\x20\x11\x29\x25\x26\x1e\x89\xce\xd9\x55\x98\x17\xb5\x42\x7d\x81\xfb\xe3\x00\xea\xdf\xef\xa7\xcb\xb4\x93\x7f\x37\x5d\xae\x99\x86\x57\xfa\x63\x4a\xee\xef\x74\x53\xce\x94\xee\x6d\x4e\x76\x1e\xf6\xfd\x6b\xfa\xf4\x77\x84\x0e\xce\xd5\xde\x29\xf6\x76\x55\x24\x14\x98\x6a\x96\x8d\x49\xf9\x9f\x15\x25\xe1\x26\x56\x3a\xb9\xc0\x20\xea\x23\x5d\xd5\xb5\xd7\x84\x67\xfc\xb8\x3e\xba\xdf\xad\x63\x66\x08\x59\xd6\x1d\xa6\x59\xc6\xf4\xe6\xac\x3e\xed\x63\x9d\xdf\xf1\x03\xa4\xe6\xcd\xde\xc7\xa6\x2f\x30\xc9\xa8\xce\xcf\x6d\xdf\xed\x58\x4b\xed\xeb\xd9\x08\xf9\x14\xf3\x5e\xb9\x66\x17\x71\xce\x01\xea\xbd\x46\x5f\x35\x9f\x55\x37\xe4\x51\xad\x72\xcd\x95\xe6\xf6\xe5\xe7\xb6\xb9\x48\xff\x6f\xb7\x04\x77\xf7\x10\x04\xbb\xfb\xf5\xee\xa7\x55\x67\xb7\x36\x07\x00\xa0\xca\x33\xb8\xc2\x5d\x1e\x97\x09\x7e\xec\x7e\xdd\x85\x60\xf7\x55\xe0\x64\xf0\x25\xdc\xe0\x7b\x27\x31\x88\x35\x27\x1e\x33\x11\xcc\xdb\xc0\x16\xbe\x2d\xeb\x1e\x82\x5f\x79\xfa\xe2\x62\xa1\x30\x58\x01\x32\x99\xf4\x51\xd7\x4c\x4b\x2e\xd3\x60\x0e\x37\x12\x3b\x40\x35\xcc\xfc\x04\xd7\x6f\x98\xf0\x22\xf3\x67\xe3\x72\xa9\x2c\x95\x5d\xdd\x53\x9d\xa4\x79\x50\xeb\x1e\x87\x4c\x5a\x4f\xba\x7f\xd7\xff\xae\xe9\x42\x3b\x69\xae\x4f\x6d\x63\x0c\xb8\x27\xb9\x35\xd9\x31\x0f\xd7\x2e\xee\x9c\x97\x7b\x97\x73\xf0\xb4\x8b\x7d\x27\x4f\x39\xbb\x47\xea\x7f\xfb\x4f\x00\x00\x00\xff\xff\x30\xb3\x3d\xcd\x73\x1c\x00\x00"),
compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\xec\x59\x4d\x6f\xe3\x36\x10\xbd\xfb\x57\x0c\xb4\x3d\xc4\x87\x68\x8b\x1e\x03\x04\xc5\xa2\xe8\xc7\x21\x2d\x0a\x2f\xd2\x4b\x51\x08\x8c\x34\x56\x98\x50\xa4\x42\x8e\xec\x18\x8e\xfe\x7b\x41\x49\x96\xa9\x0f\xdb\x94\xd7\x3d\xd5\xb7\x98\x9a\x79\x33\x7c\x6f\x34\x43\x2a\xdb\x2d\x24\xb8\xe4\x12\x21\x88\x22\x26\x50\x53\xc6\x24\x4b\x51\x07\x50\x96\x5f\x9c\xdf\xdb\x2d\xa0\x4c\xa0\x2c\x67\x07\x5d\x1e\x17\x0f\xd6\x6b\xbb\x85\xf0\xe7\x77\x42\x2d\x99\x78\x5c\x3c\x40\x59\x7e\xfe\xf4\xb9\xb2\x33\x3f\x6a\x8c\x91\xaf\x50\xdf\x5b\xa3\x45\xf3\x03\x3e\xa0\xd0\xe2\xad\x40\xbd\xa9\xdd\x9b\x40\xdd\x48\xa6\x78\x7a\xc1\x98\x6c\x84\xbf\xad\xf7\x57\x62\x54\x18\xf8\x00\x52\x8f\x79\x8e\xba\x76\xe5\x4b\xc0\xb7\xf6\x61\xb0\xe4\x9a\xcb\xd4\xfa\xdc\x59\x9f\x6a\x43\x26\xfc\xa5\x5a\x85\x0f\x10\x28\xdd\x88\xff\x80\x35\xfa\x55\xab\x22\x7f\x60\x4f\x28\x4c\xf8\x55\x69\xc2\xe4\x4f\xc6\xb5\x09\xff\x62\xa2\x40\x1b\xf0\x45\x71\x09\x01\x58\x54\xa8\x43\xa6\x04\x37\x16\x2b\xfc\x49\x65\x99\x92\xb5\xf3\xbc\x59\x73\xf0\xe6\x50\x96\x37\xdb\x2d\xac\x39\x3d\x77\x8d\xc3\x05\x66\x6a\x85\xdd\xe8\x7f\xb0\x0c\x4d\xc3\xe8\x58\xf4\x36\xf1\x79\xfb\xd7\x01\x99\x12\x34\xb1\xe6\x39\x71\x25\x83\x23\x1c\x13\xbe\x53\x2d\x69\x24\xb8\xa1\xc6\x54\x33\x99\x22\x84\x50\x96\x75\x5e\x77\xb3\xfd\xe2\x90\x27\xcb\xca\x6d\x45\xa4\x4d\xdf\xfe\xba\x87\x76\x03\x4d\x62\x75\xf0\x2f\x52\x2a\x62\x36\xa7\x0e\xa4\xb3\x7c\x1e\xee\x57\x55\xe8\x18\xef\x6a\x31\x51\xa2\x66\xa4\x74\x5d\x89\xb3\x11\xa2\x8e\x52\x10\x65\x4c\xbf\x26\x6a\x2d\x07\x5c\xcc\x7c\xc9\xf0\xcc\x7a\x36\x9d\x0e\x5f\x64\x2f\x42\x66\xe3\x8c\x18\xc1\xe2\xd7\x30\xc1\x25\x2b\x04\x85\xc4\x49\x60\x43\x05\x61\x96\x0b\x46\xdd\x97\x33\x3c\x54\x83\x5d\x9c\xc2\xd8\xf6\x90\x8d\x41\x75\x9b\x90\x27\xde\x92\x09\xf1\xc4\xe2\xd7\x01\xde\x68\xfa\x16\x14\x3e\xe0\x94\xa1\xe0\xf2\xd5\x3b\x83\xb8\xc9\x80\x27\x81\x9f\x43\xae\xd1\xd6\x9a\xa7\xb5\x93\xd0\x51\xc6\xaa\x1e\xec\x99\x32\x8f\x95\xc4\x4c\xbd\xf0\xc0\xdf\xbe\xd0\xc2\x37\x63\xff\xcd\x2d\x95\xa2\x7a\xe2\x38\x35\xe8\x9a\xe7\x76\x6b\x49\x41\x9b\xd6\x65\xd8\xd0\xa6\x95\xe3\x10\x31\x16\x1c\x25\x9d\x5f\x90\x87\x10\xf7\x53\xf1\x3c\xcd\x86\xb8\x5c\x1a\x62\x32\x46\x33\x82\x3b\xe8\xe0\xe1\x61\x56\x55\x6e\x52\x94\x1c\x5b\xe0\x0c\x8d\x61\xe9\x79\xef\xf7\x00\x6c\xa8\x50\x33\xf0\x0e\x34\xb4\xd1\x09\x37\xeb\xcd\xd7\xce\x00\x9f\xc3\xf7\x70\x6b\x1b\x67\xb5\x08\xf5\x62\xd5\x3a\x8f\x33\xd2\x3d\x05\x54\x41\x6e\x9d\x1d\x8d\xc4\x5b\xa0\x51\x62\x85\x49\x2f\xe2\x6e\xd9\x3f\xe6\xce\x63\x10\xf5\xd6\x87\x52\x53\xf5\xf1\xe9\xd5\xd4\x51\x7d\x8d\xf1\x33\xa3\xa9\x9a\xcf\xae\xfa\x1d\xd1\xcf\x3d\x28\x3f\x6a\x31\xc0\x1b\xd5\xe7\x80\xea\x3d\x7d\x48\x45\x76\x58\x1e\xec\xa4\x43\xf3\x9c\x69\xda\x4c\xb0\x27\x96\xfa\x5a\xb3\x14\x25\x45\xfd\x11\xd7\xad\xaf\x15\x8f\x49\x69\x95\x9b\x7d\xd9\x12\x23\x8c\xba\x85\x76\xad\xa5\x69\xbd\x60\xc8\x2a\x4a\xe2\xb4\x89\x12\x6e\x72\xc1\x36\xd1\x81\xd3\xd4\xe9\xc6\x3d\x44\xce\x94\xe4\xa4\x2c\x21\x11\x29\x25\x26\x8e\xc4\xce\xec\x2a\xcc\xb3\x5a\xa1\xbe\xc0\xf9\x71\x00\xf5\xdf\xd7\xd3\x65\xca\xc9\xbf\x9a\x2e\x57\x4c\xc3\x23\xfd\x31\x26\xf7\x67\xba\x29\x33\xc5\x3d\xcd\x49\xe7\x65\xdf\x5f\xd3\xa7\xdf\x11\x1c\x9c\xab\xbc\x53\xe4\x75\x59\x24\x14\x98\x6a\x96\x8d\x51\xf9\xbf\x25\x25\xe1\x26\x56\x3a\xb9\x40\x23\xea\x23\x5d\xd9\xb5\xc7\x84\x27\x7c\xbf\xbe\xba\xdf\xcc\x63\x66\x08\x59\xe6\x36\xd3\x2c\x63\x7a\x73\x56\x9d\xf6\xb1\xce\xaf\xf8\x01\x52\x73\xb3\xf7\x91\xe9\x13\x4c\x12\xca\xf9\xdc\xf6\xcd\x8a\xb5\xa1\x7d\x35\x1b\x09\x7e\x86\x78\xab\x1f\x2e\x47\xb9\x8b\x75\x25\x7d\x8c\xf4\x17\xae\xd9\x45\x5e\x97\x0e\x50\xef\xdb\xc5\x95\xf3\x59\x75\x2d\x19\xe5\x2a\xd7\x5c\x69\x6e\x6f\x9c\xb7\xcd\xed\xe5\xbb\xdd\x12\xdc\xdd\x43\x10\xec\x2e\x35\xbb\xef\xd9\x9d\xdd\x5a\x1f\x00\x80\xca\xcf\xe0\x0a\x77\x7e\x5c\x26\xf8\xbe\xfb\xa4\x0e\xc1\xee\x51\xd0\xf1\xe0\x4b\xb8\xc1\x37\xc7\x31\x88\x35\x27\x1e\x33\x11\xcc\x5b\xc3\x16\xbe\x4d\xeb\x1e\x82\xdf\x78\xfa\xdc\xc5\x42\x61\xb0\x02\x64\x32\xe9\xa3\xae\x99\x96\x5c\xa6\xc1\x1c\x6e\x24\x3a\x40\x35\xcc\xfc\x44\xac\xdf\x31\xe1\x45\xe6\x1f\x8d\xcb\xa5\xb2\xa1\xec\xea\x3e\xd4\xc9\x30\x0f\x6a\xdd\x8b\x21\x93\x56\x13\xf7\xef\xfa\x7f\x64\x2e\x74\xc7\xad\xab\x53\x5b\x18\x83\xd8\x93\xd4\x9a\xac\x98\x87\x6a\x17\x57\xce\x4b\xbd\xcb\x29\x78\x5a\xc5\xbe\x92\xa7\x94\xdd\x23\xf5\x9f\xfe\x1b\x00\x00\xff\xff\x7a\xa6\xa9\x68\xe8\x1d\x00\x00"),
},
"/templates/email.tmpl": &vfsgen۰CompressedFileInfo{
name: "email.tmpl",

View File

@ -263,6 +263,9 @@ func resolveFilepaths(baseDir string, cfg *Config) {
for _, cfg := range receiver.MSTeamsConfigs {
cfg.HTTPConfig.SetDirectory(baseDir)
}
for _, cfg := range receiver.MSTeamsV2Configs {
cfg.HTTPConfig.SetDirectory(baseDir)
}
for _, cfg := range receiver.JiraConfigs {
cfg.HTTPConfig.SetDirectory(baseDir)
}
@ -282,7 +285,7 @@ func (mt *MuteTimeInterval) UnmarshalYAML(unmarshal func(interface{}) error) err
return err
}
if mt.Name == "" {
return fmt.Errorf("missing name in mute time interval")
return errors.New("missing name in mute time interval")
}
return nil
}
@ -300,7 +303,7 @@ func (ti *TimeInterval) UnmarshalYAML(unmarshal func(interface{}) error) error {
return err
}
if ti.Name == "" {
return fmt.Errorf("missing name in time interval")
return errors.New("missing name in time interval")
}
return nil
}
@ -346,19 +349,19 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
if c.Global.SlackAPIURL != nil && len(c.Global.SlackAPIURLFile) > 0 {
return fmt.Errorf("at most one of slack_api_url & slack_api_url_file must be configured")
return errors.New("at most one of slack_api_url & slack_api_url_file must be configured")
}
if c.Global.OpsGenieAPIKey != "" && len(c.Global.OpsGenieAPIKeyFile) > 0 {
return fmt.Errorf("at most one of opsgenie_api_key & opsgenie_api_key_file must be configured")
return errors.New("at most one of opsgenie_api_key & opsgenie_api_key_file must be configured")
}
if c.Global.VictorOpsAPIKey != "" && len(c.Global.VictorOpsAPIKeyFile) > 0 {
return fmt.Errorf("at most one of victorops_api_key & victorops_api_key_file must be configured")
return errors.New("at most one of victorops_api_key & victorops_api_key_file must be configured")
}
if len(c.Global.SMTPAuthPassword) > 0 && len(c.Global.SMTPAuthPasswordFile) > 0 {
return fmt.Errorf("at most one of smtp_auth_password & smtp_auth_password_file must be configured")
return errors.New("at most one of smtp_auth_password & smtp_auth_password_file must be configured")
}
names := map[string]struct{}{}
@ -378,13 +381,13 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
if ec.Smarthost.String() == "" {
if c.Global.SMTPSmarthost.String() == "" {
return fmt.Errorf("no global SMTP smarthost set")
return errors.New("no global SMTP smarthost set")
}
ec.Smarthost = c.Global.SMTPSmarthost
}
if ec.From == "" {
if c.Global.SMTPFrom == "" {
return fmt.Errorf("no global SMTP from set")
return errors.New("no global SMTP from set")
}
ec.From = c.Global.SMTPFrom
}
@ -415,7 +418,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
if sc.APIURL == nil && len(sc.APIURLFile) == 0 {
if c.Global.SlackAPIURL == nil && len(c.Global.SlackAPIURLFile) == 0 {
return fmt.Errorf("no global Slack API URL set either inline or in a file")
return errors.New("no global Slack API URL set either inline or in a file")
}
sc.APIURL = c.Global.SlackAPIURL
sc.APIURLFile = c.Global.SlackAPIURLFile
@ -432,7 +435,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
if pdc.URL == nil {
if c.Global.PagerdutyURL == nil {
return fmt.Errorf("no global PagerDuty URL set")
return errors.New("no global PagerDuty URL set")
}
pdc.URL = c.Global.PagerdutyURL
}
@ -443,7 +446,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
if ogc.APIURL == nil {
if c.Global.OpsGenieAPIURL == nil {
return fmt.Errorf("no global OpsGenie URL set")
return errors.New("no global OpsGenie URL set")
}
ogc.APIURL = c.Global.OpsGenieAPIURL
}
@ -452,7 +455,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
if ogc.APIKey == "" && len(ogc.APIKeyFile) == 0 {
if c.Global.OpsGenieAPIKey == "" && len(c.Global.OpsGenieAPIKeyFile) == 0 {
return fmt.Errorf("no global OpsGenie API Key set either inline or in a file")
return errors.New("no global OpsGenie API Key set either inline or in a file")
}
ogc.APIKey = c.Global.OpsGenieAPIKey
ogc.APIKeyFile = c.Global.OpsGenieAPIKeyFile
@ -465,21 +468,21 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
if wcc.APIURL == nil {
if c.Global.WeChatAPIURL == nil {
return fmt.Errorf("no global Wechat URL set")
return errors.New("no global Wechat URL set")
}
wcc.APIURL = c.Global.WeChatAPIURL
}
if wcc.APISecret == "" {
if c.Global.WeChatAPISecret == "" {
return fmt.Errorf("no global Wechat ApiSecret set")
return errors.New("no global Wechat ApiSecret set")
}
wcc.APISecret = c.Global.WeChatAPISecret
}
if wcc.CorpID == "" {
if c.Global.WeChatAPICorpID == "" {
return fmt.Errorf("no global Wechat CorpID set")
return errors.New("no global Wechat CorpID set")
}
wcc.CorpID = c.Global.WeChatAPICorpID
}
@ -494,7 +497,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
if voc.APIURL == nil {
if c.Global.VictorOpsAPIURL == nil {
return fmt.Errorf("no global VictorOps URL set")
return errors.New("no global VictorOps URL set")
}
voc.APIURL = c.Global.VictorOpsAPIURL
}
@ -503,7 +506,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
if voc.APIKey == "" && len(voc.APIKeyFile) == 0 {
if c.Global.VictorOpsAPIKey == "" && len(c.Global.VictorOpsAPIKeyFile) == 0 {
return fmt.Errorf("no global VictorOps API Key set")
return errors.New("no global VictorOps API Key set")
}
voc.APIKey = c.Global.VictorOpsAPIKey
voc.APIKeyFile = c.Global.VictorOpsAPIKeyFile
@ -528,7 +531,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
discord.HTTPConfig = c.Global.HTTPConfig
}
if discord.WebhookURL == nil && len(discord.WebhookURLFile) == 0 {
return fmt.Errorf("no discord webhook URL or URLFile provided")
return errors.New("no discord webhook URL or URLFile provided")
}
}
for _, webex := range rcv.WebexConfigs {
@ -537,7 +540,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
if webex.APIURL == nil {
if c.Global.WebexAPIURL == nil {
return fmt.Errorf("no global Webex URL set")
return errors.New("no global Webex URL set")
}
webex.APIURL = c.Global.WebexAPIURL
@ -548,7 +551,15 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
msteams.HTTPConfig = c.Global.HTTPConfig
}
if msteams.WebhookURL == nil && len(msteams.WebhookURLFile) == 0 {
return fmt.Errorf("no msteams webhook URL or URLFile provided")
return errors.New("no msteams webhook URL or URLFile provided")
}
}
for _, msteamsv2 := range rcv.MSTeamsV2Configs {
if msteamsv2.HTTPConfig == nil {
msteamsv2.HTTPConfig = c.Global.HTTPConfig
}
if msteamsv2.WebhookURL == nil && len(msteamsv2.WebhookURLFile) == 0 {
return errors.New("no msteamsv2 webhook URL or URLFile provided")
}
}
for _, jira := range rcv.JiraConfigs {
@ -557,7 +568,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
if jira.APIURL == nil {
if c.Global.JiraAPIURL == nil {
return fmt.Errorf("no global Jira Cloud URL set")
return errors.New("no global Jira Cloud URL set")
}
jira.APIURL = c.Global.JiraAPIURL
}
@ -569,20 +580,20 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
// The root route must not have any matchers as it is the fallback node
// for all alerts.
if c.Route == nil {
return fmt.Errorf("no routes provided")
return errors.New("no routes provided")
}
if len(c.Route.Receiver) == 0 {
return fmt.Errorf("root route must specify a default receiver")
return errors.New("root route must specify a default receiver")
}
if len(c.Route.Match) > 0 || len(c.Route.MatchRE) > 0 || len(c.Route.Matchers) > 0 {
return fmt.Errorf("root route must not have any matchers")
return errors.New("root route must not have any matchers")
}
if len(c.Route.MuteTimeIntervals) > 0 {
return fmt.Errorf("root route must not have any mute time intervals")
return errors.New("root route must not have any mute time intervals")
}
if len(c.Route.ActiveTimeIntervals) > 0 {
return fmt.Errorf("root route must not have any active time intervals")
return errors.New("root route must not have any active time intervals")
}
// Validate that all receivers used in the routing tree are defined.
@ -685,7 +696,7 @@ func parseURL(s string) (*URL, error) {
return nil, fmt.Errorf("unsupported scheme %q for URL", u.Scheme)
}
if u.Host == "" {
return nil, fmt.Errorf("missing host for URL")
return nil, errors.New("missing host for URL")
}
return &URL{u}, nil
}
@ -848,7 +859,7 @@ func (r *Route) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
if len(r.GroupBy) > 0 && r.GroupByAll {
return fmt.Errorf("cannot have wildcard group_by (`...`) and other other labels at the same time")
return errors.New("cannot have wildcard group_by (`...`) and other other labels at the same time")
}
groupBy := map[model.LabelName]struct{}{}
@ -861,10 +872,10 @@ func (r *Route) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
if r.GroupInterval != nil && time.Duration(*r.GroupInterval) == time.Duration(0) {
return fmt.Errorf("group_interval cannot be zero")
return errors.New("group_interval cannot be zero")
}
if r.RepeatInterval != nil && time.Duration(*r.RepeatInterval) == time.Duration(0) {
return fmt.Errorf("repeat_interval cannot be zero")
return errors.New("repeat_interval cannot be zero")
}
return nil
@ -935,6 +946,7 @@ type Receiver struct {
TelegramConfigs []*TelegramConfig `yaml:"telegram_configs,omitempty" json:"telegram_configs,omitempty"`
WebexConfigs []*WebexConfig `yaml:"webex_configs,omitempty" json:"webex_configs,omitempty"`
MSTeamsConfigs []*MSTeamsConfig `yaml:"msteams_configs,omitempty" json:"msteams_configs,omitempty"`
MSTeamsV2Configs []*MSTeamsV2Config `yaml:"msteamsv2_configs,omitempty" json:"msteamsv2_configs,omitempty"`
JiraConfigs []*JiraConfig `yaml:"jira_configs,omitempty" json:"jira_configs,omitempty"`
}
@ -945,7 +957,7 @@ func (c *Receiver) UnmarshalYAML(unmarshal func(interface{}) error) error {
return err
}
if c.Name == "" {
return fmt.Errorf("missing name in receiver")
return errors.New("missing name in receiver")
}
return nil
}

View File

@ -14,6 +14,7 @@
package config
import (
"errors"
"fmt"
"net/textproto"
"regexp"
@ -174,6 +175,14 @@ var (
Text: `{{ template "msteams.default.text" . }}`,
}
DefaultMSTeamsV2Config = MSTeamsV2Config{
NotifierConfig: NotifierConfig{
VSendResolved: true,
},
Title: `{{ template "msteamsv2.default.title" . }}`,
Text: `{{ template "msteamsv2.default.text" . }}`,
}
DefaultJiraConfig = JiraConfig{
NotifierConfig: NotifierConfig{
VSendResolved: true,
@ -212,11 +221,11 @@ func (c *WebexConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
if c.RoomID == "" {
return fmt.Errorf("missing room_id on webex_config")
return errors.New("missing room_id on webex_config")
}
if c.HTTPConfig == nil || c.HTTPConfig.Authorization == nil {
return fmt.Errorf("missing webex_configs.http_config.authorization")
return errors.New("missing webex_configs.http_config.authorization")
}
return nil
@ -243,11 +252,11 @@ func (c *DiscordConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
if c.WebhookURL == nil && c.WebhookURLFile == "" {
return fmt.Errorf("one of webhook_url or webhook_url_file must be configured")
return errors.New("one of webhook_url or webhook_url_file must be configured")
}
if c.WebhookURL != nil && len(c.WebhookURLFile) > 0 {
return fmt.Errorf("at most one of webhook_url & webhook_url_file must be configured")
return errors.New("at most one of webhook_url & webhook_url_file must be configured")
}
return nil
@ -282,7 +291,7 @@ func (c *EmailConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
return err
}
if c.To == "" {
return fmt.Errorf("missing to address in email config")
return errors.New("missing to address in email config")
}
// Header names are case-insensitive, check for collisions.
normalizedHeaders := map[string]string{}
@ -343,13 +352,13 @@ func (c *PagerdutyConfig) UnmarshalYAML(unmarshal func(interface{}) error) error
return err
}
if c.RoutingKey == "" && c.ServiceKey == "" && c.RoutingKeyFile == "" && c.ServiceKeyFile == "" {
return fmt.Errorf("missing service or routing key in PagerDuty config")
return errors.New("missing service or routing key in PagerDuty config")
}
if len(c.RoutingKey) > 0 && len(c.RoutingKeyFile) > 0 {
return fmt.Errorf("at most one of routing_key & routing_key_file must be configured")
return errors.New("at most one of routing_key & routing_key_file must be configured")
}
if len(c.ServiceKey) > 0 && len(c.ServiceKeyFile) > 0 {
return fmt.Errorf("at most one of service_key & service_key_file must be configured")
return errors.New("at most one of service_key & service_key_file must be configured")
}
if c.Details == nil {
c.Details = make(map[string]string)
@ -385,10 +394,10 @@ func (c *SlackAction) UnmarshalYAML(unmarshal func(interface{}) error) error {
return err
}
if c.Type == "" {
return fmt.Errorf("missing type in Slack action configuration")
return errors.New("missing type in Slack action configuration")
}
if c.Text == "" {
return fmt.Errorf("missing text in Slack action configuration")
return errors.New("missing text in Slack action configuration")
}
if c.URL != "" {
// Clear all message action fields.
@ -398,7 +407,7 @@ func (c *SlackAction) UnmarshalYAML(unmarshal func(interface{}) error) error {
} else if c.Name != "" {
c.URL = ""
} else {
return fmt.Errorf("missing name or url in Slack action configuration")
return errors.New("missing name or url in Slack action configuration")
}
return nil
}
@ -420,7 +429,7 @@ func (c *SlackConfirmationField) UnmarshalYAML(unmarshal func(interface{}) error
return err
}
if c.Text == "" {
return fmt.Errorf("missing text in Slack confirmation configuration")
return errors.New("missing text in Slack confirmation configuration")
}
return nil
}
@ -442,10 +451,10 @@ func (c *SlackField) UnmarshalYAML(unmarshal func(interface{}) error) error {
return err
}
if c.Title == "" {
return fmt.Errorf("missing title in Slack field configuration")
return errors.New("missing title in Slack field configuration")
}
if c.Value == "" {
return fmt.Errorf("missing value in Slack field configuration")
return errors.New("missing value in Slack field configuration")
}
return nil
}
@ -491,7 +500,7 @@ func (c *SlackConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
if c.APIURL != nil && len(c.APIURLFile) > 0 {
return fmt.Errorf("at most one of api_url & api_url_file must be configured")
return errors.New("at most one of api_url & api_url_file must be configured")
}
return nil
@ -521,10 +530,10 @@ func (c *WebhookConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
return err
}
if c.URL == nil && c.URLFile == "" {
return fmt.Errorf("one of url or url_file must be configured")
return errors.New("one of url or url_file must be configured")
}
if c.URL != nil && c.URLFile != "" {
return fmt.Errorf("at most one of url & url_file must be configured")
return errors.New("at most one of url & url_file must be configured")
}
return nil
}
@ -604,7 +613,7 @@ func (c *OpsGenieConfig) UnmarshalYAML(unmarshal func(interface{}) error) error
}
if c.APIKey != "" && len(c.APIKeyFile) > 0 {
return fmt.Errorf("at most one of api_key & api_key_file must be configured")
return errors.New("at most one of api_key & api_key_file must be configured")
}
for _, r := range c.Responders {
@ -663,10 +672,10 @@ func (c *VictorOpsConfig) UnmarshalYAML(unmarshal func(interface{}) error) error
return err
}
if c.RoutingKey == "" {
return fmt.Errorf("missing Routing key in VictorOps config")
return errors.New("missing Routing key in VictorOps config")
}
if c.APIKey != "" && len(c.APIKeyFile) > 0 {
return fmt.Errorf("at most one of api_key & api_key_file must be configured")
return errors.New("at most one of api_key & api_key_file must be configured")
}
reservedFields := []string{"routing_key", "message_type", "state_message", "entity_display_name", "monitoring_tool", "entity_id", "entity_state"}
@ -724,16 +733,16 @@ func (c *PushoverConfig) UnmarshalYAML(unmarshal func(interface{}) error) error
return err
}
if c.UserKey == "" && c.UserKeyFile == "" {
return fmt.Errorf("one of user_key or user_key_file must be configured")
return errors.New("one of user_key or user_key_file must be configured")
}
if c.UserKey != "" && c.UserKeyFile != "" {
return fmt.Errorf("at most one of user_key & user_key_file must be configured")
return errors.New("at most one of user_key & user_key_file must be configured")
}
if c.Token == "" && c.TokenFile == "" {
return fmt.Errorf("one of token or token_file must be configured")
return errors.New("one of token or token_file must be configured")
}
if c.Token != "" && c.TokenFile != "" {
return fmt.Errorf("at most one of token & token_file must be configured")
return errors.New("at most one of token & token_file must be configured")
}
return nil
}
@ -761,7 +770,7 @@ func (c *SNSConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
return err
}
if (c.TargetARN == "") != (c.TopicARN == "") != (c.PhoneNumber == "") {
return fmt.Errorf("must provide either a Target ARN, Topic ARN, or Phone Number for SNS config")
return errors.New("must provide either a Target ARN, Topic ARN, or Phone Number for SNS config")
}
return nil
}
@ -790,19 +799,19 @@ func (c *TelegramConfig) UnmarshalYAML(unmarshal func(interface{}) error) error
return err
}
if c.BotToken == "" && c.BotTokenFile == "" {
return fmt.Errorf("missing bot_token or bot_token_file on telegram_config")
return errors.New("missing bot_token or bot_token_file on telegram_config")
}
if c.BotToken != "" && c.BotTokenFile != "" {
return fmt.Errorf("at most one of bot_token & bot_token_file must be configured")
return errors.New("at most one of bot_token & bot_token_file must be configured")
}
if c.ChatID == 0 {
return fmt.Errorf("missing chat_id on telegram_config")
return errors.New("missing chat_id on telegram_config")
}
if c.ParseMode != "" &&
c.ParseMode != "Markdown" &&
c.ParseMode != "MarkdownV2" &&
c.ParseMode != "HTML" {
return fmt.Errorf("unknown parse_mode on telegram_config, must be Markdown, MarkdownV2, HTML or empty string")
return errors.New("unknown parse_mode on telegram_config, must be Markdown, MarkdownV2, HTML or empty string")
}
return nil
}
@ -826,11 +835,39 @@ func (c *MSTeamsConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
if c.WebhookURL == nil && c.WebhookURLFile == "" {
return fmt.Errorf("one of webhook_url or webhook_url_file must be configured")
return errors.New("one of webhook_url or webhook_url_file must be configured")
}
if c.WebhookURL != nil && len(c.WebhookURLFile) > 0 {
return fmt.Errorf("at most one of webhook_url & webhook_url_file must be configured")
return errors.New("at most one of webhook_url & webhook_url_file must be configured")
}
return nil
}
type MSTeamsV2Config 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"`
WebhookURLFile string `yaml:"webhook_url_file,omitempty" json:"webhook_url_file,omitempty"`
Title string `yaml:"title,omitempty" json:"title,omitempty"`
Text string `yaml:"text,omitempty" json:"text,omitempty"`
}
func (c *MSTeamsV2Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
*c = DefaultMSTeamsV2Config
type plain MSTeamsV2Config
if err := unmarshal((*plain)(c)); err != nil {
return err
}
if c.WebhookURL == nil && c.WebhookURLFile == "" {
return errors.New("one of webhook_url or webhook_url_file must be configured")
}
if c.WebhookURL != nil && len(c.WebhookURLFile) > 0 {
return errors.New("at most one of webhook_url & webhook_url_file must be configured")
}
return nil
@ -865,10 +902,10 @@ func (c *JiraConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
if c.Project == "" {
return fmt.Errorf("missing project in jira_config")
return errors.New("missing project in jira_config")
}
if c.IssueType == "" {
return fmt.Errorf("missing issue_type in jira_config")
return errors.New("missing issue_type in jira_config")
}
return nil

View File

@ -24,6 +24,7 @@ import (
"github.com/prometheus/alertmanager/notify/email"
"github.com/prometheus/alertmanager/notify/jira"
"github.com/prometheus/alertmanager/notify/msteams"
"github.com/prometheus/alertmanager/notify/msteamsv2"
"github.com/prometheus/alertmanager/notify/opsgenie"
"github.com/prometheus/alertmanager/notify/pagerduty"
"github.com/prometheus/alertmanager/notify/pushover"
@ -93,6 +94,9 @@ func BuildReceiverIntegrations(nc config.Receiver, tmpl *template.Template, logg
for i, c := range nc.MSTeamsConfigs {
add("msteams", i, c, func(l log.Logger) (notify.Notifier, error) { return msteams.New(c, tmpl, l, httpOpts...) })
}
for i, c := range nc.MSTeamsV2Configs {
add("msteamsv2", i, c, func(l log.Logger) (notify.Notifier, error) { return msteamsv2.New(c, tmpl, l, httpOpts...) })
}
for i, c := range nc.JiraConfigs {
add("jira", i, c, func(l log.Logger) (notify.Notifier, error) { return jira.New(c, tmpl, l, httpOpts...) })
}

View File

@ -700,6 +700,8 @@ email_configs:
[ - <email_config>, ... ]
msteams_configs:
[ - <msteams_config>, ... ]
msteamsv2_configs:
[ - <msteamsv2_config>, ... ]
jira_configs:
[ - <jira_config>, ... ]
opsgenie_configs:
@ -927,6 +929,8 @@ tls_config:
Microsoft Teams notifications are sent via the [Incoming Webhooks](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/what-are-webhooks-and-connectors) API endpoint.
DEPRECATION NOTICE: Microsoft is deprecating the creation and usage of [Microsoft 365 connectors via Microsoft Teams](https://devblogs.microsoft.com/microsoft365dev/retirement-of-office-365-connectors-within-microsoft-teams/). Consider migrating to using [Workflows](https://learn.microsoft.com/en-us/power-automate/teams/send-a-message-in-teams) with the msteamsv2 config.
```yaml
# Whether to notify about resolved alerts.
[ send_resolved: <boolean> | default = true ]
@ -949,6 +953,29 @@ Microsoft Teams notifications are sent via the [Incoming Webhooks](https://learn
[ http_config: <http_config> | default = global.http_config ]
```
### `<msteamsv2_config>`
Microsoft Teams v2 notifications using the new message format with adaptive cards as required by [flows](https://learn.microsoft.com/en-us/power-automate/teams/overview). Please follow [the documentation](https://support.microsoft.com/en-gb/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498) for more information on how to set up this integration.
```yaml
# Whether to notify about resolved alerts.
[ send_resolved: <boolean> | default = true ]
# The incoming webhook URL.
# webhook_url and webhook_url_file are mutually exclusive.
[ webhook_url: <secret> ]
[ webhook_url_file: <filepath> ]
# Message title template.
[ title: <tmpl_string> | default = '{{ template "msteamsv2.default.title" . }}' ]
# Message body template.
[ text: <tmpl_string> | default = '{{ template "msteamsv2.default.text" . }}' ]
# The HTTP client's configuration.
[ http_config: <http_config> | default = global.http_config ]
```
### `<jira_config>`
JIRA notifications are sent via [JIRA Rest API v2](https://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/)

View File

@ -0,0 +1,193 @@
// Copyright 2024 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 msteamsv2
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"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 = "Attention"
colorGreen = "Good"
colorGrey = "Warning"
)
type Notifier struct {
conf *config.MSTeamsV2Config
tmpl *template.Template
logger log.Logger
client *http.Client
retrier *notify.Retrier
webhookURL *config.SecretURL
postJSONFunc func(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error)
}
// https://learn.microsoft.com/en-us/connectors/teams/?tabs=text1#adaptivecarditemschema
type Content struct {
Schema string `json:"$schema"`
Type string `json:"type"`
Version string `json:"version"`
Body []Body `json:"body"`
}
type Body struct {
Type string `json:"type"`
Text string `json:"text"`
Weight string `json:"weigth,omitempty"`
Size string `json:"size,omitempty"`
Wrap bool `json:"wrap,omitempty"`
Style string `json:"style,omitempty"`
Color string `json:"color,omitempty"`
}
type Attachment struct {
ContentType string `json:"contentType"`
ContentURL *string `json:"contentUrl"` // Use a pointer to handle null values
Content Content `json:"content"`
}
type teamsMessage struct {
Type string `json:"type"`
Attachments []Attachment `json:"attachments"`
}
// New returns a new notifier that uses the Microsoft Teams Power Platform connector.
func New(c *config.MSTeamsV2Config, t *template.Template, l log.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {
client, err := commoncfg.NewClientFromConfig(*c.HTTPConfig, "msteamsv2", httpOpts...)
if err != nil {
return nil, err
}
n := &Notifier{
conf: c,
tmpl: t,
logger: l,
client: client,
retrier: &notify.Retrier{},
webhookURL: c.WebhookURL,
postJSONFunc: notify.PostJSON,
}
return n, nil
}
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
}
title := tmpl(n.conf.Title)
if err != nil {
return false, err
}
text := tmpl(n.conf.Text)
if err != nil {
return false, err
}
alerts := types.Alerts(as...)
color := colorGrey
switch alerts.Status() {
case model.AlertFiring:
color = colorRed
case model.AlertResolved:
color = colorGreen
}
var url string
if n.conf.WebhookURL != nil {
url = n.conf.WebhookURL.String()
} else {
content, err := os.ReadFile(n.conf.WebhookURLFile)
if err != nil {
return false, fmt.Errorf("read webhook_url_file: %w", err)
}
url = strings.TrimSpace(string(content))
}
// A message as referenced in https://learn.microsoft.com/en-us/connectors/teams/?tabs=text1%2Cdotnet#request-body-schema
t := teamsMessage{
Type: "message",
Attachments: []Attachment{
{
ContentType: "application/vnd.microsoft.card.adaptive",
ContentURL: nil,
Content: Content{
Schema: "http://adaptivecards.io/schemas/adaptive-card.json",
Type: "AdaptiveCard",
Version: "1.2",
Body: []Body{
{
Type: "TextBlock",
Text: title,
Weight: "Bolder",
Size: "Medium",
Wrap: true,
Style: "heading",
Color: color,
},
{
Type: "TextBlock",
Text: text,
},
},
},
},
},
}
var payload bytes.Buffer
if err = json.NewEncoder(&payload).Encode(t); err != nil {
return false, err
}
resp, err := n.postJSONFunc(ctx, n.client, url, &payload)
if err != nil {
return true, notify.RedactURL(err)
}
defer notify.Drain(resp)
// https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?tabs=cURL#rate-limiting-for-connectors
shouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body)
if err != nil {
return shouldRetry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err)
}
return shouldRetry, err
}

View File

@ -0,0 +1,225 @@
// Copyright 2024 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 msteamsv2
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"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://example.westeurope.logic.azure.com:443/workflows/xxx/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=xxx")
func TestMSTeamsV2Retry(t *testing.T) {
notifier, err := New(
&config.MSTeamsV2Config{
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 TestNotifier_Notify_WithReason(t *testing.T) {
tests := []struct {
name string
statusCode int
responseContent string
expectedReason notify.Reason
noError bool
}{
{
name: "with a 2xx status code and response 1",
statusCode: http.StatusOK,
responseContent: "1",
noError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
notifier, err := New(
&config.MSTeamsV2Config{
WebhookURL: &config.SecretURL{URL: testWebhookURL},
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
test.CreateTmpl(t),
log.NewNopLogger(),
)
require.NoError(t, err)
notifier.postJSONFunc = func(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error) {
resp := httptest.NewRecorder()
resp.WriteString(tt.responseContent)
resp.WriteHeader(tt.statusCode)
return resp.Result(), nil
}
ctx := context.Background()
ctx = notify.WithGroupKey(ctx, "1")
alert1 := &types.Alert{
Alert: model.Alert{
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
},
}
_, err = notifier.Notify(ctx, alert1)
if tt.noError {
require.NoError(t, err)
} else {
var reasonError *notify.ErrorWithReason
require.ErrorAs(t, err, &reasonError)
require.Equal(t, tt.expectedReason, reasonError.Reason)
}
})
}
}
func TestMSTeamsV2Templating(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.MSTeamsV2Config
retry bool
errMsg string
}{
{
title: "full-blown message",
cfg: &config.MSTeamsV2Config{
Title: `{{ template "msteams.default.title" . }}`,
Text: `{{ template "msteams.default.text" . }}`,
},
retry: false,
},
{
title: "title with templating errors",
cfg: &config.MSTeamsV2Config{
Title: "{{ ",
},
errMsg: "template: :1: unclosed action",
},
{
title: "message with templating errors",
cfg: &config.MSTeamsV2Config{
Title: `{{ template "msteams.default.title" . }}`,
Text: "{{ ",
},
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)
})
}
}
func TestMSTeamsV2RedactedURL(t *testing.T) {
ctx, u, fn := test.GetContextWithCancelingURL()
defer fn()
secret := "secret"
notifier, err := New(
&config.MSTeamsV2Config{
WebhookURL: &config.SecretURL{URL: u},
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
test.CreateTmpl(t),
log.NewNopLogger(),
)
require.NoError(t, err)
test.AssertNotifyLeaksNoSecret(ctx, t, notifier, secret)
}
func TestMSTeamsV2ReadingURLFromFile(t *testing.T) {
ctx, u, fn := test.GetContextWithCancelingURL()
defer fn()
f, err := os.CreateTemp("", "webhook_url")
require.NoError(t, err, "creating temp file failed")
_, err = f.WriteString(u.String() + "\n")
require.NoError(t, err, "writing to temp file failed")
notifier, err := New(
&config.MSTeamsV2Config{
WebhookURLFile: f.Name(),
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
test.CreateTmpl(t),
log.NewNopLogger(),
)
require.NoError(t, err)
test.AssertNotifyLeaksNoSecret(ctx, t, notifier, u.String())
}

View File

@ -365,6 +365,7 @@ func (m *Metrics) InitializeFor(receiver map[string][]Integration) {
"discord",
"webex",
"msteams",
"msteamsv2",
"jira",
} {
m.numNotifications.WithLabelValues(integration)

View File

@ -159,6 +159,18 @@ Alerts Resolved:
{{ end }}
{{ end }}
{{ define "msteamsv2.default.title" }}{{ template "__subject" . }}{{ end }}
{{ define "msteamsv2.default.text" }}
{{ if gt (len .Alerts.Firing) 0 }}
# Alerts Firing:
{{ template "__text_alert_list_markdown" .Alerts.Firing }}
{{ end }}
{{ if gt (len .Alerts.Resolved) 0 }}
# Alerts Resolved:
{{ template "__text_alert_list_markdown" .Alerts.Resolved }}
{{ end }}
{{ end }}
{{ define "jira.default.summary" }}{{ template "__subject" . }}{{ end }}
{{ define "jira.default.description" }}
{{ if gt (len .Alerts.Firing) 0 }}