- 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
261 lines
6.6 KiB
Go
261 lines
6.6 KiB
Go
// 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"
|
||
)
|
||
|
||
// Severity represents the severity level of a notification
|
||
type Severity string
|
||
|
||
const (
|
||
SeverityInfo Severity = "info"
|
||
SeverityWarning Severity = "warning"
|
||
SeverityError Severity = "error"
|
||
SeverityCritical Severity = "critical"
|
||
)
|
||
|
||
// 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 := "ℹ️"
|
||
switch event.Severity {
|
||
case SeverityWarning:
|
||
icon = "⚠️"
|
||
case SeverityError, SeverityCritical:
|
||
icon = "❌"
|
||
}
|
||
|
||
verb := "Event"
|
||
switch event.Type {
|
||
case EventBackupStarted:
|
||
verb = "Backup Started"
|
||
icon = "🔄"
|
||
case EventBackupCompleted:
|
||
verb = "Backup Completed"
|
||
icon = "✅"
|
||
case EventBackupFailed:
|
||
verb = "Backup Failed"
|
||
icon = "❌"
|
||
case EventRestoreStarted:
|
||
verb = "Restore Started"
|
||
icon = "🔄"
|
||
case EventRestoreCompleted:
|
||
verb = "Restore Completed"
|
||
icon = "✅"
|
||
case EventRestoreFailed:
|
||
verb = "Restore Failed"
|
||
icon = "❌"
|
||
case EventCleanupCompleted:
|
||
verb = "Cleanup Completed"
|
||
icon = "🗑️"
|
||
case EventVerifyCompleted:
|
||
verb = "Verification Passed"
|
||
icon = "✅"
|
||
case EventVerifyFailed:
|
||
verb = "Verification Failed"
|
||
icon = "❌"
|
||
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])
|
||
}
|