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!