feat(agent): auto-renice top CPU process after 2 consecutive high-CPU reports
- Agent tracks consecutive reports above cpu_threshold (default 90%) - After renice_consecutive reports (default 2 = 60s sustained), runs renice -n 19 on the top CPU process - Reports the action back in the heartbeat payload - Backend sends Telegram alert when renice is applied - All params configurable in agent.yml and install.sh flags Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
fd72eec281
commit
66088ce691
+13
-4
@@ -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"
|
||||
|
||||
|
||||
+53
-11
@@ -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",
|
||||
|
||||
@@ -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("🔧 <b>Renice automático aplicado</b>\nServidor: <b>%s</b>\n%s\n\n<i>CPU sostenida por encima del umbral.</i>",
|
||||
escapeTelegramHTML(servidorNombre),
|
||||
escapeTelegramHTML(accion))
|
||||
sendTelegramAdmin(msg)
|
||||
}
|
||||
|
||||
func sendTelegramAdmin(mensaje string) {
|
||||
configs, err := models.GetAllTelegramConfigs()
|
||||
if err != nil {
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user