diff --git a/cmd/notifier/config.go b/cmd/notifier/config.go index 1043dd2d4..b39db66ce 100644 --- a/cmd/notifier/config.go +++ b/cmd/notifier/config.go @@ -55,6 +55,8 @@ type notifierConfig struct { MaxFailAttemptToSendAvailable int `yaml:"max_fail_attempt_to_send_available"` // Specify log level by entities SetLogLevel setLogLevelConfig `yaml:"set_log_level"` + // Sets the maximum number of sends per sender + MaxParallelSendsPerSender int `yaml:"max_parallel_sends_per_sender"` } type selfStateConfig struct { @@ -114,6 +116,7 @@ func getDefault() config { Timezone: "UTC", ReadBatchSize: int(notifier.NotificationsLimitUnlimited), MaxFailAttemptToSendAvailable: 3, + MaxParallelSendsPerSender: 16, }, Telemetry: cmd.TelemetryConfig{ Listen: ":8093", @@ -208,6 +211,7 @@ func (config *notifierConfig) getSettings(logger moira.Logger) notifier.Config { MaxFailAttemptToSendAvailable: config.MaxFailAttemptToSendAvailable, LogContactsToLevel: contacts, LogSubscriptionsToLevel: subscriptions, + MaxParallelSendsPerSender: config.MaxParallelSendsPerSender, } } diff --git a/cmd/notifier/main.go b/cmd/notifier/main.go index 22c8293fe..872fcbd2d 100644 --- a/cmd/notifier/main.go +++ b/cmd/notifier/main.go @@ -104,6 +104,7 @@ func main() { notifierConfig := config.Notifier.getSettings(logger) notifierMetrics := metrics.ConfigureNotifierMetrics(telemetry.Metrics, serviceName) + scheduler := notifier.NewScheduler(database, logger, notifierMetrics) sender := notifier.NewNotifier( database, logger, @@ -111,6 +112,7 @@ func main() { notifierMetrics, metricSourceProvider, imageStoreMap, + scheduler, ) // Register moira senders diff --git a/database/redis/trigger.go b/database/redis/trigger.go index 83055aa0c..cc7edba21 100644 --- a/database/redis/trigger.go +++ b/database/redis/trigger.go @@ -208,7 +208,7 @@ func (connector *DbConnector) GetTriggerIDsStartWith(prefix string) ([]string, e return matchedTriggers, nil } -func (connector *DbConnector) updateTrigger(triggerID string, newTrigger *moira.Trigger, oldTrigger *moira.Trigger) error { +func (connector *DbConnector) updateTrigger(triggerID string, newTrigger *moira.Trigger, oldTrigger *moira.Trigger) error { // nolint:gocyclo bytes, err := reply.GetTriggerBytes(triggerID, newTrigger) if err != nil { return err diff --git a/integration_tests/notifier/notifier_test.go b/integration_tests/notifier/notifier_test.go index 51bc894a9..741b8acfb 100644 --- a/integration_tests/notifier/notifier_test.go +++ b/integration_tests/notifier/notifier_test.go @@ -27,11 +27,12 @@ var location, _ = time.LoadLocation("UTC") var dateTimeFormat = "15:04 02.01.2006" var notifierConfig = notifier.Config{ - SendingTimeout: time.Millisecond * 10, - ResendingTimeout: time.Hour * 24, - Location: location, - DateTimeFormat: dateTimeFormat, - ReadBatchSize: notifier.NotificationsLimitUnlimited, + SendingTimeout: time.Millisecond * 10, + ResendingTimeout: time.Hour * 24, + Location: location, + DateTimeFormat: dateTimeFormat, + ReadBatchSize: notifier.NotificationsLimitUnlimited, + MaxParallelSendsPerSender: 16, } var shutdown = make(chan struct{}) @@ -87,6 +88,7 @@ func TestNotifier(t *testing.T) { database.PushNotificationEvent(&event, true) //nolint metricsSourceProvider := metricSource.CreateMetricSourceProvider(local.Create(database), nil, nil) + scheduler := notifier.NewScheduler(database, logger, notifierMetrics) notifierInstance := notifier.NewNotifier( database, @@ -95,6 +97,7 @@ func TestNotifier(t *testing.T) { notifierMetrics, metricsSourceProvider, map[string]moira.ImageStore{}, + scheduler, ) sender := mock_moira_alert.NewMockSender(mockCtrl) @@ -113,7 +116,7 @@ func TestNotifier(t *testing.T) { Database: database, Logger: logger, Metrics: notifierMetrics, - Scheduler: notifier.NewScheduler(database, logger, notifierMetrics), + Scheduler: scheduler, } fetchNotificationsWorker := notifications.FetchNotificationsWorker{ diff --git a/local/api.yml b/local/api.yml index 1b5b9834e..4368b8d70 100644 --- a/local/api.yml +++ b/local/api.yml @@ -50,7 +50,7 @@ web: is_plotting_available: true is_plotting_default_on: true is_subscription_to_all_tags_available: true - is_readonly_enabled: true + is_readonly_enabled: false notification_history: ttl: 48h query_limit: 10000 diff --git a/local/notifier.yml b/local/notifier.yml index 925dbb5c4..0855153f4 100644 --- a/local/notifier.yml +++ b/local/notifier.yml @@ -39,6 +39,7 @@ notifier: front_uri: http://localhost timezone: UTC date_time_format: "15:04 02.01.2006" + max_parallel_sends_per_sender: 16 notification_history: ttl: 48h query_limit: 10000 diff --git a/notifier/config.go b/notifier/config.go index 10639101a..eec486d76 100644 --- a/notifier/config.go +++ b/notifier/config.go @@ -23,4 +23,5 @@ type Config struct { MaxFailAttemptToSendAvailable int LogContactsToLevel map[string]string LogSubscriptionsToLevel map[string]string + MaxParallelSendsPerSender int } diff --git a/notifier/notifier.go b/notifier/notifier.go index e1ca215f6..18ee20516 100644 --- a/notifier/notifier.go +++ b/notifier/notifier.go @@ -71,10 +71,12 @@ type Notifier interface { GetReadBatchSize() int64 } +type SendersNotificationChannel map[string]chan NotificationPackage + // StandardNotifier represent notification functionality type StandardNotifier struct { waitGroup sync.WaitGroup - senders map[string]chan NotificationPackage + sendersChannel SendersNotificationChannel logger moira.Logger database moira.Database scheduler Scheduler @@ -85,12 +87,20 @@ type StandardNotifier struct { } // NewNotifier is initializer for StandardNotifier -func NewNotifier(database moira.Database, logger moira.Logger, config Config, metrics *metrics.NotifierMetrics, metricSourceProvider *metricSource.SourceProvider, imageStoreMap map[string]moira.ImageStore) *StandardNotifier { +func NewNotifier( + database moira.Database, + logger moira.Logger, + config Config, + metrics *metrics.NotifierMetrics, + metricSourceProvider *metricSource.SourceProvider, + imageStoreMap map[string]moira.ImageStore, + scheduler Scheduler, +) *StandardNotifier { return &StandardNotifier{ - senders: make(map[string]chan NotificationPackage), + sendersChannel: make(SendersNotificationChannel), logger: logger, database: database, - scheduler: NewScheduler(database, logger, metrics), + scheduler: scheduler, config: config, metrics: metrics, metricSourceProvider: metricSourceProvider, @@ -100,11 +110,12 @@ func NewNotifier(database moira.Database, logger moira.Logger, config Config, me // Send is realization of StandardNotifier Send functionality func (notifier *StandardNotifier) Send(pkg *NotificationPackage, waitGroup *sync.WaitGroup) { - ch, found := notifier.senders[pkg.Contact.Type] + ch, found := notifier.sendersChannel[pkg.Contact.Type] if !found { notifier.resend(pkg, fmt.Sprintf("Unknown contact type '%s' [%s]", pkg.Contact.Type, pkg)) return } + waitGroup.Add(1) go func(pkg *NotificationPackage) { defer waitGroup.Done() @@ -126,7 +137,7 @@ func (notifier *StandardNotifier) Send(pkg *NotificationPackage, waitGroup *sync // GetSenders get hash of registered notifier senders func (notifier *StandardNotifier) GetSenders() map[string]bool { hash := make(map[string]bool) - for key := range notifier.senders { + for key := range notifier.sendersChannel { hash[key] = true } return hash @@ -142,6 +153,7 @@ func (notifier *StandardNotifier) resend(pkg *NotificationPackage, reason string notifier.metrics.MarkSendersDroppedNotifications(pkg.Contact.Type) return } + notifier.metrics.SendingFailed.Mark(1) if metric, found := notifier.metrics.SendersFailedMetrics.GetRegisteredMeter(pkg.Contact.Type); found { metric.Mark(1) @@ -212,6 +224,7 @@ func (notifier *StandardNotifier) runSender(sender moira.Sender, ch chan Notific notifier.metrics.MarkSendersOkMetrics(pkg.Contact.Type) continue } + switch e := err.(type) { case moira.SenderBrokenContactError: log.Warning(). diff --git a/notifier/notifier_test.go b/notifier/notifier_test.go index e3085e3f9..96589d4a9 100644 --- a/notifier/notifier_test.go +++ b/notifier/notifier_test.go @@ -19,17 +19,31 @@ import ( ) var ( - plots [][]byte - shutdown = make(chan struct{}) + plots [][]byte + shutdown = make(chan struct{}) + location, _ = time.LoadLocation("UTC") + dateTimeFormat = "15:04 02.01.2006" + defaultConfig = Config{ + SendingTimeout: time.Millisecond * 10, + ResendingTimeout: time.Hour * 24, + Location: location, + DateTimeFormat: dateTimeFormat, + MaxParallelSendsPerSender: 16, + Senders: []map[string]interface{}{ + { + "type": "test", + }, + }, + } ) var ( - mockCtrl *gomock.Controller - sender *mock_moira_alert.MockSender - notif *StandardNotifier - scheduler *mock_scheduler.MockScheduler - dataBase *mock_moira_alert.MockDatabase - logger moira.Logger + mockCtrl *gomock.Controller + sender *mock_moira_alert.MockSender + standardNotifier *StandardNotifier + scheduler *mock_scheduler.MockScheduler + dataBase *mock_moira_alert.MockDatabase + logger moira.Logger ) func TestGetMetricNames(t *testing.T) { @@ -39,6 +53,7 @@ func TestGetMetricNames(t *testing.T) { actual := notificationsPackage.GetMetricNames() So(actual, ShouldResemble, expected) }) + Convey("Test package with no trigger events", func() { pkg := NotificationPackage{} for _, event := range notificationsPackage.Events { @@ -52,6 +67,7 @@ func TestGetMetricNames(t *testing.T) { So(actual, ShouldResemble, expected) }) }) + Convey("Test empty notification package", t, func() { emptyNotificationPackage := NotificationPackage{} actual := emptyNotificationPackage.GetMetricNames() @@ -66,6 +82,7 @@ func TestGetWindow(t *testing.T) { So(from, ShouldEqual, 11) So(to, ShouldEqual, 179) }) + Convey("Test empty notification package", t, func() { emptyNotificationPackage := NotificationPackage{} _, _, err := emptyNotificationPackage.GetWindow() @@ -74,7 +91,7 @@ func TestGetWindow(t *testing.T) { } func TestUnknownContactType(t *testing.T) { - configureNotifier(t) + configureNotifier(t, defaultConfig) defer afterTest() var eventsData moira.NotificationEvents = []moira.NotificationEvent{event} @@ -90,12 +107,12 @@ func TestUnknownContactType(t *testing.T) { dataBase.EXPECT().AddNotification(¬ification).Return(nil) var wg sync.WaitGroup - notif.Send(&pkg, &wg) + standardNotifier.Send(&pkg, &wg) wg.Wait() } func TestFailSendEvent(t *testing.T) { - configureNotifier(t) + configureNotifier(t, defaultConfig) defer afterTest() var eventsData moira.NotificationEvents = []moira.NotificationEvent{event} @@ -106,13 +123,14 @@ func TestFailSendEvent(t *testing.T) { Type: "test", }, } + notification := moira.ScheduledNotification{} sender.EXPECT().SendEvents(eventsData, pkg.Contact, pkg.Trigger, plots, pkg.Throttled).Return(fmt.Errorf("Cant't send")) scheduler.EXPECT().ScheduleNotification(gomock.Any(), event, pkg.Trigger, pkg.Contact, pkg.Plotting, pkg.Throttled, pkg.FailCount+1, gomock.Any()).Return(¬ification) dataBase.EXPECT().AddNotification(¬ification).Return(nil) var wg sync.WaitGroup - notif.Send(&pkg, &wg) + standardNotifier.Send(&pkg, &wg) wg.Wait() time.Sleep(time.Second * 2) } @@ -122,7 +140,7 @@ func TestNoResendForSendToBrokenContact(t *testing.T) { t.Skip("skipping test in short mode.") } - configureNotifier(t) + configureNotifier(t, defaultConfig) defer afterTest() var eventsData moira.NotificationEvents = []moira.NotificationEvent{event} @@ -137,13 +155,13 @@ func TestNoResendForSendToBrokenContact(t *testing.T) { Return(moira.NewSenderBrokenContactError(fmt.Errorf("some sender reason"))) var wg sync.WaitGroup - notif.Send(&pkg, &wg) + standardNotifier.Send(&pkg, &wg) wg.Wait() time.Sleep(time.Second * 2) } func TestTimeout(t *testing.T) { - configureNotifier(t) + configureNotifier(t, defaultConfig) var wg sync.WaitGroup defer afterTest() @@ -160,10 +178,10 @@ func TestTimeout(t *testing.T) { sender.EXPECT().SendEvents(eventsData, pkg.Contact, pkg.Trigger, plots, pkg.Throttled).Return(nil).Do(func(arg0, arg1, arg2, arg3, arg4 interface{}) { fmt.Print("Trying to send for 10 second") time.Sleep(time.Second * 10) - }).Times(maxParallelSendsPerSender) + }).Times(standardNotifier.GetMaxParallelSendsPerSender()) - for i := 0; i < maxParallelSendsPerSender; i++ { - notif.Send(&pkg, &wg) + for i := 0; i < standardNotifier.GetMaxParallelSendsPerSender(); i++ { + standardNotifier.Send(&pkg, &wg) wg.Wait() } @@ -180,7 +198,7 @@ func TestTimeout(t *testing.T) { scheduler.EXPECT().ScheduleNotification(gomock.Any(), event, pkg2.Trigger, pkg2.Contact, pkg.Plotting, pkg2.Throttled, pkg2.FailCount+1, gomock.Any()).Return(¬ification) dataBase.EXPECT().AddNotification(¬ification).Return(nil).Do(func(f ...interface{}) { close(shutdown) }) - notif.Send(&pkg2, &wg) + standardNotifier.Send(&pkg2, &wg) wg.Wait() waitTestEnd() } @@ -195,16 +213,8 @@ func waitTestEnd() { } } -func configureNotifier(t *testing.T) { +func configureNotifier(t *testing.T, config Config) { notifierMetrics := metrics.ConfigureNotifierMetrics(metrics.NewDummyRegistry(), "notifier") - var location, _ = time.LoadLocation("UTC") - dateTimeFormat := "15:04 02.01.2006" - config := Config{ - SendingTimeout: time.Millisecond * 10, - ResendingTimeout: time.Hour * 24, - Location: location, - DateTimeFormat: dateTimeFormat, - } mockCtrl = gomock.NewController(t) dataBase = mock_moira_alert.NewMockDatabase(mockCtrl) @@ -213,24 +223,24 @@ func configureNotifier(t *testing.T) { sender = mock_moira_alert.NewMockSender(mockCtrl) metricsSourceProvider := metricSource.CreateMetricSourceProvider(local.Create(dataBase), nil, nil) - notif = NewNotifier(dataBase, logger, config, notifierMetrics, metricsSourceProvider, map[string]moira.ImageStore{}) - notif.scheduler = scheduler + standardNotifier = NewNotifier(dataBase, logger, config, notifierMetrics, metricsSourceProvider, map[string]moira.ImageStore{}, scheduler) senderSettings := map[string]interface{}{ "type": "test", } - sender.EXPECT().Init(senderSettings, logger, location, "15:04 02.01.2006").Return(nil) + sender.EXPECT().Init(senderSettings, logger, location, dateTimeFormat).Return(nil) - notif.RegisterSender(senderSettings, sender) //nolint + err := standardNotifier.RegisterSender(senderSettings, sender) Convey("Should return one sender", t, func() { - So(notif.GetSenders(), ShouldResemble, map[string]bool{"test": true}) + So(err, ShouldBeNil) + So(standardNotifier.GetSenders(), ShouldResemble, map[string]bool{"test": true}) }) } func afterTest() { mockCtrl.Finish() - notif.StopSenders() + standardNotifier.StopSenders() } var subID = "SubscriptionID-000000000000001" diff --git a/notifier/plotting_test.go b/notifier/plotting_test.go index eb5795b3b..1ee37cdf1 100644 --- a/notifier/plotting_test.go +++ b/notifier/plotting_test.go @@ -61,7 +61,7 @@ func generateTestMetricsData() map[string][]metricSource.MetricData { func TestResolveMetricsWindow(t *testing.T) { testLaunchTime := time.Now().UTC() - logger, _ := logging.GetLogger("Notifier") + logger, _ = logging.GetLogger("Notifier") emptyEventsPackage := NotificationPackage{} triggerJustCreatedEvents := NotificationPackage{ Events: []moira.NotificationEvent{ @@ -248,7 +248,7 @@ func TestBuildTriggerPlots(t *testing.T) { Convey("Run buildTriggerPlots", t, func() { triggerID := uuid.Must(uuid.NewV4()).String() trigger := moira.Trigger{ID: triggerID} - location, _ := time.LoadLocation("UTC") + location, _ = time.LoadLocation("UTC") plotTemplate, _ := plotting.GetPlotTemplate("", location) Convey("without errors", func() { diff --git a/notifier/registrator.go b/notifier/registrator.go index e31f6e7b1..8f9fee109 100644 --- a/notifier/registrator.go +++ b/notifier/registrator.go @@ -40,99 +40,131 @@ const ( mattermostSender = "mattermost" ) +func (notifier *StandardNotifier) registerMetrics(senderType string) { + notifier.metrics.SendersOkMetrics.RegisterMeter(senderType, getGraphiteSenderIdent(senderType), "sends_ok") + notifier.metrics.SendersFailedMetrics.RegisterMeter(senderType, getGraphiteSenderIdent(senderType), "sends_failed") + notifier.metrics.SendersDroppedNotifications.RegisterMeter(senderType, getGraphiteSenderIdent(senderType), "notifications_dropped") +} + // RegisterSenders watch on senders config and register all configured senders func (notifier *StandardNotifier) RegisterSenders(connector moira.Database) error { //nolint var err error + var sender moira.Sender + senders := make(map[string]moira.Sender) + for _, senderSettings := range notifier.config.Senders { senderSettings["front_uri"] = notifier.config.FrontURL - switch senderSettings["type"] { + + senderType, ok := senderSettings["type"].(string) + if !ok { + return fmt.Errorf("failed to get sender type from settings") + } + + if sender, ok = senders[senderType]; ok { + if err = notifier.RegisterSender(senderSettings, sender); err != nil { + return err + } + continue + } + + switch senderType { case mailSender: - err = notifier.RegisterSender(senderSettings, &mail.Sender{}) + sender = &mail.Sender{} case pushoverSender: - err = notifier.RegisterSender(senderSettings, &pushover.Sender{}) + sender = &pushover.Sender{} case scriptSender: - err = notifier.RegisterSender(senderSettings, &script.Sender{}) + sender = &script.Sender{} case discordSender: - err = notifier.RegisterSender(senderSettings, &discord.Sender{DataBase: connector}) + sender = &discord.Sender{DataBase: connector} case slackSender: - err = notifier.RegisterSender(senderSettings, &slack.Sender{}) + sender = &slack.Sender{} case telegramSender: - err = notifier.RegisterSender(senderSettings, &telegram.Sender{DataBase: connector}) + sender = &telegram.Sender{DataBase: connector} case msTeamsSender: - err = notifier.RegisterSender(senderSettings, &msteams.Sender{}) + sender = &msteams.Sender{} case pagerdutySender: - err = notifier.RegisterSender(senderSettings, &pagerduty.Sender{ImageStores: notifier.imageStores}) + sender = &pagerduty.Sender{ImageStores: notifier.imageStores} case twilioSmsSender, twilioVoiceSender: - err = notifier.RegisterSender(senderSettings, &twilio.Sender{}) + sender = &twilio.Sender{} case webhookSender: - err = notifier.RegisterSender(senderSettings, &webhook.Sender{}) + sender = &webhook.Sender{} case opsgenieSender: - err = notifier.RegisterSender(senderSettings, &opsgenie.Sender{ImageStores: notifier.imageStores}) + sender = &opsgenie.Sender{ImageStores: notifier.imageStores} case victoropsSender: - err = notifier.RegisterSender(senderSettings, &victorops.Sender{ImageStores: notifier.imageStores}) + sender = &victorops.Sender{ImageStores: notifier.imageStores} case mattermostSender: - err = notifier.RegisterSender(senderSettings, &mattermost.Sender{}) + sender = &mattermost.Sender{} // case "email": - // err = notifier.RegisterSender(senderSettings, &kontur.MailSender{}) + // sender = &kontur.MailSender{} // case "phone": - // err = notifier.RegisterSender(senderSettings, &kontur.SmsSender{}) + // sender = &kontur.SmsSender{} default: return fmt.Errorf("unknown sender type [%s]", senderSettings["type"]) } - if err != nil { + + if err = notifier.RegisterSender(senderSettings, sender); err != nil { return err } + + senders[senderType] = sender } + if notifier.config.SelfStateEnabled { + sender = &selfstate.Sender{Database: connector} selfStateSettings := map[string]interface{}{"type": selfStateSender} - if err = notifier.RegisterSender(selfStateSettings, &selfstate.Sender{Database: connector}); err != nil { + if err = notifier.RegisterSender(selfStateSettings, sender); err != nil { notifier.logger.Warning(). Error(err). Msg("Failed to register selfstate sender") } } + return nil } // RegisterSender adds sender for notification type and registers metrics func (notifier *StandardNotifier) RegisterSender(senderSettings map[string]interface{}, sender moira.Sender) error { - var senderIdent string senderType, ok := senderSettings["type"].(string) if !ok { return fmt.Errorf("failed to retrieve sender type from sender settings") } - switch senderType { - case scriptSender, webhookSender: - name, ok := senderSettings["name"].(string) + err := sender.Init(senderSettings, notifier.logger, notifier.config.Location, notifier.config.DateTimeFormat) + if err != nil { + return fmt.Errorf("failed to initialize sender [%s], err [%s]", senderType, err.Error()) + } + + var senderIdent string + if senderName, ok := senderSettings["name"]; ok { + senderIdent, ok = senderName.(string) if !ok { - return fmt.Errorf("failed to retrieve sender name from sender settings") + return fmt.Errorf("failed to get sender name because it is not a string") } - senderIdent = name - default: + } else { senderIdent = senderType } - err := sender.Init(senderSettings, notifier.logger, notifier.config.Location, notifier.config.DateTimeFormat) - if err != nil { - return fmt.Errorf("failed to initialize sender [%s], err [%s]", senderIdent, err.Error()) + if !notifier.GetSenders()[senderType] { + eventsChannel := make(chan NotificationPackage) + notifier.sendersChannel[senderType] = eventsChannel + notifier.registerMetrics(senderType) + notifier.runSenders(sender, eventsChannel) } - eventsChannel := make(chan NotificationPackage) - notifier.senders[senderIdent] = eventsChannel - notifier.metrics.SendersOkMetrics.RegisterMeter(senderIdent, getGraphiteSenderIdent(senderIdent), "sends_ok") - notifier.metrics.SendersFailedMetrics.RegisterMeter(senderIdent, getGraphiteSenderIdent(senderIdent), "sends_failed") - notifier.metrics.SendersDroppedNotifications.RegisterMeter(senderIdent, getGraphiteSenderIdent(senderIdent), "notifications_dropped") - notifier.runSenders(sender, eventsChannel) + notifier.logger.Info(). String("sender_id", senderIdent). Msg("Sender registered") + return nil } -const maxParallelSendsPerSender = 16 +// GetParallelSendsPerSender returns the maximum number of running goroutines for each sentinel +func (notifier *StandardNotifier) GetMaxParallelSendsPerSender() int { + return notifier.config.MaxParallelSendsPerSender +} func (notifier *StandardNotifier) runSenders(sender moira.Sender, eventsChannel chan NotificationPackage) { - for i := 0; i < maxParallelSendsPerSender; i++ { + for i := 0; i < notifier.GetMaxParallelSendsPerSender(); i++ { notifier.waitGroup.Add(1) go notifier.runSender(sender, eventsChannel) } @@ -140,10 +172,11 @@ func (notifier *StandardNotifier) runSenders(sender moira.Sender, eventsChannel // StopSenders close all sending channels func (notifier *StandardNotifier) StopSenders() { - for _, ch := range notifier.senders { + for _, ch := range notifier.sendersChannel { close(ch) } - notifier.senders = make(map[string]chan NotificationPackage) + + notifier.sendersChannel = make(map[string]chan NotificationPackage) notifier.logger.Info().Msg("Waiting senders finish...") notifier.waitGroup.Wait() notifier.logger.Info().Msg("Moira Notifier Senders stopped") diff --git a/notifier/registrator_test.go b/notifier/registrator_test.go new file mode 100644 index 000000000..ee43878e1 --- /dev/null +++ b/notifier/registrator_test.go @@ -0,0 +1,79 @@ +package notifier + +import ( + "fmt" + "testing" + "time" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestRegisterSender(t *testing.T) { + configureNotifier(t, defaultConfig) + defer afterTest() + + Convey("Test RegisterSender", t, func() { + Convey("With sender without type", func() { + senderSettings := map[string]interface{}{} + + err := standardNotifier.RegisterSender(senderSettings, sender) + So(err, ShouldResemble, fmt.Errorf("failed to retrieve sender type from sender settings")) + }) + + Convey("With sender one type", func() { + senderSettings := map[string]interface{}{ + "type": "test", + } + + sender.EXPECT().Init(senderSettings, logger, location, dateTimeFormat).Return(nil) + + err := standardNotifier.RegisterSender(senderSettings, sender) + So(err, ShouldBeNil) + }) + + Convey("With multiple senders one type", func() { + senderSettings := map[string]interface{}{ + "name": "test_name", + "type": "test", + } + + Convey("With first sender", func() { + sender.EXPECT().Init(senderSettings, logger, location, dateTimeFormat).Return(nil) + + err := standardNotifier.RegisterSender(senderSettings, sender) + So(err, ShouldBeNil) + }) + + senderSettings["name"] = "test_name_2" + + Convey("With second sender", func() { + sender.EXPECT().Init(senderSettings, logger, location, dateTimeFormat).Return(nil) + + err := standardNotifier.RegisterSender(senderSettings, sender) + So(err, ShouldBeNil) + }) + }) + }) +} + +func TestRegisterSendersWithoutType(t *testing.T) { + config := Config{ + SendingTimeout: time.Millisecond * 10, + ResendingTimeout: time.Hour * 24, + Location: location, + DateTimeFormat: dateTimeFormat, + Senders: []map[string]interface{}{ + { + "test": map[string]string{}, + }, + }, + } + + configureNotifier(t, config) + defer afterTest() + + Convey("With sender without type", t, func() { + err := standardNotifier.RegisterSenders(dataBase) + So(err, ShouldResemble, fmt.Errorf("failed to get sender type from settings")) + }) +} diff --git a/pkg/api/api.yml b/pkg/api/api.yml index ba9809935..12d776f2e 100644 --- a/pkg/api/api.yml +++ b/pkg/api/api.yml @@ -32,6 +32,7 @@ web: is_plotting_available: true is_plotting_default_on: true is_subscription_to_all_tags_available: true + is_readonly_enabled: false log: log_file: stdout log_level: info diff --git a/pkg/notifier/notifier.yml b/pkg/notifier/notifier.yml index d23e8038b..44924069b 100644 --- a/pkg/notifier/notifier.yml +++ b/pkg/notifier/notifier.yml @@ -25,6 +25,12 @@ notifier: front_uri: http://localhost timezone: UTC date_time_format: "15:04 02.01.2006" + max_parallel_sends_per_sender: 16 +notification: + delayed_time: 1m + transaction_timeout: 100ms + transaction_max_retries: 10 + transaction_heuristic_limit: 10000 log: log_file: stdout log_level: info diff --git a/senders/discord/init.go b/senders/discord/init.go index 6461c1761..b725d43ce 100644 --- a/senders/discord/init.go +++ b/senders/discord/init.go @@ -19,6 +19,8 @@ const ( // Structure that represents the Discord configuration in the YAML file type config struct { + Name string `mapstructure:"name"` + Type string `mapstructure:"type"` Token string `mapstructure:"token"` FrontURI string `mapstructure:"front_uri"` } @@ -44,6 +46,7 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca if cfg.Token == "" { return fmt.Errorf("cannot read the discord token from the config") } + sender.session, err = discordgo.New("Bot " + cfg.Token) if err != nil { return fmt.Errorf("error creating discord session: %s", err) diff --git a/senders/discord/init_test.go b/senders/discord/init_test.go index a63596285..24067c866 100644 --- a/senders/discord/init_test.go +++ b/senders/discord/init_test.go @@ -11,6 +11,8 @@ import ( . "github.com/smartystreets/goconvey/convey" ) +const discordType = "discord" + type MockDB struct { moira.Database } @@ -38,10 +40,13 @@ func TestInit(t *testing.T) { Convey("Has settings", func() { senderSettings := map[string]interface{}{ + "type": discordType, "token": "123", "front_uri": "http://moira.uri", } - sender.Init(senderSettings, logger, location, "15:04") //nolint + + err := sender.Init(senderSettings, logger, location, "15:04") + So(err, ShouldBeNil) So(sender.frontURI, ShouldResemble, "http://moira.uri") So(sender.session.Token, ShouldResemble, "Bot 123") So(sender.logger, ShouldResemble, logger) diff --git a/senders/mail/mail.go b/senders/mail/mail.go index 478eff39e..649e13ff3 100644 --- a/senders/mail/mail.go +++ b/senders/mail/mail.go @@ -14,6 +14,8 @@ import ( // Structure that represents the Mail configuration in the YAML file type config struct { + Name string `mapstructure:"name"` + Type string `mapstructure:"type"` MailFrom string `mapstructure:"mail_from"` SMTPHello string `mapstructure:"smtp_hello"` SMTPHost string `mapstructure:"smtp_host"` @@ -76,12 +78,15 @@ func (sender *Sender) fillSettings(senderSettings interface{}, logger moira.Logg sender.TemplateFile = cfg.TemplateFile sender.location = location sender.dateTimeFormat = dateTimeFormat + if sender.Username == "" { sender.Username = sender.From } + if sender.From == "" { return fmt.Errorf("mail_from can't be empty") } + return nil } diff --git a/senders/mail/mail_test.go b/senders/mail/mail_test.go index 3c3036130..d9f3b02f7 100644 --- a/senders/mail/mail_test.go +++ b/senders/mail/mail_test.go @@ -7,6 +7,8 @@ import ( . "github.com/smartystreets/goconvey/convey" ) +const mailType = "mail" + func TestFillSettings(t *testing.T) { Convey("Empty map", t, func() { sender := Sender{} @@ -17,12 +19,17 @@ func TestFillSettings(t *testing.T) { Convey("Has From", t, func() { sender := Sender{} - settings := map[string]interface{}{"mail_from": "123"} + settings := map[string]interface{}{ + "type": mailType, + "mail_from": "123", + } + Convey("No username", func() { err := sender.fillSettings(settings, nil, nil, "") So(err, ShouldBeNil) So(sender, ShouldResemble, Sender{From: "123", Username: "123"}) }) + Convey("Has username", func() { settings["smtp_user"] = "user" err := sender.fillSettings(settings, nil, nil, "") diff --git a/senders/mattermost/sender.go b/senders/mattermost/sender.go index 0b514069f..c7563e0f3 100644 --- a/senders/mattermost/sender.go +++ b/senders/mattermost/sender.go @@ -17,6 +17,8 @@ import ( // Structure that represents the Mattermost configuration in the YAML file type config struct { + Name string `mapstructure:"name"` + Type string `mapstructure:"type"` Url string `mapstructure:"url"` InsecureTLS bool `mapstructure:"insecure_tls"` APIToken string `mapstructure:"api_token"` @@ -44,11 +46,17 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca if cfg.Url == "" { return fmt.Errorf("can not read Mattermost url from config") } - client := model.NewAPIv4Client(cfg.Url) - if err != nil { - return fmt.Errorf("can not parse insecure_tls: %v", err) + if cfg.APIToken == "" { + return fmt.Errorf("can not read Mattermost api_token from config") + } + + if cfg.FrontURI == "" { + return fmt.Errorf("can not read Mattermost front_uri from config") } + + client := model.NewAPIv4Client(cfg.Url) + client.HTTPClient = &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ @@ -56,16 +64,9 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca }, }, } - sender.client = client - if cfg.APIToken == "" { - return fmt.Errorf("can not read Mattermost api_token from config") - } + sender.client = client sender.client.SetToken(cfg.APIToken) - - if cfg.FrontURI == "" { - return fmt.Errorf("can not read Mattermost front_uri from config") - } sender.frontURI = cfg.FrontURI sender.location = location sender.logger = logger diff --git a/senders/mattermost/sender_internal_test.go b/senders/mattermost/sender_internal_test.go index ffc0d4299..6afbc6181 100644 --- a/senders/mattermost/sender_internal_test.go +++ b/senders/mattermost/sender_internal_test.go @@ -17,17 +17,21 @@ import ( . "github.com/smartystreets/goconvey/convey" ) +const mattermostType = "mattermost" + func TestSendEvents(t *testing.T) { logger, _ := logging.ConfigureLog("stdout", "debug", "test", true) sender := &Sender{} Convey("Given configured sender", t, func() { senderSettings := map[string]interface{}{ // redundant, but necessary config + "type": mattermostType, "url": "qwerty", "api_token": "qwerty", "front_uri": "qwerty", "insecure_tls": true, } + err := sender.Init(senderSettings, logger, nil, "") So(err, ShouldBeNil) @@ -79,11 +83,13 @@ func TestBuildMessage(t *testing.T) { Convey("Given configured sender", t, func() { senderSettings := map[string]interface{}{ - "url": "qwerty", "api_token": "qwerty", // redundant, but necessary config + "type": mattermostType, + "url": "qwerty", "api_token": "qwerty", // redundant, but necessary config "front_uri": "http://moira.url", "insecure_tls": true, } location, _ := time.LoadLocation("UTC") + err := sender.Init(senderSettings, logger, location, "") So(err, ShouldBeNil) diff --git a/senders/mattermost/sender_test.go b/senders/mattermost/sender_test.go index 75e12c909..fb1f24bd8 100644 --- a/senders/mattermost/sender_test.go +++ b/senders/mattermost/sender_test.go @@ -9,6 +9,8 @@ import ( . "github.com/smartystreets/goconvey/convey" ) +const mattermostType = "mattermost" + func TestInit(t *testing.T) { logger, _ := logging.ConfigureLog("stdout", "debug", "test", true) Convey("Init tests", t, func() { @@ -16,56 +18,84 @@ func TestInit(t *testing.T) { Convey("No url", func() { senderSettings := map[string]interface{}{ + "type": mattermostType, "api_token": "qwerty", "front_uri": "qwerty", "insecure_tls": true, } + err := sender.Init(senderSettings, logger, nil, "") So(err, ShouldNotBeNil) }) Convey("Empty url", func() { senderSettings := map[string]interface{}{ + "type": mattermostType, "url": "", "api_token": "qwerty", "front_uri": "qwerty", "insecure_tls": true, } + err := sender.Init(senderSettings, logger, nil, "") So(err, ShouldNotBeNil) }) Convey("No api_token", func() { - senderSettings := map[string]interface{}{"url": "qwerty", "front_uri": "qwerty"} + senderSettings := map[string]interface{}{ + "type": mattermostType, + "url": "qwerty", + "front_uri": "qwerty", + } + err := sender.Init(senderSettings, logger, nil, "") So(err, ShouldNotBeNil) }) Convey("Empty api_token", func() { - senderSettings := map[string]interface{}{"url": "qwerty", "front_uri": "qwerty", "api_token": ""} + senderSettings := map[string]interface{}{ + "type": mattermostType, + "url": "qwerty", + "front_uri": "qwerty", + "api_token": "", + } + err := sender.Init(senderSettings, logger, nil, "") So(err, ShouldNotBeNil) }) Convey("No front_uri", func() { - senderSettings := map[string]interface{}{"url": "qwerty", "api_token": "qwerty"} + senderSettings := map[string]interface{}{ + "type": mattermostType, + "url": "qwerty", + "api_token": "qwerty", + } + err := sender.Init(senderSettings, logger, nil, "") So(err, ShouldNotBeNil) }) Convey("Empty front_uri", func() { - senderSettings := map[string]interface{}{"url": "qwerty", "api_token": "qwerty", "front_uri": ""} + senderSettings := map[string]interface{}{ + "type": mattermostType, + "url": "qwerty", + "api_token": "qwerty", + "front_uri": "", + } + err := sender.Init(senderSettings, logger, nil, "") So(err, ShouldNotBeNil) }) Convey("Full config", func() { senderSettings := map[string]interface{}{ + "type": mattermostType, "url": "qwerty", "api_token": "qwerty", "front_uri": "qwerty", "insecure_tls": true, } + err := sender.Init(senderSettings, logger, nil, "") So(err, ShouldBeNil) }) diff --git a/senders/msteams/msteams.go b/senders/msteams/msteams.go index bf91c2f1b..12a477979 100644 --- a/senders/msteams/msteams.go +++ b/senders/msteams/msteams.go @@ -37,6 +37,8 @@ var headers = map[string]string{ // Structure that represents the MSTeams configuration in the YAML file type config struct { + Name string `mapstructure:"name"` + Type string `mapstructure:"type"` FrontURI string `mapstructure:"front_uri"` MaxEvents int `mapstructure:"max_events"` } diff --git a/senders/msteams/msteams_test.go b/senders/msteams/msteams_test.go index faf612731..161db3357 100644 --- a/senders/msteams/msteams_test.go +++ b/senders/msteams/msteams_test.go @@ -11,13 +11,17 @@ import ( "gopkg.in/h2non/gock.v1" ) +const msteamsType = "msteams" + func TestInit(t *testing.T) { logger, _ := logging.ConfigureLog("stdout", "debug", "test", true) Convey("Init tests", t, func() { sender := Sender{} senderSettings := map[string]interface{}{ + "type": msteamsType, "max_events": -1, } + Convey("Minimal settings", func() { err := sender.Init(senderSettings, logger, nil, "") So(err, ShouldResemble, nil) @@ -31,9 +35,16 @@ func TestMSTeamsHttpResponse(t *testing.T) { sender := Sender{} logger, _ := logging.ConfigureLog("stdout", "info", "test", true) location, _ := time.LoadLocation("UTC") - _ = sender.Init(map[string]interface{}{ + senderSettings := map[string]interface{}{ + "type": msteamsType, "max_events": -1, - }, logger, location, "") + } + + Convey("Test init", t, func() { + err := sender.Init(senderSettings, logger, location, "") + So(err, ShouldBeNil) + }) + event := moira.NotificationEvent{ TriggerID: "TriggerID", Values: map[string]float64{"t1": 123}, diff --git a/senders/opsgenie/init.go b/senders/opsgenie/init.go index 5b627c6f1..77c3cb1ac 100644 --- a/senders/opsgenie/init.go +++ b/senders/opsgenie/init.go @@ -13,6 +13,8 @@ import ( // Structure that represents the OpsGenie configuration in the YAML file type config struct { + Name string `mapstructure:"name"` + Type string `mapstructure:"type"` APIKey string `mapstructure:"api_key"` FrontURI string `mapstructure:"front_uri"` } @@ -39,11 +41,12 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca return fmt.Errorf("failed to decode senderSettings to opsgenie config: %w", err) } - sender.apiKey = cfg.APIKey - if sender.apiKey == "" { + if cfg.APIKey == "" { return fmt.Errorf("cannot read the api_key from the sender settings") } + sender.apiKey = cfg.APIKey + sender.imageStoreID, sender.imageStore, sender.imageStoreConfigured = senders.ReadImageStoreConfig(senderSettings, sender.ImageStores, logger) diff --git a/senders/opsgenie/init_test.go b/senders/opsgenie/init_test.go index 2733071a1..c1142b424 100644 --- a/senders/opsgenie/init_test.go +++ b/senders/opsgenie/init_test.go @@ -13,6 +13,8 @@ import ( . "github.com/smartystreets/goconvey/convey" ) +const opsgenieType = "opsgenie" + func TestInit(t *testing.T) { logger, _ := logging.ConfigureLog("stdout", "debug", "test", true) location, _ := time.LoadLocation("UTC") @@ -31,17 +33,21 @@ func TestInit(t *testing.T) { So(sender, ShouldResemble, Sender{ ImageStores: map[string]moira.ImageStore{ "s3": imageStore, - }}) + }, + }) }) Convey("Has settings", func() { imageStore.EXPECT().IsEnabled().Return(true) senderSettings := map[string]interface{}{ + "type": opsgenieType, "api_key": "testkey", "front_uri": "http://moira.uri", "image_store": "s3", } - sender.Init(senderSettings, logger, location, "15:04") //nolint + + err := sender.Init(senderSettings, logger, location, "15:04") + So(err, ShouldBeNil) So(sender.apiKey, ShouldResemble, "testkey") So(sender.frontURI, ShouldResemble, "http://moira.uri") So(sender.logger, ShouldResemble, logger) @@ -50,11 +56,14 @@ func TestInit(t *testing.T) { Convey("Wrong image_store name", func() { senderSettings := map[string]interface{}{ + "type": opsgenieType, "front_uri": "http://moira.uri", "api_key": "testkey", "image_store": "s4", } - sender.Init(senderSettings, logger, location, "15:04") //nolint + + err := sender.Init(senderSettings, logger, location, "15:04") + So(err, ShouldBeNil) So(sender.imageStoreConfigured, ShouldResemble, false) So(sender.imageStore, ShouldResemble, nil) }) @@ -62,6 +71,7 @@ func TestInit(t *testing.T) { Convey("image store not configured", func() { imageStore.EXPECT().IsEnabled().Return(false) senderSettings := map[string]interface{}{ + "type": opsgenieType, "api_key": "testkey", "front_uri": "http://moira.uri", "image_store": "s3", @@ -69,7 +79,9 @@ func TestInit(t *testing.T) { sender := Sender{ImageStores: map[string]moira.ImageStore{ "s3": imageStore, }} - sender.Init(senderSettings, logger, location, "15:04") //nolint + + err := sender.Init(senderSettings, logger, location, "15:04") + So(err, ShouldBeNil) So(sender.imageStoreConfigured, ShouldResemble, false) So(sender.imageStore, ShouldResemble, nil) }) diff --git a/senders/pagerduty/init.go b/senders/pagerduty/init.go index 0679511df..0047a19e6 100644 --- a/senders/pagerduty/init.go +++ b/senders/pagerduty/init.go @@ -11,6 +11,8 @@ import ( // Structure that represents the PagerDuty configuration in the YAML file type config struct { + Name string `mapstructure:"name"` + Type string `mapstructure:"type"` FrontURI string `mapstructure:"front_uri"` } diff --git a/senders/pagerduty/init_test.go b/senders/pagerduty/init_test.go index 6d79d1053..6ef2d30fc 100644 --- a/senders/pagerduty/init_test.go +++ b/senders/pagerduty/init_test.go @@ -12,6 +12,8 @@ import ( . "github.com/smartystreets/goconvey/convey" ) +const pagerdutyType = "pagerduty" + func TestInit(t *testing.T) { logger, _ := logging.ConfigureLog("stdout", "debug", "test", true) location, _ := time.LoadLocation("UTC") @@ -27,35 +29,46 @@ func TestInit(t *testing.T) { Convey("Has settings", func() { imageStore.EXPECT().IsEnabled().Return(true) senderSettings := map[string]interface{}{ + "type": pagerdutyType, "front_uri": "http://moira.uri", "image_store": "s3", } - sender.Init(senderSettings, logger, location, "15:04") //nolint + + err := sender.Init(senderSettings, logger, location, "15:04") + So(err, ShouldBeNil) So(sender.frontURI, ShouldResemble, "http://moira.uri") So(sender.logger, ShouldResemble, logger) So(sender.location, ShouldResemble, location) So(sender.imageStoreConfigured, ShouldResemble, true) So(sender.imageStore, ShouldResemble, imageStore) }) + Convey("Wrong image_store name", func() { senderSettings := map[string]interface{}{ + "type": pagerdutyType, "front_uri": "http://moira.uri", "image_store": "s4", } - sender.Init(senderSettings, logger, location, "15:04") //nolint + + err := sender.Init(senderSettings, logger, location, "15:04") + So(err, ShouldBeNil) So(sender.imageStoreConfigured, ShouldResemble, false) So(sender.imageStore, ShouldResemble, nil) }) + Convey("image store not configured", func() { imageStore.EXPECT().IsEnabled().Return(false) senderSettings := map[string]interface{}{ + "type": pagerdutyType, "front_uri": "http://moira.uri", "image_store": "s3", } sender := Sender{ImageStores: map[string]moira.ImageStore{ "s3": imageStore, }} - sender.Init(senderSettings, logger, location, "15:04") //nolint + + err := sender.Init(senderSettings, logger, location, "15:04") + So(err, ShouldBeNil) So(sender.imageStoreConfigured, ShouldResemble, false) So(sender.imageStore, ShouldResemble, nil) }) diff --git a/senders/pushover/pushover.go b/senders/pushover/pushover.go index 8e87c1e15..529d0ea5f 100644 --- a/senders/pushover/pushover.go +++ b/senders/pushover/pushover.go @@ -17,6 +17,8 @@ const urlLimit = 512 // Structure that represents the Pushover configuration in the YAML file type config struct { + Name string `mapstructure:"name"` + Type string `mapstructure:"type"` APIToken string `mapstructure:"api_token"` FrontURI string `mapstructure:"front_uri"` } @@ -39,10 +41,11 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca return fmt.Errorf("failed to decode senderSettings to pushover config: %w", err) } - sender.apiToken = cfg.APIToken - if sender.apiToken == "" { + if cfg.APIToken == "" { return fmt.Errorf("can not read pushover api_token from config") } + + sender.apiToken = cfg.APIToken sender.client = pushover_client.New(sender.apiToken) sender.logger = logger sender.frontURI = cfg.FrontURI diff --git a/senders/pushover/pushover_test.go b/senders/pushover/pushover_test.go index 96ea0b894..88554092e 100644 --- a/senders/pushover/pushover_test.go +++ b/senders/pushover/pushover_test.go @@ -12,26 +12,37 @@ import ( . "github.com/smartystreets/goconvey/convey" ) +const pushoverType = "pushover" + func TestSender_Init(t *testing.T) { logger, _ := logging.ConfigureLog("stdout", "debug", "test", true) Convey("Empty map", t, func() { sender := Sender{} + err := sender.Init(map[string]interface{}{}, logger, nil, "") So(err, ShouldResemble, fmt.Errorf("can not read pushover api_token from config")) So(sender, ShouldResemble, Sender{}) }) + senderSettings := map[string]interface{}{ + "type": pushoverType, + } + Convey("Settings has api_token", t, func() { + senderSettings["api_token"] = "123" sender := Sender{} - err := sender.Init(map[string]interface{}{"api_token": "123"}, logger, nil, "") + + err := sender.Init(senderSettings, logger, nil, "") So(err, ShouldBeNil) So(sender, ShouldResemble, Sender{apiToken: "123", client: pushover_client.New("123"), logger: logger}) }) Convey("Settings has all data", t, func() { + senderSettings["front_uri"] = "321" sender := Sender{} location, _ := time.LoadLocation("UTC") - err := sender.Init(map[string]interface{}{"api_token": "123", "front_uri": "321"}, logger, location, "") + + err := sender.Init(senderSettings, logger, location, "") So(err, ShouldBeNil) So(sender, ShouldResemble, Sender{apiToken: "123", client: pushover_client.New("123"), frontURI: "321", logger: logger, location: location}) }) diff --git a/senders/script/script.go b/senders/script/script.go index 6d55e09ad..a3a30ba25 100644 --- a/senders/script/script.go +++ b/senders/script/script.go @@ -15,12 +15,19 @@ import ( // Structure that represents the Script configuration in the YAML file type config struct { + Type string `mapstructure:"type"` Name string `mapstructure:"name"` Exec string `mapstructure:"exec"` } // Sender implements moira sender interface via script execution type Sender struct { + scriptSenders map[string]scriptSender + logger moira.Logger +} + +// scriptSender saves data for the script's sender +type scriptSender struct { exec string logger moira.Logger } @@ -44,22 +51,41 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca if cfg.Name == "" { return fmt.Errorf("required name for sender type script") } + _, _, err = parseExec(cfg.Exec) if err != nil { return err } - sender.exec = cfg.Exec + + script := scriptSender{ + exec: cfg.Exec, + logger: logger, + } + + if sender.scriptSenders == nil { + sender.scriptSenders = make(map[string]scriptSender) + } + + sender.scriptSenders[cfg.Name] = script sender.logger = logger + return nil } // SendEvents implements Sender interface Send func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) error { - scriptFile, args, scriptBody, err := sender.buildCommandData(events, contact, trigger, throttled) + script, ok := sender.scriptSenders[contact.Type] + if !ok { + return fmt.Errorf("failed to send events because there is not %s sender", contact.Type) + } + + scriptFile, args, scriptBody, err := script.buildCommandData(events, contact, trigger, throttled) if err != nil { return err } + command := exec.Command(scriptFile, args...) + var scriptOutput bytes.Buffer command.Stdin = bytes.NewReader(scriptBody) command.Stdout = &scriptOutput @@ -73,33 +99,38 @@ func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira. Msg("Finished executing script") if err != nil { - return fmt.Errorf("failed exec [%s] Error [%s] Output: [%s]", sender.exec, err.Error(), scriptOutput.String()) + return fmt.Errorf("failed exec [%s] Error [%w] Output: [%s]", script.exec, err, scriptOutput.String()) } + return nil } -func (sender *Sender) buildCommandData(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, throttled bool) (scriptFile string, args []string, scriptBody []byte, err error) { +func (s *scriptSender) buildCommandData(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, throttled bool) (scriptFile string, args []string, scriptBody []byte, err error) { // TODO: Remove moira.VariableTriggerName from buildExecString in 2.6 - if strings.Contains(sender.exec, moira.VariableTriggerName) { - sender.logger.Warning(). + if strings.Contains(s.exec, moira.VariableTriggerName) { + s.logger.Warning(). String("variable_name", moira.VariableTriggerName). Msg("Variable is deprecated and will be removed in 2.6 release") } - execString := buildExecString(sender.exec, trigger, contact) + + execString := buildExecString(s.exec, trigger, contact) scriptFile, args, err = parseExec(execString) if err != nil { return scriptFile, args[1:], []byte{}, err } + scriptMessage := &scriptNotification{ Events: events, Trigger: trigger, Contact: contact, Throttled: throttled, } + scriptJSON, err := json.MarshalIndent(scriptMessage, "", "\t") if err != nil { return scriptFile, args[1:], scriptJSON, fmt.Errorf("failed marshal json: %s", err.Error()) } + return scriptFile, args[1:], scriptJSON, nil } @@ -110,9 +141,11 @@ func parseExec(execString string) (scriptFile string, args []string, err error) if err != nil { return scriptFile, args, fmt.Errorf("file %s not found", scriptFile) } + if !infoFile.Mode().IsRegular() { return scriptFile, args, fmt.Errorf("%s not file", scriptFile) } + return scriptFile, args, nil } @@ -124,8 +157,10 @@ func buildExecString(template string, trigger moira.TriggerData, contact moira.C moira.VariableTriggerID: trigger.ID, moira.VariableTriggerName: trigger.Name, } + for k, v := range templateVariables { template = strings.Replace(template, k, v, -1) } + return template } diff --git a/senders/script/script_test.go b/senders/script/script_test.go index 05564e0a2..d6c1321b9 100644 --- a/senders/script/script_test.go +++ b/senders/script/script_test.go @@ -9,7 +9,11 @@ import ( . "github.com/smartystreets/goconvey/convey" ) -const testDir = "/tmp" +const ( + testDir = "/tmp" + scriptType = "script" + scriptName = "script_name" +) var ( testTrigger = moira.TriggerData{ID: "triggerID"} @@ -55,7 +59,9 @@ func TestInit(t *testing.T) { So(sender, ShouldResemble, Sender{}) }) - settings["name"] = "script_name" + settings["type"] = scriptType + settings["name"] = scriptName + Convey("Empty exec", func() { err := sender.Init(settings, logger, nil, "") So(err, ShouldResemble, fmt.Errorf("file not found")) @@ -64,6 +70,7 @@ func TestInit(t *testing.T) { Convey("Exec with not exists file", func() { settings["exec"] = "./test_file1" + err := sender.Init(settings, logger, nil, "") So(err, ShouldResemble, fmt.Errorf("file ./test_file1 not found")) So(sender, ShouldResemble, Sender{}) @@ -71,9 +78,47 @@ func TestInit(t *testing.T) { Convey("Exec with exists file", func() { settings["exec"] = "script.go" + err := sender.Init(settings, logger, nil, "") So(err, ShouldBeNil) - So(sender, ShouldResemble, Sender{exec: "script.go", logger: logger}) + So(sender, ShouldResemble, Sender{ + scriptSenders: map[string]scriptSender{ + scriptName: { + exec: "script.go", + logger: logger, + }, + }, + logger: logger, + }) + }) + + Convey("Test with multiple scripts", func() { + settings["exec"] = "script.go" + settings2 := map[string]interface{}{ + "type": scriptType, + "name": "script_name_2", + "exec": "script.go", + } + + err := sender.Init(settings, logger, nil, "") + So(err, ShouldBeNil) + + script1 := sender.scriptSenders[scriptName] + So(script1, ShouldResemble, scriptSender{ + exec: "script.go", + logger: logger, + }) + + err = sender.Init(settings2, logger, nil, "") + So(err, ShouldBeNil) + + script2 := sender.scriptSenders["script_name_2"] + So(script2, ShouldResemble, scriptSender{ + exec: "script.go", + logger: logger, + }) + + So(len(sender.scriptSenders), ShouldEqual, 2) }) }) } @@ -81,7 +126,11 @@ func TestInit(t *testing.T) { func TestBuildCommandData(t *testing.T) { logger, _ := logging.ConfigureLog("stdout", "debug", "test", true) Convey("Test send events", t, func() { - sender := Sender{exec: "script.go first second", logger: logger} + sender := scriptSender{ + exec: "script.go first second", + logger: logger, + } + scriptFile, args, scriptBody, err := sender.buildCommandData( []moira.NotificationEvent{{Metric: "New metric"}}, moira.ContactData{ID: "ContactID"}, @@ -95,7 +144,11 @@ func TestBuildCommandData(t *testing.T) { }) Convey("Test file not found", t, func() { - sender := Sender{exec: "script1.go first second", logger: logger} + sender := scriptSender{ + exec: "script1.go first second", + logger: logger, + } + scriptFile, args, scriptBody, err := sender.buildCommandData([]moira.NotificationEvent{{Metric: "New metric"}}, moira.ContactData{ID: "ContactID"}, moira.TriggerData{ID: "TriggerID"}, true) So(scriptFile, ShouldResemble, "script1.go") So(args, ShouldResemble, []string{"first", "second"}) diff --git a/senders/slack/slack.go b/senders/slack/slack.go index bdb43f899..86f5f1905 100644 --- a/senders/slack/slack.go +++ b/senders/slack/slack.go @@ -42,6 +42,8 @@ var stateEmoji = map[moira.State]string{ // Structure that represents the Slack configuration in the YAML file type config struct { + Name string `mapstructure:"name"` + Type string `mapstructure:"type"` APIToken string `mapstructure:"api_token"` UseEmoji bool `mapstructure:"use_emoji"` FrontURI string `mapstructure:"front_uri"` @@ -67,6 +69,7 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca if cfg.APIToken == "" { return fmt.Errorf("can not read slack api_token from config") } + sender.useEmoji = cfg.UseEmoji sender.logger = logger sender.frontURI = cfg.FrontURI diff --git a/senders/slack/slack_test.go b/senders/slack/slack_test.go index c368d178e..997bf9359 100644 --- a/senders/slack/slack_test.go +++ b/senders/slack/slack_test.go @@ -12,6 +12,8 @@ import ( . "github.com/smartystreets/goconvey/convey" ) +const slackType = "slack" + func TestInit(t *testing.T) { logger, _ := logging.ConfigureLog("stdout", "debug", "test", true) Convey("Init tests", t, func() { @@ -24,6 +26,7 @@ func TestInit(t *testing.T) { }) Convey("has api_token", func() { + senderSettings["type"] = slackType senderSettings["api_token"] = "123" client := slack_client.New("123") @@ -35,6 +38,7 @@ func TestInit(t *testing.T) { Convey("use_emoji set to false", func() { senderSettings["use_emoji"] = false + err := sender.Init(senderSettings, logger, nil, "") So(err, ShouldBeNil) So(sender, ShouldResemble, Sender{logger: logger, client: client}) @@ -42,6 +46,7 @@ func TestInit(t *testing.T) { Convey("use_emoji set to true", func() { senderSettings["use_emoji"] = true + err := sender.Init(senderSettings, logger, nil, "") So(err, ShouldBeNil) So(sender, ShouldResemble, Sender{logger: logger, useEmoji: true, client: client}) @@ -49,6 +54,7 @@ func TestInit(t *testing.T) { Convey("use_emoji set to something wrong", func() { senderSettings["use_emoji"] = 123 + err := sender.Init(senderSettings, logger, nil, "") So(err, ShouldNotBeNil) }) diff --git a/senders/telegram/init.go b/senders/telegram/init.go index e477f3e4d..397e8da44 100644 --- a/senders/telegram/init.go +++ b/senders/telegram/init.go @@ -33,6 +33,8 @@ var ( // Structure that represents the Telegram configuration in the YAML file type config struct { + Name string `mapstructure:"name"` + Type string `mapstructure:"type"` APIToken string `mapstructure:"api_token"` FrontURI string `mapstructure:"front_uri"` } @@ -69,6 +71,7 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca if cfg.APIToken == "" { return fmt.Errorf("can not read telegram api_token from config") } + sender.apiToken = cfg.APIToken sender.frontURI = cfg.FrontURI sender.logger = logger @@ -88,7 +91,9 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca Msg("Error handling incoming message: %s") } }) + go sender.runTelebot() + return nil } diff --git a/senders/telegram/init_test.go b/senders/telegram/init_test.go index 3542fc13f..b0d80132b 100644 --- a/senders/telegram/init_test.go +++ b/senders/telegram/init_test.go @@ -9,6 +9,8 @@ import ( . "github.com/smartystreets/goconvey/convey" ) +const telegramType = "telegram" + func TestInit(t *testing.T) { logger, _ := logging.ConfigureLog("stdout", "debug", "test", true) location, _ := time.LoadLocation("UTC") @@ -22,10 +24,12 @@ func TestInit(t *testing.T) { Convey("Has settings", func() { senderSettings := map[string]interface{}{ + "type": telegramType, "api_token": "123", "front_uri": "http://moira.uri", } - sender.Init(senderSettings, logger, location, "15:04") //nolint + + _ = sender.Init(senderSettings, logger, location, "15:04") So(sender.apiToken, ShouldResemble, "123") So(sender.frontURI, ShouldResemble, "http://moira.uri") So(sender.logger, ShouldResemble, logger) diff --git a/senders/twilio/twilio.go b/senders/twilio/twilio.go index ef51f0970..6c875a2ed 100644 --- a/senders/twilio/twilio.go +++ b/senders/twilio/twilio.go @@ -12,6 +12,7 @@ import ( // Structure that represents the Twilio configuration in the YAML file type config struct { Type string `mapstructure:"type"` + Name string `mapstructure:"name"` APIAsid string `mapstructure:"api_asid"` APIAuthToken string `mapstructure:"api_authtoken"` APIFromPhone string `mapstructure:"api_fromphone"` @@ -43,6 +44,7 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca if err != nil { return fmt.Errorf("failed to decode senderSettings to twilio config: %w", err) } + apiType := cfg.Type if cfg.APIAsid == "" { @@ -65,6 +67,7 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca logger: logger, location: location, } + switch apiType { case "twilio sms": sender.sender = &twilioSenderSms{tSender} diff --git a/senders/twilio/twilio_test.go b/senders/twilio/twilio_test.go index 98fb01689..9b5ec018c 100644 --- a/senders/twilio/twilio_test.go +++ b/senders/twilio/twilio_test.go @@ -10,15 +10,20 @@ import ( . "github.com/smartystreets/goconvey/convey" ) +const twilioType = "twilio" + func TestInit(t *testing.T) { Convey("Tests init twilio sender", t, func() { sender := Sender{} logger, _ := logging.ConfigureLog("stdout", "debug", "test", true) location, _ := time.LoadLocation("UTC") - settings := map[string]interface{}{} + settings := map[string]interface{}{ + "type": twilioType, + } + Convey("no api asid", func() { err := sender.Init(settings, logger, nil, "15:04") - So(err, ShouldResemble, fmt.Errorf("can not read [%s] api_sid param from config", "")) + So(err, ShouldResemble, fmt.Errorf("can not read [%s] api_sid param from config", twilioType)) So(sender, ShouldResemble, Sender{}) }) @@ -26,7 +31,7 @@ func TestInit(t *testing.T) { Convey("no api authtoken", func() { err := sender.Init(settings, logger, nil, "15:04") - So(err, ShouldResemble, fmt.Errorf("can not read [%s] api_authtoken param from config", "")) + So(err, ShouldResemble, fmt.Errorf("can not read [%s] api_authtoken param from config", twilioType)) So(sender, ShouldResemble, Sender{}) }) @@ -34,7 +39,7 @@ func TestInit(t *testing.T) { Convey("no api fromphone", func() { err := sender.Init(settings, logger, nil, "15:04") - So(err, ShouldResemble, fmt.Errorf("can not read [%s] api_fromphone param from config", "")) + So(err, ShouldResemble, fmt.Errorf("can not read [%s] api_fromphone param from config", twilioType)) So(sender, ShouldResemble, Sender{}) }) @@ -42,12 +47,13 @@ func TestInit(t *testing.T) { Convey("no api type", func() { err := sender.Init(settings, logger, nil, "15:04") - So(err, ShouldResemble, fmt.Errorf("wrong twilio type: %s", "")) + So(err, ShouldResemble, fmt.Errorf("wrong twilio type: %s", twilioType)) So(sender, ShouldResemble, Sender{}) }) Convey("config sms", func() { settings["type"] = "twilio sms" + err := sender.Init(settings, logger, location, "15:04") So(err, ShouldBeNil) So(sender, ShouldResemble, Sender{sender: &twilioSenderSms{ @@ -62,6 +68,7 @@ func TestInit(t *testing.T) { Convey("config voice", func() { settings["type"] = "twilio voice" + Convey("no voice url", func() { err := sender.Init(settings, logger, location, "15:04") So(err, ShouldResemble, fmt.Errorf("can not read [%s] voiceurl param from config", "twilio voice")) @@ -72,6 +79,7 @@ func TestInit(t *testing.T) { settings["voiceurl"] = "url here" Convey("append_message == true", func() { settings["append_message"] = true + err := sender.Init(settings, logger, location, "15:04") So(err, ShouldBeNil) So(sender, ShouldResemble, Sender{sender: &twilioSenderVoice{ @@ -88,6 +96,7 @@ func TestInit(t *testing.T) { Convey("append_message is false", func() { settings["append_message"] = false + err := sender.Init(settings, logger, location, "15:04") So(err, ShouldBeNil) So(sender, ShouldResemble, Sender{sender: &twilioSenderVoice{ diff --git a/senders/victorops/init.go b/senders/victorops/init.go index f9462f91c..8c02ffdda 100644 --- a/senders/victorops/init.go +++ b/senders/victorops/init.go @@ -12,6 +12,8 @@ import ( // Structure that represents the VictorOps configuration in the YAML file type config struct { + Name string `mapstructure:"name"` + Type string `mapstructure:"type"` RoutingURL string `mapstructure:"routing_url"` ImageStore string `mapstructure:"image_store"` FrontURI string `mapstructure:"front_uri"` @@ -61,7 +63,6 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca } sender.client = api.NewClient(sender.routingURL, nil) - sender.frontURI = cfg.FrontURI sender.logger = logger sender.location = location diff --git a/senders/victorops/init_test.go b/senders/victorops/init_test.go index 7bff013ec..8f945312a 100644 --- a/senders/victorops/init_test.go +++ b/senders/victorops/init_test.go @@ -14,6 +14,8 @@ import ( . "github.com/smartystreets/goconvey/convey" ) +const victoropsType = "victorops" + func TestInit(t *testing.T) { logger, _ := logging.ConfigureLog("stdout", "debug", "test", true) location, _ := time.LoadLocation("UTC") @@ -25,42 +27,53 @@ func TestInit(t *testing.T) { sender := Sender{ImageStores: map[string]moira.ImageStore{ "s3": imageStore, }} + Convey("Empty map", func() { err := sender.Init(map[string]interface{}{}, logger, nil, "") So(err, ShouldResemble, fmt.Errorf("cannot read the routing url from the yaml config")) So(sender, ShouldResemble, Sender{ ImageStores: map[string]moira.ImageStore{ "s3": imageStore, - }}) + }, + }) }) Convey("Has settings", func() { imageStore.EXPECT().IsEnabled().Return(true) senderSettings := map[string]interface{}{ + "type": victoropsType, "routing_url": "https://testurl.com", "front_uri": "http://moira.uri", "image_store": "s3", } - sender.Init(senderSettings, logger, location, "15:04") //nolint + + err := sender.Init(senderSettings, logger, location, "15:04") + So(err, ShouldBeNil) So(sender.routingURL, ShouldResemble, "https://testurl.com") So(sender.frontURI, ShouldResemble, "http://moira.uri") So(sender.logger, ShouldResemble, logger) So(sender.location, ShouldResemble, location) So(sender.client, ShouldResemble, api.NewClient("https://testurl.com", nil)) }) + Convey("Wrong image_store name", func() { senderSettings := map[string]interface{}{ + "type": victoropsType, "front_uri": "http://moira.uri", "routing_url": "https://testurl.com", "image_store": "s4", } - sender.Init(senderSettings, logger, location, "15:04") //nolint + + err := sender.Init(senderSettings, logger, location, "15:04") + So(err, ShouldBeNil) So(sender.imageStoreConfigured, ShouldResemble, false) So(sender.imageStore, ShouldResemble, nil) }) + Convey("image store not configured", func() { imageStore.EXPECT().IsEnabled().Return(false) senderSettings := map[string]interface{}{ + "type": victoropsType, "front_uri": "http://moira.uri", "routing_url": "https://testurl.com", "image_store": "s3", @@ -68,7 +81,9 @@ func TestInit(t *testing.T) { sender := Sender{ImageStores: map[string]moira.ImageStore{ "s3": imageStore, }} - sender.Init(senderSettings, logger, location, "15:04") //nolint + + err := sender.Init(senderSettings, logger, location, "15:04") + So(err, ShouldBeNil) So(sender.imageStoreConfigured, ShouldResemble, false) So(sender.imageStore, ShouldResemble, nil) }) diff --git a/senders/webhook/request.go b/senders/webhook/request.go index a7f8d73ca..7fd941d1a 100644 --- a/senders/webhook/request.go +++ b/senders/webhook/request.go @@ -10,28 +10,33 @@ import ( "github.com/moira-alert/moira" ) -func (sender *Sender) buildRequest(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) (*http.Request, error) { - if sender.url == moira.VariableContactValue { - sender.log.Warning(). - String("potentially_dangerous_url", sender.url). +func (client *webhookClient) buildRequest(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) (*http.Request, error) { + if client.url == moira.VariableContactValue { + client.logger.Warning(). + String("potentially_dangerous_url", client.url). Msg("Found potentially dangerous url template, api contact validation is advised") } - requestURL := buildRequestURL(sender.url, trigger, contact) + + requestURL := buildRequestURL(client.url, trigger, contact) requestBody, err := buildRequestBody(events, contact, trigger, plots, throttled) if err != nil { return nil, err } + request, err := http.NewRequest("POST", requestURL, bytes.NewBuffer(requestBody)) if err != nil { return request, err } - if sender.user != "" && sender.password != "" { - request.SetBasicAuth(sender.user, sender.password) + + if client.user != "" && client.password != "" { + request.SetBasicAuth(client.user, client.password) } - for k, v := range sender.headers { + + for k, v := range client.headers { request.Header.Set(k, v) } - sender.log.Debug(). + + client.logger.Debug(). String("method", request.Method). String("url", request.URL.String()). String("body", bytes.NewBuffer(requestBody).String()). @@ -50,6 +55,7 @@ func buildRequestBody(events moira.NotificationEvents, contact moira.ContactData encodedFirstPlot = encodedPlot } } + requestPayload := payload{ Trigger: toTriggerData(trigger), Events: toEventsData(events), @@ -64,6 +70,7 @@ func buildRequestBody(events moira.NotificationEvents, contact moira.ContactData Plots: encodedPlots, Throttled: throttled, } + return json.Marshal(requestPayload) } @@ -74,6 +81,7 @@ func buildRequestURL(template string, trigger moira.TriggerData, contact moira.C moira.VariableContactType: contact.Type, moira.VariableTriggerID: trigger.ID, } + for k, v := range templateVariables { value := url.PathEscape(v) if k == moira.VariableContactValue && @@ -82,5 +90,6 @@ func buildRequestURL(template string, trigger moira.TriggerData, contact moira.C } template = strings.Replace(template, k, value, -1) } + return template } diff --git a/senders/webhook/request_test.go b/senders/webhook/request_test.go index 5dbe46341..49eeb36d6 100644 --- a/senders/webhook/request_test.go +++ b/senders/webhook/request_test.go @@ -17,7 +17,7 @@ var ( testTemplate = fmt.Sprintf("%s/%s/%s/%s/%s", testHost, moira.VariableTriggerID, moira.VariableContactType, moira.VariableContactID, moira.VariableContactValue) testContact = moira.ContactData{ ID: "contactID", - Type: "contactType", + Type: webhookName, Value: "contactValue", User: "contactUser", Team: "contactTeam", @@ -94,7 +94,7 @@ const expectedStateChangePayload = ` } ], "contact": { - "type": "contactType", + "type": "webhook_name", "value": "contactValue", "id": "contactID", "user": "contactUser", @@ -215,7 +215,7 @@ func TestBuildRequestURL(t *testing.T) { var testContactWithURL = moira.ContactData{ ID: "contactID", - Type: "contactType", + Type: webhookName, Value: "https://test.org/moirahook", User: "contactUser", Team: "contactTeam", diff --git a/senders/webhook/webhook.go b/senders/webhook/webhook.go index 7bbfe44f0..2f5cb9164 100644 --- a/senders/webhook/webhook.go +++ b/senders/webhook/webhook.go @@ -13,6 +13,7 @@ import ( // Structure that represents the Webhook configuration in the YAML file type config struct { Name string `mapstructure:"name"` + Type string `mapstructure:"type"` URL string `mapstructure:"url"` User string `mapstructure:"user"` Password string `mapstructure:"password"` @@ -21,12 +22,18 @@ type config struct { // Sender implements moira sender interface via webhook type Sender struct { + webhookClients map[string]*webhookClient + logger moira.Logger +} + +// webhookClient stores data for the webhook client +type webhookClient struct { + client *http.Client url string user string password string headers map[string]string - client *http.Client - log moira.Logger + logger moira.Logger } // Init read yaml config @@ -41,18 +48,10 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca return fmt.Errorf("required name for sender type webhook") } - sender.url = cfg.URL - if sender.url == "" { + if cfg.URL == "" { return fmt.Errorf("can not read url from config") } - sender.user, sender.password = cfg.User, cfg.Password - - sender.headers = map[string]string{ - "User-Agent": "Moira", - "Content-Type": "application/json", - } - var timeout int if cfg.Timeout != 0 { timeout = cfg.Timeout @@ -60,17 +59,39 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca timeout = 30 } - sender.log = logger - sender.client = &http.Client{ - Timeout: time.Duration(timeout) * time.Second, - Transport: &http.Transport{DisableKeepAlives: true}, + client := &webhookClient{ + url: cfg.URL, + user: cfg.User, + password: cfg.Password, + headers: map[string]string{ + "User-Agent": "Moira", + "Content-Type": "application/json", + }, + client: &http.Client{ + Timeout: time.Duration(timeout) * time.Second, + Transport: &http.Transport{DisableKeepAlives: true}, + }, + logger: logger, } + + if sender.webhookClients == nil { + sender.webhookClients = make(map[string]*webhookClient) + } + + sender.webhookClients[cfg.Name] = client + sender.logger = logger + return nil } // SendEvents implements Sender interface Send func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) error { - request, err := sender.buildRequest(events, contact, trigger, plots, throttled) + webhookClient, ok := sender.webhookClients[contact.Type] + if !ok { + return fmt.Errorf("failed to send events because there is not %s client", contact.Type) + } + + request, err := webhookClient.buildRequest(events, contact, trigger, plots, throttled) if request != nil { defer request.Body.Close() } @@ -79,7 +100,7 @@ func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira. return fmt.Errorf("failed to build request: %s", err.Error()) } - response, err := sender.client.Do(request) + response, err := webhookClient.client.Do(request) if response != nil { defer response.Body.Close() } @@ -96,6 +117,7 @@ func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira. } else { serverResponse = string(responseBody) } + return fmt.Errorf("invalid status code: %d, server response: %s", response.StatusCode, serverResponse) } diff --git a/senders/webhook/webhook_test.go b/senders/webhook/webhook_test.go index 6908632bf..b02607981 100644 --- a/senders/webhook/webhook_test.go +++ b/senders/webhook/webhook_test.go @@ -21,10 +21,107 @@ import ( const ( testUser = "testUser" testPass = "testPass" + + webhookType = "webhook" + webhookName = "webhook_name" ) var logger, _ = logging.GetLogger("webhook") +func TestSender_Init(t *testing.T) { + Convey("Test Init", t, func() { + Convey("Init without name", func() { + senderSettings := map[string]interface{}{ + "type": webhookType, + } + sender := Sender{} + + err := sender.Init(senderSettings, logger, time.UTC, "") + So(err, ShouldResemble, fmt.Errorf("required name for sender type webhook")) + }) + + Convey("Test without url", func() { + senderSettings := map[string]interface{}{ + "type": webhookType, + "name": webhookName, + } + sender := Sender{} + + err := sender.Init(senderSettings, logger, time.UTC, "") + So(err, ShouldResemble, fmt.Errorf("can not read url from config")) + }) + + Convey("Init with full config", func() { + senderSettings := map[string]interface{}{ + "type": webhookType, + "name": webhookName, + "user": "user", + "password": "password", + "url": "url", + } + sender := Sender{} + + err := sender.Init(senderSettings, logger, time.UTC, "") + So(err, ShouldBeNil) + }) + + Convey("Multiple Init", func() { + senderSettings1 := map[string]interface{}{ + "type": webhookType, + "name": webhookName, + "url": "url", + } + + webhookName2 := "webhook_name_2" + senderSettings2 := map[string]interface{}{ + "type": webhookType, + "name": webhookName2, + "url": "url", + } + + sender := Sender{} + + err := sender.Init(senderSettings1, logger, time.UTC, "") + So(err, ShouldBeNil) + + client := sender.webhookClients[webhookName] + So(client, ShouldNotBeNil) + So(client, ShouldResemble, &webhookClient{ + url: "url", + headers: map[string]string{ + "User-Agent": "Moira", + "Content-Type": "application/json", + }, + client: &http.Client{ + Timeout: time.Duration(30) * time.Second, + Transport: &http.Transport{DisableKeepAlives: true}, + }, + logger: logger, + }) + + err = sender.Init(senderSettings2, logger, time.UTC, "") + So(err, ShouldBeNil) + + client = sender.webhookClients[webhookName2] + So(client, ShouldNotBeNil) + So(client, ShouldResemble, &webhookClient{ + url: "url", + headers: map[string]string{ + "User-Agent": "Moira", + "Content-Type": "application/json", + }, + client: &http.Client{ + Timeout: time.Duration(30) * time.Second, + Transport: &http.Transport{DisableKeepAlives: true}, + }, + logger: logger, + }) + + So(len(sender.webhookClients), ShouldEqual, 2) + }) + }) +} + func TestSender_SendEvents(t *testing.T) { Convey("Receive test webhook", t, func() { ts := httptest.NewServer( @@ -52,12 +149,19 @@ func TestSender_SendEvents(t *testing.T) { defer ts.Close() senderSettings := map[string]interface{}{ - "name": "testWebhook", + "name": webhookName, + "type": webhookType, "url": fmt.Sprintf("%s/%s", ts.URL, moira.VariableTriggerID), "user": testUser, "password": testPass, } - sender := Sender{} + + sender := Sender{ + webhookClients: map[string]*webhookClient{ + webhookName: {}, + }, + } + err := sender.Init(senderSettings, logger, time.UTC, "") So(err, ShouldBeNil) @@ -72,6 +176,7 @@ func testRequestURL(r *http.Request) (int, error) { if actualPath != expectedPath { return http.StatusBadRequest, fmt.Errorf("invalid url path: %s\nexpected: %s", actualPath, expectedPath) } + return http.StatusCreated, nil } @@ -80,17 +185,20 @@ func testRequestHeaders(r *http.Request) (int, error) { "User-Agent": "Moira", "Content-Type": "application/json", } + for headerName, headerValue := range expectedHeaders { actualHeaderValue := r.Header.Get(headerName) if actualHeaderValue != headerValue { return http.StatusBadRequest, fmt.Errorf("invalid header value: %s\nexpected: %s", actualHeaderValue, headerValue) } } + authHeader := strings.SplitN(r.Header.Get("Authorization"), " ", 2) authPayload, err := base64.StdEncoding.DecodeString(authHeader[1]) if err != nil { return http.StatusInternalServerError, err } + authPair := strings.SplitN(string(authPayload), ":", 2) actualUser, actualPass := authPair[0], authPair[1] if actualUser != testUser || actualPass != testPass { @@ -98,6 +206,7 @@ func testRequestHeaders(r *http.Request) (int, error) { expectedCred := fmt.Sprintf("user: %s, pass: %s", testUser, testPass) return http.StatusBadRequest, fmt.Errorf("invalid credentials: %s\nexpected: %s", actualCred, expectedCred) } + return http.StatusCreated, nil } @@ -107,14 +216,17 @@ func testRequestBody(r *http.Request) (int, error) { if err != nil { return http.StatusInternalServerError, err } + actualJSON, err := getLastLine(requestBodyBuff.String()) if err != nil { return http.StatusInternalServerError, err } + actualJSON, expectedJSON := prepareStrings(actualJSON, expectedStateChangePayload) if actualJSON != expectedJSON { return http.StatusBadRequest, fmt.Errorf("invalid json body: %s\nexpected: %s", actualJSON, expectedJSON) } + return http.StatusCreated, nil } @@ -125,8 +237,10 @@ func getLastLine(longString string) (string, error) { for s.Scan() { lastLine = s.Text() } + if err := s.Err(); err != nil { return "", err } + return lastLine, nil }