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,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
|
||||
}
|
||||
Reference in New Issue
Block a user