Como posso ter a saída de log do ruby ​​logger para stdout, bem como arquivo?

94

Algo como uma funcionalidade tee no logger.

Manish Sapariya
fonte
1
Adicionar | teeantes do arquivo funcionou para mim, então Logger.new("| tee test.log"). Observe o tubo. Isso foi de uma dica em coderwall.com/p/y_b3ra/…
Mike W
@mjwatts Use tee --append test.logpara evitar substituições.
fangxing

Respostas:

124

Você pode escrever uma pseudo IOclasse que gravará em vários 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

Em seguida, defina-o como seu arquivo de registro:

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

Cada vez que Loggerchama putsseu MultiIOobjeto, ele gravará em ambos STDOUTe em seu arquivo de log.

Edit: fui em frente e descobri o resto da interface. Um dispositivo de log deve responder writee close(não puts). Contanto que MultiIOresponda a esses e os atue como proxy para os objetos IO reais, isso deve funcionar.

David
fonte
se você olhar para o ctor do logger, verá que isso bagunçará a rotação do 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 em Ruby 2.2, @targets.each(&:close)é depreciado.
xis
Trabalhou para mim até que percebi que precisava chamar periodicamente: close em log_file para obter log_file para atualizar o logger registrado (essencialmente um "salvar"). STDOUT não gostou: quase não foi convocado, meio que derrotando a ideia do MultoIO. Adicionado um hack para pular: fechar, exceto para a classe Arquivo, mas gostaria de ter uma solução mais elegante.
Kim Miller de
48

A solução de @David é muito boa. Eu fiz uma classe delegadora genérica para vários destinos com base em seu 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
fonte
Você poderia explicar como isso é melhor ou quais são os utilitários aprimorados dessa abordagem do que a simples sugerida por David
Manish Sapariya
5
É separação de preocupações. MultiDelegator só sabe delegar chamadas a vários destinos. O fato de que um dispositivo de registro precisa de um método de gravação e fechamento é implementado no chamador. Isso torna o MultiDelegator utilizável em outras situações além do registro.
jonas054 de
Ótima solução. Tentei usar isso para colocar a saída de minhas tarefas de rake em um arquivo de log. Porém, para fazê-lo funcionar com puts (para poder chamar $ stdout.puts sem obter "método privado` puts 'chamado "), eu tive que adicionar mais alguns métodos: log_file = File.open (" tmp / rake.log "," a ") $ stdout = MultiDelegator.delegate (: escrever,: fechar,: puts,: imprimir) .to (STDOUT, log_file) Seria bom se fosse possível criar uma classe Tee herdada de MultiDelegator, como você pode fazer com a classe Delegator em stdlib ...
Tyler Rick
Eu vim com uma implementação do tipo Delegator que chamei de DelegatorToAll. Dessa forma, você não precisa listar todos os métodos que deseja delegar, pois ele delegará todos os métodos definidos na classe delegada (IO): class Tee <DelegateToAllClass (IO) end $ stdout = Tee.new (STDOUT , File.open ("# { FILE } .log", "a")) Consulte gist.github.com/TylerRick/4990898 para obter mais detalhes.
Tyler Rick
1
Eu realmente gosto da sua solução, mas não é boa como um delegador genérico que pode ser usado várias vezes, pois cada delegação polui todas as instâncias com novos métodos. Postei uma resposta abaixo ( stackoverflow.com/a/36659911/123376 ) que corrige esse problema. Publiquei uma resposta em vez de uma edição, pois pode ser educativo ver a diferença entre as duas implementações, pois também publiquei exemplos.
Rado de
35

Se você estiver no Rails 3 ou 4, como esta postagem no blog aponta, o Rails 4 tem essa funcionalidade embutida . Então você pode fazer:

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

Ou se você estiver no Rails 3, você pode fazer o 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))
Phillbaker
fonte
isso é aplicável fora dos trilhos ou somente trilhos?
Ed Sykes,
É baseado no ActiveSupport, então se você já tem essa dependência, você pode usar extendqualquer ActiveSupport::Loggerinstância como mostrado acima.
Phillbaker
Obrigado, foi útil.
Lucas
Acho que essa é a resposta mais simples e eficaz, embora tenha tido algumas estranhezas ao usar a config.logger.extend()configuração interna do meu ambiente. Em vez disso, configurei config.loggerpara STDOUTem meu ambiente e, em seguida, estendi o logger em inicializadores diferentes.
mattsch
14

Para quem gosta de coisas simples:

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

fonte

Ou imprima a mensagem no formatador 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

Na verdade, estou usando essa técnica para imprimir em um arquivo de log, um serviço de log em nuvem (logentries) e se for um ambiente de desenvolvimento - também imprimir em STDOUT.

Igor
fonte
2
"| tee test.log"vai substitui as antigas saídas, pode ser "| tee -a test.log"em vez
Fangxing
13

Embora eu goste bastante das outras sugestões, descobri que tinha o mesmo problema, mas queria a capacidade de ter diferentes níveis de registro para STDERR e o arquivo.

Acabei com uma estratégia de roteamento que multiplexa no nível do logger, em vez de no nível IO, para que cada logger pudesse operar em níveis de log independentes:

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
fonte
1
Eu gosto mais dessa solução porque é (1) simples e (2) incentiva você a reutilizar suas classes Logger em vez de assumir que tudo vai para um arquivo. No meu caso, gostaria de fazer logon em STDOUT e um appender GELF para Graylog. Ter um MultiLoggercomo @dsz descreve é ​​uma ótima opção. Obrigado por compartilhar!
Eric Kramer
Seção adicionada para lidar com pseudovariáveis ​​(setters / getters)
Eric Kramer
11

Você também pode adicionar a funcionalidade de registro de vários dispositivos diretamente no 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

Por exemplo:

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')
Ramon de C Valle
fonte
9

Aqui está outra implementação, inspirada na resposta de @ jonas054 .

Isso usa um padrão semelhante a Delegator. Dessa forma, você não precisa listar todos os métodos que deseja delegar, uma vez que delegará todos os métodos definidos em qualquer um dos objetos de destino:

class Tee < DelegateToAllClass(IO)
end

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

Você deve ser capaz de usar isso com o Logger também.

delegate_to_all.rb está disponível aqui: https://gist.github.com/TylerRick/4990898

Tyler Rick
fonte
3

A resposta de @ jonas054 acima é ótima, mas polui a MultiDelegatorclasse a cada novo delegado. Se você usar MultiDelegatorvárias vezes, ele continuará adicionando métodos à classe, o que é indesejável. (Veja abaixo por exemplo)

Aqui está a mesma implementação, mas usando classes anônimas para que os métodos não poluam a classe 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

Aqui está um exemplo da poluição do método com a implementação original, em contraste com a implementação modificada:

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

Tudo está bem acima. teetem um writemétodo, mas nenhum sizemétodo como o esperado. Agora, considere quando criamos outro delegado:

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

Ah não, tee2responde sizeconforme o esperado, mas também responde por writecausa do primeiro delegado. Mesmo teeagora responde por sizecausa da poluição do método.

Compare isso com a solução de classe anônima, tudo está conforme o esperado:

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
fonte
2

Você está restrito ao logger padrão?

Caso contrário, você pode usar o 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

Uma vantagem: você também pode definir diferentes níveis de log para stdout e arquivo.

Knut
fonte
1

Tive a mesma ideia de "Delegar todos os métodos a subelementos" que outras pessoas já exploraram, mas estou retornando para cada um deles o valor de retorno da última chamada do método. Se eu não fizesse isso, ele quebrou o logger-colorsque estava esperando um Integere o mapa estava retornando um 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

Isso redelegará todos os métodos a todos os destinos e retornará apenas o valor de retorno da última chamada.

Além disso, se você quiser cores, STDOUT ou STDERR deve ser colocado por último, uma vez que são as únicas duas onde as cores devem ser impressas. Mas então, ele também produzirá cores em seu arquivo.

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

Eu escrevi um pequeno RubyGem que permite que você faça várias dessas coisas:

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

Você pode encontrar o código no github: teerb

Patrick Hüsler
fonte
1

Mais uma maneira. Se você estiver usando o registro com tags e também precisar de tags em outro arquivo de registro, poderá fazê-lo desta forma

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

Depois disso, você obterá tags uuid no 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

Espero que ajude alguém.

devorador
fonte
Simples, confiável e funciona perfeitamente. Obrigado! Observe que ActiveSupport::Loggerfunciona fora da caixa com isso - você só precisa usar Rails.logger.extendcom ActiveSupport::Logger.broadcast(...).
XtraSimplicity
0

Mais uma opção ;-)

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
fonte
0

Gosto da abordagem MultiIO . Funciona bem com Ruby Logger . Se você usar IO puro, ele para de funcionar porque não possui alguns métodos que os objetos IO devem ter. Pipes foram mencionados antes aqui: Como posso ter a saída do log do ruby ​​logger para stdout, bem como arquivo? . Aqui está o que funciona melhor para mim.

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: eu sei que isso não responde à pergunta diretamente, mas está fortemente relacionado. Sempre que eu procurava por saída para vários IOs, encontrei este tópico. Portanto, espero que você também ache isso útil.

knugie
fonte
0

Esta é uma simplificação da solução do @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

Ele tem os mesmos benefícios que o dele, sem a necessidade do invólucro de classe externa. É um utilitário útil para ter em um arquivo ruby ​​separado.

Use-o como uma linha única para gerar instâncias de delegador como:

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

OU use-o como uma fábrica como:

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
fonte
0

Você pode usar o Loog::Teeobjeto da looggema:

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

Exatamente o que você está procurando.

Yegor256
fonte
0

Se você estiver bem com o uso ActiveSupport, eu recomendo altamente verificar ActiveSupport::Logger.broadcast, que é uma maneira excelente e muito concisa de adicionar destinos de log adicionais a um logger.

Na verdade, se você está usando Rails 4+ (a partir deste commit ), você não precisa fazer nada para obter o comportamento desejado - pelo menos se estiver usando o rails console. Sempre que você usa o rails console, o Rails se estende automaticamente deRails.logger forma que a saída seja para o destino de arquivo usual ( log/production.logpor exemplo) e 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 algum motivo desconhecido e infeliz, esse método não é documentado, mas você pode consultar o código-fonte ou as postagens do blog para saber como funciona ou ver exemplos.

https://www.joshmcarthur.com/til/2018/08/16/logging-to-multiple-destinations-using-activesupport-4.html tem outro exemplo:

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
fonte
0

Eu também tenho essa necessidade recentemente, então implementei uma biblioteca que faz isso. Acabei de descobrir esta questão StackOverflow, então estou colocando-a lá para quem precisa: https://github.com/agis/multi_io .

Comparado com as outras soluções mencionadas aqui, este se esforça para ser um IOobjeto próprio, portanto, pode ser usado como um substituto para outros objetos IO regulares (arquivos, soquetes etc.)

Dito isso, ainda não implementei todos os métodos IO padrão, mas aqueles que o são, seguem a semântica IO (por exemplo, #writeretorna a soma do número de bytes gravados em todos os alvos IO subjacentes).

Agis
fonte
-3

Acho que seu STDOUT é usado para informações críticas de tempo de execução e erros levantados.

Então eu uso

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

para registrar depuração e registro regular e, em seguida, escreveu alguns

  puts "doing stuff..."

onde eu preciso ver informações STDOUT de que meus scripts estavam em execução!

Bah, só meus 10 centavos :-)

rupweb
fonte