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,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 := ¬ification.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 := ¬ification.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"`
|
||||
}
|
||||
Reference in New Issue
Block a user