From ae74d0ef90a7fc28d6375c47ed51d7f998f77550 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:03:16 -0500 Subject: [PATCH] feat(agent): persist 7-day metrics history per server heartbeat - New table servidor_metricas_history (cpu%, ram%, swap%, disco%, tcp, load1) - Each heartbeat inserts a compact row (~60 bytes) - GET /app/servidor/:id/metricas-history?horas=24 returns the data - Daily cron at 3AM purges rows older than 7 days - ~1.2 MB per server per 7 days at 30s interval Co-Authored-By: Claude Sonnet 4.6 --- migrations/migrate.go | 2 + pkg/models/servidor.go | 46 +++++++++++++++++++ pkg/services/cron_service.go | 12 +++++ rest/controllers/agent_controller.go | 66 ++++++++++++++++++++++++++++ rest/routes/user.go | 1 + 5 files changed, 127 insertions(+) diff --git a/migrations/migrate.go b/migrations/migrate.go index 182a513..c1384d8 100755 --- a/migrations/migrate.go +++ b/migrations/migrate.go @@ -96,6 +96,8 @@ func Migrate() { &models.VcardApiConfig{}, // Integración Coolify &models.CoolifyConfig{}, + // Historial de métricas del agente + &models.ServidorMetricasHistory{}, ); err != nil { log.Fatalf("Error during main migration: %v", err) } diff --git a/pkg/models/servidor.go b/pkg/models/servidor.go index 6f1770b..5b32a1a 100755 --- a/pkg/models/servidor.go +++ b/pkg/models/servidor.go @@ -103,3 +103,49 @@ func DeleteServidor(servidor Servidor) error { } return nil } + +// ── Historial de métricas ───────────────────────────────────────────────────── + +type ServidorMetricasHistory struct { + ID uint `json:"id" gorm:"primaryKey;autoIncrement"` + ServidorID uint `json:"servidor_id" gorm:"column:servidor_id;index"` + CreatedAt time.Time `json:"created_at" gorm:"column:created_at;index"` + CpuPct float64 `json:"cpu_pct" gorm:"column:cpu_pct"` + RamPct float64 `json:"ram_pct" gorm:"column:ram_pct"` + SwapPct float64 `json:"swap_pct" gorm:"column:swap_pct"` + DiscoPct float64 `json:"disco_pct" gorm:"column:disco_pct"` + TCPConns int `json:"tcp_conns" gorm:"column:tcp_conns"` + Load1 float64 `json:"load1" gorm:"column:load1"` +} + +func (ServidorMetricasHistory) TableName() string { return "servidor_metricas_history" } + +func InsertMetricasHistory(servidorID uint, cpu, ram, swap, disco float64, tcp int, load1 float64) { + app.Http.Database.DB.Create(&ServidorMetricasHistory{ + ServidorID: servidorID, + CreatedAt: time.Now(), + CpuPct: cpu, + RamPct: ram, + SwapPct: swap, + DiscoPct: disco, + TCPConns: tcp, + Load1: load1, + }) +} + +func GetMetricasHistory(servidorID uint, horas int) ([]ServidorMetricasHistory, error) { + var items []ServidorMetricasHistory + desde := time.Now().Add(-time.Duration(horas) * time.Hour) + err := app.Http.Database.DB. + Where("servidor_id = ? AND created_at >= ?", servidorID, desde). + Order("created_at ASC"). + Find(&items).Error + return items, err +} + +func PurgarMetricasHistory(dias int) error { + corte := time.Now().AddDate(0, 0, -dias) + return app.Http.Database.DB. + Where("created_at < ?", corte). + Delete(&ServidorMetricasHistory{}).Error +} diff --git a/pkg/services/cron_service.go b/pkg/services/cron_service.go index f84bcdd..4f64cbb 100644 --- a/pkg/services/cron_service.go +++ b/pkg/services/cron_service.go @@ -48,6 +48,18 @@ func IniciarCron() { return } + // Purga de historial de métricas — cada noche a las 3 AM (retención 7 días) + if _, err := cronScheduler.AddFunc("0 3 * * *", func() { + if err := models.PurgarMetricasHistory(7); err != nil { + log.Printf("[CRON] Error purgando historial métricas: %v", err) + } else { + log.Println("[CRON] Historial de métricas purgado (>7 días)") + } + }); err != nil { + log.Printf("[CRON] Error registrando tarea purga_metricas: %v", err) + return + } + cronScheduler.Start() log.Println("[CRON] Scheduler iniciado — vencimientos próximos 8AM, ya vencidos 9AM, Bold polling cada 15min, salud servidores cada 5min") } diff --git a/rest/controllers/agent_controller.go b/rest/controllers/agent_controller.go index 26be45e..3cbc573 100644 --- a/rest/controllers/agent_controller.go +++ b/rest/controllers/agent_controller.go @@ -89,9 +89,75 @@ func AgentHeartbeat(c *fiber.Ctx) error { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"ok": false, "error": "no se pudo guardar"}) } + // Insertar punto en el historial de métricas + if req.Metricas != "" { + var raw map[string]any + if json.Unmarshal([]byte(req.Metricas), &raw) == nil { + getFloat := func(obj map[string]any, key string) float64 { + if sub, ok := obj[key].(map[string]any); ok { + if v, ok := sub["porcentaje"].(float64); ok { + return v + } + } + return 0 + } + discoPct := 0.0 + if discos, ok := raw["discos"].([]any); ok && len(discos) > 0 { + if d, ok := discos[0].(map[string]any); ok { + if v, ok := d["porcentaje"].(float64); ok { + discoPct = v + } + } + } + tcpConns := 0 + if v, ok := raw["tcp_conns"].(float64); ok { + tcpConns = int(v) + } + load1 := 0.0 + if la, ok := raw["load_avg"].(map[string]any); ok { + if v, ok := la["load1"].(float64); ok { + load1 = v + } + } + swapPct := 0.0 + if sw, ok := raw["swap"].(map[string]any); ok { + if v, ok := sw["porcentaje"].(float64); ok { + swapPct = v + } + } + models.InsertMetricasHistory(servidor.ID, + getFloat(raw, "cpu"), + getFloat(raw, "ram"), + swapPct, + discoPct, + tcpConns, + load1, + ) + } + } + return c.JSON(fiber.Map{"ok": true, "servidor_id": servidor.ID}) } +// GetMetricasHistory devuelve el historial de métricas de un servidor. +// GET /app/servidor/:id/metricas-history?horas=24 +func GetMetricasHistory(c *fiber.Ctx) error { + id := c.Params("id") + horas := c.QueryInt("horas", 24) + if horas <= 0 || horas > 168 { + horas = 24 + } + var srv models.Servidor + if err := app.Http.Database.DB.First(&srv, id).Error; err != nil { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "servidor no encontrado"}) + } + items, err := models.GetMetricasHistory(srv.ID, horas) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(items) +} + // GenerateAgentToken genera o regenera el token de agente para un servidor. // Ruta protegida: POST /app/servidor/:id/agent-token func GenerateAgentToken(c *fiber.Ctx) error { diff --git a/rest/routes/user.go b/rest/routes/user.go index 3191e71..61ce3ab 100755 --- a/rest/routes/user.go +++ b/rest/routes/user.go @@ -81,6 +81,7 @@ func UserRoutes(app fiber.Router) { protected.Get("/servidor-dashboard/:id", controllers.GetServidorDashboard) protected.Get("/conx-ping/:id", controllers.PingConexion) protected.Post("/servidor/:id/agent-token", controllers.GenerateAgentToken) + protected.Get("/servidor/:id/metricas-history", controllers.GetMetricasHistory) protected.Post("/servidor/:id/sync-hostinger", controllers.SyncServidorFromHostinger) // Rutas de proveedores de servidor