// Package notify - Notification templates package notify import ( "bytes" "fmt" "html/template" "strings" "time" ) // TemplateType represents the notification format type type TemplateType string const ( TemplateText TemplateType = "text" TemplateHTML TemplateType = "html" TemplateMarkdown TemplateType = "markdown" TemplateSlack TemplateType = "slack" ) // Templates holds notification templates type Templates struct { Subject string TextBody string HTMLBody string } // DefaultTemplates returns default notification templates func DefaultTemplates() map[EventType]Templates { return map[EventType]Templates{ EventBackupStarted: { Subject: "[EXEC] Backup Started: {{.Database}} on {{.Hostname}}", TextBody: backupStartedText, HTMLBody: backupStartedHTML, }, EventBackupCompleted: { Subject: "[OK] Backup Completed: {{.Database}} on {{.Hostname}}", TextBody: backupCompletedText, HTMLBody: backupCompletedHTML, }, EventBackupFailed: { Subject: "[FAIL] Backup FAILED: {{.Database}} on {{.Hostname}}", TextBody: backupFailedText, HTMLBody: backupFailedHTML, }, EventRestoreStarted: { Subject: "[EXEC] Restore Started: {{.Database}} on {{.Hostname}}", TextBody: restoreStartedText, HTMLBody: restoreStartedHTML, }, EventRestoreCompleted: { Subject: "[OK] Restore Completed: {{.Database}} on {{.Hostname}}", TextBody: restoreCompletedText, HTMLBody: restoreCompletedHTML, }, EventRestoreFailed: { Subject: "[FAIL] Restore FAILED: {{.Database}} on {{.Hostname}}", TextBody: restoreFailedText, HTMLBody: restoreFailedHTML, }, EventVerificationPassed: { Subject: "[OK] Verification Passed: {{.Database}}", TextBody: verificationPassedText, HTMLBody: verificationPassedHTML, }, EventVerificationFailed: { Subject: "[FAIL] Verification FAILED: {{.Database}}", TextBody: verificationFailedText, HTMLBody: verificationFailedHTML, }, EventDRDrillPassed: { Subject: "[OK] DR Drill Passed: {{.Database}}", TextBody: drDrillPassedText, HTMLBody: drDrillPassedHTML, }, EventDRDrillFailed: { Subject: "[FAIL] DR Drill FAILED: {{.Database}}", TextBody: drDrillFailedText, HTMLBody: drDrillFailedHTML, }, } } // Template strings const backupStartedText = ` Backup Operation Started Database: {{.Database}} Hostname: {{.Hostname}} Started At: {{formatTime .Timestamp}} {{if .Message}}{{.Message}}{{end}} ` const backupStartedHTML = `

[EXEC] Backup Started

Database:{{.Database}}
Hostname:{{.Hostname}}
Started At:{{formatTime .Timestamp}}
{{if .Message}}

{{.Message}}

{{end}}
` const backupCompletedText = ` Backup Operation Completed Successfully Database: {{.Database}} Hostname: {{.Hostname}} Completed: {{formatTime .Timestamp}} {{with .Details}} {{if .size}}Size: {{.size}}{{end}} {{if .duration}}Duration: {{.duration}}{{end}} {{if .path}}Path: {{.path}}{{end}} {{end}} {{if .Message}}{{.Message}}{{end}} ` const backupCompletedHTML = `

[OK] Backup Completed

{{with .Details}} {{if .size}}{{end}} {{if .duration}}{{end}} {{if .path}}{{end}} {{end}}
Database:{{.Database}}
Hostname:{{.Hostname}}
Completed:{{formatTime .Timestamp}}
Size:{{.size}}
Duration:{{.duration}}
Path:{{.path}}
{{if .Message}}

{{.Message}}

{{end}}
` const backupFailedText = ` [WARN] BACKUP FAILED [WARN] Database: {{.Database}} Hostname: {{.Hostname}} Failed At: {{formatTime .Timestamp}} {{if .Error}} Error: {{.Error}} {{end}} {{if .Message}}{{.Message}}{{end}} Please investigate immediately. ` const backupFailedHTML = `

[FAIL] Backup FAILED

{{if .Error}}{{end}}
Database:{{.Database}}
Hostname:{{.Hostname}}
Failed At:{{formatTime .Timestamp}}
Error:{{.Error}}
{{if .Message}}

{{.Message}}

{{end}}

Please investigate immediately.

` const restoreStartedText = ` Restore Operation Started Database: {{.Database}} Hostname: {{.Hostname}} Started At: {{formatTime .Timestamp}} {{if .Message}}{{.Message}}{{end}} ` const restoreStartedHTML = `

[EXEC] Restore Started

Database:{{.Database}}
Hostname:{{.Hostname}}
Started At:{{formatTime .Timestamp}}
{{if .Message}}

{{.Message}}

{{end}}
` const restoreCompletedText = ` Restore Operation Completed Successfully Database: {{.Database}} Hostname: {{.Hostname}} Completed: {{formatTime .Timestamp}} {{with .Details}} {{if .duration}}Duration: {{.duration}}{{end}} {{end}} {{if .Message}}{{.Message}}{{end}} ` const restoreCompletedHTML = `

[OK] Restore Completed

{{with .Details}} {{if .duration}}{{end}} {{end}}
Database:{{.Database}}
Hostname:{{.Hostname}}
Completed:{{formatTime .Timestamp}}
Duration:{{.duration}}
{{if .Message}}

{{.Message}}

{{end}}
` const restoreFailedText = ` [WARN] RESTORE FAILED [WARN] Database: {{.Database}} Hostname: {{.Hostname}} Failed At: {{formatTime .Timestamp}} {{if .Error}} Error: {{.Error}} {{end}} {{if .Message}}{{.Message}}{{end}} Please investigate immediately. ` const restoreFailedHTML = `

[FAIL] Restore FAILED

{{if .Error}}{{end}}
Database:{{.Database}}
Hostname:{{.Hostname}}
Failed At:{{formatTime .Timestamp}}
Error:{{.Error}}
{{if .Message}}

{{.Message}}

{{end}}

Please investigate immediately.

` const verificationPassedText = ` Backup Verification Passed Database: {{.Database}} Hostname: {{.Hostname}} Verified: {{formatTime .Timestamp}} {{with .Details}} {{if .checksum}}Checksum: {{.checksum}}{{end}} {{end}} {{if .Message}}{{.Message}}{{end}} ` const verificationPassedHTML = `

[OK] Verification Passed

{{with .Details}} {{if .checksum}}{{end}} {{end}}
Database:{{.Database}}
Hostname:{{.Hostname}}
Verified:{{formatTime .Timestamp}}
Checksum:{{.checksum}}
{{if .Message}}

{{.Message}}

{{end}}
` const verificationFailedText = ` [WARN] VERIFICATION FAILED [WARN] Database: {{.Database}} Hostname: {{.Hostname}} Failed At: {{formatTime .Timestamp}} {{if .Error}} Error: {{.Error}} {{end}} {{if .Message}}{{.Message}}{{end}} Backup integrity may be compromised. Please investigate. ` const verificationFailedHTML = `

[FAIL] Verification FAILED

{{if .Error}}{{end}}
Database:{{.Database}}
Hostname:{{.Hostname}}
Failed At:{{formatTime .Timestamp}}
Error:{{.Error}}
{{if .Message}}

{{.Message}}

{{end}}

Backup integrity may be compromised. Please investigate.

` const drDrillPassedText = ` DR Drill Test Passed Database: {{.Database}} Hostname: {{.Hostname}} Tested At: {{formatTime .Timestamp}} {{with .Details}} {{if .tables_restored}}Tables: {{.tables_restored}}{{end}} {{if .rows_validated}}Rows: {{.rows_validated}}{{end}} {{if .duration}}Duration: {{.duration}}{{end}} {{end}} {{if .Message}}{{.Message}}{{end}} Backup restore capability verified. ` const drDrillPassedHTML = `

[OK] DR Drill Passed

{{with .Details}} {{if .tables_restored}}{{end}} {{if .rows_validated}}{{end}} {{if .duration}}{{end}} {{end}}
Database:{{.Database}}
Hostname:{{.Hostname}}
Tested At:{{formatTime .Timestamp}}
Tables:{{.tables_restored}}
Rows:{{.rows_validated}}
Duration:{{.duration}}
{{if .Message}}

{{.Message}}

{{end}}

[OK] Backup restore capability verified

` const drDrillFailedText = ` [WARN] DR DRILL FAILED [WARN] Database: {{.Database}} Hostname: {{.Hostname}} Failed At: {{formatTime .Timestamp}} {{if .Error}} Error: {{.Error}} {{end}} {{if .Message}}{{.Message}}{{end}} Backup may not be restorable. Please investigate immediately. ` const drDrillFailedHTML = `

[FAIL] DR Drill FAILED

{{if .Error}}{{end}}
Database:{{.Database}}
Hostname:{{.Hostname}}
Failed At:{{formatTime .Timestamp}}
Error:{{.Error}}
{{if .Message}}

{{.Message}}

{{end}}

Backup may not be restorable. Please investigate immediately.

` // TemplateRenderer renders notification templates type TemplateRenderer struct { templates map[EventType]Templates funcMap template.FuncMap } // NewTemplateRenderer creates a new template renderer func NewTemplateRenderer() *TemplateRenderer { return &TemplateRenderer{ templates: DefaultTemplates(), funcMap: template.FuncMap{ "formatTime": func(t time.Time) string { return t.Format("2006-01-02 15:04:05 MST") }, "upper": strings.ToUpper, "lower": strings.ToLower, }, } } // RenderSubject renders the subject template for an event func (r *TemplateRenderer) RenderSubject(event *Event) (string, error) { tmpl, ok := r.templates[event.Type] if !ok { return fmt.Sprintf("[%s] %s: %s", event.Severity, event.Type, event.Database), nil } return r.render(tmpl.Subject, event) } // RenderText renders the text body template for an event func (r *TemplateRenderer) RenderText(event *Event) (string, error) { tmpl, ok := r.templates[event.Type] if !ok { return event.Message, nil } return r.render(tmpl.TextBody, event) } // RenderHTML renders the HTML body template for an event func (r *TemplateRenderer) RenderHTML(event *Event) (string, error) { tmpl, ok := r.templates[event.Type] if !ok { return fmt.Sprintf("

%s

", event.Message), nil } return r.render(tmpl.HTMLBody, event) } // render executes a template with the given event func (r *TemplateRenderer) render(templateStr string, event *Event) (string, error) { tmpl, err := template.New("notification").Funcs(r.funcMap).Parse(templateStr) if err != nil { return "", fmt.Errorf("failed to parse template: %w", err) } var buf bytes.Buffer if err := tmpl.Execute(&buf, event); err != nil { return "", fmt.Errorf("failed to execute template: %w", err) } return strings.TrimSpace(buf.String()), nil } // SetTemplate sets a custom template for an event type func (r *TemplateRenderer) SetTemplate(eventType EventType, templates Templates) { r.templates[eventType] = templates } // RenderSlackMessage creates a Slack-formatted message func (r *TemplateRenderer) RenderSlackMessage(event *Event) map[string]interface{} { color := "#3498db" // blue switch event.Severity { case SeveritySuccess: color = "#27ae60" // green case SeverityWarning: color = "#f39c12" // orange case SeverityError, SeverityCritical: color = "#e74c3c" // red } fields := []map[string]interface{}{ { "title": "Database", "value": event.Database, "short": true, }, { "title": "Hostname", "value": event.Hostname, "short": true, }, { "title": "Event", "value": string(event.Type), "short": true, }, { "title": "Severity", "value": string(event.Severity), "short": true, }, } if event.Error != "" { fields = append(fields, map[string]interface{}{ "title": "Error", "value": event.Error, "short": false, }) } for key, value := range event.Details { fields = append(fields, map[string]interface{}{ "title": key, "value": value, "short": true, }) } subject, _ := r.RenderSubject(event) return map[string]interface{}{ "attachments": []map[string]interface{}{ { "color": color, "title": subject, "text": event.Message, "fields": fields, "footer": "dbbackup", "ts": event.Timestamp.Unix(), "mrkdwn_in": []string{"text", "fields"}, }, }, } }