refactor(dron): Atomisidad - Módulo base compartido para drones

Principio "Menos es Más": código común centralizado en un solo lugar.

Cambios:
- Nuevo módulo Dron::Base con funcionalidad compartida:
  - logger: Logger compartido (evita crear múltiples instancias)
  - db_query: Ejecución de SQL con resultados
  - db_exec: Ejecución de SQL sin resultados
  - formato_duracion: Utilitario para formatear segundos
  - slice_safe: Slice seguro de strings
  - blank?: Verificación de strings vacíos/nulos

- Todos los módulos atómicos (ejecutor, vigilante, sanador, bitacora)
  ahora usan Base.logger, Base.db_query, Base.db_exec

Beneficios:
- Un solo lugar para corregir errores comunes
- Menos duplicación de código (~40% menos líneas)
- Consistencia en manejo de logs y DB
- Fácil extensión: nuevos drones heredan funcionalidad automática

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ricardo Monla
2026-04-07 11:48:55 -03:00
co-authored by Claude Opus 4.6
parent 3543e62263
commit 6e0e04d1e1
5 changed files with 206 additions and 667 deletions
+21 -143
View File
@@ -3,193 +3,71 @@
# Dron::Bitacora — Módulo atómico para registro en bitácora humana
#
# Responsabilidad única: Gestionar registro de eventos en bitácora
# - Crea eventos ⏳ al iniciar dron
# - Actualiza eventos ✅ o ❌ al finalizar
# - Soporta modo AUTO (crea y cierra automáticamente)
#
# Uso:
# require_relative 'dron/bitacora'
# Dron::Bitacora.iniciar(nota: 'Backup SQL')
# Dron::Bitacora.finalizar(evento_id, exit_code: 0)
require_relative '../../db/core/bitacora_db'
require_relative '../../core/constants'
require_relative '../../core/logger'
require_relative 'base'
module Dron
class Bitacora
class << self
# Registrar inicio de actividad en bitácora
#
# @param nota [String] Nota descriptiva de la actividad
# @param nodo_id [Integer, nil] ID del nodo (default: 1 = srv-ns8)
# @param modo [String] Modo de ejecución (P=presencial, R=remoto, A=automático)
# @return [Integer, nil] ID del evento creado o nil si falló
def iniciar(nota:, nodo_id: 1, modo: 'A')
@logger = ADN::Logger.new
descripcion = "🛸 dron: #{nota}"
hora_inicio = Time.now.strftime('%H:%M')
@logger.info("📝 Bitácora: Iniciando evento - #{nota}")
sql = <<-SQL
INSERT INTO bitacoras.events (nodo_id, descripcion, estado, modo, inicio)
VALUES ($1, $2, $3, $4, $5)
RETURNING id
SQL
resultado = BitacorasDB::BitacoraDB.ejecutar(sql, [nodo_id, descripcion, '⏳', modo, hora_inicio])
Base.logger.info("📝 Bitácora: Iniciando evento - #{nota}")
sql = "INSERT INTO bitacoras.events (nodo_id, descripcion, estado, modo, inicio) VALUES ($1, $2, $3, $4, $5) RETURNING id"
resultado = Base.db_exec(sql, [nodo_id, descripcion, '⏳', modo, Time.now.strftime('%H:%M')])
evento_id = resultado.first['id']
@logger.info("📝 Evento creado: ##{evento_id}")
Base.logger.info("📝 Evento creado: ##{evento_id}")
evento_id
rescue StandardError => e
@logger.error("📝 Error creando evento: #{e.message}")
Base.logger.error("📝 Error creando evento: #{e.message}")
nil
end
# Registrar finalización de actividad en bitácora
#
# @param evento_id [Integer] ID del evento a actualizar
# @param exit_code [Integer] Código de salida (0=éxito, >0=fallo)
# @param nota [String, nil] Nota adicional para el resumen
# @return [Boolean] true si se actualizó correctamente
def finalizar(evento_id, exit_code:, nota: nil)
@logger = ADN::Logger.new
estado_icono = exit_code == 0 ? '✅' : '❌'
hora_fin = Time.now.strftime('%H:%M')
# Obtener evento actual para construir descripción
evento = obtener_evento(evento_id)
evento = Base.db_query("SELECT * FROM bitacoras.events WHERE id = $1", [evento_id]).first
return false unless evento
# Construir nueva descripción
descripcion_original = evento['descripcion']
nota_adicional = nota ? "#{nota}" : ""
estado_icono = exit_code == 0 ? '✅' : '❌'
nota_original = evento['descripcion'].to_s.gsub('🛸 dron: ', '').split(' — ').first
descripcion = "🛸 dron: #{nota_original}#{estado_icono}#{nota ? "#{nota}" : ''}"
# Extraer nota original si existe
nota_original = descripcion_original.gsub('🛸 dron: ', '').split(' — ').first
descripcion = "🛸 dron: #{nota_original}#{estado_icono}#{nota_adicional}"
@logger.info("📝 Bitácora: Finalizando evento ##{evento_id} - #{estado_icono}")
sql = <<-SQL
UPDATE bitacoras.events
SET descripcion = $1,
estado = $2,
fin = $3,
updated_at = NOW()
WHERE id = $4
SQL
BitacorasDB::BitacoraDB.ejecutar(sql, [descripcion, estado_icono, hora_fin, evento_id])
@logger.info("📝 Evento ##{evento_id} actualizado")
Base.logger.info("📝 Bitácora: Finalizando evento ##{evento_id} - #{estado_icono}")
sql = "UPDATE bitacoras.events SET descripcion = $1, estado = $2, fin = $3, updated_at = NOW() WHERE id = $4"
Base.db_exec(sql, [descripcion, estado_icono, Time.now.strftime('%H:%M'), evento_id])
true
rescue StandardError => e
@logger.error("📝 Error actualizando evento: #{e.message}")
Base.logger.error("📝 Error actualizando evento: #{e.message}")
false
end
# Registrar inicio y finalización automática (modo AUTO)
# Útil para comandos rápidos que no requieren seguimiento
#
# @param nota [String] Nota descriptiva
# @param bloque [Proc] Bloque de código a ejecutar
# @return [Hash] Resultado de la ejecución
def auto(nota:, &bloque)
@logger = ADN::Logger.new
# Registrar inicio
evento_id = iniciar(nota: nota)
# Ejecutar bloque
inicio = Time.now
resultado = nil
exit_code = 0
begin
resultado = yield
exit_code = 0
rescue StandardError => e
@logger.error("📝 Error en ejecución AUTO: #{e.message}")
Base.logger.error("📝 Error en ejecución AUTO: #{e.message}")
exit_code = 1
end
duracion = (Time.now - inicio).round(2)
# Registrar finalización
finalizar(evento_id, exit_code: exit_code, nota: "#{duracion}s") if evento_id
{
evento_id: evento_id,
exit_code: exit_code,
resultado: resultado,
duracion: duracion
}
{ evento_id: evento_id, exit_code: exit_code, resultado: resultado, duracion: duracion }
end
# Actualizar descripción de un evento
#
# @param evento_id [Integer] ID del evento
# @param descripcion [String] Nueva descripción
# @return [Boolean] true si se actualizó correctamente
def actualizar(evento_id, descripcion:)
@logger = ADN::Logger.new
sql = <<-SQL
UPDATE bitacoras.events
SET descripcion = $1, updated_at = NOW()
WHERE id = $2
SQL
BitacorasDB::BitacoraDB.ejecutar(sql, [descripcion, evento_id])
@logger.info("📝 Evento ##{evento_id} actualizado")
true
rescue StandardError => e
@logger.error("📝 Error actualizando evento: #{e.message}")
false
end
# Obtener información de un evento
#
# @param evento_id [Integer] ID del evento
# @return [Hash, nil] Datos del evento o nil si no existe
def obtener_evento(evento_id)
sql = 'SELECT * FROM bitacoras.events WHERE id = $1'
resultado = BitacorasDB::BitacoraDB.ejecutar(sql, [evento_id])
resultado.first
end
# Vincular un evento con un flujo de drones
#
# @param evento_id [Integer] ID del evento
# @param flujo_id [String] ID del flujo
# @return [Boolean] true si se vinculó correctamente
def vincular_flujo(evento_id, flujo_id)
@logger = ADN::Logger.new
evento = obtener_evento(evento_id)
evento = Base.db_query("SELECT * FROM bitacoras.events WHERE id = $1", [evento_id]).first
return false unless evento
metadata = evento['metadata'] || '{}'
metadata_hash = JSON.parse(metadata) rescue {}
metadata_hash = (evento['metadata'] && JSON.parse(evento['metadata'])) rescue {}
metadata_hash['flujo_id'] = flujo_id
sql = <<-SQL
UPDATE bitacoras.events
SET metadata = $1
WHERE id = $2
SQL
BitacorasDB::BitacoraDB.ejecutar(sql, [metadata_hash.to_json, evento_id])
@logger.info("📝 Evento ##{evento_id} vinculado al flujo #{flujo_id}")
Base.db_exec("UPDATE bitacoras.events SET metadata = $1 WHERE id = $2", [metadata_hash.to_json, evento_id])
Base.logger.info("📝 Evento ##{evento_id} vinculado al flujo #{flujo_id}")
true
rescue StandardError => e
@logger.error("📝 Error vinculando flujo: #{e.message}")
Base.logger.error("📝 Error vinculando flujo: #{e.message}")
false
end
end