diff --git a/agent/main.go b/agent/main.go index 6d8d55d..a3d9f1b 100644 --- a/agent/main.go +++ b/agent/main.go @@ -9,6 +9,7 @@ import ( "net/http" "os" "runtime" + "sort" "time" "github.com/shirou/gopsutil/v3/cpu" @@ -16,15 +17,17 @@ import ( "github.com/shirou/gopsutil/v3/host" "github.com/shirou/gopsutil/v3/load" "github.com/shirou/gopsutil/v3/mem" + pnet "github.com/shirou/gopsutil/v3/net" + "github.com/shirou/gopsutil/v3/process" "gopkg.in/yaml.v3" ) // ── Config ──────────────────────────────────────────────────────────────────── type Config struct { - APIURL string `yaml:"api_url"` // https://admin.u-site.app - Token string `yaml:"token"` // token generado desde el panel - Interval int `yaml:"interval"` // segundos entre reportes (default: 30) + APIURL string `yaml:"api_url"` + Token string `yaml:"token"` + Interval int `yaml:"interval"` Debug bool `yaml:"debug"` } @@ -46,16 +49,20 @@ func loadConfig(path string) (*Config, error) { // ── Métricas ────────────────────────────────────────────────────────────────── type Metricas struct { - Hostname string `json:"hostname"` - OS string `json:"os"` - Arch string `json:"arch"` - Uptime uint64 `json:"uptime_seg"` - UptimeStr string `json:"uptime"` - RAM RAMInfo `json:"ram"` - CPU CPUInfo `json:"cpu"` - Discos []DiscoInfo `json:"discos"` - LoadAvg LoadInfo `json:"load_avg"` - ReportadoEn string `json:"reportado_en"` + Hostname string `json:"hostname"` + OS string `json:"os"` + Arch string `json:"arch"` + Uptime uint64 `json:"uptime_seg"` + UptimeStr string `json:"uptime"` + RAM RAMInfo `json:"ram"` + Swap SwapInfo `json:"swap"` + CPU CPUInfo `json:"cpu"` + Discos []DiscoInfo `json:"discos"` + LoadAvg LoadInfo `json:"load_avg"` + Red []RedInfo `json:"red"` + TCPConns int `json:"tcp_conns"` + TopProcs []ProcesoInfo `json:"top_procs"` + ReportadoEn string `json:"reportado_en"` } type RAMInfo struct { @@ -65,6 +72,12 @@ type RAMInfo struct { Porcentaje float64 `json:"porcentaje"` } +type SwapInfo struct { + TotalGB float64 `json:"total_gb"` + UsadoGB float64 `json:"usado_gb"` + Porcentaje float64 `json:"porcentaje"` +} + type CPUInfo struct { Nucleos int `json:"nucleos"` Porcentaje float64 `json:"porcentaje"` @@ -84,6 +97,21 @@ type LoadInfo struct { Load15 float64 `json:"load15"` } +type RedInfo struct { + Interface string `json:"interface"` + BytesEnvMB float64 `json:"bytes_env_mb"` + BytesRecMB float64 `json:"bytes_rec_mb"` + PktEnv uint64 `json:"pkt_env"` + PktRec uint64 `json:"pkt_rec"` +} + +type ProcesoInfo struct { + PID int32 `json:"pid"` + Nombre string `json:"nombre"` + CPU float64 `json:"cpu"` + RAMMB float64 `json:"ram_mb"` +} + func round2(v float64) float64 { return float64(int(v*100)) / 100 } @@ -110,11 +138,11 @@ func collectMetrics() (*Metricas, error) { m.Hostname = hn } - // Uptime + // Uptime + OS if info, err := host.Info(); err == nil { m.Uptime = info.Uptime m.UptimeStr = uptimeStr(info.Uptime) - if info.OS != "" { + if info.Platform != "" { m.OS = info.Platform + " " + info.PlatformVersion } } @@ -130,16 +158,26 @@ func collectMetrics() (*Metricas, error) { } } + // Swap + if sv, err := mem.SwapMemory(); err == nil && sv.Total > 0 { + gb := 1024.0 * 1024 * 1024 + m.Swap = SwapInfo{ + TotalGB: round2(float64(sv.Total) / gb), + UsadoGB: round2(float64(sv.Used) / gb), + Porcentaje: round2(sv.UsedPercent), + } + } + // CPU m.CPU.Nucleos = runtime.NumCPU() if pcts, err := cpu.Percent(1*time.Second, false); err == nil && len(pcts) > 0 { m.CPU.Porcentaje = round2(pcts[0]) } - // Discos: todas las particiones físicas/reales + // Discos if parts, err := disk.Partitions(false); err == nil { gb := 1024.0 * 1024 * 1024 - skipFSTypes := map[string]bool{ + skipFS := map[string]bool{ "proc": true, "sysfs": true, "tmpfs": true, "devtmpfs": true, "devpts": true, "cgroup": true, "cgroup2": true, "overlay": true, "hugetlbfs": true, "mqueue": true, "pstore": true, "securityfs": true, @@ -148,7 +186,7 @@ func collectMetrics() (*Metricas, error) { "squashfs": true, "ramfs": true, "nsfs": true, "none": true, } for _, p := range parts { - if skipFSTypes[p.Fstype] { + if skipFS[p.Fstype] { continue } if d, err := disk.Usage(p.Mountpoint); err == nil { @@ -162,7 +200,6 @@ func collectMetrics() (*Metricas, error) { } } } - // Fallback: si no se encontraron particiones, usar raíz if len(m.Discos) == 0 { root := "/" if runtime.GOOS == "windows" { @@ -180,11 +217,69 @@ func collectMetrics() (*Metricas, error) { } } - // Load average (Linux/macOS) + // Load average if la, err := load.Avg(); err == nil { m.LoadAvg = LoadInfo{Load1: round2(la.Load1), Load5: round2(la.Load5), Load15: round2(la.Load15)} } + // Red (interfaces físicas, excluir loopback) + if counters, err := pnet.IOCounters(true); err == nil { + mb := 1024.0 * 1024 + for _, c := range counters { + if c.Name == "lo" || c.Name == "lo0" { + continue + } + m.Red = append(m.Red, RedInfo{ + Interface: c.Name, + BytesEnvMB: round2(float64(c.BytesSent) / mb), + BytesRecMB: round2(float64(c.BytesRecv) / mb), + PktEnv: c.PacketsSent, + PktRec: c.PacketsRecv, + }) + } + } + + // TCP conexiones activas + if conns, err := pnet.Connections("tcp"); err == nil { + m.TCPConns = len(conns) + } + + // Top 10 procesos por CPU (luego por RAM como desempate) + if procs, err := process.Processes(); err == nil { + type pd struct { + pid int32 + name string + cpu float64 + ramMB float64 + } + var lista []pd + for _, p := range procs { + cpuPct, _ := p.CPUPercent() + name, _ := p.Name() + if name == "" { + continue + } + ramMB := 0.0 + if mi, err := p.MemoryInfo(); err == nil && mi != nil { + ramMB = round2(float64(mi.RSS) / (1024 * 1024)) + } + lista = append(lista, pd{p.Pid, name, round2(cpuPct), ramMB}) + } + sort.Slice(lista, func(i, j int) bool { + if lista[i].cpu != lista[j].cpu { + return lista[i].cpu > lista[j].cpu + } + return lista[i].ramMB > lista[j].ramMB + }) + limit := 10 + if len(lista) < limit { + limit = len(lista) + } + for _, d := range lista[:limit] { + m.TopProcs = append(m.TopProcs, ProcesoInfo{d.pid, d.name, d.cpu, d.ramMB}) + } + } + return m, nil } @@ -238,14 +333,12 @@ func main() { cfg, err := loadConfig(*configPath) if err != nil { - // Si no hay config pero se pasaron flags, usarlos directamente if *apiURL == "" || *token == "" { log.Fatalf("Error cargando config: %v\nUso: usite-agent --api-url=https://... --token=...", err) } cfg = &Config{APIURL: *apiURL, Token: *token, Interval: 30} } - // Flags sobreescriben config if *apiURL != "" { cfg.APIURL = *apiURL } @@ -262,7 +355,6 @@ func main() { ticker := time.NewTicker(time.Duration(cfg.Interval) * time.Second) defer ticker.Stop() - // Enviar inmediatamente al iniciar reportar := func() { m, err := collectMetrics() if err != nil { @@ -276,8 +368,8 @@ func main() { if err := sendHeartbeat(cfg, m); err != nil { log.Printf("⚠️ Error enviando heartbeat: %v", err) } else { - log.Printf("✅ Heartbeat enviado | RAM: %.1f%% | CPU: %.1f%% | Discos: %d", - m.RAM.Porcentaje, m.CPU.Porcentaje, len(m.Discos)) + log.Printf("✅ Heartbeat | RAM: %.1f%% | CPU: %.1f%% | TCP: %d | Procs: %d", + m.RAM.Porcentaje, m.CPU.Porcentaje, m.TCPConns, len(m.TopProcs)) } } diff --git a/resources/views/servidor_dashboard.html b/resources/views/servidor_dashboard.html index aecc064..a362114 100644 --- a/resources/views/servidor_dashboard.html +++ b/resources/views/servidor_dashboard.html @@ -109,6 +109,79 @@

+ + + + + + + +
+ 🔌 TCP: + 📊 Load: +
+ + + +
@@ -654,6 +727,13 @@ catch { return {}; } }, + fmtMB(mb) { + if (mb === undefined || mb === null) return '—'; + if (mb >= 1024) return (mb / 1024).toFixed(1) + ' GB'; + if (mb >= 1) return mb.toFixed(0) + ' MB'; + return (mb * 1024).toFixed(0) + ' KB'; + }, + copiar(texto) { navigator.clipboard.writeText(texto).catch(() => {}); },