template sintax rails escape comment ruby logging class-design

sintax - ruby on rails haml



Ruby: comparte la instancia del registrador entre el módulo/clases (9)

¿Qué hay de envolver el registrador en un singleton luego podría acceder a él utilizando MyLogger.instance

Trabajando en un pequeño script de Ruby que sale a la web y rastrea varios servicios. Tengo un módulo con varias clases dentro:

module Crawler class Runner class Options class Engine end

Quiero compartir un registrador entre todos aquellos de esas clases. Normalmente solo pondría esto en una constante en el módulo y lo referenciaría así:

Crawler::LOGGER.info("Hello, world")

El problema es que no puedo crear mi instancia de registro hasta que sepa a dónde va la salida. Inicia el rastreador a través de la línea de comandos y en ese punto puede indicarle que desea ejecutar en desarrollo (el resultado del registro va a STDOUT) o producción (el resultado del registro va a un archivo, crawler.log):

crawler --environment=production

Tengo una clase Options que analiza las opciones pasadas a través de la línea de comando. Solo en ese punto sé cómo crear una instancia del registrador con la ubicación de salida correcta.

Entonces, mi pregunta es: ¿cómo / dónde colocar mi objeto logger para que todas mis clases tengan acceso a él?

Podría pasar mi instancia de registrador a cada llamada new() para cada instancia de clase que cree, pero sé que tiene que haber una forma mejor, Ruby para hacerlo. Me estoy imaginando una variable de clase extraña en el módulo que compartió con la class << self o alguna otra magia. :)

Un poco más de detalle: Runner comienza todo pasando las opciones de la línea de comando a la clase Options y recupera un objeto con un par de variables de instancia:

module Crawler class Runner def initialize(argv) @options = Options.new(argv) # feels like logger initialization should go here # @options.log_output => STDOUT or string (log file name) # @options.log_level => Logger::DEBUG or Logger::INFO @engine = Engine.new() end def run @engine.go end end end runner = Runner.new(ARGV) runner.run

Necesito el código en Engine para poder acceder al objeto logger (junto con un par de clases más que se inicializan dentro de Engine ). ¡Ayuda!

Todo esto podría evitarse si pudiera cambiar dinámicamente la ubicación de salida de un registrador ya instanciado (similar a cómo cambia el nivel de registro). Lo instanciaría en STDOUT y luego cambiaría a un archivo si estoy en producción. Vi una sugerencia en algún lugar sobre el cambio de la variable global $ stdout de Ruby, que redirigiría la salida a otro lugar que STDOUT, pero esto parece bastante hacky.

¡Gracias!


Aunque es una vieja pregunta, pensé que valía la pena documentar un enfoque diferente.

Basándome en la respuesta de Jacob, sugeriría un módulo que puede agregar cuando sea necesario.

Mi versión es esta:

# saved into lib/my_log.rb require ''logger'' module MyLog def self.logger if @logger.nil? @logger = Logger.new( STDERR) @logger.datetime_format = "%H:%M:%S " end @logger end def self.logger=( logger) @logger = logger end levels = %w(debug info warn error fatal) levels.each do |level| define_method( "#{level.to_sym}") do |msg| self.logger.send( level, msg) end end end include MyLog

Tengo esto guardado en una biblioteca de prácticos módulos, y lo usaría así:

#! /usr/bin/env ruby # require_relative ''../lib/my_log.rb'' MyLog.debug "hi" # => D, [19:19:32 #31112] DEBUG -- : hi MyLog.warn "ho" # => W, [19:20:14 #31112] WARN -- : ho MyLog.logger.level = Logger::INFO MyLog.logger = Logger.new( ''logfile.log'') MyLog.debug ''huh'' # => no output, sent to logfile.log instead

Me parece mucho más fácil y más versátil que otras opciones que he visto hasta ahora, así que espero que te ayude con las tuyas.


Basado en tu comentario

Todo esto podría evitarse si pudiera cambiar dinámicamente la ubicación de salida de un registrador ya instanciado (similar a cómo cambia el nivel de registro).

Si no está restringido al registrador predeterminado, puede usar otro log-gem.

Como un ejemplo con log4r :

require ''log4r'' module Crawler LOGGER = Log4r::Logger.new(''mylog'') class Runner def initialize LOGGER.info(''Created instance for %s'' % self.class) end end end ARGV << ''test'' #testcode #... case ARGV.first when ''test'' Crawler::LOGGER.outputters = Log4r::StdoutOutputter.new(''stdout'') when ''prod'' Crawler::LOGGER.outputters = Log4r::FileOutputter.new(''file'', :filename => ''test.log'') #append to existing log end #... Crawler::Runner.new

En el modo prod, los datos de registro se almacenan en un archivo (adjunto al archivo existente, pero hay opciones para crear nuevos archivos de registro o implementar archivos de registro continuos).

El resultado:

INFO main: Created instance for Crawler::Runner

Si usa el mecanismo de herencia de log4r (a), puede definir un registrador para cada clase (o en mi ejemplo siguiente para cada instancia) y compartir el outputter.

El ejemplo:

require ''log4r'' module Crawler LOGGER = Log4r::Logger.new(''mylog'') class Runner def initialize(id) @log = Log4r::Logger.new(''%s::%s %s'' % [LOGGER.fullname,self.class,id]) @log.info(''Created instance for %s with id %s'' % [self.class, id]) end end end ARGV << ''test'' #testcode #... case ARGV.first when ''test'' Crawler::LOGGER.outputters = Log4r::StdoutOutputter.new(''stdout'') when ''prod'' Crawler::LOGGER.outputters = Log4r::FileOutputter.new(''file'', :filename => ''test.log'') #append to existing log end #... Crawler::Runner.new(1) Crawler::Runner.new(2)

El resultado:

INFO Runner 1: Created instance for Crawler::Runner with id 1 INFO Runner 2: Created instance for Crawler::Runner with id 2

(a) Un nombre de registrador como A::B tiene el nombre B y es un hijo del registrador con el nombre A Hasta donde yo sé, esto no es herencia de objetos.

Una ventaja de este enfoque: si desea utilizar un único registrador para cada clase, solo necesita cambiar el nombre del registrador.


Como lo señala Zenagray, la extracción de los métodos de clase se dejó fuera de la respuesta de Jacob. Una pequeña adición resuelve eso:

require ''logger'' module Logging class << self def logger @logger ||= Logger.new($stdout) end def logger=(logger) @logger = logger end end # Addition def self.included(base) class << base def logger Logging.logger end end end def logger Logging.logger end end

El uso previsto es a través de "incluir":

class Dog include Logging def self.bark logger.debug "chirp" puts "#{logger.__id__}" end def bark logger.debug "grrr" puts "#{logger.__id__}" end end class Cat include Logging def self.bark logger.debug "chirp" puts "#{logger.__id__}" end def bark logger.debug "grrr" puts "#{logger.__id__}" end end Dog.new.bark Dog.bark Cat.new.bark Cat.bark

Produce:

D, [2014-05-06T22:27:33.991454 #2735] DEBUG -- : grrr 70319381806200 D, [2014-05-06T22:27:33.991531 #2735] DEBUG -- : chirp 70319381806200 D, [2014-05-06T22:27:33.991562 #2735] DEBUG -- : grrr 70319381806200 D, [2014-05-06T22:27:33.991588 #2735] DEBUG -- : chirp 70319381806200

Tenga en cuenta que la identificación del registrador es la misma en los cuatro casos. Si desea una instancia diferente para cada clase, no use Logging.logger , más bien use self.class.logger :

require ''logger'' module Logging def self.included(base) class << base def logger @logger ||= Logger.new($stdout) end def logger=(logger) @logger = logger end end end def logger self.class.logger end end

El mismo programa ahora produce:

D, [2014-05-06T22:36:07.709645 #2822] DEBUG -- : grrr 70350390296120 D, [2014-05-06T22:36:07.709723 #2822] DEBUG -- : chirp 70350390296120 D, [2014-05-06T22:36:07.709763 #2822] DEBUG -- : grrr 70350390295100 D, [2014-05-06T22:36:07.709791 #2822] DEBUG -- : chirp 70350390295100

Tenga en cuenta que los dos primeros identificadores son los mismos, pero son diferentes de los dos identificadores que muestran que tenemos dos instancias, una para cada clase.


Con el diseño que ha diseñado, parece que la solución más fácil es darle a Crawler un método de módulo que devuelve un módulo ivar.

module Crawler def self.logger @logger end def self.logger=(logger) @logger = logger end end

O podría usar " class <<self magia" si quisiera:

module Crawler class <<self attr_accessor :logger end end

Hace exactamente lo mismo.


Inspirado por este hilo, creé la joya easy_logging .

Combina todas las características discutidas, tales como:

  • Agrega funcionalidad de registro en cualquier lugar con un comando breve y autodescriptivo
  • Logger funciona tanto en clase como en métodos de instancia
  • Logger es específico de la clase y contiene el nombre de la clase

Instalación:

gem install ''easy_logging

Uso:

require ''easy_logging'' class YourClass include EasyLogging def do_something # ... logger.info ''something happened'' end end class YourOtherClass include EasyLogging def self.do_something # ... logger.info ''something happened'' end end YourClass.new.do_something YourOtherClass.do_something

Salida

I, [2017-06-03T21:59:25.160686 #5900] INFO -- YourClass: something happened I, [2017-06-03T21:59:25.160686 #5900] INFO -- YourOtherClass: something happened

Más detalles sobre easy_logging .


Me gusta tener un método de logger disponible en mis clases, pero no me gusta @logger = Logging.logger en todos mis inicializadores. Por lo general, hago esto:

module Logging # This is the magical bit that gets mixed into your classes def logger Logging.logger end # Global, memoized, lazy initialized instance of a logger def self.logger @logger ||= Logger.new(STDOUT) end end

Luego, en tus clases:

class Widget # Mix in the ability to log stuff ... include Logging # ... and proceed to log with impunity: def discombobulate(whizbang) logger.warn "About to combobulate the whizbang" # commence discombobulation end end

Debido a que el método Logging#logger puede acceder a la instancia en la que se mezcla el módulo, es trivial ampliar su módulo de registro para registrar el nombre de clase con los mensajes de registro:

module Logging def logger @logger ||= Logging.logger_for(self.class.name) end # Use a hash class-ivar to cache a unique Logger per class: @loggers = {} class << self def logger_for(classname) @loggers[classname] ||= configure_logger_for(classname) end def configure_logger_for(classname) logger = Logger.new(STDOUT) logger.progname = classname logger end end end

Su Widget ahora registra mensajes con su nombre de clase, y no necesita cambiar un bit :)


Puede haber alguna extraña magia de Ruby que te permita evitarla, pero hay una solución bastante simple que no necesita ser extraña. Simplemente coloque el registrador en el módulo y acceda directamente, con un mecanismo para configurarlo. Si quiere estar tranquilo, defina un "registrador diferido" que mantenga un indicador que indique si todavía tiene un registrador, y cuelgue mensajes en silencio hasta que se configure el registrador, emite una excepción de que algo se registra antes de que el registrador establecer o agrega el mensaje de registro a una lista para que pueda registrarse una vez que se haya definido el registrador.


Una pequeña porción de código para demostrar cómo funciona esto. Simplemente estoy creando un nuevo Objeto básico para poder observar que object_id permanece igual durante todas las llamadas:

module M class << self attr_accessor :logger end @logger = nil class C def initialize puts "C.initialize, before setting M.logger: #{M.logger.object_id}" M.logger = Object.new puts "C.initialize, after setting M.logger: #{M.logger.object_id}" @base = D.new end end class D def initialize puts "D.initialize M.logger: #{M.logger.object_id}" end end end puts "M.logger (before C.new): #{M.logger.object_id}" engine = M::C.new puts "M.logger (after C.new): #{M.logger.object_id}"

El resultado de este código es similar (un object_id de 4 significa nil ):

M.logger (before C.new): 4 C.initialize, before setting M.logger: 4 C.initialize, after setting M.logger: 59360 D.initialize M.logger: 59360 M.logger (after C.new): 59360

¡Gracias por la ayuda chicos!