Refactor(ADN): Migración de tools/adn a adn/tools en busca de la Armonía Integral del DIIAA

This commit is contained in:
Ricardo Monla
2026-03-10 22:25:01 -03:00
parent 174ef7d53c
commit 78006032eb
69 changed files with 432 additions and 671 deletions
+228
View File
@@ -0,0 +1,228 @@
# frozen_string_literal: true
# adn/tools/logger.rb — Logger estructurado JSON para el sistema ADN
# ====================================================================
# Este módulo proporciona logging estructurado en JSON para todas las
# herramientas del sistema ADN. Los logs se almacenan en `logs/adn/`
# con rotación diaria automática.
require 'json'
require 'fileutils'
require 'date'
require 'time'
module ADN
class Logger
# Niveles de log soportados
NIVELES = %w[DEBUG INFO EXITO ADVERTENCIA ERROR].freeze
require_relative 'colores'
# Inicializa el logger
#
# @param opts [Hash] Opciones de configuración
# @option opts [String] :directorio Directorio para logs (default: logs/adn/)
# @option opts [Boolean] :terminal Mostrar logs en terminal (default: true)
# @option opts [Boolean] :colores Usar colores en terminal (default: true)
# @option opts [String] :archivo Nombre base del archivo de log (default: fecha actual)
def initialize(opts = {})
@directorio = opts[:directorio] || File.join(File.expand_path('../../..', __dir__), 'logs', 'adn')
@mostrar_terminal = opts.fetch(:terminal, true)
@usar_colores = opts.fetch(:colores, true)
@archivo_base = opts[:archivo] || Date.today.iso8601
# Crear directorio si no existe
FileUtils.mkdir_p(@directorio) unless Dir.exist?(@directorio)
# Ruta completa del archivo de log
@ruta_log = File.join(@directorio, "#{@archivo_base}.log")
# Buffer para logs (útil para pruebas o modo batch)
@buffer = []
end
# Registra un mensaje de nivel DEBUG
#
# @param mensaje [String] Mensaje descriptivo
# @param datos [Hash] Datos adicionales estructurados
# @param opts [Hash] Opciones adicionales
# @option opts [Boolean] :silencioso No mostrar en terminal
def debug(mensaje, datos = {}, opts = {})
escribir('DEBUG', mensaje, datos, opts)
mostrar_terminal('🐛', mensaje, Color::DIM, opts) if debug_activado?
end
# Registra un mensaje de nivel INFO
#
# @param mensaje [String] Mensaje descriptivo
# @param datos [Hash] Datos adicionales estructurados
# @param opts [Hash] Opciones adicionales
# @option opts [Boolean] :silencioso No mostrar en terminal
def info(mensaje, datos = {}, opts = {})
escribir('INFO', mensaje, datos, opts)
mostrar_terminal('', mensaje, Color::CYAN, opts)
end
# Registra un mensaje de nivel ÉXITO
#
# @param mensaje [String] Mensaje descriptivo
# @param datos [Hash] Datos adicionales estructurados
# @param opts [Hash] Opciones adicionales
# @option opts [Boolean] :silencioso No mostrar en terminal
def exito(mensaje, datos = {}, opts = {})
escribir('EXITO', mensaje, datos, opts)
mostrar_terminal('✓', mensaje, Color::GREEN, opts)
end
# Registra un mensaje de nivel ADVERTENCIA
#
# @param mensaje [String] Mensaje descriptivo
# @param datos [Hash] Datos adicionales estructurados
# @param opts [Hash] Opciones adicionales
# @option opts [Boolean] :silencioso No mostrar en terminal
def advertencia(mensaje, datos = {}, opts = {})
escribir('ADVERTENCIA', mensaje, datos, opts)
mostrar_terminal('⚠', mensaje, Color::YELLOW, opts)
end
# Alias inglés para advertencia
#
# @param mensaje [String] Mensaje descriptivo
# @param datos [Hash] Datos adicionales estructurados
# @param opts [Hash] Opciones adicionales
# @option opts [Boolean] :silencioso No mostrar en terminal
def warn(mensaje, datos = {}, opts = {})
advertencia(mensaje, datos, opts)
end
# Registra un mensaje de nivel ERROR
#
# @param mensaje [String] Mensaje descriptivo
# @param datos [Hash] Datos adicionales estructurados
# @param opts [Hash] Opciones adicionales
# @option opts [Boolean] :silencioso No mostrar en terminal
def error(mensaje, datos = {}, opts = {})
escribir('ERROR', mensaje, datos, opts)
mostrar_terminal('✗', mensaje, Color::RED, opts)
end
# Muestra una sugerencia de comandos ADN a utilizar
#
# @param comandos [String, Array<String>] Comando o comandos sugeridos
def sugerencia(comandos)
comandos = [comandos] unless comandos.is_a?(Array)
puts ""
puts " 💡 #{Color::BOLD}Sugerencia:#{Color::RESET} Podés solucionar esto usando las herramientas ADN:"
comandos.each do |cmd|
puts " #{Color::CYAN}#{cmd}#{Color::RESET}"
end
puts ""
end
# Obtiene los logs almacenados en buffer
#
# @return [Array<Hash>] Logs en buffer
def buffer
@buffer.dup
end
# Limpia el buffer interno
def limpiar_buffer
@buffer.clear
end
# Cierra cualquier recurso del logger (archivos abiertos, etc.)
def cerrar
# Por ahora no hay recursos persistentes abiertos
true
end
# Verifica si el debug está activado (variable de entorno ADN_DEBUG)
#
# @return [Boolean]
def debug_activado?
ENV['ADN_DEBUG'] == '1' || ENV['ADN_DEBUG'] == 'true'
end
# Ruta actual del archivo de log
#
# @return [String]
def ruta_log
@ruta_log
end
private
# Escribe una entrada de log en formato JSON
#
# @param nivel [String] Nivel del log
# @param mensaje [String] Mensaje descriptivo
# @param datos [Hash] Datos adicionales estructurados
# @param opts [Hash] Opciones adicionales
def escribir(nivel, mensaje, datos, opts)
entrada = {
timestamp: Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S%z'),
nivel: nivel,
mensaje: mensaje,
datos: datos.reject { |k, _| k == :silencioso }, # Excluir opciones internas
herramienta: opts[:herramienta] || determinar_herramienta_llamante
}
# Agregar al buffer
@buffer << entrada.dup
# Escribir al archivo
File.open(@ruta_log, 'a') do |archivo|
archivo.puts(entrada.to_json)
end
end
# Muestra el mensaje en terminal (si está habilitado)
#
# @param icono [String] Icono para representar el nivel
# @param mensaje [String] Mensaje a mostrar
# @param color [String] Código ANSI de color
# @param opts [Hash] Opciones adicionales
def mostrar_terminal(icono, mensaje, color, opts)
return unless @mostrar_terminal && !opts[:silencioso]
if @usar_colores
puts "#{color}#{icono} #{mensaje}#{Color::RESET}"
else
puts "#{icono} #{mensaje}"
end
end
# Intenta determinar qué herramienta llamó al logger
#
# @return [String] Nombre de la herramienta
def determinar_herramienta_llamante
# Obtener el backtrace y buscar llamadas desde adn/tools/
caller_locations.each do |loc|
ruta = loc.absolute_path || loc.path
if ruta.include?('adn/tools')
# Extraer nombre del archivo sin extensión
nombre = File.basename(ruta, '.*')
return "adn.#{nombre}" unless nombre.empty?
end
end
'adn.desconocido'
end
end
# Logger global preconfigurado para uso rápido
#
# @return [ADN::Logger]
def self.logger
@logger ||= Logger.new
end
# Configura el logger global
#
# @param opts [Hash] Opciones de configuración
# @return [ADN::Logger]
def self.configurar_logger(opts = {})
@logger = Logger.new(opts)
end
end