Algo como una función de tee en logger.
Algo como una función de tee en logger.
tee --append test.log
para evitar sobrescrituras.
Respuestas:
Puede escribir una pseudoclase IO
que escribirá en varios IO
objetos. 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 Logger
llame puts
a su MultiIO
objeto, escribirá en ambos STDOUT
y en su archivo de registro.
Editar: Seguí adelante y descubrí el resto de la interfaz. Un dispositivo de registro debe responder write
y close
(no puts
). Siempre que MultiIO
responda a ellos y los transmita a los objetos IO reales, esto debería funcionar.
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
@targets.each(&:close)
está depreciado.
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)
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))
extend
cualquier ActiveSupport::Logger
instancia como se muestra arriba.
config.logger.extend()
configuración interna de mi entorno. En su lugar, me puse config.logger
a STDOUT
en mi ambiente, luego se extendió el registrador en diferentes inicializadores.
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
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.
"| tee test.log"
se sobrescribe las salidas de edad, puede ser "| tee -a test.log"
en su lugar
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)
MultiLogger
descripción similar a la de @dsz encaja perfectamente. ¡Gracias por compartir!
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')
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
Rápido y sucio (ref: https://coderwall.com/p/y_b3ra/log-to-stdout-and-a-file-at-the-same-time )
require 'logger'
ll=Logger.new('| tee script.log')
ll.info('test')
La respuesta de @ jonas054 anterior es excelente, pero contamina la MultiDelegator
clase 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. tee
tiene un write
método, pero ningún size
mé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, tee2
responde size
como se esperaba, pero también responde write
por el primer delegado. Incluso tee
ahora 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
¿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.
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-colors
que esperaba un Integer
y 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"
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
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.
ActiveSupport::Logger
funciona de inmediato con esto, solo necesita usar Rails.logger.extend
con ActiveSupport::Logger.broadcast(...)
.
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 ...')
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.
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")
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.log
por 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"
…
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 IO
objeto 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).
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 :-)
| tee
antes del archivo funcionó para mí, entoncesLogger.new("| tee test.log")
. Tenga en cuenta la tubería. Esto fue de un consejo en coderwall.com/p/y_b3ra/…