Qualcosa come una funzionalità tee nel logger.
tee --append test.log
per evitare sovrascritture.
Qualcosa come una funzionalità tee nel logger.
tee --append test.log
per evitare sovrascritture.
Risposte:
Puoi scrivere una pseudo IO
classe che scriverà su più IO
oggetti. Qualcosa di simile a:
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
Quindi impostalo come file di registro:
log_file = File.open("log/debug.log", "a")
Logger.new MultiIO.new(STDOUT, log_file)
Ogni volta che Logger
chiama puts
il tuo MultiIO
oggetto, scriverà sia sul STDOUT
tuo file di log.
Modifica: sono andato avanti e ho capito il resto dell'interfaccia. Un dispositivo di registro deve rispondere a write
e close
(non puts
). Finché MultiIO
risponde a quelli e li invia tramite proxy agli oggetti IO reali, dovrebbe funzionare.
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)
è ammortizzato.
@ La soluzione di David è molto buona. Ho creato una classe delegante generica per più obiettivi in base al suo codice.
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)
Se sei in Rails 3 o 4, come sottolinea questo post del blog , Rails 4 ha questa funzionalità integrata . Quindi puoi fare:
# config/environment/production.rb
file_logger = Logger.new(Rails.root.join("log/alternative-output.log"))
config.logger.extend(ActiveSupport::Logger.broadcast(file_logger))
Oppure, se sei su Rails 3, puoi eseguire il backport:
# 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
qualsiasi ActiveSupport::Logger
istanza come mostrato sopra.
config.logger.extend()
configurazione interna del mio ambiente. Invece, ho impostato config.logger
su STDOUT
nel mio ambiente, quindi ho esteso il logger in diversi inizializzatori.
Per chi ama la semplicità:
log = Logger.new("| tee test.log") # note the pipe ( '|' )
log.info "hi" # will log to both STDOUT and test.log
Oppure stampa il messaggio nel formattatore 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
In realtà sto usando questa tecnica per stampare su un file di log, un servizio di cloud logger (logentries) e se è un ambiente di sviluppo, stampa anche su STDOUT.
"| tee test.log"
sovrascriverà le vecchie uscite, potrebbe essere "| tee -a test.log"
invece
Anche se mi piacciono molto gli altri suggerimenti, ho riscontrato lo stesso problema ma volevo la possibilità di avere livelli di registrazione diversi per STDERR e il file.
Ho finito con una strategia di routing che multiplexa a livello di logger piuttosto che a livello di IO, in modo che ogni logger potesse quindi operare a livelli di log indipendenti:
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
like che descrive @dsz è perfetto. Grazie per la condivisione!
Puoi anche aggiungere più funzionalità di registrazione del dispositivo direttamente nel Logger:
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
Per esempio:
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')
Ecco un'altra implementazione, ispirata dalla risposta di @ jonas054 .
Questo utilizza un modello simile a Delegator
. In questo modo non è necessario elencare tutti i metodi che si desidera delegare, poiché delegherà tutti i metodi definiti in uno qualsiasi degli oggetti di destinazione:
class Tee < DelegateToAllClass(IO)
end
$stdout = Tee.new(STDOUT, File.open("#{__FILE__}.log", "a"))
Dovresti essere in grado di usarlo anche con Logger.
delegate_to_all.rb è disponibile da qui: https://gist.github.com/TylerRick/4990898
Veloce e sporco (rif: 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 risposta di @ jonas054 sopra è ottima, ma inquina la MultiDelegator
classe con ogni nuovo delegato. Se lo usi MultiDelegator
più volte, continuerà ad aggiungere metodi alla classe, il che è indesiderabile. (Vedi sotto per esempio)
Ecco la stessa implementazione, ma utilizzando classi anonime in modo che i metodi non inquinino la classe delegante.
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
Ecco un esempio dell'inquinamento del metodo con l'implementazione originale, in contrasto con l'implementazione modificata:
tee = MultiDelegator.delegate(:write).to(STDOUT)
tee.respond_to? :write
# => true
tee.respond_to? :size
# => false
Va tutto bene sopra. tee
ha un write
metodo, ma nessun size
metodo come previsto. Ora, considera quando creiamo un altro delegato:
tee2 = MultiDelegator.delegate(:size).to("bar")
tee2.respond_to? :size
# => true
tee2.respond_to? :write
# => true !!!!! Bad
tee.respond_to? :size
# => true !!!!! Bad
Eh no, tee2
risponde size
come previsto, ma risponde anche a write
causa del primo delegato. Anche tee
ora risponde a size
causa del metodo di inquinamento.
In contrasto con la soluzione della classe anonima, tutto è come previsto:
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
Sei limitato al logger standard?
In caso contrario puoi usare 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
Un vantaggio: è anche possibile definire diversi livelli di log per stdout e file.
Sono passato alla stessa idea di "Delegare tutti i metodi a sottoelementi" che altre persone hanno già esplorato, ma sto restituendo per ciascuno di essi il valore di ritorno dell'ultima chiamata del metodo. Se non l'ho fatto, si è rotto logger-colors
che si aspettava un Integer
e la mappa stava restituendo 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
Ciò ridelegherà ogni metodo a tutte le destinazioni e restituirà solo il valore di ritorno dell'ultima chiamata.
Inoltre, se vuoi i colori, STDOUT o STDERR devono essere messi per ultimi, poiché sono gli unici due in cui dovrebbero essere emessi i colori. Ma poi, produrrà anche i colori nel tuo file.
logger = Logger.new MultiIO.new(File.open("log/test.log", 'w'), STDOUT)
logger.error "Roses are red"
logger.unknown "Violets are blue"
Ho scritto un po 'di RubyGem che ti permette di fare molte di queste cose:
# 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"
Puoi trovare il codice su github: teerb
Un altro modo. Se stai usando la registrazione con tag e hai bisogno di tag anche in un altro file di log, puoi farlo in questo modo
# 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))
Dopo questo otterrai tag uuid nel logger 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
Spero che aiuti qualcuno.
ActiveSupport::Logger
funziona immediatamente con questo - devi solo usare Rails.logger.extend
con ActiveSupport::Logger.broadcast(...)
.
Un'altra opzione ;-)
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 ...')
Mi piace l' approccio MultiIO . Funziona bene con Ruby Logger . Se usi l'IO puro , smette di funzionare perché mancano alcuni metodi che ci si aspetta che gli oggetti IO abbiano. I pipe sono stati menzionati prima qui: Come posso avere l'output del log del logger di ruby su stdout e file? . Ecco cosa funziona meglio per me.
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')
Nota So che questo non risponde direttamente alla domanda, ma è fortemente correlato. Ogni volta che ho cercato l'output su più IO mi sono imbattuto in questo thread, quindi spero che anche tu lo trovi utile.
Questa è una semplificazione della soluzione di @ 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
Ha tutti gli stessi vantaggi del suo senza la necessità del wrapper di classe esterna. È un'utilità utile da avere in un file ruby separato.
Usalo come una riga per generare istanze del delegatore in questo modo:
IO_delegator_instance = delegator(:write, :read).for(STDOUT, STDERR)
IO_delegator_instance.write("blah")
O usalo come una fabbrica in questo modo:
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")
Se sei d'accordo con l'utilizzo ActiveSupport
, ti consiglio vivamente di fare il check-out ActiveSupport::Logger.broadcast
, che è un modo eccellente e molto conciso per aggiungere ulteriori destinazioni di log a un logger.
In effetti, se stai usando Rails 4+ (a partire da questo commit ), non devi fare nulla per ottenere il comportamento desiderato, almeno se stai usando il rails console
. Ogni volta che usi rails console
, Rails si estende automaticamente inRails.logger
modo tale da restituire sia la sua normale destinazione di file ( log/production.log
, per esempio) che 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
Per qualche motivo sconosciuto e sfortunato, questo metodo non è documentato ma puoi fare riferimento al codice sorgente o ai post del blog per sapere come funziona o vedere esempi.
https://www.joshmcarthur.com/til/2018/08/16/logging-to-multiple-destinations-using-activesupport-4.html ha un altro esempio:
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"
…
Ho anche questo bisogno di recente, quindi ho implementato una libreria che lo fa. Ho appena scoperto questa domanda StackOverflow, quindi la sto pubblicando per chiunque ne abbia bisogno: https://github.com/agis/multi_io .
Rispetto alle altre soluzioni qui menzionate, questo si sforza di essere un IO
oggetto a sé stante, quindi può essere utilizzato come sostituto immediato per altri normali oggetti IO (file, socket, ecc.)
Detto questo, non ho ancora implementato tutti i metodi IO standard, ma quelli che lo sono seguono la semantica IO (ad esempio, #write
restituisce la somma del numero di byte scritti su tutte le destinazioni IO sottostanti).
Penso che il tuo STDOUT venga utilizzato per le informazioni critiche di runtime e gli errori generati.
Quindi io uso
$log = Logger.new('process.log', 'daily')
per registrare il debug e la registrazione regolare, quindi ne ha scritti alcuni
puts "doing stuff..."
dove ho bisogno di vedere le informazioni STDOUT che i miei script erano in esecuzione!
Bah, solo i miei 10 centesimi :-)
| tee
prima che il file funzionasse per me, quindiLogger.new("| tee test.log")
. Nota il tubo. Questo era da un suggerimento su coderwall.com/p/y_b3ra/…