- S1: Eliminar 'triggers' duplicado en run (líneas 109/131) - S2: Eliminar cli/commit.rb (truncado, sin uso) - S3: Eliminar cli/inicio.rb y cli/cierre.rb (legacy, jornada.rb los reemplaza) - S4: Mover 4 planes obsoletos de docs/plan/adn/ a docs/_hist/plan/adn/ - S5: Mover 14 backups de planes y 3 docs técnicos a docs/_hist/ - S6: Eliminar manifiesto vacío P2604_proyecto_p2604.md - S7: Corregir referencia a plan obsoleto en run - S8: Dejar de cargar core/validador.rb obsoleto en run - S9: Limpiar progreso duplicado en P2604_mejoras_ADN.md - Agregar Fase 10 al plan P2604 con 10 tareas - Un solo punto de verdad: adn/README.md - Un solo plan: docs/proy/p2604_mejoras_ADN/P2604_mejoras_ADN.md - Registro en bitácora: evento 1073
266 lines
8.1 KiB
Ruby
266 lines
8.1 KiB
Ruby
# 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 y por tamaño.
|
||
|
||
require 'json'
|
||
require 'fileutils'
|
||
require 'date'
|
||
require 'time'
|
||
|
||
module ADN
|
||
class Logger
|
||
NIVELES = %w[DEBUG INFO EXITO ADVERTENCIA ERROR].freeze
|
||
|
||
# Configuración de rotación
|
||
MAX_TAMANO_MB = 10
|
||
MAX_ARCHIVOS = 7
|
||
|
||
require_relative 'colores'
|
||
|
||
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
|
||
|
||
# Nivel mínimo de log (configurable)
|
||
@nivel_minimo = opts[:nivel] || 'DEBUG'
|
||
|
||
FileUtils.mkdir_p(@directorio) unless Dir.exist?(@directorio)
|
||
|
||
@ruta_log = File.join(@directorio, "#{@archivo_base}.log")
|
||
@buffer = []
|
||
|
||
# Verificar rotación al iniciar
|
||
verificar_rotacion
|
||
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
|
||
|
||
# Verifica y ejecuta rotación de logs si es necesario
|
||
def verificar_rotacion
|
||
return unless File.exist?(@ruta_log)
|
||
|
||
tamano_mb = File.size(@ruta_log) / (1024.0 * 1024.0)
|
||
|
||
if tamano_mb > MAX_TAMANO_MB
|
||
rotar_por_tamano
|
||
end
|
||
end
|
||
|
||
# Rota el log por tamaño (archivo muy grande)
|
||
def rotar_por_tamano
|
||
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
|
||
archivo_rotado = "#{@ruta_log}.#{timestamp}"
|
||
|
||
FileUtils.mv(@ruta_log, archivo_rotado)
|
||
limpiar_logs_antiguos
|
||
|
||
puts "#{Color::YELLOW}⚠ Log rotado por tamaño: #{archivo_rotado}#{Color::RESET}"
|
||
end
|
||
|
||
# Limpia logs antiguos (más de MAX_ARCHIVOS)
|
||
def limpiar_logs_antiguos
|
||
patron = File.join(@directorio, '*.log.*')
|
||
archivos = Dir.glob(patron).sort_by { |f| File.mtime(f) }
|
||
|
||
archivos[0...-MAX_ARCHIVOS].each do |archivo|
|
||
File.delete(archivo) if File.exist?(archivo)
|
||
end
|
||
end
|
||
|
||
# Verifica si el nivel está habilitado
|
||
def nivel_habilitado?(nivel)
|
||
idx_actual = NIVELES.index(@nivel_minimo) || 0
|
||
idx_nivel = NIVELES.index(nivel) || 0
|
||
idx_nivel >= idx_actual
|
||
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
|