diff --git a/agent/install.sh b/agent/install.sh
index 2e46afb..5abded1 100644
--- a/agent/install.sh
+++ b/agent/install.sh
@@ -18,13 +18,19 @@ TOKEN=""
API_URL=""
INTERVAL=30
INSTALL_DIR=""
+CPU_THRESHOLD=90
+RENICE_ENABLED=true
+RENICE_CONSECUTIVE=2
for arg in "$@"; do
case $arg in
- --token=*) TOKEN="${arg#*=}" ;;
- --url=*) API_URL="${arg#*=}" ;;
- --interval=*) INTERVAL="${arg#*=}" ;;
- --dir=*) INSTALL_DIR="${arg#*=}" ;;
+ --token=*) TOKEN="${arg#*=}" ;;
+ --url=*) API_URL="${arg#*=}" ;;
+ --interval=*) INTERVAL="${arg#*=}" ;;
+ --dir=*) INSTALL_DIR="${arg#*=}" ;;
+ --cpu-threshold=*) CPU_THRESHOLD="${arg#*=}" ;;
+ --renice=*) RENICE_ENABLED="${arg#*=}" ;;
+ --renice-consecutive=*) RENICE_CONSECUTIVE="${arg#*=}" ;;
esac
done
@@ -113,6 +119,9 @@ api_url: ${API_URL}
token: ${TOKEN}
interval: ${INTERVAL}
debug: false
+cpu_threshold: ${CPU_THRESHOLD}
+renice_enabled: ${RENICE_ENABLED}
+renice_consecutive: ${RENICE_CONSECUTIVE}
EOF
ok "Configuración creada en ${INSTALL_DIR}/agent.yml"
diff --git a/agent/main.go b/agent/main.go
index 6ccf5d5..ec431a3 100644
--- a/agent/main.go
+++ b/agent/main.go
@@ -8,6 +8,7 @@ import (
"log"
"net/http"
"os"
+ "os/exec"
"runtime"
"sort"
"time"
@@ -25,10 +26,13 @@ import (
// ── Config ────────────────────────────────────────────────────────────────────
type Config struct {
- APIURL string `yaml:"api_url"`
- Token string `yaml:"token"`
- Interval int `yaml:"interval"`
- Debug bool `yaml:"debug"`
+ APIURL string `yaml:"api_url"`
+ Token string `yaml:"token"`
+ Interval int `yaml:"interval"`
+ Debug bool `yaml:"debug"`
+ CPUThreshold float64 `yaml:"cpu_threshold"` // % para activar renice (default 90)
+ ReniceEnabled bool `yaml:"renice_enabled"` // activar renice automático
+ ReniceConsecutive int `yaml:"renice_consecutive"` // reportes consecutivos requeridos (default 2)
}
func loadConfig(path string) (*Config, error) {
@@ -43,6 +47,12 @@ func loadConfig(path string) (*Config, error) {
if cfg.Interval <= 0 {
cfg.Interval = 30
}
+ if cfg.CPUThreshold <= 0 {
+ cfg.CPUThreshold = 90
+ }
+ if cfg.ReniceConsecutive <= 0 {
+ cfg.ReniceConsecutive = 2
+ }
return &cfg, nil
}
@@ -286,19 +296,21 @@ func collectMetrics() (*Metricas, error) {
// ── Reporte ───────────────────────────────────────────────────────────────────
type HeartbeatRequest struct {
- Token string `json:"token"`
- Metricas string `json:"metricas"`
+ Token string `json:"token"`
+ Metricas string `json:"metricas"`
+ ReniceAction string `json:"renice_action,omitempty"` // descripción si se reniceó algo
}
-func sendHeartbeat(cfg *Config, metricas *Metricas) error {
+func sendHeartbeat(cfg *Config, metricas *Metricas, reniceAction string) error {
metJSON, err := json.Marshal(metricas)
if err != nil {
return err
}
payload := HeartbeatRequest{
- Token: cfg.Token,
- Metricas: string(metJSON),
+ Token: cfg.Token,
+ Metricas: string(metJSON),
+ ReniceAction: reniceAction,
}
body, _ := json.Marshal(payload)
@@ -350,11 +362,14 @@ func main() {
log.Fatal("api_url y token son requeridos")
}
- log.Printf("🚀 usite-agent iniciado | API: %s | Intervalo: %ds", cfg.APIURL, cfg.Interval)
+ log.Printf("🚀 usite-agent iniciado | API: %s | Intervalo: %ds | Renice: %v (umbral %.0f%%, %d reportes)",
+ cfg.APIURL, cfg.Interval, cfg.ReniceEnabled, cfg.CPUThreshold, cfg.ReniceConsecutive)
ticker := time.NewTicker(time.Duration(cfg.Interval) * time.Second)
defer ticker.Stop()
+ consecAlto := 0 // contador de reportes consecutivos con CPU alta
+
reportar := func() {
m, err := collectMetrics()
if err != nil {
@@ -365,7 +380,34 @@ func main() {
b, _ := json.MarshalIndent(m, "", " ")
log.Printf("📊 Métricas:\n%s", string(b))
}
- if err := sendHeartbeat(cfg, m); err != nil {
+
+ // ── Lógica de renice ──────────────────────────────────────────────────
+ var reniceAction string
+ if cfg.ReniceEnabled && m.CPU.Porcentaje >= cfg.CPUThreshold {
+ consecAlto++
+ log.Printf("⚠️ CPU alta: %.1f%% (reporte %d/%d)", m.CPU.Porcentaje, consecAlto, cfg.ReniceConsecutive)
+ if consecAlto >= cfg.ReniceConsecutive && len(m.TopProcs) > 0 {
+ // Ordenar top_procs por CPU para encontrar el culpable real
+ procs := make([]ProcesoInfo, len(m.TopProcs))
+ copy(procs, m.TopProcs)
+ sort.Slice(procs, func(i, j int) bool { return procs[i].CPU > procs[j].CPU })
+ culpable := procs[0]
+ if culpable.CPU > 0 && culpable.PID > 0 {
+ cmd := exec.Command("renice", "-n", "19", "-p", fmt.Sprintf("%d", culpable.PID))
+ if rerr := cmd.Run(); rerr != nil {
+ log.Printf("❌ renice falló para PID %d (%s): %v", culpable.PID, culpable.Nombre, rerr)
+ } else {
+ reniceAction = fmt.Sprintf("renice +19 aplicado a %s (PID %d, CPU %.1f%%)", culpable.Nombre, culpable.PID, culpable.CPU)
+ log.Printf("🔧 %s", reniceAction)
+ consecAlto = 0 // reiniciar tras actuar
+ }
+ }
+ }
+ } else {
+ consecAlto = 0
+ }
+
+ if err := sendHeartbeat(cfg, m, reniceAction); err != nil {
log.Printf("⚠️ Error enviando heartbeat: %v", err)
} else {
log.Printf("✅ Heartbeat | RAM: %.1f%% | CPU: %.1f%% | TCP: %d | Procs: %d",
diff --git a/pkg/services/notif_dispatch.go b/pkg/services/notif_dispatch.go
index 3e4cdd7..f9d3cea 100644
--- a/pkg/services/notif_dispatch.go
+++ b/pkg/services/notif_dispatch.go
@@ -246,6 +246,14 @@ func sendTelegramPortalUser(chatID string, mensaje string) error {
return fmt.Errorf("no hay bots de Telegram activos para enviar")
}
+// NotificarReniceAction envía Telegram cuando el agente aplica un renice automático.
+func NotificarReniceAction(servidorNombre, accion string) {
+ msg := fmt.Sprintf("🔧 Renice automático aplicado\nServidor: %s\n%s\n\nCPU sostenida por encima del umbral.",
+ escapeTelegramHTML(servidorNombre),
+ escapeTelegramHTML(accion))
+ sendTelegramAdmin(msg)
+}
+
func sendTelegramAdmin(mensaje string) {
configs, err := models.GetAllTelegramConfigs()
if err != nil {
diff --git a/rest/controllers/agent_controller.go b/rest/controllers/agent_controller.go
index 3cbc573..fc3d296 100644
--- a/rest/controllers/agent_controller.go
+++ b/rest/controllers/agent_controller.go
@@ -10,14 +10,16 @@ import (
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/app"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
+ "github.com/sujit-baniya/fiber-boilerplate/pkg/services"
)
// 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
+ Token string `json:"token"`
+ Metricas string `json:"metricas"`
+ ReniceAction string `json:"renice_action"`
}
var req HeartbeatRequest
@@ -136,6 +138,11 @@ func AgentHeartbeat(c *fiber.Ctx) error {
}
}
+ // Notificar por Telegram si el agente aplicó un renice
+ if req.ReniceAction != "" {
+ go services.NotificarReniceAction(servidor.Nombre, req.ReniceAction)
+ }
+
return c.JSON(fiber.Map{"ok": true, "servidor_id": servidor.ID})
}