- Replace all emoji characters with ASCII equivalents throughout codebase - Replace Unicode box-drawing characters (═║╔╗╚╝━─) with ASCII (+|-=) - Replace checkmarks (✓✗) with [OK]/[FAIL] markers - 59 files updated, 741 lines changed - Improves terminal compatibility and reduces visual noise
286 lines
7.3 KiB
Go
286 lines
7.3 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"
|
|
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])
|
|
}
|