172 lines
3.9 KiB
Go
172 lines
3.9 KiB
Go
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)
|
|
}
|
|
}
|