Files
dbbackup/internal/notify/notify.go
Alexander Renz f69bfe7071 feat: Add enterprise DBA features for production reliability
New features implemented:

1. Backup Catalog (internal/catalog/)
   - SQLite-based backup tracking
   - Gap detection and RPO monitoring
   - Search and statistics
   - Filesystem sync

2. DR Drill Testing (internal/drill/)
   - Automated restore testing in Docker containers
   - Database validation with custom queries
   - Catalog integration for drill-tested status

3. Smart Notifications (internal/notify/)
   - Event batching with configurable intervals
   - Time-based escalation policies
   - HTML/text/Slack templates

4. Compliance Reports (internal/report/)
   - SOC2, GDPR, HIPAA, PCI-DSS, ISO27001 frameworks
   - Evidence collection from catalog
   - JSON, Markdown, HTML output formats

5. RTO/RPO Calculator (internal/rto/)
   - Recovery objective analysis
   - RTO breakdown by phase
   - Recommendations for improvement

6. Replica-Aware Backup (internal/replica/)
   - Topology detection for PostgreSQL/MySQL
   - Automatic replica selection
   - Configurable selection strategies

7. Parallel Table Backup (internal/parallel/)
   - Concurrent table dumps
   - Worker pool with progress tracking
   - Large table optimization

8. MySQL/MariaDB PITR (internal/pitr/)
   - Binary log parsing and replay
   - Point-in-time recovery support
   - Transaction filtering

CLI commands added: catalog, drill, report, rto

All changes support the goal: reliable 3 AM database recovery.
2025-12-13 20:28:55 +01:00

286 lines
7.3 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 := ""
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])
}