Initial commit: Nerd Monitor - Cross-platform system monitoring application
Features: - Multi-platform agents (Linux, macOS, Windows - AMD64 & ARM64) - Real-time CPU, RAM, and disk usage monitoring - Responsive web dashboard with live status indicators - Session-based authentication with secure credentials - Stale agent detection and removal (6+ months inactive) - Auto-refresh dashboard (5 second intervals) - 15-second agent reporting intervals - Auto-generated agent IDs from hostnames - In-memory storage (zero database setup) - Minimal dependencies (Chi router + Templ templating) Project Structure: - cmd/: Agent and Server executables - internal/: API, Auth, Stats, Storage, and UI packages - views/: Templ templates for dashboard UI - Makefile: Build automation for all platforms Ready for deployment with comprehensive documentation: - README.md: Full project documentation - QUICKSTART.md: Getting started guide - AGENTS.md: Development guidelines
This commit is contained in:
164
cmd/agent/main.go
Normal file
164
cmd/agent/main.go
Normal file
@@ -0,0 +1,164 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user