Alerting: refactor receiver validation to be reusable (#46103)

This commit is contained in:
Jean-Philippe Quéméner 2022-03-15 00:27:10 +01:00 committed by GitHub
parent 00f67cad1b
commit e135b8531a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 980 additions and 580 deletions

View File

@ -451,11 +451,6 @@ func (am *Alertmanager) buildIntegrationsMap(receivers []*apimodels.PostableApiR
return integrationsMap, nil
}
type NotificationChannel interface {
notify.Notifier
notify.ResolvedSender
}
// buildReceiverIntegrations builds a list of integration notifiers off of a receiver config.
func (am *Alertmanager) buildReceiverIntegrations(receiver *apimodels.PostableApiReceiver, tmpl *template.Template) ([]notify.Integration, error) {
var integrations []notify.Integration
@ -469,7 +464,7 @@ func (am *Alertmanager) buildReceiverIntegrations(receiver *apimodels.PostableAp
return integrations, nil
}
func (am *Alertmanager) buildReceiverIntegration(r *apimodels.PostableGrafanaReceiver, tmpl *template.Template) (NotificationChannel, error) {
func (am *Alertmanager) buildReceiverIntegration(r *apimodels.PostableGrafanaReceiver, tmpl *template.Template) (channels.NotificationChannel, error) {
// secure settings are already encrypted at this point
secureSettings := make(map[string][]byte, len(r.SecureSettings))
@ -494,60 +489,28 @@ func (am *Alertmanager) buildReceiverIntegration(r *apimodels.PostableGrafanaRec
Settings: r.Settings,
SecureSettings: secureSettings,
}
n NotificationChannel
err error
)
switch r.Type {
case "email":
n, err = channels.NewEmailNotifier(cfg, am.NotificationService, tmpl) // Email notifier already has a default template.
case "pagerduty":
n, err = channels.NewPagerdutyNotifier(cfg, am.NotificationService, tmpl, am.decryptFn)
case "pushover":
n, err = channels.NewPushoverNotifier(cfg, am.NotificationService, tmpl, am.decryptFn)
case "slack":
n, err = channels.NewSlackNotifier(cfg, tmpl, am.decryptFn)
case "telegram":
n, err = channels.NewTelegramNotifier(cfg, am.NotificationService, tmpl, am.decryptFn)
case "victorops":
n, err = channels.NewVictoropsNotifier(cfg, am.NotificationService, tmpl)
case "teams":
n, err = channels.NewTeamsNotifier(cfg, am.NotificationService, tmpl)
case "dingding":
n, err = channels.NewDingDingNotifier(cfg, am.NotificationService, tmpl)
case "kafka":
n, err = channels.NewKafkaNotifier(cfg, am.NotificationService, tmpl)
case "webhook":
n, err = channels.NewWebHookNotifier(cfg, am.NotificationService, tmpl, am.decryptFn)
case "wecom":
n, err = channels.NewWeComNotifier(cfg, am.NotificationService, tmpl, am.decryptFn)
case "sensugo":
n, err = channels.NewSensuGoNotifier(cfg, am.NotificationService, tmpl, am.decryptFn)
case "discord":
n, err = channels.NewDiscordNotifier(cfg, am.NotificationService, tmpl)
case "googlechat":
n, err = channels.NewGoogleChatNotifier(cfg, am.NotificationService, tmpl)
case "LINE":
n, err = channels.NewLineNotifier(cfg, am.NotificationService, tmpl, am.decryptFn)
case "threema":
n, err = channels.NewThreemaNotifier(cfg, am.NotificationService, tmpl, am.decryptFn)
case "opsgenie":
n, err = channels.NewOpsgenieNotifier(cfg, am.NotificationService, tmpl, am.decryptFn)
case "prometheus-alertmanager":
n, err = channels.NewAlertmanagerNotifier(cfg, tmpl, am.decryptFn)
default:
return nil, InvalidReceiverError{
Receiver: r,
Err: fmt.Errorf("notifier %s is not supported", r.Type),
}
}
factoryConfig, err := channels.NewFactoryConfig(cfg, am.NotificationService, am.decryptFn, tmpl)
if err != nil {
return nil, InvalidReceiverError{
Receiver: r,
Err: err,
}
}
receiverFactory, exists := channels.Factory(r.Type)
if !exists {
return nil, InvalidReceiverError{
Receiver: r,
Err: fmt.Errorf("notifier %s is not supported", r.Type),
}
}
n, err := receiverFactory(factoryConfig)
if err != nil {
return nil, InvalidReceiverError{
Receiver: r,
Err: err,
}
}
return n, nil
}

View File

@ -3,6 +3,7 @@ package channels
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
@ -17,49 +18,64 @@ import (
// the given key. If the key is not present, then it returns the fallback value.
type GetDecryptedValueFn func(ctx context.Context, sjd map[string][]byte, key string, fallback string) string
// NewAlertmanagerNotifier returns a new Alertmanager notifier.
func NewAlertmanagerNotifier(model *NotificationChannelConfig, _ *template.Template, fn GetDecryptedValueFn) (*AlertmanagerNotifier, error) {
if model.Settings == nil {
return nil, receiverInitError{Reason: "no settings supplied"}
}
if model.SecureSettings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no secure settings supplied"}
}
urlStr := model.Settings.Get("url").MustString()
if urlStr == "" {
return nil, receiverInitError{Reason: "could not find url property in settings", Cfg: *model}
}
type AlertmanagerConfig struct {
*NotificationChannelConfig
URLs []*url.URL
BasicAuthUser string
BasicAuthPassword string
}
func NewAlertmanagerConfig(config *NotificationChannelConfig, fn GetDecryptedValueFn) (*AlertmanagerConfig, error) {
urlStr := config.Settings.Get("url").MustString()
if urlStr == "" {
return nil, errors.New("could not find url property in settings")
}
var urls []*url.URL
for _, uS := range strings.Split(urlStr, ",") {
uS = strings.TrimSpace(uS)
if uS == "" {
continue
}
uS = strings.TrimSuffix(uS, "/") + "/api/v1/alerts"
u, err := url.Parse(uS)
url, err := url.Parse(uS)
if err != nil {
return nil, receiverInitError{Reason: "invalid url property in settings", Cfg: *model, Err: err}
return nil, fmt.Errorf("invalid url property in settings: %w", err)
}
urls = append(urls, u)
urls = append(urls, url)
}
basicAuthUser := model.Settings.Get("basicAuthUser").MustString()
basicAuthPassword := fn(context.Background(), model.SecureSettings, "basicAuthPassword", model.Settings.Get("basicAuthPassword").MustString())
return &AlertmanagerConfig{
NotificationChannelConfig: config,
URLs: urls,
BasicAuthUser: config.Settings.Get("basicAuthUser").MustString(),
BasicAuthPassword: fn(context.Background(), config.SecureSettings, "basicAuthPassword", config.Settings.Get("basicAuthPassword").MustString()),
}, nil
}
func AlertmanagerFactory(fc FactoryConfig) (NotificationChannel, error) {
config, err := NewAlertmanagerConfig(fc.Config, fc.DecryptFunc)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
return NewAlertmanagerNotifier(config, nil, fc.DecryptFunc), nil
}
// NewAlertmanagerNotifier returns a new Alertmanager notifier.
func NewAlertmanagerNotifier(config *AlertmanagerConfig, _ *template.Template, fn GetDecryptedValueFn) *AlertmanagerNotifier {
return &AlertmanagerNotifier{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
DisableResolveMessage: model.DisableResolveMessage,
Settings: model.Settings,
Uid: config.UID,
Name: config.Name,
DisableResolveMessage: config.DisableResolveMessage,
Settings: config.Settings,
}),
urls: urls,
basicAuthUser: basicAuthUser,
basicAuthPassword: basicAuthPassword,
urls: config.URLs,
basicAuthUser: config.BasicAuthUser,
basicAuthPassword: config.BasicAuthPassword,
logger: log.New("alerting.notifier.prometheus-alertmanager"),
}, nil
}
}
// AlertmanagerNotifier sends alert notifications to the alert manager

View File

@ -35,13 +35,13 @@ func TestNewAlertmanagerNotifier(t *testing.T) {
{
name: "Error in initing: missing URL",
settings: `{}`,
expectedInitError: `failed to validate receiver of type "alertmanager": could not find url property in settings`,
expectedInitError: `could not find url property in settings`,
}, {
name: "Error in initing: invalid URL",
settings: `{
"url": "://alertmanager.com"
}`,
expectedInitError: `failed to validate receiver "Alertmanager" of type "alertmanager": invalid url property in settings: parse "://alertmanager.com/api/v1/alerts": missing protocol scheme`,
expectedInitError: `invalid url property in settings: parse "://alertmanager.com/api/v1/alerts": missing protocol scheme`,
receiverName: "Alertmanager",
},
}
@ -53,19 +53,20 @@ func TestNewAlertmanagerNotifier(t *testing.T) {
m := &NotificationChannelConfig{
Name: c.receiverName,
Type: "alertmanager",
Type: "prometheus-alertmanager",
Settings: settingsJSON,
SecureSettings: secureSettings,
}
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
decryptFn := secretsService.GetDecryptedValue
sn, err := NewAlertmanagerNotifier(m, tmpl, decryptFn)
cfg, err := NewAlertmanagerConfig(m, decryptFn)
if c.expectedInitError != "" {
require.Equal(t, c.expectedInitError, err.Error())
return
}
require.NoError(t, err)
sn := NewAlertmanagerNotifier(cfg, tmpl, decryptFn)
require.NotNil(t, sn)
})
}
@ -136,16 +137,16 @@ func TestAlertmanagerNotifier_Notify(t *testing.T) {
m := &NotificationChannelConfig{
Name: c.receiverName,
Type: "alertmanager",
Type: "prometheus-alertmanager",
Settings: settingsJSON,
SecureSettings: secureSettings,
}
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
decryptFn := secretsService.GetDecryptedValue
sn, err := NewAlertmanagerNotifier(m, tmpl, decryptFn)
cfg, err := NewAlertmanagerConfig(m, decryptFn)
require.NoError(t, err)
sn := NewAlertmanagerNotifier(cfg, tmpl, decryptFn)
var body []byte
origSendHTTPRequest := sendHTTPRequest
t.Cleanup(func() {

View File

@ -3,6 +3,7 @@ package channels
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
@ -16,34 +17,53 @@ import (
const defaultDingdingMsgType = "link"
// NewDingDingNotifier is the constructor for the Dingding notifier
func NewDingDingNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template) (*DingDingNotifier, error) {
if model.Settings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"}
}
type DingDingConfig struct {
*NotificationChannelConfig
MsgType string
Message string
URL string
}
url := model.Settings.Get("url").MustString()
func NewDingDingConfig(config *NotificationChannelConfig) (*DingDingConfig, error) {
url := config.Settings.Get("url").MustString()
if url == "" {
return nil, receiverInitError{Reason: "could not find url property in settings", Cfg: *model}
return nil, errors.New("could not find url property in settings")
}
return &DingDingConfig{
NotificationChannelConfig: config,
MsgType: config.Settings.Get("msgType").MustString(defaultDingdingMsgType),
Message: config.Settings.Get("message").MustString(`{{ template "default.message" .}}`),
URL: config.Settings.Get("url").MustString(),
}, nil
}
func DingDingFactory(fc FactoryConfig) (NotificationChannel, error) {
cfg, err := NewDingDingConfig(fc.Config)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
return NewDingDingNotifier(cfg, fc.NotificationService, fc.Template), nil
}
msgType := model.Settings.Get("msgType").MustString(defaultDingdingMsgType)
// NewDingDingNotifier is the constructor for the Dingding notifier
func NewDingDingNotifier(config *DingDingConfig, ns notifications.WebhookSender, t *template.Template) *DingDingNotifier {
return &DingDingNotifier{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
DisableResolveMessage: model.DisableResolveMessage,
Settings: model.Settings,
Uid: config.UID,
Name: config.Name,
Type: config.Type,
DisableResolveMessage: config.DisableResolveMessage,
Settings: config.Settings,
}),
MsgType: msgType,
URL: url,
Message: model.Settings.Get("message").MustString(`{{ template "default.message" .}}`),
MsgType: config.MsgType,
Message: config.Message,
URL: config.URL,
log: log.New("alerting.notifier.dingding"),
tmpl: t,
ns: ns,
}, nil
}
}
// DingDingNotifier is responsible for sending alert notifications to ding ding.

View File

@ -82,7 +82,7 @@ func TestDingdingNotifier(t *testing.T) {
}, {
name: "Error in initing",
settings: `{}`,
expInitError: `failed to validate receiver "dingding_testing" of type "dingding": could not find url property in settings`,
expInitError: `could not find url property in settings`,
},
}
@ -98,7 +98,7 @@ func TestDingdingNotifier(t *testing.T) {
}
webhookSender := mockNotificationService()
pn, err := NewDingDingNotifier(m, webhookSender, tmpl)
cfg, err := NewDingDingConfig(m)
if c.expInitError != "" {
require.Equal(t, c.expInitError, err.Error())
return
@ -107,6 +107,7 @@ func TestDingdingNotifier(t *testing.T) {
ctx := notify.WithGroupKey(context.Background(), "alertname")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
pn := NewDingDingNotifier(cfg, webhookSender, tmpl)
ok, err := pn.Notify(ctx, c.alerts...)
if c.expMsgError != nil {
require.False(t, ok)

View File

@ -3,6 +3,7 @@ package channels
import (
"context"
"encoding/json"
"errors"
"strconv"
"strings"
@ -27,39 +28,57 @@ type DiscordNotifier struct {
UseDiscordUsername bool
}
func NewDiscordNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template) (*DiscordNotifier, error) {
if model.Settings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"}
}
type DiscordConfig struct {
*NotificationChannelConfig
Content string
AvatarURL string
WebhookURL string
UseDiscordUsername bool
}
avatarURL := model.Settings.Get("avatar_url").MustString()
discordURL := model.Settings.Get("url").MustString()
func NewDiscordConfig(config *NotificationChannelConfig) (*DiscordConfig, error) {
discordURL := config.Settings.Get("url").MustString()
if discordURL == "" {
return nil, receiverInitError{Reason: "could not find webhook url property in settings", Cfg: *model}
return nil, errors.New("could not find webhook url property in settings")
}
return &DiscordConfig{
NotificationChannelConfig: config,
Content: config.Settings.Get("message").MustString(`{{ template "default.message" . }}`),
AvatarURL: config.Settings.Get("avatar_url").MustString(),
WebhookURL: discordURL,
UseDiscordUsername: config.Settings.Get("use_discord_username").MustBool(false),
}, nil
}
useDiscordUsername := model.Settings.Get("use_discord_username").MustBool(false)
content := model.Settings.Get("message").MustString(`{{ template "default.message" . }}`)
func DiscrodFactory(fc FactoryConfig) (NotificationChannel, error) {
cfg, err := NewDiscordConfig(fc.Config)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
return NewDiscordNotifier(cfg, fc.NotificationService, fc.Template), nil
}
func NewDiscordNotifier(config *DiscordConfig, ns notifications.WebhookSender, t *template.Template) *DiscordNotifier {
return &DiscordNotifier{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
DisableResolveMessage: model.DisableResolveMessage,
Settings: model.Settings,
SecureSettings: model.SecureSettings,
Uid: config.UID,
Name: config.Name,
Type: config.Type,
DisableResolveMessage: config.DisableResolveMessage,
Settings: config.Settings,
SecureSettings: config.SecureSettings,
}),
Content: content,
AvatarURL: avatarURL,
WebhookURL: discordURL,
Content: config.Content,
AvatarURL: config.AvatarURL,
WebhookURL: config.WebhookURL,
log: log.New("alerting.notifier.discord"),
ns: ns,
tmpl: t,
UseDiscordUsername: useDiscordUsername,
}, nil
UseDiscordUsername: config.UseDiscordUsername,
}
}
func (d DiscordNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {

View File

@ -98,7 +98,7 @@ func TestDiscordNotifier(t *testing.T) {
{
name: "Error in initialization",
settings: `{}`,
expInitError: `failed to validate receiver "discord_testing" of type "discord": could not find webhook url property in settings`,
expInitError: `could not find webhook url property in settings`,
},
{
name: "Invalid template returns error",
@ -151,7 +151,7 @@ func TestDiscordNotifier(t *testing.T) {
}
webhookSender := mockNotificationService()
dn, err := NewDiscordNotifier(m, webhookSender, tmpl)
cfg, err := NewDiscordConfig(m)
if c.expInitError != "" {
require.Equal(t, c.expInitError, err.Error())
return
@ -160,6 +160,7 @@ func TestDiscordNotifier(t *testing.T) {
ctx := notify.WithGroupKey(context.Background(), "alertname")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
dn := NewDiscordNotifier(cfg, webhookSender, tmpl)
ok, err := dn.Notify(ctx, c.alerts...)
if c.expMsgError != nil {
require.False(t, ok)

View File

@ -2,6 +2,7 @@ package channels
import (
"context"
"errors"
"net/url"
"path"
@ -26,38 +27,57 @@ type EmailNotifier struct {
tmpl *template.Template
}
// NewEmailNotifier is the constructor function
// for the EmailNotifier.
func NewEmailNotifier(model *NotificationChannelConfig, ns notifications.EmailSender, t *template.Template) (*EmailNotifier, error) {
if model.Settings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"}
type EmailConfig struct {
*NotificationChannelConfig
SingleEmail bool
Addresses []string
Message string
}
func EmailFactory(fc FactoryConfig) (NotificationChannel, error) {
cfg, err := NewEmailConfig(fc.Config)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
return NewEmailNotifier(cfg, fc.NotificationService, fc.Template), nil
}
addressesString := model.Settings.Get("addresses").MustString()
singleEmail := model.Settings.Get("singleEmail").MustBool(false)
func NewEmailConfig(config *NotificationChannelConfig) (*EmailConfig, error) {
addressesString := config.Settings.Get("addresses").MustString()
if addressesString == "" {
return nil, receiverInitError{Reason: "could not find addresses in settings", Cfg: *model}
return nil, errors.New("could not find addresses in settings")
}
// split addresses with a few different ways
addresses := util.SplitEmails(addressesString)
return &EmailConfig{
NotificationChannelConfig: config,
SingleEmail: config.Settings.Get("singleEmail").MustBool(false),
Message: config.Settings.Get("message").MustString(),
Addresses: addresses,
}, nil
}
// NewEmailNotifier is the constructor function
// for the EmailNotifier.
func NewEmailNotifier(config *EmailConfig, ns notifications.EmailSender, t *template.Template) *EmailNotifier {
return &EmailNotifier{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
DisableResolveMessage: model.DisableResolveMessage,
Settings: model.Settings,
Uid: config.UID,
Name: config.Name,
Type: config.Type,
DisableResolveMessage: config.DisableResolveMessage,
Settings: config.Settings,
}),
Addresses: addresses,
SingleEmail: singleEmail,
Message: model.Settings.Get("message").MustString(),
Addresses: config.Addresses,
SingleEmail: config.SingleEmail,
Message: config.Message,
log: log.New("alerting.notifier.email"),
ns: ns,
tmpl: t,
}, nil
}
}
// Notify sends the alert notification.

View File

@ -33,7 +33,7 @@ func TestEmailNotifier(t *testing.T) {
Settings: settingsJSON,
}
_, err := NewEmailNotifier(model, nil, tmpl)
_, err := NewEmailConfig(model)
require.Error(t, err)
})
@ -46,14 +46,13 @@ func TestEmailNotifier(t *testing.T) {
require.NoError(t, err)
emailSender := mockNotificationService()
emailNotifier, err := NewEmailNotifier(&NotificationChannelConfig{
cfg, err := NewEmailConfig(&NotificationChannelConfig{
Name: "ops",
Type: "email",
Settings: settingsJSON,
}, emailSender, tmpl)
})
require.NoError(t, err)
emailNotifier := NewEmailNotifier(cfg, emailSender, tmpl)
alerts := []*types.Alert{
{
@ -284,13 +283,13 @@ func createSut(t *testing.T, messageTmpl string, emailTmpl *template.Template, n
settingsJSON.Set("message", messageTmpl)
}
require.NoError(t, err)
emailNotifier, err := NewEmailNotifier(&NotificationChannelConfig{
cfg, err := NewEmailConfig(&NotificationChannelConfig{
Name: "ops",
Type: "email",
Settings: settingsJSON,
}, ns, emailTmpl)
})
require.NoError(t, err)
emailNotifier := NewEmailNotifier(cfg, ns, emailTmpl)
return emailNotifier
}

View File

@ -0,0 +1,61 @@
package channels
import (
"errors"
"strings"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/prometheus/alertmanager/template"
)
type FactoryConfig struct {
Config *NotificationChannelConfig
NotificationService notifications.Service
DecryptFunc GetDecryptedValueFn
Template *template.Template
}
func NewFactoryConfig(config *NotificationChannelConfig, notificationService notifications.Service,
decryptFunc GetDecryptedValueFn, template *template.Template) (FactoryConfig, error) {
if config.Settings == nil {
return FactoryConfig{}, errors.New("no settings supplied")
}
// not all receivers do need secure settings, we still might interact with
// them, so we make sure they are never nil
if config.SecureSettings == nil {
config.SecureSettings = map[string][]byte{}
}
return FactoryConfig{
Config: config,
NotificationService: notificationService,
DecryptFunc: decryptFunc,
Template: template,
}, nil
}
var receiverFactories = map[string]func(FactoryConfig) (NotificationChannel, error){
"prometheus-alertmanager": AlertmanagerFactory,
"dingding": DingDingFactory,
"discord": DiscrodFactory,
"email": EmailFactory,
"googlechat": GoogleChatFactory,
"kafka": KafkaFactory,
"line": LineFactory,
"opsgenie": OpsgenieFactory,
"pagerduty": PagerdutyFactory,
"pushover": PushoverFactory,
"sensugo": SensuGoFactory,
"slack": SlackFactory,
"teams": TeamsFactory,
"telegram": TelegramFactory,
"threema": ThreemaFactory,
"victorops": VictorOpsFactory,
"webhook": WebHookFactory,
"wecom": WeComFactory,
}
func Factory(receiverType string) (func(FactoryConfig) (NotificationChannel, error), bool) {
receiverType = strings.ToLower(receiverType)
factory, exists := receiverFactories[receiverType]
return factory, exists
}

View File

@ -3,6 +3,7 @@ package channels
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
@ -26,32 +27,50 @@ type GoogleChatNotifier struct {
content string
}
func NewGoogleChatNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template) (*GoogleChatNotifier, error) {
if model.Settings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"}
}
type GoogleChatConfig struct {
*NotificationChannelConfig
URL string
Content string
}
url := model.Settings.Get("url").MustString()
func GoogleChatFactory(fc FactoryConfig) (NotificationChannel, error) {
cfg, err := NewGoogleChatConfig(fc.Config)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
return NewGoogleChatNotifier(cfg, fc.NotificationService, fc.Template), nil
}
func NewGoogleChatConfig(config *NotificationChannelConfig) (*GoogleChatConfig, error) {
url := config.Settings.Get("url").MustString()
if url == "" {
return nil, receiverInitError{Cfg: *model, Reason: "could not find url property in settings"}
return nil, errors.New("could not find url property in settings")
}
return &GoogleChatConfig{
NotificationChannelConfig: config,
URL: url,
Content: config.Settings.Get("message").MustString(`{{ template "default.message" . }}`),
}, nil
}
content := model.Settings.Get("message").MustString(`{{ template "default.message" . }}`)
func NewGoogleChatNotifier(config *GoogleChatConfig, ns notifications.WebhookSender, t *template.Template) *GoogleChatNotifier {
return &GoogleChatNotifier{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
DisableResolveMessage: model.DisableResolveMessage,
Settings: model.Settings,
Uid: config.UID,
Name: config.Name,
Type: config.Type,
DisableResolveMessage: config.DisableResolveMessage,
Settings: config.Settings,
}),
URL: url,
content: config.Content,
URL: config.URL,
log: log.New("alerting.notifier.googlechat"),
ns: ns,
tmpl: t,
content: content,
}, nil
}
}
// Notify send an alert notification to Google Chat.

View File

@ -149,7 +149,7 @@ func TestGoogleChatNotifier(t *testing.T) {
}, {
name: "Error in initing",
settings: `{}`,
expInitError: `failed to validate receiver "googlechat_testing" of type "googlechat": could not find url property in settings`,
expInitError: `could not find url property in settings`,
}, {
name: "Customized message",
settings: `{"url": "http://localhost", "message": "I'm a custom template and you have {{ len .Alerts.Firing }} firing alert."}`,
@ -273,7 +273,7 @@ func TestGoogleChatNotifier(t *testing.T) {
}
webhookSender := mockNotificationService()
pn, err := NewGoogleChatNotifier(m, webhookSender, tmpl)
cfg, err := NewGoogleChatConfig(m)
if c.expInitError != "" {
require.Error(t, err)
require.Equal(t, c.expInitError, err.Error())
@ -283,6 +283,7 @@ func TestGoogleChatNotifier(t *testing.T) {
ctx := notify.WithGroupKey(context.Background(), "alertname")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
pn := NewGoogleChatNotifier(cfg, webhookSender, tmpl)
ok, err := pn.Notify(ctx, c.alerts...)
if c.expMsgError != nil {
require.False(t, ok)

View File

@ -2,6 +2,7 @@ package channels
import (
"context"
"errors"
"strings"
"github.com/prometheus/alertmanager/notify"
@ -26,35 +27,55 @@ type KafkaNotifier struct {
tmpl *template.Template
}
// NewKafkaNotifier is the constructor function for the Kafka notifier.
func NewKafkaNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template) (*KafkaNotifier, error) {
if model.Settings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"}
}
type KafkaConfig struct {
*NotificationChannelConfig
Endpoint string
Topic string
}
endpoint := model.Settings.Get("kafkaRestProxy").MustString()
func KafkaFactory(fc FactoryConfig) (NotificationChannel, error) {
cfg, err := NewKafkaConfig(fc.Config)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
return NewKafkaNotifier(cfg, fc.NotificationService, fc.Template), nil
}
func NewKafkaConfig(config *NotificationChannelConfig) (*KafkaConfig, error) {
endpoint := config.Settings.Get("kafkaRestProxy").MustString()
if endpoint == "" {
return nil, receiverInitError{Cfg: *model, Reason: "could not find kafka rest proxy endpoint property in settings"}
return nil, errors.New("could not find kafka rest proxy endpoint property in settings")
}
topic := model.Settings.Get("kafkaTopic").MustString()
topic := config.Settings.Get("kafkaTopic").MustString()
if topic == "" {
return nil, receiverInitError{Cfg: *model, Reason: "could not find kafka topic property in settings"}
return nil, errors.New("could not find kafka topic property in settings")
}
return &KafkaConfig{
NotificationChannelConfig: config,
Endpoint: endpoint,
Topic: topic,
}, nil
}
// NewKafkaNotifier is the constructor function for the Kafka notifier.
func NewKafkaNotifier(config *KafkaConfig, ns notifications.WebhookSender, t *template.Template) *KafkaNotifier {
return &KafkaNotifier{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
DisableResolveMessage: model.DisableResolveMessage,
Settings: model.Settings,
Uid: config.UID,
Name: config.Name,
Type: config.Type,
DisableResolveMessage: config.DisableResolveMessage,
Settings: config.Settings,
}),
Endpoint: endpoint,
Topic: topic,
Endpoint: config.Endpoint,
Topic: config.Topic,
log: log.New("alerting.notifier.kafka"),
ns: ns,
tmpl: t,
}, nil
}
}
// Notify sends the alert notification.

View File

@ -96,11 +96,11 @@ func TestKafkaNotifier(t *testing.T) {
}, {
name: "Endpoint missing",
settings: `{"kafkaTopic": "sometopic"}`,
expInitError: `failed to validate receiver "kafka_testing" of type "kafka": could not find kafka rest proxy endpoint property in settings`,
expInitError: `could not find kafka rest proxy endpoint property in settings`,
}, {
name: "Topic missing",
settings: `{"kafkaRestProxy": "http://localhost"}`,
expInitError: `failed to validate receiver "kafka_testing" of type "kafka": could not find kafka topic property in settings`,
expInitError: `could not find kafka topic property in settings`,
},
}
@ -116,7 +116,7 @@ func TestKafkaNotifier(t *testing.T) {
}
webhookSender := mockNotificationService()
pn, err := NewKafkaNotifier(m, webhookSender, tmpl)
cfg, err := NewKafkaConfig(m)
if c.expInitError != "" {
require.Error(t, err)
require.Equal(t, c.expInitError, err.Error())
@ -126,6 +126,8 @@ func TestKafkaNotifier(t *testing.T) {
ctx := notify.WithGroupKey(context.Background(), "alertname")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
pn := NewKafkaNotifier(cfg, webhookSender, tmpl)
ok, err := pn.Notify(ctx, c.alerts...)
if c.expMsgError != nil {
require.False(t, ok)

View File

@ -2,6 +2,7 @@ package channels
import (
"context"
"errors"
"fmt"
"net/url"
"path"
@ -17,33 +18,48 @@ var (
LineNotifyURL string = "https://notify-api.line.me/api/notify"
)
// NewLineNotifier is the constructor for the LINE notifier
func NewLineNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template, fn GetDecryptedValueFn) (*LineNotifier, error) {
if model.Settings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"}
}
if model.SecureSettings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no secure settings supplied"}
}
type LineConfig struct {
*NotificationChannelConfig
Token string
}
token := fn(context.Background(), model.SecureSettings, "token", model.Settings.Get("token").MustString())
func LineFactory(fc FactoryConfig) (NotificationChannel, error) {
cfg, err := NewLineConfig(fc.Config, fc.DecryptFunc)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
return NewLineNotifier(cfg, fc.NotificationService, fc.Template), nil
}
func NewLineConfig(config *NotificationChannelConfig, decryptFunc GetDecryptedValueFn) (*LineConfig, error) {
token := decryptFunc(context.Background(), config.SecureSettings, "token", config.Settings.Get("token").MustString())
if token == "" {
return nil, receiverInitError{Cfg: *model, Reason: "could not find token in settings"}
return nil, errors.New("could not find token in settings")
}
return &LineConfig{
NotificationChannelConfig: config,
Token: token,
}, nil
}
// NewLineNotifier is the constructor for the LINE notifier
func NewLineNotifier(config *LineConfig, ns notifications.WebhookSender, t *template.Template) *LineNotifier {
return &LineNotifier{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
DisableResolveMessage: model.DisableResolveMessage,
Settings: model.Settings,
Uid: config.UID,
Name: config.Name,
Type: config.Type,
DisableResolveMessage: config.DisableResolveMessage,
Settings: config.Settings,
}),
Token: token,
Token: config.Token,
log: log.New("alerting.notifier.line"),
ns: ns,
tmpl: t,
}, nil
}
}
// LineNotifier is responsible for sending

View File

@ -73,7 +73,7 @@ func TestLineNotifier(t *testing.T) {
}, {
name: "Token missing",
settings: `{}`,
expInitError: `failed to validate receiver "line_testing" of type "line": could not find token in settings`,
expInitError: `could not find token in settings`,
},
}
@ -93,7 +93,7 @@ func TestLineNotifier(t *testing.T) {
webhookSender := mockNotificationService()
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
decryptFn := secretsService.GetDecryptedValue
pn, err := NewLineNotifier(m, webhookSender, tmpl, decryptFn)
cfg, err := NewLineConfig(m, decryptFn)
if c.expInitError != "" {
require.Error(t, err)
require.Equal(t, c.expInitError, err.Error())
@ -103,6 +103,7 @@ func TestLineNotifier(t *testing.T) {
ctx := notify.WithGroupKey(context.Background(), "alertname")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
pn := NewLineNotifier(cfg, webhookSender, tmpl)
ok, err := pn.Notify(ctx, c.alerts...)
if c.expMsgError != nil {
require.False(t, ok)

View File

@ -3,6 +3,7 @@ package channels
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"sort"
@ -41,49 +42,66 @@ type OpsgenieNotifier struct {
ns notifications.WebhookSender
}
// NewOpsgenieNotifier is the constructor for the Opsgenie notifier
func NewOpsgenieNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template, fn GetDecryptedValueFn) (*OpsgenieNotifier, error) {
if model.Settings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"}
}
if model.SecureSettings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no secure settings supplied"}
}
autoClose := model.Settings.Get("autoClose").MustBool(true)
overridePriority := model.Settings.Get("overridePriority").MustBool(true)
apiKey := fn(context.Background(), model.SecureSettings, "apiKey", model.Settings.Get("apiKey").MustString())
apiURL := model.Settings.Get("apiUrl").MustString()
if apiKey == "" {
return nil, receiverInitError{Cfg: *model, Reason: "could not find api key property in settings"}
}
if apiURL == "" {
apiURL = OpsgenieAlertURL
}
type OpsgenieConfig struct {
*NotificationChannelConfig
APIKey string
APIUrl string
AutoClose bool
OverridePriority bool
SendTagsAs string
}
sendTagsAs := model.Settings.Get("sendTagsAs").MustString(OpsgenieSendTags)
if sendTagsAs != OpsgenieSendTags && sendTagsAs != OpsgenieSendDetails && sendTagsAs != OpsgenieSendBoth {
return nil, receiverInitError{Cfg: *model,
Reason: fmt.Sprintf("invalid value for sendTagsAs: %q", sendTagsAs),
func OpsgenieFactory(fc FactoryConfig) (NotificationChannel, error) {
cfg, err := NewOpsgenieConfig(fc.Config, fc.DecryptFunc)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
return NewOpsgenieNotifier(cfg, fc.NotificationService, fc.Template, fc.DecryptFunc), nil
}
func NewOpsgenieConfig(config *NotificationChannelConfig, decryptFunc GetDecryptedValueFn) (*OpsgenieConfig, error) {
apiKey := decryptFunc(context.Background(), config.SecureSettings, "apiKey", config.Settings.Get("apiKey").MustString())
if apiKey == "" {
return nil, errors.New("could not find api key property in settings")
}
sendTagsAs := config.Settings.Get("sendTagsAs").MustString(OpsgenieSendTags)
if sendTagsAs != OpsgenieSendTags &&
sendTagsAs != OpsgenieSendDetails &&
sendTagsAs != OpsgenieSendBoth {
return nil, fmt.Errorf("invalid value for sendTagsAs: %q", sendTagsAs)
}
return &OpsgenieConfig{
NotificationChannelConfig: config,
APIKey: apiKey,
APIUrl: config.Settings.Get("apiUrl").MustString(OpsgenieAlertURL),
AutoClose: config.Settings.Get("autoClose").MustBool(true),
OverridePriority: config.Settings.Get("overridePriority").MustBool(true),
SendTagsAs: sendTagsAs,
}, nil
}
// NewOpsgenieNotifier is the constructor for the Opsgenie notifier
func NewOpsgenieNotifier(config *OpsgenieConfig, ns notifications.WebhookSender, t *template.Template, fn GetDecryptedValueFn) *OpsgenieNotifier {
return &OpsgenieNotifier{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
DisableResolveMessage: model.DisableResolveMessage,
Settings: model.Settings,
Uid: config.UID,
Name: config.Name,
Type: config.Type,
DisableResolveMessage: config.DisableResolveMessage,
Settings: config.Settings,
}),
APIKey: apiKey,
APIUrl: apiURL,
AutoClose: autoClose,
OverridePriority: overridePriority,
SendTagsAs: sendTagsAs,
APIKey: config.APIKey,
APIUrl: config.APIUrl,
AutoClose: config.AutoClose,
OverridePriority: config.OverridePriority,
SendTagsAs: config.SendTagsAs,
tmpl: t,
log: log.New("alerting.notifier." + model.Name),
log: log.New("alerting.notifier." + config.Name),
ns: ns,
}, nil
}
}
// Notify sends an alert notification to Opsgenie

View File

@ -153,7 +153,7 @@ func TestOpsgenieNotifier(t *testing.T) {
{
name: "Error when incorrect settings",
settings: `{}`,
expInitError: `failed to validate receiver "opsgenie_testing" of type "opsgenie": could not find api key property in settings`,
expInitError: `could not find api key property in settings`,
},
}
@ -174,7 +174,7 @@ func TestOpsgenieNotifier(t *testing.T) {
webhookSender.Webhook.Body = "<not-sent>"
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
decryptFn := secretsService.GetDecryptedValue
pn, err := NewOpsgenieNotifier(m, webhookSender, tmpl, decryptFn)
cfg, err := NewOpsgenieConfig(m, decryptFn)
if c.expInitError != "" {
require.Error(t, err)
require.Equal(t, c.expInitError, err.Error())
@ -184,6 +184,7 @@ func TestOpsgenieNotifier(t *testing.T) {
ctx := notify.WithGroupKey(context.Background(), "alertname")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
pn := NewOpsgenieNotifier(cfg, webhookSender, tmpl, decryptFn)
ok, err := pn.Notify(ctx, c.alerts...)
if c.expMsgError != nil {
require.False(t, ok)

View File

@ -3,6 +3,7 @@ package channels
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
@ -40,44 +41,69 @@ type PagerdutyNotifier struct {
ns notifications.WebhookSender
}
// NewPagerdutyNotifier is the constructor for the PagerDuty notifier
func NewPagerdutyNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template, fn GetDecryptedValueFn) (*PagerdutyNotifier, error) {
if model.Settings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"}
}
if model.SecureSettings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no secure settings supplied"}
}
type PagerdutyConfig struct {
*NotificationChannelConfig
Key string
Severity string
Class string
Component string
Group string
Summary string
}
key := fn(context.Background(), model.SecureSettings, "integrationKey", model.Settings.Get("integrationKey").MustString())
func PagerdutyFactory(fc FactoryConfig) (NotificationChannel, error) {
cfg, err := NewPagerdutyConfig(fc.Config, fc.DecryptFunc)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
return NewPagerdutyNotifier(cfg, fc.NotificationService, fc.Template), nil
}
func NewPagerdutyConfig(config *NotificationChannelConfig, decryptFunc GetDecryptedValueFn) (*PagerdutyConfig, error) {
key := decryptFunc(context.Background(), config.SecureSettings, "integrationKey", config.Settings.Get("integrationKey").MustString())
if key == "" {
return nil, receiverInitError{Cfg: *model, Reason: "could not find integration key property in settings"}
return nil, errors.New("could not find integration key property in settings")
}
return &PagerdutyConfig{
NotificationChannelConfig: config,
Key: key,
Severity: config.Settings.Get("severity").MustString("critical"),
Class: config.Settings.Get("class").MustString("default"),
Component: config.Settings.Get("component").MustString("Grafana"),
Group: config.Settings.Get("group").MustString("default"),
Summary: config.Settings.Get("summary").MustString(DefaultMessageTitleEmbed),
}, nil
}
// NewPagerdutyNotifier is the constructor for the PagerDuty notifier
func NewPagerdutyNotifier(config *PagerdutyConfig, ns notifications.WebhookSender, t *template.Template) *PagerdutyNotifier {
return &PagerdutyNotifier{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
DisableResolveMessage: model.DisableResolveMessage,
Settings: model.Settings,
Uid: config.UID,
Name: config.Name,
Type: config.Type,
DisableResolveMessage: config.DisableResolveMessage,
Settings: config.Settings,
}),
Key: key,
Key: config.Key,
CustomDetails: map[string]string{
"firing": `{{ template "__text_alert_list" .Alerts.Firing }}`,
"resolved": `{{ template "__text_alert_list" .Alerts.Resolved }}`,
"num_firing": `{{ .Alerts.Firing | len }}`,
"num_resolved": `{{ .Alerts.Resolved | len }}`,
},
Severity: model.Settings.Get("severity").MustString("critical"),
Class: model.Settings.Get("class").MustString("default"),
Component: model.Settings.Get("component").MustString("Grafana"),
Group: model.Settings.Get("group").MustString("default"),
Summary: model.Settings.Get("summary").MustString(DefaultMessageTitleEmbed),
Severity: config.Severity,
Class: config.Class,
Component: config.Component,
Group: config.Group,
Summary: config.Summary,
tmpl: t,
log: log.New("alerting.notifier." + model.Name),
log: log.New("alerting.notifier." + config.Name),
ns: ns,
}, nil
}
}
// Notify sends an alert notification to PagerDuty

View File

@ -119,7 +119,7 @@ func TestPagerdutyNotifier(t *testing.T) {
}, {
name: "Error in initing",
settings: `{}`,
expInitError: `failed to validate receiver "pageduty_testing" of type "pagerduty": could not find integration key property in settings`,
expInitError: `could not find integration key property in settings`,
},
}
@ -139,7 +139,7 @@ func TestPagerdutyNotifier(t *testing.T) {
webhookSender := mockNotificationService()
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
decryptFn := secretsService.GetDecryptedValue
pn, err := NewPagerdutyNotifier(m, webhookSender, tmpl, decryptFn)
cfg, err := NewPagerdutyConfig(m, decryptFn)
if c.expInitError != "" {
require.Error(t, err)
require.Equal(t, c.expInitError, err.Error())
@ -149,6 +149,7 @@ func TestPagerdutyNotifier(t *testing.T) {
ctx := notify.WithGroupKey(context.Background(), "alertname")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
pn := NewPagerdutyNotifier(cfg, webhookSender, tmpl)
ok, err := pn.Notify(ctx, c.alerts...)
if c.expMsgError != nil {
require.False(t, ok)

View File

@ -3,6 +3,7 @@ package channels
import (
"bytes"
"context"
"errors"
"fmt"
"mime/multipart"
"strconv"
@ -39,62 +40,93 @@ type PushoverNotifier struct {
ns notifications.WebhookSender
}
// NewSlackNotifier is the constructor for the Slack notifier
func NewPushoverNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template, fn GetDecryptedValueFn) (*PushoverNotifier, error) {
if model.Settings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"}
}
if model.SecureSettings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no secure settings supplied"}
}
type PushoverConfig struct {
*NotificationChannelConfig
UserKey string
APIToken string
AlertingPriority int
OKPriority int
Retry int
Expire int
Device string
AlertingSound string
OKSound string
Upload bool
Message string
}
userKey := fn(context.Background(), model.SecureSettings, "userKey", model.Settings.Get("userKey").MustString())
APIToken := fn(context.Background(), model.SecureSettings, "apiToken", model.Settings.Get("apiToken").MustString())
device := model.Settings.Get("device").MustString()
alertingPriority, err := strconv.Atoi(model.Settings.Get("priority").MustString("0")) // default Normal
func PushoverFactory(fc FactoryConfig) (NotificationChannel, error) {
cfg, err := NewPushoverConfig(fc.Config, fc.DecryptFunc)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
return NewPushoverNotifier(cfg, fc.NotificationService, fc.Template), nil
}
func NewPushoverConfig(config *NotificationChannelConfig, decryptFunc GetDecryptedValueFn) (*PushoverConfig, error) {
userKey := decryptFunc(context.Background(), config.SecureSettings, "userKey", config.Settings.Get("userKey").MustString())
if userKey == "" {
return nil, errors.New("user key not found")
}
APIToken := decryptFunc(context.Background(), config.SecureSettings, "apiToken", config.Settings.Get("apiToken").MustString())
if APIToken == "" {
return nil, errors.New("API token not found")
}
alertingPriority, err := strconv.Atoi(config.Settings.Get("priority").MustString("0")) // default Normal
if err != nil {
return nil, fmt.Errorf("failed to convert alerting priority to integer: %w", err)
}
okPriority, err := strconv.Atoi(model.Settings.Get("okPriority").MustString("0")) // default Normal
okPriority, err := strconv.Atoi(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(model.Settings.Get("retry").MustString())
expire, _ := strconv.Atoi(model.Settings.Get("expire").MustString())
alertingSound := model.Settings.Get("sound").MustString()
okSound := model.Settings.Get("okSound").MustString()
uploadImage := model.Settings.Get("uploadImage").MustBool(true)
retry, _ := strconv.Atoi(config.Settings.Get("retry").MustString())
expire, _ := strconv.Atoi(config.Settings.Get("expire").MustString())
return &PushoverConfig{
NotificationChannelConfig: config,
APIToken: APIToken,
UserKey: userKey,
AlertingPriority: alertingPriority,
OKPriority: okPriority,
Retry: retry,
Expire: expire,
Device: config.Settings.Get("device").MustString(),
AlertingSound: config.Settings.Get("sound").MustString(),
OKSound: config.Settings.Get("okSound").MustString(),
Upload: config.Settings.Get("uploadImage").MustBool(true),
Message: config.Settings.Get("message").MustString(`{{ template "default.message" .}}`),
}, nil
}
if userKey == "" {
return nil, receiverInitError{Cfg: *model, Reason: "user key not found"}
}
if APIToken == "" {
return nil, receiverInitError{Cfg: *model, Reason: "API token not found"}
}
// NewSlackNotifier is the constructor for the Slack notifier
func NewPushoverNotifier(config *PushoverConfig, ns notifications.WebhookSender, t *template.Template) *PushoverNotifier {
return &PushoverNotifier{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
DisableResolveMessage: model.DisableResolveMessage,
Settings: model.Settings,
SecureSettings: model.SecureSettings,
Uid: config.UID,
Name: config.Name,
Type: config.Type,
DisableResolveMessage: config.DisableResolveMessage,
Settings: config.Settings,
SecureSettings: config.SecureSettings,
}),
UserKey: userKey,
APIToken: APIToken,
AlertingPriority: alertingPriority,
OKPriority: okPriority,
Retry: retry,
Expire: expire,
Device: device,
AlertingSound: alertingSound,
OKSound: okSound,
Upload: uploadImage,
Message: model.Settings.Get("message").MustString(`{{ template "default.message" .}}`),
UserKey: config.UserKey,
APIToken: config.APIToken,
AlertingPriority: config.AlertingPriority,
OKPriority: config.OKPriority,
Retry: config.Retry,
Expire: config.Expire,
Device: config.Device,
AlertingSound: config.AlertingSound,
OKSound: config.OKSound,
Upload: config.Upload,
Message: config.Message,
tmpl: t,
log: log.New("alerting.notifier.pushover"),
ns: ns,
}, nil
}
}
// Notify sends an alert notification to Slack.

View File

@ -112,13 +112,13 @@ func TestPushoverNotifier(t *testing.T) {
settings: `{
"apiToken": "<apiToken>"
}`,
expInitError: `failed to validate receiver "pushover_testing" of type "pushover": user key not found`,
expInitError: `user key not found`,
}, {
name: "Missing api key",
settings: `{
"userKey": "<userKey>"
}`,
expInitError: `failed to validate receiver "pushover_testing" of type "pushover": API token not found`,
expInitError: `API token not found`,
},
}
@ -147,7 +147,7 @@ func TestPushoverNotifier(t *testing.T) {
webhookSender := mockNotificationService()
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
decryptFn := secretsService.GetDecryptedValue
pn, err := NewPushoverNotifier(m, webhookSender, tmpl, decryptFn)
cfg, err := NewPushoverConfig(m, decryptFn)
if c.expInitError != "" {
require.Error(t, err)
require.Equal(t, c.expInitError, err.Error())
@ -157,6 +157,7 @@ func TestPushoverNotifier(t *testing.T) {
ctx := notify.WithGroupKey(context.Background(), "alertname")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
pn := NewPushoverNotifier(cfg, webhookSender, tmpl)
ok, err := pn.Notify(ctx, c.alerts...)
if c.expMsgError != nil {
require.Error(t, err)

View File

@ -3,6 +3,7 @@ package channels
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
@ -29,44 +30,71 @@ type SensuGoNotifier struct {
Message string
}
// NewSensuGoNotifier is the constructor for the SensuGo notifier
func NewSensuGoNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template, fn GetDecryptedValueFn) (*SensuGoNotifier, error) {
if model.Settings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"}
type SensuGoConfig struct {
*NotificationChannelConfig
URL string
Entity string
Check string
Namespace string
Handler string
APIKey string
Message string
}
func SensuGoFactory(fc FactoryConfig) (NotificationChannel, error) {
cfg, err := NewSensuGoConfig(fc.Config, fc.DecryptFunc)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
if model.SecureSettings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no secure settings supplied"}
}
url := model.Settings.Get("url").MustString()
return NewSensuGoNotifier(cfg, fc.NotificationService, fc.Template), nil
}
func NewSensuGoConfig(config *NotificationChannelConfig, decryptFunc GetDecryptedValueFn) (*SensuGoConfig, error) {
url := config.Settings.Get("url").MustString()
if url == "" {
return nil, receiverInitError{Cfg: *model, Reason: "could not find URL property in settings"}
return nil, errors.New("could not find URL property in settings")
}
apikey := fn(context.Background(), model.SecureSettings, "apikey", model.Settings.Get("apikey").MustString())
apikey := decryptFunc(context.Background(), config.SecureSettings, "apikey", config.Settings.Get("apikey").MustString())
if apikey == "" {
return nil, receiverInitError{Cfg: *model, Reason: "could not find the API key property in settings"}
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(`{{ template "default.message" .}}`),
}, nil
}
// NewSensuGoNotifier is the constructor for the SensuGo notifier
func NewSensuGoNotifier(config *SensuGoConfig, ns notifications.WebhookSender, t *template.Template) *SensuGoNotifier {
return &SensuGoNotifier{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
DisableResolveMessage: model.DisableResolveMessage,
Settings: model.Settings,
SecureSettings: model.SecureSettings,
Uid: config.UID,
Name: config.Name,
Type: config.Type,
DisableResolveMessage: config.DisableResolveMessage,
Settings: config.Settings,
SecureSettings: config.SecureSettings,
}),
URL: url,
Entity: model.Settings.Get("entity").MustString(),
Check: model.Settings.Get("check").MustString(),
Namespace: model.Settings.Get("namespace").MustString(),
Handler: model.Settings.Get("handler").MustString(),
APIKey: apikey,
Message: model.Settings.Get("message").MustString(`{{ template "default.message" .}}`),
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"),
ns: ns,
tmpl: t,
}, nil
}
}
// Notify sends an alert notification to Sensu Go

View File

@ -121,13 +121,13 @@ func TestSensuGoNotifier(t *testing.T) {
settings: `{
"apikey": "<apikey>"
}`,
expInitError: `failed to validate receiver "Sensu Go" of type "sensugo": could not find URL property in settings`,
expInitError: `could not find URL property in settings`,
}, {
name: "Error in initing: missing API key",
settings: `{
"url": "http://sensu-api.local:8080"
}`,
expInitError: `failed to validate receiver "Sensu Go" of type "sensugo": could not find the API key property in settings`,
expInitError: `could not find the API key property in settings`,
},
}
@ -147,7 +147,7 @@ func TestSensuGoNotifier(t *testing.T) {
webhookSender := mockNotificationService()
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
decryptFn := secretsService.GetDecryptedValue
sn, err := NewSensuGoNotifier(m, webhookSender, tmpl, decryptFn)
cfg, err := NewSensuGoConfig(m, decryptFn)
if c.expInitError != "" {
require.Error(t, err)
require.Equal(t, c.expInitError, err.Error())
@ -157,6 +157,7 @@ func TestSensuGoNotifier(t *testing.T) {
ctx := notify.WithGroupKey(context.Background(), "alertname")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
sn := NewSensuGoNotifier(cfg, webhookSender, tmpl)
ok, err := sn.Notify(ctx, c.alerts...)
if c.expMsgError != nil {
require.False(t, ok)

View File

@ -5,6 +5,7 @@ import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"net"
@ -21,6 +22,8 @@ import (
"github.com/prometheus/alertmanager/types"
)
var SlackAPIEndpoint = "https://slack.com/api/chat.postMessage"
// SlackNotifier is responsible for sending
// alert notification to Slack.
type SlackNotifier struct {
@ -41,43 +44,55 @@ type SlackNotifier struct {
Token string
}
var SlackAPIEndpoint = "https://slack.com/api/chat.postMessage"
type SlackConfig struct {
*NotificationChannelConfig
URL *url.URL
Username string
IconEmoji string
IconURL string
Recipient string
Text string
Title string
MentionUsers []string
MentionGroups []string
MentionChannel string
Token string
}
// NewSlackNotifier is the constructor for the Slack notifier
func NewSlackNotifier(model *NotificationChannelConfig, t *template.Template, fn GetDecryptedValueFn) (*SlackNotifier, error) {
if model.Settings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"}
}
if model.SecureSettings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no secure settings supplied"}
func SlackFactory(fc FactoryConfig) (NotificationChannel, error) {
cfg, err := NewSlackConfig(fc.Config, fc.DecryptFunc)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
return NewSlackNotifier(cfg, fc.Template), nil
}
endpointURL := model.Settings.Get("endpointUrl").MustString(SlackAPIEndpoint)
slackURL := fn(context.Background(), model.SecureSettings, "url", model.Settings.Get("url").MustString())
func NewSlackConfig(config *NotificationChannelConfig, decryptFunc GetDecryptedValueFn) (*SlackConfig, error) {
endpointURL := config.Settings.Get("endpointUrl").MustString(SlackAPIEndpoint)
slackURL := decryptFunc(context.Background(), config.SecureSettings, "url", config.Settings.Get("url").MustString())
if slackURL == "" {
slackURL = endpointURL
}
apiURL, err := url.Parse(slackURL)
if err != nil {
return nil, receiverInitError{Cfg: *model, Reason: fmt.Sprintf("invalid URL %q", slackURL), Err: err}
return nil, fmt.Errorf("invalid URL %q", slackURL)
}
recipient := strings.TrimSpace(model.Settings.Get("recipient").MustString())
if recipient == "" && apiURL.String() == endpointURL {
return nil, receiverInitError{Cfg: *model,
Reason: "recipient must be specified when using the Slack chat API",
}
recipient := strings.TrimSpace(config.Settings.Get("recipient").MustString())
if recipient == "" && apiURL.String() == SlackAPIEndpoint {
return nil, errors.New("recipient must be specified when using the Slack chat API")
}
mentionChannel := model.Settings.Get("mentionChannel").MustString()
mentionChannel := config.Settings.Get("mentionChannel").MustString()
if mentionChannel != "" && mentionChannel != "here" && mentionChannel != "channel" {
return nil, receiverInitError{Cfg: *model,
Reason: fmt.Sprintf("invalid value for mentionChannel: %q", mentionChannel),
}
return nil, fmt.Errorf("invalid value for mentionChannel: %q", mentionChannel)
}
mentionUsersStr := model.Settings.Get("mentionUsers").MustString()
token := decryptFunc(context.Background(), config.SecureSettings, "token", config.Settings.Get("token").MustString())
if token == "" && apiURL.String() == SlackAPIEndpoint {
return nil, errors.New("token must be specified when using the Slack chat API")
}
mentionUsersStr := config.Settings.Get("mentionUsers").MustString()
mentionUsers := []string{}
for _, u := range strings.Split(mentionUsersStr, ",") {
u = strings.TrimSpace(u)
@ -85,8 +100,7 @@ func NewSlackNotifier(model *NotificationChannelConfig, t *template.Template, fn
mentionUsers = append(mentionUsers, u)
}
}
mentionGroupsStr := model.Settings.Get("mentionGroups").MustString()
mentionGroupsStr := config.Settings.Get("mentionGroups").MustString()
mentionGroups := []string{}
for _, g := range strings.Split(mentionGroupsStr, ",") {
g = strings.TrimSpace(g)
@ -94,36 +108,46 @@ func NewSlackNotifier(model *NotificationChannelConfig, t *template.Template, fn
mentionGroups = append(mentionGroups, g)
}
}
return &SlackConfig{
NotificationChannelConfig: config,
Recipient: strings.TrimSpace(config.Settings.Get("recipient").MustString()),
MentionChannel: config.Settings.Get("mentionChannel").MustString(),
MentionUsers: mentionUsers,
MentionGroups: mentionGroups,
URL: apiURL,
Username: config.Settings.Get("username").MustString("Grafana"),
IconEmoji: config.Settings.Get("icon_emoji").MustString(),
IconURL: config.Settings.Get("icon_url").MustString(),
Token: token,
Text: config.Settings.Get("text").MustString(`{{ template "default.message" . }}`),
Title: config.Settings.Get("title").MustString(DefaultMessageTitleEmbed),
}, nil
}
token := fn(context.Background(), model.SecureSettings, "token", model.Settings.Get("token").MustString())
if token == "" && apiURL.String() == SlackAPIEndpoint {
return nil, receiverInitError{Cfg: *model,
Reason: "token must be specified when using the Slack chat API",
}
}
// NewSlackNotifier is the constructor for the Slack notifier
func NewSlackNotifier(config *SlackConfig, t *template.Template) *SlackNotifier {
return &SlackNotifier{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
DisableResolveMessage: model.DisableResolveMessage,
Settings: model.Settings,
Uid: config.UID,
Name: config.Name,
Type: config.Type,
DisableResolveMessage: config.DisableResolveMessage,
Settings: config.Settings,
}),
URL: apiURL,
Recipient: recipient,
MentionUsers: mentionUsers,
MentionGroups: mentionGroups,
MentionChannel: mentionChannel,
Username: model.Settings.Get("username").MustString("Grafana"),
IconEmoji: model.Settings.Get("icon_emoji").MustString(),
IconURL: model.Settings.Get("icon_url").MustString(),
Token: token,
Text: model.Settings.Get("text").MustString(`{{ template "default.message" . }}`),
Title: model.Settings.Get("title").MustString(DefaultMessageTitleEmbed),
URL: config.URL,
Recipient: config.Recipient,
MentionUsers: config.MentionUsers,
MentionGroups: config.MentionGroups,
MentionChannel: config.MentionChannel,
Username: config.Username,
IconEmoji: config.IconEmoji,
IconURL: config.IconURL,
Token: config.Token,
Text: config.Text,
Title: config.Title,
log: log.New("alerting.notifier.slack"),
tmpl: t,
}, nil
}
}
// slackMessage is the slackMessage for sending a slack notification.

View File

@ -152,13 +152,13 @@ func TestSlackNotifier(t *testing.T) {
settings: `{
"recipient": "#testchannel"
}`,
expInitError: `failed to validate receiver "slack_testing" of type "slack": token must be specified when using the Slack chat API`,
expInitError: `token must be specified when using the Slack chat API`,
}, {
name: "Missing recipient",
settings: `{
"token": "1234"
}`,
expInitError: `failed to validate receiver "slack_testing" of type "slack": recipient must be specified when using the Slack chat API`,
expInitError: `recipient must be specified when using the Slack chat API`,
},
{
name: "Custom endpoint url",
@ -213,7 +213,7 @@ func TestSlackNotifier(t *testing.T) {
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
decryptFn := secretsService.GetDecryptedValue
pn, err := NewSlackNotifier(m, tmpl, decryptFn)
cfg, err := NewSlackConfig(m, decryptFn)
if c.expInitError != "" {
require.Error(t, err)
require.Equal(t, c.expInitError, err.Error())
@ -246,6 +246,7 @@ func TestSlackNotifier(t *testing.T) {
ctx := notify.WithGroupKey(context.Background(), "alertname")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
pn := NewSlackNotifier(cfg, tmpl)
ok, err := pn.Notify(ctx, c.alerts...)
if c.expMsgError != nil {
require.Error(t, err)

View File

@ -24,31 +24,51 @@ type TeamsNotifier struct {
ns notifications.WebhookSender
}
type TeamsConfig struct {
*NotificationChannelConfig
URL string
Message string
}
func TeamsFactory(fc FactoryConfig) (NotificationChannel, error) {
cfg, err := NewTeamsConfig(fc.Config)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
return NewTeamsNotifier(cfg, fc.NotificationService, fc.Template), nil
}
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")
}
return &TeamsConfig{
NotificationChannelConfig: config,
URL: URL,
Message: config.Settings.Get("message").MustString(`{{ template "teams.default.message" .}}`),
}, nil
}
// NewTeamsNotifier is the constructor for Teams notifier.
func NewTeamsNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template) (*TeamsNotifier, error) {
if model.Settings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"}
}
u := model.Settings.Get("url").MustString()
if u == "" {
return nil, receiverInitError{Cfg: *model, Reason: "could not find url property in settings"}
}
func NewTeamsNotifier(config *TeamsConfig, ns notifications.WebhookSender, t *template.Template) *TeamsNotifier {
return &TeamsNotifier{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
DisableResolveMessage: model.DisableResolveMessage,
Settings: model.Settings,
Uid: config.UID,
Name: config.Name,
Type: config.Type,
DisableResolveMessage: config.DisableResolveMessage,
Settings: config.Settings,
}),
URL: u,
Message: model.Settings.Get("message").MustString(`{{ template "teams.default.message" .}}`),
URL: config.URL,
Message: config.Message,
log: log.New("alerting.notifier.teams"),
ns: ns,
tmpl: t,
}, nil
}
}
// Notify send an alert notification to Microsoft teams.

View File

@ -106,7 +106,7 @@ func TestTeamsNotifier(t *testing.T) {
}, {
name: "Error in initing",
settings: `{}`,
expInitError: `failed to validate receiver "teams_testing" of type "teams": could not find url property in settings`,
expInitError: `could not find url property in settings`,
},
}
@ -122,7 +122,7 @@ func TestTeamsNotifier(t *testing.T) {
}
webhookSender := mockNotificationService()
pn, err := NewTeamsNotifier(m, webhookSender, tmpl)
cfg, err := NewTeamsConfig(m)
if c.expInitError != "" {
require.Error(t, err)
require.Equal(t, c.expInitError, err.Error())
@ -132,6 +132,7 @@ func TestTeamsNotifier(t *testing.T) {
ctx := notify.WithGroupKey(context.Background(), "alertname")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
pn := NewTeamsNotifier(cfg, webhookSender, tmpl)
ok, err := pn.Notify(ctx, c.alerts...)
if c.expMsgError != nil {
require.False(t, ok)

View File

@ -3,6 +3,7 @@ package channels
import (
"bytes"
"context"
"errors"
"fmt"
"mime/multipart"
@ -29,42 +30,58 @@ type TelegramNotifier struct {
tmpl *template.Template
}
// NewTelegramNotifier is the constructor for the Telegram notifier
func NewTelegramNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template, fn GetDecryptedValueFn) (*TelegramNotifier, error) {
if model.Settings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"}
}
if model.SecureSettings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no secure settings supplied"}
}
type TelegramConfig struct {
*NotificationChannelConfig
BotToken string
ChatID string
Message string
}
botToken := fn(context.Background(), model.SecureSettings, "bottoken", model.Settings.Get("bottoken").MustString())
chatID := model.Settings.Get("chatid").MustString()
message := model.Settings.Get("message").MustString(`{{ template "default.message" . }}`)
func TelegramFactory(fc FactoryConfig) (NotificationChannel, error) {
config, err := NewTelegramConfig(fc.Config, fc.DecryptFunc)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
return NewTelegramNotifier(config, fc.NotificationService, fc.Template), nil
}
func NewTelegramConfig(config *NotificationChannelConfig, fn GetDecryptedValueFn) (*TelegramConfig, error) {
botToken := fn(context.Background(), config.SecureSettings, "bottoken", config.Settings.Get("bottoken").MustString())
if botToken == "" {
return nil, receiverInitError{Cfg: *model, Reason: "could not find Bot Token in settings"}
return &TelegramConfig{}, errors.New("could not find Bot Token in settings")
}
chatID := config.Settings.Get("chatid").MustString()
if chatID == "" {
return nil, receiverInitError{Cfg: *model, Reason: "could not find Chat Id in settings"}
return &TelegramConfig{}, errors.New("could not find Chat Id in settings")
}
return &TelegramConfig{
NotificationChannelConfig: config,
BotToken: botToken,
ChatID: chatID,
Message: config.Settings.Get("message").MustString(`{{ template "default.message" . }}`),
}, nil
}
// NewTelegramNotifier is the constructor for the Telegram notifier
func NewTelegramNotifier(config *TelegramConfig, ns notifications.WebhookSender, t *template.Template) *TelegramNotifier {
return &TelegramNotifier{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
DisableResolveMessage: model.DisableResolveMessage,
Settings: model.Settings,
Uid: config.UID,
Name: config.Name,
Type: config.Type,
DisableResolveMessage: config.DisableResolveMessage,
Settings: config.Settings,
}),
BotToken: botToken,
ChatID: chatID,
Message: message,
BotToken: config.BotToken,
ChatID: config.ChatID,
Message: config.Message,
tmpl: t,
log: log.New("alerting.notifier.telegram"),
ns: ns,
}, nil
}
}
// Notify send an alert notification to Telegram.

View File

@ -81,7 +81,7 @@ func TestTelegramNotifier(t *testing.T) {
}, {
name: "Error in initing",
settings: `{}`,
expInitError: `failed to validate receiver "telegram_testing" of type "telegram": could not find Bot Token in settings`,
expInitError: `could not find Bot Token in settings`,
},
}
@ -101,16 +101,16 @@ func TestTelegramNotifier(t *testing.T) {
webhookSender := mockNotificationService()
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
decryptFn := secretsService.GetDecryptedValue
pn, err := NewTelegramNotifier(m, webhookSender, tmpl, decryptFn)
cfg, err := NewTelegramConfig(m, decryptFn)
if c.expInitError != "" {
require.Error(t, err)
require.Equal(t, c.expInitError, err.Error())
return
}
require.NoError(t, err)
ctx := notify.WithGroupKey(context.Background(), "alertname")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
pn := NewTelegramNotifier(cfg, webhookSender, tmpl)
msg, err := pn.buildTelegramMessage(ctx, c.alerts)
if c.expMsgError != nil {
require.Error(t, err)

View File

@ -2,6 +2,7 @@ package channels
import (
"context"
"errors"
"fmt"
"net/url"
"path"
@ -31,53 +32,71 @@ type ThreemaNotifier struct {
tmpl *template.Template
}
// NewThreemaNotifier is the constructor for the Threema notifier
func NewThreemaNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template, fn GetDecryptedValueFn) (*ThreemaNotifier, error) {
if model.Settings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"}
}
if model.SecureSettings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no secure settings supplied"}
}
gatewayID := model.Settings.Get("gateway_id").MustString()
recipientID := model.Settings.Get("recipient_id").MustString()
apiSecret := fn(context.Background(), model.SecureSettings, "api_secret", model.Settings.Get("api_secret").MustString())
type ThreemaConfig struct {
*NotificationChannelConfig
GatewayID string
RecipientID string
APISecret string
}
// Validation
func ThreemaFactory(fc FactoryConfig) (NotificationChannel, error) {
cfg, err := NewThreemaConfig(fc.Config, fc.DecryptFunc)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
return NewThreemaNotifier(cfg, fc.NotificationService, fc.Template), nil
}
func NewThreemaConfig(config *NotificationChannelConfig, decryptFunc GetDecryptedValueFn) (*ThreemaConfig, error) {
gatewayID := config.Settings.Get("gateway_id").MustString()
if gatewayID == "" {
return nil, receiverInitError{Cfg: *model, Reason: "could not find Threema Gateway ID in settings"}
return nil, errors.New("could not find Threema Gateway ID in settings")
}
if !strings.HasPrefix(gatewayID, "*") {
return nil, receiverInitError{Cfg: *model, Reason: "invalid Threema Gateway ID: Must start with a *"}
return nil, errors.New("invalid Threema Gateway ID: Must start with a *")
}
if len(gatewayID) != 8 {
return nil, receiverInitError{Cfg: *model, Reason: "invalid Threema Gateway ID: Must be 8 characters long"}
return nil, errors.New("invalid Threema Gateway ID: Must be 8 characters long")
}
recipientID := config.Settings.Get("recipient_id").MustString()
if recipientID == "" {
return nil, receiverInitError{Cfg: *model, Reason: "could not find Threema Recipient ID in settings"}
return nil, errors.New("could not find Threema Recipient ID in settings")
}
if len(recipientID) != 8 {
return nil, receiverInitError{Cfg: *model, Reason: "invalid Threema Recipient ID: Must be 8 characters long"}
return nil, errors.New("invalid Threema Recipient ID: Must be 8 characters long")
}
apiSecret := decryptFunc(context.Background(), config.SecureSettings, "api_secret", config.Settings.Get("api_secret").MustString())
if apiSecret == "" {
return nil, receiverInitError{Cfg: *model, Reason: "could not find Threema API secret in settings"}
return nil, errors.New("could not find Threema API secret in settings")
}
return &ThreemaConfig{
NotificationChannelConfig: config,
GatewayID: gatewayID,
RecipientID: recipientID,
APISecret: apiSecret,
}, nil
}
// NewThreemaNotifier is the constructor for the Threema notifier
func NewThreemaNotifier(config *ThreemaConfig, ns notifications.WebhookSender, t *template.Template) *ThreemaNotifier {
return &ThreemaNotifier{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
DisableResolveMessage: model.DisableResolveMessage,
Settings: model.Settings,
Uid: config.UID,
Name: config.Name,
Type: config.Type,
DisableResolveMessage: config.DisableResolveMessage,
Settings: config.Settings,
}),
GatewayID: gatewayID,
RecipientID: recipientID,
APISecret: apiSecret,
GatewayID: config.GatewayID,
RecipientID: config.RecipientID,
APISecret: config.APISecret,
log: log.New("alerting.notifier.threema"),
ns: ns,
tmpl: t,
}, nil
}
}
// Notify send an alert notification to Threema

View File

@ -76,7 +76,7 @@ func TestThreemaNotifier(t *testing.T) {
"recipient_id": "87654321",
"api_secret": "supersecret"
}`,
expInitError: `failed to validate receiver "threema_testing" of type "threema": invalid Threema Gateway ID: Must start with a *`,
expInitError: `invalid Threema Gateway ID: Must start with a *`,
}, {
name: "Invalid receipent id",
settings: `{
@ -84,14 +84,14 @@ func TestThreemaNotifier(t *testing.T) {
"recipient_id": "8765432",
"api_secret": "supersecret"
}`,
expInitError: `failed to validate receiver "threema_testing" of type "threema": invalid Threema Recipient ID: Must be 8 characters long`,
expInitError: `invalid Threema Recipient ID: Must be 8 characters long`,
}, {
name: "No API secret",
settings: `{
"gateway_id": "*1234567",
"recipient_id": "87654321"
}`,
expInitError: `failed to validate receiver "threema_testing" of type "threema": could not find Threema API secret in settings`,
expInitError: `could not find Threema API secret in settings`,
},
}
@ -111,7 +111,7 @@ func TestThreemaNotifier(t *testing.T) {
webhookSender := mockNotificationService()
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
decryptFn := secretsService.GetDecryptedValue
pn, err := NewThreemaNotifier(m, webhookSender, tmpl, decryptFn)
cfg, err := NewThreemaConfig(m, decryptFn)
if c.expInitError != "" {
require.Error(t, err)
require.Equal(t, c.expInitError, err.Error())
@ -121,6 +121,7 @@ func TestThreemaNotifier(t *testing.T) {
ctx := notify.WithGroupKey(context.Background(), "alertname")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
pn := NewThreemaNotifier(cfg, webhookSender, tmpl)
ok, err := pn.Notify(ctx, c.alerts...)
if c.expMsgError != nil {
require.False(t, ok)

View File

@ -12,6 +12,7 @@ import (
"path"
"time"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/common/model"
"github.com/grafana/grafana/pkg/infra/log"
@ -60,6 +61,10 @@ func getAlertStatusColor(status model.AlertStatus) string {
return ColorAlertResolved
}
type NotificationChannel interface {
notify.Notifier
notify.ResolvedSender
}
type NotificationChannelConfig struct {
OrgID int64 // only used internally
UID string `json:"uid"`

View File

@ -2,6 +2,7 @@ package channels
import (
"context"
"errors"
"strings"
"time"
@ -25,32 +26,52 @@ const (
victoropsAlertStateRecovery = "RECOVERY"
)
type VictorOpsConfig struct {
*NotificationChannelConfig
URL string
MessageType string
}
func VictorOpsFactory(fc FactoryConfig) (NotificationChannel, error) {
cfg, err := NewVictorOpsConfig(fc.Config)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
return NewVictoropsNotifier(cfg, fc.NotificationService, fc.Template), nil
}
func NewVictorOpsConfig(config *NotificationChannelConfig) (*VictorOpsConfig, error) {
url := config.Settings.Get("url").MustString()
if url == "" {
return nil, errors.New("could not find victorops url property in settings")
}
return &VictorOpsConfig{
NotificationChannelConfig: config,
URL: url,
MessageType: config.Settings.Get("messageType").MustString(),
}, nil
}
// NewVictoropsNotifier creates an instance of VictoropsNotifier that
// handles posting notifications to Victorops REST API
func NewVictoropsNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template) (*VictoropsNotifier, error) {
if model.Settings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"}
}
url := model.Settings.Get("url").MustString()
if url == "" {
return nil, receiverInitError{Cfg: *model, Reason: "could not find victorops url property in settings"}
}
func NewVictoropsNotifier(config *VictorOpsConfig, ns notifications.WebhookSender, t *template.Template) *VictoropsNotifier {
return &VictoropsNotifier{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
DisableResolveMessage: model.DisableResolveMessage,
Settings: model.Settings,
Uid: config.UID,
Name: config.Name,
Type: config.Type,
DisableResolveMessage: config.DisableResolveMessage,
Settings: config.Settings,
}),
URL: url,
MessageType: model.Settings.Get("messageType").MustString(),
URL: config.URL,
MessageType: config.MessageType,
log: log.New("alerting.notifier.victorops"),
ns: ns,
tmpl: t,
}, nil
}
}
// VictoropsNotifier defines URL property for Victorops REST API

View File

@ -78,7 +78,7 @@ func TestVictoropsNotifier(t *testing.T) {
}, {
name: "Error in initing, no URL",
settings: `{}`,
expInitError: `failed to validate receiver "victorops_testing" of type "victorops": could not find victorops url property in settings`,
expInitError: `could not find victorops url property in settings`,
},
}
@ -94,7 +94,7 @@ func TestVictoropsNotifier(t *testing.T) {
}
webhookSender := mockNotificationService()
pn, err := NewVictoropsNotifier(m, webhookSender, tmpl)
cfg, err := NewVictorOpsConfig(m)
if c.expInitError != "" {
require.Error(t, err)
require.Equal(t, c.expInitError, err.Error())
@ -104,6 +104,7 @@ func TestVictoropsNotifier(t *testing.T) {
ctx := notify.WithGroupKey(context.Background(), "alertname")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
pn := NewVictoropsNotifier(cfg, webhookSender, tmpl)
ok, err := pn.Notify(ctx, c.alerts...)
if c.expMsgError != nil {
require.False(t, ok)

View File

@ -3,6 +3,7 @@ package channels
import (
"context"
"encoding/json"
"errors"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
@ -28,37 +29,62 @@ type WebhookNotifier struct {
orgID int64
}
type WebhookConfig struct {
*NotificationChannelConfig
URL string
User string
Password string
HTTPMethod string
MaxAlerts int
}
func WebHookFactory(fc FactoryConfig) (NotificationChannel, error) {
cfg, err := NewWebHookConfig(fc.Config, fc.DecryptFunc)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
return NewWebHookNotifier(cfg, fc.NotificationService, fc.Template), nil
}
func NewWebHookConfig(config *NotificationChannelConfig, decryptFunc GetDecryptedValueFn) (*WebhookConfig, error) {
url := config.Settings.Get("url").MustString()
if url == "" {
return nil, errors.New("could not find url property in settings")
}
return &WebhookConfig{
NotificationChannelConfig: config,
URL: url,
User: config.Settings.Get("username").MustString(),
Password: decryptFunc(context.Background(), config.SecureSettings, "password", config.Settings.Get("password").MustString()),
HTTPMethod: config.Settings.Get("httpMethod").MustString("POST"),
MaxAlerts: config.Settings.Get("maxAlerts").MustInt(0),
}, nil
}
// NewWebHookNotifier is the constructor for
// the WebHook notifier.
func NewWebHookNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template, fn GetDecryptedValueFn) (*WebhookNotifier, error) {
if model.Settings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"}
}
if model.SecureSettings == nil {
return nil, receiverInitError{Cfg: *model, Reason: "no secure settings supplied"}
}
url := model.Settings.Get("url").MustString()
if url == "" {
return nil, receiverInitError{Cfg: *model, Reason: "could not find url property in settings"}
}
func NewWebHookNotifier(config *WebhookConfig, ns notifications.WebhookSender, t *template.Template) *WebhookNotifier {
return &WebhookNotifier{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
DisableResolveMessage: model.DisableResolveMessage,
Settings: model.Settings,
Uid: config.UID,
Name: config.Name,
Type: config.Type,
DisableResolveMessage: config.DisableResolveMessage,
Settings: config.Settings,
}),
orgID: model.OrgID,
URL: url,
User: model.Settings.Get("username").MustString(),
Password: fn(context.Background(), model.SecureSettings, "password", model.Settings.Get("password").MustString()),
HTTPMethod: model.Settings.Get("httpMethod").MustString("POST"),
MaxAlerts: model.Settings.Get("maxAlerts").MustInt(0),
orgID: config.OrgID,
URL: config.URL,
User: config.User,
Password: config.Password,
HTTPMethod: config.HTTPMethod,
MaxAlerts: config.MaxAlerts,
log: log.New("alerting.notifier.webhook"),
ns: ns,
tmpl: t,
}, nil
}
}
// webhookMessage defines the JSON object send to webhook endpoints.

View File

@ -172,7 +172,7 @@ func TestWebhookNotifier(t *testing.T) {
}, {
name: "Error in initing",
settings: `{}`,
expInitError: `failed to validate receiver "webhook_testing" of type "webhook": could not find url property in settings`,
expInitError: `could not find url property in settings`,
},
}
@ -193,7 +193,7 @@ func TestWebhookNotifier(t *testing.T) {
webhookSender := mockNotificationService()
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
decryptFn := secretsService.GetDecryptedValue
pn, err := NewWebHookNotifier(m, webhookSender, tmpl, decryptFn)
cfg, err := NewWebHookConfig(m, decryptFn)
if c.expInitError != "" {
require.Error(t, err)
require.Equal(t, c.expInitError, err.Error())
@ -204,6 +204,7 @@ func TestWebhookNotifier(t *testing.T) {
ctx := notify.WithGroupKey(context.Background(), "alertname")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
ctx = notify.WithReceiverName(ctx, "my_receiver")
pn := NewWebHookNotifier(cfg, webhookSender, tmpl)
ok, err := pn.Notify(ctx, c.alerts...)
if c.expMsgError != nil {
require.False(t, ok)

View File

@ -3,6 +3,7 @@ package channels
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/prometheus/alertmanager/types"
@ -13,28 +14,51 @@ import (
"github.com/prometheus/alertmanager/template"
)
// NewWeComNotifier is the constructor for WeCom notifier.
func NewWeComNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template, fn GetDecryptedValueFn) (*WeComNotifier, error) {
url := fn(context.Background(), model.SecureSettings, "url", model.Settings.Get("url").MustString())
type WeComConfig struct {
*NotificationChannelConfig
URL string
Message string
}
if url == "" {
return nil, receiverInitError{Cfg: *model, Reason: "could not find webhook URL in settings"}
func WeComFactory(fc FactoryConfig) (NotificationChannel, error) {
cfg, err := NewWeComConfig(fc.Config, fc.DecryptFunc)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
return NewWeComNotifier(cfg, fc.NotificationService, fc.Template), nil
}
func NewWeComConfig(config *NotificationChannelConfig, decryptFunc GetDecryptedValueFn) (*WeComConfig, error) {
url := decryptFunc(context.Background(), config.SecureSettings, "url", config.Settings.Get("url").MustString())
if url == "" {
return nil, errors.New("could not find webhook URL in settings")
}
return &WeComConfig{
NotificationChannelConfig: config,
URL: url,
Message: config.Settings.Get("message").MustString(`{{ template "default.message" .}}`),
}, nil
}
// NewWeComNotifier is the constructor for WeCom notifier.
func NewWeComNotifier(config *WeComConfig, ns notifications.WebhookSender, t *template.Template) *WeComNotifier {
return &WeComNotifier{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
DisableResolveMessage: model.DisableResolveMessage,
Settings: model.Settings,
Uid: config.UID,
Name: config.Name,
Type: config.Type,
DisableResolveMessage: config.DisableResolveMessage,
Settings: config.Settings,
}),
URL: url,
URL: config.URL,
Message: config.Message,
log: log.New("alerting.notifier.wecom"),
ns: ns,
Message: model.Settings.Get("message").MustString(`{{ template "default.message" .}}`),
tmpl: t,
}, nil
}
}
// WeComNotifier is responsible for sending alert notifications to WeCom.

View File

@ -79,7 +79,7 @@ func TestWeComNotifier(t *testing.T) {
}, {
name: "Error in initing",
settings: `{}`,
expInitError: `failed to validate receiver "wecom_testing" of type "wecom": could not find webhook URL in settings`,
expInitError: `could not find webhook URL in settings`,
},
}
@ -97,7 +97,7 @@ func TestWeComNotifier(t *testing.T) {
webhookSender := mockNotificationService()
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
decryptFn := secretsService.GetDecryptedValue
pn, err := NewWeComNotifier(m, webhookSender, tmpl, decryptFn)
cfg, err := NewWeComConfig(m, decryptFn)
if c.expInitError != "" {
require.Equal(t, c.expInitError, err.Error())
return
@ -106,6 +106,7 @@ func TestWeComNotifier(t *testing.T) {
ctx := notify.WithGroupKey(context.Background(), "alertname")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
pn := NewWeComNotifier(cfg, webhookSender, tmpl)
ok, err := pn.Notify(ctx, c.alerts...)
if c.expMsgError != nil {
require.False(t, ok)

View File

@ -490,46 +490,15 @@ func (m *migration) validateAlertmanagerConfig(orgID int64, config *PostableUser
}
return fallback
}
switch gr.Type {
case "email":
_, err = channels.NewEmailNotifier(cfg, nil, nil) // Email notifier already has a default template.
case "pagerduty":
_, err = channels.NewPagerdutyNotifier(cfg, nil, nil, decryptFunc)
case "pushover":
_, err = channels.NewPushoverNotifier(cfg, nil, nil, decryptFunc)
case "slack":
_, err = channels.NewSlackNotifier(cfg, nil, decryptFunc)
case "telegram":
_, err = channels.NewTelegramNotifier(cfg, nil, nil, decryptFunc)
case "victorops":
_, err = channels.NewVictoropsNotifier(cfg, nil, nil)
case "teams":
_, err = channels.NewTeamsNotifier(cfg, nil, nil)
case "dingding":
_, err = channels.NewDingDingNotifier(cfg, nil, nil)
case "kafka":
_, err = channels.NewKafkaNotifier(cfg, nil, nil)
case "webhook":
_, err = channels.NewWebHookNotifier(cfg, nil, nil, decryptFunc)
case "sensugo":
_, err = channels.NewSensuGoNotifier(cfg, nil, nil, decryptFunc)
case "discord":
_, err = channels.NewDiscordNotifier(cfg, nil, nil)
case "googlechat":
_, err = channels.NewGoogleChatNotifier(cfg, nil, nil)
case "LINE":
_, err = channels.NewLineNotifier(cfg, nil, nil, decryptFunc)
case "threema":
_, err = channels.NewThreemaNotifier(cfg, nil, nil, decryptFunc)
case "opsgenie":
_, err = channels.NewOpsgenieNotifier(cfg, nil, nil, decryptFunc)
case "prometheus-alertmanager":
_, err = channels.NewAlertmanagerNotifier(cfg, nil, decryptFunc)
default:
receiverFactory, exists := channels.Factory(gr.Type)
if !exists {
return fmt.Errorf("notifier %s is not supported", gr.Type)
}
factoryConfig, err := channels.NewFactoryConfig(cfg, nil, decryptFunc, nil)
if err != nil {
return err
}
_, err = receiverFactory(factoryConfig)
if err != nil {
return err
}

View File

@ -32,7 +32,7 @@ func Test_validateAlertmanagerConfig(t *testing.T) {
SecureSettings: map[string]string{"url": invalidUri},
},
},
err: fmt.Errorf("failed to validate receiver \"SlackWithBadURL\" of type \"slack\": invalid URL %q: parse %q: net/url: invalid control character in URL", invalidUri, invalidUri),
err: fmt.Errorf("failed to validate receiver \"SlackWithBadURL\" of type \"slack\": invalid URL %q", invalidUri),
},
{
name: "when a slack receiver has an invalid recipient - it should not error",