Files
nerd-monitor/cmd/agent/main.go
Ducky SSH User 50dcfcdc83
All checks were successful
Build and Release / build (push) Successful in 35s
Add logging and fix /agents/ route error
2025-12-20 07:34:02 +00:00

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)
}
}