feat: agente con métricas de red, swap, top procesos y TCP; dashboard mejorado
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
491e908448
commit
34b9c81262
+117
-25
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user