package main import ( "bytes" "encoding/json" "flag" "fmt" "log" "net/http" "net/url" "os" "os/signal" "strings" "syscall" "time" "nerd-monitor/internal/stats" ) func main() { var ( server = flag.String("server", "", "server URL (required)") interval = flag.Duration("interval", 15*time.Second, "reporting interval") agentID = flag.String("id", "", "agent ID (auto-generated if empty)") ) flag.Parse() if *server == "" { log.Fatal("--server flag is required") } // Normalize server URL (add http:// if missing) *server = normalizeURL(*server) // Generate or use provided agent ID id := *agentID if id == "" { var err error id, err = generateAgentID() if err != nil { log.Fatalf("Failed to generate agent ID: %v", err) } } log.Printf("Starting agent with ID: %s\n", id) log.Printf("Reporting to: %s every %v\n", *server, *interval) // Initialize stats collector collector := stats.NewCollector() // Setup graceful shutdown sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) // Report loop ticker := time.NewTicker(*interval) defer ticker.Stop() // Report immediately on start reportStats(id, *server, collector) for { select { case <-ticker.C: reportStats(id, *server, collector) case <-sigChan: log.Println("Agent shutting down gracefully...") os.Exit(0) } } } // reportStats collects and reports system statistics to the server. func reportStats(agentID, serverURL string, collector *stats.Collector) { hostname, err := os.Hostname() if err != nil { hostname = agentID } stat, err := collector.Collect(hostname) if err != nil { log.Printf("Error collecting stats: %v", err) return } // Marshal to JSON body, err := json.Marshal(stat) if err != nil { log.Printf("Error marshaling stats: %v", err) return } // Send to server reportURL := fmt.Sprintf("%s/api/report?id=%s", serverURL, agentID) resp, err := http.Post(reportURL, "application/json", bytes.NewReader(body)) if err != nil { log.Printf("Error reporting stats: %v", err) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { log.Printf("Server returned status %d", resp.StatusCode) return } log.Printf("Stats reported: CPU %.1f%% | RAM %s / %s | Disk %s / %s", stat.CPUUsage, formatBytes(stat.RAMUsage), formatBytes(stat.RAMTotal), formatBytes(stat.DiskUsage), formatBytes(stat.DiskTotal), ) } // generateAgentID creates an ID based on hostname. func generateAgentID() (string, error) { hostname, err := os.Hostname() if err != nil { return "", fmt.Errorf("failed to get hostname: %w", err) } return hostname, nil } // normalizeURL ensures the URL has a valid scheme and port. func normalizeURL(server string) string { // If no scheme, add http:// if !strings.Contains(server, "://") { server = "http://" + server } // Parse to check if port is specified u, err := url.Parse(server) if err != nil { return server } // If no port specified, add default port 8080 if u.Port() == "" { return u.Scheme + "://" + u.Hostname() + ":8080" } return server } // formatBytes converts bytes to human-readable format. func formatBytes(bytes uint64) string { const ( kb = 1024 mb = kb * 1024 gb = mb * 1024 ) switch { case bytes >= gb: return fmt.Sprintf("%.1f GB", float64(bytes)/float64(gb)) case bytes >= mb: return fmt.Sprintf("%.1f MB", float64(bytes)/float64(mb)) case bytes >= kb: return fmt.Sprintf("%.1f KB", float64(bytes)/float64(kb)) default: return fmt.Sprintf("%d B", bytes) } }