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
@@ -18,6 +18,9 @@ TOKEN=""
|
|||||||
API_URL=""
|
API_URL=""
|
||||||
INTERVAL=30
|
INTERVAL=30
|
||||||
INSTALL_DIR=""
|
INSTALL_DIR=""
|
||||||
|
CPU_THRESHOLD=90
|
||||||
|
RENICE_ENABLED=true
|
||||||
|
RENICE_CONSECUTIVE=2
|
||||||
|
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case $arg in
|
case $arg in
|
||||||
@@ -25,6 +28,9 @@ for arg in "$@"; do
|
|||||||
--url=*) API_URL="${arg#*=}" ;;
|
--url=*) API_URL="${arg#*=}" ;;
|
||||||
--interval=*) INTERVAL="${arg#*=}" ;;
|
--interval=*) INTERVAL="${arg#*=}" ;;
|
||||||
--dir=*) INSTALL_DIR="${arg#*=}" ;;
|
--dir=*) INSTALL_DIR="${arg#*=}" ;;
|
||||||
|
--cpu-threshold=*) CPU_THRESHOLD="${arg#*=}" ;;
|
||||||
|
--renice=*) RENICE_ENABLED="${arg#*=}" ;;
|
||||||
|
--renice-consecutive=*) RENICE_CONSECUTIVE="${arg#*=}" ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -113,6 +119,9 @@ api_url: ${API_URL}
|
|||||||
token: ${TOKEN}
|
token: ${TOKEN}
|
||||||
interval: ${INTERVAL}
|
interval: ${INTERVAL}
|
||||||
debug: false
|
debug: false
|
||||||
|
cpu_threshold: ${CPU_THRESHOLD}
|
||||||
|
renice_enabled: ${RENICE_ENABLED}
|
||||||
|
renice_consecutive: ${RENICE_CONSECUTIVE}
|
||||||
EOF
|
EOF
|
||||||
ok "Configuración creada en ${INSTALL_DIR}/agent.yml"
|
ok "Configuración creada en ${INSTALL_DIR}/agent.yml"
|
||||||
|
|
||||||
|
|||||||
+45
-3
@@ -8,6 +8,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
@@ -29,6 +30,9 @@ type Config struct {
|
|||||||
Token string `yaml:"token"`
|
Token string `yaml:"token"`
|
||||||
Interval int `yaml:"interval"`
|
Interval int `yaml:"interval"`
|
||||||
Debug bool `yaml:"debug"`
|
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) {
|
func loadConfig(path string) (*Config, error) {
|
||||||
@@ -43,6 +47,12 @@ func loadConfig(path string) (*Config, error) {
|
|||||||
if cfg.Interval <= 0 {
|
if cfg.Interval <= 0 {
|
||||||
cfg.Interval = 30
|
cfg.Interval = 30
|
||||||
}
|
}
|
||||||
|
if cfg.CPUThreshold <= 0 {
|
||||||
|
cfg.CPUThreshold = 90
|
||||||
|
}
|
||||||
|
if cfg.ReniceConsecutive <= 0 {
|
||||||
|
cfg.ReniceConsecutive = 2
|
||||||
|
}
|
||||||
return &cfg, nil
|
return &cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,9 +298,10 @@ func collectMetrics() (*Metricas, error) {
|
|||||||
type HeartbeatRequest struct {
|
type HeartbeatRequest struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Metricas string `json:"metricas"`
|
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)
|
metJSON, err := json.Marshal(metricas)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -299,6 +310,7 @@ func sendHeartbeat(cfg *Config, metricas *Metricas) error {
|
|||||||
payload := HeartbeatRequest{
|
payload := HeartbeatRequest{
|
||||||
Token: cfg.Token,
|
Token: cfg.Token,
|
||||||
Metricas: string(metJSON),
|
Metricas: string(metJSON),
|
||||||
|
ReniceAction: reniceAction,
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(payload)
|
body, _ := json.Marshal(payload)
|
||||||
|
|
||||||
@@ -350,11 +362,14 @@ func main() {
|
|||||||
log.Fatal("api_url y token son requeridos")
|
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)
|
ticker := time.NewTicker(time.Duration(cfg.Interval) * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
consecAlto := 0 // contador de reportes consecutivos con CPU alta
|
||||||
|
|
||||||
reportar := func() {
|
reportar := func() {
|
||||||
m, err := collectMetrics()
|
m, err := collectMetrics()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -365,7 +380,34 @@ func main() {
|
|||||||
b, _ := json.MarshalIndent(m, "", " ")
|
b, _ := json.MarshalIndent(m, "", " ")
|
||||||
log.Printf("📊 Métricas:\n%s", string(b))
|
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)
|
log.Printf("⚠️ Error enviando heartbeat: %v", err)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("✅ Heartbeat | RAM: %.1f%% | CPU: %.1f%% | TCP: %d | Procs: %d",
|
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")
|
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) {
|
func sendTelegramAdmin(mensaje string) {
|
||||||
configs, err := models.GetAllTelegramConfigs()
|
configs, err := models.GetAllTelegramConfigs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/sujit-baniya/fiber-boilerplate/app"
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
"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.
|
// AgentHeartbeat recibe métricas del agente instalado en un servidor.
|
||||||
@@ -17,7 +18,8 @@ import (
|
|||||||
func AgentHeartbeat(c *fiber.Ctx) error {
|
func AgentHeartbeat(c *fiber.Ctx) error {
|
||||||
type HeartbeatRequest struct {
|
type HeartbeatRequest struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Metricas string `json:"metricas"` // JSON string ya serializado por el agente
|
Metricas string `json:"metricas"`
|
||||||
|
ReniceAction string `json:"renice_action"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var req HeartbeatRequest
|
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})
|
return c.JSON(fiber.Map{"ok": true, "servidor_id": servidor.ID})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user