mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-05 15:23:27 +00:00
404 lines
16 KiB
Go
404 lines
16 KiB
Go
package notifications
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
|
|
)
|
|
|
|
// EmailTemplate generates a professional HTML email template for alerts
|
|
func EmailTemplate(alertList []*alerts.Alert, isSingle bool) (subject, htmlBody, textBody string) {
|
|
if isSingle && len(alertList) == 1 {
|
|
return singleAlertTemplate(alertList[0])
|
|
}
|
|
return groupedAlertTemplate(alertList)
|
|
}
|
|
|
|
func singleAlertTemplate(alert *alerts.Alert) (subject, htmlBody, textBody string) {
|
|
levelColor := "#ff6b6b"
|
|
levelBg := "#fee"
|
|
if alert.Level == "warning" {
|
|
levelColor = "#ffd93d"
|
|
levelBg = "#fffaeb"
|
|
}
|
|
|
|
// Properly format alert type (CPU, Memory, etc.)
|
|
alertType := alert.Type
|
|
switch strings.ToLower(alertType) {
|
|
case "cpu":
|
|
alertType = "CPU"
|
|
case "memory":
|
|
alertType = "Memory"
|
|
case "disk":
|
|
alertType = "Disk"
|
|
case "io":
|
|
alertType = "I/O"
|
|
default:
|
|
alertType = strings.Title(alertType)
|
|
}
|
|
|
|
subject = fmt.Sprintf("[Pulse Alert] %s: %s on %s",
|
|
strings.Title(string(alert.Level)), alertType, alert.ResourceName)
|
|
|
|
htmlBody = fmt.Sprintf(`<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; background: #f5f5f5; margin: 0; padding: 0; }
|
|
.container { max-width: 600px; margin: 20px auto; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
.header { background: #1a1a1a; color: #fff; padding: 20px; text-align: center; }
|
|
.header h1 { margin: 0; font-size: 24px; font-weight: 500; }
|
|
.pulse-logo { width: 40px; height: 40px; margin: 0 auto 10px; }
|
|
.content { padding: 30px; }
|
|
.alert-box { background: %s; border-left: 4px solid %s; padding: 20px; margin: 20px 0; border-radius: 4px; }
|
|
.alert-level { color: %s; font-weight: bold; text-transform: uppercase; font-size: 14px; }
|
|
.alert-resource { font-size: 18px; font-weight: 500; margin: 10px 0; color: #1a1a1a; }
|
|
.metrics { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin: 20px 0; }
|
|
.metric { background: #f8f9fa; padding: 15px; border-radius: 4px; }
|
|
.metric-label { color: #666; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
.metric-value { font-size: 24px; font-weight: 500; color: #1a1a1a; margin-top: 5px; }
|
|
.details { background: #f8f9fa; padding: 20px; border-radius: 4px; margin: 20px 0; }
|
|
.detail-row { display: flex; justify-content: space-between; align-items: center; margin: 10px 0; padding-bottom: 10px; border-bottom: 1px solid #e9ecef; gap: 20px; }
|
|
.detail-row:last-child { border-bottom: none; padding-bottom: 0; }
|
|
.detail-label { color: #666; min-width: 120px; }
|
|
.detail-value { font-weight: 500; color: #1a1a1a; text-align: right; flex: 1; }
|
|
.footer { background: #f8f9fa; padding: 20px; text-align: center; color: #666; font-size: 12px; }
|
|
.footer a { color: #0066cc; text-decoration: none; }
|
|
@media (max-width: 600px) {
|
|
.metrics { grid-template-columns: 1fr; }
|
|
.container { margin: 0; border-radius: 0; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<svg class="pulse-logo" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M10 50 L30 50 L35 30 L40 70 L45 10 L50 90 L55 30 L60 70 L65 50 L90 50"
|
|
stroke="#4ade80" stroke-width="3" fill="none"/>
|
|
</svg>
|
|
<h1>Pulse Monitoring Alert</h1>
|
|
</div>
|
|
<div class="content">
|
|
<div class="alert-box">
|
|
<div class="alert-level">%s Alert</div>
|
|
<div class="alert-resource">%s</div>
|
|
<div>%s</div>
|
|
</div>
|
|
|
|
<div class="metrics">
|
|
<div class="metric">
|
|
<div class="metric-label">Current Value</div>
|
|
<div class="metric-value">%s</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-label">Threshold</div>
|
|
<div class="metric-value">%s</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="details">
|
|
<div class="detail-row">
|
|
<span class="detail-label">Resource ID</span>
|
|
<span class="detail-value">%s</span>
|
|
</div>
|
|
<div class="detail-row">
|
|
<span class="detail-label">Alert Type</span>
|
|
<span class="detail-value">%s</span>
|
|
</div>
|
|
<div class="detail-row">
|
|
<span class="detail-label">Node</span>
|
|
<span class="detail-value">%s</span>
|
|
</div>
|
|
<div class="detail-row">
|
|
<span class="detail-label">Instance</span>
|
|
<span class="detail-value">%s</span>
|
|
</div>
|
|
<div class="detail-row">
|
|
<span class="detail-label">Started</span>
|
|
<span class="detail-value">%s</span>
|
|
</div>
|
|
<div class="detail-row">
|
|
<span class="detail-label">Duration</span>
|
|
<span class="detail-value">%s</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="footer">
|
|
<p>This is an automated notification from Pulse Monitoring</p>
|
|
<p>View alerts and configure settings in your Pulse dashboard</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>`,
|
|
levelBg, levelColor, levelColor,
|
|
alert.Level,
|
|
alert.ResourceName,
|
|
alert.Message,
|
|
formatMetricValue(alert.Type, alert.Value),
|
|
formatMetricThreshold(alert.Type, alert.Threshold),
|
|
alert.ResourceID,
|
|
alertType,
|
|
alert.Node,
|
|
alert.Instance,
|
|
alert.StartTime.Format("Jan 2, 2006 at 3:04 PM"),
|
|
formatDuration(time.Since(alert.StartTime)),
|
|
)
|
|
|
|
// Plain text version
|
|
textBody = fmt.Sprintf(`PULSE MONITORING ALERT
|
|
|
|
%s ALERT: %s
|
|
|
|
Resource: %s (%s)
|
|
Type: %s
|
|
Current Value: %s (Threshold: %s)
|
|
Message: %s
|
|
|
|
Details:
|
|
- Node: %s
|
|
- Instance: %s
|
|
- Started: %s
|
|
- Duration: %s
|
|
|
|
This is an automated notification from Pulse Monitoring.
|
|
View alerts and configure settings in your Pulse dashboard.`,
|
|
strings.ToUpper(string(alert.Level)),
|
|
alert.ResourceName,
|
|
alert.ResourceName,
|
|
alert.ResourceID,
|
|
alert.Type,
|
|
formatMetricValue(alert.Type, alert.Value),
|
|
formatMetricThreshold(alert.Type, alert.Threshold),
|
|
alert.Message,
|
|
alert.Node,
|
|
alert.Instance,
|
|
alert.StartTime.Format("Jan 2, 2006 at 3:04 PM"),
|
|
formatDuration(time.Since(alert.StartTime)),
|
|
)
|
|
|
|
return subject, htmlBody, textBody
|
|
}
|
|
|
|
func groupedAlertTemplate(alertList []*alerts.Alert) (subject, htmlBody, textBody string) {
|
|
critical := 0
|
|
warning := 0
|
|
for _, alert := range alertList {
|
|
if alert.Level == "critical" {
|
|
critical++
|
|
} else {
|
|
warning++
|
|
}
|
|
}
|
|
|
|
// Subject line
|
|
if critical > 0 && warning > 0 {
|
|
subject = fmt.Sprintf("[Pulse Alert] %d Critical, %d Warning alerts", critical, warning)
|
|
} else if critical > 0 {
|
|
subject = fmt.Sprintf("[Pulse Alert] %d Critical alert%s", critical, pluralize(critical))
|
|
} else {
|
|
subject = fmt.Sprintf("[Pulse Alert] %d Warning alert%s", warning, pluralize(warning))
|
|
}
|
|
|
|
// Build alert rows
|
|
var alertRows strings.Builder
|
|
for _, alert := range alertList {
|
|
levelColor := "#ff6b6b"
|
|
if alert.Level == "warning" {
|
|
levelColor = "#ffd93d"
|
|
}
|
|
|
|
alertRows.WriteString(fmt.Sprintf(`
|
|
<tr>
|
|
<td style="padding: 12px; border-bottom: 1px solid #e9ecef;">
|
|
<div style="display: flex; align-items: center;">
|
|
<span style="display: inline-block; width: 8px; height: 8px; background: %s; border-radius: 50%%; margin-right: 10px;"></span>
|
|
<div>
|
|
<div style="font-weight: 500; color: #1a1a1a;">%s</div>
|
|
<div style="font-size: 12px; color: #666; margin-top: 2px;">%s on %s</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid #e9ecef; text-align: center;">
|
|
<span style="color: %s; font-weight: 500; text-transform: uppercase; font-size: 12px;">%s</span>
|
|
</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid #e9ecef; text-align: right;">
|
|
<div style="font-weight: 500;">%s</div>
|
|
<div style="font-size: 12px; color: #666;">of %s</div>
|
|
</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid #e9ecef; text-align: right; color: #666; font-size: 12px;">
|
|
%s ago
|
|
</td>
|
|
</tr>`,
|
|
levelColor,
|
|
alert.ResourceName,
|
|
alert.Type, alert.Node,
|
|
levelColor, alert.Level,
|
|
formatMetricValue(alert.Type, alert.Value), formatMetricThreshold(alert.Type, alert.Threshold),
|
|
formatDuration(time.Since(alert.StartTime)),
|
|
))
|
|
}
|
|
|
|
htmlBody = fmt.Sprintf(`<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; background: #f5f5f5; margin: 0; padding: 0; }
|
|
.container { max-width: 800px; margin: 20px auto; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
.header { background: #1a1a1a; color: #fff; padding: 20px; text-align: center; }
|
|
.header h1 { margin: 0; font-size: 24px; font-weight: 500; }
|
|
.pulse-logo { width: 40px; height: 40px; margin: 0 auto 10px; }
|
|
.content { padding: 30px; }
|
|
.summary { background: #f8f9fa; padding: 20px; border-radius: 4px; margin-bottom: 30px; }
|
|
.summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px; margin-top: 15px; }
|
|
.summary-item { text-align: center; }
|
|
.summary-count { font-size: 32px; font-weight: 500; }
|
|
.critical-count { color: #ff6b6b; }
|
|
.warning-count { color: #ffd93d; }
|
|
.summary-label { color: #666; font-size: 14px; margin-top: 5px; }
|
|
.alerts-table { width: 100%%; margin-top: 20px; border-collapse: collapse; }
|
|
.alerts-table th { text-align: left; padding: 12px; border-bottom: 2px solid #e9ecef; color: #666; font-weight: 500; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
.footer { background: #f8f9fa; padding: 20px; text-align: center; color: #666; font-size: 12px; }
|
|
.footer a { color: #0066cc; text-decoration: none; }
|
|
@media (max-width: 600px) {
|
|
.container { margin: 0; border-radius: 0; }
|
|
.alerts-table { font-size: 14px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<svg class="pulse-logo" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M10 50 L30 50 L35 30 L40 70 L45 10 L50 90 L55 30 L60 70 L65 50 L90 50"
|
|
stroke="#4ade80" stroke-width="3" fill="none"/>
|
|
</svg>
|
|
<h1>Pulse Monitoring Alert Summary</h1>
|
|
</div>
|
|
<div class="content">
|
|
<div class="summary">
|
|
<h2 style="margin: 0 0 15px 0; font-size: 18px;">%d New Alert%s</h2>
|
|
<div class="summary-grid">`,
|
|
len(alertList), pluralize(len(alertList)))
|
|
|
|
if critical > 0 {
|
|
htmlBody += fmt.Sprintf(`
|
|
<div class="summary-item">
|
|
<div class="summary-count critical-count">%d</div>
|
|
<div class="summary-label">Critical</div>
|
|
</div>`, critical)
|
|
}
|
|
|
|
if warning > 0 {
|
|
htmlBody += fmt.Sprintf(`
|
|
<div class="summary-item">
|
|
<div class="summary-count warning-count">%d</div>
|
|
<div class="summary-label">Warning</div>
|
|
</div>`, warning)
|
|
}
|
|
|
|
htmlBody += fmt.Sprintf(`
|
|
</div>
|
|
</div>
|
|
|
|
<table class="alerts-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Resource</th>
|
|
<th style="text-align: center;">Level</th>
|
|
<th style="text-align: right;">Value</th>
|
|
<th style="text-align: right;">Duration</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>%s
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="footer">
|
|
<p>This is an automated notification from Pulse Monitoring</p>
|
|
<p>View alerts and configure settings in your Pulse dashboard</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>`, alertRows.String())
|
|
|
|
// Plain text version
|
|
var textBuilder strings.Builder
|
|
textBuilder.WriteString("PULSE MONITORING ALERT SUMMARY\n\n")
|
|
textBuilder.WriteString(fmt.Sprintf("%d New Alert%s\n", len(alertList), pluralize(len(alertList))))
|
|
if critical > 0 {
|
|
textBuilder.WriteString(fmt.Sprintf("Critical: %d\n", critical))
|
|
}
|
|
if warning > 0 {
|
|
textBuilder.WriteString(fmt.Sprintf("Warning: %d\n", warning))
|
|
}
|
|
textBuilder.WriteString("\nAlert Details:\n")
|
|
textBuilder.WriteString("─────────────────────────────────────────────────────────────\n")
|
|
|
|
for i, alert := range alertList {
|
|
textBuilder.WriteString(fmt.Sprintf("\n%d. %s (%s)\n", i+1, alert.ResourceName, alert.ResourceID))
|
|
textBuilder.WriteString(fmt.Sprintf(" Level: %s | Type: %s\n", strings.ToUpper(string(alert.Level)), alert.Type))
|
|
textBuilder.WriteString(fmt.Sprintf(" Value: %s (Threshold: %s)\n", formatMetricValue(alert.Type, alert.Value), formatMetricThreshold(alert.Type, alert.Threshold)))
|
|
textBuilder.WriteString(fmt.Sprintf(" Node: %s | Started: %s ago\n", alert.Node, formatDuration(time.Since(alert.StartTime))))
|
|
textBuilder.WriteString(fmt.Sprintf(" Message: %s\n", alert.Message))
|
|
}
|
|
|
|
textBuilder.WriteString("\n─────────────────────────────────────────────────────────────\n")
|
|
textBuilder.WriteString("This is an automated notification from Pulse Monitoring.\n")
|
|
textBuilder.WriteString("Configure alert settings in the Pulse dashboard.")
|
|
|
|
textBody = textBuilder.String()
|
|
return subject, htmlBody, textBody
|
|
}
|
|
|
|
func formatDuration(d time.Duration) string {
|
|
if d < time.Minute {
|
|
return fmt.Sprintf("%d seconds", int(d.Seconds()))
|
|
} else if d < time.Hour {
|
|
return fmt.Sprintf("%d minutes", int(d.Minutes()))
|
|
} else if d < 24*time.Hour {
|
|
return fmt.Sprintf("%.1f hours", d.Hours())
|
|
}
|
|
return fmt.Sprintf("%.1f days", d.Hours()/24)
|
|
}
|
|
|
|
func pluralize(count int) string {
|
|
if count == 1 {
|
|
return ""
|
|
}
|
|
return "s"
|
|
}
|
|
|
|
// formatMetricValue formats a metric value with the appropriate unit
|
|
func formatMetricValue(metricType string, value float64) string {
|
|
switch strings.ToLower(metricType) {
|
|
case "diskread", "diskwrite", "networkin", "networkout":
|
|
return fmt.Sprintf("%.1f MB/s", value)
|
|
case "temperature":
|
|
return fmt.Sprintf("%.1f°C", value)
|
|
case "cpu", "memory", "disk", "usage":
|
|
return fmt.Sprintf("%.1f%%", value)
|
|
default:
|
|
return fmt.Sprintf("%.1f", value)
|
|
}
|
|
}
|
|
|
|
// formatMetricThreshold formats a metric threshold with the appropriate unit
|
|
func formatMetricThreshold(metricType string, threshold float64) string {
|
|
switch strings.ToLower(metricType) {
|
|
case "diskread", "diskwrite", "networkin", "networkout":
|
|
return fmt.Sprintf("%.0f MB/s", threshold)
|
|
case "temperature":
|
|
return fmt.Sprintf("%.0f°C", threshold)
|
|
case "cpu", "memory", "disk", "usage":
|
|
return fmt.Sprintf("%.0f%%", threshold)
|
|
default:
|
|
return fmt.Sprintf("%.0f", threshold)
|
|
}
|
|
}
|