feat: Implement comprehensive multi-channel notification system with templating and SSL monitoring
- Add robust notification system supporting 7+ communication channels with intelligent message templating, resource monitoring, and SSL certificate alerts. Notification Channels: (Telegram, Discord, Slack, Signal, Email, Google Chat, Webhooks). - This notification system provides enterprise-grade alerting capabilities with extensive customization options and multi-channel redundancy for critical service monitoring.
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
|
||||
package notification
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DiscordService handles Discord notifications
|
||||
type DiscordService struct{}
|
||||
|
||||
// NewDiscordService creates a new Discord notification service
|
||||
func NewDiscordService() *DiscordService {
|
||||
return &DiscordService{}
|
||||
}
|
||||
|
||||
// DiscordPayload represents the payload for Discord webhook
|
||||
type DiscordPayload struct {
|
||||
Content string `json:"content,omitempty"`
|
||||
Embeds []DiscordEmbed `json:"embeds,omitempty"`
|
||||
}
|
||||
|
||||
// DiscordEmbed represents a Discord embed
|
||||
type DiscordEmbed struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Color int `json:"color,omitempty"`
|
||||
Timestamp string `json:"timestamp,omitempty"`
|
||||
Fields []DiscordEmbedField `json:"fields,omitempty"`
|
||||
Footer *DiscordEmbedFooter `json:"footer,omitempty"`
|
||||
}
|
||||
|
||||
// DiscordEmbedField represents a field in Discord embed
|
||||
type DiscordEmbedField struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Inline bool `json:"inline,omitempty"`
|
||||
}
|
||||
|
||||
// DiscordEmbedFooter represents footer in Discord embed
|
||||
type DiscordEmbedFooter struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// SendNotification sends a notification via Discord webhook
|
||||
func (ds *DiscordService) SendNotification(config *AlertConfiguration, message string) error {
|
||||
if config.DiscordWebhookURL == "" {
|
||||
return fmt.Errorf("discord webhook URL is required")
|
||||
}
|
||||
|
||||
// Create rich embed for better formatting
|
||||
embed := ds.createDiscordEmbed(message)
|
||||
|
||||
payload := DiscordPayload{
|
||||
Embeds: []DiscordEmbed{embed},
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal Discord payload: %v", err)
|
||||
}
|
||||
|
||||
resp, err := http.Post(config.DiscordWebhookURL, "application/json", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("discord webhook request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("discord webhook error, status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createDiscordEmbed creates a formatted Discord embed from the message
|
||||
func (ds *DiscordService) createDiscordEmbed(message string) DiscordEmbed {
|
||||
// Parse the message to extract structured information
|
||||
lines := strings.Split(message, "\n")
|
||||
|
||||
if len(lines) == 0 {
|
||||
return DiscordEmbed{
|
||||
Description: message,
|
||||
Color: ds.getDefaultColor(),
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
// Extract title from first line and determine status
|
||||
title := strings.TrimSpace(lines[0])
|
||||
color := ds.determineColorFromMessage(title)
|
||||
|
||||
// Create fields from the remaining lines
|
||||
var fields []DiscordEmbedField
|
||||
|
||||
for i, line := range lines {
|
||||
if i == 0 {
|
||||
continue // Skip title line
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse field from "• Key: Value" format
|
||||
if strings.HasPrefix(line, "•") || strings.HasPrefix(line, "-") {
|
||||
line = strings.TrimPrefix(line, "•")
|
||||
line = strings.TrimPrefix(line, "-")
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
fieldName := strings.TrimSpace(parts[0])
|
||||
fieldValue := strings.TrimSpace(parts[1])
|
||||
|
||||
if fieldName != "" && fieldValue != "" {
|
||||
fields = append(fields, DiscordEmbedField{
|
||||
Name: fieldName,
|
||||
Value: fieldValue,
|
||||
Inline: true,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// If not in key:value format, add as description field
|
||||
fields = append(fields, DiscordEmbedField{
|
||||
Name: "Details",
|
||||
Value: line,
|
||||
Inline: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no structured fields found, use the entire message as description
|
||||
if len(fields) == 0 {
|
||||
// Remove the first line from description since it's used as title
|
||||
description := ""
|
||||
if len(lines) > 1 {
|
||||
description = strings.Join(lines[1:], "\n")
|
||||
}
|
||||
|
||||
return DiscordEmbed{
|
||||
Title: title,
|
||||
Description: description,
|
||||
Color: color,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Footer: &DiscordEmbedFooter{
|
||||
Text: "CheckCle System Alert",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return DiscordEmbed{
|
||||
Title: title,
|
||||
Color: color,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Fields: fields,
|
||||
Footer: &DiscordEmbedFooter{
|
||||
Text: "CheckCle System Alert",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// determineColorFromMessage determines Discord embed color based on message content
|
||||
func (ds *DiscordService) determineColorFromMessage(message string) int {
|
||||
messageLower := strings.ToLower(message)
|
||||
|
||||
// Red for critical/error states
|
||||
if strings.Contains(messageLower, "expired") ||
|
||||
strings.Contains(messageLower, "down") ||
|
||||
strings.Contains(messageLower, "failed") ||
|
||||
strings.Contains(messageLower, "error") ||
|
||||
strings.Contains(messageLower, "critical") ||
|
||||
strings.Contains(messageLower, "🚨") ||
|
||||
strings.Contains(messageLower, "🔴") {
|
||||
return 15158332 // Red (#E74C3C)
|
||||
}
|
||||
|
||||
// Orange for warnings
|
||||
if strings.Contains(messageLower, "expiring_soon") ||
|
||||
strings.Contains(messageLower, "expiring soon") ||
|
||||
strings.Contains(messageLower, "warning") ||
|
||||
strings.Contains(messageLower, "maintenance") ||
|
||||
strings.Contains(messageLower, "paused") ||
|
||||
strings.Contains(messageLower, "⚠️") ||
|
||||
strings.Contains(messageLower, "🟡") ||
|
||||
strings.Contains(messageLower, "🟠") {
|
||||
return 15105570 // Orange (#E67E22)
|
||||
}
|
||||
|
||||
// Green for success/up states
|
||||
if strings.Contains(messageLower, "up") ||
|
||||
strings.Contains(messageLower, "resolved") ||
|
||||
strings.Contains(messageLower, "success") ||
|
||||
strings.Contains(messageLower, "restored") ||
|
||||
strings.Contains(messageLower, "valid") ||
|
||||
strings.Contains(messageLower, "🟢") ||
|
||||
strings.Contains(messageLower, "✅") {
|
||||
return 3066993 // Green (#2ECC71)
|
||||
}
|
||||
|
||||
// Blue for info/default
|
||||
return 3447003 // Blue (#3498DB)
|
||||
}
|
||||
|
||||
// getDefaultColor returns the default Discord embed color (blue)
|
||||
func (ds *DiscordService) getDefaultColor() int {
|
||||
return 3447003 // Blue (#3498DB)
|
||||
}
|
||||
@@ -0,0 +1,532 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// EmailService handles email notifications
|
||||
type EmailService struct{}
|
||||
|
||||
// NewEmailService creates a new email notification service
|
||||
func NewEmailService() *EmailService {
|
||||
// log.Printf("✅ Email notification service initialized")
|
||||
return &EmailService{}
|
||||
}
|
||||
|
||||
// SendNotification sends a notification via email
|
||||
func (es *EmailService) SendNotification(config *AlertConfiguration, message string) error {
|
||||
// log.Printf("📧 === SENDING EMAIL NOTIFICATION ===")
|
||||
// log.Printf("🔔 Email notification request received")
|
||||
// log.Printf("📊 Email Configuration:")
|
||||
// log.Printf(" - Email Address: %s", config.EmailAddress)
|
||||
// log.Printf(" - Sender Name: %s", config.EmailSenderName)
|
||||
// log.Printf(" - SMTP Server: %s", config.SMTPServer)
|
||||
// log.Printf(" - SMTP Port: %s", config.SMTPPort)
|
||||
// log.Printf(" - SMTP Password present: %t", config.SMTPPassword != "")
|
||||
// log.Printf(" - Message Length: %d chars", len(message))
|
||||
|
||||
// Validate email configuration
|
||||
if config.EmailAddress == "" || config.SMTPServer == "" || config.SMTPPort == "" {
|
||||
// log.Printf("❌ EMAIL CONFIGURATION ERROR: Missing required fields")
|
||||
// log.Printf(" - Email Address present: %t", config.EmailAddress != "")
|
||||
// log.Printf(" - SMTP Server present: %t", config.SMTPServer != "")
|
||||
// log.Printf(" - SMTP Port present: %t", config.SMTPPort != "")
|
||||
return fmt.Errorf("email configuration is incomplete")
|
||||
}
|
||||
|
||||
if config.SMTPPassword == "" {
|
||||
// log.Printf("⚠️ WARNING: SMTP password not provided - authentication may fail")
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(config.SMTPPort)
|
||||
if err != nil {
|
||||
// log.Printf("❌ SMTP PORT ERROR: Invalid port '%s': %v", config.SMTPPort, err)
|
||||
return fmt.Errorf("invalid SMTP port: %v", err)
|
||||
}
|
||||
|
||||
// log.Printf("✅ Email configuration validation passed")
|
||||
// log.Printf("🔧 Parsed SMTP Port: %d", port)
|
||||
|
||||
// Determine alert severity and get appropriate emoji/color
|
||||
severity, emoji := es.parseMessageSeverity(message)
|
||||
// log.Printf("📋 Message Analysis:")
|
||||
// log.Printf(" - Detected Severity: %s", severity)
|
||||
// log.Printf(" - Emoji: %s", emoji)
|
||||
|
||||
// Create enhanced email content
|
||||
subject := es.createEmailSubject(config.EmailSenderName, severity)
|
||||
htmlBody := es.createHTMLEmailBody(message, severity, emoji)
|
||||
plainBody := es.createPlainEmailBody(message, severity, emoji)
|
||||
|
||||
// log.Printf("📝 Email Content Created:")
|
||||
// log.Printf(" - Subject: %s", subject)
|
||||
// log.Printf(" - HTML Body Length: %d chars", len(htmlBody))
|
||||
// log.Printf(" - Plain Body Length: %d chars", len(plainBody))
|
||||
|
||||
// Prepare email message with both HTML and plain text
|
||||
emailMessage := es.createMIMEMessage(config.EmailSenderName, config.EmailAddress, subject, htmlBody, plainBody)
|
||||
|
||||
// log.Printf("📤 Preparing to send email...")
|
||||
// log.Printf(" - SMTP Server: %s:%d", config.SMTPServer, port)
|
||||
// log.Printf(" - From: %s", config.EmailSenderName)
|
||||
// log.Printf(" - To: %s", config.EmailAddress)
|
||||
|
||||
// Send email with enhanced SMTP handling
|
||||
err = es.sendSMTPEmail(config.SMTPServer, port, config.EmailSenderName, config.EmailAddress, config.SMTPPassword, emailMessage)
|
||||
if err != nil {
|
||||
// log.Printf("❌ EMAIL SENDING FAILED: %v", err)
|
||||
// log.Printf("❌ Error Details:")
|
||||
// log.Printf(" - SMTP Server: %s:%d", config.SMTPServer, port)
|
||||
// log.Printf(" - Error Type: %T", err)
|
||||
// log.Printf(" - Error Message: %s", err.Error())
|
||||
return fmt.Errorf("failed to send email: %v", err)
|
||||
}
|
||||
|
||||
// log.Printf("✅ EMAIL SENT SUCCESSFULLY")
|
||||
// log.Printf("✅ Email Details:")
|
||||
// log.Printf(" - Recipient: %s", config.EmailAddress)
|
||||
// log.Printf(" - Subject: %s", subject)
|
||||
// log.Printf(" - Severity: %s", severity)
|
||||
// log.Printf(" - Timestamp: %s", time.Now().Format("2006-01-02 15:04:05"))
|
||||
// log.Printf("=== EMAIL NOTIFICATION COMPLETE ===")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseMessageSeverity analyzes the message to determine severity level
|
||||
func (es *EmailService) parseMessageSeverity(message string) (string, string) {
|
||||
messageUpper := strings.ToUpper(message)
|
||||
|
||||
// Check for critical indicators
|
||||
if strings.Contains(messageUpper, "🚨") || strings.Contains(messageUpper, "CRITICAL") ||
|
||||
strings.Contains(messageUpper, "DOWN") || strings.Contains(messageUpper, "ERROR") ||
|
||||
strings.Contains(messageUpper, "FAILED") || strings.Contains(messageUpper, "🔴") {
|
||||
return "CRITICAL", "🚨"
|
||||
}
|
||||
|
||||
// Check for warning indicators
|
||||
if strings.Contains(messageUpper, "⚠️") || strings.Contains(messageUpper, "WARNING") ||
|
||||
strings.Contains(messageUpper, "🟡") || strings.Contains(messageUpper, "INCIDENT") {
|
||||
return "WARNING", "⚠️"
|
||||
}
|
||||
|
||||
// Check for success indicators
|
||||
if strings.Contains(messageUpper, "🟢") || strings.Contains(messageUpper, "UP") ||
|
||||
strings.Contains(messageUpper, "SUCCESS") || strings.Contains(messageUpper, "RESOLVED") ||
|
||||
strings.Contains(messageUpper, "RESTORED") {
|
||||
return "SUCCESS", "✅"
|
||||
}
|
||||
|
||||
// Check for maintenance indicators
|
||||
if strings.Contains(messageUpper, "🟠") || strings.Contains(messageUpper, "MAINTENANCE") ||
|
||||
strings.Contains(messageUpper, "PAUSED") {
|
||||
return "MAINTENANCE", "🔧"
|
||||
}
|
||||
|
||||
// Default to info
|
||||
return "INFO", "ℹ️"
|
||||
}
|
||||
|
||||
// createEmailSubject creates an appropriate email subject
|
||||
func (es *EmailService) createEmailSubject(senderName, severity string) string {
|
||||
prefix := "Service Alert"
|
||||
if senderName != "" {
|
||||
prefix = fmt.Sprintf("%s - Service Alert", senderName)
|
||||
}
|
||||
|
||||
switch severity {
|
||||
case "CRITICAL":
|
||||
return fmt.Sprintf("🚨 [CRITICAL] %s", prefix)
|
||||
case "WARNING":
|
||||
return fmt.Sprintf("⚠️ [WARNING] %s", prefix)
|
||||
case "SUCCESS":
|
||||
return fmt.Sprintf("✅ [RESOLVED] %s", prefix)
|
||||
case "MAINTENANCE":
|
||||
return fmt.Sprintf("🔧 [MAINTENANCE] %s", prefix)
|
||||
default:
|
||||
return fmt.Sprintf("ℹ️ [INFO] %s", prefix)
|
||||
}
|
||||
}
|
||||
|
||||
// createHTMLEmailBody creates a rich HTML email body
|
||||
func (es *EmailService) createHTMLEmailBody(message, severity, emoji string) string {
|
||||
// Get color scheme based on severity
|
||||
var bgColor, borderColor, textColor string
|
||||
switch severity {
|
||||
case "CRITICAL":
|
||||
bgColor = "#fee2e2"
|
||||
borderColor = "#dc2626"
|
||||
textColor = "#991b1b"
|
||||
case "WARNING":
|
||||
bgColor = "#fef3c7"
|
||||
borderColor = "#d97706"
|
||||
textColor = "#92400e"
|
||||
case "SUCCESS":
|
||||
bgColor = "#dcfce7"
|
||||
borderColor = "#16a34a"
|
||||
textColor = "#15803d"
|
||||
case "MAINTENANCE":
|
||||
bgColor = "#fed7aa"
|
||||
borderColor = "#ea580c"
|
||||
textColor = "#c2410c"
|
||||
default:
|
||||
bgColor = "#dbeafe"
|
||||
borderColor = "#2563eb"
|
||||
textColor = "#1d4ed8"
|
||||
}
|
||||
|
||||
// Format message content for HTML
|
||||
htmlMessage := strings.ReplaceAll(message, "\n", "<br>")
|
||||
|
||||
// Enhance bullet points and formatting
|
||||
htmlMessage = strings.ReplaceAll(htmlMessage, " - ", "<br> • ")
|
||||
htmlMessage = strings.ReplaceAll(htmlMessage, "Service:", "<strong>Service:</strong>")
|
||||
htmlMessage = strings.ReplaceAll(htmlMessage, "Status:", "<strong>Status:</strong>")
|
||||
htmlMessage = strings.ReplaceAll(htmlMessage, "Host:", "<strong>Host:</strong>")
|
||||
htmlMessage = strings.ReplaceAll(htmlMessage, "Type:", "<strong>Type:</strong>")
|
||||
htmlMessage = strings.ReplaceAll(htmlMessage, "Response time:", "<strong>Response time:</strong>")
|
||||
htmlMessage = strings.ReplaceAll(htmlMessage, "Time:", "<strong>Time:</strong>")
|
||||
htmlMessage = strings.ReplaceAll(htmlMessage, "Domain:", "<strong>Domain:</strong>")
|
||||
htmlMessage = strings.ReplaceAll(htmlMessage, "Days Remaining:", "<strong>Days Remaining:</strong>")
|
||||
htmlMessage = strings.ReplaceAll(htmlMessage, "Expiration Date:", "<strong>Expiration Date:</strong>")
|
||||
|
||||
return fmt.Sprintf(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Service Alert Notification</title>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 20px; font-family: Arial, sans-serif; background-color: #f5f5f5;">
|
||||
<div style="max-width: 600px; margin: 0 auto; background-color: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
|
||||
<!-- Header -->
|
||||
<div style="background-color: %s; color: %s; padding: 20px; border-radius: 8px 8px 0 0; border-left: 4px solid %s;">
|
||||
<h2 style="margin: 0; font-size: 18px;">
|
||||
%s Service Alert Notification
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div style="padding: 20px;">
|
||||
<div style="background-color: #f8f9fa; padding: 15px; border-radius: 6px; border-left: 3px solid %s;">
|
||||
<p style="margin: 0; font-size: 14px; line-height: 1.6; color: #333;">
|
||||
%s
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #e5e7eb;">
|
||||
<p style="margin: 0; font-size: 12px; color: #6b7280; text-align: center;">
|
||||
This is an automated notification from your monitoring system.<br>
|
||||
Generated at: %s
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`,
|
||||
bgColor, textColor, borderColor, emoji, borderColor, htmlMessage, time.Now().Format("2006-01-02 15:04:05 MST"))
|
||||
}
|
||||
|
||||
// createPlainEmailBody creates a plain text email body
|
||||
func (es *EmailService) createPlainEmailBody(message, severity, emoji string) string {
|
||||
separator := strings.Repeat("=", 50)
|
||||
|
||||
return fmt.Sprintf(`%s
|
||||
%s SERVICE ALERT NOTIFICATION
|
||||
%s
|
||||
|
||||
%s
|
||||
|
||||
%s
|
||||
This is an automated notification from your monitoring system.
|
||||
Generated at: %s
|
||||
%s`,
|
||||
separator,
|
||||
emoji+" "+severity,
|
||||
separator,
|
||||
message,
|
||||
separator,
|
||||
time.Now().Format("2006-01-02 15:04:05 MST"),
|
||||
separator)
|
||||
}
|
||||
|
||||
// createMIMEMessage creates a MIME message with both HTML and plain text
|
||||
func (es *EmailService) createMIMEMessage(fromName, toEmail, subject, htmlBody, plainBody string) string {
|
||||
boundary := fmt.Sprintf("boundary_%d", time.Now().Unix())
|
||||
|
||||
headers := fmt.Sprintf(`From: %s
|
||||
To: %s
|
||||
Subject: %s
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/alternative; boundary="%s"
|
||||
|
||||
`, fromName, toEmail, subject, boundary)
|
||||
|
||||
body := fmt.Sprintf(`--%s
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
%s
|
||||
|
||||
--%s
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
%s
|
||||
|
||||
--%s--
|
||||
`, boundary, plainBody, boundary, htmlBody, boundary)
|
||||
|
||||
return headers + body
|
||||
}
|
||||
|
||||
// sendSMTPEmail sends the email using SMTP with proper authentication
|
||||
func (es *EmailService) sendSMTPEmail(smtpServer string, port int, fromEmail, toEmail, password, message string) error {
|
||||
addr := fmt.Sprintf("%s:%d", smtpServer, port)
|
||||
|
||||
// log.Printf("🔌 Connecting to SMTP server: %s", addr)
|
||||
|
||||
// Extract hostname from SMTP server for proper HELO
|
||||
hostname := smtpServer
|
||||
if strings.Contains(hostname, ".") {
|
||||
// Use the SMTP server hostname for HELO
|
||||
hostname = smtpServer
|
||||
}
|
||||
|
||||
// log.Printf("🔧 Using hostname for HELO: %s", hostname)
|
||||
|
||||
// For port 587 (STARTTLS) - most common for authenticated SMTP
|
||||
if port == 587 {
|
||||
// log.Printf("🔐 Attempting STARTTLS connection with authentication...")
|
||||
return es.sendWithSTARTTLSAuth(addr, hostname, fromEmail, toEmail, password, message)
|
||||
}
|
||||
|
||||
// For port 465 (SSL/TLS)
|
||||
if port == 465 {
|
||||
// log.Printf("🔒 Attempting SSL connection with authentication...")
|
||||
return es.sendWithSSLAuth(addr, hostname, fromEmail, toEmail, password, message)
|
||||
}
|
||||
|
||||
// For port 25 (Plain SMTP with optional STARTTLS)
|
||||
if port == 25 {
|
||||
// log.Printf("📧 Attempting plain SMTP with optional STARTTLS...")
|
||||
return es.sendWithSTARTTLSAuth(addr, hostname, fromEmail, toEmail, password, message)
|
||||
}
|
||||
|
||||
// Fallback to STARTTLS for any other port
|
||||
// log.Printf("📧 Using STARTTLS with auth fallback for port %d...", port)
|
||||
return es.sendWithSTARTTLSAuth(addr, hostname, fromEmail, toEmail, password, message)
|
||||
}
|
||||
|
||||
// sendWithSTARTTLSAuth sends email with STARTTLS and authentication
|
||||
func (es *EmailService) sendWithSTARTTLSAuth(addr, hostname, fromEmail, toEmail, password, message string) error {
|
||||
// log.Printf("🔐 Establishing STARTTLS connection to %s", addr)
|
||||
|
||||
// Connect to server
|
||||
client, err := smtp.Dial(addr)
|
||||
if err != nil {
|
||||
// log.Printf("❌ Failed to connect to SMTP server: %v", err)
|
||||
return fmt.Errorf("failed to connect to SMTP server: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// Send EHLO with proper hostname
|
||||
// log.Printf("👋 Sending EHLO with hostname: %s", hostname)
|
||||
if err = client.Hello(hostname); err != nil {
|
||||
// log.Printf("❌ EHLO failed: %v", err)
|
||||
return fmt.Errorf("EHLO failed: %v", err)
|
||||
}
|
||||
|
||||
// Check if STARTTLS is supported and use it
|
||||
if ok, _ := client.Extension("STARTTLS"); ok {
|
||||
// log.Printf("🔐 STARTTLS supported, initiating TLS...")
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: strings.Split(addr, ":")[0],
|
||||
InsecureSkipVerify: false,
|
||||
}
|
||||
if err = client.StartTLS(tlsConfig); err != nil {
|
||||
// log.Printf("❌ STARTTLS failed: %v", err)
|
||||
return fmt.Errorf("STARTTLS failed: %v", err)
|
||||
}
|
||||
// log.Printf("✅ TLS connection established")
|
||||
} else {
|
||||
// log.Printf("⚠️ STARTTLS not supported by server, continuing with plain connection")
|
||||
}
|
||||
|
||||
// Check for AUTH support and authenticate if available
|
||||
if ok, mechanisms := client.Extension("AUTH"); ok {
|
||||
// Suppress unused variable warning
|
||||
_ = mechanisms
|
||||
// log.Printf("🔑 AUTH extension supported with mechanisms: %s", mechanisms)
|
||||
|
||||
// Use the provided credentials for authentication
|
||||
username := fromEmail
|
||||
|
||||
// log.Printf("🔐 Attempting authentication for user: %s", username)
|
||||
// log.Printf("🔑 Password provided: %t", password != "")
|
||||
|
||||
if password != "" {
|
||||
// Create auth mechanism - try PLAIN first as it's most common
|
||||
auth := smtp.PlainAuth("", username, password, strings.Split(addr, ":")[0])
|
||||
|
||||
if err := client.Auth(auth); err != nil {
|
||||
// log.Printf("❌ Authentication failed: %v", err)
|
||||
return fmt.Errorf("SMTP authentication failed: %v", err)
|
||||
} else {
|
||||
// log.Printf("✅ Authentication successful")
|
||||
}
|
||||
} else {
|
||||
// log.Printf("⚠️ No password provided, skipping authentication")
|
||||
return fmt.Errorf("SMTP password is required for authentication")
|
||||
}
|
||||
} else {
|
||||
// log.Printf("ℹ️ AUTH extension not available, proceeding without authentication")
|
||||
}
|
||||
|
||||
// Set sender
|
||||
// log.Printf("📤 Setting sender: %s", fromEmail)
|
||||
if err = client.Mail(fromEmail); err != nil {
|
||||
// log.Printf("❌ Failed to set sender: %v", err)
|
||||
return fmt.Errorf("failed to set sender: %v", err)
|
||||
}
|
||||
|
||||
// Set recipient
|
||||
// log.Printf("📥 Setting recipient: %s", toEmail)
|
||||
if err = client.Rcpt(toEmail); err != nil {
|
||||
// log.Printf("❌ Failed to set recipient: %v", err)
|
||||
return fmt.Errorf("failed to set recipient: %v", err)
|
||||
}
|
||||
|
||||
// Send message
|
||||
// log.Printf("📝 Sending message data...")
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
// log.Printf("❌ Failed to initiate data transfer: %v", err)
|
||||
return fmt.Errorf("failed to initiate data transfer: %v", err)
|
||||
}
|
||||
|
||||
_, err = w.Write([]byte(message))
|
||||
if err != nil {
|
||||
// log.Printf("❌ Failed to write message data: %v", err)
|
||||
return fmt.Errorf("failed to write message data: %v", err)
|
||||
}
|
||||
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
// log.Printf("❌ Failed to close data writer: %v", err)
|
||||
return fmt.Errorf("failed to close data writer: %v", err)
|
||||
}
|
||||
|
||||
// Quit gracefully
|
||||
err = client.Quit()
|
||||
if err != nil {
|
||||
// log.Printf("⚠️ Warning during QUIT: %v", err)
|
||||
// Don't return error for QUIT issues as email might have been sent
|
||||
}
|
||||
|
||||
// log.Printf("✅ Email sent successfully via STARTTLS")
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendWithSSLAuth sends email with SSL/TLS and authentication (for port 465)
|
||||
func (es *EmailService) sendWithSSLAuth(addr, hostname, fromEmail, toEmail, password, message string) error {
|
||||
// log.Printf("🔒 Establishing SSL/TLS connection to %s", addr)
|
||||
|
||||
// Create TLS configuration
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: false,
|
||||
ServerName: strings.Split(addr, ":")[0],
|
||||
}
|
||||
|
||||
// Connect with TLS
|
||||
conn, err := tls.Dial("tcp", addr, tlsConfig)
|
||||
if err != nil {
|
||||
// log.Printf("❌ Failed to establish TLS connection: %v", err)
|
||||
return fmt.Errorf("failed to establish TLS connection: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Create SMTP client
|
||||
client, err := smtp.NewClient(conn, strings.Split(addr, ":")[0])
|
||||
if err != nil {
|
||||
// log.Printf("❌ Failed to create SMTP client: %v", err)
|
||||
return fmt.Errorf("failed to create SMTP client: %v", err)
|
||||
}
|
||||
defer client.Quit()
|
||||
|
||||
// Send EHLO with proper hostname
|
||||
// log.Printf("👋 Sending EHLO with hostname: %s", hostname)
|
||||
if err = client.Hello(hostname); err != nil {
|
||||
// log.Printf("❌ EHLO failed: %v", err)
|
||||
return fmt.Errorf("EHLO failed: %v", err)
|
||||
}
|
||||
|
||||
// Authenticate if AUTH is supported and password is provided
|
||||
if ok, mechanisms := client.Extension("AUTH"); ok {
|
||||
// Suppress unused variable warning
|
||||
_ = mechanisms
|
||||
// log.Printf("🔑 AUTH extension supported with mechanisms: %s", mechanisms)
|
||||
|
||||
username := fromEmail
|
||||
|
||||
// log.Printf("🔐 Attempting authentication for user: %s", username)
|
||||
// log.Printf("🔑 Password provided: %t", password != "")
|
||||
|
||||
if password != "" {
|
||||
auth := smtp.PlainAuth("", username, password, strings.Split(addr, ":")[0])
|
||||
|
||||
if err := client.Auth(auth); err != nil {
|
||||
// log.Printf("❌ Authentication failed: %v", err)
|
||||
return fmt.Errorf("SMTP authentication failed: %v", err)
|
||||
} else {
|
||||
// log.Printf("✅ Authentication successful")
|
||||
}
|
||||
} else {
|
||||
// log.Printf("⚠️ No password provided, skipping authentication")
|
||||
return fmt.Errorf("SMTP password is required for authentication")
|
||||
}
|
||||
}
|
||||
|
||||
// Set sender and recipient
|
||||
// log.Printf("📤 Setting sender: %s", fromEmail)
|
||||
if err := client.Mail(fromEmail); err != nil {
|
||||
// log.Printf("❌ Failed to set sender: %v", err)
|
||||
return fmt.Errorf("failed to set sender: %v", err)
|
||||
}
|
||||
|
||||
// log.Printf("📥 Setting recipient: %s", toEmail)
|
||||
if err := client.Rcpt(toEmail); err != nil {
|
||||
// log.Printf("❌ Failed to set recipient: %v", err)
|
||||
return fmt.Errorf("failed to set recipient: %v", err)
|
||||
}
|
||||
|
||||
// Send message
|
||||
// log.Printf("📝 Sending message data...")
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
// log.Printf("❌ Failed to initiate data transfer: %v", err)
|
||||
return fmt.Errorf("failed to initiate data transfer: %v", err)
|
||||
}
|
||||
|
||||
_, err = w.Write([]byte(message))
|
||||
if err != nil {
|
||||
// log.Printf("❌ Failed to write message data: %v", err)
|
||||
return fmt.Errorf("failed to write message data: %v", err)
|
||||
}
|
||||
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
// log.Printf("❌ Failed to close data writer: %v", err)
|
||||
return fmt.Errorf("failed to close data writer: %v", err)
|
||||
}
|
||||
|
||||
// log.Printf("✅ Email sent successfully via SSL/TLS")
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
|
||||
package notification
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GoogleChatService handles Google Chat notifications
|
||||
type GoogleChatService struct{}
|
||||
|
||||
// NewGoogleChatService creates a new Google Chat notification service
|
||||
func NewGoogleChatService() *GoogleChatService {
|
||||
return &GoogleChatService{}
|
||||
}
|
||||
|
||||
// GoogleChatPayload represents the payload for Google Chat webhook
|
||||
type GoogleChatPayload struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// SendNotification sends a notification via Google Chat webhook
|
||||
func (gcs *GoogleChatService) SendNotification(config *AlertConfiguration, message string) error {
|
||||
// fmt.Printf("💬 [GOOGLE_CHAT] Attempting to send notification...\n")
|
||||
// fmt.Printf("💬 [GOOGLE_CHAT] Config - Webhook URL present: %v\n", config.GoogleChatWebhookURL != "")
|
||||
// fmt.Printf("💬 [GOOGLE_CHAT] Message: %s\n", message)
|
||||
|
||||
if config.GoogleChatWebhookURL == "" {
|
||||
return fmt.Errorf("google chat webhook URL is required")
|
||||
}
|
||||
|
||||
payload := GoogleChatPayload{
|
||||
Text: message,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
// fmt.Printf("❌ [GOOGLE_CHAT] JSON marshal error: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// fmt.Printf("💬 [GOOGLE_CHAT] Sending POST request to webhook...\n")
|
||||
resp, err := http.Post(config.GoogleChatWebhookURL, "application/json", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
// fmt.Printf("❌ [GOOGLE_CHAT] HTTP error: %v\n", err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// fmt.Printf("💬 [GOOGLE_CHAT] Response status: %d\n", resp.StatusCode)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
// fmt.Printf("❌ [GOOGLE_CHAT] Webhook error, status: %d\n", resp.StatusCode)
|
||||
return fmt.Errorf("google chat webhook error, status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// fmt.Printf("✅ [GOOGLE_CHAT] Message sent successfully!\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendServerNotification sends a server-specific notification via Google Chat
|
||||
func (gcs *GoogleChatService) SendServerNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) error {
|
||||
message := gcs.generateServerMessage(payload, template, resourceType)
|
||||
return gcs.SendNotification(config, message)
|
||||
}
|
||||
|
||||
// SendServiceNotification sends a service-specific notification via Google Chat
|
||||
func (gcs *GoogleChatService) SendServiceNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServiceNotificationTemplate) error {
|
||||
message := gcs.generateServiceMessage(payload, template)
|
||||
return gcs.SendNotification(config, message)
|
||||
}
|
||||
|
||||
// generateServerMessage creates a message for server notifications using server template
|
||||
func (gcs *GoogleChatService) generateServerMessage(payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) string {
|
||||
var templateMessage string
|
||||
|
||||
// Select appropriate template message based on status and resource type
|
||||
switch strings.ToLower(payload.Status) {
|
||||
case "down":
|
||||
templateMessage = template.DownMessage
|
||||
case "up":
|
||||
templateMessage = template.UpMessage
|
||||
case "warning":
|
||||
templateMessage = template.WarningMessage
|
||||
case "paused":
|
||||
templateMessage = template.PausedMessage
|
||||
default:
|
||||
// Handle resource-specific messages
|
||||
switch resourceType {
|
||||
case "cpu":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreCPUMessage
|
||||
} else {
|
||||
templateMessage = template.CPUMessage
|
||||
}
|
||||
case "ram", "memory":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreRAMMessage
|
||||
} else {
|
||||
templateMessage = template.RAMMessage
|
||||
}
|
||||
case "disk":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreDiskMessage
|
||||
} else {
|
||||
templateMessage = template.DiskMessage
|
||||
}
|
||||
case "network":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreNetworkMessage
|
||||
} else {
|
||||
templateMessage = template.NetworkMessage
|
||||
}
|
||||
case "cpu_temp", "cpu_temperature":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreCPUTempMessage
|
||||
} else {
|
||||
templateMessage = template.CPUTempMessage
|
||||
}
|
||||
case "disk_io":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreDiskIOMessage
|
||||
} else {
|
||||
templateMessage = template.DiskIOMessage
|
||||
}
|
||||
default:
|
||||
templateMessage = template.WarningMessage
|
||||
}
|
||||
}
|
||||
|
||||
// If no template message found, use a default
|
||||
if templateMessage == "" {
|
||||
templateMessage = gcs.generateDefaultServerMessage(payload, resourceType)
|
||||
}
|
||||
|
||||
return gcs.replacePlaceholders(templateMessage, payload)
|
||||
}
|
||||
|
||||
// generateServiceMessage creates a message for service notifications using service template
|
||||
func (gcs *GoogleChatService) generateServiceMessage(payload *NotificationPayload, template *ServiceNotificationTemplate) string {
|
||||
var templateMessage string
|
||||
|
||||
// Select appropriate template message based on status
|
||||
switch strings.ToLower(payload.Status) {
|
||||
case "up":
|
||||
templateMessage = template.UpMessage
|
||||
case "down":
|
||||
templateMessage = template.DownMessage
|
||||
case "maintenance":
|
||||
templateMessage = template.MaintenanceMessage
|
||||
case "incident":
|
||||
templateMessage = template.IncidentMessage
|
||||
case "resolved":
|
||||
templateMessage = template.ResolvedMessage
|
||||
case "warning":
|
||||
templateMessage = template.WarningMessage
|
||||
default:
|
||||
templateMessage = template.WarningMessage
|
||||
}
|
||||
|
||||
// If no template message found, use a default
|
||||
if templateMessage == "" {
|
||||
templateMessage = gcs.generateDefaultUptimeMessage(payload)
|
||||
}
|
||||
|
||||
return gcs.replacePlaceholders(templateMessage, payload)
|
||||
}
|
||||
|
||||
// replacePlaceholders replaces all placeholders in the message with actual values
|
||||
func (gcs *GoogleChatService) replacePlaceholders(message string, payload *NotificationPayload) string {
|
||||
// Replace basic placeholders
|
||||
message = strings.ReplaceAll(message, "${service_name}", payload.ServiceName)
|
||||
message = strings.ReplaceAll(message, "${status}", strings.ToUpper(payload.Status))
|
||||
message = strings.ReplaceAll(message, "${host}", gcs.safeString(payload.Host))
|
||||
message = strings.ReplaceAll(message, "${hostname}", gcs.safeString(payload.Hostname))
|
||||
|
||||
// Replace URL with fallback to host
|
||||
url := gcs.safeString(payload.URL)
|
||||
if url == "N/A" && payload.Host != "" {
|
||||
url = payload.Host
|
||||
}
|
||||
message = strings.ReplaceAll(message, "${url}", url)
|
||||
|
||||
// Replace domain
|
||||
message = strings.ReplaceAll(message, "${domain}", gcs.safeString(payload.Domain))
|
||||
|
||||
// Replace service type
|
||||
if payload.ServiceType != "" {
|
||||
message = strings.ReplaceAll(message, "${service_type}", strings.ToUpper(payload.ServiceType))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${service_type}", "N/A")
|
||||
}
|
||||
|
||||
// Replace region and agent info
|
||||
message = strings.ReplaceAll(message, "${region_name}", gcs.safeString(payload.RegionName))
|
||||
message = strings.ReplaceAll(message, "${agent_id}", gcs.safeString(payload.AgentID))
|
||||
|
||||
// Handle numeric fields safely
|
||||
if payload.Port > 0 {
|
||||
message = strings.ReplaceAll(message, "${port}", fmt.Sprintf("%d", payload.Port))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${port}", "N/A")
|
||||
}
|
||||
|
||||
if payload.ResponseTime > 0 {
|
||||
message = strings.ReplaceAll(message, "${response_time}", fmt.Sprintf("%dms", payload.ResponseTime))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${response_time}", "N/A")
|
||||
}
|
||||
|
||||
if payload.Uptime > 0 {
|
||||
message = strings.ReplaceAll(message, "${uptime}", fmt.Sprintf("%d%%", payload.Uptime))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${uptime}", "N/A")
|
||||
}
|
||||
|
||||
// Replace server monitoring fields
|
||||
message = strings.ReplaceAll(message, "${cpu_usage}", gcs.safeString(payload.CPUUsage))
|
||||
message = strings.ReplaceAll(message, "${ram_usage}", gcs.safeString(payload.RAMUsage))
|
||||
message = strings.ReplaceAll(message, "${disk_usage}", gcs.safeString(payload.DiskUsage))
|
||||
message = strings.ReplaceAll(message, "${network_usage}", gcs.safeString(payload.NetworkUsage))
|
||||
message = strings.ReplaceAll(message, "${cpu_temp}", gcs.safeString(payload.CPUTemp))
|
||||
message = strings.ReplaceAll(message, "${disk_io}", gcs.safeString(payload.DiskIO))
|
||||
message = strings.ReplaceAll(message, "${threshold}", gcs.safeString(payload.Threshold))
|
||||
|
||||
// Replace error message - important for uptime services
|
||||
message = strings.ReplaceAll(message, "${error_message}", gcs.safeString(payload.ErrorMessage))
|
||||
message = strings.ReplaceAll(message, "${error}", gcs.safeString(payload.ErrorMessage))
|
||||
|
||||
// Replace time placeholders
|
||||
message = strings.ReplaceAll(message, "${time}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||
message = strings.ReplaceAll(message, "${timestamp}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
// safeString returns the string value or "N/A" if empty
|
||||
func (gcs *GoogleChatService) safeString(value string) string {
|
||||
if value == "" {
|
||||
return "N/A"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// generateDefaultUptimeMessage creates a default uptime message with proper formatting
|
||||
func (gcs *GoogleChatService) generateDefaultUptimeMessage(payload *NotificationPayload) string {
|
||||
// Status emoji mapping for Google Chat
|
||||
statusEmoji := "🔵"
|
||||
switch strings.ToLower(payload.Status) {
|
||||
case "up":
|
||||
statusEmoji = "🟢"
|
||||
case "down":
|
||||
statusEmoji = "🔴"
|
||||
case "warning":
|
||||
statusEmoji = "🟡"
|
||||
case "maintenance", "paused":
|
||||
statusEmoji = "🟠"
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("%s Service %s is %s.", statusEmoji, payload.ServiceName, strings.ToUpper(payload.Status))
|
||||
|
||||
// Build formatted details
|
||||
details := []string{}
|
||||
|
||||
// Add URL or host
|
||||
if payload.URL != "" {
|
||||
details = append(details, fmt.Sprintf(" - Host URL: %s", payload.URL))
|
||||
} else if payload.Host != "" {
|
||||
details = append(details, fmt.Sprintf(" - Host: %s", payload.Host))
|
||||
}
|
||||
|
||||
// Add service type
|
||||
if payload.ServiceType != "" {
|
||||
details = append(details, fmt.Sprintf(" - Type: %s", strings.ToUpper(payload.ServiceType)))
|
||||
}
|
||||
|
||||
// Add port if available
|
||||
if payload.Port > 0 {
|
||||
details = append(details, fmt.Sprintf(" - Port: %d", payload.Port))
|
||||
}
|
||||
|
||||
// Add domain if available
|
||||
if payload.Domain != "" {
|
||||
details = append(details, fmt.Sprintf(" - Domain: %s", payload.Domain))
|
||||
}
|
||||
|
||||
// Add response time
|
||||
if payload.ResponseTime > 0 {
|
||||
details = append(details, fmt.Sprintf(" - Response time: %dms", payload.ResponseTime))
|
||||
} else {
|
||||
details = append(details, " - Response time: N/A")
|
||||
}
|
||||
|
||||
// Add region info
|
||||
if payload.RegionName != "" {
|
||||
details = append(details, fmt.Sprintf(" - Region: %s", payload.RegionName))
|
||||
}
|
||||
|
||||
// Add agent info
|
||||
if payload.AgentID != "" {
|
||||
details = append(details, fmt.Sprintf(" - Agent: %s", payload.AgentID))
|
||||
}
|
||||
|
||||
// Add uptime if available
|
||||
if payload.Uptime > 0 {
|
||||
details = append(details, fmt.Sprintf(" - Uptime: %d%%", payload.Uptime))
|
||||
}
|
||||
|
||||
// Add timestamp
|
||||
details = append(details, fmt.Sprintf(" - Time: %s", payload.Timestamp.Format("2006-01-02 15:04:05")))
|
||||
|
||||
// Combine message with details
|
||||
if len(details) > 0 {
|
||||
message += "\n" + strings.Join(details, "\n")
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
// generateDefaultServerMessage creates a default server message
|
||||
func (gcs *GoogleChatService) generateDefaultServerMessage(payload *NotificationPayload, resourceType string) string {
|
||||
statusEmoji := "🔵"
|
||||
switch strings.ToLower(payload.Status) {
|
||||
case "up":
|
||||
statusEmoji = "🟢"
|
||||
case "down":
|
||||
statusEmoji = "🔴"
|
||||
case "warning":
|
||||
statusEmoji = "🟡"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s🖥️ Server %s (%s) status: %s", statusEmoji, payload.ServiceName, payload.Hostname, strings.ToUpper(payload.Status))
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
|
||||
package notification
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"service-operation/pocketbase"
|
||||
"service-operation/types"
|
||||
)
|
||||
|
||||
// ServiceNotifier provides an easy interface to send notifications for service events
|
||||
type ServiceNotifier struct {
|
||||
manager *NotificationManager
|
||||
}
|
||||
|
||||
// NewServiceNotifier creates a new service notifier
|
||||
func NewServiceNotifier(pbClient *pocketbase.PocketBaseClient) *ServiceNotifier {
|
||||
return &ServiceNotifier{
|
||||
manager: NewNotificationManager(pbClient),
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyServiceStatus sends a notification for service status change
|
||||
func (sn *ServiceNotifier) NotifyServiceStatus(service pocketbase.Service, result *types.OperationResult) {
|
||||
// Check if service has notification configured
|
||||
if service.NotificationID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Create notification payload
|
||||
payload := &NotificationPayload{
|
||||
ServiceName: service.Name,
|
||||
Status: service.Status,
|
||||
Host: service.Host,
|
||||
Port: service.Port,
|
||||
ServiceType: service.ServiceType,
|
||||
ResponseTime: result.ResponseTime.Milliseconds(),
|
||||
Timestamp: time.Now(),
|
||||
ErrorMessage: result.Error,
|
||||
}
|
||||
|
||||
// Send notification
|
||||
if err := sn.manager.SendServiceNotification(payload, service.NotificationID, service.TemplateID); err != nil {
|
||||
log.Printf("Failed to send notification for service %s: %v", service.Name, err)
|
||||
} else {
|
||||
log.Printf("Notification sent successfully for service %s", service.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyCustom sends a custom notification
|
||||
func (sn *ServiceNotifier) NotifyCustom(notificationID, templateID, serviceName, status, message string) error {
|
||||
payload := &NotificationPayload{
|
||||
ServiceName: serviceName,
|
||||
Status: status,
|
||||
Message: message,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
return sn.manager.SendServiceNotification(payload, notificationID, templateID)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
|
||||
package notification
|
||||
|
||||
import (
|
||||
// "log"
|
||||
|
||||
"service-operation/pocketbase"
|
||||
)
|
||||
|
||||
// NotificationManager handles all notification operations
|
||||
type NotificationManager struct {
|
||||
pbClient *pocketbase.PocketBaseClient
|
||||
services map[string]NotificationService
|
||||
serverManager *ServerNotificationManager
|
||||
uptimeManager *UptimeNotificationManager
|
||||
sslManager *SSLNotificationManager
|
||||
}
|
||||
|
||||
// NewNotificationManager creates a new notification manager
|
||||
func NewNotificationManager(pbClient *pocketbase.PocketBaseClient) *NotificationManager {
|
||||
// Initialize notification services
|
||||
services := make(map[string]NotificationService)
|
||||
|
||||
// log.Printf("🔧 Initializing notification services...")
|
||||
services["telegram"] = NewTelegramService()
|
||||
services["signal"] = NewSignalService()
|
||||
services["discord"] = NewDiscordService()
|
||||
services["slack"] = NewSlackService()
|
||||
services["google_chat"] = NewGoogleChatService()
|
||||
services["email"] = NewEmailService()
|
||||
services["webhook"] = NewWebhookService()
|
||||
|
||||
// log.Printf("✅ Notification services initialized: %v", getKeys(services))
|
||||
|
||||
// Create specialized managers
|
||||
serverManager := NewServerNotificationManager(pbClient, services)
|
||||
uptimeManager := NewUptimeNotificationManager(pbClient, services)
|
||||
sslManager := NewSSLNotificationManager(pbClient, services)
|
||||
|
||||
return &NotificationManager{
|
||||
pbClient: pbClient,
|
||||
services: services,
|
||||
serverManager: serverManager,
|
||||
uptimeManager: uptimeManager,
|
||||
sslManager: sslManager,
|
||||
}
|
||||
}
|
||||
|
||||
// SendServiceNotification sends notification for a service based on its configuration
|
||||
func (nm *NotificationManager) SendServiceNotification(payload *NotificationPayload, notificationID, templateID string) error {
|
||||
return nm.serverManager.SendServiceNotification(payload, notificationID, templateID)
|
||||
}
|
||||
|
||||
// SendResourceNotification sends notification for specific resource alerts (CPU, RAM, Disk, etc.)
|
||||
func (nm *NotificationManager) SendResourceNotification(payload *NotificationPayload, notificationID, templateID, resourceType string) error {
|
||||
return nm.serverManager.SendResourceNotification(payload, notificationID, templateID, resourceType)
|
||||
}
|
||||
|
||||
// SendUptimeServiceNotification sends notification for uptime services using service templates
|
||||
func (nm *NotificationManager) SendUptimeServiceNotification(payload *NotificationPayload, notificationID, templateID string) error {
|
||||
return nm.uptimeManager.SendUptimeServiceNotification(payload, notificationID, templateID)
|
||||
}
|
||||
|
||||
// SendSSLNotification sends notification for SSL certificates using SSL templates
|
||||
func (nm *NotificationManager) SendSSLNotification(payload *NotificationPayload, notificationID, templateID string) error {
|
||||
return nm.sslManager.SendSSLNotification(payload, notificationID, templateID)
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
|
||||
package notification
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"service-operation/pocketbase"
|
||||
)
|
||||
|
||||
// ServerNotificationManager handles server-specific notifications
|
||||
type ServerNotificationManager struct {
|
||||
pbClient *pocketbase.PocketBaseClient
|
||||
services map[string]NotificationService
|
||||
}
|
||||
|
||||
// NewServerNotificationManager creates a new server notification manager
|
||||
func NewServerNotificationManager(pbClient *pocketbase.PocketBaseClient, services map[string]NotificationService) *ServerNotificationManager {
|
||||
return &ServerNotificationManager{
|
||||
pbClient: pbClient,
|
||||
services: services,
|
||||
}
|
||||
}
|
||||
|
||||
// SendServiceNotification sends notification for a service based on its configuration
|
||||
func (snm *ServerNotificationManager) SendServiceNotification(payload *NotificationPayload, notificationID, templateID string) error {
|
||||
// log.Printf("📨 SendServiceNotification called with notification_id: %s, template_id: %s", notificationID, templateID)
|
||||
// log.Printf("📨 Payload: %+v", payload)
|
||||
|
||||
if notificationID == "" {
|
||||
// log.Printf("❌ Notification ID is required but was empty")
|
||||
return fmt.Errorf("notification ID is required")
|
||||
}
|
||||
|
||||
// Parse multiple notification IDs
|
||||
notificationIDs := parseNotificationIDs(notificationID)
|
||||
if len(notificationIDs) == 0 {
|
||||
// log.Printf("❌ No valid notification IDs found")
|
||||
return fmt.Errorf("no valid notification IDs found")
|
||||
}
|
||||
|
||||
var errors []string
|
||||
successCount := 0
|
||||
|
||||
// Send notification to each channel
|
||||
for _, id := range notificationIDs {
|
||||
// log.Printf("📤 Processing notification ID: %s", id)
|
||||
|
||||
// Check if notification is enabled
|
||||
if !isNotificationEnabled(snm.pbClient, id) {
|
||||
// log.Printf("⚠️ Notification %s is disabled, skipping", id)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get alert configuration
|
||||
alertConfig, err := getAlertConfiguration(snm.pbClient, id)
|
||||
if err != nil {
|
||||
// log.Printf("❌ Failed to get alert configuration for %s: %v", id, err)
|
||||
errors = append(errors, fmt.Sprintf("failed to get config for %s: %v", id, err))
|
||||
continue
|
||||
}
|
||||
|
||||
// Get template if provided
|
||||
var template *ServerNotificationTemplate
|
||||
if templateID != "" {
|
||||
template, err = getNotificationTemplate(snm.pbClient, templateID)
|
||||
if err != nil {
|
||||
// log.Printf("⚠️ Warning: failed to get template %s for notification %s: %v", templateID, id, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate message from template or use default
|
||||
message := snm.generateMessage(payload, template)
|
||||
// log.Printf("📝 Generated message for %s: %s", id, message)
|
||||
|
||||
// Send notification using appropriate service
|
||||
service, exists := snm.services[alertConfig.NotificationType]
|
||||
if !exists {
|
||||
// log.Printf("❌ Unsupported notification type for %s: %s", id, alertConfig.NotificationType)
|
||||
errors = append(errors, fmt.Sprintf("unsupported notification type for %s: %s", id, alertConfig.NotificationType))
|
||||
continue
|
||||
}
|
||||
|
||||
err = service.SendNotification(alertConfig, message)
|
||||
if err != nil {
|
||||
// log.Printf("❌ Failed to send notification via %s for %s: %v", alertConfig.NotificationType, id, err)
|
||||
errors = append(errors, fmt.Sprintf("failed to send via %s for %s: %v", alertConfig.NotificationType, id, err))
|
||||
} else {
|
||||
// log.Printf("✅ Successfully sent notification via %s for %s", alertConfig.NotificationType, id)
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Report results
|
||||
if successCount > 0 {
|
||||
// log.Printf("✅ Successfully sent %d out of %d notifications", successCount, len(notificationIDs))
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
// log.Printf("❌ Errors occurred: %v", errors)
|
||||
if successCount == 0 {
|
||||
return fmt.Errorf("all notifications failed: %v", errors)
|
||||
}
|
||||
// If some succeeded, just log errors but don't return error
|
||||
// log.Printf("⚠️ Some notifications failed but %d succeeded", successCount)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendResourceNotification sends notification for specific resource alerts (CPU, RAM, Disk, etc.)
|
||||
func (snm *ServerNotificationManager) SendResourceNotification(payload *NotificationPayload, notificationID, templateID, resourceType string) error {
|
||||
// log.Printf("📨 SendResourceNotification called for resource: %s", resourceType)
|
||||
// log.Printf("📨 Notification ID: %s, Template ID: %s", notificationID, templateID)
|
||||
// log.Printf("📨 Payload: %+v", payload)
|
||||
|
||||
if notificationID == "" {
|
||||
// log.Printf("❌ Notification ID is required but was empty")
|
||||
return fmt.Errorf("notification ID is required")
|
||||
}
|
||||
|
||||
// Parse multiple notification IDs
|
||||
notificationIDs := parseNotificationIDs(notificationID)
|
||||
if len(notificationIDs) == 0 {
|
||||
// log.Printf("❌ No valid notification IDs found")
|
||||
return fmt.Errorf("no valid notification IDs found")
|
||||
}
|
||||
|
||||
var errors []string
|
||||
successCount := 0
|
||||
|
||||
// Send notification to each channel
|
||||
for _, id := range notificationIDs {
|
||||
// log.Printf("📤 Processing resource notification ID: %s", id)
|
||||
|
||||
// Check if notification is enabled
|
||||
if !isNotificationEnabled(snm.pbClient, id) {
|
||||
// log.Printf("⚠️ Notification %s is disabled, skipping", id)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get alert configuration
|
||||
alertConfig, err := getAlertConfiguration(snm.pbClient, id)
|
||||
if err != nil {
|
||||
// log.Printf("❌ Failed to get alert configuration for %s: %v", id, err)
|
||||
errors = append(errors, fmt.Sprintf("failed to get config for %s: %v", id, err))
|
||||
continue
|
||||
}
|
||||
|
||||
// Get template if provided
|
||||
var template *ServerNotificationTemplate
|
||||
if templateID != "" {
|
||||
template, err = getNotificationTemplate(snm.pbClient, templateID)
|
||||
if err != nil {
|
||||
// log.Printf("⚠️ Warning: failed to get template %s for notification %s: %v", templateID, id, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate resource-specific message
|
||||
message := snm.generateResourceMessage(payload, template, resourceType)
|
||||
// log.Printf("📝 Generated resource message for %s: %s", id, message)
|
||||
|
||||
// Send notification
|
||||
service, exists := snm.services[alertConfig.NotificationType]
|
||||
if !exists {
|
||||
// log.Printf("❌ Unsupported notification type for %s: %s", id, alertConfig.NotificationType)
|
||||
errors = append(errors, fmt.Sprintf("unsupported notification type for %s: %s", id, alertConfig.NotificationType))
|
||||
continue
|
||||
}
|
||||
|
||||
err = service.SendNotification(alertConfig, message)
|
||||
if err != nil {
|
||||
// log.Printf("❌ Failed to send resource notification via %s for %s: %v", alertConfig.NotificationType, id, err)
|
||||
errors = append(errors, fmt.Sprintf("failed to send via %s for %s: %v", alertConfig.NotificationType, id, err))
|
||||
} else {
|
||||
// log.Printf("✅ Successfully sent resource notification via %s for %s", alertConfig.NotificationType, id)
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Report results
|
||||
if successCount > 0 {
|
||||
// log.Printf("✅ Successfully sent %d out of %d resource notifications", successCount, len(notificationIDs))
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
// log.Printf("❌ Resource notification errors occurred: %v", errors)
|
||||
if successCount == 0 {
|
||||
return fmt.Errorf("all resource notifications failed: %v", errors)
|
||||
}
|
||||
// If some succeeded, just log errors but don't return error
|
||||
// log.Printf("⚠️ Some resource notifications failed but %d succeeded", successCount)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateResourceMessage creates notification message for specific resource alerts
|
||||
func (snm *ServerNotificationManager) generateResourceMessage(payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) string {
|
||||
var baseMessage string
|
||||
|
||||
// Select appropriate resource message from template based on status
|
||||
if template != nil {
|
||||
// log.Printf("🔧 Using template for resource type: %s with status: %s", resourceType, payload.Status)
|
||||
|
||||
// Check if this is a recovery/restore notification
|
||||
if payload.Status == "up" {
|
||||
// Use restore messages for recovery notifications
|
||||
switch strings.ToLower(resourceType) {
|
||||
case "cpu":
|
||||
baseMessage = template.RestoreCPUMessage
|
||||
case "ram", "memory":
|
||||
baseMessage = template.RestoreRAMMessage
|
||||
case "disk":
|
||||
baseMessage = template.RestoreDiskMessage
|
||||
case "network":
|
||||
baseMessage = template.RestoreNetworkMessage
|
||||
case "cpu_temp", "cpu_temperature":
|
||||
baseMessage = template.RestoreCPUTempMessage
|
||||
case "disk_io":
|
||||
baseMessage = template.RestoreDiskIOMessage
|
||||
default:
|
||||
baseMessage = template.UpMessage // fallback to general up message
|
||||
}
|
||||
} else {
|
||||
// Use regular alert messages for warning/down notifications
|
||||
switch strings.ToLower(resourceType) {
|
||||
case "cpu":
|
||||
baseMessage = template.CPUMessage
|
||||
case "ram", "memory":
|
||||
baseMessage = template.RAMMessage
|
||||
case "disk":
|
||||
baseMessage = template.DiskMessage
|
||||
case "network":
|
||||
baseMessage = template.NetworkMessage
|
||||
case "cpu_temp", "cpu_temperature":
|
||||
baseMessage = template.CPUTempMessage
|
||||
case "disk_io":
|
||||
baseMessage = template.DiskIOMessage
|
||||
default:
|
||||
baseMessage = template.WarningMessage // fallback to warning message
|
||||
}
|
||||
}
|
||||
|
||||
// log.Printf("📝 Template message selected for %s (status: %s): %s", resourceType, payload.Status, baseMessage)
|
||||
}
|
||||
|
||||
// If no template or empty message, use default
|
||||
if baseMessage == "" {
|
||||
// log.Printf("🔧 Using default message for resource %s", resourceType)
|
||||
baseMessage = snm.getDefaultResourceMessage(payload, resourceType)
|
||||
}
|
||||
|
||||
// Replace placeholders with actual values
|
||||
message := snm.replacePlaceholders(baseMessage, payload)
|
||||
|
||||
// log.Printf("📝 Final resource message after placeholder replacement: %s", message)
|
||||
return message
|
||||
}
|
||||
|
||||
// getDefaultResourceMessage provides default messages for resource alerts
|
||||
func (snm *ServerNotificationManager) getDefaultResourceMessage(payload *NotificationPayload, resourceType string) string {
|
||||
statusEmoji := "⚠️"
|
||||
statusText := "Alert"
|
||||
|
||||
if payload.Status == "up" {
|
||||
statusEmoji = "✅"
|
||||
statusText = "Recovery"
|
||||
} else if payload.Status == "down" {
|
||||
statusEmoji = "❌"
|
||||
statusText = "Critical"
|
||||
}
|
||||
|
||||
switch strings.ToLower(resourceType) {
|
||||
case "cpu":
|
||||
if payload.Status == "up" {
|
||||
return fmt.Sprintf("%s CPU %s: Server %s CPU usage has returned to normal: %s", statusEmoji, statusText, payload.ServiceName, payload.CPUUsage)
|
||||
}
|
||||
return fmt.Sprintf("%s CPU %s: Server %s CPU usage is %s", statusEmoji, statusText, payload.ServiceName, payload.CPUUsage)
|
||||
case "ram", "memory":
|
||||
if payload.Status == "up" {
|
||||
return fmt.Sprintf("%s RAM %s: Server %s RAM usage has returned to normal: %s", statusEmoji, statusText, payload.ServiceName, payload.RAMUsage)
|
||||
}
|
||||
return fmt.Sprintf("%s RAM %s: Server %s RAM usage is %s", statusEmoji, statusText, payload.ServiceName, payload.RAMUsage)
|
||||
case "disk":
|
||||
if payload.Status == "up" {
|
||||
return fmt.Sprintf("%s Disk %s: Server %s disk usage has returned to normal: %s", statusEmoji, statusText, payload.ServiceName, payload.DiskUsage)
|
||||
}
|
||||
return fmt.Sprintf("%s Disk %s: Server %s disk usage is %s", statusEmoji, statusText, payload.ServiceName, payload.DiskUsage)
|
||||
case "network":
|
||||
if payload.Status == "up" {
|
||||
return fmt.Sprintf("%s Network %s: Server %s network usage has returned to normal: %s", statusEmoji, statusText, payload.ServiceName, payload.NetworkUsage)
|
||||
}
|
||||
return fmt.Sprintf("%s Network %s: Server %s network usage is %s", statusEmoji, statusText, payload.ServiceName, payload.NetworkUsage)
|
||||
case "cpu_temp", "cpu_temperature":
|
||||
if payload.Status == "up" {
|
||||
return fmt.Sprintf("%s CPU Temperature %s: Server %s CPU temperature has returned to normal: %s", statusEmoji, statusText, payload.ServiceName, payload.CPUTemp)
|
||||
}
|
||||
return fmt.Sprintf("%s CPU Temperature %s: Server %s CPU temperature is %s", statusEmoji, statusText, payload.ServiceName, payload.CPUTemp)
|
||||
case "disk_io":
|
||||
if payload.Status == "up" {
|
||||
return fmt.Sprintf("%s Disk I/O %s: Server %s disk I/O has returned to normal: %s", statusEmoji, statusText, payload.ServiceName, payload.DiskIO)
|
||||
}
|
||||
return fmt.Sprintf("%s Disk I/O %s: Server %s disk I/O is %s", statusEmoji, statusText, payload.ServiceName, payload.DiskIO)
|
||||
default:
|
||||
if payload.Status == "up" {
|
||||
return fmt.Sprintf("%s Resource %s: Server %s %s has recovered", statusEmoji, statusText, payload.ServiceName, payload.Message)
|
||||
}
|
||||
return fmt.Sprintf("%s Resource %s: Server %s %s", statusEmoji, statusText, payload.ServiceName, payload.Message)
|
||||
}
|
||||
}
|
||||
|
||||
// generateMessage creates notification message from template or default
|
||||
func (snm *ServerNotificationManager) generateMessage(payload *NotificationPayload, template *ServerNotificationTemplate) string {
|
||||
var baseMessage string
|
||||
|
||||
// Select appropriate message based on status and template
|
||||
if template != nil {
|
||||
// log.Printf("🔧 Using template for status: %s", strings.ToLower(payload.Status))
|
||||
switch strings.ToLower(payload.Status) {
|
||||
case "up":
|
||||
baseMessage = template.UpMessage
|
||||
case "down":
|
||||
baseMessage = template.DownMessage
|
||||
case "warning":
|
||||
baseMessage = template.WarningMessage
|
||||
case "paused":
|
||||
baseMessage = template.PausedMessage
|
||||
default:
|
||||
baseMessage = template.UpMessage // fallback
|
||||
}
|
||||
// log.Printf("📝 Template message selected: %s", baseMessage)
|
||||
}
|
||||
|
||||
// If no template or empty message, use default
|
||||
if baseMessage == "" {
|
||||
// log.Printf("🔧 Using default message (no template or empty template message)")
|
||||
baseMessage = snm.getDefaultMessage(payload)
|
||||
}
|
||||
|
||||
// Replace placeholders with actual values
|
||||
message := snm.replacePlaceholders(baseMessage, payload)
|
||||
|
||||
// log.Printf("📝 Final message after placeholder replacement: %s", message)
|
||||
return message
|
||||
}
|
||||
|
||||
// replacePlaceholders replaces all placeholders in the message with actual values
|
||||
func (snm *ServerNotificationManager) replacePlaceholders(message string, payload *NotificationPayload) string {
|
||||
// Service/Server name placeholders
|
||||
message = strings.ReplaceAll(message, "${service_name}", payload.ServiceName)
|
||||
message = strings.ReplaceAll(message, "${server_name}", payload.ServiceName) // For server notifications
|
||||
|
||||
// Status placeholder
|
||||
message = strings.ReplaceAll(message, "${status}", strings.ToUpper(payload.Status))
|
||||
|
||||
// Host/IP placeholders
|
||||
message = strings.ReplaceAll(message, "${host}", payload.Host)
|
||||
message = strings.ReplaceAll(message, "${ip}", payload.Host) // Alternative placeholder
|
||||
message = strings.ReplaceAll(message, "${ip_address}", payload.Host) // New server monitoring placeholder
|
||||
|
||||
// Hostname placeholder
|
||||
message = strings.ReplaceAll(message, "${hostname}", payload.Hostname)
|
||||
|
||||
// Port placeholder
|
||||
if payload.Port > 0 {
|
||||
message = strings.ReplaceAll(message, "${port}", fmt.Sprintf("%d", payload.Port))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${port}", "N/A")
|
||||
}
|
||||
|
||||
// Response time placeholder
|
||||
if payload.ResponseTime > 0 {
|
||||
message = strings.ReplaceAll(message, "${response_time}", fmt.Sprintf("%dms", payload.ResponseTime))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${response_time}", "N/A")
|
||||
}
|
||||
|
||||
// Timestamp placeholders
|
||||
message = strings.ReplaceAll(message, "${timestamp}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||
message = strings.ReplaceAll(message, "${time}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||
message = strings.ReplaceAll(message, "${date}", payload.Timestamp.Format("2006-01-02"))
|
||||
|
||||
// Service type placeholder
|
||||
message = strings.ReplaceAll(message, "${service_type}", payload.ServiceType)
|
||||
|
||||
// Error message placeholder
|
||||
if payload.ErrorMessage != "" {
|
||||
message = strings.ReplaceAll(message, "${error}", payload.ErrorMessage)
|
||||
message = strings.ReplaceAll(message, "${error_message}", payload.ErrorMessage)
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${error}", "")
|
||||
message = strings.ReplaceAll(message, "${error_message}", "")
|
||||
}
|
||||
|
||||
// Custom message placeholder
|
||||
if payload.Message != "" {
|
||||
message = strings.ReplaceAll(message, "${message}", payload.Message)
|
||||
}
|
||||
|
||||
// Server monitoring specific placeholders
|
||||
if payload.CPUUsage != "" {
|
||||
message = strings.ReplaceAll(message, "${cpu_usage}", payload.CPUUsage)
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${cpu_usage}", "N/A")
|
||||
}
|
||||
|
||||
if payload.RAMUsage != "" {
|
||||
message = strings.ReplaceAll(message, "${ram_usage}", payload.RAMUsage)
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${ram_usage}", "N/A")
|
||||
}
|
||||
|
||||
if payload.DiskUsage != "" {
|
||||
message = strings.ReplaceAll(message, "${disk_usage}", payload.DiskUsage)
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${disk_usage}", "N/A")
|
||||
}
|
||||
|
||||
if payload.NetworkUsage != "" {
|
||||
message = strings.ReplaceAll(message, "${network_usage}", payload.NetworkUsage)
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${network_usage}", "N/A")
|
||||
}
|
||||
|
||||
if payload.CPUTemp != "" {
|
||||
message = strings.ReplaceAll(message, "${cpu_temp}", payload.CPUTemp)
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${cpu_temp}", "N/A")
|
||||
}
|
||||
|
||||
if payload.DiskIO != "" {
|
||||
message = strings.ReplaceAll(message, "${disk_io}", payload.DiskIO)
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${disk_io}", "N/A")
|
||||
}
|
||||
|
||||
if payload.Threshold != "" {
|
||||
message = strings.ReplaceAll(message, "${threshold}", payload.Threshold)
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${threshold}", "N/A")
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
// getDefaultMessage provides a default notification message
|
||||
func (snm *ServerNotificationManager) getDefaultMessage(payload *NotificationPayload) string {
|
||||
statusEmoji := "✅"
|
||||
if payload.Status == "down" {
|
||||
statusEmoji = "❌"
|
||||
} else if payload.Status == "warning" {
|
||||
statusEmoji = "⚠️"
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("%s Server Alert\n\n", statusEmoji)
|
||||
message += fmt.Sprintf("Server: %s\n", payload.ServiceName)
|
||||
message += fmt.Sprintf("Status: %s\n", strings.ToUpper(payload.Status))
|
||||
message += fmt.Sprintf("IP Address: %s\n", payload.Host)
|
||||
|
||||
if payload.CPUUsage != "" {
|
||||
message += fmt.Sprintf("CPU Usage: %s\n", payload.CPUUsage)
|
||||
}
|
||||
|
||||
if payload.RAMUsage != "" {
|
||||
message += fmt.Sprintf("RAM Usage: %s\n", payload.RAMUsage)
|
||||
}
|
||||
|
||||
if payload.DiskUsage != "" {
|
||||
message += fmt.Sprintf("Disk Usage: %s\n", payload.DiskUsage)
|
||||
}
|
||||
|
||||
message += fmt.Sprintf("Time: %s\n", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||
|
||||
if payload.ErrorMessage != "" {
|
||||
message += fmt.Sprintf("Error: %s\n", payload.ErrorMessage)
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
|
||||
package notification
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"service-operation/pocketbase"
|
||||
)
|
||||
|
||||
// parseNotificationIDs parses comma-separated notification IDs
|
||||
func parseNotificationIDs(notificationID string) []string {
|
||||
if notificationID == "" {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// Split by comma and trim whitespace
|
||||
ids := strings.Split(notificationID, ",")
|
||||
var cleanIDs []string
|
||||
for _, id := range ids {
|
||||
cleanID := strings.TrimSpace(id)
|
||||
if cleanID != "" {
|
||||
cleanIDs = append(cleanIDs, cleanID)
|
||||
}
|
||||
}
|
||||
|
||||
//log.Printf("📋 Parsed notification IDs: %v", cleanIDs)
|
||||
return cleanIDs
|
||||
}
|
||||
|
||||
// isNotificationEnabled checks if the notification is enabled
|
||||
func isNotificationEnabled(pbClient *pocketbase.PocketBaseClient, notificationID string) bool {
|
||||
config, err := getAlertConfiguration(pbClient, notificationID)
|
||||
if err != nil {
|
||||
//log.Printf("❌ Error getting alert configuration for enabled check: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
enabled, err := strconv.ParseBool(config.Enabled)
|
||||
if err != nil {
|
||||
//log.Printf("❌ Error parsing enabled field: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
//log.Printf("ℹ️ Notification %s enabled status: %t", notificationID, enabled)
|
||||
return enabled
|
||||
}
|
||||
|
||||
// getAlertConfiguration fetches alert configuration from PocketBase
|
||||
func getAlertConfiguration(pbClient *pocketbase.PocketBaseClient, notificationID string) (*AlertConfiguration, error) {
|
||||
url := fmt.Sprintf("%s/api/collections/alert_configurations/records/%s", pbClient.GetBaseURL(), notificationID)
|
||||
//log.Printf("🌐 Fetching alert configuration from: %s", url)
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
//log.Printf("❌ HTTP error fetching alert configuration: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Printf("❌ Failed to fetch alert configuration, status: %d", resp.StatusCode)
|
||||
return nil, fmt.Errorf("failed to fetch alert configuration, status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var config AlertConfiguration
|
||||
if err := json.NewDecoder(resp.Body).Decode(&config); err != nil {
|
||||
//log.Printf("❌ Error decoding alert configuration JSON: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//log.Printf("✅ Successfully fetched alert configuration: %+v", config)
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// getNotificationTemplate fetches server notification template from PocketBase
|
||||
func getNotificationTemplate(pbClient *pocketbase.PocketBaseClient, templateID string) (*ServerNotificationTemplate, error) {
|
||||
url := fmt.Sprintf("%s/api/collections/server_notification_templates/records/%s", pbClient.GetBaseURL(), templateID)
|
||||
//log.Printf("🌐 Fetching notification template from: %s", url)
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
//log.Printf("❌ HTTP error fetching notification template: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Printf("❌ Failed to fetch notification template, status: %d", resp.StatusCode)
|
||||
return nil, fmt.Errorf("failed to fetch notification template, status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var template ServerNotificationTemplate
|
||||
if err := json.NewDecoder(resp.Body).Decode(&template); err != nil {
|
||||
//log.Printf("❌ Error decoding notification template JSON: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//log.Printf("✅ Successfully fetched notification template: %+v", template)
|
||||
return &template, nil
|
||||
}
|
||||
|
||||
// getServiceNotificationTemplate fetches service notification template from PocketBase
|
||||
func getServiceNotificationTemplate(pbClient *pocketbase.PocketBaseClient, templateID string) (*ServiceNotificationTemplate, error) {
|
||||
url := fmt.Sprintf("%s/api/collections/service_notification_templates/records/%s", pbClient.GetBaseURL(), templateID)
|
||||
//log.Printf("🌐 Fetching service notification template from: %s", url)
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
//log.Printf("❌ HTTP error fetching service notification template: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
//log.Printf("❌ Failed to fetch service notification template, status: %d", resp.StatusCode)
|
||||
return nil, fmt.Errorf("failed to fetch service notification template, status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var template ServiceNotificationTemplate
|
||||
if err := json.NewDecoder(resp.Body).Decode(&template); err != nil {
|
||||
//log.Printf("❌ Error decoding service notification template JSON: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//log.Printf("✅ Successfully fetched service notification template: %+v", template)
|
||||
return &template, nil
|
||||
}
|
||||
|
||||
// Helper function to get map keys
|
||||
func getKeys(m map[string]NotificationService) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
|
||||
package notification
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SignalService handles Signal notifications
|
||||
type SignalService struct{}
|
||||
|
||||
// NewSignalService creates a new Signal notification service
|
||||
func NewSignalService() *SignalService {
|
||||
return &SignalService{}
|
||||
}
|
||||
|
||||
// SignalPayload represents the payload for Signal REST API
|
||||
type SignalPayload struct {
|
||||
Number string `json:"number"`
|
||||
Recipients []string `json:"recipients"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// SendNotification sends a notification via Signal REST API
|
||||
func (ss *SignalService) SendNotification(config *AlertConfiguration, message string) error {
|
||||
// fmt.Printf("📱 [SIGNAL] Attempting to send notification...\n")
|
||||
// fmt.Printf("📱 [SIGNAL] Config - Phone Number: %s, API Endpoint: %s\n", config.SignalNumber, config.SignalAPIEndpoint)
|
||||
// fmt.Printf("📱 [SIGNAL] Message: %s\n", message)
|
||||
|
||||
if config.SignalNumber == "" {
|
||||
return fmt.Errorf("signal phone number is required")
|
||||
}
|
||||
|
||||
if config.SignalAPIEndpoint == "" {
|
||||
return fmt.Errorf("signal API endpoint is required")
|
||||
}
|
||||
|
||||
// Use the configured endpoint directly (it should already include the full path like /v2/send)
|
||||
apiURL := config.SignalAPIEndpoint
|
||||
// fmt.Printf("📱 [SIGNAL] API URL: %s\n", apiURL)
|
||||
|
||||
// Prepare payload for Signal REST API
|
||||
payload := SignalPayload{
|
||||
Number: config.SignalNumber, // This should be the sender's number (registered with signal-cli)
|
||||
Recipients: []string{config.SignalNumber}, // Send to the same number for now
|
||||
Message: message,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
// fmt.Printf("❌ [SIGNAL] JSON marshal error: %v\n", err)
|
||||
return fmt.Errorf("failed to marshal signal payload: %v", err)
|
||||
}
|
||||
|
||||
// fmt.Printf("📱 [SIGNAL] Payload: %s\n", string(jsonData))
|
||||
// fmt.Printf("📱 [SIGNAL] Sending POST request...\n")
|
||||
|
||||
// Send POST request to Signal REST API
|
||||
resp, err := http.Post(apiURL, "application/json", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
// fmt.Printf("❌ [SIGNAL] HTTP error: %v\n", err)
|
||||
return fmt.Errorf("failed to send signal request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// fmt.Printf("📱 [SIGNAL] Response Status: %d\n", resp.StatusCode)
|
||||
|
||||
// Check if the request was successful
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
// fmt.Printf("❌ [SIGNAL] HTTP Status error: %d\n", resp.StatusCode)
|
||||
|
||||
// Try to read error response
|
||||
var errorResponse map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&errorResponse); err == nil {
|
||||
// fmt.Printf("❌ [SIGNAL] Error response: %+v\n", errorResponse)
|
||||
return fmt.Errorf("signal API error (status %d): %v", resp.StatusCode, errorResponse)
|
||||
}
|
||||
|
||||
return fmt.Errorf("signal API returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Parse response
|
||||
var result map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
// fmt.Printf("❌ [SIGNAL] JSON decode error: %v\n", err)
|
||||
// Don't fail if we can't decode response but got success status
|
||||
// fmt.Printf("✅ [SIGNAL] Message sent successfully (response decode failed but status was OK)!\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// fmt.Printf("📱 [SIGNAL] Response: %+v\n", result)
|
||||
// fmt.Printf("✅ [SIGNAL] Message sent successfully!\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendServerNotification sends a server-specific notification via Signal
|
||||
func (ss *SignalService) SendServerNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) error {
|
||||
message := ss.generateServerMessage(payload, template, resourceType)
|
||||
return ss.SendNotification(config, message)
|
||||
}
|
||||
|
||||
// SendServiceNotification sends a service-specific notification via Signal
|
||||
func (ss *SignalService) SendServiceNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServiceNotificationTemplate) error {
|
||||
message := ss.generateServiceMessage(payload, template)
|
||||
return ss.SendNotification(config, message)
|
||||
}
|
||||
|
||||
// generateServerMessage creates a message for server notifications using server template
|
||||
func (ss *SignalService) generateServerMessage(payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) string {
|
||||
var templateMessage string
|
||||
|
||||
// Select appropriate template message based on status and resource type
|
||||
switch strings.ToLower(payload.Status) {
|
||||
case "down":
|
||||
templateMessage = template.DownMessage
|
||||
case "up":
|
||||
templateMessage = template.UpMessage
|
||||
case "warning":
|
||||
templateMessage = template.WarningMessage
|
||||
case "paused":
|
||||
templateMessage = template.PausedMessage
|
||||
default:
|
||||
// Handle resource-specific messages
|
||||
switch resourceType {
|
||||
case "cpu":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreCPUMessage
|
||||
} else {
|
||||
templateMessage = template.CPUMessage
|
||||
}
|
||||
case "ram", "memory":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreRAMMessage
|
||||
} else {
|
||||
templateMessage = template.RAMMessage
|
||||
}
|
||||
case "disk":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreDiskMessage
|
||||
} else {
|
||||
templateMessage = template.DiskMessage
|
||||
}
|
||||
case "network":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreNetworkMessage
|
||||
} else {
|
||||
templateMessage = template.NetworkMessage
|
||||
}
|
||||
case "cpu_temp", "cpu_temperature":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreCPUTempMessage
|
||||
} else {
|
||||
templateMessage = template.CPUTempMessage
|
||||
}
|
||||
case "disk_io":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreDiskIOMessage
|
||||
} else {
|
||||
templateMessage = template.DiskIOMessage
|
||||
}
|
||||
default:
|
||||
templateMessage = template.WarningMessage
|
||||
}
|
||||
}
|
||||
|
||||
// If no template message found, use a default
|
||||
if templateMessage == "" {
|
||||
templateMessage = ss.generateDefaultServerMessage(payload, resourceType)
|
||||
}
|
||||
|
||||
return ss.replacePlaceholders(templateMessage, payload)
|
||||
}
|
||||
|
||||
// generateServiceMessage creates a message for service notifications using service template
|
||||
func (ss *SignalService) generateServiceMessage(payload *NotificationPayload, template *ServiceNotificationTemplate) string {
|
||||
var templateMessage string
|
||||
|
||||
// Select appropriate template message based on status
|
||||
switch strings.ToLower(payload.Status) {
|
||||
case "up":
|
||||
templateMessage = template.UpMessage
|
||||
case "down":
|
||||
templateMessage = template.DownMessage
|
||||
case "maintenance":
|
||||
templateMessage = template.MaintenanceMessage
|
||||
case "incident":
|
||||
templateMessage = template.IncidentMessage
|
||||
case "resolved":
|
||||
templateMessage = template.ResolvedMessage
|
||||
case "warning":
|
||||
templateMessage = template.WarningMessage
|
||||
default:
|
||||
templateMessage = template.WarningMessage
|
||||
}
|
||||
|
||||
// If no template message found, use a default
|
||||
if templateMessage == "" {
|
||||
templateMessage = ss.generateDefaultUptimeMessage(payload)
|
||||
}
|
||||
|
||||
return ss.replacePlaceholders(templateMessage, payload)
|
||||
}
|
||||
|
||||
// replacePlaceholders replaces all placeholders in the message with actual values
|
||||
func (ss *SignalService) replacePlaceholders(message string, payload *NotificationPayload) string {
|
||||
// Replace basic placeholders
|
||||
message = strings.ReplaceAll(message, "${service_name}", payload.ServiceName)
|
||||
message = strings.ReplaceAll(message, "${status}", strings.ToUpper(payload.Status))
|
||||
message = strings.ReplaceAll(message, "${host}", ss.safeString(payload.Host))
|
||||
message = strings.ReplaceAll(message, "${hostname}", ss.safeString(payload.Hostname))
|
||||
|
||||
// Replace URL with fallback to host
|
||||
url := ss.safeString(payload.URL)
|
||||
if url == "N/A" && payload.Host != "" {
|
||||
url = payload.Host
|
||||
}
|
||||
message = strings.ReplaceAll(message, "${url}", url)
|
||||
|
||||
// Replace domain
|
||||
message = strings.ReplaceAll(message, "${domain}", ss.safeString(payload.Domain))
|
||||
|
||||
// Replace service type
|
||||
if payload.ServiceType != "" {
|
||||
message = strings.ReplaceAll(message, "${service_type}", strings.ToUpper(payload.ServiceType))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${service_type}", "N/A")
|
||||
}
|
||||
|
||||
// Replace region and agent info
|
||||
message = strings.ReplaceAll(message, "${region_name}", ss.safeString(payload.RegionName))
|
||||
message = strings.ReplaceAll(message, "${agent_id}", ss.safeString(payload.AgentID))
|
||||
|
||||
// Handle numeric fields safely
|
||||
if payload.Port > 0 {
|
||||
message = strings.ReplaceAll(message, "${port}", fmt.Sprintf("%d", payload.Port))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${port}", "N/A")
|
||||
}
|
||||
|
||||
if payload.ResponseTime > 0 {
|
||||
message = strings.ReplaceAll(message, "${response_time}", fmt.Sprintf("%dms", payload.ResponseTime))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${response_time}", "N/A")
|
||||
}
|
||||
|
||||
if payload.Uptime > 0 {
|
||||
message = strings.ReplaceAll(message, "${uptime}", fmt.Sprintf("%d%%", payload.Uptime))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${uptime}", "N/A")
|
||||
}
|
||||
|
||||
// Replace server monitoring fields
|
||||
message = strings.ReplaceAll(message, "${cpu_usage}", ss.safeString(payload.CPUUsage))
|
||||
message = strings.ReplaceAll(message, "${ram_usage}", ss.safeString(payload.RAMUsage))
|
||||
message = strings.ReplaceAll(message, "${disk_usage}", ss.safeString(payload.DiskUsage))
|
||||
message = strings.ReplaceAll(message, "${network_usage}", ss.safeString(payload.NetworkUsage))
|
||||
message = strings.ReplaceAll(message, "${cpu_temp}", ss.safeString(payload.CPUTemp))
|
||||
message = strings.ReplaceAll(message, "${disk_io}", ss.safeString(payload.DiskIO))
|
||||
message = strings.ReplaceAll(message, "${threshold}", ss.safeString(payload.Threshold))
|
||||
|
||||
// Replace error message - important for uptime services
|
||||
message = strings.ReplaceAll(message, "${error_message}", ss.safeString(payload.ErrorMessage))
|
||||
message = strings.ReplaceAll(message, "${error}", ss.safeString(payload.ErrorMessage))
|
||||
|
||||
// Replace time placeholders
|
||||
message = strings.ReplaceAll(message, "${time}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||
message = strings.ReplaceAll(message, "${timestamp}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
// safeString returns the string value or "N/A" if empty
|
||||
func (ss *SignalService) safeString(value string) string {
|
||||
if value == "" {
|
||||
return "N/A"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// generateDefaultUptimeMessage creates a default uptime message with proper formatting
|
||||
func (ss *SignalService) generateDefaultUptimeMessage(payload *NotificationPayload) string {
|
||||
// Status emoji mapping
|
||||
statusEmoji := "🔵"
|
||||
switch strings.ToLower(payload.Status) {
|
||||
case "up":
|
||||
statusEmoji = "🟢"
|
||||
case "down":
|
||||
statusEmoji = "🔴"
|
||||
case "warning":
|
||||
statusEmoji = "🟡"
|
||||
case "maintenance", "paused":
|
||||
statusEmoji = "🟠"
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("%s Service %s is %s.", statusEmoji, payload.ServiceName, strings.ToUpper(payload.Status))
|
||||
|
||||
// Build formatted details
|
||||
details := []string{}
|
||||
|
||||
// Add URL or host
|
||||
if payload.URL != "" {
|
||||
details = append(details, fmt.Sprintf(" - Host URL: %s", payload.URL))
|
||||
} else if payload.Host != "" {
|
||||
details = append(details, fmt.Sprintf(" - Host: %s", payload.Host))
|
||||
}
|
||||
|
||||
// Add service type
|
||||
if payload.ServiceType != "" {
|
||||
details = append(details, fmt.Sprintf(" - Type: %s", strings.ToUpper(payload.ServiceType)))
|
||||
}
|
||||
|
||||
// Add port if available
|
||||
if payload.Port > 0 {
|
||||
details = append(details, fmt.Sprintf(" - Port: %d", payload.Port))
|
||||
}
|
||||
|
||||
// Add domain if available
|
||||
if payload.Domain != "" {
|
||||
details = append(details, fmt.Sprintf(" - Domain: %s", payload.Domain))
|
||||
}
|
||||
|
||||
// Add response time
|
||||
if payload.ResponseTime > 0 {
|
||||
details = append(details, fmt.Sprintf(" - Response time: %dms", payload.ResponseTime))
|
||||
} else {
|
||||
details = append(details, " - Response time: N/A")
|
||||
}
|
||||
|
||||
// Add region info
|
||||
if payload.RegionName != "" {
|
||||
details = append(details, fmt.Sprintf(" - Region: %s", payload.RegionName))
|
||||
}
|
||||
|
||||
// Add agent info
|
||||
if payload.AgentID != "" {
|
||||
details = append(details, fmt.Sprintf(" - Agent: %s", payload.AgentID))
|
||||
}
|
||||
|
||||
// Add uptime if available
|
||||
if payload.Uptime > 0 {
|
||||
details = append(details, fmt.Sprintf(" - Uptime: %d%%", payload.Uptime))
|
||||
}
|
||||
|
||||
// Add timestamp
|
||||
details = append(details, fmt.Sprintf(" - Time: %s", payload.Timestamp.Format("2006-01-02 15:04:05")))
|
||||
|
||||
// Combine message with details
|
||||
if len(details) > 0 {
|
||||
message += "\n" + strings.Join(details, "\n")
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
// generateDefaultServerMessage creates a default server message
|
||||
func (ss *SignalService) generateDefaultServerMessage(payload *NotificationPayload, resourceType string) string {
|
||||
statusEmoji := "🔵"
|
||||
switch strings.ToLower(payload.Status) {
|
||||
case "up":
|
||||
statusEmoji = "🟢"
|
||||
case "down":
|
||||
statusEmoji = "🔴"
|
||||
case "warning":
|
||||
statusEmoji = "🟡"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s 🖥️ Server %s (%s) status: %s", statusEmoji, payload.ServiceName, payload.Hostname, strings.ToUpper(payload.Status))
|
||||
}
|
||||
@@ -0,0 +1,538 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SlackService handles Slack notifications
|
||||
type SlackService struct{}
|
||||
|
||||
// NewSlackService creates a new Slack notification service
|
||||
func NewSlackService() *SlackService {
|
||||
return &SlackService{}
|
||||
}
|
||||
|
||||
// SlackPayload represents the payload for Slack webhook
|
||||
type SlackPayload struct {
|
||||
Text string `json:"text"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
IconEmoji string `json:"icon_emoji,omitempty"`
|
||||
Attachments []SlackAttachment `json:"attachments,omitempty"`
|
||||
Blocks []SlackBlock `json:"blocks,omitempty"`
|
||||
}
|
||||
|
||||
// SlackAttachment represents a Slack attachment
|
||||
type SlackAttachment struct {
|
||||
Color string `json:"color,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Timestamp int64 `json:"ts,omitempty"`
|
||||
Fields []SlackField `json:"fields,omitempty"`
|
||||
}
|
||||
|
||||
// SlackField represents a field in Slack attachment
|
||||
type SlackField struct {
|
||||
Title string `json:"title"`
|
||||
Value string `json:"value"`
|
||||
Short bool `json:"short"`
|
||||
}
|
||||
|
||||
// SlackBlock represents a Slack block element
|
||||
type SlackBlock struct {
|
||||
Type string `json:"type"`
|
||||
Text *SlackText `json:"text,omitempty"`
|
||||
}
|
||||
|
||||
// SlackText represents text in Slack blocks
|
||||
type SlackText struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// SendNotification sends a notification via Slack webhook
|
||||
func (ss *SlackService) SendNotification(config *AlertConfiguration, message string) error {
|
||||
if config.SlackWebhookURL == "" {
|
||||
return fmt.Errorf("slack webhook URL is required")
|
||||
}
|
||||
|
||||
// Parse and format the message for better Slack presentation
|
||||
formattedPayload := ss.createSlackPayload(config, message)
|
||||
|
||||
jsonData, err := json.Marshal(formattedPayload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := http.Post(config.SlackWebhookURL, "application/json", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("slack webhook error, status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendServerNotification sends a server-specific notification via Slack
|
||||
func (ss *SlackService) SendServerNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) error {
|
||||
message := ss.generateServerMessage(payload, template, resourceType)
|
||||
return ss.SendNotification(config, message)
|
||||
}
|
||||
|
||||
// SendServiceNotification sends a service-specific notification via Slack
|
||||
func (ss *SlackService) SendServiceNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServiceNotificationTemplate) error {
|
||||
message := ss.generateServiceMessage(payload, template)
|
||||
return ss.SendNotification(config, message)
|
||||
}
|
||||
|
||||
// createSlackPayload creates a properly formatted Slack payload with rich formatting
|
||||
func (ss *SlackService) createSlackPayload(config *AlertConfiguration, message string) SlackPayload {
|
||||
// Determine color based on message content
|
||||
color := ss.getColorFromMessage(message)
|
||||
|
||||
// Parse message for structured presentation
|
||||
title, fields := ss.parseMessageForSlack(message)
|
||||
|
||||
payload := SlackPayload{
|
||||
Username: config.NotifyName,
|
||||
IconEmoji: ss.getEmojiFromMessage(message),
|
||||
Text: title,
|
||||
}
|
||||
|
||||
// Create attachment for better formatting
|
||||
if len(fields) > 0 {
|
||||
attachment := SlackAttachment{
|
||||
Color: color,
|
||||
Title: title,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Fields: fields,
|
||||
}
|
||||
payload.Attachments = []SlackAttachment{attachment}
|
||||
payload.Text = "" // Clear text when using attachments
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
// parseMessageForSlack parses the message to extract title and create structured fields
|
||||
func (ss *SlackService) parseMessageForSlack(message string) (string, []SlackField) {
|
||||
lines := strings.Split(message, "\n")
|
||||
if len(lines) == 0 {
|
||||
return message, nil
|
||||
}
|
||||
|
||||
title := strings.TrimSpace(lines[0])
|
||||
var fields []SlackField
|
||||
|
||||
// Process bullet points and key-value pairs
|
||||
for i, line := range lines {
|
||||
if i == 0 {
|
||||
continue // Skip title
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle bullet points (•, -, *)
|
||||
if strings.HasPrefix(line, "•") || strings.HasPrefix(line, "-") || strings.HasPrefix(line, "*") {
|
||||
// Remove bullet and clean up
|
||||
cleaned := strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(strings.TrimPrefix(line, "•"), "-"), "*"))
|
||||
|
||||
// Try to split on colon for key-value pairs
|
||||
if strings.Contains(cleaned, ":") {
|
||||
parts := strings.SplitN(cleaned, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
fields = append(fields, SlackField{
|
||||
Title: key,
|
||||
Value: value,
|
||||
Short: len(value) < 30, // Short fields for compact display
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Add as a single field
|
||||
fields = append(fields, SlackField{
|
||||
Title: "Info",
|
||||
Value: cleaned,
|
||||
Short: false,
|
||||
})
|
||||
}
|
||||
} else if strings.Contains(line, ":") {
|
||||
// Direct key-value pairs
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
fields = append(fields, SlackField{
|
||||
Title: key,
|
||||
Value: value,
|
||||
Short: len(value) < 30,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return title, fields
|
||||
}
|
||||
|
||||
// getColorFromMessage determines the appropriate color based on message content
|
||||
func (ss *SlackService) getColorFromMessage(message string) string {
|
||||
lowerMsg := strings.ToLower(message)
|
||||
|
||||
// Critical/Error states - Red
|
||||
if strings.Contains(lowerMsg, "down") || strings.Contains(lowerMsg, "failed") ||
|
||||
strings.Contains(lowerMsg, "error") || strings.Contains(lowerMsg, "expired") ||
|
||||
strings.Contains(lowerMsg, "🔴") || strings.Contains(lowerMsg, "🚨") {
|
||||
return "#FF0000" // Red
|
||||
}
|
||||
|
||||
// Warning states - Yellow/Orange
|
||||
if strings.Contains(lowerMsg, "warning") || strings.Contains(lowerMsg, "expiring") ||
|
||||
strings.Contains(lowerMsg, "🟡") || strings.Contains(lowerMsg, "⚠️") {
|
||||
return "#FFA500" // Orange
|
||||
}
|
||||
|
||||
// Success states - Green
|
||||
if strings.Contains(lowerMsg, "up") || strings.Contains(lowerMsg, "restored") ||
|
||||
strings.Contains(lowerMsg, "resolved") || strings.Contains(lowerMsg, "🟢") ||
|
||||
strings.Contains(lowerMsg, "✅") {
|
||||
return "#00FF00" // Green
|
||||
}
|
||||
|
||||
// Maintenance/Info states - Blue
|
||||
if strings.Contains(lowerMsg, "maintenance") || strings.Contains(lowerMsg, "paused") ||
|
||||
strings.Contains(lowerMsg, "🟠") || strings.Contains(lowerMsg, "🔵") {
|
||||
return "#0080FF" // Blue
|
||||
}
|
||||
|
||||
return "#808080" // Default gray
|
||||
}
|
||||
|
||||
// getEmojiFromMessage extracts or determines appropriate emoji for the message
|
||||
func (ss *SlackService) getEmojiFromMessage(message string) string {
|
||||
lowerMsg := strings.ToLower(message)
|
||||
|
||||
// Check for specific service types first
|
||||
if strings.Contains(lowerMsg, "server") {
|
||||
if strings.Contains(lowerMsg, "down") || strings.Contains(lowerMsg, "failed") {
|
||||
return ":red_circle:"
|
||||
} else if strings.Contains(lowerMsg, "warning") {
|
||||
return ":warning:"
|
||||
} else if strings.Contains(lowerMsg, "up") || strings.Contains(lowerMsg, "restored") {
|
||||
return ":white_check_mark:"
|
||||
}
|
||||
return ":desktop_computer:"
|
||||
}
|
||||
|
||||
// SSL Certificate notifications
|
||||
if strings.Contains(lowerMsg, "certificate") || strings.Contains(lowerMsg, "ssl") {
|
||||
if strings.Contains(lowerMsg, "expired") {
|
||||
return ":no_entry:"
|
||||
} else if strings.Contains(lowerMsg, "expiring") {
|
||||
return ":warning:"
|
||||
}
|
||||
return ":lock:"
|
||||
}
|
||||
|
||||
// General status-based emojis
|
||||
if strings.Contains(lowerMsg, "down") || strings.Contains(lowerMsg, "failed") {
|
||||
return ":red_circle:"
|
||||
} else if strings.Contains(lowerMsg, "warning") {
|
||||
return ":warning:"
|
||||
} else if strings.Contains(lowerMsg, "up") || strings.Contains(lowerMsg, "restored") {
|
||||
return ":white_check_mark:"
|
||||
} else if strings.Contains(lowerMsg, "maintenance") {
|
||||
return ":construction:"
|
||||
}
|
||||
|
||||
return ":information_source:" // Default info emoji
|
||||
}
|
||||
|
||||
// generateServerMessage creates a message for server notifications using server template
|
||||
func (ss *SlackService) generateServerMessage(payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) string {
|
||||
var templateMessage string
|
||||
|
||||
// Select appropriate template message based on status and resource type
|
||||
switch strings.ToLower(payload.Status) {
|
||||
case "down":
|
||||
templateMessage = template.DownMessage
|
||||
case "up":
|
||||
templateMessage = template.UpMessage
|
||||
case "warning":
|
||||
templateMessage = template.WarningMessage
|
||||
case "paused":
|
||||
templateMessage = template.PausedMessage
|
||||
default:
|
||||
// Handle resource-specific messages
|
||||
switch resourceType {
|
||||
case "cpu":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreCPUMessage
|
||||
} else {
|
||||
templateMessage = template.CPUMessage
|
||||
}
|
||||
case "ram", "memory":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreRAMMessage
|
||||
} else {
|
||||
templateMessage = template.RAMMessage
|
||||
}
|
||||
case "disk":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreDiskMessage
|
||||
} else {
|
||||
templateMessage = template.DiskMessage
|
||||
}
|
||||
case "network":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreNetworkMessage
|
||||
} else {
|
||||
templateMessage = template.NetworkMessage
|
||||
}
|
||||
case "cpu_temp", "cpu_temperature":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreCPUTempMessage
|
||||
} else {
|
||||
templateMessage = template.CPUTempMessage
|
||||
}
|
||||
case "disk_io":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreDiskIOMessage
|
||||
} else {
|
||||
templateMessage = template.DiskIOMessage
|
||||
}
|
||||
default:
|
||||
templateMessage = template.WarningMessage
|
||||
}
|
||||
}
|
||||
|
||||
// If no template message found, use a default
|
||||
if templateMessage == "" {
|
||||
templateMessage = ss.generateDefaultServerMessage(payload, resourceType)
|
||||
}
|
||||
|
||||
return ss.replacePlaceholders(templateMessage, payload)
|
||||
}
|
||||
|
||||
// generateServiceMessage creates a message for service notifications using service template
|
||||
func (ss *SlackService) generateServiceMessage(payload *NotificationPayload, template *ServiceNotificationTemplate) string {
|
||||
var templateMessage string
|
||||
|
||||
// Select appropriate template message based on status
|
||||
switch strings.ToLower(payload.Status) {
|
||||
case "up":
|
||||
templateMessage = template.UpMessage
|
||||
case "down":
|
||||
templateMessage = template.DownMessage
|
||||
case "maintenance":
|
||||
templateMessage = template.MaintenanceMessage
|
||||
case "incident":
|
||||
templateMessage = template.IncidentMessage
|
||||
case "resolved":
|
||||
templateMessage = template.ResolvedMessage
|
||||
case "warning":
|
||||
templateMessage = template.WarningMessage
|
||||
default:
|
||||
templateMessage = template.WarningMessage
|
||||
}
|
||||
|
||||
// If no template message found, use a default
|
||||
if templateMessage == "" {
|
||||
templateMessage = ss.generateDefaultUptimeMessage(payload)
|
||||
}
|
||||
|
||||
return ss.replacePlaceholders(templateMessage, payload)
|
||||
}
|
||||
|
||||
// replacePlaceholders replaces all placeholders in the message with actual values
|
||||
func (ss *SlackService) replacePlaceholders(message string, payload *NotificationPayload) string {
|
||||
// Replace basic placeholders
|
||||
message = strings.ReplaceAll(message, "${service_name}", payload.ServiceName)
|
||||
message = strings.ReplaceAll(message, "${status}", strings.ToUpper(payload.Status))
|
||||
message = strings.ReplaceAll(message, "${host}", ss.safeString(payload.Host))
|
||||
message = strings.ReplaceAll(message, "${hostname}", ss.safeString(payload.Hostname))
|
||||
|
||||
// Replace URL with fallback to host
|
||||
url := ss.safeString(payload.URL)
|
||||
if url == "N/A" && payload.Host != "" {
|
||||
url = payload.Host
|
||||
}
|
||||
message = strings.ReplaceAll(message, "${url}", url)
|
||||
|
||||
// Replace domain
|
||||
message = strings.ReplaceAll(message, "${domain}", ss.safeString(payload.Domain))
|
||||
|
||||
// Replace service type
|
||||
if payload.ServiceType != "" {
|
||||
message = strings.ReplaceAll(message, "${service_type}", strings.ToUpper(payload.ServiceType))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${service_type}", "N/A")
|
||||
}
|
||||
|
||||
// Replace region and agent info
|
||||
message = strings.ReplaceAll(message, "${region_name}", ss.safeString(payload.RegionName))
|
||||
message = strings.ReplaceAll(message, "${agent_id}", ss.safeString(payload.AgentID))
|
||||
|
||||
// Handle numeric fields safely
|
||||
if payload.Port > 0 {
|
||||
message = strings.ReplaceAll(message, "${port}", fmt.Sprintf("%d", payload.Port))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${port}", "N/A")
|
||||
}
|
||||
|
||||
if payload.ResponseTime > 0 {
|
||||
message = strings.ReplaceAll(message, "${response_time}", fmt.Sprintf("%dms", payload.ResponseTime))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${response_time}", "N/A")
|
||||
}
|
||||
|
||||
if payload.Uptime > 0 {
|
||||
message = strings.ReplaceAll(message, "${uptime}", fmt.Sprintf("%d%%", payload.Uptime))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${uptime}", "N/A")
|
||||
}
|
||||
|
||||
// Replace server monitoring fields
|
||||
message = strings.ReplaceAll(message, "${cpu_usage}", ss.safeString(payload.CPUUsage))
|
||||
message = strings.ReplaceAll(message, "${ram_usage}", ss.safeString(payload.RAMUsage))
|
||||
message = strings.ReplaceAll(message, "${disk_usage}", ss.safeString(payload.DiskUsage))
|
||||
message = strings.ReplaceAll(message, "${network_usage}", ss.safeString(payload.NetworkUsage))
|
||||
message = strings.ReplaceAll(message, "${cpu_temp}", ss.safeString(payload.CPUTemp))
|
||||
message = strings.ReplaceAll(message, "${disk_io}", ss.safeString(payload.DiskIO))
|
||||
message = strings.ReplaceAll(message, "${threshold}", ss.safeString(payload.Threshold))
|
||||
|
||||
// Replace SSL certificate fields
|
||||
message = strings.ReplaceAll(message, "${certificate_name}", ss.safeString(payload.CertificateName))
|
||||
message = strings.ReplaceAll(message, "${expiry_date}", ss.safeString(payload.ExpiryDate))
|
||||
message = strings.ReplaceAll(message, "${days_left}", ss.safeString(payload.DaysLeft))
|
||||
message = strings.ReplaceAll(message, "${issuer_cn}", ss.safeString(payload.IssuerCN))
|
||||
message = strings.ReplaceAll(message, "${serial_number}", ss.safeString(payload.SerialNumber))
|
||||
|
||||
// Replace error message - important for uptime services
|
||||
message = strings.ReplaceAll(message, "${error_message}", ss.safeString(payload.ErrorMessage))
|
||||
message = strings.ReplaceAll(message, "${error}", ss.safeString(payload.ErrorMessage))
|
||||
|
||||
// Replace time placeholders
|
||||
message = strings.ReplaceAll(message, "${time}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||
message = strings.ReplaceAll(message, "${timestamp}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||
message = strings.ReplaceAll(message, "${date}", payload.Timestamp.Format("2006-01-02"))
|
||||
|
||||
// Replace message placeholder
|
||||
if payload.Message != "" {
|
||||
message = strings.ReplaceAll(message, "${message}", payload.Message)
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${message}", "N/A")
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
// safeString returns the string value or "N/A" if empty
|
||||
func (ss *SlackService) safeString(value string) string {
|
||||
if value == "" {
|
||||
return "N/A"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// generateDefaultUptimeMessage creates a default uptime message with proper formatting
|
||||
func (ss *SlackService) generateDefaultUptimeMessage(payload *NotificationPayload) string {
|
||||
// Status emoji mapping
|
||||
statusEmoji := "🔵"
|
||||
switch strings.ToLower(payload.Status) {
|
||||
case "up":
|
||||
statusEmoji = "🟢"
|
||||
case "down":
|
||||
statusEmoji = "🔴"
|
||||
case "warning":
|
||||
statusEmoji = "🟡"
|
||||
case "maintenance", "paused":
|
||||
statusEmoji = "🟠"
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("%s Service %s is %s", statusEmoji, payload.ServiceName, strings.ToUpper(payload.Status))
|
||||
|
||||
// Build formatted details
|
||||
details := []string{}
|
||||
|
||||
// Add URL or host
|
||||
if payload.URL != "" {
|
||||
details = append(details, fmt.Sprintf("• Host URL: %s", payload.URL))
|
||||
} else if payload.Host != "" {
|
||||
details = append(details, fmt.Sprintf("• Host: %s", payload.Host))
|
||||
}
|
||||
|
||||
// Add service type
|
||||
if payload.ServiceType != "" {
|
||||
details = append(details, fmt.Sprintf("• Type: %s", strings.ToUpper(payload.ServiceType)))
|
||||
}
|
||||
|
||||
// Add port if available
|
||||
if payload.Port > 0 {
|
||||
details = append(details, fmt.Sprintf("• Port: %d", payload.Port))
|
||||
}
|
||||
|
||||
// Add domain if available
|
||||
if payload.Domain != "" {
|
||||
details = append(details, fmt.Sprintf("• Domain: %s", payload.Domain))
|
||||
}
|
||||
|
||||
// Add response time
|
||||
if payload.ResponseTime > 0 {
|
||||
details = append(details, fmt.Sprintf("• Response time: %dms", payload.ResponseTime))
|
||||
} else {
|
||||
details = append(details, "• Response time: N/A")
|
||||
}
|
||||
|
||||
// Add region info
|
||||
if payload.RegionName != "" {
|
||||
details = append(details, fmt.Sprintf("• Region: %s", payload.RegionName))
|
||||
}
|
||||
|
||||
// Add agent info
|
||||
if payload.AgentID != "" {
|
||||
details = append(details, fmt.Sprintf("• Agent: %s", payload.AgentID))
|
||||
}
|
||||
|
||||
// Add uptime if available
|
||||
if payload.Uptime > 0 {
|
||||
details = append(details, fmt.Sprintf("• Uptime: %d%%", payload.Uptime))
|
||||
}
|
||||
|
||||
// Add timestamp
|
||||
details = append(details, fmt.Sprintf("• Time: %s", payload.Timestamp.Format("2006-01-02 15:04:05")))
|
||||
|
||||
// Combine message with details
|
||||
if len(details) > 0 {
|
||||
message += "\n" + strings.Join(details, "\n")
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
// generateDefaultServerMessage creates a default server message
|
||||
func (ss *SlackService) generateDefaultServerMessage(payload *NotificationPayload, resourceType string) string {
|
||||
statusEmoji := "🔵"
|
||||
switch strings.ToLower(payload.Status) {
|
||||
case "up":
|
||||
statusEmoji = "🟢"
|
||||
case "down":
|
||||
statusEmoji = "🔴"
|
||||
case "warning":
|
||||
statusEmoji = "🟡"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s 🖥️ Server %s (%s) status: %s", statusEmoji, payload.ServiceName, payload.Hostname, strings.ToUpper(payload.Status))
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
|
||||
package notification
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"service-operation/pocketbase"
|
||||
)
|
||||
|
||||
// SSLNotificationTemplate represents an SSL notification template
|
||||
type SSLNotificationTemplate struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Expired string `json:"expired"`
|
||||
ExpiringSoon string `json:"exiring_soon"`
|
||||
Warning string `json:"warning"`
|
||||
Placeholder string `json:"placeholder"`
|
||||
}
|
||||
|
||||
// SSLNotificationManager handles SSL certificate notifications
|
||||
type SSLNotificationManager struct {
|
||||
pbClient *pocketbase.PocketBaseClient
|
||||
services map[string]NotificationService
|
||||
}
|
||||
|
||||
// NewSSLNotificationManager creates a new SSL notification manager
|
||||
func NewSSLNotificationManager(pbClient *pocketbase.PocketBaseClient, services map[string]NotificationService) *SSLNotificationManager {
|
||||
return &SSLNotificationManager{
|
||||
pbClient: pbClient,
|
||||
services: services,
|
||||
}
|
||||
}
|
||||
|
||||
// SendSSLNotification sends notification for SSL certificate status
|
||||
func (snm *SSLNotificationManager) SendSSLNotification(payload *NotificationPayload, notificationID, templateID string) error {
|
||||
// log.Printf("📨 [SSL-MANAGER] IMMEDIATE send for certificate: %s, status: %s", payload.Domain, payload.Status)
|
||||
// log.Printf("📨 [SSL-MANAGER] Notification ID: %s, Template ID: %s", notificationID, templateID)
|
||||
|
||||
if notificationID == "" {
|
||||
return fmt.Errorf("notification ID required for SSL certificate: %s", payload.Domain)
|
||||
}
|
||||
|
||||
// Parse notification IDs
|
||||
notificationIDs := parseNotificationIDs(notificationID)
|
||||
if len(notificationIDs) == 0 {
|
||||
return fmt.Errorf("no valid notification IDs for SSL certificate: %s", payload.Domain)
|
||||
}
|
||||
|
||||
var errors []string
|
||||
successCount := 0
|
||||
|
||||
// Send to each notification channel IMMEDIATELY
|
||||
for _, id := range notificationIDs {
|
||||
// log.Printf("📤 [SSL-SEND] Processing notification ID: %s for certificate %s", id, payload.Domain)
|
||||
|
||||
// Check if enabled
|
||||
if !isNotificationEnabled(snm.pbClient, id) {
|
||||
// log.Printf("⚠️ Notification %s disabled for SSL certificate %s, skipping", id, payload.Domain)
|
||||
_ = id
|
||||
continue
|
||||
}
|
||||
|
||||
// Get alert configuration
|
||||
alertConfig, err := getAlertConfiguration(snm.pbClient, id)
|
||||
if err != nil {
|
||||
// log.Printf("❌ Failed to get alert config for %s (certificate: %s): %v", id, payload.Domain, err)
|
||||
errors = append(errors, fmt.Sprintf("config error %s: %v", id, err))
|
||||
continue
|
||||
}
|
||||
|
||||
// log.Printf("📋 [SSL-CONFIG] Alert config for %s: Type=%s, ChatID=%s, Token present=%v",
|
||||
// id, alertConfig.NotificationType, alertConfig.TelegramChatID, alertConfig.BotToken != "")
|
||||
|
||||
// Get SSL template
|
||||
var sslTemplate *SSLNotificationTemplate
|
||||
if templateID != "" {
|
||||
sslTemplate, err = snm.getSSLNotificationTemplate(templateID)
|
||||
if err != nil {
|
||||
// log.Printf("⚠️ Template error for %s (certificate: %s): %v", templateID, payload.Domain, err)
|
||||
_ = err
|
||||
} else {
|
||||
// log.Printf("📄 [SSL-TEMPLATE] Retrieved template: Expired=%s, ExpiringSoon=%s, Warning=%s",
|
||||
// sslTemplate.Expired, sslTemplate.ExpiringSoon, sslTemplate.Warning)
|
||||
_ = sslTemplate
|
||||
}
|
||||
}
|
||||
|
||||
// Generate message
|
||||
message := snm.generateSSLMessage(payload, sslTemplate)
|
||||
// log.Printf("📝 [SSL-MESSAGE] Generated for %s (%s): %s", payload.Domain, id, message)
|
||||
|
||||
// Get notification service
|
||||
service, exists := snm.services[alertConfig.NotificationType]
|
||||
if !exists {
|
||||
// log.Printf("❌ Unsupported notification type for SSL: %s", alertConfig.NotificationType)
|
||||
errors = append(errors, fmt.Sprintf("unsupported type %s", alertConfig.NotificationType))
|
||||
continue
|
||||
}
|
||||
|
||||
// SEND IMMEDIATELY - NO DELAYS
|
||||
// log.Printf("⚡ [SSL-TELEGRAM] Sending via %s for SSL certificate %s", alertConfig.NotificationType, payload.Domain)
|
||||
err = service.SendNotification(alertConfig, message)
|
||||
if err != nil {
|
||||
// log.Printf("❌ [SSL-FAILED] Failed to send via %s for %s: %v", alertConfig.NotificationType, payload.Domain, err)
|
||||
errors = append(errors, fmt.Sprintf("send failed %s: %v", alertConfig.NotificationType, err))
|
||||
} else {
|
||||
// log.Printf("✅ [SSL-SUCCESS] Successfully sent via %s for %s", alertConfig.NotificationType, payload.Domain)
|
||||
successCount++
|
||||
}
|
||||
|
||||
_ = alertConfig
|
||||
_ = message
|
||||
}
|
||||
|
||||
// Report results
|
||||
if successCount > 0 {
|
||||
// log.Printf("✅ [SSL-FINAL] Sent %d/%d SSL notifications for %s", successCount, len(notificationIDs), payload.Domain)
|
||||
_ = successCount
|
||||
_ = notificationIDs
|
||||
}
|
||||
|
||||
if len(errors) > 0 && successCount == 0 {
|
||||
return fmt.Errorf("all SSL notifications failed for %s: %v", payload.Domain, errors)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSSLNotificationTemplate fetches SSL notification template from PocketBase
|
||||
func (snm *SSLNotificationManager) getSSLNotificationTemplate(templateID string) (*SSLNotificationTemplate, error) {
|
||||
url := fmt.Sprintf("%s/api/collections/ssl_notification_templates/records/%s", snm.pbClient.GetBaseURL(), templateID)
|
||||
// log.Printf("🌐 Fetching SSL notification template from: %s", url)
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
// log.Printf("❌ HTTP error fetching SSL notification template: %v", err)
|
||||
_ = err
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
// log.Printf("❌ Failed to fetch SSL notification template, status: %d", resp.StatusCode)
|
||||
return nil, fmt.Errorf("failed to fetch SSL notification template, status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var template SSLNotificationTemplate
|
||||
if err := json.NewDecoder(resp.Body).Decode(&template); err != nil {
|
||||
// log.Printf("❌ Error decoding SSL notification template JSON: %v", err)
|
||||
_ = err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// log.Printf("✅ Successfully fetched SSL notification template: %+v", template)
|
||||
_ = url
|
||||
_ = template
|
||||
return &template, nil
|
||||
}
|
||||
|
||||
// generateSSLMessage creates notification message for SSL certificates
|
||||
func (snm *SSLNotificationManager) generateSSLMessage(payload *NotificationPayload, template *SSLNotificationTemplate) string {
|
||||
var baseMessage string
|
||||
|
||||
// Use template if available
|
||||
if template != nil {
|
||||
// log.Printf("🔧 Using SSL template for status: %s", strings.ToLower(payload.Status))
|
||||
// log.Printf("🔧 [SSL-TEMPLATE-DEBUG] Available templates - Expired: '%s', ExpiringSoon: '%s', Warning: '%s'",
|
||||
// template.Expired, template.ExpiringSoon, template.Warning)
|
||||
|
||||
switch strings.ToLower(payload.Status) {
|
||||
case "expired":
|
||||
baseMessage = template.Expired
|
||||
// log.Printf("🔧 [SSL-EXPIRED] Selected expired template: '%s'", baseMessage)
|
||||
case "expiring_soon":
|
||||
baseMessage = template.ExpiringSoon
|
||||
// log.Printf("🔧 [SSL-EXPIRING] Selected expiring soon template: '%s'", baseMessage)
|
||||
case "warning":
|
||||
baseMessage = template.Warning
|
||||
// log.Printf("🔧 [SSL-WARNING] Selected warning template: '%s'", baseMessage)
|
||||
default:
|
||||
baseMessage = template.Warning
|
||||
// log.Printf("🔧 [SSL-DEFAULT] Using warning template for status '%s': '%s'", payload.Status, baseMessage)
|
||||
}
|
||||
|
||||
_ = baseMessage
|
||||
_ = template
|
||||
}
|
||||
|
||||
// Use default if no template or template message is empty
|
||||
if baseMessage == "" {
|
||||
// log.Printf("🔧 Using default SSL message (no template or empty template)")
|
||||
baseMessage = snm.getDefaultSSLMessage(payload)
|
||||
}
|
||||
|
||||
// Replace placeholders
|
||||
message := snm.replaceSSLPlaceholders(baseMessage, payload)
|
||||
|
||||
// log.Printf("📝 Final SSL message: %s", message)
|
||||
_ = message
|
||||
return message
|
||||
}
|
||||
|
||||
// replaceSSLPlaceholders replaces all placeholders in the SSL message
|
||||
func (snm *SSLNotificationManager) replaceSSLPlaceholders(message string, payload *NotificationPayload) string {
|
||||
// log.Printf("🔄 [SSL-REPLACE] Before replacement: %s", message)
|
||||
// log.Printf("🔄 [SSL-DATA] Payload data - Domain: %s, ExpiryDate: %s, DaysLeft: %s, IssuerCN: %s",
|
||||
// payload.Domain, payload.ExpiryDate, payload.DaysLeft, payload.IssuerCN)
|
||||
|
||||
// SSL Certificate specific placeholders - ensure all are replaced
|
||||
message = strings.ReplaceAll(message, "${domain}", snm.safeString(payload.Domain))
|
||||
message = strings.ReplaceAll(message, "${certificate_name}", snm.safeString(payload.CertificateName))
|
||||
message = strings.ReplaceAll(message, "${expiry_date}", snm.safeString(payload.ExpiryDate))
|
||||
message = strings.ReplaceAll(message, "${days_left}", snm.safeString(payload.DaysLeft))
|
||||
message = strings.ReplaceAll(message, "${issuer_cn}", snm.safeString(payload.IssuerCN))
|
||||
message = strings.ReplaceAll(message, "${serial_number}", snm.safeString(payload.SerialNumber))
|
||||
|
||||
// Basic placeholders
|
||||
message = strings.ReplaceAll(message, "${status}", strings.ToUpper(payload.Status))
|
||||
message = strings.ReplaceAll(message, "${service_name}", snm.safeString(payload.ServiceName))
|
||||
message = strings.ReplaceAll(message, "${host}", snm.safeString(payload.Host))
|
||||
|
||||
// Time placeholders
|
||||
message = strings.ReplaceAll(message, "${timestamp}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||
message = strings.ReplaceAll(message, "${time}", payload.Timestamp.Format("15:04:05"))
|
||||
message = strings.ReplaceAll(message, "${date}", payload.Timestamp.Format("2006-01-02"))
|
||||
|
||||
// Message placeholder
|
||||
if payload.Message != "" {
|
||||
message = strings.ReplaceAll(message, "${message}", payload.Message)
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${message}", "N/A")
|
||||
}
|
||||
|
||||
// log.Printf("🔄 [SSL-REPLACE] After replacement: %s", message)
|
||||
_ = payload
|
||||
return message
|
||||
}
|
||||
|
||||
// safeString returns the string value or "N/A" if empty
|
||||
func (snm *SSLNotificationManager) safeString(value string) string {
|
||||
if value == "" {
|
||||
return "N/A"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// getDefaultSSLMessage provides a default notification message for SSL certificates
|
||||
func (snm *SSLNotificationManager) getDefaultSSLMessage(payload *NotificationPayload) string {
|
||||
statusEmoji := "🔒"
|
||||
if payload.Status == "expired" {
|
||||
statusEmoji = "🚨"
|
||||
} else if payload.Status == "expiring_soon" {
|
||||
statusEmoji = "⚠️"
|
||||
} else if payload.Status == "warning" {
|
||||
statusEmoji = "🔔"
|
||||
}
|
||||
|
||||
// Create the default message with all SSL details
|
||||
message := fmt.Sprintf("%s SSL certificate for %s has %s", statusEmoji, payload.Domain, strings.ToUpper(payload.Status))
|
||||
|
||||
// Add certificate details if available
|
||||
if payload.CertificateName != "" && payload.CertificateName != payload.Domain {
|
||||
message += fmt.Sprintf("\n • Certs Name: %s", payload.CertificateName)
|
||||
}
|
||||
|
||||
if payload.ExpiryDate != "" {
|
||||
message += fmt.Sprintf("\n • Expiry Date: %s", payload.ExpiryDate)
|
||||
}
|
||||
|
||||
if payload.DaysLeft != "" {
|
||||
message += fmt.Sprintf("\n • Days Left: %s", payload.DaysLeft)
|
||||
}
|
||||
|
||||
if payload.IssuerCN != "" {
|
||||
message += fmt.Sprintf("\n • Issuer: %s", payload.IssuerCN)
|
||||
}
|
||||
|
||||
// Add timestamp
|
||||
message += fmt.Sprintf("\n • Time: %s", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||
|
||||
return message
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
|
||||
package notification
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TelegramService handles Telegram notifications
|
||||
type TelegramService struct{}
|
||||
|
||||
// NewTelegramService creates a new Telegram notification service
|
||||
func NewTelegramService() *TelegramService {
|
||||
return &TelegramService{}
|
||||
}
|
||||
|
||||
// TelegramPayload represents the payload for Telegram API
|
||||
type TelegramPayload struct {
|
||||
ChatID string `json:"chat_id"`
|
||||
Text string `json:"text"`
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
}
|
||||
|
||||
// SendNotification sends a notification via Telegram
|
||||
func (ts *TelegramService) SendNotification(config *AlertConfiguration, message string) error {
|
||||
// fmt.Printf("📱 [TELEGRAM] Attempting to send notification...\n")
|
||||
// fmt.Printf("📱 [TELEGRAM] Config - Chat ID: %s, Bot Token present: %v\n", config.TelegramChatID, config.BotToken != "")
|
||||
// fmt.Printf("📱 [TELEGRAM] Message: %s\n", message)
|
||||
|
||||
if config.BotToken == "" || config.TelegramChatID == "" {
|
||||
return fmt.Errorf("telegram bot token and chat ID are required")
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", config.BotToken)
|
||||
// fmt.Printf("📱 [TELEGRAM] API URL: %s\n", strings.Replace(url, config.BotToken, "[REDACTED]", 1))
|
||||
|
||||
payload := TelegramPayload{
|
||||
ChatID: config.TelegramChatID,
|
||||
Text: message,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// fmt.Printf("📱 [TELEGRAM] Sending POST request...\n")
|
||||
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
// fmt.Printf("❌ [TELEGRAM] HTTP error: %v\n", err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result NotificationResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
// fmt.Printf("❌ [TELEGRAM] JSON decode error: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// fmt.Printf("📱 [TELEGRAM] Response - OK: %v, Description: %s\n", result.OK, result.Description)
|
||||
|
||||
if !result.OK {
|
||||
// fmt.Printf("❌ [TELEGRAM] API error: %s\n", result.Description)
|
||||
return fmt.Errorf("telegram API error: %s", result.Description)
|
||||
}
|
||||
|
||||
// fmt.Printf("✅ [TELEGRAM] Message sent successfully!\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendServerNotification sends a server-specific notification via Telegram
|
||||
func (ts *TelegramService) SendServerNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) error {
|
||||
message := ts.generateServerMessage(payload, template, resourceType)
|
||||
return ts.SendNotification(config, message)
|
||||
}
|
||||
|
||||
// SendServiceNotification sends a service-specific notification via Telegram
|
||||
func (ts *TelegramService) SendServiceNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServiceNotificationTemplate) error {
|
||||
message := ts.generateServiceMessage(payload, template)
|
||||
return ts.SendNotification(config, message)
|
||||
}
|
||||
|
||||
// generateServerMessage creates a message for server notifications using server template
|
||||
func (ts *TelegramService) generateServerMessage(payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) string {
|
||||
var templateMessage string
|
||||
|
||||
// Select appropriate template message based on status and resource type
|
||||
switch strings.ToLower(payload.Status) {
|
||||
case "down":
|
||||
templateMessage = template.DownMessage
|
||||
case "up":
|
||||
templateMessage = template.UpMessage
|
||||
case "warning":
|
||||
templateMessage = template.WarningMessage
|
||||
case "paused":
|
||||
templateMessage = template.PausedMessage
|
||||
default:
|
||||
// Handle resource-specific messages
|
||||
switch resourceType {
|
||||
case "cpu":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreCPUMessage
|
||||
} else {
|
||||
templateMessage = template.CPUMessage
|
||||
}
|
||||
case "ram", "memory":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreRAMMessage
|
||||
} else {
|
||||
templateMessage = template.RAMMessage
|
||||
}
|
||||
case "disk":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreDiskMessage
|
||||
} else {
|
||||
templateMessage = template.DiskMessage
|
||||
}
|
||||
case "network":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreNetworkMessage
|
||||
} else {
|
||||
templateMessage = template.NetworkMessage
|
||||
}
|
||||
case "cpu_temp", "cpu_temperature":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreCPUTempMessage
|
||||
} else {
|
||||
templateMessage = template.CPUTempMessage
|
||||
}
|
||||
case "disk_io":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreDiskIOMessage
|
||||
} else {
|
||||
templateMessage = template.DiskIOMessage
|
||||
}
|
||||
default:
|
||||
templateMessage = template.WarningMessage
|
||||
}
|
||||
}
|
||||
|
||||
// If no template message found, use a default
|
||||
if templateMessage == "" {
|
||||
templateMessage = ts.generateDefaultServerMessage(payload, resourceType)
|
||||
}
|
||||
|
||||
return ts.replacePlaceholders(templateMessage, payload)
|
||||
}
|
||||
|
||||
// generateServiceMessage creates a message for service notifications using service template
|
||||
func (ts *TelegramService) generateServiceMessage(payload *NotificationPayload, template *ServiceNotificationTemplate) string {
|
||||
var templateMessage string
|
||||
|
||||
// Select appropriate template message based on status
|
||||
switch strings.ToLower(payload.Status) {
|
||||
case "up":
|
||||
templateMessage = template.UpMessage
|
||||
case "down":
|
||||
templateMessage = template.DownMessage
|
||||
case "maintenance":
|
||||
templateMessage = template.MaintenanceMessage
|
||||
case "incident":
|
||||
templateMessage = template.IncidentMessage
|
||||
case "resolved":
|
||||
templateMessage = template.ResolvedMessage
|
||||
case "warning":
|
||||
templateMessage = template.WarningMessage
|
||||
default:
|
||||
templateMessage = template.WarningMessage
|
||||
}
|
||||
|
||||
// If no template message found, use a default
|
||||
if templateMessage == "" {
|
||||
templateMessage = ts.generateDefaultUptimeMessage(payload)
|
||||
}
|
||||
|
||||
return ts.replacePlaceholders(templateMessage, payload)
|
||||
}
|
||||
|
||||
// replacePlaceholders replaces all placeholders in the message with actual values
|
||||
func (ts *TelegramService) replacePlaceholders(message string, payload *NotificationPayload) string {
|
||||
// Replace basic placeholders
|
||||
message = strings.ReplaceAll(message, "${service_name}", payload.ServiceName)
|
||||
message = strings.ReplaceAll(message, "${status}", strings.ToUpper(payload.Status))
|
||||
message = strings.ReplaceAll(message, "${host}", ts.safeString(payload.Host))
|
||||
message = strings.ReplaceAll(message, "${hostname}", ts.safeString(payload.Hostname))
|
||||
|
||||
// Replace URL with fallback to host
|
||||
url := ts.safeString(payload.URL)
|
||||
if url == "N/A" && payload.Host != "" {
|
||||
url = payload.Host
|
||||
}
|
||||
message = strings.ReplaceAll(message, "${url}", url)
|
||||
|
||||
// Replace domain
|
||||
message = strings.ReplaceAll(message, "${domain}", ts.safeString(payload.Domain))
|
||||
|
||||
// Replace service type
|
||||
if payload.ServiceType != "" {
|
||||
message = strings.ReplaceAll(message, "${service_type}", strings.ToUpper(payload.ServiceType))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${service_type}", "N/A")
|
||||
}
|
||||
|
||||
// Replace region and agent info
|
||||
message = strings.ReplaceAll(message, "${region_name}", ts.safeString(payload.RegionName))
|
||||
message = strings.ReplaceAll(message, "${agent_id}", ts.safeString(payload.AgentID))
|
||||
|
||||
// Handle numeric fields safely
|
||||
if payload.Port > 0 {
|
||||
message = strings.ReplaceAll(message, "${port}", fmt.Sprintf("%d", payload.Port))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${port}", "N/A")
|
||||
}
|
||||
|
||||
if payload.ResponseTime > 0 {
|
||||
message = strings.ReplaceAll(message, "${response_time}", fmt.Sprintf("%dms", payload.ResponseTime))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${response_time}", "N/A")
|
||||
}
|
||||
|
||||
if payload.Uptime > 0 {
|
||||
message = strings.ReplaceAll(message, "${uptime}", fmt.Sprintf("%d%%", payload.Uptime))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${uptime}", "N/A")
|
||||
}
|
||||
|
||||
// Replace server monitoring fields
|
||||
message = strings.ReplaceAll(message, "${cpu_usage}", ts.safeString(payload.CPUUsage))
|
||||
message = strings.ReplaceAll(message, "${ram_usage}", ts.safeString(payload.RAMUsage))
|
||||
message = strings.ReplaceAll(message, "${disk_usage}", ts.safeString(payload.DiskUsage))
|
||||
message = strings.ReplaceAll(message, "${network_usage}", ts.safeString(payload.NetworkUsage))
|
||||
message = strings.ReplaceAll(message, "${cpu_temp}", ts.safeString(payload.CPUTemp))
|
||||
message = strings.ReplaceAll(message, "${disk_io}", ts.safeString(payload.DiskIO))
|
||||
message = strings.ReplaceAll(message, "${threshold}", ts.safeString(payload.Threshold))
|
||||
|
||||
// Replace error message - important for uptime services
|
||||
message = strings.ReplaceAll(message, "${error_message}", ts.safeString(payload.ErrorMessage))
|
||||
message = strings.ReplaceAll(message, "${error}", ts.safeString(payload.ErrorMessage))
|
||||
|
||||
// Replace time placeholders
|
||||
message = strings.ReplaceAll(message, "${time}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||
message = strings.ReplaceAll(message, "${timestamp}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
// safeString returns the string value or "N/A" if empty
|
||||
func (ts *TelegramService) safeString(value string) string {
|
||||
if value == "" {
|
||||
return "N/A"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// generateDefaultUptimeMessage creates a default uptime message with proper formatting
|
||||
func (ts *TelegramService) generateDefaultUptimeMessage(payload *NotificationPayload) string {
|
||||
// Status emoji mapping
|
||||
statusEmoji := "🔵"
|
||||
switch strings.ToLower(payload.Status) {
|
||||
case "up":
|
||||
statusEmoji = "🟢"
|
||||
case "down":
|
||||
statusEmoji = "🔴"
|
||||
case "warning":
|
||||
statusEmoji = "🟡"
|
||||
case "maintenance", "paused":
|
||||
statusEmoji = "🟠"
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("%sService %s is %s.", statusEmoji, payload.ServiceName, strings.ToUpper(payload.Status))
|
||||
|
||||
// Build formatted details
|
||||
details := []string{}
|
||||
|
||||
// Add URL or host
|
||||
if payload.URL != "" {
|
||||
details = append(details, fmt.Sprintf(" - Host URL: %s", payload.URL))
|
||||
} else if payload.Host != "" {
|
||||
details = append(details, fmt.Sprintf(" - Host: %s", payload.Host))
|
||||
}
|
||||
|
||||
// Add service type
|
||||
if payload.ServiceType != "" {
|
||||
details = append(details, fmt.Sprintf(" - Type: %s", strings.ToUpper(payload.ServiceType)))
|
||||
}
|
||||
|
||||
// Add port if available
|
||||
if payload.Port > 0 {
|
||||
details = append(details, fmt.Sprintf(" - Port: %d", payload.Port))
|
||||
}
|
||||
|
||||
// Add domain if available
|
||||
if payload.Domain != "" {
|
||||
details = append(details, fmt.Sprintf(" - Domain: %s", payload.Domain))
|
||||
}
|
||||
|
||||
// Add response time
|
||||
if payload.ResponseTime > 0 {
|
||||
details = append(details, fmt.Sprintf(" - Response time: %dms", payload.ResponseTime))
|
||||
} else {
|
||||
details = append(details, " - Response time: N/A")
|
||||
}
|
||||
|
||||
// Add region info
|
||||
if payload.RegionName != "" {
|
||||
details = append(details, fmt.Sprintf(" - Region: %s", payload.RegionName))
|
||||
}
|
||||
|
||||
// Add agent info
|
||||
if payload.AgentID != "" {
|
||||
details = append(details, fmt.Sprintf(" - Agent: %s", payload.AgentID))
|
||||
}
|
||||
|
||||
// Add uptime if available
|
||||
if payload.Uptime > 0 {
|
||||
details = append(details, fmt.Sprintf(" - Uptime: %d%%", payload.Uptime))
|
||||
}
|
||||
|
||||
// Add timestamp
|
||||
details = append(details, fmt.Sprintf(" - Time: %s", payload.Timestamp.Format("2006-01-02 15:04:05")))
|
||||
|
||||
// Combine message with details
|
||||
if len(details) > 0 {
|
||||
message += "\n" + strings.Join(details, "\n")
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
// generateDefaultServerMessage creates a default server message
|
||||
func (ts *TelegramService) generateDefaultServerMessage(payload *NotificationPayload, resourceType string) string {
|
||||
statusEmoji := "🔵"
|
||||
switch strings.ToLower(payload.Status) {
|
||||
case "up":
|
||||
statusEmoji = "🟢"
|
||||
case "down":
|
||||
statusEmoji = "🔴"
|
||||
case "warning":
|
||||
statusEmoji = "🟡"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s🖥️ Server %s (%s) status: %s", statusEmoji, payload.ServiceName, payload.Hostname, strings.ToUpper(payload.Status))
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package notification
|
||||
|
||||
import "time"
|
||||
|
||||
// NotificationPayload represents the data sent in notifications
|
||||
type NotificationPayload struct {
|
||||
ServiceName string `json:"service_name"`
|
||||
Status string `json:"status"`
|
||||
Host string `json:"host"`
|
||||
Hostname string `json:"hostname"`
|
||||
Port int `json:"port"`
|
||||
ServiceType string `json:"service_type"`
|
||||
ResponseTime int64 `json:"response_time"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Message string `json:"message"`
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
|
||||
// Service-specific fields
|
||||
URL string `json:"url,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
RegionName string `json:"region_name,omitempty"`
|
||||
AgentID string `json:"agent_id,omitempty"`
|
||||
Uptime int `json:"uptime,omitempty"`
|
||||
|
||||
// Server monitoring specific fields
|
||||
CPUUsage string `json:"cpu_usage,omitempty"`
|
||||
RAMUsage string `json:"ram_usage,omitempty"`
|
||||
DiskUsage string `json:"disk_usage,omitempty"`
|
||||
NetworkUsage string `json:"network_usage,omitempty"`
|
||||
CPUTemp string `json:"cpu_temp,omitempty"`
|
||||
DiskIO string `json:"disk_io,omitempty"`
|
||||
Threshold string `json:"threshold,omitempty"`
|
||||
|
||||
// SSL Certificate specific fields
|
||||
CertificateName string `json:"certificate_name,omitempty"`
|
||||
ExpiryDate string `json:"expiry_date,omitempty"`
|
||||
DaysLeft string `json:"days_left,omitempty"`
|
||||
IssuerCN string `json:"issuer_cn,omitempty"`
|
||||
SerialNumber string `json:"serial_number,omitempty"`
|
||||
}
|
||||
|
||||
// AlertConfiguration represents an alert configuration from PocketBase
|
||||
type AlertConfiguration struct {
|
||||
ID string `json:"id"`
|
||||
NotificationType string `json:"notification_type"`
|
||||
TelegramChatID string `json:"telegram_chat_id"`
|
||||
DiscordWebhookURL string `json:"discord_webhook_url"`
|
||||
SignalNumber string `json:"signal_number"`
|
||||
SignalAPIEndpoint string `json:"signal_api_endpoint"`
|
||||
NotifyName string `json:"notify_name"`
|
||||
BotToken string `json:"bot_token"`
|
||||
TemplateID string `json:"template_id"`
|
||||
SlackWebhookURL string `json:"slack_webhook_url"`
|
||||
GoogleChatWebhookURL string `json:"google_chat_webhook_url"`
|
||||
Enabled string `json:"enabled"` // String because PocketBase returns it as string
|
||||
EmailAddress string `json:"email_address"`
|
||||
EmailSenderName string `json:"email_sender_name"`
|
||||
SMTPServer string `json:"smtp_server"`
|
||||
SMTPPassword string `json:"smtp_password,omitempty"`
|
||||
SMTPPort string `json:"smtp_port"`
|
||||
WebhookID string `json:"webhook_id"`
|
||||
ChannelID string `json:"channel_id"`
|
||||
// New webhook fields
|
||||
WebhookURL string `json:"webhook_url"`
|
||||
WebhookPayloadTemplate string `json:"webhook_payload_template"`
|
||||
}
|
||||
|
||||
// ServerNotificationTemplate represents a server notification template
|
||||
type ServerNotificationTemplate struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
UpMessage string `json:"up_message"`
|
||||
DownMessage string `json:"down_message"`
|
||||
WarningMessage string `json:"warning_message"`
|
||||
PausedMessage string `json:"paused_message"`
|
||||
// Resource-specific messages
|
||||
RAMMessage string `json:"ram_message"`
|
||||
CPUMessage string `json:"cpu_message"`
|
||||
DiskMessage string `json:"disk_message"`
|
||||
NetworkMessage string `json:"network_message"`
|
||||
CPUTempMessage string `json:"cpu_temp_message"`
|
||||
DiskIOMessage string `json:"disk_io_message"`
|
||||
// Resource restore messages
|
||||
RestoreRAMMessage string `json:"restore_ram_message"`
|
||||
RestoreCPUMessage string `json:"restore_cpu_message"`
|
||||
RestoreDiskMessage string `json:"restore_disk_message"`
|
||||
RestoreNetworkMessage string `json:"restore_network_message"`
|
||||
RestoreCPUTempMessage string `json:"restore_cpu_temp_message"`
|
||||
RestoreDiskIOMessage string `json:"restore_disk_io_message"`
|
||||
Placeholder string `json:"placeholder"`
|
||||
}
|
||||
|
||||
// ServiceNotificationTemplate represents a service notification template
|
||||
type ServiceNotificationTemplate struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
UpMessage string `json:"up_message"`
|
||||
DownMessage string `json:"down_message"`
|
||||
MaintenanceMessage string `json:"maintenance_message"`
|
||||
IncidentMessage string `json:"incident_message"`
|
||||
ResolvedMessage string `json:"resolved_message"`
|
||||
WarningMessage string `json:"warning_message"`
|
||||
Placeholder string `json:"placeholder"`
|
||||
}
|
||||
|
||||
// NotificationResponse represents the response from notification APIs (like Telegram)
|
||||
type NotificationResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
Description string `json:"description,omitempty"`
|
||||
ErrorCode int `json:"error_code,omitempty"`
|
||||
Result interface{} `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
// NotificationService interface for different notification services
|
||||
type NotificationService interface {
|
||||
SendNotification(config *AlertConfiguration, message string) error
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
// "log"
|
||||
"strings"
|
||||
|
||||
"service-operation/pocketbase"
|
||||
)
|
||||
|
||||
// UptimeNotificationManager handles uptime service notifications with IMMEDIATE sending
|
||||
type UptimeNotificationManager struct {
|
||||
pbClient *pocketbase.PocketBaseClient
|
||||
services map[string]NotificationService
|
||||
}
|
||||
|
||||
// NewUptimeNotificationManager creates a new uptime notification manager
|
||||
func NewUptimeNotificationManager(pbClient *pocketbase.PocketBaseClient, services map[string]NotificationService) *UptimeNotificationManager {
|
||||
return &UptimeNotificationManager{
|
||||
pbClient: pbClient,
|
||||
services: services,
|
||||
}
|
||||
}
|
||||
|
||||
// SendUptimeServiceNotification sends notification IMMEDIATELY - NO QUEUING
|
||||
func (unm *UptimeNotificationManager) SendUptimeServiceNotification(payload *NotificationPayload, notificationID, templateID string) error {
|
||||
// log.Printf("📨 [UPTIME-MANAGER] IMMEDIATE send for service: %s, status: %s", payload.ServiceName, payload.Status)
|
||||
// log.Printf("📨 [UPTIME-MANAGER] Notification ID: %s, Template ID: %s", notificationID, templateID)
|
||||
|
||||
if notificationID == "" {
|
||||
return fmt.Errorf("notification ID required for uptime service: %s", payload.ServiceName)
|
||||
}
|
||||
|
||||
// Parse notification IDs
|
||||
notificationIDs := parseNotificationIDs(notificationID)
|
||||
if len(notificationIDs) == 0 {
|
||||
return fmt.Errorf("no valid notification IDs for uptime service: %s", payload.ServiceName)
|
||||
}
|
||||
|
||||
var errors []string
|
||||
successCount := 0
|
||||
|
||||
// Send to each notification channel IMMEDIATELY
|
||||
for _, id := range notificationIDs {
|
||||
// log.Printf("📤 [UPTIME-SEND] Processing notification ID: %s for service %s", id, payload.ServiceName)
|
||||
|
||||
// Check if enabled
|
||||
if !isNotificationEnabled(unm.pbClient, id) {
|
||||
// log.Printf("⚠️ Notification %s disabled for uptime service %s, skipping", id, payload.ServiceName)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get alert configuration
|
||||
alertConfig, err := getAlertConfiguration(unm.pbClient, id)
|
||||
if err != nil {
|
||||
// log.Printf("❌ Failed to get alert config for %s (service: %s): %v", id, payload.ServiceName, err)
|
||||
errors = append(errors, fmt.Sprintf("config error %s: %v", id, err))
|
||||
continue
|
||||
}
|
||||
|
||||
// log.Printf("📋 [UPTIME-CONFIG] Alert config for %s: Type=%s, ChatID=%s, Token present=%v",
|
||||
// id, alertConfig.NotificationType, alertConfig.TelegramChatID, alertConfig.BotToken != "")
|
||||
|
||||
// Get service template
|
||||
var serviceTemplate *ServiceNotificationTemplate
|
||||
if templateID != "" {
|
||||
serviceTemplate, err = getServiceNotificationTemplate(unm.pbClient, templateID)
|
||||
if err != nil {
|
||||
// log.Printf("⚠️ Template error for %s (service: %s): %v", templateID, payload.ServiceName, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate message
|
||||
message := unm.generateUptimeMessage(payload, serviceTemplate)
|
||||
// log.Printf("📝 [UPTIME-MESSAGE] Generated for %s (%s): %s", payload.ServiceName, id, message)
|
||||
|
||||
// Get notification service
|
||||
service, exists := unm.services[alertConfig.NotificationType]
|
||||
if !exists {
|
||||
// log.Printf("❌ Unsupported notification type for uptime: %s", alertConfig.NotificationType)
|
||||
errors = append(errors, fmt.Sprintf("unsupported type %s", alertConfig.NotificationType))
|
||||
continue
|
||||
}
|
||||
|
||||
// SEND IMMEDIATELY - NO DELAYS
|
||||
// log.Printf("⚡ [UPTIME-TELEGRAM] Sending via %s for uptime service %s", alertConfig.NotificationType, payload.ServiceName)
|
||||
err = service.SendNotification(alertConfig, message)
|
||||
if err != nil {
|
||||
// log.Printf("❌ [UPTIME-FAILED] Failed to send via %s for %s: %v", alertConfig.NotificationType, payload.ServiceName, err)
|
||||
errors = append(errors, fmt.Sprintf("send failed %s: %v", alertConfig.NotificationType, err))
|
||||
} else {
|
||||
// log.Printf("✅ [UPTIME-SUCCESS] Successfully sent via %s for %s", alertConfig.NotificationType, payload.ServiceName)
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Report results
|
||||
if successCount > 0 {
|
||||
// log.Printf("✅ [UPTIME-FINAL] Sent %d/%d uptime notifications for %s", successCount, len(notificationIDs), payload.ServiceName)
|
||||
}
|
||||
|
||||
if len(errors) > 0 && successCount == 0 {
|
||||
return fmt.Errorf("all uptime notifications failed for %s: %v", payload.ServiceName, errors)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateUptimeMessage creates notification message for uptime services
|
||||
func (unm *UptimeNotificationManager) generateUptimeMessage(payload *NotificationPayload, template *ServiceNotificationTemplate) string {
|
||||
var baseMessage string
|
||||
|
||||
// Use template if available
|
||||
if template != nil {
|
||||
// log.Printf("🔧 Using uptime template for status: %s", strings.ToLower(payload.Status))
|
||||
switch strings.ToLower(payload.Status) {
|
||||
case "up":
|
||||
baseMessage = template.UpMessage
|
||||
case "down":
|
||||
baseMessage = template.DownMessage
|
||||
case "maintenance":
|
||||
baseMessage = template.MaintenanceMessage
|
||||
case "incident":
|
||||
baseMessage = template.IncidentMessage
|
||||
case "resolved":
|
||||
baseMessage = template.ResolvedMessage
|
||||
case "warning":
|
||||
baseMessage = template.WarningMessage
|
||||
default:
|
||||
baseMessage = template.WarningMessage
|
||||
}
|
||||
}
|
||||
|
||||
// Use default if no template
|
||||
if baseMessage == "" {
|
||||
// log.Printf("🔧 Using default uptime message (no template)")
|
||||
baseMessage = unm.getDefaultUptimeMessage(payload)
|
||||
}
|
||||
|
||||
// Replace placeholders
|
||||
message := unm.replacePlaceholders(baseMessage, payload)
|
||||
|
||||
// log.Printf("📝 Final uptime message: %s", message)
|
||||
return message
|
||||
}
|
||||
|
||||
// replacePlaceholders replaces all placeholders in the message
|
||||
func (unm *UptimeNotificationManager) replacePlaceholders(message string, payload *NotificationPayload) string {
|
||||
message = strings.ReplaceAll(message, "${service_name}", payload.ServiceName)
|
||||
message = strings.ReplaceAll(message, "${status}", strings.ToUpper(payload.Status))
|
||||
message = strings.ReplaceAll(message, "${host}", payload.Host)
|
||||
message = strings.ReplaceAll(message, "${ip}", payload.Host)
|
||||
|
||||
if payload.Port > 0 {
|
||||
message = strings.ReplaceAll(message, "${port}", fmt.Sprintf("%d", payload.Port))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${port}", "N/A")
|
||||
}
|
||||
|
||||
if payload.ResponseTime > 0 {
|
||||
message = strings.ReplaceAll(message, "${response_time}", fmt.Sprintf("%dms", payload.ResponseTime))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${response_time}", "N/A")
|
||||
}
|
||||
|
||||
if payload.URL != "" {
|
||||
message = strings.ReplaceAll(message, "${url}", payload.URL)
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${url}", "N/A")
|
||||
}
|
||||
|
||||
if payload.Domain != "" {
|
||||
message = strings.ReplaceAll(message, "${domain}", payload.Domain)
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${domain}", "N/A")
|
||||
}
|
||||
|
||||
if payload.ServiceType != "" {
|
||||
message = strings.ReplaceAll(message, "${service_type}", strings.ToUpper(payload.ServiceType))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${service_type}", "N/A")
|
||||
}
|
||||
|
||||
if payload.RegionName != "" {
|
||||
message = strings.ReplaceAll(message, "${region_name}", payload.RegionName)
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${region_name}", "N/A")
|
||||
}
|
||||
|
||||
if payload.AgentID != "" {
|
||||
message = strings.ReplaceAll(message, "${agent_id}", payload.AgentID)
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${agent_id}", "N/A")
|
||||
}
|
||||
|
||||
if payload.Uptime > 0 {
|
||||
message = strings.ReplaceAll(message, "${uptime}", fmt.Sprintf("%d%%", payload.Uptime))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${uptime}", "N/A")
|
||||
}
|
||||
|
||||
message = strings.ReplaceAll(message, "${timestamp}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||
message = strings.ReplaceAll(message, "${time}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||
message = strings.ReplaceAll(message, "${date}", payload.Timestamp.Format("2006-01-02"))
|
||||
|
||||
// Handle error message placeholder - critical for uptime services
|
||||
if payload.ErrorMessage != "" {
|
||||
message = strings.ReplaceAll(message, "${error}", payload.ErrorMessage)
|
||||
message = strings.ReplaceAll(message, "${error_message}", payload.ErrorMessage)
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${error}", "")
|
||||
message = strings.ReplaceAll(message, "${error_message}", "")
|
||||
}
|
||||
|
||||
if payload.Message != "" {
|
||||
message = strings.ReplaceAll(message, "${message}", payload.Message)
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
// getDefaultUptimeMessage provides a default notification message for uptime services
|
||||
func (unm *UptimeNotificationManager) getDefaultUptimeMessage(payload *NotificationPayload) string {
|
||||
statusEmoji := "✅"
|
||||
if payload.Status == "down" {
|
||||
statusEmoji = "❌"
|
||||
} else if payload.Status == "warning" {
|
||||
statusEmoji = "⚠️"
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("%s [UPTIME] %s is %s", statusEmoji, payload.ServiceName, strings.ToUpper(payload.Status))
|
||||
|
||||
if payload.Host != "" {
|
||||
message += fmt.Sprintf(" | Host: %s", payload.Host)
|
||||
}
|
||||
|
||||
if payload.ResponseTime > 0 {
|
||||
message += fmt.Sprintf(" | %dms", payload.ResponseTime)
|
||||
}
|
||||
|
||||
message += fmt.Sprintf(" | %s", payload.Timestamp.Format("15:04:05"))
|
||||
|
||||
return message
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
|
||||
package notification
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
// WebhookService handles webhook notifications
|
||||
type WebhookService struct{}
|
||||
|
||||
// NewWebhookService creates a new webhook notification service
|
||||
func NewWebhookService() *WebhookService {
|
||||
return &WebhookService{}
|
||||
}
|
||||
|
||||
// WebhookPayload represents the payload for webhook
|
||||
type WebhookPayload struct {
|
||||
Message string `json:"message"`
|
||||
NotifyName string `json:"notify_name"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
ServiceName string `json:"service_name,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
ServiceType string `json:"service_type,omitempty"`
|
||||
ResponseTime int64 `json:"response_time,omitempty"`
|
||||
// Server monitoring fields
|
||||
CPUUsage string `json:"cpu_usage,omitempty"`
|
||||
RAMUsage string `json:"ram_usage,omitempty"`
|
||||
DiskUsage string `json:"disk_usage,omitempty"`
|
||||
NetworkUsage string `json:"network_usage,omitempty"`
|
||||
CPUTemp string `json:"cpu_temp,omitempty"`
|
||||
DiskIO string `json:"disk_io,omitempty"`
|
||||
Threshold string `json:"threshold,omitempty"`
|
||||
// SSL fields
|
||||
CertificateName string `json:"certificate_name,omitempty"`
|
||||
ExpiryDate string `json:"expiry_date,omitempty"`
|
||||
DaysLeft string `json:"days_left,omitempty"`
|
||||
IssuerCN string `json:"issuer_cn,omitempty"`
|
||||
}
|
||||
|
||||
// DiscordWebhookPayload represents Discord-specific webhook payload
|
||||
type DiscordWebhookPayload struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// SendNotification sends a notification via webhook
|
||||
func (ws *WebhookService) SendNotification(config *AlertConfiguration, message string) error {
|
||||
// fmt.Printf("📡 [WEBHOOK] Attempting to send notification...\n")
|
||||
// fmt.Printf("📡 [WEBHOOK] Config - URL: %s, Notify Name: %s\n", config.WebhookURL, config.NotifyName)
|
||||
// fmt.Printf("📡 [WEBHOOK] Message: %s\n", message)
|
||||
|
||||
// Use the webhook_url field from alert_configurations
|
||||
if config.WebhookURL == "" {
|
||||
return fmt.Errorf("webhook URL is required")
|
||||
}
|
||||
|
||||
// Create payload using webhook_payload_template if available
|
||||
var jsonData []byte
|
||||
var err error
|
||||
|
||||
if config.WebhookPayloadTemplate != "" && strings.TrimSpace(config.WebhookPayloadTemplate) != "" {
|
||||
// Use custom payload template
|
||||
jsonData, err = ws.generateCustomPayload(config, message)
|
||||
if err != nil {
|
||||
// fmt.Printf("❌ [WEBHOOK] Custom payload generation error: %v\n", err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Detect Discord webhook and use appropriate payload
|
||||
if strings.Contains(strings.ToLower(config.WebhookURL), "discord") {
|
||||
jsonData, err = ws.generateDiscordPayload(message)
|
||||
} else {
|
||||
// Use default payload structure for other webhooks
|
||||
jsonData, err = ws.generateDefaultPayload(config, message)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// fmt.Printf("❌ [WEBHOOK] JSON marshal error: %v\n", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// fmt.Printf("📡 [WEBHOOK] Payload: %s\n", string(jsonData))
|
||||
// fmt.Printf("📡 [WEBHOOK] Sending POST request...\n")
|
||||
|
||||
resp, err := http.Post(config.WebhookURL, "application/json", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
// fmt.Printf("❌ [WEBHOOK] HTTP error: %v\n", err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
// fmt.Printf("❌ [WEBHOOK] API error, status: %d\n", resp.StatusCode)
|
||||
return fmt.Errorf("webhook error, status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// fmt.Printf("✅ [WEBHOOK] Message sent successfully!\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateDiscordPayload generates a Discord-compatible webhook payload
|
||||
func (ws *WebhookService) generateDiscordPayload(message string) ([]byte, error) {
|
||||
payload := DiscordWebhookPayload{
|
||||
Content: message,
|
||||
}
|
||||
return json.Marshal(payload)
|
||||
}
|
||||
|
||||
// generateDefaultPayload generates the default webhook payload
|
||||
func (ws *WebhookService) generateDefaultPayload(config *AlertConfiguration, message string) ([]byte, error) {
|
||||
payload := WebhookPayload{
|
||||
Message: message,
|
||||
NotifyName: config.NotifyName,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
return json.Marshal(payload)
|
||||
}
|
||||
|
||||
// generateCustomPayload generates payload using the custom template
|
||||
func (ws *WebhookService) generateCustomPayload(config *AlertConfiguration, message string) ([]byte, error) {
|
||||
// Parse the template
|
||||
tmpl, err := template.New("webhook").Parse(config.WebhookPayloadTemplate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse webhook payload template: %v", err)
|
||||
}
|
||||
|
||||
// Create template data
|
||||
templateData := map[string]interface{}{
|
||||
"message": message,
|
||||
"notify_name": config.NotifyName,
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
// Execute template
|
||||
var buf bytes.Buffer
|
||||
err = tmpl.Execute(&buf, templateData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute webhook payload template: %v", err)
|
||||
}
|
||||
|
||||
// Validate that the result is valid JSON
|
||||
var jsonCheck interface{}
|
||||
if err := json.Unmarshal(buf.Bytes(), &jsonCheck); err != nil {
|
||||
return nil, fmt.Errorf("webhook payload template did not generate valid JSON: %v", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// SendServerNotification sends a server-specific notification via webhook
|
||||
func (ws *WebhookService) SendServerNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) error {
|
||||
message := ws.generateServerMessage(payload, template, resourceType)
|
||||
return ws.SendNotification(config, message)
|
||||
}
|
||||
|
||||
// SendServiceNotification sends a service-specific notification via webhook
|
||||
func (ws *WebhookService) SendServiceNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServiceNotificationTemplate) error {
|
||||
message := ws.generateServiceMessage(payload, template)
|
||||
return ws.SendNotification(config, message)
|
||||
}
|
||||
|
||||
// generateServerMessage creates a message for server notifications using server template
|
||||
func (ws *WebhookService) generateServerMessage(payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) string {
|
||||
var templateMessage string
|
||||
|
||||
// Select appropriate template message based on status and resource type
|
||||
switch strings.ToLower(payload.Status) {
|
||||
case "down":
|
||||
templateMessage = template.DownMessage
|
||||
case "up":
|
||||
templateMessage = template.UpMessage
|
||||
case "warning":
|
||||
templateMessage = template.WarningMessage
|
||||
case "paused":
|
||||
templateMessage = template.PausedMessage
|
||||
default:
|
||||
// Handle resource-specific messages
|
||||
switch resourceType {
|
||||
case "cpu":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreCPUMessage
|
||||
} else {
|
||||
templateMessage = template.CPUMessage
|
||||
}
|
||||
case "ram", "memory":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreRAMMessage
|
||||
} else {
|
||||
templateMessage = template.RAMMessage
|
||||
}
|
||||
case "disk":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreDiskMessage
|
||||
} else {
|
||||
templateMessage = template.DiskMessage
|
||||
}
|
||||
case "network":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreNetworkMessage
|
||||
} else {
|
||||
templateMessage = template.NetworkMessage
|
||||
}
|
||||
case "cpu_temp", "cpu_temperature":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreCPUTempMessage
|
||||
} else {
|
||||
templateMessage = template.CPUTempMessage
|
||||
}
|
||||
case "disk_io":
|
||||
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||
templateMessage = template.RestoreDiskIOMessage
|
||||
} else {
|
||||
templateMessage = template.DiskIOMessage
|
||||
}
|
||||
default:
|
||||
templateMessage = template.WarningMessage
|
||||
}
|
||||
}
|
||||
|
||||
// If no template message found, use a default
|
||||
if templateMessage == "" {
|
||||
templateMessage = ws.generateDefaultServerMessage(payload, resourceType)
|
||||
}
|
||||
|
||||
return ws.replacePlaceholders(templateMessage, payload)
|
||||
}
|
||||
|
||||
// generateServiceMessage creates a message for service notifications using service template
|
||||
func (ws *WebhookService) generateServiceMessage(payload *NotificationPayload, template *ServiceNotificationTemplate) string {
|
||||
var templateMessage string
|
||||
|
||||
// Select appropriate template message based on status
|
||||
switch strings.ToLower(payload.Status) {
|
||||
case "up":
|
||||
templateMessage = template.UpMessage
|
||||
case "down":
|
||||
templateMessage = template.DownMessage
|
||||
case "maintenance":
|
||||
templateMessage = template.MaintenanceMessage
|
||||
case "incident":
|
||||
templateMessage = template.IncidentMessage
|
||||
case "resolved":
|
||||
templateMessage = template.ResolvedMessage
|
||||
case "warning":
|
||||
templateMessage = template.WarningMessage
|
||||
default:
|
||||
templateMessage = template.WarningMessage
|
||||
}
|
||||
|
||||
// If no template message found, use a default
|
||||
if templateMessage == "" {
|
||||
templateMessage = ws.generateDefaultUptimeMessage(payload)
|
||||
}
|
||||
|
||||
return ws.replacePlaceholders(templateMessage, payload)
|
||||
}
|
||||
|
||||
// replacePlaceholders replaces all placeholders in the message with actual values
|
||||
func (ws *WebhookService) replacePlaceholders(message string, payload *NotificationPayload) string {
|
||||
// Replace basic placeholders
|
||||
message = strings.ReplaceAll(message, "${service_name}", payload.ServiceName)
|
||||
message = strings.ReplaceAll(message, "${server_name}", payload.ServiceName) // server_name maps to service_name
|
||||
message = strings.ReplaceAll(message, "${status}", strings.ToUpper(payload.Status))
|
||||
message = strings.ReplaceAll(message, "${host}", ws.safeString(payload.Host))
|
||||
message = strings.ReplaceAll(message, "${hostname}", ws.safeString(payload.Hostname))
|
||||
|
||||
// Replace URL with fallback to host
|
||||
url := ws.safeString(payload.URL)
|
||||
if url == "N/A" && payload.Host != "" {
|
||||
url = payload.Host
|
||||
}
|
||||
message = strings.ReplaceAll(message, "${url}", url)
|
||||
|
||||
// Replace domain
|
||||
message = strings.ReplaceAll(message, "${domain}", ws.safeString(payload.Domain))
|
||||
|
||||
// Replace service type
|
||||
if payload.ServiceType != "" {
|
||||
message = strings.ReplaceAll(message, "${service_type}", strings.ToUpper(payload.ServiceType))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${service_type}", "N/A")
|
||||
}
|
||||
|
||||
// Replace region and agent info
|
||||
message = strings.ReplaceAll(message, "${region_name}", ws.safeString(payload.RegionName))
|
||||
message = strings.ReplaceAll(message, "${agent_id}", ws.safeString(payload.AgentID))
|
||||
|
||||
// Handle numeric fields safely
|
||||
if payload.Port > 0 {
|
||||
message = strings.ReplaceAll(message, "${port}", fmt.Sprintf("%d", payload.Port))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${port}", "N/A")
|
||||
}
|
||||
|
||||
if payload.ResponseTime > 0 {
|
||||
message = strings.ReplaceAll(message, "${response_time}", fmt.Sprintf("%dms", payload.ResponseTime))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${response_time}", "N/A")
|
||||
}
|
||||
|
||||
if payload.Uptime > 0 {
|
||||
message = strings.ReplaceAll(message, "${uptime}", fmt.Sprintf("%d%%", payload.Uptime))
|
||||
} else {
|
||||
message = strings.ReplaceAll(message, "${uptime}", "N/A")
|
||||
}
|
||||
|
||||
// Replace server monitoring fields
|
||||
message = strings.ReplaceAll(message, "${cpu_usage}", ws.safeString(payload.CPUUsage))
|
||||
message = strings.ReplaceAll(message, "${ram_usage}", ws.safeString(payload.RAMUsage))
|
||||
message = strings.ReplaceAll(message, "${disk_usage}", ws.safeString(payload.DiskUsage))
|
||||
message = strings.ReplaceAll(message, "${network_usage}", ws.safeString(payload.NetworkUsage))
|
||||
message = strings.ReplaceAll(message, "${cpu_temp}", ws.safeString(payload.CPUTemp))
|
||||
message = strings.ReplaceAll(message, "${disk_io}", ws.safeString(payload.DiskIO))
|
||||
message = strings.ReplaceAll(message, "${threshold}", ws.safeString(payload.Threshold))
|
||||
|
||||
// Replace SSL certificate fields
|
||||
message = strings.ReplaceAll(message, "${certificate_name}", ws.safeString(payload.CertificateName))
|
||||
message = strings.ReplaceAll(message, "${expiry_date}", ws.safeString(payload.ExpiryDate))
|
||||
message = strings.ReplaceAll(message, "${days_left}", ws.safeString(payload.DaysLeft))
|
||||
message = strings.ReplaceAll(message, "${issuer_cn}", ws.safeString(payload.IssuerCN))
|
||||
message = strings.ReplaceAll(message, "${issuer}", ws.safeString(payload.IssuerCN)) // alias
|
||||
|
||||
// Replace error message - important for uptime services
|
||||
message = strings.ReplaceAll(message, "${error_message}", ws.safeString(payload.ErrorMessage))
|
||||
message = strings.ReplaceAll(message, "${error}", ws.safeString(payload.ErrorMessage))
|
||||
message = strings.ReplaceAll(message, "${message}", ws.safeString(payload.Message))
|
||||
|
||||
// Replace time placeholders
|
||||
message = strings.ReplaceAll(message, "${time}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||
message = strings.ReplaceAll(message, "${timestamp}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
// safeString returns the string value or "N/A" if empty
|
||||
func (ws *WebhookService) safeString(value string) string {
|
||||
if value == "" {
|
||||
return "N/A"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// generateDefaultUptimeMessage creates a default uptime message with proper formatting
|
||||
func (ws *WebhookService) generateDefaultUptimeMessage(payload *NotificationPayload) string {
|
||||
// Status emoji mapping
|
||||
statusEmoji := "🔵"
|
||||
switch strings.ToLower(payload.Status) {
|
||||
case "up":
|
||||
statusEmoji = "🟢"
|
||||
case "down":
|
||||
statusEmoji = "🔴"
|
||||
case "warning":
|
||||
statusEmoji = "🟡"
|
||||
case "maintenance", "paused":
|
||||
statusEmoji = "🟠"
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("%s Service %s is %s.", statusEmoji, payload.ServiceName, strings.ToUpper(payload.Status))
|
||||
|
||||
// Build formatted details
|
||||
details := []string{}
|
||||
|
||||
// Add URL or host
|
||||
if payload.URL != "" {
|
||||
details = append(details, fmt.Sprintf(" - Host URL: %s", payload.URL))
|
||||
} else if payload.Host != "" {
|
||||
details = append(details, fmt.Sprintf(" - Host: %s", payload.Host))
|
||||
}
|
||||
|
||||
// Add service type
|
||||
if payload.ServiceType != "" {
|
||||
details = append(details, fmt.Sprintf(" - Type: %s", strings.ToUpper(payload.ServiceType)))
|
||||
}
|
||||
|
||||
// Add port if available
|
||||
if payload.Port > 0 {
|
||||
details = append(details, fmt.Sprintf(" - Port: %d", payload.Port))
|
||||
}
|
||||
|
||||
// Add domain if available
|
||||
if payload.Domain != "" {
|
||||
details = append(details, fmt.Sprintf(" - Domain: %s", payload.Domain))
|
||||
}
|
||||
|
||||
// Add response time
|
||||
if payload.ResponseTime > 0 {
|
||||
details = append(details, fmt.Sprintf(" - Response time: %dms", payload.ResponseTime))
|
||||
} else {
|
||||
details = append(details, " - Response time: N/A")
|
||||
}
|
||||
|
||||
// Add region info
|
||||
if payload.RegionName != "" {
|
||||
details = append(details, fmt.Sprintf(" - Region: %s", payload.RegionName))
|
||||
}
|
||||
|
||||
// Add agent info
|
||||
if payload.AgentID != "" {
|
||||
details = append(details, fmt.Sprintf(" - Agent: %s", payload.AgentID))
|
||||
}
|
||||
|
||||
// Add uptime if available
|
||||
if payload.Uptime > 0 {
|
||||
details = append(details, fmt.Sprintf(" - Uptime: %d%%", payload.Uptime))
|
||||
}
|
||||
|
||||
// Add timestamp
|
||||
details = append(details, fmt.Sprintf(" - Time: %s", payload.Timestamp.Format("2006-01-02 15:04:05")))
|
||||
|
||||
// Combine message with details
|
||||
if len(details) > 0 {
|
||||
message += "\n" + strings.Join(details, "\n")
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
// generateDefaultServerMessage creates a default server message
|
||||
func (ws *WebhookService) generateDefaultServerMessage(payload *NotificationPayload, resourceType string) string {
|
||||
statusEmoji := "🔵"
|
||||
switch strings.ToLower(payload.Status) {
|
||||
case "up":
|
||||
statusEmoji = "🟢"
|
||||
case "down":
|
||||
statusEmoji = "🔴"
|
||||
case "warning":
|
||||
statusEmoji = "🟡"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s 🖥️ Server %s (%s) status: %s", statusEmoji, payload.ServiceName, payload.Hostname, strings.ToUpper(payload.Status))
|
||||
}
|
||||
Reference in New Issue
Block a user