¿Cómo puedo tener la salida del registro de ruby ​​logger en stdout así como en el archivo?

94

Algo como una función de tee en logger.

Manish Sapariya
fuente
1
Agregar | teeantes del archivo funcionó para mí, entonces Logger.new("| tee test.log"). Tenga en cuenta la tubería. Esto fue de un consejo en coderwall.com/p/y_b3ra/…
Mike W
@mjwatts Se utiliza tee --append test.logpara evitar sobrescrituras.
fangxing

Respuestas:

124

Puede escribir una pseudoclase IOque escribirá en varios IOobjetos. Algo como:

class MultiIO
  def initialize(*targets)
     @targets = targets
  end

  def write(*args)
    @targets.each {|t| t.write(*args)}
  end

  def close
    @targets.each(&:close)
  end
end

Luego configúrelo como su archivo de registro:

log_file = File.open("log/debug.log", "a")
Logger.new MultiIO.new(STDOUT, log_file)

Cada vez que Loggerllame putsa su MultiIOobjeto, escribirá en ambos STDOUTy en su archivo de registro.

Editar: Seguí adelante y descubrí el resto de la interfaz. Un dispositivo de registro debe responder writey close(no puts). Siempre que MultiIOresponda a ellos y los transmita a los objetos IO reales, esto debería funcionar.

David
fuente
si observa el ctor del registrador, verá que esto estropeará la rotación del registro. def initialize(log = nil, opt = {}) @dev = @filename = @shift_age = @shift_size = nil @mutex = LogDeviceMutex.new if log.respond_to?(:write) and log.respond_to?(:close) @dev = log else @dev = open_logfile(log) @dev.sync = true @filename = log @shift_age = opt[:shift_age] || 7 @shift_size = opt[:shift_size] || 1048576 end end
JeffCharter
3
Nota en Ruby 2.2, @targets.each(&:close)está depreciado.
xis
Funcionó para mí hasta que me di cuenta de que necesitaba llamar periódicamente: close on log_file para obtener log_file para actualizar lo que el registrador había registrado (esencialmente un "guardar"). A STDOUT no le gustó: estar cerca de ser llamado, como derrotar la idea de MultoIO. Se agregó un truco para omitir: cerrar excepto para la clase Archivo, pero desearía tener una solución más elegante.
Kim Miller
48

La solución de @ David es muy buena. Hice una clase de delegador genérico para múltiples objetivos en función de su código.

require 'logger'

class MultiDelegator
  def initialize(*targets)
    @targets = targets
  end

  def self.delegate(*methods)
    methods.each do |m|
      define_method(m) do |*args|
        @targets.map { |t| t.send(m, *args) }
      end
    end
    self
  end

  class <<self
    alias to new
  end
end

log_file = File.open("debug.log", "a")
log = Logger.new MultiDelegator.delegate(:write, :close).to(STDOUT, log_file)
jonas054
fuente
¿Podría explicar cómo es esto mejor o cuáles son las utilidades mejoradas de este enfoque que el simple sugerido por David?
Manish Sapariya
5
Es separación de preocupaciones. MultiDelegator solo sabe cómo delegar llamadas a múltiples objetivos. El hecho de que un dispositivo de registro necesite un método de escritura y cierre se implementa en la persona que llama. Esto hace que MultiDelegator se pueda utilizar en otras situaciones además del registro.
jonas054
Buena solucion. Traté de usar esto para enviar el resultado de mis tareas de rake a un archivo de registro. Sin embargo, para que funcione con put (para poder llamar a $ stdout.puts sin obtener el "método privado` put 'llamado "), tuve que agregar algunos métodos más: log_file = File.open (" tmp / rake.log "," a ") $ stdout = MultiDelegator.delegate (: write,: close,: puts,: print) .to (STDOUT, log_file) Sería bueno si fuera posible crear una clase Tee que heredara de MultiDelegator, como puedes hacer con la clase Delegator en stdlib ...
Tyler Rick
Se me ocurrió una implementación similar a Delegator de esto que llamé DelegatorToAll. De esta manera, no tiene que enumerar todos los métodos que desea delegar, ya que delegará todos los métodos que están definidos en la clase delegada (IO): class Tee <DelegateToAllClass (IO) end $ stdout = Tee.new (STDOUT , File.open ("# { FILE } .log", "a")) Consulte gist.github.com/TylerRick/4990898 para obtener más detalles.
Tyler Rick
1
Realmente me gusta su solución, pero no es buena como delegador genérico que se puede usar varias veces ya que cada delegación contamina todas las instancias con nuevos métodos. Publiqué una respuesta a continuación ( stackoverflow.com/a/36659911/123376 ) que soluciona este problema. Publiqué una respuesta en lugar de una edición, ya que puede ser educativo ver la diferencia entre las dos implementaciones, ya que también publiqué ejemplos.
Rado
35

Si está en Rails 3 o 4, como señala esta publicación de blog , Rails 4 tiene esta funcionalidad incorporada . Entonces puedes hacer:

# config/environment/production.rb
file_logger = Logger.new(Rails.root.join("log/alternative-output.log"))
config.logger.extend(ActiveSupport::Logger.broadcast(file_logger))

O si está en Rails 3, puede realizar una copia de respaldo:

# config/initializers/alternative_output_log.rb

# backported from rails4
module ActiveSupport
  class Logger < ::Logger
    # Broadcasts logs to multiple loggers. Returns a module to be
    # `extended`'ed into other logger instances.
    def self.broadcast(logger)
      Module.new do
        define_method(:add) do |*args, &block|
          logger.add(*args, &block)
          super(*args, &block)
        end

        define_method(:<<) do |x|
          logger << x
          super(x)
        end

        define_method(:close) do
          logger.close
          super()
        end

        define_method(:progname=) do |name|
          logger.progname = name
          super(name)
        end

        define_method(:formatter=) do |formatter|
          logger.formatter = formatter
          super(formatter)
        end

        define_method(:level=) do |level|
          logger.level = level
          super(level)
        end
      end
    end
  end
end

file_logger = Logger.new(Rails.root.join("log/alternative-output.log"))
Rails.logger.extend(ActiveSupport::Logger.broadcast(file_logger))
phillbaker
fuente
¿Es esto aplicable fuera de los rieles o solo rieles?
Ed Sykes
Está basado en ActiveSupport, por lo que si ya tiene esa dependencia, puede usar extendcualquier ActiveSupport::Loggerinstancia como se muestra arriba.
phillbaker
Gracias, fue de gran ayuda.
Lucas
Creo que esta es la respuesta más simple y efectiva, aunque tuve algunas rarezas al usar la config.logger.extend()configuración interna de mi entorno. En su lugar, me puse config.loggera STDOUTen mi ambiente, luego se extendió el registrador en diferentes inicializadores.
mattsch
14

Para los que les gusta lo simple:

log = Logger.new("| tee test.log") # note the pipe ( '|' )
log.info "hi" # will log to both STDOUT and test.log

fuente

O imprima el mensaje en el formateador de Logger:

log = Logger.new("test.log")
log.formatter = proc do |severity, datetime, progname, msg|
    puts msg
    msg
end
log.info "hi" # will log to both STDOUT and test.log

De hecho, estoy usando esta técnica para imprimir en un archivo de registro, un servicio de registro en la nube (logentries) y, si es un entorno de desarrollo, también imprimir en STDOUT.

Igor
fuente
2
"| tee test.log"se sobrescribe las salidas de edad, puede ser "| tee -a test.log"en su lugar
fangxing
13

Si bien me gustan bastante las otras sugerencias, descubrí que tenía el mismo problema pero quería tener la capacidad de tener diferentes niveles de registro para STDERR y el archivo.

Terminé con una estrategia de enrutamiento que se multiplexa en el nivel del registrador en lugar de en el nivel de IO, de modo que cada registrador pudiera operar a niveles de registro independientes:

class MultiLogger
  def initialize(*targets)
    @targets = targets
  end

  %w(log debug info warn error fatal unknown).each do |m|
    define_method(m) do |*args|
      @targets.map { |t| t.send(m, *args) }
    end
  end
end

stderr_log = Logger.new(STDERR)
file_log = Logger.new(File.open('logger.log', 'a'))

stderr_log.level = Logger::INFO
file_log.level = Logger::DEBUG

log = MultiLogger.new(stderr_log, file_log)
dsz
fuente
1
Me gusta más esta solución porque es (1) simple y (2) te anima a reutilizar tus clases de Logger en lugar de asumir que todo va a un archivo. En mi caso, me gustaría iniciar sesión en STDOUT y un appender GELF para Graylog. Tener una MultiLoggerdescripción similar a la de @dsz encaja perfectamente. ¡Gracias por compartir!
Eric Kramer
Sección agregada para manejar pseudovariables (establecedores / captadores)
Eric Kramer
11

También puede agregar la funcionalidad de registro de varios dispositivos directamente en el registrador:

require 'logger'

class Logger
  # Creates or opens a secondary log file.
  def attach(name)
    @logdev.attach(name)
  end

  # Closes a secondary log file.
  def detach(name)
    @logdev.detach(name)
  end

  class LogDevice # :nodoc:
    attr_reader :devs

    def attach(log)
      @devs ||= {}
      @devs[log] = open_logfile(log)
    end

    def detach(log)
      @devs ||= {}
      @devs[log].close
      @devs.delete(log)
    end

    alias_method :old_write, :write
    def write(message)
      old_write(message)

      @devs ||= {}
      @devs.each do |log, dev|
        dev.write(message)
      end
    end
  end
end

Por ejemplo:

logger = Logger.new(STDOUT)
logger.warn('This message goes to stdout')

logger.attach('logfile.txt')
logger.warn('This message goes both to stdout and logfile.txt')

logger.detach('logfile.txt')
logger.warn('This message goes just to stdout')
Ramón de C Valle
fuente
9

Aquí hay otra implementación, inspirada en @ jonas054 la respuesta de .

Esto usa un patrón similar al Delegator. De esta manera, no tiene que enumerar todos los métodos que desea delegar, ya que delegará todos los métodos que estén definidos en cualquiera de los objetos de destino:

class Tee < DelegateToAllClass(IO)
end

$stdout = Tee.new(STDOUT, File.open("#{__FILE__}.log", "a"))

También debería poder usar esto con Logger.

delegate_to_all.rb está disponible desde aquí: https://gist.github.com/TylerRick/4990898

Tyler Rick
fuente
3

La respuesta de @ jonas054 anterior es excelente, pero contamina la MultiDelegatorclase con cada nuevo delegado. Si utilizaMultiDelegator varias veces, seguirá agregando métodos a la clase, lo cual no es deseable. (Ver abajo por ejemplo)

Aquí está la misma implementación, pero usando clases anónimas para que los métodos no contaminen la clase delegadora.

class BetterMultiDelegator

  def self.delegate(*methods)
    Class.new do
      def initialize(*targets)
        @targets = targets
      end

      methods.each do |m|
        define_method(m) do |*args|
          @targets.map { |t| t.send(m, *args) }
        end
      end

      class <<self
        alias to new
      end
    end # new class
  end # delegate

end

A continuación se muestra un ejemplo del método de contaminación con la implementación original, en contraste con la implementación modificada:

tee = MultiDelegator.delegate(:write).to(STDOUT)
tee.respond_to? :write
# => true
tee.respond_to? :size
# => false 

Todo está bien arriba. teetiene un writemétodo, pero ningún sizemétodo como se esperaba. Ahora, considere cuando creamos otro delegado:

tee2 = MultiDelegator.delegate(:size).to("bar")
tee2.respond_to? :size
# => true
tee2.respond_to? :write
# => true   !!!!! Bad
tee.respond_to? :size
# => true   !!!!! Bad

Oh no, tee2responde sizecomo se esperaba, pero también responde writepor el primer delegado. Incluso teeahora responde asize causa del método de contaminación.

Compare esto con la solución de clase anónima, todo es como se esperaba:

see = BetterMultiDelegator.delegate(:write).to(STDOUT)
see.respond_to? :write
# => true
see.respond_to? :size
# => false

see2 = BetterMultiDelegator.delegate(:size).to("bar")
see2.respond_to? :size
# => true
see2.respond_to? :write
# => false
see.respond_to? :size
# => false
Rado
fuente
2

¿Está restringido al registrador estándar?

Si no, puede utilizar log4r :

require 'log4r' 

LOGGER = Log4r::Logger.new('mylog')
LOGGER.outputters << Log4r::StdoutOutputter.new('stdout')
LOGGER.outputters << Log4r::FileOutputter.new('file', :filename => 'test.log') #attach to existing log-file

LOGGER.info('aa') #Writs on STDOUT and sends to file

Una ventaja: también puede definir diferentes niveles de registro para stdout y file.

knut
fuente
1

Fui a la misma idea de "Delegar todos los métodos a subelementos" que otras personas ya exploraron, pero estoy devolviendo para cada uno de ellos el valor de retorno de la última llamada del método. Si no lo hacía, se rompía, lo logger-colorsque esperaba un Integery el mapa devolvía un Array.

class MultiIO
  def self.delegate_all
    IO.methods.each do |m|
      define_method(m) do |*args|
        ret = nil
        @targets.each { |t| ret = t.send(m, *args) }
        ret
      end
    end
  end

  def initialize(*targets)
    @targets = targets
    MultiIO.delegate_all
  end
end

Esto volverá a delegar cada método a todos los objetivos y devolverá solo el valor de retorno de la última llamada.

Además, si desea colores, STDOUT o STDERR deben colocarse al final, ya que son los únicos dos donde se supone que se emiten los colores. Pero luego, también generará colores en su archivo.

logger = Logger.new MultiIO.new(File.open("log/test.log", 'w'), STDOUT)
logger.error "Roses are red"
logger.unknown "Violets are blue"
Jerska
fuente
1

He escrito un pequeño RubyGem que te permite hacer varias de estas cosas:

# Pipe calls to an instance of Ruby's logger class to $stdout
require 'teerb'

log_file = File.open("debug.log", "a")
logger = Logger.new(TeeRb::IODelegate.new(log_file, STDOUT))

logger.warn "warn"
$stderr.puts "stderr hello"
puts "stdout hello"

Puedes encontrar el código en github: teerb

Patrick Hüsler
fuente
1

Una forma más. Si está utilizando el registro etiquetado y también necesita etiquetas en otro archivo de registro, puede hacerlo de esta manera

# backported from rails4
# config/initializers/active_support_logger.rb
module ActiveSupport
 class Logger < ::Logger

 # Broadcasts logs to multiple loggers. Returns a module to be
 # `extended`'ed into other logger instances.
 def self.broadcast(logger)
  Module.new do
    define_method(:add) do |*args, &block|
      logger.add(*args, &block)
      super(*args, &block)
    end

    define_method(:<<) do |x|
      logger << x
      super(x)
    end

    define_method(:close) do
      logger.close
      super()
    end

    define_method(:progname=) do |name|
      logger.progname = name
      super(name)
    end

    define_method(:formatter=) do |formatter|
      logger.formatter = formatter
      super(formatter)
    end

    define_method(:level=) do |level|
      logger.level = level
      super(level)
    end

   end # Module.new
 end # broadcast

 def initialize(*args)
   super
   @formatter = SimpleFormatter.new
 end

  # Simple formatter which only displays the message.
  class SimpleFormatter < ::Logger::Formatter
   # This method is invoked when a log event occurs
   def call(severity, time, progname, msg)
   element = caller[4] ? caller[4].split("/").last : "UNDEFINED"
    "#{Thread.current[:activesupport_tagged_logging_tags]||nil } # {time.to_s(:db)} #{severity} #{element} -- #{String === msg ? msg : msg.inspect}\n"
   end
  end

 end # class Logger
end # module ActiveSupport

custom_logger = ActiveSupport::Logger.new(Rails.root.join("log/alternative_#{Rails.env}.log"))
Rails.logger.extend(ActiveSupport::Logger.broadcast(custom_logger))

Después de esto, obtendrá etiquetas uuid en el registrador alternativo

["fbfea87d1d8cc101a4ff9d12461ae810"] 2015-03-12 16:54:04 INFO logger.rb:28:in `call_app' -- 
["fbfea87d1d8cc101a4ff9d12461ae810"] 2015-03-12 16:54:04 INFO   logger.rb:31:in `call_app' -- Started POST "/psp/entrypoint" for 192.168.56.1 at 2015-03-12 16:54:04 +0700

Espero que ayude a alguien.

retirarse
fuente
Simple, confiable y funciona de manera brillante. ¡Gracias! Tenga en cuenta que ActiveSupport::Loggerfunciona de inmediato con esto, solo necesita usar Rails.logger.extendcon ActiveSupport::Logger.broadcast(...).
XtraSimplicity
0

Una opción más ;-)

require 'logger'

class MultiDelegator
  def initialize(*targets)
    @targets = targets
  end

  def method_missing(method_sym, *arguments, &block)
    @targets.each do |target|
      target.send(method_sym, *arguments, &block) if target.respond_to?(method_sym)
    end
  end
end

log = MultiDelegator.new(Logger.new(STDOUT), Logger.new(File.open("debug.log", "a")))

log.info('Hello ...')
Michael Voigt
fuente
0

Me gusta el enfoque MultiIO . Funciona bien con Ruby Logger . Si usa IO puro , deja de funcionar porque carece de algunos métodos que se espera que tengan los objetos IO. Las tuberías se mencionaron antes aquí: ¿Cómo puedo tener la salida del registro de ruby ​​logger en stdout y en el archivo? . Esto es lo que funciona mejor para mí.

def watch(cmd)
  output = StringIO.new
  IO.popen(cmd) do |fd|
    until fd.eof?
      bit = fd.getc
      output << bit
      $stdout.putc bit
    end
  end
  output.rewind
  [output.read, $?.success?]
ensure
  output.close
end

result, success = watch('./my/shell_command as a String')

Tenga en cuenta que sé que esto no responde a la pregunta directamente, pero está fuertemente relacionado. Siempre que buscaba salida a varios IO, me encontraba con este hilo, así que espero que también lo encuentre útil.

knugie
fuente
0

Esta es una simplificación de la solución de @ rado.

def delegator(*methods)
  Class.new do
    def initialize(*targets)
      @targets = targets
    end

    methods.each do |m|
      define_method(m) do |*args|
        @targets.map { |t| t.send(m, *args) }
      end
    end

    class << self
      alias for new
    end
  end # new class
end # delegate

Tiene todos los mismos beneficios que el suyo sin la necesidad de la envoltura de clase externa. Es una utilidad útil para tener en un archivo ruby ​​separado.

Úselo como una sola línea para generar instancias de delegador como este:

IO_delegator_instance = delegator(:write, :read).for(STDOUT, STDERR)
IO_delegator_instance.write("blah")

O úselo como una fábrica así:

logger_delegator_class = delegator(:log, :warn, :error)
secret_delegator = logger_delegator_class(main_logger, secret_logger)
secret_delegator.warn("secret")

general_delegator = logger_delegator_class(main_logger, debug_logger, other_logger) 
general_delegator.log("message")
Charles Murphy
fuente
0

Puedes usar el Loog::Teeobjeto de la looggema:

require 'loog'
logger = Loog::Tee.new(first, second)

Exactamente lo que busca.

yegor256
fuente
0

Si está de acuerdo con el uso ActiveSupport, le recomiendo encarecidamente que consulte ActiveSupport::Logger.broadcast, que es una forma excelente y muy concisa de agregar destinos de registro adicionales a un registrador.

De hecho, si está usando Rails 4+ (a partir de este compromiso ), no necesita hacer nada para obtener el comportamiento deseado, al menos si está usando rails console. Siempre que use rails console, Rails se extiende automáticamente deRails.logger manera que genera tanto en su destino de archivo habitual ( log/production.logpor ejemplo) como STDERR:

    console do |app|
      
      unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDERR, STDOUT)
        console = ActiveSupport::Logger.new(STDERR)
        Rails.logger.extend ActiveSupport::Logger.broadcast console
      end
      ActiveRecord::Base.verbose_query_logs = false
    end

Por alguna razón desconocida y desafortunada, este método no está documentado, pero puede consultar el código fuente o las publicaciones del blog para aprender cómo funciona o ver ejemplos.

https://www.joshmcarthur.com/til/2018/08/16/logging-to-multiple-destinations-using-activesupport-4.html tiene otro ejemplo:

require "active_support/logger"
console_logger = ActiveSupport::Logger.new(STDOUT)
file_logger = ActiveSupport::Logger.new("my_log.log")
combined_logger = console_logger.extend(ActiveSupport::Logger.broadcast(file_logger))

combined_logger.debug "Debug level"
Tyler Rick
fuente
0

También tengo esta necesidad recientemente, así que implementé una biblioteca que hace esto. Acabo de descubrir esta pregunta de StackOverflow, así que la estoy publicando para cualquiera que la necesite: https://github.com/agis/multi_io .

En comparación con las otras soluciones mencionadas aquí, esto se esfuerza por ser un IOobjeto propio, por lo que puede usarse como un reemplazo directo de otros objetos IO regulares (archivos, sockets, etc.)

Dicho esto, todavía no he implementado todos los métodos estándar de E / S, pero los que sí lo son, siguen la semántica de E / S (por ejemplo, #write devuelve la suma del número de bytes escritos en todos los objetivos de E / S subyacentes).

Agis
fuente
-3

Creo que su STDOUT se usa para información crítica de tiempo de ejecución y errores planteados.

Entonces uso

  $log = Logger.new('process.log', 'daily')

para registrar la depuración y el registro regular, y luego escribí algunos

  puts "doing stuff..."

donde necesito ver la información STDOUT de que mis scripts se estaban ejecutando.

Bah, solo mis 10 centavos :-)

rupweb
fuente