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:
Yuri Tseretyan 2022-11-18 09:24:12 -05:00 committed by GitHub
parent 5226a61f67
commit 8c72d19bcc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 312 additions and 222 deletions

View File

@ -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)
}
}

View File

@ -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())

View File

@ -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 {

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)