- 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 <noreply@anthropic.com>
189 lines
5.6 KiB
Go
189 lines
5.6 KiB
Go
package controllers
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
|
)
|
|
|
|
// AgentHeartbeat recibe métricas del agente instalado en un servidor.
|
|
// Ruta pública: POST /agent/heartbeat — autenticada por token en JSON body.
|
|
func AgentHeartbeat(c *fiber.Ctx) error {
|
|
type HeartbeatRequest struct {
|
|
Token string `json:"token"`
|
|
Metricas string `json:"metricas"` // JSON string ya serializado por el agente
|
|
}
|
|
|
|
var req HeartbeatRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"ok": false, "error": "body inválido"})
|
|
}
|
|
if req.Token == "" {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"ok": false, "error": "token requerido"})
|
|
}
|
|
|
|
db := app.Http.Database.DB
|
|
var servidor models.Servidor
|
|
if err := db.Where("agent_token = ?", req.Token).First(&servidor).Error; err != nil {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"ok": false, "error": "token inválido"})
|
|
}
|
|
|
|
now := time.Now()
|
|
updates := map[string]interface{}{
|
|
"agent_last_seen": &now,
|
|
"ultimo_ping": now.Format(time.RFC3339),
|
|
}
|
|
if req.Metricas != "" {
|
|
updates["metricas_json"] = req.Metricas
|
|
// Actualizar campos planos desde las métricas del agente (la verdad viene del agente)
|
|
type agentRAM struct {
|
|
TotalGB float64 `json:"total_gb"`
|
|
}
|
|
type agentCPU struct {
|
|
Nucleos int `json:"nucleos"`
|
|
}
|
|
var rawMap map[string]any
|
|
if jsonErr := json.Unmarshal([]byte(req.Metricas), &rawMap); jsonErr == nil {
|
|
if os, _ := rawMap["os"].(string); os != "" {
|
|
updates["so"] = os
|
|
}
|
|
if ram, _ := rawMap["ram"].(map[string]any); ram != nil {
|
|
if total, _ := ram["total_gb"].(float64); total > 0 {
|
|
updates["ram"] = fmt.Sprintf("%.0f GB", total)
|
|
}
|
|
}
|
|
if cpu, _ := rawMap["cpu"].(map[string]any); cpu != nil {
|
|
if n, _ := cpu["nucleos"].(float64); n > 0 {
|
|
updates["nucleos"] = fmt.Sprintf("%d", int(n))
|
|
}
|
|
}
|
|
// Compatibilidad: nuevo formato (discos array) y viejo (disco objeto)
|
|
discos, hasArray := rawMap["discos"].([]any)
|
|
if hasArray {
|
|
var maxTotal float64
|
|
for _, item := range discos {
|
|
if d, _ := item.(map[string]any); d != nil {
|
|
if t, _ := d["total_gb"].(float64); t > maxTotal {
|
|
maxTotal = t
|
|
}
|
|
}
|
|
}
|
|
if maxTotal > 0 {
|
|
updates["disco"] = fmt.Sprintf("%.0f GB", maxTotal)
|
|
}
|
|
} else if disco, _ := rawMap["disco"].(map[string]any); disco != nil {
|
|
if total, _ := disco["total_gb"].(float64); total > 0 {
|
|
updates["disco"] = fmt.Sprintf("%.0f GB", total)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := db.Model(&servidor).Updates(updates).Error; err != nil {
|
|
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 {
|
|
id := c.Params("id")
|
|
db := app.Http.Database.DB
|
|
|
|
var servidor models.Servidor
|
|
if err := db.First(&servidor, id).Error; err != nil {
|
|
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "servidor no encontrado"})
|
|
}
|
|
|
|
// Generar token aleatorio de 32 bytes (64 hex chars)
|
|
tokenBytes := make([]byte, 32)
|
|
if _, err := rand.Read(tokenBytes); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "no se pudo generar token"})
|
|
}
|
|
token := hex.EncodeToString(tokenBytes)
|
|
|
|
if err := db.Model(&servidor).Update("agent_token", token).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "no se pudo guardar token"})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"ok": true,
|
|
"agent_token": token,
|
|
"servidor_id": servidor.ID,
|
|
})
|
|
}
|