package main import ( "bytes" "encoding/json" "flag" "fmt" "log/slog" "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() // Set up verbose logging slog.SetLogLoggerLevel(slog.LevelDebug) if *server == "" { slog.Error("Server flag is required") os.Exit(1) } // 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 { slog.Error("Failed to generate agent ID", "error", err) os.Exit(1) } } slog.Info("Starting agent", "id", id) slog.Info("Reporting configuration", "server", *server, "interval", *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: slog.Info("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 { slog.Error("Error collecting stats", "agentID", agentID, "hostname", hostname, "error", err) return } // Marshal to JSON body, err := json.Marshal(stat) if err != nil { slog.Error("Error marshaling stats", "agentID", agentID, "error", err) return } // Send to server reportURL := fmt.Sprintf("%s/api/report?id=%s", serverURL, agentID) slog.Debug("Sending stats report", "agentID", agentID, "url", reportURL, "bodySize", len(body)) resp, err := http.Post(reportURL, "application/json", bytes.NewReader(body)) if err != nil { slog.Error("Error reporting stats", "agentID", agentID, "url", reportURL, "error", err) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { slog.Error("Server returned non-OK status", "agentID", agentID, "statusCode", resp.StatusCode, "url", reportURL) return } slog.Debug("Stats reported successfully", "agentID", agentID, "cpu", stat.CPUUsage, "ramUsage", formatBytes(stat.RAMUsage), "ramTotal", formatBytes(stat.RAMTotal), "diskUsage", formatBytes(stat.DiskUsage), "diskTotal", 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) } }