mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Refactor MS teams, Pushover and Webhook notifiers to use encoding/json to parse settings (#56834)
* update teams * update sensugo * update pushover * update webhook to use json.Number
This commit is contained in:
parent
5226a61f67
commit
8c72d19bcc
@ -3,6 +3,7 @@ package channels
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@ -42,10 +43,10 @@ type PushoverNotifier struct {
|
||||
type pushoverSettings struct {
|
||||
userKey string
|
||||
apiToken string
|
||||
alertingPriority int
|
||||
okPriority int
|
||||
retry int
|
||||
expire int
|
||||
alertingPriority int64
|
||||
okPriority int64
|
||||
retry int64
|
||||
expire int64
|
||||
device string
|
||||
alertingSound string
|
||||
okSound string
|
||||
@ -54,40 +55,91 @@ type pushoverSettings struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func buildPushoverSettings(fc FactoryConfig) (pushoverSettings, error) {
|
||||
settings := pushoverSettings{}
|
||||
rawSettings := struct {
|
||||
UserKey string `json:"userKey,omitempty" yaml:"userKey,omitempty"`
|
||||
APIToken string `json:"apiToken,omitempty" yaml:"apiToken,omitempty"`
|
||||
AlertingPriority json.Number `json:"priority,omitempty" yaml:"priority,omitempty"`
|
||||
OKPriority json.Number `json:"okPriority,omitempty" yaml:"okPriority,omitempty"`
|
||||
Retry json.Number `json:"retry,omitempty" yaml:"retry,omitempty"`
|
||||
Expire json.Number `json:"expire,omitempty" yaml:"expire,omitempty"`
|
||||
Device string `json:"device,omitempty" yaml:"device,omitempty"`
|
||||
AlertingSound string `json:"sound,omitempty" yaml:"sound,omitempty"`
|
||||
OKSound string `json:"okSound,omitempty" yaml:"okSound,omitempty"`
|
||||
Upload *bool `json:"uploadImage,omitempty" yaml:"uploadImage,omitempty"`
|
||||
Title string `json:"title,omitempty" yaml:"title,omitempty"`
|
||||
Message string `json:"message,omitempty" yaml:"message,omitempty"`
|
||||
}{}
|
||||
|
||||
err := fc.Config.unmarshalSettings(&rawSettings)
|
||||
if err != nil {
|
||||
return settings, fmt.Errorf("failed to unmarshal settings: %w", err)
|
||||
}
|
||||
|
||||
settings.userKey = fc.DecryptFunc(context.Background(), fc.Config.SecureSettings, "userKey", rawSettings.UserKey)
|
||||
if settings.userKey == "" {
|
||||
return settings, errors.New("user key not found")
|
||||
}
|
||||
settings.apiToken = fc.DecryptFunc(context.Background(), fc.Config.SecureSettings, "apiToken", rawSettings.APIToken)
|
||||
if settings.apiToken == "" {
|
||||
return settings, errors.New("API token not found")
|
||||
}
|
||||
if rawSettings.AlertingPriority != "" {
|
||||
settings.alertingPriority, err = rawSettings.AlertingPriority.Int64()
|
||||
if err != nil {
|
||||
return settings, fmt.Errorf("failed to convert alerting priority to integer: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if rawSettings.OKPriority != "" {
|
||||
settings.okPriority, err = rawSettings.OKPriority.Int64()
|
||||
if err != nil {
|
||||
return settings, fmt.Errorf("failed to convert OK priority to integer: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
settings.retry, _ = rawSettings.Retry.Int64()
|
||||
settings.expire, _ = rawSettings.Expire.Int64()
|
||||
|
||||
settings.device = rawSettings.Device
|
||||
settings.alertingSound = rawSettings.AlertingSound
|
||||
settings.okSound = rawSettings.OKSound
|
||||
|
||||
if rawSettings.Upload == nil || *rawSettings.Upload {
|
||||
settings.upload = true
|
||||
}
|
||||
|
||||
settings.message = rawSettings.Message
|
||||
if settings.message == "" {
|
||||
settings.message = DefaultMessageEmbed
|
||||
}
|
||||
|
||||
settings.title = rawSettings.Title
|
||||
if settings.title == "" {
|
||||
settings.title = DefaultMessageTitleEmbed
|
||||
}
|
||||
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
func PushoverFactory(fc FactoryConfig) (NotificationChannel, error) {
|
||||
pn, err := newPushoverNotifier(fc)
|
||||
notifier, err := NewPushoverNotifier(fc)
|
||||
if err != nil {
|
||||
return nil, receiverInitError{
|
||||
Reason: err.Error(),
|
||||
Cfg: *fc.Config,
|
||||
}
|
||||
}
|
||||
return pn, nil
|
||||
return notifier, nil
|
||||
}
|
||||
|
||||
// newPushoverNotifier is the constructor for the Pushover notifier
|
||||
func newPushoverNotifier(fc FactoryConfig) (*PushoverNotifier, error) {
|
||||
decryptFunc := fc.DecryptFunc
|
||||
userKey := decryptFunc(context.Background(), fc.Config.SecureSettings, "userKey", fc.Config.Settings.Get("userKey").MustString())
|
||||
if userKey == "" {
|
||||
return nil, errors.New("user key not found")
|
||||
}
|
||||
apiToken := decryptFunc(context.Background(), fc.Config.SecureSettings, "apiToken", fc.Config.Settings.Get("apiToken").MustString())
|
||||
if apiToken == "" {
|
||||
return nil, errors.New("API token not found")
|
||||
}
|
||||
|
||||
alertingPriority, err := strconv.Atoi(fc.Config.Settings.Get("priority").MustString("0")) // default Normal
|
||||
// NewSlackNotifier is the constructor for the Slack notifier
|
||||
func NewPushoverNotifier(fc FactoryConfig) (*PushoverNotifier, error) {
|
||||
settings, err := buildPushoverSettings(fc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert alerting priority to integer: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
okPriority, err := strconv.Atoi(fc.Config.Settings.Get("okPriority").MustString("0")) // default Normal
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert OK priority to integer: %w", err)
|
||||
}
|
||||
retry, _ := strconv.Atoi(fc.Config.Settings.Get("retry").MustString())
|
||||
expire, _ := strconv.Atoi(fc.Config.Settings.Get("expire").MustString())
|
||||
|
||||
return &PushoverNotifier{
|
||||
Base: NewBase(&models.AlertNotification{
|
||||
Uid: fc.Config.UID,
|
||||
@ -97,24 +149,11 @@ func newPushoverNotifier(fc FactoryConfig) (*PushoverNotifier, error) {
|
||||
Settings: fc.Config.Settings,
|
||||
SecureSettings: fc.Config.SecureSettings,
|
||||
}),
|
||||
tmpl: fc.Template,
|
||||
log: log.New("alerting.notifier.pushover"),
|
||||
images: fc.ImageStore,
|
||||
ns: fc.NotificationService,
|
||||
settings: pushoverSettings{
|
||||
userKey: userKey,
|
||||
apiToken: apiToken,
|
||||
alertingPriority: alertingPriority,
|
||||
okPriority: okPriority,
|
||||
retry: retry,
|
||||
expire: expire,
|
||||
device: fc.Config.Settings.Get("device").MustString(),
|
||||
alertingSound: fc.Config.Settings.Get("sound").MustString(),
|
||||
okSound: fc.Config.Settings.Get("okSound").MustString(),
|
||||
upload: fc.Config.Settings.Get("uploadImage").MustBool(true),
|
||||
title: fc.Config.Settings.Get("title").MustString(DefaultMessageTitleEmbed),
|
||||
message: fc.Config.Settings.Get("message").MustString(DefaultMessageEmbed),
|
||||
},
|
||||
tmpl: fc.Template,
|
||||
log: log.New("alerting.notifier.pushover"),
|
||||
images: fc.ImageStore,
|
||||
ns: fc.NotificationService,
|
||||
settings: settings,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -172,16 +211,16 @@ func (pn *PushoverNotifier) genPushoverBody(ctx context.Context, as ...*types.Al
|
||||
if status == model.AlertResolved {
|
||||
priority = pn.settings.okPriority
|
||||
}
|
||||
if err := w.WriteField("priority", strconv.Itoa(priority)); err != nil {
|
||||
if err := w.WriteField("priority", strconv.FormatInt(priority, 10)); err != nil {
|
||||
return nil, b, fmt.Errorf("failed to write the priority: %w", err)
|
||||
}
|
||||
|
||||
if priority == 2 {
|
||||
if err := w.WriteField("retry", strconv.Itoa(pn.settings.retry)); err != nil {
|
||||
if err := w.WriteField("retry", strconv.FormatInt(pn.settings.retry, 10)); err != nil {
|
||||
return nil, b, fmt.Errorf("failed to write retry: %w", err)
|
||||
}
|
||||
|
||||
if err := w.WriteField("expire", strconv.Itoa(pn.settings.expire)); err != nil {
|
||||
if err := w.WriteField("expire", strconv.FormatInt(pn.settings.expire, 10)); err != nil {
|
||||
return nil, b, fmt.Errorf("failed to write expire: %w", err)
|
||||
}
|
||||
}
|
||||
|
@ -140,6 +140,50 @@ func TestPushoverNotifier(t *testing.T) {
|
||||
},
|
||||
expMsgError: nil,
|
||||
},
|
||||
{
|
||||
name: "Integer fields as integers",
|
||||
settings: `{
|
||||
"userKey": "<userKey>",
|
||||
"apiToken": "<apiToken>",
|
||||
"device": "device",
|
||||
"priority": 2,
|
||||
"okpriority": 0,
|
||||
"retry": 30,
|
||||
"expire": 86400,
|
||||
"sound": "echo",
|
||||
"oksound": "magic",
|
||||
"message": "{{ len .Alerts.Firing }} alerts are firing, {{ len .Alerts.Resolved }} are resolved"
|
||||
}`,
|
||||
alerts: []*types.Alert{
|
||||
{
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{"__alert_rule_uid__": "rule uid", "alertname": "alert1", "lbl1": "val1"},
|
||||
Annotations: model.LabelSet{"ann1": "annv1", "__alertImageToken__": "test-image-1"},
|
||||
},
|
||||
}, {
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val2"},
|
||||
Annotations: model.LabelSet{"ann1": "annv2", "__alertImageToken__": "test-image-2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expMsg: map[string]string{
|
||||
"user": "<userKey>",
|
||||
"token": "<apiToken>",
|
||||
"priority": "2",
|
||||
"sound": "echo",
|
||||
"title": "[FIRING:2] ",
|
||||
"url": "http://localhost/alerting/list",
|
||||
"url_title": "Show alert rule",
|
||||
"message": "2 alerts are firing, 0 are resolved",
|
||||
"attachment": "\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\b\x04\x00\x00\x00\xb5\x1c\f\x02\x00\x00\x00\vIDATx\xdacd`\x00\x00\x00\x06\x00\x020\x81\xd0/\x00\x00\x00\x00IEND\xaeB`\x82",
|
||||
"html": "1",
|
||||
"retry": "30",
|
||||
"expire": "86400",
|
||||
"device": "device",
|
||||
},
|
||||
expMsgError: nil,
|
||||
},
|
||||
{
|
||||
name: "Missing user key",
|
||||
settings: `{
|
||||
@ -186,7 +230,8 @@ func TestPushoverNotifier(t *testing.T) {
|
||||
DecryptFunc: decryptFn,
|
||||
Template: tmpl,
|
||||
}
|
||||
pn, err := newPushoverNotifier(fc)
|
||||
|
||||
pn, err := NewPushoverNotifier(fc)
|
||||
if c.expInitError != "" {
|
||||
require.Error(t, err)
|
||||
require.Equal(t, c.expInitError, err.Error())
|
||||
|
@ -19,86 +19,74 @@ import (
|
||||
|
||||
type SensuGoNotifier struct {
|
||||
*Base
|
||||
log log.Logger
|
||||
images ImageStore
|
||||
ns notifications.WebhookSender
|
||||
tmpl *template.Template
|
||||
|
||||
URL string
|
||||
Entity string
|
||||
Check string
|
||||
Namespace string
|
||||
Handler string
|
||||
APIKey string
|
||||
Message string
|
||||
log log.Logger
|
||||
images ImageStore
|
||||
ns notifications.WebhookSender
|
||||
tmpl *template.Template
|
||||
settings sensuGoSettings
|
||||
}
|
||||
|
||||
type SensuGoConfig struct {
|
||||
*NotificationChannelConfig
|
||||
URL string
|
||||
Entity string
|
||||
Check string
|
||||
Namespace string
|
||||
Handler string
|
||||
APIKey string
|
||||
Message string
|
||||
type sensuGoSettings struct {
|
||||
URL string `json:"url,omitempty" yaml:"url,omitempty"`
|
||||
Entity string `json:"entity,omitempty" yaml:"entity,omitempty"`
|
||||
Check string `json:"check,omitempty" yaml:"check,omitempty"`
|
||||
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
|
||||
Handler string `json:"handler,omitempty" yaml:"handler,omitempty"`
|
||||
APIKey string `json:"apikey,omitempty" yaml:"apikey,omitempty"`
|
||||
Message string `json:"message,omitempty" yaml:"message,omitempty"`
|
||||
}
|
||||
|
||||
func buildSensuGoConfig(fc FactoryConfig) (sensuGoSettings, error) {
|
||||
settings := sensuGoSettings{}
|
||||
err := fc.Config.unmarshalSettings(&settings)
|
||||
if err != nil {
|
||||
return settings, fmt.Errorf("failed to unmarshal settings: %w", err)
|
||||
}
|
||||
if settings.URL == "" {
|
||||
return settings, errors.New("could not find URL property in settings")
|
||||
}
|
||||
settings.APIKey = fc.DecryptFunc(context.Background(), fc.Config.SecureSettings, "apikey", settings.APIKey)
|
||||
if settings.APIKey == "" {
|
||||
return settings, errors.New("could not find the API key property in settings")
|
||||
}
|
||||
if settings.Message == "" {
|
||||
settings.Message = DefaultMessageEmbed
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
func SensuGoFactory(fc FactoryConfig) (NotificationChannel, error) {
|
||||
cfg, err := NewSensuGoConfig(fc.Config, fc.DecryptFunc)
|
||||
notifier, err := NewSensuGoNotifier(fc)
|
||||
if err != nil {
|
||||
return nil, receiverInitError{
|
||||
Reason: err.Error(),
|
||||
Cfg: *fc.Config,
|
||||
}
|
||||
}
|
||||
return NewSensuGoNotifier(cfg, fc.ImageStore, fc.NotificationService, fc.Template), nil
|
||||
}
|
||||
|
||||
func NewSensuGoConfig(config *NotificationChannelConfig, decryptFunc GetDecryptedValueFn) (*SensuGoConfig, error) {
|
||||
url := config.Settings.Get("url").MustString()
|
||||
if url == "" {
|
||||
return nil, errors.New("could not find URL property in settings")
|
||||
}
|
||||
apikey := decryptFunc(context.Background(), config.SecureSettings, "apikey", config.Settings.Get("apikey").MustString())
|
||||
if apikey == "" {
|
||||
return nil, errors.New("could not find the API key property in settings")
|
||||
}
|
||||
return &SensuGoConfig{
|
||||
NotificationChannelConfig: config,
|
||||
URL: url,
|
||||
Entity: config.Settings.Get("entity").MustString(),
|
||||
Check: config.Settings.Get("check").MustString(),
|
||||
Namespace: config.Settings.Get("namespace").MustString(),
|
||||
Handler: config.Settings.Get("handler").MustString(),
|
||||
APIKey: apikey,
|
||||
Message: config.Settings.Get("message").MustString(DefaultMessageEmbed),
|
||||
}, nil
|
||||
return notifier, nil
|
||||
}
|
||||
|
||||
// NewSensuGoNotifier is the constructor for the SensuGo notifier
|
||||
func NewSensuGoNotifier(config *SensuGoConfig, images ImageStore, ns notifications.WebhookSender, t *template.Template) *SensuGoNotifier {
|
||||
func NewSensuGoNotifier(fc FactoryConfig) (*SensuGoNotifier, error) {
|
||||
settings, err := buildSensuGoConfig(fc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SensuGoNotifier{
|
||||
Base: NewBase(&models.AlertNotification{
|
||||
Uid: config.UID,
|
||||
Name: config.Name,
|
||||
Type: config.Type,
|
||||
DisableResolveMessage: config.DisableResolveMessage,
|
||||
Settings: config.Settings,
|
||||
SecureSettings: config.SecureSettings,
|
||||
Uid: fc.Config.UID,
|
||||
Name: fc.Config.Name,
|
||||
Type: fc.Config.Type,
|
||||
DisableResolveMessage: fc.Config.DisableResolveMessage,
|
||||
Settings: fc.Config.Settings,
|
||||
SecureSettings: fc.Config.SecureSettings,
|
||||
}),
|
||||
URL: config.URL,
|
||||
Entity: config.Entity,
|
||||
Check: config.Check,
|
||||
Namespace: config.Namespace,
|
||||
Handler: config.Handler,
|
||||
APIKey: config.APIKey,
|
||||
Message: config.Message,
|
||||
log: log.New("alerting.notifier.sensugo"),
|
||||
images: images,
|
||||
ns: ns,
|
||||
tmpl: t,
|
||||
}
|
||||
log: log.New("alerting.notifier.sensugo"),
|
||||
images: fc.ImageStore,
|
||||
ns: fc.NotificationService,
|
||||
tmpl: fc.Template,
|
||||
settings: settings,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Notify sends an alert notification to Sensu Go
|
||||
@ -110,12 +98,12 @@ func (sn *SensuGoNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool
|
||||
|
||||
// Sensu Go alerts require an entity and a check. We set it to the user-specified
|
||||
// value (optional), else we fallback and use the grafana rule anme and ruleID.
|
||||
entity := tmpl(sn.Entity)
|
||||
entity := tmpl(sn.settings.Entity)
|
||||
if entity == "" {
|
||||
entity = "default"
|
||||
}
|
||||
|
||||
check := tmpl(sn.Check)
|
||||
check := tmpl(sn.settings.Check)
|
||||
if check == "" {
|
||||
check = "default"
|
||||
}
|
||||
@ -127,14 +115,14 @@ func (sn *SensuGoNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool
|
||||
status = 2
|
||||
}
|
||||
|
||||
namespace := tmpl(sn.Namespace)
|
||||
namespace := tmpl(sn.settings.Namespace)
|
||||
if namespace == "" {
|
||||
namespace = "default"
|
||||
}
|
||||
|
||||
var handlers []string
|
||||
if sn.Handler != "" {
|
||||
handlers = []string{tmpl(sn.Handler)}
|
||||
if sn.settings.Handler != "" {
|
||||
handlers = []string{tmpl(sn.settings.Handler)}
|
||||
}
|
||||
|
||||
labels := make(map[string]string)
|
||||
@ -166,7 +154,7 @@ func (sn *SensuGoNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool
|
||||
"name": check,
|
||||
"labels": labels,
|
||||
},
|
||||
"output": tmpl(sn.Message),
|
||||
"output": tmpl(sn.settings.Message),
|
||||
"issued": timeNow().Unix(),
|
||||
"interval": 86400,
|
||||
"status": status,
|
||||
@ -185,12 +173,12 @@ func (sn *SensuGoNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool
|
||||
}
|
||||
|
||||
cmd := &models.SendWebhookSync{
|
||||
Url: fmt.Sprintf("%s/api/core/v2/namespaces/%s/events", strings.TrimSuffix(sn.URL, "/"), namespace),
|
||||
Url: fmt.Sprintf("%s/api/core/v2/namespaces/%s/events", strings.TrimSuffix(sn.settings.URL, "/"), namespace),
|
||||
Body: string(body),
|
||||
HttpMethod: "POST",
|
||||
HttpHeader: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": fmt.Sprintf("Key %s", sn.APIKey),
|
||||
"Authorization": fmt.Sprintf("Key %s", sn.settings.APIKey),
|
||||
},
|
||||
}
|
||||
if err := sn.ns.SendWebhookSync(ctx, cmd); err != nil {
|
||||
|
@ -151,7 +151,16 @@ func TestSensuGoNotifier(t *testing.T) {
|
||||
webhookSender := mockNotificationService()
|
||||
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||
decryptFn := secretsService.GetDecryptedValue
|
||||
cfg, err := NewSensuGoConfig(m, decryptFn)
|
||||
|
||||
fc := FactoryConfig{
|
||||
Config: m,
|
||||
ImageStore: images,
|
||||
NotificationService: webhookSender,
|
||||
Template: tmpl,
|
||||
DecryptFunc: decryptFn,
|
||||
}
|
||||
|
||||
sn, err := NewSensuGoNotifier(fc)
|
||||
if c.expInitError != "" {
|
||||
require.Error(t, err)
|
||||
require.Equal(t, c.expInitError, err.Error())
|
||||
@ -161,7 +170,6 @@ func TestSensuGoNotifier(t *testing.T) {
|
||||
|
||||
ctx := notify.WithGroupKey(context.Background(), "alertname")
|
||||
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
|
||||
sn := NewSensuGoNotifier(cfg, images, webhookSender, tmpl)
|
||||
ok, err := sn.Notify(ctx, c.alerts...)
|
||||
if c.expMsgError != nil {
|
||||
require.False(t, ok)
|
||||
|
@ -224,70 +224,71 @@ func (i AdaptiveCardOpenURLActionItem) MarshalJSON() ([]byte, error) {
|
||||
})
|
||||
}
|
||||
|
||||
type TeamsConfig struct {
|
||||
*NotificationChannelConfig
|
||||
URL string
|
||||
Message string
|
||||
Title string
|
||||
SectionTitle string
|
||||
type teamsSettings struct {
|
||||
URL string `json:"url,omitempty" yaml:"url,omitempty"`
|
||||
Message string `json:"message,omitempty" yaml:"message,omitempty"`
|
||||
Title string `json:"title,omitempty" yaml:"title,omitempty"`
|
||||
SectionTitle string `json:"sectiontitle,omitempty" yaml:"sectiontitle,omitempty"`
|
||||
}
|
||||
|
||||
func NewTeamsConfig(config *NotificationChannelConfig) (*TeamsConfig, error) {
|
||||
URL := config.Settings.Get("url").MustString()
|
||||
if URL == "" {
|
||||
return nil, errors.New("could not find url property in settings")
|
||||
func buildTeamsSettings(fc FactoryConfig) (teamsSettings, error) {
|
||||
settings := teamsSettings{}
|
||||
err := fc.Config.unmarshalSettings(&settings)
|
||||
if err != nil {
|
||||
return settings, fmt.Errorf("failed to unmarshal settings: %w", err)
|
||||
}
|
||||
return &TeamsConfig{
|
||||
NotificationChannelConfig: config,
|
||||
URL: URL,
|
||||
Message: config.Settings.Get("message").MustString(`{{ template "teams.default.message" .}}`),
|
||||
Title: config.Settings.Get("title").MustString(DefaultMessageTitleEmbed),
|
||||
SectionTitle: config.Settings.Get("sectiontitle").MustString(""),
|
||||
}, nil
|
||||
if settings.URL == "" {
|
||||
return settings, errors.New("could not find url property in settings")
|
||||
}
|
||||
if settings.Message == "" {
|
||||
settings.Message = `{{ template "teams.default.message" .}}`
|
||||
}
|
||||
if settings.Title == "" {
|
||||
settings.Title = DefaultMessageTitleEmbed
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
type TeamsNotifier struct {
|
||||
*Base
|
||||
URL string
|
||||
Message string
|
||||
Title string
|
||||
SectionTitle string
|
||||
tmpl *template.Template
|
||||
log log.Logger
|
||||
ns notifications.WebhookSender
|
||||
images ImageStore
|
||||
tmpl *template.Template
|
||||
log log.Logger
|
||||
ns notifications.WebhookSender
|
||||
images ImageStore
|
||||
settings teamsSettings
|
||||
}
|
||||
|
||||
// NewTeamsNotifier is the constructor for Teams notifier.
|
||||
func NewTeamsNotifier(config *TeamsConfig, ns notifications.WebhookSender, images ImageStore, t *template.Template) *TeamsNotifier {
|
||||
func NewTeamsNotifier(fc FactoryConfig) (*TeamsNotifier, error) {
|
||||
settings, err := buildTeamsSettings(fc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &TeamsNotifier{
|
||||
Base: NewBase(&models.AlertNotification{
|
||||
Uid: config.UID,
|
||||
Name: config.Name,
|
||||
Type: config.Type,
|
||||
DisableResolveMessage: config.DisableResolveMessage,
|
||||
Settings: config.Settings,
|
||||
Uid: fc.Config.UID,
|
||||
Name: fc.Config.Name,
|
||||
Type: fc.Config.Type,
|
||||
DisableResolveMessage: fc.Config.DisableResolveMessage,
|
||||
Settings: fc.Config.Settings,
|
||||
}),
|
||||
URL: config.URL,
|
||||
Message: config.Message,
|
||||
Title: config.Title,
|
||||
SectionTitle: config.SectionTitle,
|
||||
log: log.New("alerting.notifier.teams"),
|
||||
ns: ns,
|
||||
images: images,
|
||||
tmpl: t,
|
||||
}
|
||||
log: log.New("alerting.notifier.teams"),
|
||||
ns: fc.NotificationService,
|
||||
images: fc.ImageStore,
|
||||
tmpl: fc.Template,
|
||||
settings: settings,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TeamsFactory(fc FactoryConfig) (NotificationChannel, error) {
|
||||
cfg, err := NewTeamsConfig(fc.Config)
|
||||
notifier, err := NewTeamsNotifier(fc)
|
||||
if err != nil {
|
||||
return nil, receiverInitError{
|
||||
Reason: err.Error(),
|
||||
Cfg: *fc.Config,
|
||||
}
|
||||
}
|
||||
return NewTeamsNotifier(cfg, fc.NotificationService, fc.ImageStore, fc.Template), nil
|
||||
return notifier, nil
|
||||
}
|
||||
|
||||
func (tn *TeamsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
|
||||
@ -297,13 +298,13 @@ func (tn *TeamsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
|
||||
card := NewAdaptiveCard()
|
||||
card.AppendItem(AdaptiveCardTextBlockItem{
|
||||
Color: getTeamsTextColor(types.Alerts(as...)),
|
||||
Text: tmpl(tn.Title),
|
||||
Text: tmpl(tn.settings.Title),
|
||||
Size: TextSizeLarge,
|
||||
Weight: TextWeightBolder,
|
||||
Wrap: true,
|
||||
})
|
||||
card.AppendItem(AdaptiveCardTextBlockItem{
|
||||
Text: tmpl(tn.Message),
|
||||
Text: tmpl(tn.settings.Message),
|
||||
Wrap: true,
|
||||
})
|
||||
|
||||
@ -335,7 +336,7 @@ func (tn *TeamsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
|
||||
})
|
||||
|
||||
msg := NewAdaptiveCardsMessage(card)
|
||||
msg.Summary = tmpl(tn.Title)
|
||||
msg.Summary = tmpl(tn.settings.Title)
|
||||
|
||||
// This check for tmplErr must happen before templating the URL
|
||||
if tmplErr != nil {
|
||||
@ -343,10 +344,10 @@ func (tn *TeamsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
|
||||
tmplErr = nil
|
||||
}
|
||||
|
||||
u := tmpl(tn.URL)
|
||||
u := tmpl(tn.settings.URL)
|
||||
if tmplErr != nil {
|
||||
tn.log.Warn("failed to template Teams URL", "error", tmplErr.Error(), "fallback", tn.URL)
|
||||
u = tn.URL
|
||||
tn.log.Warn("failed to template Teams URL", "error", tmplErr.Error(), "fallback", tn.settings.URL)
|
||||
u = tn.settings.URL
|
||||
}
|
||||
|
||||
b, err := json.Marshal(msg)
|
||||
|
@ -289,7 +289,14 @@ func TestTeamsNotifier(t *testing.T) {
|
||||
clientStub := newMockClient(c.response)
|
||||
notifications.SetWebhookClient(clientStub)
|
||||
|
||||
cfg, err := NewTeamsConfig(m)
|
||||
fc := FactoryConfig{
|
||||
Config: m,
|
||||
ImageStore: &UnavailableImageStore{},
|
||||
NotificationService: webhookSender,
|
||||
Template: tmpl,
|
||||
}
|
||||
|
||||
pn, err := NewTeamsNotifier(fc)
|
||||
if c.expInitError != "" {
|
||||
require.Error(t, err)
|
||||
require.Equal(t, c.expInitError, err.Error())
|
||||
@ -299,7 +306,7 @@ func TestTeamsNotifier(t *testing.T) {
|
||||
|
||||
ctx := notify.WithGroupKey(context.Background(), "alertname")
|
||||
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
|
||||
pn := NewTeamsNotifier(cfg, webhookSender, &UnavailableImageStore{}, tmpl)
|
||||
|
||||
ok, err := pn.Notify(ctx, c.alerts...)
|
||||
if c.expMsgError != nil {
|
||||
require.False(t, ok)
|
||||
|
@ -23,55 +23,76 @@ import (
|
||||
// alert notifications as webhooks.
|
||||
type WebhookNotifier struct {
|
||||
*Base
|
||||
log log.Logger
|
||||
ns notifications.WebhookSender
|
||||
images ImageStore
|
||||
tmpl *template.Template
|
||||
orgID int64
|
||||
maxAlerts int
|
||||
settings webhookSettings
|
||||
log log.Logger
|
||||
ns notifications.WebhookSender
|
||||
images ImageStore
|
||||
tmpl *template.Template
|
||||
orgID int64
|
||||
settings webhookSettings
|
||||
}
|
||||
|
||||
type webhookSettings struct {
|
||||
URL string `json:"url,omitempty" yaml:"url,omitempty"`
|
||||
HTTPMethod string `json:"httpMethod,omitempty" yaml:"httpMethod,omitempty"`
|
||||
MaxAlerts interface{} `json:"maxAlerts,omitempty" yaml:"maxAlerts,omitempty"`
|
||||
|
||||
URL string
|
||||
HTTPMethod string
|
||||
MaxAlerts int
|
||||
// Authorization Header.
|
||||
AuthorizationScheme string `json:"authorization_scheme,omitempty" yaml:"authorization_scheme,omitempty"`
|
||||
AuthorizationCredentials string `json:"authorization_credentials,omitempty" yaml:"authorization_credentials,omitempty"`
|
||||
AuthorizationScheme string
|
||||
AuthorizationCredentials string
|
||||
// HTTP Basic Authentication.
|
||||
User string `json:"username,omitempty" yaml:"username,omitempty"`
|
||||
Password string `json:"password,omitempty" yaml:"password,omitempty"`
|
||||
User string
|
||||
Password string
|
||||
|
||||
Title string `json:"title,omitempty" yaml:"title,omitempty"`
|
||||
Message string `json:"message,omitempty" yaml:"message,omitempty"`
|
||||
Title string
|
||||
Message string
|
||||
}
|
||||
|
||||
func buildWebhookSettings(factoryConfig FactoryConfig) (webhookSettings, error) {
|
||||
settings := webhookSettings{}
|
||||
err := factoryConfig.Config.unmarshalSettings(&settings)
|
||||
rawSettings := struct {
|
||||
URL string `json:"url,omitempty" yaml:"url,omitempty"`
|
||||
HTTPMethod string `json:"httpMethod,omitempty" yaml:"httpMethod,omitempty"`
|
||||
MaxAlerts json.Number `json:"maxAlerts,omitempty" yaml:"maxAlerts,omitempty"`
|
||||
AuthorizationScheme string `json:"authorization_scheme,omitempty" yaml:"authorization_scheme,omitempty"`
|
||||
AuthorizationCredentials string `json:"authorization_credentials,omitempty" yaml:"authorization_credentials,omitempty"`
|
||||
User string `json:"username,omitempty" yaml:"username,omitempty"`
|
||||
Password string `json:"password,omitempty" yaml:"password,omitempty"`
|
||||
Title string `json:"title,omitempty" yaml:"title,omitempty"`
|
||||
Message string `json:"message,omitempty" yaml:"message,omitempty"`
|
||||
}{}
|
||||
|
||||
err := factoryConfig.Config.unmarshalSettings(&rawSettings)
|
||||
if err != nil {
|
||||
return settings, fmt.Errorf("failed to unmarshal settings: %w", err)
|
||||
}
|
||||
if settings.URL == "" {
|
||||
if rawSettings.URL == "" {
|
||||
return settings, errors.New("required field 'url' is not specified")
|
||||
}
|
||||
if settings.HTTPMethod == "" {
|
||||
settings.HTTPMethod = http.MethodPost
|
||||
settings.URL = rawSettings.URL
|
||||
|
||||
if rawSettings.HTTPMethod == "" {
|
||||
rawSettings.HTTPMethod = http.MethodPost
|
||||
}
|
||||
settings.User = factoryConfig.DecryptFunc(context.Background(), factoryConfig.Config.SecureSettings, "username", settings.User)
|
||||
settings.Password = factoryConfig.DecryptFunc(context.Background(), factoryConfig.Config.SecureSettings, "password", settings.Password)
|
||||
settings.AuthorizationCredentials = factoryConfig.DecryptFunc(context.Background(), factoryConfig.Config.SecureSettings, "authorization_scheme", settings.AuthorizationCredentials)
|
||||
settings.HTTPMethod = rawSettings.HTTPMethod
|
||||
|
||||
if rawSettings.MaxAlerts != "" {
|
||||
settings.MaxAlerts, _ = strconv.Atoi(rawSettings.MaxAlerts.String())
|
||||
}
|
||||
|
||||
settings.User = factoryConfig.DecryptFunc(context.Background(), factoryConfig.Config.SecureSettings, "username", rawSettings.User)
|
||||
settings.Password = factoryConfig.DecryptFunc(context.Background(), factoryConfig.Config.SecureSettings, "password", rawSettings.Password)
|
||||
settings.AuthorizationCredentials = factoryConfig.DecryptFunc(context.Background(), factoryConfig.Config.SecureSettings, "authorization_scheme", rawSettings.AuthorizationCredentials)
|
||||
|
||||
if settings.AuthorizationCredentials != "" && settings.AuthorizationScheme == "" {
|
||||
settings.AuthorizationScheme = "Bearer"
|
||||
}
|
||||
if settings.User != "" && settings.Password != "" && settings.AuthorizationScheme != "" && settings.AuthorizationCredentials != "" {
|
||||
return settings, errors.New("both HTTP Basic Authentication and Authorization Header are set, only 1 is permitted")
|
||||
}
|
||||
settings.Title = rawSettings.Title
|
||||
if settings.Title == "" {
|
||||
settings.Title = DefaultMessageTitleEmbed
|
||||
}
|
||||
settings.Message = rawSettings.Message
|
||||
if settings.Message == "" {
|
||||
settings.Message = DefaultMessageEmbed
|
||||
}
|
||||
@ -96,24 +117,6 @@ func buildWebhookNotifier(factoryConfig FactoryConfig) (*WebhookNotifier, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger := log.New("alerting.notifier.webhook")
|
||||
maxAlerts := 0
|
||||
if settings.MaxAlerts != nil {
|
||||
switch value := settings.MaxAlerts.(type) {
|
||||
case int:
|
||||
maxAlerts = value
|
||||
case string:
|
||||
maxAlerts, err = strconv.Atoi(value)
|
||||
if err != nil {
|
||||
logger.Warn("failed to convert setting maxAlerts to integer. Using default", "error", err, "original", value)
|
||||
maxAlerts = 0
|
||||
}
|
||||
default:
|
||||
logger.Warn("unexpected type of setting maxAlerts. Expected integer. Using default", "type", fmt.Sprintf("%T", settings.MaxAlerts))
|
||||
}
|
||||
}
|
||||
|
||||
return &WebhookNotifier{
|
||||
Base: NewBase(&models.AlertNotification{
|
||||
Uid: factoryConfig.Config.UID,
|
||||
@ -122,13 +125,12 @@ func buildWebhookNotifier(factoryConfig FactoryConfig) (*WebhookNotifier, error)
|
||||
DisableResolveMessage: factoryConfig.Config.DisableResolveMessage,
|
||||
Settings: factoryConfig.Config.Settings,
|
||||
}),
|
||||
orgID: factoryConfig.Config.OrgID,
|
||||
log: logger,
|
||||
ns: factoryConfig.NotificationService,
|
||||
images: factoryConfig.ImageStore,
|
||||
tmpl: factoryConfig.Template,
|
||||
maxAlerts: maxAlerts,
|
||||
settings: settings,
|
||||
orgID: factoryConfig.Config.OrgID,
|
||||
log: log.New("alerting.notifier.webhook"),
|
||||
ns: factoryConfig.NotificationService,
|
||||
images: factoryConfig.ImageStore,
|
||||
tmpl: factoryConfig.Template,
|
||||
settings: settings,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -153,7 +155,7 @@ func (wn *WebhookNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool
|
||||
return false, err
|
||||
}
|
||||
|
||||
as, numTruncated := truncateAlerts(wn.maxAlerts, as)
|
||||
as, numTruncated := truncateAlerts(wn.settings.MaxAlerts, as)
|
||||
var tmplErr error
|
||||
tmpl, data := TmplText(ctx, wn.tmpl, as, wn.log, &tmplErr)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user