如何将ruby logger日志输出到stdout以及文件?


Answers:


124

您可以编写一个IO将写入多个IO对象的伪类。就像是:

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

然后将其设置为您的日志文件:

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

每次Logger调用puts您的MultiIO对象时,它将同时写入STDOUT和您的日志文件。

编辑:我继续并找出其余的界面。日志设备必须响应writeclose(不是puts)。只要MultiIO响应这些问题并将它们代理到真正的IO对象,这就应该起作用。


如果您查看记录器的ctor,您会发现这将使日志轮换混乱。 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 2014年

3
注意在Ruby 2.2中@targets.each(&:close)已弃用。
xis 2015年

为我工作,直到我意识到需要定期在log_file上调用:close来获取log_file来更新记录器记录的内容(本质上是“保存”)。STDOUT不喜欢:close被调用,有点打败了MultoIO的想法。添加了一个可以跳过:close的技巧,但类File除外,但是希望我有一个更优雅的解决方案。
金·米勒

48

@David的解决方案非常好。我已经根据他的代码为多个目标创建了一个通用委托类。

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)

您能否解释一下,这种方法比David
Manish Sapariya

5
这是关注点分离。MultiDelegator只知道将调用委派给多个目标。在调用方中实现了日志记录设备需要写操作和close方法的事实。这使MultiDelegator在除日志记录之外的其他情况下可用。
jonas054 2011年

不错的解决方案。我试图使用它来将我的rake任务的输出准备到日志文件中。为了使它能够与puts一起使用(为了能够调用$ stdout.puts而不会得到“调用私有方法'puts'”),我不得不添加一些其他方法:log_file = File.open(“ tmp / rake.log“,” a“)$ stdout = MultiDelegator.delegate(:write,:close,:puts,:print).to(STDOUT,log_file)如果可以创建从继承的Tee类,那会很好MultiDelegator,就像您可以在stdlib中使用Delegator类一样...
Tyler Rick

我想出了类似Delegator的实现,我称之为DelegatorToAll。这样,您不必列出要委托的所有方法,因为它将委托在委托类(IO)中定义的所有方法:类Tee <DelegateToAllClass(IO)end $ stdout = Tee.new(STDOUT) ,File.open(“#{ FILE } .log”,“ a”))有关更多详细信息,请参见gist.github.com/TylerRick/4990898
泰勒里克(Tyler Rick)

1
我真的很喜欢您的解决方案,但是它不能用作通用委托者,因为每个委托都使用新方法污染所有实例,因此它可以多次使用。我发布了以下解决问题的答案(stackoverflow.com/a/36659911/123376)。我发布答案而不是编辑内容,因为在我也发布示例的同时,了解这两种实现之间的差异可能会很有帮助。
拉多

35

本博客文章所述,如果您使用的是Rails 3或4 ,Rails 4内置了此功能。因此,您可以执行以下操作:

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

或者,如果您使用的是Rails 3,则可以向后移植:

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

这是否适用于导轨之外,或仅适用于导轨?
Ed Sykes 2015年

它基于ActiveSupport,因此,如果您已经具有该依赖关系,则可以使用上面显示的extend任何ActiveSupport::Logger实例。
phillbaker

谢谢,这很有帮助。
卢卡斯

我认为这是最简单,最有效的答案,尽管我config.logger.extend()在环境配置内部使用时有些怪异。相反,我在环境中设置config.loggerSTDOUT,然后在不同的初始化程序中扩展了记录器。
mattsch '17

14

对于那些喜欢简单的人:

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

资源

或在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

我实际上是在使用这种技术来打印到日志文件,云记录器服务(登录项),如果是开发环境,也要打印到STDOUT。


2
"| tee test.log"将会覆盖旧的输出,而可能会"| tee -a test.log"替代
方兴

13

虽然我很喜欢其他建议,但我发现我遇到了同样的问题,但是希望能够为STDERR和文件设置不同的日志记录级别。

我最终提出了一种路由策略,该策略在记录器级别而不是在IO级别进行多路复用,这样每个记录器便可以在独立的日志级别进行操作:

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
我最喜欢此解决方案,因为它很简单(1),并且(2)鼓励您重用Logger类,而不是假设一切都放在文件中。就我而言,我想登录到STDOUT和Graylog的GELF附加程序。拥有一个MultiLogger喜欢@dsz的描述非常适合。感谢分享!
埃里克·克莱默

添加了处理伪变量(设置者/获取者)的部分
Eric Kramer,

11

您还可以将多个设备日志记录功能直接添加到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

例如:

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

这是另一个受@ jonas054回答启发的实现。

这使用类似于的模式Delegator。这样,您不必列出要委派的所有方法,因为它将委派在任何目标对象中定义的所有方法:

class Tee < DelegateToAllClass(IO)
end

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

您也应该能够将其与Logger一起使用。

代理人_全部_.rb可从此处获得:https : //gist.github.com/TylerRick/4990898



3

上面的@ jonas054的答案很好,但是它会污染MultiDelegator每个新的委托人。如果使用MultiDelegator多次,它将继续向类添加方法,这是不希望的。(例如,参见波纹管)

这是相同的实现,但是使用匿名类,因此方法不会污染委托者类。

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

这是原始实现与修改后的实现对比的方法污染示例:

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

上面一切都很好。teewrite方法,但没有size预期的方法。现在,考虑当我们创建另一个委托时:

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

哦,不,按预期方式tee2响应size,但它也write因第一个委托而响应。甚至tee现在size由于方法污染而做出反应。

将此与匿名类解决方案进行对比,一切都按预期进行:

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

您是否限于标准记录仪?

如果没有,您可以使用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

优点之一:您还可以为stdout和file定义不同的日志级别。


1

我采用了其他人已经探讨过的“将所有方法委托给子元素”的相同思想,但是我为每个方法都返回了该方法最后一次调用的返回值。如果我不这样做,那么它就崩溃了logger-colors,它原本期望一个Integerand映射返回一个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

这会将每个方法重新委托给所有目标,并且仅返回上一次调用的返回值。

另外,如果您要颜色,则必须将STDOUT或STDERR放在最后,因为只有这两种颜色应该被输出。但随后,它还将向您的文件输出颜色。

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

1

我写了一点RubyGem,可以让您做以下几件事:

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

您可以在github上找到代码:teerb


1

另一种方式。如果您使用的是标记日志记录,并且还需要另一个日志文件中的标记,则可以通过这种方式进行

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

之后,您将在替代记录器中获得uuid标签

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

希望能对某人有所帮助。


简单,可靠且效果出色。谢谢!需要注意的是ActiveSupport::Logger工作出这个盒子-你只需要使用Rails.logger.extendActiveSupport::Logger.broadcast(...)
XtraSimplicity

0

还有一个选择;-)

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

我喜欢MultiIO方法。它与Ruby Logger一起很好地工作。如果使用纯IO,它将停止工作,因为它缺少IO对象预期具有的某些方法。管道之前在这里提到:如何将ruby logger日志输出到stdout以及文件?。这是最适合我的东西。

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

请注意,我知道这不能直接回答问题,但关系密切。每当我搜索到多个IO的输出时,都会遇到这个线程,因此,我希望您也觉得这个有用。


0

这是@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

它具有与他相同的所有优点,而无需外部类包装器。它在单独的ruby文件中有用。

将其用作单行生成代理实例,如下所示:

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

或像这样将其用作工厂:

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

您可以使用gem中的Loog::Tee对象loog

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

正是您要找的东西。


0

如果您可以使用ActiveSupport,那么我强烈建议您检出ActiveSupport::Logger.broadcast,这是将额外的日志目标添加到记录器的极好且非常简洁的方法。

实际上,如果您正在使用Rails 4+(截至本次提交),则无需执行任何操作即可获得所需的行为-至少在使用时rails console。每当您使用时rails console,Rails都会自动扩展Rails.logger,以使其输出到其通常的文件目标位置(log/production.log例如)和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

由于某些未知和不幸的原因,此方法未公开,但您可以参考源代码或博客文章以了解其工作方式或查看示例。

https://www.joshmcarthur.com/til/2018/08/16/logging-to-multiple-destinations-using-activesupport-4.html还有另一个示例:

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

最近我也有这种需要,所以我实现了一个可以做到这一点的库。我刚刚发现了这个StackOverflow问题,所以我将它发布给需要它的任何人:https : //github.com/agis/multi_io

与这里提到的其他解决方案相比,它努力成为IO自己的对象,因此可以用作其他常规IO对象(文件,套接字等)的直接替代。

也就是说,我尚未实现所有标准的IO方法,但遵循IO语义的方法(例如,#write返回写入所有基础IO目标的字节数之和)。


-3

我认为您的STDOUT用于关键的运行时信息和引发的错误。

所以我用

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

记录调试和常规记录,然后写了一些

  puts "doing stuff..."

我需要在这里查看我的脚本正在运行的STDOUT信息!

ah,只有我的10美分:-)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.