// Package notify provides notification capabilities for backup events package notify import ( "context" "fmt" "time" ) // EventType represents the type of notification event type EventType string const ( EventBackupStarted EventType = "backup_started" EventBackupCompleted EventType = "backup_completed" EventBackupFailed EventType = "backup_failed" EventRestoreStarted EventType = "restore_started" EventRestoreCompleted EventType = "restore_completed" EventRestoreFailed EventType = "restore_failed" EventCleanupCompleted EventType = "cleanup_completed" EventVerifyCompleted EventType = "verify_completed" EventVerifyFailed EventType = "verify_failed" EventPITRRecovery EventType = "pitr_recovery" EventVerificationPassed EventType = "verification_passed" EventVerificationFailed EventType = "verification_failed" EventDRDrillPassed EventType = "dr_drill_passed" EventDRDrillFailed EventType = "dr_drill_failed" EventGapDetected EventType = "gap_detected" EventRPOViolation EventType = "rpo_violation" ) // Severity represents the severity level of a notification type Severity string const ( SeverityInfo Severity = "info" SeveritySuccess Severity = "success" SeverityWarning Severity = "warning" SeverityError Severity = "error" SeverityCritical Severity = "critical" ) // severityOrder returns numeric order for severity comparison func severityOrder(s Severity) int { switch s { case SeverityInfo: return 0 case SeveritySuccess: return 1 case SeverityWarning: return 2 case SeverityError: return 3 case SeverityCritical: return 4 default: return 0 } } // Event represents a notification event type Event struct { Type EventType `json:"type"` Severity Severity `json:"severity"` Timestamp time.Time `json:"timestamp"` Database string `json:"database,omitempty"` Message string `json:"message"` Details map[string]string `json:"details,omitempty"` Error string `json:"error,omitempty"` Duration time.Duration `json:"duration,omitempty"` BackupFile string `json:"backup_file,omitempty"` BackupSize int64 `json:"backup_size,omitempty"` Hostname string `json:"hostname,omitempty"` } // NewEvent creates a new notification event func NewEvent(eventType EventType, severity Severity, message string) *Event { return &Event{ Type: eventType, Severity: severity, Timestamp: time.Now(), Message: message, Details: make(map[string]string), } } // WithDatabase adds database name to the event func (e *Event) WithDatabase(db string) *Event { e.Database = db return e } // WithError adds error information to the event func (e *Event) WithError(err error) *Event { if err != nil { e.Error = err.Error() } return e } // WithDuration adds duration to the event func (e *Event) WithDuration(d time.Duration) *Event { e.Duration = d return e } // WithBackupInfo adds backup file and size information func (e *Event) WithBackupInfo(file string, size int64) *Event { e.BackupFile = file e.BackupSize = size return e } // WithHostname adds hostname to the event func (e *Event) WithHostname(hostname string) *Event { e.Hostname = hostname return e } // WithDetail adds a custom detail to the event func (e *Event) WithDetail(key, value string) *Event { if e.Details == nil { e.Details = make(map[string]string) } e.Details[key] = value return e } // Notifier is the interface that all notification backends must implement type Notifier interface { // Name returns the name of the notifier (e.g., "smtp", "webhook") Name() string // Send sends a notification event Send(ctx context.Context, event *Event) error // IsEnabled returns whether the notifier is configured and enabled IsEnabled() bool } // Config holds configuration for all notification backends type Config struct { // SMTP configuration SMTPEnabled bool SMTPHost string SMTPPort int SMTPUser string SMTPPassword string SMTPFrom string SMTPTo []string SMTPTLS bool SMTPStartTLS bool // Webhook configuration WebhookEnabled bool WebhookURL string WebhookMethod string // GET, POST WebhookHeaders map[string]string WebhookSecret string // For signing payloads // General settings OnSuccess bool // Send notifications on successful operations OnFailure bool // Send notifications on failed operations OnWarning bool // Send notifications on warnings MinSeverity Severity Retries int // Number of retry attempts RetryDelay time.Duration // Delay between retries } // DefaultConfig returns a configuration with sensible defaults func DefaultConfig() Config { return Config{ SMTPPort: 587, SMTPTLS: false, SMTPStartTLS: true, WebhookMethod: "POST", OnSuccess: true, OnFailure: true, OnWarning: true, MinSeverity: SeverityInfo, Retries: 3, RetryDelay: 5 * time.Second, } } // FormatEventSubject generates a subject line for notifications func FormatEventSubject(event *Event) string { icon := "[INFO]" switch event.Severity { case SeverityWarning: icon = "[WARN]" case SeverityError, SeverityCritical: icon = "[FAIL]" } verb := "Event" switch event.Type { case EventBackupStarted: verb = "Backup Started" icon = "[EXEC]" case EventBackupCompleted: verb = "Backup Completed" icon = "[OK]" case EventBackupFailed: verb = "Backup Failed" icon = "[FAIL]" case EventRestoreStarted: verb = "Restore Started" icon = "[EXEC]" case EventRestoreCompleted: verb = "Restore Completed" icon = "[OK]" case EventRestoreFailed: verb = "Restore Failed" icon = "[FAIL]" case EventCleanupCompleted: verb = "Cleanup Completed" icon = "[DEL]" case EventVerifyCompleted: verb = "Verification Passed" icon = "[OK]" case EventVerifyFailed: verb = "Verification Failed" icon = "[FAIL]" case EventPITRRecovery: verb = "PITR Recovery" icon = "⏪" } if event.Database != "" { return fmt.Sprintf("%s [dbbackup] %s: %s", icon, verb, event.Database) } return fmt.Sprintf("%s [dbbackup] %s", icon, verb) } // FormatEventBody generates a message body for notifications func FormatEventBody(event *Event) string { body := fmt.Sprintf("%s\n\n", event.Message) body += fmt.Sprintf("Time: %s\n", event.Timestamp.Format(time.RFC3339)) if event.Database != "" { body += fmt.Sprintf("Database: %s\n", event.Database) } if event.Hostname != "" { body += fmt.Sprintf("Host: %s\n", event.Hostname) } if event.Duration > 0 { body += fmt.Sprintf("Duration: %s\n", event.Duration.Round(time.Second)) } if event.BackupFile != "" { body += fmt.Sprintf("Backup File: %s\n", event.BackupFile) } if event.BackupSize > 0 { body += fmt.Sprintf("Backup Size: %s\n", formatBytes(event.BackupSize)) } if event.Error != "" { body += fmt.Sprintf("\nError: %s\n", event.Error) } if len(event.Details) > 0 { body += "\nDetails:\n" for k, v := range event.Details { body += fmt.Sprintf(" %s: %s\n", k, v) } } return body } // formatBytes formats bytes as human-readable string func formatBytes(bytes int64) string { const unit = 1024 if bytes < unit { return fmt.Sprintf("%d B", bytes) } div, exp := int64(unit), 0 for n := bytes / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) }