Enable notifications on multiple levels

This commit is contained in:
Frederik Ring 2021-12-17 17:44:22 +01:00
parent 08d78a0bd6
commit 8bad0656b3
2 changed files with 69 additions and 44 deletions

View File

@ -236,9 +236,9 @@ You can populate below template according to your requirements and use it as you
# BACKUP_STOP_CONTAINER_LABEL="service1" # BACKUP_STOP_CONTAINER_LABEL="service1"
########### NOTIFICATIONS ON FAILED BACKUP RUNS ########### NOTIFICATIONS
# In case a backup fails, notifications (email, Slack, etc.) can be sent out. # Notifications (email, Slack, etc.) can be sent out when a backup run finishes.
# Configuration is provided as a comma-separated list of URLs as consumed # Configuration is provided as a comma-separated list of URLs as consumed
# by `shoutrrr`: https://containrrr.dev/shoutrrr/v0.5/services/overview/ # by `shoutrrr`: https://containrrr.dev/shoutrrr/v0.5/services/overview/
# When providing multiple URLs or an URL that contains a comma, the values # When providing multiple URLs or an URL that contains a comma, the values
@ -249,17 +249,22 @@ You can populate below template according to your requirements and use it as you
# NOTIFICATION_URLS=smtp://username:password@host:587/?fromAddress=sender@example.com&toAddresses=recipient@example.com # NOTIFICATION_URLS=smtp://username:password@host:587/?fromAddress=sender@example.com&toAddresses=recipient@example.com
########### EMAIL NOTIFICATIONS ON FAILED BACKUP RUNS # By default, notifications will only be sent out when a backup run fails.
# To receive notifications for every run, set `NOTIFICATION_LEVEL` to `always`
# NOTIFICATION_LEVEL="always"
########### EMAIL NOTIFICATIONS
# ************************************************************************ # ************************************************************************
# Providing notification configuration like this has been deprecated # Providing notification configuration like this has been deprecated
# and will be removed in the next major version. Please use NOTIFICATION_ULRS # and will be removed in the next major version. Please use NOTIFICATION_URLS
# as documented above instead. # as documented above instead.
# ************************************************************************ # ************************************************************************
# In case SMTP credentials are provided, notification emails can be sent out on # In case SMTP credentials are provided, notification emails can be sent out when
# failed backup runs. These emails will contain the start time, the error # a backup run finished. These emails will contain the start time, the error
# message and all log output prior to the failure. # message on failure and all prior log output.
# The recipient(s) of the notification. Supply a comma separated list # The recipient(s) of the notification. Supply a comma separated list
# of adresses if you want to notify multiple recipients. If this is # of adresses if you want to notify multiple recipients. If this is

View File

@ -44,7 +44,7 @@ func main() {
defer func() { defer func() {
if pArg := recover(); pArg != nil { if pArg := recover(); pArg != nil {
if err, ok := pArg.(error); ok { if err, ok := pArg.(error); ok {
if hookErr := s.runHooks(err, hookLevelCleanup, hookLevelFailure); hookErr != nil { if hookErr := s.runHooks(err, hookLevelCleanup, hookLevelFailure, hookLevelAlways); hookErr != nil {
s.logger.Errorf("An error occurred calling the registered hooks: %s", hookErr) s.logger.Errorf("An error occurred calling the registered hooks: %s", hookErr)
} }
os.Exit(1) os.Exit(1)
@ -52,7 +52,7 @@ func main() {
panic(pArg) panic(pArg)
} }
if err := s.runHooks(nil, hookLevelCleanup); err != nil { if err := s.runHooks(nil, hookLevelCleanup, hookLevelAlways); err != nil {
s.logger.Errorf( s.logger.Errorf(
"Backup procedure ran successfully, but an error ocurred calling the registered hooks: %v", "Backup procedure ran successfully, but an error ocurred calling the registered hooks: %v",
err, err,
@ -115,6 +115,7 @@ type config struct {
AwsIamRoleEndpoint string `split_words:"true"` AwsIamRoleEndpoint string `split_words:"true"`
GpgPassphrase string `split_words:"true"` GpgPassphrase string `split_words:"true"`
NotificationURLs []string `envconfig:"NOTIFICATION_URLS"` NotificationURLs []string `envconfig:"NOTIFICATION_URLS"`
NotificationLevel string `split_words:"true" default:"failure"`
EmailNotificationRecipient string `split_words:"true"` EmailNotificationRecipient string `split_words:"true"`
EmailNotificationSender string `split_words:"true" default:"noreply@nohost"` EmailNotificationSender string `split_words:"true" default:"noreply@nohost"`
EmailSMTPHost string `envconfig:"EMAIL_SMTP_HOST"` EmailSMTPHost string `envconfig:"EMAIL_SMTP_HOST"`
@ -208,38 +209,19 @@ func newScript() (*script, error) {
s.c.EmailNotificationRecipient, s.c.EmailNotificationRecipient,
) )
s.c.NotificationURLs = append(s.c.NotificationURLs, emailURL) s.c.NotificationURLs = append(s.c.NotificationURLs, emailURL)
s.logger.Warn("Using EMAIL_* keys for providing notification configuration has been deprecated and will be removed in the next major version.") s.logger.Warn(
s.logger.Warn("Please use NOTIFICATION_URLS instead. Refer to the README for an upgrade guide.") "Using EMAIL_* keys for providing notification configuration has been deprecated and will be removed in the next major version.",
)
s.logger.Warn(
"Please use NOTIFICATION_URLS instead. Refer to the README for an upgrade guide.",
)
} }
if len(s.c.NotificationURLs) > 0 { notificationLevel := hookLevelFailure
s.hooks = append(s.hooks, hook{hookLevelFailure, func(err error, start time.Time, logOutput string) error { if s.c.NotificationLevel == "always" {
sender, senderErr := shoutrrr.CreateSender(s.c.NotificationURLs...) notificationLevel = hookLevelAlways
if senderErr != nil {
return fmt.Errorf("notifications: error creating sender: %w", senderErr)
}
body := fmt.Sprintf(
"Running docker-volume-backup failed with error: %s\n\nLog output of the failed run was:\n\n%s\n", err, logOutput,
)
results := sender.Send(body, &sTypes.Params{
"title": fmt.Sprintf(
"Failure running docker-volume-backup at %s", start.Format(time.RFC3339),
),
})
var errs []error
for _, result := range results {
if result != nil {
errs = append(errs, result)
}
}
if len(errs) != 0 {
return fmt.Errorf("notifications: error sending message: %w", join(errs...))
}
return nil
}})
} }
s.registerHook(notificationLevel, s.sendNotification)
return s, nil return s, nil
} }
@ -247,10 +229,47 @@ func newScript() (*script, error) {
var noop = func() error { return nil } var noop = func() error { return nil }
// registerHook adds the given action at the given level. // registerHook adds the given action at the given level.
func (s *script) registerHook(level hookLevel, action func(err error, start time.Time, logOutput string) error) { func (s *script) registerHook(level hookLevel, action func(err error) error) {
s.hooks = append(s.hooks, hook{level, action}) s.hooks = append(s.hooks, hook{level, action})
} }
// sendNotification sends a notification to third party services, containing
// information about the result of a backup run
func (s *script) sendNotification(err error) error {
if len(s.c.NotificationURLs) == 0 {
return nil
}
sender, senderErr := shoutrrr.CreateSender(s.c.NotificationURLs...)
if senderErr != nil {
return fmt.Errorf("notifications: error creating sender: %w", senderErr)
}
var body, title string
if err != nil {
body = fmt.Sprintf(
"Running docker-volume-backup failed with error: %s\n\nLog output of the failed run was:\n\n%s\n", err, s.output.String(),
)
title = fmt.Sprintf("Failure running docker-volume-backup at %s", s.start.Format(time.RFC3339))
} else {
body = fmt.Sprintf(
"Running docker-volume-backup succeeded.\n\nLog output was:\n\n%s\n", s.output.String(),
)
title = fmt.Sprintf("Success running docker-volume-backup at %s", s.start.Format(time.RFC3339))
}
var errs []error
for _, result := range sender.Send(body, &sTypes.Params{"title": title}) {
if result != nil {
errs = append(errs, result)
}
}
if len(errs) != 0 {
return fmt.Errorf("notifications: error sending message: %w", join(errs...))
}
return nil
}
// stopContainers stops all Docker containers that are marked as to being // stopContainers stops all Docker containers that are marked as to being
// stopped during the backup and returns a function that can be called to // stopped during the backup and returns a function that can be called to
// restart everything that has been stopped. // restart everything that has been stopped.
@ -373,7 +392,7 @@ func (s *script) takeBackup() error {
if s.c.BackupFromSnapshot { if s.c.BackupFromSnapshot {
backupSources = filepath.Join("/tmp", s.c.BackupSources) backupSources = filepath.Join("/tmp", s.c.BackupSources)
// copy before compressing guard against a situation where backup folder's content are still growing. // copy before compressing guard against a situation where backup folder's content are still growing.
s.registerHook(hookLevelCleanup, func(error, time.Time, string) error { s.registerHook(hookLevelCleanup, func(error) error {
if err := remove(backupSources); err != nil { if err := remove(backupSources); err != nil {
return fmt.Errorf("takeBackup: error removing snapshot: %w", err) return fmt.Errorf("takeBackup: error removing snapshot: %w", err)
} }
@ -390,7 +409,7 @@ func (s *script) takeBackup() error {
} }
tarFile := s.file tarFile := s.file
s.registerHook(hookLevelCleanup, func(error, time.Time, string) error { s.registerHook(hookLevelCleanup, func(error) error {
if err := remove(tarFile); err != nil { if err := remove(tarFile); err != nil {
return fmt.Errorf("takeBackup: error removing tar file: %w", err) return fmt.Errorf("takeBackup: error removing tar file: %w", err)
} }
@ -414,7 +433,7 @@ func (s *script) encryptBackup() error {
} }
gpgFile := fmt.Sprintf("%s.gpg", s.file) gpgFile := fmt.Sprintf("%s.gpg", s.file)
s.registerHook(hookLevelCleanup, func(error, time.Time, string) error { s.registerHook(hookLevelCleanup, func(error) error {
if err := remove(gpgFile); err != nil { if err := remove(gpgFile); err != nil {
return fmt.Errorf("encryptBackup: error removing gpg file: %w", err) return fmt.Errorf("encryptBackup: error removing gpg file: %w", err)
} }
@ -648,7 +667,7 @@ func (s *script) runHooks(err error, levels ...hookLevel) error {
if hook.level != level { if hook.level != level {
continue continue
} }
if actionErr := hook.action(err, s.start, s.output.String()); actionErr != nil { if actionErr := hook.action(err); actionErr != nil {
actionErrors = append(actionErrors, fmt.Errorf("runHooks: error running hook: %w", actionErr)) actionErrors = append(actionErrors, fmt.Errorf("runHooks: error running hook: %w", actionErr))
} }
} }
@ -762,12 +781,13 @@ func (b *bufferingWriter) Write(p []byte) (n int, err error) {
// reaches a certain point (e.g. unsuccessful backup) // reaches a certain point (e.g. unsuccessful backup)
type hook struct { type hook struct {
level hookLevel level hookLevel
action func(err error, start time.Time, logOutput string) error action func(err error) error
} }
type hookLevel int type hookLevel int
const ( const (
hookLevelFailure hookLevel = iota hookLevelFailure hookLevel = iota
hookLevelAlways
hookLevelCleanup hookLevelCleanup
) )