- Add --dry-run/-n flag for backup commands with comprehensive preflight checks - Database connectivity validation - Required tools availability check - Storage target and permissions verification - Backup size estimation - Encryption and cloud storage configuration validation - Implement GFS (Grandfather-Father-Son) retention policies - Daily/Weekly/Monthly/Yearly tier classification - Configurable retention counts per tier - Custom weekly day and monthly day settings - ISO week handling for proper week boundaries - Add notification system with SMTP and webhook support - SMTP email notifications with TLS/STARTTLS - Webhook HTTP notifications with HMAC-SHA256 signing - Slack-compatible webhook payload format - Event types: backup/restore started/completed/failed, cleanup, verify, PITR - Configurable severity levels and retry logic - Update README.md with documentation for all new features
257 lines
6.9 KiB
Go
257 lines
6.9 KiB
Go
// Package notify - Notification manager for fan-out to multiple backends
|
|
package notify
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
)
|
|
|
|
// Manager manages multiple notification backends
|
|
type Manager struct {
|
|
config Config
|
|
notifiers []Notifier
|
|
mu sync.RWMutex
|
|
hostname string
|
|
}
|
|
|
|
// NewManager creates a new notification manager with configured backends
|
|
func NewManager(config Config) *Manager {
|
|
hostname, _ := os.Hostname()
|
|
|
|
m := &Manager{
|
|
config: config,
|
|
notifiers: make([]Notifier, 0),
|
|
hostname: hostname,
|
|
}
|
|
|
|
// Initialize enabled backends
|
|
if config.SMTPEnabled {
|
|
m.notifiers = append(m.notifiers, NewSMTPNotifier(config))
|
|
}
|
|
|
|
if config.WebhookEnabled {
|
|
m.notifiers = append(m.notifiers, NewWebhookNotifier(config))
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// AddNotifier adds a custom notifier to the manager
|
|
func (m *Manager) AddNotifier(n Notifier) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.notifiers = append(m.notifiers, n)
|
|
}
|
|
|
|
// Notify sends an event to all enabled notification backends
|
|
// This is a non-blocking operation that runs in a goroutine
|
|
func (m *Manager) Notify(event *Event) {
|
|
go m.NotifySync(context.Background(), event)
|
|
}
|
|
|
|
// NotifySync sends an event synchronously to all enabled backends
|
|
func (m *Manager) NotifySync(ctx context.Context, event *Event) error {
|
|
// Add hostname if not set
|
|
if event.Hostname == "" && m.hostname != "" {
|
|
event.Hostname = m.hostname
|
|
}
|
|
|
|
// Check if we should send based on event type/severity
|
|
if !m.shouldSend(event) {
|
|
return nil
|
|
}
|
|
|
|
m.mu.RLock()
|
|
notifiers := make([]Notifier, len(m.notifiers))
|
|
copy(notifiers, m.notifiers)
|
|
m.mu.RUnlock()
|
|
|
|
var errors []error
|
|
var wg sync.WaitGroup
|
|
|
|
for _, n := range notifiers {
|
|
if !n.IsEnabled() {
|
|
continue
|
|
}
|
|
|
|
wg.Add(1)
|
|
go func(notifier Notifier) {
|
|
defer wg.Done()
|
|
if err := notifier.Send(ctx, event); err != nil {
|
|
errors = append(errors, fmt.Errorf("%s: %w", notifier.Name(), err))
|
|
}
|
|
}(n)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
if len(errors) > 0 {
|
|
return fmt.Errorf("notification errors: %v", errors)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// shouldSend determines if an event should be sent based on configuration
|
|
func (m *Manager) shouldSend(event *Event) bool {
|
|
// Check minimum severity
|
|
if !m.meetsSeverity(event.Severity) {
|
|
return false
|
|
}
|
|
|
|
// Check event type filters
|
|
switch event.Type {
|
|
case EventBackupCompleted, EventRestoreCompleted, EventCleanupCompleted, EventVerifyCompleted:
|
|
return m.config.OnSuccess
|
|
case EventBackupFailed, EventRestoreFailed, EventVerifyFailed:
|
|
return m.config.OnFailure
|
|
case EventBackupStarted, EventRestoreStarted:
|
|
return m.config.OnSuccess
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
// meetsSeverity checks if event severity meets minimum threshold
|
|
func (m *Manager) meetsSeverity(severity Severity) bool {
|
|
severityOrder := map[Severity]int{
|
|
SeverityInfo: 0,
|
|
SeverityWarning: 1,
|
|
SeverityError: 2,
|
|
SeverityCritical: 3,
|
|
}
|
|
|
|
eventLevel, ok := severityOrder[severity]
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
minLevel, ok := severityOrder[m.config.MinSeverity]
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
return eventLevel >= minLevel
|
|
}
|
|
|
|
// HasEnabledNotifiers returns true if at least one notifier is enabled
|
|
func (m *Manager) HasEnabledNotifiers() bool {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
for _, n := range m.notifiers {
|
|
if n.IsEnabled() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// EnabledNotifiers returns the names of all enabled notifiers
|
|
func (m *Manager) EnabledNotifiers() []string {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
names := make([]string, 0)
|
|
for _, n := range m.notifiers {
|
|
if n.IsEnabled() {
|
|
names = append(names, n.Name())
|
|
}
|
|
}
|
|
return names
|
|
}
|
|
|
|
// BackupStarted sends a backup started notification
|
|
func (m *Manager) BackupStarted(database string) {
|
|
event := NewEvent(EventBackupStarted, SeverityInfo, fmt.Sprintf("Starting backup of database '%s'", database)).
|
|
WithDatabase(database)
|
|
m.Notify(event)
|
|
}
|
|
|
|
// BackupCompleted sends a backup completed notification
|
|
func (m *Manager) BackupCompleted(database, backupFile string, size int64, duration interface{}) {
|
|
event := NewEvent(EventBackupCompleted, SeverityInfo, fmt.Sprintf("Backup of database '%s' completed successfully", database)).
|
|
WithDatabase(database).
|
|
WithBackupInfo(backupFile, size)
|
|
|
|
if d, ok := duration.(interface{ Seconds() float64 }); ok {
|
|
event.WithDetail("duration_seconds", fmt.Sprintf("%.2f", d.Seconds()))
|
|
}
|
|
|
|
m.Notify(event)
|
|
}
|
|
|
|
// BackupFailed sends a backup failed notification
|
|
func (m *Manager) BackupFailed(database string, err error) {
|
|
event := NewEvent(EventBackupFailed, SeverityError, fmt.Sprintf("Backup of database '%s' failed", database)).
|
|
WithDatabase(database).
|
|
WithError(err)
|
|
m.Notify(event)
|
|
}
|
|
|
|
// RestoreStarted sends a restore started notification
|
|
func (m *Manager) RestoreStarted(database, backupFile string) {
|
|
event := NewEvent(EventRestoreStarted, SeverityInfo, fmt.Sprintf("Starting restore of database '%s' from '%s'", database, backupFile)).
|
|
WithDatabase(database).
|
|
WithBackupInfo(backupFile, 0)
|
|
m.Notify(event)
|
|
}
|
|
|
|
// RestoreCompleted sends a restore completed notification
|
|
func (m *Manager) RestoreCompleted(database, backupFile string, duration interface{}) {
|
|
event := NewEvent(EventRestoreCompleted, SeverityInfo, fmt.Sprintf("Restore of database '%s' completed successfully", database)).
|
|
WithDatabase(database).
|
|
WithBackupInfo(backupFile, 0)
|
|
|
|
if d, ok := duration.(interface{ Seconds() float64 }); ok {
|
|
event.WithDetail("duration_seconds", fmt.Sprintf("%.2f", d.Seconds()))
|
|
}
|
|
|
|
m.Notify(event)
|
|
}
|
|
|
|
// RestoreFailed sends a restore failed notification
|
|
func (m *Manager) RestoreFailed(database string, err error) {
|
|
event := NewEvent(EventRestoreFailed, SeverityError, fmt.Sprintf("Restore of database '%s' failed", database)).
|
|
WithDatabase(database).
|
|
WithError(err)
|
|
m.Notify(event)
|
|
}
|
|
|
|
// CleanupCompleted sends a cleanup completed notification
|
|
func (m *Manager) CleanupCompleted(directory string, deleted int, spaceFreed int64) {
|
|
event := NewEvent(EventCleanupCompleted, SeverityInfo, fmt.Sprintf("Cleanup completed: %d backups deleted", deleted)).
|
|
WithDetail("directory", directory).
|
|
WithDetail("space_freed", formatBytes(spaceFreed))
|
|
m.Notify(event)
|
|
}
|
|
|
|
// VerifyCompleted sends a verification completed notification
|
|
func (m *Manager) VerifyCompleted(backupFile string, isValid bool) {
|
|
if isValid {
|
|
event := NewEvent(EventVerifyCompleted, SeverityInfo, "Backup verification passed").
|
|
WithBackupInfo(backupFile, 0)
|
|
m.Notify(event)
|
|
} else {
|
|
event := NewEvent(EventVerifyFailed, SeverityError, "Backup verification failed").
|
|
WithBackupInfo(backupFile, 0)
|
|
m.Notify(event)
|
|
}
|
|
}
|
|
|
|
// PITRRecovery sends a PITR recovery notification
|
|
func (m *Manager) PITRRecovery(database, targetTime string) {
|
|
event := NewEvent(EventPITRRecovery, SeverityInfo, fmt.Sprintf("Point-in-time recovery initiated for '%s' to %s", database, targetTime)).
|
|
WithDatabase(database).
|
|
WithDetail("target_time", targetTime)
|
|
m.Notify(event)
|
|
}
|
|
|
|
// NullManager returns a no-op notification manager
|
|
func NullManager() *Manager {
|
|
return &Manager{
|
|
notifiers: make([]Notifier, 0),
|
|
}
|
|
}
|