Algo como uma funcionalidade tee no logger.
tee --append test.log
para evitar substituições.
Algo como uma funcionalidade tee no logger.
tee --append test.log
para evitar substituições.
Respostas:
Você pode escrever uma pseudo IO
classe que gravará em vários 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
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 Logger
chama puts
seu MultiIO
objeto, ele gravará em ambos STDOUT
e em seu arquivo de log.
Edit: fui em frente e descobri o resto da interface. Um dispositivo de log deve responder write
e close
(não puts
). Contanto que MultiIO
responda a esses e os atue como proxy para os objetos IO reais, isso deve 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)
é depreciado.
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)
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))
extend
qualquer ActiveSupport::Logger
instância como mostrado acima.
config.logger.extend()
configuração interna do meu ambiente. Em vez disso, configurei config.logger
para STDOUT
em meu ambiente e, em seguida, estendi o logger em inicializadores diferentes.
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
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.
"| tee test.log"
vai substitui as antigas saídas, pode ser "| tee -a test.log"
em vez
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)
MultiLogger
como @dsz descreve é uma ótima opção. Obrigado por compartilhar!
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')
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
Rápido e sujo (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')
A resposta de @ jonas054 acima é ótima, mas polui a MultiDelegator
classe a cada novo delegado. Se você usar MultiDelegator
vá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. tee
tem um write
método, mas nenhum size
mé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, tee2
responde size
conforme o esperado, mas também responde por write
causa do primeiro delegado. Mesmo tee
agora responde por size
causa 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
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.
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-colors
que estava esperando um Integer
e 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"
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
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.
ActiveSupport::Logger
funciona fora da caixa com isso - você só precisa usar Rails.logger.extend
com ActiveSupport::Logger.broadcast(...)
.
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 ...')
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.
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")
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.log
por 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"
…
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 IO
objeto 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, #write
retorna a soma do número de bytes gravados em todos os alvos IO subjacentes).
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 :-)
| tee
antes do arquivo funcionou para mim, entãoLogger.new("| tee test.log")
. Observe o tubo. Isso foi de uma dica em coderwall.com/p/y_b3ra/…