package server import ( "context" "database/sql" "encoding/json" "fmt" "io" "net/http" "strings" "time" "github.com/corecontrol/agent/internal/models" "github.com/corecontrol/agent/internal/notifications" ) // MonitorServers checks and updates the status of all servers func MonitorServers(db *sql.DB, client *http.Client, servers []models.Server, notifSender *notifications.NotificationSender) { var notificationTemplate string err := db.QueryRow("SELECT notification_text_server FROM settings LIMIT 1").Scan(¬ificationTemplate) if err != nil || notificationTemplate == "" { notificationTemplate = "The server !name is now !status!" } for _, server := range servers { if !server.Monitoring || !server.MonitoringURL.Valid { continue } logPrefix := fmt.Sprintf("[Server %s]", server.Name) fmt.Printf("%s Checking...\n", logPrefix) baseURL := strings.TrimSuffix(server.MonitoringURL.String, "/") var cpuUsage, ramUsage, diskUsage float64 var online = true var uptimeStr string // Get CPU usage online, cpuUsage = fetchCPUUsage(client, baseURL, logPrefix) if !online { updateServerStatus(db, server.ID, false, 0, 0, 0, "") sendStatusChangeNotification(server, online, notificationTemplate, notifSender) addServerHistoryEntry(db, server.ID, false, 0, 0, 0) continue } // Get uptime if server is online uptimeStr = fetchUptime(client, baseURL, logPrefix) // Get Memory usage memOnline, memUsage := fetchMemoryUsage(client, baseURL, logPrefix) if !memOnline { online = false updateServerStatus(db, server.ID, false, 0, 0, 0, "") sendStatusChangeNotification(server, online, notificationTemplate, notifSender) addServerHistoryEntry(db, server.ID, false, 0, 0, 0) continue } ramUsage = memUsage // Get Disk usage diskOnline, diskUsageVal := fetchDiskUsage(client, baseURL, logPrefix) if !diskOnline { online = false updateServerStatus(db, server.ID, false, 0, 0, 0, "") sendStatusChangeNotification(server, online, notificationTemplate, notifSender) addServerHistoryEntry(db, server.ID, false, 0, 0, 0) continue } diskUsage = diskUsageVal // Check if status changed and send notification if needed if online != server.Online { sendStatusChangeNotification(server, online, notificationTemplate, notifSender) } // Update server status with metrics updateServerStatus(db, server.ID, online, cpuUsage, ramUsage, diskUsage, uptimeStr) // Add entry to server history addServerHistoryEntry(db, server.ID, online, cpuUsage, ramUsage, diskUsage) fmt.Printf("%s Updated - CPU: %.2f%%, RAM: %.2f%%, Disk: %.2f%%, Uptime: %s\n", logPrefix, cpuUsage, ramUsage, diskUsage, uptimeStr) } } // Helper function to fetch CPU usage func fetchCPUUsage(client *http.Client, baseURL, logPrefix string) (bool, float64) { cpuResp, err := client.Get(fmt.Sprintf("%s/api/4/cpu", baseURL)) if err != nil { fmt.Printf("%s CPU request failed: %v\n", logPrefix, err) return false, 0 } defer cpuResp.Body.Close() if cpuResp.StatusCode != http.StatusOK { fmt.Printf("%s Bad CPU status code: %d\n", logPrefix, cpuResp.StatusCode) return false, 0 } var cpuData models.CPUResponse if err := json.NewDecoder(cpuResp.Body).Decode(&cpuData); err != nil { fmt.Printf("%s Failed to parse CPU JSON: %v\n", logPrefix, err) return false, 0 } return true, cpuData.Total } // Helper function to fetch memory usage func fetchMemoryUsage(client *http.Client, baseURL, logPrefix string) (bool, float64) { memResp, err := client.Get(fmt.Sprintf("%s/api/4/mem", baseURL)) if err != nil { fmt.Printf("%s Memory request failed: %v\n", logPrefix, err) return false, 0 } defer memResp.Body.Close() if memResp.StatusCode != http.StatusOK { fmt.Printf("%s Bad memory status code: %d\n", logPrefix, memResp.StatusCode) return false, 0 } var memData models.MemoryResponse if err := json.NewDecoder(memResp.Body).Decode(&memData); err != nil { fmt.Printf("%s Failed to parse memory JSON: %v\n", logPrefix, err) return false, 0 } return true, memData.Percent } // Helper function to fetch disk usage func fetchDiskUsage(client *http.Client, baseURL, logPrefix string) (bool, float64) { fsResp, err := client.Get(fmt.Sprintf("%s/api/4/fs", baseURL)) if err != nil { fmt.Printf("%s Filesystem request failed: %v\n", logPrefix, err) return false, 0 } defer fsResp.Body.Close() if fsResp.StatusCode != http.StatusOK { fmt.Printf("%s Bad filesystem status code: %d\n", logPrefix, fsResp.StatusCode) return false, 0 } var fsData models.FSResponse if err := json.NewDecoder(fsResp.Body).Decode(&fsData); err != nil { fmt.Printf("%s Failed to parse filesystem JSON: %v\n", logPrefix, err) return false, 0 } if len(fsData) > 0 { return true, fsData[0].Percent } return true, 0 } // Helper function to fetch uptime func fetchUptime(client *http.Client, baseURL, logPrefix string) string { uptimeResp, err := client.Get(fmt.Sprintf("%s/api/4/uptime", baseURL)) if err != nil || uptimeResp.StatusCode != http.StatusOK { if err != nil { fmt.Printf("%s Uptime request failed: %v\n", logPrefix, err) } else { fmt.Printf("%s Bad uptime status code: %d\n", logPrefix, uptimeResp.StatusCode) uptimeResp.Body.Close() } return "" } defer uptimeResp.Body.Close() // Read the response body as a string first uptimeBytes, err := io.ReadAll(uptimeResp.Body) if err != nil { fmt.Printf("%s Failed to read uptime response: %v\n", logPrefix, err) return "" } uptimeStr := strings.Trim(string(uptimeBytes), "\"") // Try to parse as JSON object first, then fallback to direct string if that fails var uptimeData models.UptimeResponse if jsonErr := json.Unmarshal(uptimeBytes, &uptimeData); jsonErr == nil && uptimeData.Value != "" { uptimeStr = formatUptime(uptimeData.Value) } else { // Use the string directly uptimeStr = formatUptime(uptimeStr) } fmt.Printf("%s Uptime: %s (formatted: %s)\n", logPrefix, string(uptimeBytes), uptimeStr) return uptimeStr } // Helper function to send notification about status change func sendStatusChangeNotification(server models.Server, online bool, template string, notifSender *notifications.NotificationSender) { status := "offline" if online { status = "online" } message := strings.ReplaceAll(template, "!name", server.Name) message = strings.ReplaceAll(message, "!status", status) notifSender.SendNotifications(message) } // Helper function to update server status func updateServerStatus(db *sql.DB, serverID int, online bool, cpuUsage, ramUsage, diskUsage float64, uptime string) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _, err := db.ExecContext(ctx, `UPDATE server SET online = $1, "cpuUsage" = $2::float8, "ramUsage" = $3::float8, "diskUsage" = $4::float8, "uptime" = $5 WHERE id = $6`, online, cpuUsage, ramUsage, diskUsage, uptime, serverID, ) if err != nil { fmt.Printf("Failed to update server status (ID: %d): %v\n", serverID, err) } } // Helper function to add server history entry func addServerHistoryEntry(db *sql.DB, serverID int, online bool, cpuUsage, ramUsage, diskUsage float64) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _, err := db.ExecContext(ctx, `INSERT INTO server_history( "serverId", online, "cpuUsage", "ramUsage", "diskUsage", "createdAt" ) VALUES ($1, $2, $3, $4, $5, now())`, serverID, online, fmt.Sprintf("%.2f", cpuUsage), fmt.Sprintf("%.2f", ramUsage), fmt.Sprintf("%.2f", diskUsage), ) if err != nil { fmt.Printf("Failed to insert server history (ID: %d): %v\n", serverID, err) } } // FormatUptime formats the uptime string to a standard format func formatUptime(uptimeStr string) string { // Example input: "3 days, 3:52:36" // Target output: "28.6 13:52" now := time.Now() // Parse the uptime components parts := strings.Split(uptimeStr, ", ") var days int var timeStr string if len(parts) == 2 { // Has days part and time part _, err := fmt.Sscanf(parts[0], "%d days", &days) if err != nil { // Try singular "day" _, err = fmt.Sscanf(parts[0], "%d day", &days) if err != nil { return uptimeStr // Return original if parsing fails } } timeStr = parts[1] } else if len(parts) == 1 { // Only has time part (less than a day) days = 0 timeStr = parts[0] } else { return uptimeStr // Return original if format is unexpected } // Parse the time component (hours:minutes:seconds) var hours, minutes, seconds int _, err := fmt.Sscanf(timeStr, "%d:%d:%d", &hours, &minutes, &seconds) if err != nil { return uptimeStr // Return original if parsing fails } // Calculate the total duration duration := time.Duration(days)*24*time.Hour + time.Duration(hours)*time.Hour + time.Duration(minutes)*time.Minute + time.Duration(seconds)*time.Second // Calculate the start time by subtracting the duration from now startTime := now.Add(-duration) // Format the result in the required format (day.month hour:minute) return startTime.Format("2.1 15:04") }