feat(url-monitor): agregar monitoreo de disponibilidad de URLs desde servidor
- Modelos UrlMonitor y UrlMonitorLog con purga automática a 7 días - Cron cada minuto ejecuta chequeos HEAD→GET con fallback, alerta Telegram en cambio de estado - CRUD completo + chequeo manual + historial de logs - Migración automática de tablas Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
569d8b0f28
commit
5f92934fba
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -55,11 +56,20 @@ func IniciarCron() {
|
||||
} else {
|
||||
log.Println("[CRON] Historial de métricas purgado (>7 días)")
|
||||
}
|
||||
if err := models.PurgarUrlMonitorLogs(7); err != nil {
|
||||
log.Printf("[CRON] Error purgando logs URL monitor: %v", err)
|
||||
}
|
||||
}); err != nil {
|
||||
log.Printf("[CRON] Error registrando tarea purga_metricas: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Monitor de URLs — cada minuto
|
||||
if _, err := cronScheduler.AddFunc("* * * * *", VerificarUrlMonitors); err != nil {
|
||||
log.Printf("[CRON] Error registrando tarea url_monitor: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
cronScheduler.Start()
|
||||
log.Println("[CRON] Scheduler iniciado — vencimientos próximos 8AM, ya vencidos 9AM, Bold polling cada 15min, salud servidores cada 5min")
|
||||
}
|
||||
@@ -362,3 +372,104 @@ func crearNotifServidor(titulo, cuerpo, evento string) {
|
||||
sendTelegramAdmin(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Monitor de URLs ──────────────────────────────────────────────────────────
|
||||
|
||||
func VerificarUrlMonitors() {
|
||||
monitors, err := models.GetActiveUrlMonitors()
|
||||
if err != nil || len(monitors) == 0 {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
for _, m := range monitors {
|
||||
m := m
|
||||
if m.UltimoCheckAt != nil {
|
||||
if now.Sub(*m.UltimoCheckAt).Minutes() < float64(m.IntervaloMin) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
go ejecutarChequeoURL(m)
|
||||
}
|
||||
}
|
||||
|
||||
func EjecutarChequeoURLPublic(m models.UrlMonitor) { ejecutarChequeoURL(m) }
|
||||
|
||||
func ejecutarChequeoURL(m models.UrlMonitor) {
|
||||
statusCode, latMs, err := doHttpCheck(m.URL, m.TimeoutSeg)
|
||||
ok := err == nil && statusCode >= 200 && statusCode < 400
|
||||
errStr := ""
|
||||
if err != nil {
|
||||
errStr = err.Error()
|
||||
}
|
||||
|
||||
anteriorOk := m.UltimoOk == nil || *m.UltimoOk
|
||||
|
||||
models.UpdateUrlMonitorStatus(m.ID, statusCode, latMs, ok)
|
||||
models.InsertUrlMonitorLog(m.ID, statusCode, latMs, ok, errStr)
|
||||
|
||||
if m.UltimoOk == nil {
|
||||
return // primer chequeo, no alertar
|
||||
}
|
||||
if !ok && anteriorOk {
|
||||
detalle := errStr
|
||||
if detalle == "" {
|
||||
detalle = fmt.Sprintf("HTTP %d", statusCode)
|
||||
}
|
||||
msg := fmt.Sprintf("🔴 <b>Sitio caído</b>\n<b>%s</b>\n%s\n\nError: %s",
|
||||
escapeTelegramHTML(m.Nombre),
|
||||
escapeTelegramHTML(m.URL),
|
||||
escapeTelegramHTML(detalle))
|
||||
sendTelegramAdmin(msg)
|
||||
log.Printf("[URL-MON] DOWN %s — %s", m.Nombre, detalle)
|
||||
} else if ok && !anteriorOk {
|
||||
msg := fmt.Sprintf("✅ <b>Sitio recuperado</b>\n<b>%s</b>\n%s\nLatencia: <b>%dms</b>",
|
||||
escapeTelegramHTML(m.Nombre),
|
||||
escapeTelegramHTML(m.URL),
|
||||
latMs)
|
||||
sendTelegramAdmin(msg)
|
||||
log.Printf("[URL-MON] UP %s — %dms", m.Nombre, latMs)
|
||||
}
|
||||
}
|
||||
|
||||
func doHttpCheck(url string, timeoutSeg int) (int, int64, error) {
|
||||
if timeoutSeg <= 0 {
|
||||
timeoutSeg = 10
|
||||
}
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(timeoutSeg) * time.Second,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= 5 {
|
||||
return fmt.Errorf("demasiadas redirecciones")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
req, err := http.NewRequest(http.MethodHead, url, nil)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "usite-monitor/1.0")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
latMs := time.Since(start).Milliseconds()
|
||||
if err != nil || (resp != nil && resp.StatusCode == http.StatusMethodNotAllowed) {
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
// Reintentar con GET
|
||||
start = time.Now()
|
||||
req2, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
req2.Header.Set("User-Agent", "usite-monitor/1.0")
|
||||
resp2, err2 := client.Do(req2)
|
||||
latMs = time.Since(start).Milliseconds()
|
||||
if err2 != nil {
|
||||
return 0, latMs, err2
|
||||
}
|
||||
resp2.Body.Close()
|
||||
return resp2.StatusCode, latMs, nil
|
||||
}
|
||||
resp.Body.Close()
|
||||
return resp.StatusCode, latMs, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user