Files
Ricardo Monla cfd0e8452a [ADN] Fase 10: Saneamiento y Consolidación - Un solo punto de verdad
- 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
2026-03-15 14:33:32 -03:00

266 lines
8.1 KiB
Ruby
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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