// 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 = `
| Database: | {{.Database}} |
| Hostname: | {{.Hostname}} |
| Started At: | {{formatTime .Timestamp}} |
{{.Message}}
{{end}}| Database: | {{.Database}} |
| Hostname: | {{.Hostname}} |
| Completed: | {{formatTime .Timestamp}} |
| Size: | {{.size}} |
| Duration: | {{.duration}} |
| Path: | {{.path}} |
{{.Message}}
{{end}}| Database: | {{.Database}} |
| Hostname: | {{.Hostname}} |
| Failed At: | {{formatTime .Timestamp}} |
| Error: | {{.Error}} |
{{.Message}}
{{end}}Please investigate immediately.
| Database: | {{.Database}} |
| Hostname: | {{.Hostname}} |
| Started At: | {{formatTime .Timestamp}} |
{{.Message}}
{{end}}| Database: | {{.Database}} |
| Hostname: | {{.Hostname}} |
| Completed: | {{formatTime .Timestamp}} |
| Duration: | {{.duration}} |
{{.Message}}
{{end}}| Database: | {{.Database}} |
| Hostname: | {{.Hostname}} |
| Failed At: | {{formatTime .Timestamp}} |
| Error: | {{.Error}} |
{{.Message}}
{{end}}Please investigate immediately.
| Database: | {{.Database}} |
| Hostname: | {{.Hostname}} |
| Verified: | {{formatTime .Timestamp}} |
| Checksum: | {{.checksum}} |
{{.Message}}
{{end}}| Database: | {{.Database}} |
| Hostname: | {{.Hostname}} |
| Failed At: | {{formatTime .Timestamp}} |
| Error: | {{.Error}} |
{{.Message}}
{{end}}Backup integrity may be compromised. Please investigate.
| Database: | {{.Database}} |
| Hostname: | {{.Hostname}} |
| Tested At: | {{formatTime .Timestamp}} |
| Tables: | {{.tables_restored}} |
| Rows: | {{.rows_validated}} |
| Duration: | {{.duration}} |
{{.Message}}
{{end}}[OK] Backup restore capability verified
| Database: | {{.Database}} |
| Hostname: | {{.Hostname}} |
| Failed At: | {{formatTime .Timestamp}} |
| Error: | {{.Error}} |
{{.Message}}
{{end}}Backup may not be restorable. Please investigate immediately.
%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"}, }, }, } }