Come posso avere l'output del registro di ruby ​​logger su stdout e file?


94

Qualcosa come una funzionalità tee nel logger.


1
L'aggiunta | teeprima che il file funzionasse per me, quindi Logger.new("| tee test.log"). Nota il tubo. Questo era da un suggerimento su coderwall.com/p/y_b3ra/…
Mike W

@mjwatts Utilizzare tee --append test.logper evitare sovrascritture.
Fangxing

Risposte:


124

Puoi scrivere una pseudo IOclasse che scriverà su più IOoggetti. 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 Loggerchiama putsil tuo MultiIOoggetto, scriverà sia sul STDOUTtuo file di log.

Modifica: sono andato avanti e ho capito il resto dell'interfaccia. Un dispositivo di registro deve rispondere a writee close(non puts). Finché MultiIOrisponde a quelli e li invia tramite proxy agli oggetti IO reali, dovrebbe funzionare.


se guardi il registro del logger vedrai che questo rovinerà la rotazione dei log. 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 in Ruby 2.2, @targets.each(&:close)è ammortizzato.
x è l'

Ha funzionato fino a quando mi sono reso conto che avevo bisogno di chiamare periodicamente: close on log_file per ottenere log_file per aggiornare ciò che il logger aveva registrato (essenzialmente un "salvataggio"). A STDOUT non piaceva: essere vicino a essere chiamato, un po 'sconfiggere l'idea di MultoIO. Aggiunto un trucco da saltare: chiudere tranne che per la classe File, ma vorrei avere una soluzione più elegante.
Kim Miller il

48

@ 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)

Potresti per favore spiegare, come è meglio o quali sono le utilità migliorate di questo approccio rispetto a quello semplice suggerito da David
Manish Sapariya

5
È la separazione delle preoccupazioni. MultiDelegator conosce solo la delega delle chiamate a più destinazioni. Il fatto che un dispositivo di registrazione necessiti di un metodo di scrittura e chiusura è implementato nel chiamante. Ciò rende MultiDelegator utilizzabile in altre situazioni oltre alla registrazione.
jonas054

Bella soluzione. Ho provato a usarlo per trasferire l'output delle mie attività di rake in un file di registro. Per farlo funzionare con put (per poter chiamare $ stdout.puts senza ottenere il "metodo privato` put 'chiamato "), ho dovuto aggiungere altri metodi: log_file = File.open (" tmp / rake.log "," a ") $ stdout = MultiDelegator.delegate (: write,: close,: put,: print) .to (STDOUT, log_file) Sarebbe bello se fosse possibile creare una classe Tee che ha ereditato da MultiDelegator, come puoi fare con la classe Delegator in stdlib ...
Tyler Rick

Ho ideato un'implementazione simile a Delegator che ho chiamato DelegatorToAll. In questo modo non è necessario elencare tutti i metodi che si desidera delegare, poiché delegherà tutti i metodi definiti nella classe delegato (IO): class Tee <DelegateToAllClass (IO) end $ stdout = Tee.new (STDOUT , File.open ("# { FILE } .log", "a")) Vedi gist.github.com/TylerRick/4990898 per maggiori dettagli.
Tyler Rick

1
Mi piace molto la tua soluzione, ma non è buona come delegante generico che può essere utilizzato più volte poiché ogni delega inquina tutte le istanze con nuovi metodi. Ho pubblicato una risposta qui sotto ( stackoverflow.com/a/36659911/123376 ) che risolve questo problema. Ho pubblicato una risposta piuttosto che una modifica poiché potrebbe essere educativo vedere la differenza tra le due implementazioni poiché ho anche pubblicato esempi.
Rado

35

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))

è applicabile al di fuori dei binari o solo dei binari?
Ed Sykes

È basato su ActiveSupport, quindi se hai già quella dipendenza, puoi extendqualsiasi ActiveSupport::Loggeristanza come mostrato sopra.
phillbaker

Grazie, è stato utile.
Lucas

Penso che questa sia la risposta più semplice ed efficace, anche se ho avuto qualche stranezza usando la config.logger.extend()configurazione interna del mio ambiente. Invece, ho impostato config.loggersu STDOUTnel mio ambiente, quindi ho esteso il logger in diversi inizializzatori.
mattsch

14

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

fonte

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.


2
"| tee test.log"sovrascriverà le vecchie uscite, potrebbe essere "| tee -a test.log"invece
fangxing

13

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)

1
Questa soluzione mi piace di più perché è (1) semplice e (2) ti incoraggia a riutilizzare le tue classi Logger invece di presumere che tutto vada in un file. Nel mio caso vorrei accedere a STDOUT e un appender GELF per Graylog. Avere un MultiLoggerlike che descrive @dsz è perfetto. Grazie per la condivisione!
Eric Kramer,

Aggiunta sezione per gestire le pseudovariabili (setter / getters)
Eric Kramer

11

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')

9

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



3

La risposta di @ jonas054 sopra è ottima, ma inquina la MultiDelegatorclasse con ogni nuovo delegato. Se lo usi MultiDelegatorpiù 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. teeha un writemetodo, ma nessun sizemetodo 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, tee2risponde sizecome previsto, ma risponde anche a writecausa del primo delegato. Anche teeora risponde a sizecausa 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

2

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.


1

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-colorsche si aspettava un Integere 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"

1

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


1

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.


Semplice, affidabile e funziona brillantemente. Grazie! Nota che ActiveSupport::Loggerfunziona immediatamente con questo - devi solo usare Rails.logger.extendcon ActiveSupport::Logger.broadcast(...).
XtraSimplicity

0

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 ...')

0

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.


0

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")

0

Puoi usare Loog::Teeoggetto dalla looggemma:

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

Esattamente quello che stai cercando.


0

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"

0

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 IOoggetto 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, #writerestituisce la somma del numero di byte scritti su tutte le destinazioni IO sottostanti).


-3

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 :-)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.