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:
Tola Leng
2025-08-14 20:50:31 +07:00
parent 58242c33f8
commit 933e70a3f6
40 changed files with 7159 additions and 97 deletions
@@ -0,0 +1,16 @@
package servermonitoring
import (
"time"
)
// LogServerUpdateAttempt logs when we attempt to update a server record
func LogServerUpdateAttempt(serverID, operation, details string) {
// log.Printf("🔄 [UPDATE ATTEMPT] Server: %s | Operation: %s | Details: %s | Time: %s",
// serverID, operation, details, time.Now().Format("2006-01-02 15:04:05"))
_ = serverID
_ = operation
_ = details
_ = time.Now()
}
@@ -0,0 +1,46 @@
package servermonitoring
// DebugServerFields helps debug server field preservation issues
func (c *ServerPocketBaseClient) DebugServerFields(serverID string, operation string) {
// log.Printf("🔍 === DEBUG: %s for server %s ===", operation, serverID)
servers, err := c.GetAllServers()
if err != nil {
// log.Printf("❌ Debug failed to fetch servers: %v", err)
_ = err
return
}
for _, server := range servers {
if server.ID == serverID || server.ServerID == serverID {
// log.Printf("📊 Server Debug Info:")
// log.Printf(" - Server ID: %s", server.ID)
// log.Printf(" - Server Name: %s", server.Name)
// log.Printf(" - Status: %s", server.Status)
// log.Printf(" - Notification ID: '%s'", server.NotificationID)
// log.Printf(" - Template ID: '%s'", server.TemplateID)
// log.Printf(" - Threshold ID: '%s'", server.ThresholdID)
// log.Printf(" - Agent Status: %s", server.AgentStatus)
// log.Printf(" - Check Interval: %d", server.CheckInterval)
// Check for empty strings vs nil
if server.NotificationID == "" {
// log.Printf("⚠️ WARNING: Notification ID is empty string")
}
if server.TemplateID == "" {
// log.Printf("⚠️ WARNING: Template ID is empty string")
}
if server.ThresholdID == "" {
// log.Printf("⚠️ WARNING: Threshold ID is empty string")
}
_ = server
break
}
}
// log.Printf("🔍 === END DEBUG: %s ===", operation)
_ = operation
_ = serverID
}
@@ -0,0 +1,40 @@
package servermonitoring
import (
"service-operation/pocketbase"
)
// ServerMonitoringService provides an easy interface for server monitoring
type ServerMonitoringService struct {
monitor *ServerMonitor
}
// NewServerMonitoringService creates a new server monitoring service
func NewServerMonitoringService(pbClient *pocketbase.PocketBaseClient) *ServerMonitoringService {
return &ServerMonitoringService{
monitor: NewServerMonitor(pbClient),
}
}
// Start starts the server monitoring service
func (sms *ServerMonitoringService) Start() {
sms.monitor.Start()
}
// Stop stops the server monitoring service
func (sms *ServerMonitoringService) Stop() {
sms.monitor.Stop()
}
// GetMonitor returns the underlying monitor for advanced usage
func (sms *ServerMonitoringService) GetMonitor() *ServerMonitor {
return sms.monitor
}
// IsRunning returns whether the monitoring service is currently running
func (sms *ServerMonitoringService) IsRunning() bool {
sms.monitor.mu.RLock()
defer sms.monitor.mu.RUnlock()
return sms.monitor.isRunning
}
@@ -0,0 +1,462 @@
package servermonitoring
import (
"fmt"
"strconv"
"sync"
"time"
"service-operation/pocketbase"
)
// StatusAlert tracks alert state for server status changes
type StatusAlert struct {
IsActive bool
Status string
LastAlerted time.Time
RetryCount int
}
// ServerMonitor tracks server metrics and sends notifications
type ServerMonitor struct {
pbClient *ServerPocketBaseClient
notificationService *ServerNotificationService
thresholdMonitor *ThresholdMonitor
lastStatuses map[string]string // Track last known status for each server
activeStatusAlerts map[string]*StatusAlert // Track status alerts with retry counts
mu sync.RWMutex
stopChan chan bool
isRunning bool
}
// NewServerMonitor creates a new server monitor
func NewServerMonitor(pbClient *pocketbase.PocketBaseClient) *ServerMonitor {
// log.Println("🔧 Creating new ServerMonitor instance")
serverPBClient := NewServerPocketBaseClient(pbClient)
notificationService := NewServerNotificationService(pbClient)
thresholdMonitor := NewThresholdMonitor(serverPBClient, notificationService)
return &ServerMonitor{
pbClient: serverPBClient,
notificationService: notificationService,
thresholdMonitor: thresholdMonitor,
lastStatuses: make(map[string]string),
activeStatusAlerts: make(map[string]*StatusAlert),
stopChan: make(chan bool),
isRunning: false,
}
}
// Start begins monitoring servers
func (sm *ServerMonitor) Start() {
sm.mu.Lock()
defer sm.mu.Unlock()
if sm.isRunning {
// log.Println("⚠️ Server monitor is already running")
return
}
sm.isRunning = true
// log.Println("🚀 Starting server monitoring service...")
go sm.monitoringLoop()
}
// Stop stops monitoring servers
func (sm *ServerMonitor) Stop() {
sm.mu.Lock()
defer sm.mu.Unlock()
if !sm.isRunning {
// log.Println("⚠️ Server monitor is not running")
return
}
// log.Println("🛑 Stopping server monitoring service...")
sm.isRunning = false
sm.stopChan <- true
}
// monitoringLoop runs the main monitoring loop
func (sm *ServerMonitor) monitoringLoop() {
// log.Println("🔄 Starting monitoring loop with 30-second intervals")
ticker := time.NewTicker(30 * time.Second) // Check every 30 seconds for more responsive monitoring
defer ticker.Stop()
// Initial check
// log.Println("🔍 Performing initial server check...")
sm.checkAllServers()
for {
select {
case <-ticker.C:
// log.Println("⏰ Timer triggered - checking all servers")
sm.checkAllServers()
case <-sm.stopChan:
// log.Println("🛑 Stop signal received - exiting monitoring loop")
return
}
}
}
// checkAllServers checks all servers for missing metrics
func (sm *ServerMonitor) checkAllServers() {
// log.Println("📋 Fetching all servers from database...")
servers, err := sm.pbClient.GetAllServers()
if err != nil {
// log.Printf("❌ CRITICAL ERROR: Failed to fetch servers: %v", err)
_ = err
return
}
// log.Printf("✅ Successfully fetched %d servers", len(servers))
if len(servers) == 0 {
// log.Println("⚠️ No servers found in database")
return
}
for i, server := range servers {
// log.Printf("🔍 [%d/%d] Processing server: %s (ID: %s)", i+1, len(servers), server.Name, server.ServerID)
sm.checkServerMetrics(server)
// log.Printf("✅ [%d/%d] Completed processing server: %s", i+1, len(servers), server.Name)
_ = i
}
// log.Println("📋 Completed checking all servers")
}
// checkServerMetrics checks if a server has recent metrics and agent status
func (sm *ServerMonitor) checkServerMetrics(server Server) {
// log.Printf("🔍 === DETAILED SERVER CHECK FOR: %s ===", server.Name)
// Debug server fields BEFORE any operations
sm.pbClient.DebugServerFields(server.ID, "BEFORE_CHECK")
// log.Printf("📊 Server Details:")
// log.Printf(" - ID: %s", server.ID)
// log.Printf(" - Server ID: %s", server.ServerID)
// log.Printf(" - Name: %s", server.Name)
// log.Printf(" - Current Status: %s", server.Status)
// log.Printf(" - Agent Status: %s", server.AgentStatus)
// log.Printf(" - Check Interval: %d minutes", server.CheckInterval)
// log.Printf(" - Notification ID: '%s'", server.NotificationID)
// log.Printf(" - Template ID: '%s'", server.TemplateID)
// log.Printf(" - Threshold ID: '%s'", server.ThresholdID)
// log.Printf(" - IP Address: %s", server.IPAddress)
// log.Printf(" - OS Type: %s", server.OSType)
// log.Printf(" - Max Retries: %s", server.MaxRetries)
// Track the notification configuration BEFORE any updates
beforeNotificationID := server.NotificationID
beforeTemplateID := server.TemplateID
_ = beforeNotificationID
_ = beforeTemplateID
// Skip paused servers
if server.Status == "paused" {
// log.Printf("⏸️ Server %s is paused - skipping monitoring", server.Name)
return
}
sm.mu.Lock()
lastStatus := sm.lastStatuses[server.ServerID]
sm.mu.Unlock()
// log.Printf("📈 Status Tracking:")
// log.Printf(" - Last Known Status: '%s'", lastStatus)
// log.Printf(" - Current Database Status: '%s'", server.Status)
currentStatus := "up"
var message string
var statusReason string
// Check agent status first - if agent is stopped, server is definitely down
if server.AgentStatus == "stopped" {
currentStatus = "down"
statusReason = "agent is stopped"
message = fmt.Sprintf("🔴 Server %s is DOWN - Agent has stopped running", server.Name)
// log.Printf("❌ SERVER DOWN DETECTED: %s - Agent is stopped", server.Name)
} else if server.AgentStatus == "running" {
// log.Printf("✅ Agent is running for server %s - checking metrics...", server.Name)
// If agent is running, check metrics
checkIntervalMinutes := time.Duration(server.CheckInterval) * time.Minute
gracePeriod := 30 * time.Second
metricsTimeout := checkIntervalMinutes + gracePeriod
// Minimum timeout of 2 minutes to avoid false positives
if metricsTimeout < 2*time.Minute {
metricsTimeout = 2 * time.Minute
}
// log.Printf("🕐 Metrics Check Configuration:")
// log.Printf(" - Check Interval: %v", checkIntervalMinutes)
// log.Printf(" - Grace Period: %v", gracePeriod)
// log.Printf(" - Total Timeout: %v", metricsTimeout)
// Get recent metrics for this server
// log.Printf("🔍 Fetching recent metrics for server %s...", server.Name)
metrics, err := sm.pbClient.GetLatestServerMetrics(server.ServerID, metricsTimeout)
if err != nil {
// log.Printf("❌ ERROR: Failed to fetch metrics for server %s: %v", server.Name, err)
_ = err
return
}
// log.Printf("📊 Metrics Query Results:")
// log.Printf(" - Found %d metrics within timeout period", len(metrics))
if len(metrics) == 0 {
// No recent metrics found - server is down
currentStatus = "down"
statusReason = fmt.Sprintf("no metrics received in last %v", metricsTimeout)
message = fmt.Sprintf("🔴 Server %s is DOWN - No metrics received in the last %v (check interval: %d minutes)",
server.Name, metricsTimeout, server.CheckInterval)
// log.Printf("❌ SERVER DOWN DETECTED: %s - No recent metrics", server.Name)
} else {
// Recent metrics found - server is up
currentStatus = "up"
latestMetric := metrics[0]
age := time.Since(latestMetric.CreatedTime)
statusReason = fmt.Sprintf("metrics received %v ago", age.Round(time.Second))
message = fmt.Sprintf("✅ Server %s is operational - Latest metrics received %v ago (check interval: %d minutes)",
server.Name, age.Round(time.Second), server.CheckInterval)
// log.Printf("✅ SERVER UP: %s - Latest metric %v ago", server.Name, age.Round(time.Second))
// log.Printf("📊 Latest Metric Details:")
// log.Printf(" - Metric ID: %s", latestMetric.ID)
// log.Printf(" - Created: %v", latestMetric.CreatedTime)
// log.Printf(" - Age: %v", age.Round(time.Second))
// log.Printf(" - Status: %s", latestMetric.Status)
// Check thresholds if server is up and has metrics
// log.Printf("🎯 Checking thresholds for server %s...", server.Name)
sm.thresholdMonitor.CheckServerThresholds(server, metrics)
}
} else {
// log.Printf("⚠️ Unknown agent status '%s' for server %s", server.AgentStatus, server.Name)
currentStatus = "up" // Default to up for unknown status
statusReason = fmt.Sprintf("unknown agent status: %s", server.AgentStatus)
message = fmt.Sprintf("⚠️ Server %s has unknown agent status: %s", server.Name, server.AgentStatus)
}
// log.Printf("📊 Status Determination:")
// log.Printf(" - Determined Status: %s", currentStatus)
// log.Printf(" - Reason: %s", statusReason)
// log.Printf(" - Message: %s", message)
// Check if status has changed
statusChanged := lastStatus != currentStatus && lastStatus != ""
isFirstCheck := lastStatus == ""
_ = statusChanged
_ = isFirstCheck
// log.Printf("🔄 Status Change Analysis:")
// log.Printf(" - Status Changed: %t", statusChanged)
// log.Printf(" - First Check: %t", isFirstCheck)
// log.Printf(" - Previous Status: '%s'", lastStatus)
// log.Printf(" - New Status: '%s'", currentStatus)
// Update last known status
sm.mu.Lock()
sm.lastStatuses[server.ServerID] = currentStatus
sm.mu.Unlock()
// Update server status in database if it changed
if server.Status != currentStatus {
// log.Printf("💾 === DATABASE UPDATE FOR: %s ===", server.Name)
// log.Printf(" - BEFORE UPDATE:")
// log.Printf(" * Status: %s → %s", server.Status, currentStatus)
// log.Printf(" * Notification ID: '%s'", beforeNotificationID)
// log.Printf(" * Template ID: '%s'", beforeTemplateID)
// Log the update attempt
LogServerUpdateAttempt(server.ID, "UpdateServerStatus", fmt.Sprintf("Status change from %s to %s", server.Status, currentStatus))
// Debug fields before update
sm.pbClient.DebugServerFields(server.ID, "BEFORE_UPDATE")
if err := sm.pbClient.UpdateServerStatus(server.ID, currentStatus); err != nil {
// log.Printf("❌ CRITICAL ERROR: Failed to update server status for %s: %v", server.Name, err)
_ = err
} else {
// log.Printf("✅ Successfully updated database status for server %s", server.Name)
// Debug fields after update
sm.pbClient.DebugServerFields(server.ID, "AFTER_UPDATE")
}
} else {
// log.Printf("️ Database status already matches current status (%s) for server %s", currentStatus, server.Name)
}
// Handle status notifications with retry logic
sm.handleStatusNotifications(server, currentStatus, message, statusChanged, isFirstCheck)
// Final status summary
// log.Printf("📊 === FINAL STATUS SUMMARY FOR: %s ===", server.Name)
// log.Printf(" - Final Status: %s (%s)", currentStatus, statusReason)
// log.Printf(" - Database Updated: %t", server.Status != currentStatus)
// log.Printf(" - Notification Config Preserved: ID='%s', Template='%s'", server.NotificationID, server.TemplateID)
// Final debug after all operations
sm.pbClient.DebugServerFields(server.ID, "FINAL_CHECK")
// log.Printf("=== END SERVER CHECK FOR: %s ===\n", server.Name)
_ = statusReason
}
// handleStatusNotifications handles sending notifications based on status changes and retry logic
func (sm *ServerMonitor) handleStatusNotifications(server Server, currentStatus, message string, statusChanged, isFirstCheck bool) {
// log.Printf("📱 === NOTIFICATION ANALYSIS FOR: %s ===", server.Name)
// log.Printf("🔧 Notification Configuration:")
// log.Printf(" - Notification ID: '%s'", server.NotificationID)
// log.Printf(" - Template ID: '%s'", server.TemplateID)
// log.Printf(" - Has Notification Config: %t", server.NotificationID != "")
// log.Printf(" - Max Retries: %s", server.MaxRetries)
if server.NotificationID == "" {
// log.Printf("🔕 NOTIFICATION SKIPPED: No notification ID configured for server %s", server.Name)
return
}
// Parse max retries
maxRetries, err := strconv.Atoi(server.MaxRetries)
if err != nil {
// log.Printf("Invalid max_retries value '%s' for server %s, using default 3", server.MaxRetries, server.Name)
maxRetries = 3
}
// Determine if notification should be sent
shouldSendNotification := false
notificationReason := ""
if currentStatus == "down" {
if sm.shouldSendStatusAlert(server.ServerID, "down", maxRetries) {
shouldSendNotification = true
retryCount := sm.getStatusRetryCount(server.ServerID)
notificationReason = fmt.Sprintf("server down - retry %d/%d", retryCount+1, maxRetries)
} else {
notificationReason = "server down but max retries reached"
}
} else if currentStatus == "up" && sm.shouldSendRecoveryStatusAlert(server.ServerID) {
shouldSendNotification = true
notificationReason = "server recovery notification"
}
// log.Printf("🎯 Notification Decision:")
// log.Printf(" - Should Send: %t", shouldSendNotification)
// log.Printf(" - Reason: %s", notificationReason)
if shouldSendNotification {
// log.Printf("📤 SENDING NOTIFICATION for server %s", server.Name)
// log.Printf("📧 Notification Details:")
// log.Printf(" - Server Name: %s", server.Name)
// log.Printf(" - Status: %s", currentStatus)
// log.Printf(" - Message: %s", message)
// log.Printf(" - Notification ID: %s", server.NotificationID)
// log.Printf(" - Template ID: %s", server.TemplateID)
// log.Printf(" - Reason: %s", notificationReason)
err := sm.notificationService.SendServerNotification(server, currentStatus, message)
if err != nil {
// log.Printf("❌ NOTIFICATION FAILED for server %s: %v", server.Name, err)
_ = err
} else {
// log.Printf("✅ NOTIFICATION SENT SUCCESSFULLY for server %s", server.Name)
// Update alert tracking based on status
if currentStatus == "down" {
sm.setStatusAlert(server.ServerID, "down")
} else if currentStatus == "up" {
sm.clearStatusAlert(server.ServerID)
}
}
} else {
// log.Printf("📱 NOTIFICATION SKIPPED for server %s: %s", server.Name, notificationReason)
}
_ = statusChanged
_ = isFirstCheck
_ = notificationReason
}
// shouldSendStatusAlert determines if a status alert should be sent based on retry count
func (sm *ServerMonitor) shouldSendStatusAlert(serverID, status string, maxRetries int) bool {
sm.mu.RLock()
alert, exists := sm.activeStatusAlerts[serverID]
sm.mu.RUnlock()
if !exists {
return true // No existing alert, send new one
}
// Check if we've reached max retries
if alert.RetryCount >= maxRetries {
// log.Printf("🔇 Status alert for server %s has reached max retries (%d), not sending notification", serverID, maxRetries)
return false
}
// Check if enough time has passed since last alert (resend every 5 minutes)
timeSinceLastAlert := time.Since(alert.LastAlerted)
return timeSinceLastAlert > 5*time.Minute
}
// getStatusRetryCount returns the current retry count for a status alert
func (sm *ServerMonitor) getStatusRetryCount(serverID string) int {
sm.mu.RLock()
defer sm.mu.RUnlock()
if alert, exists := sm.activeStatusAlerts[serverID]; exists {
return alert.RetryCount
}
return 0
}
// shouldSendRecoveryStatusAlert determines if a recovery alert should be sent
func (sm *ServerMonitor) shouldSendRecoveryStatusAlert(serverID string) bool {
sm.mu.RLock()
defer sm.mu.RUnlock()
_, exists := sm.activeStatusAlerts[serverID]
return exists // Send recovery only if there was an active alert
}
// setStatusAlert creates or updates a status alert with incremented retry count
func (sm *ServerMonitor) setStatusAlert(serverID, status string) {
sm.mu.Lock()
defer sm.mu.Unlock()
alert, exists := sm.activeStatusAlerts[serverID]
if exists {
// Update existing alert and increment retry count
alert.Status = status
alert.LastAlerted = time.Now()
alert.RetryCount++
// log.Printf("📊 Updated status alert for server %s - Retry count: %d", serverID, alert.RetryCount)
} else {
// Create new alert
sm.activeStatusAlerts[serverID] = &StatusAlert{
IsActive: true,
Status: status,
LastAlerted: time.Now(),
RetryCount: 1, // Start with 1 since we're sending the first notification
}
// log.Printf("🆕 Created new status alert for server %s - Retry count: 1", serverID)
}
}
// clearStatusAlert removes a status alert
func (sm *ServerMonitor) clearStatusAlert(serverID string) {
sm.mu.Lock()
defer sm.mu.Unlock()
if _, exists := sm.activeStatusAlerts[serverID]; exists {
// log.Printf("🗑️ Cleared status alert for server %s", serverID)
delete(sm.activeStatusAlerts, serverID)
}
}
@@ -0,0 +1,236 @@
package servermonitoring
import (
"time"
"service-operation/notification"
"service-operation/pocketbase"
)
// ServerNotificationService handles server-specific notifications
type ServerNotificationService struct {
notificationManager *notification.NotificationManager
}
// NewServerNotificationService creates a new server notification service
func NewServerNotificationService(pbClient *pocketbase.PocketBaseClient) *ServerNotificationService {
// log.Println("🔧 Creating new ServerNotificationService instance")
return &ServerNotificationService{
notificationManager: notification.NewNotificationManager(pbClient),
}
}
// SendResourceNotification sends a resource-specific notification (CPU, RAM, Disk, etc.)
func (sns *ServerNotificationService) SendResourceNotification(server Server, status string, message string, resourceType string) error {
return sns.SendResourceNotificationWithValues(server, status, message, resourceType, "N/A", "N/A")
}
// SendResourceNotificationWithValues sends a resource-specific notification with actual usage and threshold values
func (sns *ServerNotificationService) SendResourceNotificationWithValues(server Server, status string, message string, resourceType string, usageValue string, thresholdValue string) error {
// log.Printf("📨 === SENDING RESOURCE NOTIFICATION WITH VALUES ===")
// log.Printf("🔔 SendResourceNotificationWithValues called for server: %s", server.Name)
// log.Printf("📊 Resource Notification Parameters:")
// log.Printf(" - Server Name: %s", server.Name)
// log.Printf(" - Resource Type: %s", resourceType)
// log.Printf(" - Status: %s", status)
// log.Printf(" - Message: %s", message)
// log.Printf(" - Usage Value: %s", usageValue)
// log.Printf(" - Threshold Value: %s", thresholdValue)
// log.Printf(" - Notification ID: '%s'", server.NotificationID)
// log.Printf(" - Template ID: '%s'", server.TemplateID)
// Validate notification configuration
if server.NotificationID == "" {
// log.Printf("❌ NOTIFICATION FAILED: No notification ID configured for server %s", server.Name)
return nil
}
// Get server metrics for notification
cpuUsage, ramUsage, diskUsage, networkUsage, cpuTemp, diskIO := sns.getServerMetrics(server.ServerID)
// Override specific resource metric with actual value
switch resourceType {
case "cpu":
cpuUsage = usageValue
case "ram", "memory":
ramUsage = usageValue
case "disk":
diskUsage = usageValue
case "network":
networkUsage = usageValue
case "cpu_temp", "cpu_temperature":
cpuTemp = usageValue
case "disk_io":
diskIO = usageValue
}
// Convert to generic notification payload
notificationPayload := &notification.NotificationPayload{
ServiceName: server.Name,
Status: status,
Host: server.IPAddress,
Hostname: server.Hostname,
Port: 0,
ServiceType: "server",
ResponseTime: 0,
Timestamp: time.Now(),
Message: message,
// Server monitoring metrics
CPUUsage: cpuUsage,
RAMUsage: ramUsage,
DiskUsage: diskUsage,
NetworkUsage: networkUsage,
CPUTemp: cpuTemp,
DiskIO: diskIO,
Threshold: thresholdValue,
}
// log.Printf("📤 About to call SendResourceNotification...")
// log.Printf(" - Resource Type: %s", resourceType)
// log.Printf(" - Notification ID: %s", server.NotificationID)
// log.Printf(" - Template ID: %s", server.TemplateID)
// log.Printf(" - Usage Value: %s", usageValue)
// log.Printf(" - Threshold Value: %s", thresholdValue)
// Send resource-specific notification using the notification manager
err := sns.notificationManager.SendResourceNotification(notificationPayload, server.NotificationID, server.TemplateID, resourceType)
if err != nil {
// log.Printf("❌ RESOURCE NOTIFICATION ERROR for server %s: %v", server.Name, err)
return err
}
// log.Printf("✅ RESOURCE NOTIFICATION SENT SUCCESSFULLY for server %s (%s)", server.Name, resourceType)
// log.Printf("=== RESOURCE NOTIFICATION COMPLETE ===")
return nil
}
// SendServerNotification sends a notification for server status change
func (sns *ServerNotificationService) SendServerNotification(server Server, status string, message string) error {
// log.Printf("📨 === SENDING SERVER NOTIFICATION ===")
// log.Printf("🔔 SendServerNotification called for server: %s", server.Name)
// log.Printf("📊 Notification Parameters:")
// log.Printf(" - Server Name: %s", server.Name)
// log.Printf(" - Server ID: %s", server.ServerID)
// log.Printf(" - Status: %s", status)
// log.Printf(" - Message: %s", message)
// log.Printf(" - Notification ID: '%s'", server.NotificationID)
// log.Printf(" - Template ID: '%s'", server.TemplateID)
// log.Printf(" - IP Address: %s", server.IPAddress)
// log.Printf(" - Hostname: %s", server.Hostname)
// Validate notification configuration
if server.NotificationID == "" {
// log.Printf("❌ NOTIFICATION FAILED: No notification ID configured for server %s", server.Name)
return nil
}
// log.Printf("✅ Notification ID validation passed: %s", server.NotificationID)
// Get server metrics for notification
cpuUsage, ramUsage, diskUsage, networkUsage, cpuTemp, diskIO := sns.getServerMetrics(server.ServerID)
// Convert to generic notification payload
// log.Printf("🔄 Converting to generic notification payload...")
notificationPayload := &notification.NotificationPayload{
ServiceName: server.Name,
Status: status,
Host: server.IPAddress,
Hostname: server.Hostname,
Port: 0, // Servers don't have ports
ServiceType: "server",
ResponseTime: 0,
Timestamp: time.Now(),
Message: message,
// Server monitoring metrics
CPUUsage: cpuUsage,
RAMUsage: ramUsage,
DiskUsage: diskUsage,
NetworkUsage: networkUsage,
CPUTemp: cpuTemp,
DiskIO: diskIO,
Threshold: "N/A", // Can be set based on server configuration
}
// log.Printf("📦 Generic notification payload created:")
// log.Printf(" - ServiceName: %s", notificationPayload.ServiceName)
// log.Printf(" - Status: %s", notificationPayload.Status)
// log.Printf(" - Host: %s", notificationPayload.Host)
// log.Printf(" - ServiceType: %s", notificationPayload.ServiceType)
// log.Printf(" - Message: %s", notificationPayload.Message)
// log.Printf(" - CPUUsage: %s", notificationPayload.CPUUsage)
// log.Printf(" - RAMUsage: %s", notificationPayload.RAMUsage)
// log.Printf(" - DiskUsage: %s", notificationPayload.DiskUsage)
// log.Printf(" - NetworkUsage: %s", notificationPayload.NetworkUsage)
// log.Printf(" - CPUTemp: %s", notificationPayload.CPUTemp)
// log.Printf(" - DiskIO: %s", notificationPayload.DiskIO)
// log.Printf(" - Timestamp: %v", notificationPayload.Timestamp)
// log.Printf("📤 About to call SendServiceNotification...")
// log.Printf(" - Notification ID: %s", server.NotificationID)
// log.Printf(" - Template ID: %s", server.TemplateID)
// Send notification using the notification manager
err := sns.notificationManager.SendServiceNotification(notificationPayload, server.NotificationID, server.TemplateID)
if err != nil {
// log.Printf("❌ NOTIFICATION MANAGER ERROR for server %s: %v", server.Name, err)
// log.Printf("❌ Error Details:")
// log.Printf(" - Error Type: %T", err)
// log.Printf(" - Error Message: %s", err.Error())
// log.Printf(" - Notification ID used: %s", server.NotificationID)
// log.Printf(" - Template ID used: %s", server.TemplateID)
return err
}
// log.Printf("✅ NOTIFICATION SENT SUCCESSFULLY for server %s", server.Name)
// log.Printf("✅ Notification Details:")
// log.Printf(" - Status: %s", status)
// log.Printf(" - Notification ID: %s", server.NotificationID)
// log.Printf(" - Template ID: %s", server.TemplateID)
// log.Printf("=== NOTIFICATION COMPLETE ===")
return nil
}
// getServerMetrics retrieves the latest server metrics (placeholder implementation)
// This should be replaced with actual metric retrieval from your monitoring system
func (sns *ServerNotificationService) getServerMetrics(serverID string) (cpuUsage, ramUsage, diskUsage, networkUsage, cpuTemp, diskIO string) {
// TODO: Implement actual metric retrieval from your server monitoring system
// For now, returning placeholder values
// You should replace this with actual calls to your metrics collection system
// log.Printf("🔍 Retrieving server metrics for server ID: %s", serverID)
// These should be replaced with actual metric values from your monitoring system
cpuUsage = "N/A"
ramUsage = "N/A"
diskUsage = "N/A"
networkUsage = "N/A"
cpuTemp = "N/A"
diskIO = "N/A"
// Example of how you might retrieve metrics:
// You could query your metrics database, call an API, or read from system files
// For example:
// metrics, err := sns.getLatestMetrics(serverID)
// if err == nil {
// cpuUsage = fmt.Sprintf("%.1f%%", metrics.CPUUsage)
// ramUsage = fmt.Sprintf("%.1f%%", metrics.RAMUsage)
// diskUsage = fmt.Sprintf("%.1f%%", metrics.DiskUsage)
// networkUsage = fmt.Sprintf("%.2f MB/s", metrics.NetworkUsage)
// cpuTemp = fmt.Sprintf("%.1f°C", metrics.CPUTemp)
// diskIO = fmt.Sprintf("%.2f MB/s", metrics.DiskIO)
// }
// log.Printf("📊 Retrieved server metrics:")
// log.Printf(" - CPU Usage: %s", cpuUsage)
// log.Printf(" - RAM Usage: %s", ramUsage)
// log.Printf(" - Disk Usage: %s", diskUsage)
// log.Printf(" - Network Usage: %s", networkUsage)
// log.Printf(" - CPU Temperature: %s", cpuTemp)
// log.Printf(" - Disk I/O: %s", diskIO)
_ = serverID
return cpuUsage, ramUsage, diskUsage, networkUsage, cpuTemp, diskIO
}
@@ -0,0 +1,287 @@
package servermonitoring
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"time"
"service-operation/pocketbase"
)
// ServerMetrics represents server metrics record in PocketBase
type ServerMetrics struct {
ID string `json:"id"`
ServerID string `json:"server_id"`
Timestamp string `json:"timestamp"` // Changed to string for custom parsing
RAMTotal string `json:"ram_total"`
RAMUsed string `json:"ram_used"`
RAMFree string `json:"ram_free"`
CPUCores string `json:"cpu_cores"`
CPUUsage string `json:"cpu_usage"`
CPUFree string `json:"cpu_free"`
DiskTotal string `json:"disk_total"`
DiskUsed string `json:"disk_used"`
DiskFree string `json:"disk_free"`
Status string `json:"status"`
NetworkRxBytes int64 `json:"network_rx_bytes"`
NetworkTxBytes int64 `json:"network_tx_bytes"`
NetworkRxSpeed int64 `json:"network_rx_speed"`
NetworkTxSpeed int64 `json:"network_tx_speed"`
Created string `json:"created"` // Changed to string for custom parsing
Updated string `json:"updated"` // Changed to string for custom parsing
}
// ParsedServerMetrics represents ServerMetrics with parsed time fields
type ParsedServerMetrics struct {
ServerMetrics
CreatedTime time.Time
UpdatedTime time.Time
TimestampTime time.Time
}
// ParseServerMetrics converts ServerMetrics to ParsedServerMetrics with proper time parsing
func ParseServerMetrics(sm ServerMetrics) (ParsedServerMetrics, error) {
psm := ParsedServerMetrics{ServerMetrics: sm}
var err error
// Parse Created time
if sm.Created != "" {
psm.CreatedTime, err = parsePocketBaseTime(sm.Created)
if err != nil {
log.Printf("Warning: Failed to parse Created time '%s': %v", sm.Created, err)
}
}
// Parse Updated time
if sm.Updated != "" {
psm.UpdatedTime, err = parsePocketBaseTime(sm.Updated)
if err != nil {
log.Printf("Warning: Failed to parse Updated time '%s': %v", sm.Updated, err)
}
}
// Parse Timestamp
if sm.Timestamp != "" {
psm.TimestampTime, err = parsePocketBaseTime(sm.Timestamp)
if err != nil {
log.Printf("Warning: Failed to parse Timestamp '%s': %v", sm.Timestamp, err)
}
}
return psm, nil
}
// parsePocketBaseTime parses PocketBase time format "2025-08-11 13:09:13.243Z"
func parsePocketBaseTime(timeStr string) (time.Time, error) {
// First try the PocketBase format with space separator
if t, err := time.Parse("2006-01-02 15:04:05.000Z", timeStr); err == nil {
return t, nil
}
// Fallback to RFC3339 format with T separator
if t, err := time.Parse(time.RFC3339, timeStr); err == nil {
return t, nil
}
// Fallback to RFC3339Nano format
if t, err := time.Parse(time.RFC3339Nano, timeStr); err == nil {
return t, nil
}
return time.Time{}, fmt.Errorf("unable to parse time string: %s", timeStr)
}
// ServerPocketBaseClient is a wrapper around the PocketBase client for server monitoring
type ServerPocketBaseClient struct {
client *pocketbase.PocketBaseClient
}
// NewServerPocketBaseClient creates a new server PocketBase client
func NewServerPocketBaseClient(client *pocketbase.PocketBaseClient) *ServerPocketBaseClient {
return &ServerPocketBaseClient{
client: client,
}
}
// GetServerThreshold fetches threshold configuration for a server
func (spc *ServerPocketBaseClient) GetServerThreshold(thresholdID string) (*ServerThreshold, error) {
if thresholdID == "" {
log.Printf("No threshold ID provided")
_ = thresholdID
return nil, nil
}
url := fmt.Sprintf("%s/api/collections/server_threshold_templates/records/%s", spc.client.GetBaseURL(), thresholdID)
//log.Printf("🔍 Fetching server threshold from: %s", url)
resp, err := http.Get(url)
if err != nil {
//log.Printf("❌ HTTP error fetching server threshold: %v", err)
_ = err
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
//log.Printf("❌ Failed to fetch server threshold, status: %d", resp.StatusCode)
return nil, fmt.Errorf("failed to fetch server threshold, status: %d", resp.StatusCode)
}
var threshold ServerThreshold
if err := json.NewDecoder(resp.Body).Decode(&threshold); err != nil {
//log.Printf("❌ Error decoding server threshold JSON: %v", err)
_ = err
return nil, err
}
//log.Printf("✅ Successfully fetched server threshold: %+v", threshold)
_ = url
_ = threshold
return &threshold, nil
}
// GetAllServers retrieves all servers from PocketBase
func (spc *ServerPocketBaseClient) GetAllServers() ([]Server, error) {
url := fmt.Sprintf("%s/api/collections/servers/records?perPage=500", spc.client.GetBaseURL())
//log.Printf("🌐 Fetching all servers from: %s", url)
resp, err := http.Get(url)
if err != nil {
//log.Printf("❌ HTTP error fetching servers: %v", err)
_ = err
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
//log.Printf("❌ Failed to fetch servers, status: %d", resp.StatusCode)
return nil, fmt.Errorf("failed to fetch servers, status: %d", resp.StatusCode)
}
var response struct {
Items []Server `json:"items"`
}
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
//log.Printf("❌ Error decoding servers JSON: %v", err)
_ = err
return nil, err
}
//log.Printf("✅ Successfully fetched %d servers", len(response.Items))
_ = url
return response.Items, nil
}
// GetLatestServerMetrics retrieves the latest server metrics from PocketBase
func (spc *ServerPocketBaseClient) GetLatestServerMetrics(serverID string, timeout time.Duration) ([]ParsedServerMetrics, error) {
// Calculate the time before which metrics are considered too old
cutoff := time.Now().Add(-timeout).UTC().Format("2006-01-02 15:04:05.000Z")
// Construct the filter string with proper formatting (no spaces around operators)
filter := fmt.Sprintf("server_id='%s'&&created>'%s'", serverID, cutoff)
// URL encode the filter parameter
encodedFilter := url.QueryEscape(filter)
// Construct the URL with properly encoded parameters
requestURL := fmt.Sprintf("%s/api/collections/server_metrics/records?filter=%s&sort=-created&perPage=1",
spc.client.GetBaseURL(), encodedFilter)
//log.Printf("🌐 Fetching latest server metrics from: %s", requestURL)
//log.Printf("🔍 Filter used: %s", filter)
//log.Printf("🔍 Cutoff time: %s", cutoff)
resp, err := http.Get(requestURL)
if err != nil {
//log.Printf("❌ HTTP error fetching server metrics: %v", err)
_ = err
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
//log.Printf("❌ Failed to fetch server metrics, status: %d", resp.StatusCode)
return nil, fmt.Errorf("failed to fetch server metrics, status: %d", resp.StatusCode)
}
var response struct {
Items []ServerMetrics `json:"items"`
}
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
//log.Printf("❌ Error decoding server metrics JSON: %v", err)
_ = err
return nil, err
}
// Convert to ParsedServerMetrics
var parsedMetrics []ParsedServerMetrics
for _, metric := range response.Items {
parsed, err := ParseServerMetrics(metric)
if err != nil {
//log.Printf("❌ Error parsing server metric: %v", err)
_ = err
continue
}
parsedMetrics = append(parsedMetrics, parsed)
}
//log.Printf("✅ Successfully fetched and parsed %d server metrics", len(parsedMetrics))
_ = requestURL
_ = filter
_ = cutoff
return parsedMetrics, nil
}
// UpdateServerStatus updates the server status in PocketBase
func (spc *ServerPocketBaseClient) UpdateServerStatus(serverID string, status string) error {
url := fmt.Sprintf("%s/api/collections/servers/records/%s", spc.client.GetBaseURL(), serverID)
//log.Printf("🌐 Updating server status at: %s", url)
payload := map[string]interface{}{
"status": status,
"last_checked": time.Now().UTC().Format("2006-01-02 15:04:05.000Z"),
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
//log.Printf("❌ Error marshaling payload: %v", err)
_ = err
return err
}
//log.Printf("📝 Update payload: %s", string(payloadBytes))
req, err := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer(payloadBytes))
if err != nil {
//log.Printf("❌ Error creating request: %v", err)
_ = err
return err
}
req.Header.Set("Content-Type", "application/json")
httpClient := &http.Client{Timeout: 10 * time.Second}
resp, err := httpClient.Do(req)
if err != nil {
//log.Printf("❌ HTTP error updating server status: %v", err)
_ = err
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
//log.Printf("❌ Failed to update server status, status: %d", resp.StatusCode)
return fmt.Errorf("failed to update server status, status: %d", resp.StatusCode)
}
//log.Printf("✅ Successfully updated server status to %s", status)
_ = url
_ = payloadBytes
_ = status
return nil
}
@@ -0,0 +1,333 @@
package servermonitoring
import (
"fmt"
"strconv"
"strings"
"time"
)
// ThresholdAlert tracks alert state and retry count
type ThresholdAlert struct {
IsActive bool
Threshold int
CurrentValue float64
LastAlerted time.Time
RetryCount int // Track how many times we've sent this alert
}
// ThresholdMonitor handles server threshold monitoring and alerting
type ThresholdMonitor struct {
pbClient *ServerPocketBaseClient
notificationService *ServerNotificationService
activeAlerts map[string]*ThresholdAlert // serverID-metricType -> alert
}
// NewThresholdMonitor creates a new threshold monitor
func NewThresholdMonitor(pbClient *ServerPocketBaseClient, notificationService *ServerNotificationService) *ThresholdMonitor {
return &ThresholdMonitor{
pbClient: pbClient,
notificationService: notificationService,
activeAlerts: make(map[string]*ThresholdAlert),
}
}
// CheckServerThresholds checks all thresholds for a server
func (tm *ThresholdMonitor) CheckServerThresholds(server Server, metrics []ParsedServerMetrics) {
if server.ThresholdID == "" {
// log.Printf("No threshold configuration for server %s", server.Name)
return
}
// Get threshold configuration
threshold, err := tm.pbClient.GetServerThreshold(server.ThresholdID)
if err != nil {
// log.Printf("❌ Failed to get threshold config for server %s: %v", server.Name, err)
_ = err
return
}
if threshold == nil {
// log.Printf("No threshold configuration found for server %s", server.Name)
return
}
if len(metrics) == 0 {
// log.Printf("No metrics available for threshold checking on server %s", server.Name)
return
}
latestMetric := metrics[0]
// log.Printf("🎯 Checking thresholds for server %s with threshold config: %+v", server.Name, threshold)
// Parse MaxRetries from string to int
maxRetries, err := strconv.Atoi(server.MaxRetries)
if err != nil {
// log.Printf("Invalid max_retries value '%s' for server %s, using default 3", server.MaxRetries, server.Name)
maxRetries = 3
}
// Check CPU threshold
tm.checkCPUThreshold(server, latestMetric, threshold, maxRetries)
// Check RAM threshold
tm.checkRAMThreshold(server, latestMetric, threshold, maxRetries)
// Check Disk threshold
tm.checkDiskThreshold(server, latestMetric, threshold, maxRetries)
}
// checkCPUThreshold checks CPU usage against threshold
func (tm *ThresholdMonitor) checkCPUThreshold(server Server, metric ParsedServerMetrics, threshold *ServerThreshold, maxRetries int) {
cpuThresholdValue, err := strconv.Atoi(threshold.CPUThreshold)
if err != nil {
// log.Printf("Invalid CPU threshold value '%s' for server %s", threshold.CPUThreshold, server.Name)
return
}
// Parse CPU usage from metric (format: "XX.XX%")
cpuUsageStr := strings.TrimSuffix(metric.CPUUsage, "%")
cpuUsage, err := strconv.ParseFloat(cpuUsageStr, 64)
if err != nil {
// log.Printf("Failed to parse CPU usage '%s' for server %s", metric.CPUUsage, server.Name)
return
}
alertKey := fmt.Sprintf("%s-cpu", server.ServerID)
if cpuUsage > float64(cpuThresholdValue) {
// Threshold exceeded
if tm.shouldCreateAlert(alertKey, cpuUsage, float64(cpuThresholdValue), maxRetries) {
// log.Printf("🚨 CPU threshold exceeded for server %s: %.2f%% > %d%% (retry %d/%d)",
// server.Name, cpuUsage, cpuThresholdValue, tm.getRetryCount(alertKey)+1, maxRetries)
message := fmt.Sprintf("🚨 CPU Alert: Server %s CPU usage is %.2f%% (threshold: %d%%)",
server.Name, cpuUsage, cpuThresholdValue)
tm.sendThresholdAlert(server, "cpu", message, cpuUsage, float64(cpuThresholdValue), fmt.Sprintf("%.2f%%", cpuUsage), fmt.Sprintf("%d%%", cpuThresholdValue))
tm.setAlert(alertKey, cpuUsage, float64(cpuThresholdValue))
} else {
// log.Printf("🔇 CPU threshold exceeded for server %s but max retries (%d) reached, skipping notification", server.Name, maxRetries)
}
} else {
// Check if we need to send recovery notification
if tm.shouldSendRecoveryAlert(alertKey) {
// log.Printf("✅ CPU recovered for server %s: %.2f%% <= %d%%", server.Name, cpuUsage, cpuThresholdValue)
message := fmt.Sprintf("✅ CPU Recovery: Server %s CPU usage is back to normal: %.2f%% (threshold: %d%%)",
server.Name, cpuUsage, cpuThresholdValue)
tm.sendThresholdRecovery(server, "cpu", message, fmt.Sprintf("%.2f%%", cpuUsage), fmt.Sprintf("%d%%", cpuThresholdValue))
tm.clearAlert(alertKey)
}
}
}
// checkRAMThreshold checks RAM usage against threshold
func (tm *ThresholdMonitor) checkRAMThreshold(server Server, metric ParsedServerMetrics, threshold *ServerThreshold, maxRetries int) {
ramThresholdValue, err := strconv.Atoi(threshold.RAMThreshold)
if err != nil {
// log.Printf("Invalid RAM threshold value '%s' for server %s", threshold.RAMThreshold, server.Name)
return
}
// Parse RAM usage from metric (format: "X.XX GB (XX.X%)")
ramUsageStr := metric.RAMUsed
if strings.Contains(ramUsageStr, "(") && strings.Contains(ramUsageStr, "%)") {
// Extract percentage from parentheses
start := strings.Index(ramUsageStr, "(") + 1
end := strings.Index(ramUsageStr, "%)")
if start < end {
ramPercentageStr := ramUsageStr[start:end]
ramUsage, err := strconv.ParseFloat(ramPercentageStr, 64)
if err != nil {
// log.Printf("Failed to parse RAM usage percentage '%s' for server %s", ramPercentageStr, server.Name)
return
}
alertKey := fmt.Sprintf("%s-ram", server.ServerID)
if ramUsage > float64(ramThresholdValue) {
// Threshold exceeded
if tm.shouldCreateAlert(alertKey, ramUsage, float64(ramThresholdValue), maxRetries) {
// log.Printf("🚨 RAM threshold exceeded for server %s: %.2f%% > %d%% (retry %d/%d)",
// server.Name, ramUsage, ramThresholdValue, tm.getRetryCount(alertKey)+1, maxRetries)
message := fmt.Sprintf("🚨 RAM Alert: Server %s RAM usage is %.2f%% (threshold: %d%%)",
server.Name, ramUsage, ramThresholdValue)
tm.sendThresholdAlert(server, "ram", message, ramUsage, float64(ramThresholdValue), fmt.Sprintf("%.2f%%", ramUsage), fmt.Sprintf("%d%%", ramThresholdValue))
tm.setAlert(alertKey, ramUsage, float64(ramThresholdValue))
} else {
// log.Printf("🔇 RAM threshold exceeded for server %s but max retries (%d) reached, skipping notification", server.Name, maxRetries)
}
} else {
// Check if we need to send recovery notification
if tm.shouldSendRecoveryAlert(alertKey) {
// log.Printf("✅ RAM recovered for server %s: %.2f%% <= %d%%", server.Name, ramUsage, ramThresholdValue)
message := fmt.Sprintf("✅ RAM Recovery: Server %s RAM usage is back to normal: %.2f%% (threshold: %d%%)",
server.Name, ramUsage, ramThresholdValue)
tm.sendThresholdRecovery(server, "ram", message, fmt.Sprintf("%.2f%%", ramUsage), fmt.Sprintf("%d%%", ramThresholdValue))
tm.clearAlert(alertKey)
}
}
}
}
}
// checkDiskThreshold checks disk usage against threshold
func (tm *ThresholdMonitor) checkDiskThreshold(server Server, metric ParsedServerMetrics, threshold *ServerThreshold, maxRetries int) {
diskThresholdValue, err := strconv.Atoi(threshold.DiskThreshold)
if err != nil {
// log.Printf("Invalid disk threshold value '%s' for server %s", threshold.DiskThreshold, server.Name)
return
}
// Parse disk usage from metric (format: "X.XX GB (XX.X%)")
diskUsageStr := metric.DiskUsed
if strings.Contains(diskUsageStr, "(") && strings.Contains(diskUsageStr, "%)") {
// Extract percentage from parentheses
start := strings.Index(diskUsageStr, "(") + 1
end := strings.Index(diskUsageStr, "%)")
if start < end {
diskPercentageStr := diskUsageStr[start:end]
diskUsage, err := strconv.ParseFloat(diskPercentageStr, 64)
if err != nil {
// log.Printf("Failed to parse disk usage percentage '%s' for server %s", diskPercentageStr, server.Name)
return
}
alertKey := fmt.Sprintf("%s-disk", server.ServerID)
if diskUsage > float64(diskThresholdValue) {
// Threshold exceeded
if tm.shouldCreateAlert(alertKey, diskUsage, float64(diskThresholdValue), maxRetries) {
// log.Printf("🚨 Disk threshold exceeded for server %s: %.2f%% > %d%% (retry %d/%d)",
// server.Name, diskUsage, diskThresholdValue, tm.getRetryCount(alertKey)+1, maxRetries)
message := fmt.Sprintf("🚨 Disk Alert: Server %s disk usage is %.2f%% (threshold: %d%%)",
server.Name, diskUsage, diskThresholdValue)
tm.sendThresholdAlert(server, "disk", message, diskUsage, float64(diskThresholdValue), fmt.Sprintf("%.2f%%", diskUsage), fmt.Sprintf("%d%%", diskThresholdValue))
tm.setAlert(alertKey, diskUsage, float64(diskThresholdValue))
} else {
// log.Printf("🔇 Disk threshold exceeded for server %s but max retries (%d) reached, skipping notification", server.Name, maxRetries)
}
} else {
// Check if we need to send recovery notification
if tm.shouldSendRecoveryAlert(alertKey) {
// log.Printf("✅ Disk recovered for server %s: %.2f%% <= %d%%", server.Name, diskUsage, diskThresholdValue)
message := fmt.Sprintf("✅ Disk Recovery: Server %s disk usage is back to normal: %.2f%% (threshold: %d%%)",
server.Name, diskUsage, diskThresholdValue)
tm.sendThresholdRecovery(server, "disk", message, fmt.Sprintf("%.2f%%", diskUsage), fmt.Sprintf("%d%%", diskThresholdValue))
tm.clearAlert(alertKey)
}
}
}
}
}
// shouldCreateAlert determines if an alert should be created based on retry count and max retries
func (tm *ThresholdMonitor) shouldCreateAlert(alertKey string, currentValue, threshold float64, maxRetries int) bool {
alert, exists := tm.activeAlerts[alertKey]
if !exists {
return true // No existing alert, create new one
}
// Check if we've reached max retries
if alert.RetryCount >= maxRetries {
// log.Printf("🔇 Alert %s has reached max retries (%d), not sending notification", alertKey, maxRetries)
return false
}
// Check if enough time has passed since last alert (resend every 5 minutes)
timeSinceLastAlert := time.Since(alert.LastAlerted)
return timeSinceLastAlert > 5*time.Minute
}
// getRetryCount returns the current retry count for an alert
func (tm *ThresholdMonitor) getRetryCount(alertKey string) int {
if alert, exists := tm.activeAlerts[alertKey]; exists {
return alert.RetryCount
}
return 0
}
// shouldSendRecoveryAlert determines if a recovery alert should be sent
func (tm *ThresholdMonitor) shouldSendRecoveryAlert(alertKey string) bool {
_, exists := tm.activeAlerts[alertKey]
return exists // Send recovery only if there was an active alert
}
// setAlert creates or updates an alert with incremented retry count
func (tm *ThresholdMonitor) setAlert(alertKey string, currentValue, threshold float64) {
alert, exists := tm.activeAlerts[alertKey]
if exists {
// Update existing alert and increment retry count
alert.CurrentValue = currentValue
alert.LastAlerted = time.Now()
alert.RetryCount++
// log.Printf("📊 Updated alert %s - Retry count: %d", alertKey, alert.RetryCount)
} else {
// Create new alert
tm.activeAlerts[alertKey] = &ThresholdAlert{
IsActive: true,
Threshold: int(threshold),
CurrentValue: currentValue,
LastAlerted: time.Now(),
RetryCount: 1, // Start with 1 since we're sending the first notification
}
// log.Printf("🆕 Created new alert %s - Retry count: 1", alertKey)
}
}
// clearAlert removes an alert
func (tm *ThresholdMonitor) clearAlert(alertKey string) {
if _, exists := tm.activeAlerts[alertKey]; exists {
// log.Printf("🗑️ Cleared alert %s", alertKey)
delete(tm.activeAlerts, alertKey)
}
}
// sendThresholdAlert sends a threshold exceeded notification using resource-specific templates
func (tm *ThresholdMonitor) sendThresholdAlert(server Server, metricType, message string, currentValue, threshold float64, usageStr, thresholdStr string) {
if server.NotificationID == "" {
// log.Printf("No notification ID configured for server %s", server.Name)
return
}
// log.Printf("📤 Sending resource-specific threshold alert for server %s (%s): %s", server.Name, metricType, message)
// Use the new resource-specific notification method with actual values
err := tm.notificationService.SendResourceNotificationWithValues(server, "warning", message, metricType, usageStr, thresholdStr)
if err != nil {
// log.Printf("❌ Failed to send threshold alert for server %s: %v", server.Name, err)
_ = err
} else {
// log.Printf("✅ Threshold alert sent successfully for server %s", server.Name)
}
}
// sendThresholdRecovery sends a threshold recovery notification using resource-specific templates
func (tm *ThresholdMonitor) sendThresholdRecovery(server Server, metricType, message, usageStr, thresholdStr string) {
if server.NotificationID == "" {
// log.Printf("No notification ID configured for server %s", server.Name)
return
}
// log.Printf("📤 Sending resource-specific threshold recovery for server %s (%s): %s", server.Name, metricType, message)
// Use the new resource-specific notification method with actual values
err := tm.notificationService.SendResourceNotificationWithValues(server, "up", message, metricType, usageStr, thresholdStr)
if err != nil {
// log.Printf("❌ Failed to send threshold recovery for server %s: %v", server.Name, err)
_ = err
} else {
// log.Printf("✅ Threshold recovery sent successfully for server %s", server.Name)
}
}
@@ -0,0 +1,77 @@
package servermonitoring
import "time"
// Server represents a server record in PocketBase
type Server struct {
CollectionId string `json:"collectionId"`
CollectionName string `json:"collectionName"`
ID string `json:"id"`
ServerID string `json:"server_id"`
Name string `json:"name"`
Hostname string `json:"hostname"`
IPAddress string `json:"ip_address"`
OSType string `json:"os_type"`
Status string `json:"status"` // 'up' | 'down' | 'warning' | 'paused'
Uptime string `json:"uptime"`
RAMTotal float64 `json:"ram_total"`
RAMUsed float64 `json:"ram_used"`
CPUCores int `json:"cpu_cores"`
CPUUsage float64 `json:"cpu_usage"`
DiskTotal float64 `json:"disk_total"`
DiskUsed float64 `json:"disk_used"`
LastChecked string `json:"last_checked"`
ServerToken string `json:"server_token"`
TemplateID string `json:"template_id"`
ThresholdID string `json:"threshold_id"`
NotificationID string `json:"notification_id"`
NotificationStatus bool `json:"notification_status"`
MaxRetries string `json:"max_retries"`
Timestamp string `json:"timestamp"`
Connection string `json:"connection"`
AgentStatus string `json:"agent_status"`
SystemInfo string `json:"system_info"`
NetworkRxBytes string `json:"network_rx_bytes"`
NetworkTxBytes string `json:"network_tx_bytes"`
NetworkRxSpeed string `json:"network_rx_speed"`
NetworkTxSpeed string `json:"network_tx_speed"`
CheckInterval int `json:"check_interval"`
Docker string `json:"docker"`
Created string `json:"created"`
Updated string `json:"updated"`
}
// ServerStats represents server statistics
type ServerStats struct {
Total int `json:"total"`
Online int `json:"online"`
Offline int `json:"offline"`
Warning int `json:"warning"`
}
// ServerThreshold represents threshold configuration for server monitoring
type ServerThreshold struct {
ID string `json:"id"`
Name string `json:"name"`
CPUThreshold string `json:"cpu_threshold"`
RAMThreshold string `json:"ram_threshold"`
DiskThreshold string `json:"disk_threshold"`
NetworkThreshold string `json:"network_threshold"`
DiskIOThreshold string `json:"disk_io_threshold"`
CPUTempThreshold string `json:"cpu_temp_threshold"`
Created string `json:"created"`
Updated string `json:"updated"`
}
// ServerNotificationPayload represents server-specific notification data
type ServerNotificationPayload struct {
ServerName string `json:"server_name"`
ServerID string `json:"server_id"`
Hostname string `json:"hostname"`
IPAddress string `json:"ip_address"`
Status string `json:"status"`
OSType string `json:"os_type"`
Timestamp time.Time `json:"timestamp"`
Message string `json:"message"`
}