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)
|
||||
}
|
||||
}
|
||||
75
cmd/server/main.go
Normal file
75
cmd/server/main.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"nerd-monitor/internal/api"
|
||||
"nerd-monitor/internal/auth"
|
||||
"nerd-monitor/internal/store"
|
||||
"nerd-monitor/internal/ui"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
addr = flag.String("addr", "0.0.0.0", "server address")
|
||||
port = flag.String("port", "8080", "server port")
|
||||
username = flag.String("username", "admin", "admin username")
|
||||
password = flag.String("password", "admin", "admin password")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
// Initialize dependencies
|
||||
s := store.New()
|
||||
authMgr := auth.New(*username, *password)
|
||||
apiHandler := api.New(s)
|
||||
uiHandler := ui.New(s, authMgr)
|
||||
|
||||
// Setup router
|
||||
r := chi.NewRouter()
|
||||
|
||||
// Public routes (no auth required)
|
||||
r.Post("/api/report", apiHandler.ReportStats)
|
||||
r.Get("/api/agents", apiHandler.ListAgents)
|
||||
r.Get("/api/agents/{id}", apiHandler.GetAgent)
|
||||
r.Get("/login", uiHandler.Login)
|
||||
r.Post("/login", uiHandler.Login)
|
||||
|
||||
// Protected routes (auth required)
|
||||
r.Group(func(protectedRoutes chi.Router) {
|
||||
protectedRoutes.Use(authMgr.Middleware)
|
||||
protectedRoutes.Get("/", uiHandler.Dashboard)
|
||||
protectedRoutes.Get("/agents/{id}", uiHandler.AgentDetail)
|
||||
protectedRoutes.Post("/logout", uiHandler.Logout)
|
||||
protectedRoutes.Post("/api/agents/remove-stale", uiHandler.RemoveStaleAgents)
|
||||
protectedRoutes.Post("/api/agents/{id}/delete", uiHandler.DeleteAgent)
|
||||
})
|
||||
|
||||
// Setup server
|
||||
server := &http.Server{
|
||||
Addr: *addr + ":" + *port,
|
||||
Handler: r,
|
||||
}
|
||||
|
||||
// Graceful shutdown
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
<-sigChan
|
||||
log.Println("Shutting down server...")
|
||||
server.Close()
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
log.Printf("Starting server on http://%s:%s\n", *addr, *port)
|
||||
log.Printf("Login with %s / %s\n", *username, *password)
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("Server error: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user