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>
151 lines
6.0 KiB
Ruby
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
|