Files
dtic-DIIAA/adn/tools/cli/dron/vigilante.rb
T
Ricardo MonlaandClaude Opus 4.6 be3a78c50a fix(dron): Ejecutor funcional + tabla events
Cambios:
- ejecutor.rb: Corregir lectura de output con Open3
- bitacora_db.rb: Agregar métodos crear_evento/actualizar_evento (public)
- dron_db.rb: Corregir conversión de PG::Result a Hash (.first.to_h)
- vigilante.rb: Corregir formato de fecha en dashboard
- migrations/004: Crear tabla bitacoras.events para Bitácora Web

Pruebas:
- dron lanzar --evento AUTO --nota "Test" -- echo "hola" 
- dron flota 
- dron salud 

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 12:34:20 -03:00

151 lines
6.0 KiB
Ruby

# frozen_string_literal: true
# Dron::Vigilante — Módulo atómico para monitoreo de flota
#
# Responsabilidad única: Escanear flota y detectar anomalías
require_relative 'base'
require_relative '../../db/core/dron_db'
module Dron
class Vigilante
ZOMBIE_TIMEOUT = 300
ANOMALY_THRESHOLD = 5
class << self
def logger
@logger ||= ADN::Logger.new
end
def db_query(sql, params = [])
BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, params).to_a }
end
def db_exec(sql, params = [])
BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, params) }
end
def escanear_flota
logger.info("👁️ Dron Vigilante: Escaneando flota...")
resumen = ADN::DB::DronDB.resumen_flota
output = ["\n🛸 Flota de Drones", "═" * 60]
output << " Total: #{resumen[:total]} | 🟢 #{resumen[:running] || 0} | ⏳ #{resumen[:pending] || 0} | ✅ #{resumen[:completed] || 0} | ❌ #{resumen[:failed] || 0} | 🧟 #{resumen[:zombie] || 0}"
output << "═" * 60
puts output.join("\n")
{ 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
def detectar_zombies
zombies = ADN::DB::DronDB.detectar_zombies
return [] if zombies.empty?
logger.warn("👁️ Detectados #{zombies.count} drones zombies:")
zombies.each do |z|
minutos = z['minutos_sin_heartbeat'].to_f.round(1)
logger.warn(" 🧟 #{z['dron_id']} - #{minutos}min sin heartbeat")
ADN::DB::DronDB.marcar_zombie(z['dron_id'])
registrar_avance(z['dron_id'], "Dron zombie (#{minutos}min sin heartbeat)")
end
zombies
end
def salud
logger.info("👁️ Dron Vigilante: Health check...")
diagnosticos = { zombies: [], fallos_recientes: [], anomalias: [], saludable: true }
diagnosticos[:zombies] = detectar_zombies.map { |z| z['dron_id'] }
diagnosticos[:fallos_recientes] = db_query(sql_fallos_recientes)
if diagnosticos[:fallos_recientes].count >= ANOMALY_THRESHOLD
diagnosticos[:anomalias] << "Demasiados fallos: #{diagnosticos[:fallos_recientes].count} en 1h"
diagnosticos[:saludable] = false
end
running_antiguos = db_query(sql_running_antiguos)
unless running_antiguos.empty?
diagnosticos[:anomalias] << "Drones running antiguos: #{running_antiguos.count}"
diagnosticos[:saludable] = false
end
puts "\n🏥 Salud de la Flota"
puts "═" * 60
puts " Estado: #{diagnosticos[:saludable] ? '✅ Saludable' : '🚨 Problemas'}"
puts " Zombies: #{diagnosticos[:zombies].count} | Fallos recientes: #{diagnosticos[:fallos_recientes].count} | Anomalías: #{diagnosticos[:anomalias].count}"
diagnosticos[:anomalias].each { |a| puts " ⚠️ #{a}" } unless diagnosticos[:anomalias].empty?
puts "═" * 60
diagnosticos
end
def consolidar_flujo(flujo_id)
estado = ADN::DB::DronDB.consolidar_estado_flujo(flujo_id)
return nil unless estado
drones = ADN::DB::DronDB.obtener_por_flujo(flujo_id)
actualizar_evento_resumen(flujo_id, estado, drones.count)
logger.info("👁️ Flujo #{flujo_id} consolidado: #{estado} (#{drones.count} drones)")
estado
end
def dashboard
resumen = ADN::DB::DronDB.resumen_flota
activos = db_query(sql_activos)
output = ["\n🛸 Dashboard de Drones", "═" * 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 |d|
inicio = d['started_at'].is_a?(Time) ? d['started_at'].strftime('%H:%M:%S') : d['started_at'].to_s.slice(11, 8)
output << " #{d['dron_id'].slice(0, 24).ljust(25)} | #{d['tipo'].to_s.slice(0, 14).ljust(15)} | #{inicio.ljust(10)} | #{d['cmd'].to_s.slice(0, 30)}"
end
end
output.join("\n")
end
private
def sql_fallos_recientes
"SELECT * FROM bitacoras.dron_logs WHERE estado = 'failed' AND completed_at > NOW() - INTERVAL '60 minutes' ORDER BY completed_at DESC"
end
def sql_running_antiguos
"SELECT * FROM bitacoras.dron_logs WHERE estado = 'running' AND started_at < NOW() - INTERVAL '1 hour' ORDER BY started_at ASC"
end
def sql_activos
"SELECT * FROM bitacoras.dron_logs WHERE estado IN ('running', 'pending') ORDER BY started_at DESC"
end
def registrar_avance(dron_id, descripcion)
db_exec("INSERT INTO bitacoras.dron_avances (dron_id, paso, descripcion, estado) VALUES ($1, -1, $2, 'failed')", [dron_id, descripcion])
end
def actualizar_evento_resumen(flujo_id, estado, count)
estado_icono = case estado when 'completed' then '✅' when 'failed' then '❌' else '⏳' end
descripcion = "🛸 Flujo #{flujo_id}: #{count} drones - #{estado_icono}"
resultado = db_query("SELECT id FROM bitacoras.events WHERE metadata->>'flujo_id' = $1", [flujo_id])
if resultado.empty?
require_relative '../../db/core/bitacora_db'
BitacorasDB::BitacoraDB.crear_evento(nodo_id: 1, descripcion: descripcion, estado: estado_icono, metadata: { flujo_id: flujo_id, drones_count: count }.to_json)
else
BitacorasDB::BitacoraDB.actualizar_evento(resultado.first['id'], descripcion: descripcion, estado: estado_icono)
end
rescue StandardError => e
logger.warn("⚠️ No se pudo actualizar evento resumen: #{e.message}")
end
end
end
end