# 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] 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] 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