ip
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -130,3 +132,57 @@ func MarcarNotifsFacturasLeidas(usuarioID uint) error {
|
||||
Where("tipo_usuario = 'portal_user' AND usuario_id = ? AND icono = '🧾'", usuarioID).
|
||||
Update("leida", true).Error
|
||||
}
|
||||
|
||||
// ─── ServidorAlertaUmbral ─────────────────────────────────────────────────────
|
||||
// Configuración global de umbrales para alertas de servidores.
|
||||
// Solo existe un registro (singleton, ID = 1).
|
||||
|
||||
type ServidorAlertaUmbral struct {
|
||||
gorm.Model
|
||||
Activo bool `json:"activo" gorm:"column:activo;default:true"`
|
||||
MinutosSinPing int `json:"minutos_sin_ping" gorm:"column:minutos_sin_ping;default:10"`
|
||||
UmbralCPU int `json:"umbral_cpu" gorm:"column:umbral_cpu;default:90"`
|
||||
UmbralRAM int `json:"umbral_ram" gorm:"column:umbral_ram;default:90"`
|
||||
UmbralDisco int `json:"umbral_disco" gorm:"column:umbral_disco;default:90"`
|
||||
DiasAnteVencimiento int `json:"dias_ante_vencimiento" gorm:"column:dias_ante_vencimiento;default:7"`
|
||||
}
|
||||
|
||||
func (ServidorAlertaUmbral) TableName() string { return "servidor_alerta_umbral" }
|
||||
|
||||
// GetServidorAlertaUmbral obtiene el singleton de umbrales; crea uno con defaults si no existe.
|
||||
func GetServidorAlertaUmbral() *ServidorAlertaUmbral {
|
||||
var cfg ServidorAlertaUmbral
|
||||
db := app.Http.Database.DB
|
||||
if err := db.First(&cfg).Error; err != nil {
|
||||
cfg = ServidorAlertaUmbral{Activo: true, MinutosSinPing: 10, UmbralCPU: 90, UmbralRAM: 90, UmbralDisco: 90, DiasAnteVencimiento: 7}
|
||||
db.Create(&cfg)
|
||||
}
|
||||
return &cfg
|
||||
}
|
||||
|
||||
// SaveServidorAlertaUmbral guarda el singleton.
|
||||
func SaveServidorAlertaUmbral(cfg *ServidorAlertaUmbral) error {
|
||||
db := app.Http.Database.DB
|
||||
var existing ServidorAlertaUmbral
|
||||
if err := db.First(&existing).Error; err != nil {
|
||||
return db.Create(cfg).Error
|
||||
}
|
||||
return db.Model(&existing).Updates(map[string]interface{}{
|
||||
"activo": cfg.Activo,
|
||||
"minutos_sin_ping": cfg.MinutosSinPing,
|
||||
"umbral_cpu": cfg.UmbralCPU,
|
||||
"umbral_ram": cfg.UmbralRAM,
|
||||
"umbral_disco": cfg.UmbralDisco,
|
||||
"dias_ante_vencimiento": cfg.DiasAnteVencimiento,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// YaExisteAlertaServidor evita spam: true si ya se creó una notificación con el
|
||||
// mismo título para este servidor en las últimas `horas` horas.
|
||||
func YaExisteAlertaServidor(titulo string, horas int) bool {
|
||||
var count int64
|
||||
app.Http.Database.DB.Model(&SistemaNotificacion{}).
|
||||
Where("titulo = ? AND created_at > ?", titulo, time.Now().Add(-time.Duration(horas)*time.Hour)).
|
||||
Count(&count)
|
||||
return count > 0
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||
@@ -32,8 +35,14 @@ func IniciarCron() {
|
||||
return
|
||||
}
|
||||
|
||||
// Salud de servidores — cada 5 minutos
|
||||
if _, err := cronScheduler.AddFunc("*/5 * * * *", VerificarSaludServidores); err != nil {
|
||||
log.Printf("[CRON] Error registrando tarea salud_servidores: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
cronScheduler.Start()
|
||||
log.Println("[CRON] Scheduler iniciado — vencimientos próximos 8AM, ya vencidos 9AM, Bold polling cada 15min")
|
||||
log.Println("[CRON] Scheduler iniciado — vencimientos próximos 8AM, ya vencidos 9AM, Bold polling cada 15min, salud servidores cada 5min")
|
||||
}
|
||||
|
||||
// DetenerCron para graceful shutdown
|
||||
@@ -208,3 +217,100 @@ func VerificarPagosBoldPendientes() {
|
||||
|
||||
log.Println("[BOLD-POLL] Verificación completada")
|
||||
}
|
||||
|
||||
// ─── Salud de Servidores ─────────────────────────────────────────────────────
|
||||
|
||||
// VerificarSaludServidores corre cada 5 min y genera notificaciones del sistema
|
||||
// cuando detecta: agente caído, recurso al límite o VPS próximo a vencer.
|
||||
func VerificarSaludServidores() {
|
||||
cfg := models.GetServidorAlertaUmbral()
|
||||
if !cfg.Activo {
|
||||
return
|
||||
}
|
||||
|
||||
servidores, _, err := models.GetAllServidores(1000, 0, "")
|
||||
if err != nil {
|
||||
log.Printf("[CRON-SRV] Error obteniendo servidores: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, srv := range servidores {
|
||||
// 1. Agente caído (sin ping en X minutos)
|
||||
if srv.AgentToken != "" && srv.AgentLastSeen != nil {
|
||||
minutos := time.Since(*srv.AgentLastSeen).Minutes()
|
||||
if minutos > float64(cfg.MinutosSinPing) {
|
||||
titulo := fmt.Sprintf("🔴 Sin señal: %s", srv.Nombre)
|
||||
if !models.YaExisteAlertaServidor(titulo, 1) {
|
||||
crearNotifServidor(titulo,
|
||||
fmt.Sprintf("El agente no reporta hace %.0f minutos (umbral: %d min)", minutos, cfg.MinutosSinPing),
|
||||
"servidor_caido")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Recursos (CPU / RAM / Disco) al límite
|
||||
if srv.MetricasJson != "" {
|
||||
var m struct {
|
||||
CPU struct{ Porcentaje float64 `json:"porcentaje"` } `json:"cpu"`
|
||||
RAM struct{ Porcentaje float64 `json:"porcentaje"` } `json:"ram"`
|
||||
Disco struct{ Porcentaje float64 `json:"porcentaje"` } `json:"disco"`
|
||||
}
|
||||
if jsonErr := json.Unmarshal([]byte(srv.MetricasJson), &m); jsonErr == nil {
|
||||
checkRecurso(srv.Nombre, "CPU", m.CPU.Porcentaje, cfg.UmbralCPU)
|
||||
checkRecurso(srv.Nombre, "RAM", m.RAM.Porcentaje, cfg.UmbralRAM)
|
||||
checkRecurso(srv.Nombre, "Disco", m.Disco.Porcentaje, cfg.UmbralDisco)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Vencimiento próximo
|
||||
if srv.Vencimiento != "" && cfg.DiasAnteVencimiento > 0 {
|
||||
vence, parseErr := time.Parse("2006-01-02", srv.Vencimiento)
|
||||
if parseErr == nil {
|
||||
dias := int(time.Until(vence).Hours() / 24)
|
||||
if dias >= 0 && dias <= cfg.DiasAnteVencimiento {
|
||||
titulo := fmt.Sprintf("📅 %s vence en %s", srv.Nombre, strconv.Itoa(dias)+" días")
|
||||
if dias == 0 {
|
||||
titulo = fmt.Sprintf("📅 %s vence HOY", srv.Nombre)
|
||||
}
|
||||
if !models.YaExisteAlertaServidor(titulo, 12) {
|
||||
crearNotifServidor(titulo,
|
||||
fmt.Sprintf("Fecha de vencimiento: %s. Renueva a tiempo.", srv.Vencimiento),
|
||||
"servidor_vence_pronto")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkRecurso(nombre, recurso string, valor float64, umbral int) {
|
||||
if valor < float64(umbral) {
|
||||
return
|
||||
}
|
||||
titulo := fmt.Sprintf("⚠️ %s alto en %s (%.0f%%)", recurso, nombre, valor)
|
||||
if !models.YaExisteAlertaServidor(titulo, 1) {
|
||||
crearNotifServidor(titulo,
|
||||
fmt.Sprintf("%s al %.0f%% (umbral: %d%%)", recurso, valor, umbral),
|
||||
"servidor_recurso_alto")
|
||||
}
|
||||
}
|
||||
|
||||
func crearNotifServidor(titulo, cuerpo, evento string) {
|
||||
// Verificar si la notificación del sistema está habilitada para este evento
|
||||
evtCfg := models.GetNotifConfig(evento, "admin")
|
||||
// Por defecto, si no hay config, crear notificación de sistema igualmente
|
||||
if evtCfg == nil || evtCfg.CanalSistema {
|
||||
_ = models.CreateSistemaNotif(&models.SistemaNotificacion{
|
||||
TipoUsuario: "admin",
|
||||
Titulo: titulo,
|
||||
Cuerpo: cuerpo,
|
||||
Url: "/app/servidor-dashboard",
|
||||
Icono: "🖥️",
|
||||
})
|
||||
}
|
||||
// Telegram (si está configurado para este evento)
|
||||
if evtCfg != nil && evtCfg.CanalTelegram {
|
||||
msg := fmt.Sprintf("<b>%s</b>\n%s", titulo, cuerpo)
|
||||
sendTelegramAdmin(msg)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user