feat(dron): Migración a AtomicDrones con arquitectura DB-First
Implementación del sistema AtomicDrones basado en "Menos es Más": - Refactorización de dron.rb a dispatcher + módulos atómicos - Nuevos módulos: ejecutor, vigilante, sanador, bitacora - DronDB para acceso a datos en PostgreSQL - Tablas: dron_logs, dron_avances, dron_metricas - Views: vw_dron_estado, vw_dron_zombies, vw_dron_metricas_semanal - Health check con detección de zombies (heartbeat > 5min) - Dashboard compacto de la flota - Saneamiento con reintentos (backoff exponencial) y limpieza Comandos soportados: - dron lanzar: Ejecutar comando como dron - dron flota: Dashboard de la flota - dron salud: Health check activo - dron estado <ID>: Estado detallado - dron sanear: Saneamiento completo - dron limpiar: Limpieza de antiguos - dron reintentar: Re-intentar fallidos Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
b5e89144e3
commit
3543e62263
@@ -0,0 +1,286 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Dron::Vigilante — Módulo atómico para monitoreo de flota
|
||||
#
|
||||
# Responsabilidad única: Escanear flota y detectar anomalías
|
||||
# - Lee dron_logs para obtener estado de la flota
|
||||
# - Detecta zombies (heartbeat > 5min)
|
||||
# - Registra avances en dron_avances
|
||||
# - Consolida resumen en events (visible en Bitácora Web)
|
||||
# - Alerta sobre anomalías
|
||||
#
|
||||
# Uso:
|
||||
# require_relative 'dron/vigilante'
|
||||
# Dron::Vigilante.escanear_flota
|
||||
|
||||
require_relative '../../db/core/dron_db'
|
||||
require_relative '../../core/constants'
|
||||
require_relative '../../core/logger'
|
||||
|
||||
module Dron
|
||||
class Vigilante
|
||||
# Tiempo máximo sin heartbeat antes de considerar zombie (5 minutos)
|
||||
ZOMBIE_TIMEOUT = 300
|
||||
|
||||
# Umbral para alerta de anomalías (>5 fallos en 1 hora)
|
||||
ANOMALY_THRESHOLD = 5
|
||||
|
||||
class << self
|
||||
# Escanear toda la flota y reportar estado
|
||||
#
|
||||
# @return [Hash] Resumen de la flota
|
||||
def escanear_flota
|
||||
@logger = ADN::Logger.new
|
||||
@logger.info("👁️ Dron Vigilante: Escaneando flota...")
|
||||
|
||||
resumen = ADN::DB::DronDB.resumen_flota
|
||||
|
||||
# Mostrar resumen
|
||||
puts "\n🛸 Flota de Drones"
|
||||
puts "═" * 60
|
||||
puts " Total: #{resumen[:total]}"
|
||||
puts " 🟢 Running: #{resumen[:running] || 0}"
|
||||
puts " ⏳ Pending: #{resumen[:pending] || 0}"
|
||||
puts " ✅ Completed: #{resumen[:completed] || 0}"
|
||||
puts " ❌ Failed: #{resumen[:failed] || 0}"
|
||||
puts " 🧟 Zombies: #{resumen[:zombie] || 0}"
|
||||
puts "═" * 60
|
||||
|
||||
{
|
||||
exitoso: true,
|
||||
resumen: resumen,
|
||||
timestamp: Time.now
|
||||
}
|
||||
rescue StandardError => e
|
||||
@logger.error("👁️ Error escaneando flota: #{e.message}")
|
||||
{
|
||||
exitoso: false,
|
||||
error: e.message,
|
||||
timestamp: Time.now
|
||||
}
|
||||
end
|
||||
|
||||
# Detectar drones zombies (heartbeat > 5min)
|
||||
#
|
||||
# @return [Array<Hash>] Lista de drones zombies detectados
|
||||
def detectar_zombies
|
||||
@logger = ADN::Logger.new
|
||||
zombies = ADN::DB::DronDB.detectar_zombies
|
||||
|
||||
if zombies.empty?
|
||||
@logger.info("👁️ No hay drones zombies")
|
||||
return []
|
||||
end
|
||||
|
||||
@logger.warn("👁️ Detectados #{zombies.count} drones zombies:")
|
||||
|
||||
zombies.each do |zombie|
|
||||
minutos = zombie['minutos_sin_heartbeat'].to_f.round(1)
|
||||
@logger.warn(" 🧟 #{zombie['dron_id']} - #{minutos}min sin heartbeat")
|
||||
@logger.warn(" Tipo: #{zombie['tipo']}")
|
||||
@logger.warn(" Cmd: #{zombie['cmd']&.slice(0, 50)}...")
|
||||
|
||||
# Marcar como zombie en DB
|
||||
ADN::DB::DronDB.marcar_zombie(zombie['dron_id'])
|
||||
|
||||
# Registrar avance del incidente
|
||||
registrar_avance_incidente(zombie['dron_id'], "Dron detectado como zombie (#{minutos}min sin heartbeat)")
|
||||
end
|
||||
|
||||
zombies
|
||||
end
|
||||
|
||||
# Verificar salud de la flota (health check activo)
|
||||
#
|
||||
# @return [Hash] Diagnóstico de salud
|
||||
def salud
|
||||
@logger = ADN::Logger.new
|
||||
@logger.info("👁️ Dron Vigilante: Health check...")
|
||||
|
||||
diagnosticos = {
|
||||
zombies: [],
|
||||
fallos_recientes: [],
|
||||
anomalias: [],
|
||||
saludable: true
|
||||
}
|
||||
|
||||
# 1. Detectar zombies
|
||||
zombies = detectar_zombies
|
||||
diagnosticos[:zombies] = zombies.map { |z| z['dron_id'] }
|
||||
|
||||
# 2. Verificar fallos recientes (últimos 60 min)
|
||||
fallos_recientes = verificar_fallos_recientes
|
||||
diagnosticos[:fallos_recientes] = fallos_recientes
|
||||
|
||||
# 3. Detectar anomalías (>5 fallos en 1h)
|
||||
if fallos_recientes.count >= ANOMALY_THRESHOLD
|
||||
diagnosticos[:anomalias] << "Demasiados fallos en la última hora: #{fallos_recientes.count}"
|
||||
diagnosticos[:saludable] = false
|
||||
@logger.error("🚨 ANOMALÍA DETECTADA: #{diagnosticos[:anomalias].first}")
|
||||
end
|
||||
|
||||
# 4. Verificar drones running muy antiguos (>1 hora)
|
||||
running_antiguos = verificar_running_antiguos
|
||||
unless running_antiguos.empty?
|
||||
diagnosticos[:anomalias] << "Drones running demasiado antiguos: #{running_antiguos.count}"
|
||||
diagnosticos[:saludable] = false
|
||||
end
|
||||
|
||||
# Resumen
|
||||
puts "\n🏥 Salud de la Flota"
|
||||
puts "═" * 60
|
||||
puts " Estado: #{diagnosticos[:saludable] ? '✅ Saludable' : '🚨 Problemas detectados'}"
|
||||
puts " Zombies: #{diagnosticos[:zombies].count}"
|
||||
puts " Fallos recientes: #{diagnosticos[:fallos_recientes].count}"
|
||||
puts " Anomalías: #{diagnosticos[:anomalias].count}"
|
||||
unless diagnosticos[:anomalias].empty?
|
||||
puts "\n Detalle de anomalías:"
|
||||
diagnosticos[:anomalias].each { |a| puts " ⚠️ #{a}" }
|
||||
end
|
||||
puts "═" * 60
|
||||
|
||||
diagnosticos
|
||||
end
|
||||
|
||||
# Consolidar estado de un flujo y actualizar evento resumen
|
||||
#
|
||||
# @param flujo_id [String] ID del flujo a consolidar
|
||||
# @return [String, nil] Estado consolidado
|
||||
def consolidar_flujo(flujo_id)
|
||||
@logger = ADN::Logger.new
|
||||
|
||||
estado = ADN::DB::DronDB.consolidar_estado_flujo(flujo_id)
|
||||
return nil unless estado
|
||||
|
||||
drones = ADN::DB::DronDB.obtener_por_flujo(flujo_id)
|
||||
count = drones.count
|
||||
|
||||
# Actualizar evento resumen en bitácora (visible en web)
|
||||
actualizar_evento_resumen(flujo_id, estado, count)
|
||||
|
||||
@logger.info("👁️ Flujo #{flujo_id} consolidado: #{estado} (#{count} drones)")
|
||||
|
||||
estado
|
||||
end
|
||||
|
||||
# Obtener dashboard de la flota (versión compacta)
|
||||
#
|
||||
# @return [String] Dashboard formateado
|
||||
def dashboard
|
||||
resumen = ADN::DB::DronDB.resumen_flota
|
||||
activos = ADN::DB::DronDB.obtener_activos
|
||||
|
||||
output = []
|
||||
output << "\n🛸 Dashboard de Drones"
|
||||
output << "═" * 70
|
||||
output << " Total: #{resumen[:total]} | 🟢 #{resumen[:running] || 0} | ⏳ #{resumen[:pending] || 0} | ✅ #{resumen[:completed] || 0} | ❌ #{resumen[:failed] || 0} | 🧟 #{resumen[:zombie] || 0}"
|
||||
output << "═" * 70
|
||||
|
||||
unless activos.empty?
|
||||
output << "\n Activos:"
|
||||
output << " #{"ID".ljust(25)} | #{"Tipo".ljust(15)} | #{"Inicio".ljust(10)} | Cmd"
|
||||
output << " " + "─" * 70
|
||||
|
||||
activos.each do |dron|
|
||||
id_corto = dron['dron_id'].slice(0, 24)
|
||||
tipo = dron['tipo'].to_s.slice(0, 14)
|
||||
inicio = dron['started_at'].strftime('%H:%M:%S')
|
||||
cmd = dron['cmd']&.slice(0, 30) || ''
|
||||
output << " #{id_corto.ljust(25)} | #{tipo.ljust(15)} | #{inicio.ljust(10)} | #{cmd}"
|
||||
end
|
||||
end
|
||||
|
||||
output.join("\n")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Verificar fallos en la última hora
|
||||
#
|
||||
# @return [Array<Hash>] Lista de drones fallidos recientemente
|
||||
def verificar_fallos_recientes
|
||||
sql = <<-SQL
|
||||
SELECT * FROM bitacoras.dron_logs
|
||||
WHERE estado = 'failed'
|
||||
AND completed_at > NOW() - INTERVAL '60 minutes'
|
||||
ORDER BY completed_at DESC
|
||||
SQL
|
||||
|
||||
require_relative '../../db/core/bitacora_db'
|
||||
BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql).to_a }
|
||||
end
|
||||
|
||||
# Verificar drones running demasiado antiguos (>1 hora)
|
||||
#
|
||||
# @return [Array<Hash>] Lista de drones antiguos
|
||||
def verificar_running_antiguos
|
||||
sql = <<-SQL
|
||||
SELECT * FROM bitacoras.dron_logs
|
||||
WHERE estado = 'running'
|
||||
AND started_at < NOW() - INTERVAL '1 hour'
|
||||
ORDER BY started_at ASC
|
||||
SQL
|
||||
|
||||
require_relative '../../db/core/bitacora_db'
|
||||
BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql).to_a }
|
||||
end
|
||||
|
||||
# Registrar avance de incidente en dron_avances
|
||||
#
|
||||
# @param dron_id [String] ID del dron
|
||||
# @param descripcion [String] Descripción del incidente
|
||||
def registrar_avance_incidente(dron_id, descripcion)
|
||||
require_relative '../../db/core/bitacora_db'
|
||||
|
||||
sql = <<-SQL
|
||||
INSERT INTO bitacoras.dron_avances (dron_id, paso, descripcion, estado)
|
||||
VALUES ($1, -1, $2, 'failed')
|
||||
RETURNING *
|
||||
SQL
|
||||
|
||||
BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [dron_id, descripcion]).to_a }
|
||||
end
|
||||
|
||||
# Actualizar evento resumen en bitácora (visible en web)
|
||||
#
|
||||
# @param flujo_id [String] ID del flujo
|
||||
# @param estado [String] Estado consolidado
|
||||
# @param count [Integer] Cantidad de drones
|
||||
def actualizar_evento_resumen(flujo_id, estado, count)
|
||||
require_relative '../../db/core/bitacora_db'
|
||||
|
||||
estado_icono = case estado
|
||||
when 'completed' then '✅'
|
||||
when 'failed' then '❌'
|
||||
else '⏳'
|
||||
end
|
||||
|
||||
descripcion = "🛸 Flujo #{flujo_id}: #{count} drones - #{estado_icono}"
|
||||
|
||||
# Buscar evento existente por flujo_id en metadata
|
||||
sql = <<-SQL
|
||||
SELECT id FROM bitacoras.events
|
||||
WHERE metadata->>'flujo_id' = $1
|
||||
SQL
|
||||
|
||||
resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [flujo_id]).to_a }
|
||||
|
||||
if resultado.empty?
|
||||
# Crear nuevo evento resumen
|
||||
BitacorasDB::BitacoraDB.crear_evento(
|
||||
nodo_id: 1,
|
||||
descripcion: descripcion,
|
||||
estado: estado_icono,
|
||||
metadata: { flujo_id: flujo_id, drones_count: count }.to_json
|
||||
)
|
||||
else
|
||||
# Actualizar evento existente
|
||||
evento_id = resultado.first['id']
|
||||
BitacorasDB::BitacoraDB.actualizar_evento(evento_id, descripcion: descripcion, estado: estado_icono)
|
||||
end
|
||||
rescue StandardError => e
|
||||
@logger.warn("⚠️ No se pudo actualizar evento resumen: #{e.message}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user