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,192 @@
|
||||
package sslmonitoring
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"service-operation/notification"
|
||||
"service-operation/pocketbase"
|
||||
)
|
||||
|
||||
// SSLNotificationService handles SSL certificate notifications
|
||||
type SSLNotificationService struct {
|
||||
client *SSLClient
|
||||
pbClient *pocketbase.PocketBaseClient
|
||||
notificationManager *notification.NotificationManager
|
||||
statusTracker *SSLStatusTracker
|
||||
}
|
||||
|
||||
// NewSSLNotificationService creates a new SSL notification service
|
||||
func NewSSLNotificationService(client *SSLClient, pbClient *pocketbase.PocketBaseClient) *SSLNotificationService {
|
||||
return &SSLNotificationService{
|
||||
client: client,
|
||||
pbClient: pbClient,
|
||||
notificationManager: notification.NewNotificationManager(pbClient),
|
||||
statusTracker: NewSSLStatusTracker(pbClient), // Pass pbClient to status tracker
|
||||
}
|
||||
}
|
||||
|
||||
// CheckAndNotifySSLCertificate checks SSL certificate and sends notification if needed
|
||||
func (sns *SSLNotificationService) CheckAndNotifySSLCertificate(cert SSLCertificate) error {
|
||||
// Always recalculate days left from the actual expiry date to ensure accuracy
|
||||
actualDaysLeft := sns.calculateDaysLeft(cert.ValidTill)
|
||||
|
||||
// Use the calculated value as it's more accurate
|
||||
cert.DaysLeft = actualDaysLeft
|
||||
|
||||
// Determine current status based on thresholds with calculated days left
|
||||
currentStatus := sns.determineSSLStatus(cert.DaysLeft, cert.WarningThreshold, cert.ExpiryThreshold)
|
||||
|
||||
// Check if notification should be sent
|
||||
if !sns.shouldSendNotification(cert.ID, currentStatus) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip if no notification configuration
|
||||
if cert.NotificationID == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send notification
|
||||
err := sns.sendSSLNotification(cert, currentStatus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update tracking in database
|
||||
sns.statusTracker.UpdateStatus(cert.ID, currentStatus)
|
||||
sns.statusTracker.SetLastNotificationTime(cert.ID, time.Now())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateDaysLeft calculates days remaining until expiration with better error handling
|
||||
func (sns *SSLNotificationService) calculateDaysLeft(validTill string) int {
|
||||
// Try multiple date formats to parse the expiry date
|
||||
var expiryTime time.Time
|
||||
var err error
|
||||
|
||||
// Common date formats - updated to handle PocketBase format correctly
|
||||
formats := []string{
|
||||
"2006-01-02 15:04:05.000Z", // PocketBase format with space (most common)
|
||||
"2006-01-02T15:04:05.000Z", // ISO 8601 with milliseconds
|
||||
time.RFC3339, // Standard RFC3339
|
||||
time.RFC3339Nano, // RFC3339 with nanoseconds
|
||||
"2006-01-02 15:04:05Z", // Without milliseconds
|
||||
"2006-01-02T15:04:05Z", // ISO without milliseconds
|
||||
"2006-01-02 15:04:05.999Z", // Alternative milliseconds format
|
||||
"2006-01-02T15:04:05.999Z", // Alternative ISO milliseconds
|
||||
"2006-01-02 15:04:05.000000Z", // Microseconds format
|
||||
"2006-01-02T15:04:05.000000Z", // ISO microseconds format
|
||||
"2006-01-02 15:04:05", // Simple format without timezone
|
||||
"2006-01-02", // Date only
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
expiryTime, err = time.Parse(format, validTill)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
duration := expiryTime.Sub(now)
|
||||
daysLeft := int(duration.Hours() / 24)
|
||||
|
||||
// If the result is negative, the certificate is expired
|
||||
if daysLeft < 0 {
|
||||
daysLeft = 0
|
||||
}
|
||||
|
||||
return daysLeft
|
||||
}
|
||||
|
||||
// sendSSLNotification sends the actual notification
|
||||
func (sns *SSLNotificationService) sendSSLNotification(cert SSLCertificate, status string) error {
|
||||
// Determine the correct issuer value - prioritize IssuerCN over IssuerO
|
||||
issuerValue := ""
|
||||
if cert.IssuerCN != "" {
|
||||
issuerValue = cert.IssuerCN
|
||||
} else if cert.IssuerO != "" {
|
||||
issuerValue = cert.IssuerO
|
||||
} else {
|
||||
issuerValue = "Unknown"
|
||||
}
|
||||
|
||||
// Create notification payload with SSL-specific data using calculated values
|
||||
payload := ¬ification.NotificationPayload{
|
||||
ServiceName: fmt.Sprintf("SSL Certificate - %s", cert.Domain),
|
||||
Status: status,
|
||||
Host: cert.Domain,
|
||||
Domain: cert.Domain,
|
||||
ServiceType: "ssl",
|
||||
Timestamp: time.Now(),
|
||||
Message: sns.generateStatusMessage(cert, status),
|
||||
|
||||
// SSL-specific fields with calculated/corrected values
|
||||
CertificateName: cert.Domain,
|
||||
ExpiryDate: cert.ValidTill,
|
||||
DaysLeft: strconv.Itoa(cert.DaysLeft), // Use the calculated value
|
||||
IssuerCN: issuerValue,
|
||||
SerialNumber: cert.SerialNumber,
|
||||
}
|
||||
|
||||
// Send SSL notification using the manager
|
||||
return sns.notificationManager.SendSSLNotification(payload, cert.NotificationID, cert.TemplateID)
|
||||
}
|
||||
|
||||
// generateStatusMessage creates appropriate message based on certificate status using calculated days
|
||||
func (sns *SSLNotificationService) generateStatusMessage(cert SSLCertificate, status string) string {
|
||||
switch status {
|
||||
case "expired":
|
||||
return fmt.Sprintf("SSL certificate for %s expired on %s", cert.Domain, cert.ValidTill)
|
||||
case "expiring_soon":
|
||||
return fmt.Sprintf("SSL certificate for %s expires in %d days on %s", cert.Domain, cert.DaysLeft, cert.ValidTill)
|
||||
case "warning":
|
||||
return fmt.Sprintf("SSL certificate for %s expires in %d days", cert.Domain, cert.DaysLeft)
|
||||
default:
|
||||
return fmt.Sprintf("SSL certificate for %s is valid (%d days remaining)", cert.Domain, cert.DaysLeft)
|
||||
}
|
||||
}
|
||||
|
||||
// shouldSendNotification determines if notification should be sent with enhanced logic
|
||||
func (sns *SSLNotificationService) shouldSendNotification(certID, currentStatus string) bool {
|
||||
lastStatus := sns.statusTracker.GetLastStatus(certID)
|
||||
lastNotified := sns.statusTracker.GetLastNotificationTime(certID)
|
||||
|
||||
// Send if status changed or first check
|
||||
if lastStatus == "" || lastStatus != currentStatus {
|
||||
return true
|
||||
}
|
||||
|
||||
// For critical statuses (expired/expiring_soon), resend after 24 hours
|
||||
if (currentStatus == "expired" || currentStatus == "expiring_soon") &&
|
||||
!lastNotified.IsZero() && time.Since(lastNotified) > 24*time.Hour {
|
||||
return true
|
||||
}
|
||||
|
||||
// For warning status, resend after 7 days to avoid spam
|
||||
if currentStatus == "warning" &&
|
||||
!lastNotified.IsZero() && time.Since(lastNotified) > 7*24*time.Hour {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// determineSSLStatus determines SSL certificate status based on days left and thresholds
|
||||
func (sns *SSLNotificationService) determineSSLStatus(daysLeft, warningThreshold, expiryThreshold int) string {
|
||||
if daysLeft <= 0 {
|
||||
return "expired"
|
||||
} else if daysLeft <= expiryThreshold {
|
||||
return "expiring_soon"
|
||||
} else if daysLeft <= warningThreshold {
|
||||
return "warning"
|
||||
}
|
||||
return "valid"
|
||||
}
|
||||
Reference in New Issue
Block a user