【问题标题】:How can I have ruby logger log output to stdout as well as file?如何将 ruby​​ 记录器日志输出到标准输出以及文件?
【发布时间】:2011-09-18 10:13:47
【问题描述】:

类似于记录器中的 tee 功能。

【问题讨论】:

  • 在文件对我有用之前添加| tee,所以Logger.new("| tee test.log")注意管道。这是来自coderwall.com/p/y_b3ra/…的提示
  • @mjwatts 使用tee --append test.log 防止覆盖。

标签: ruby logging


【解决方案1】:

您可以编写一个伪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 在您的MultiIO 对象上调用puts,它都会同时写入STDOUT 和您的日志文件。

编辑:我继续弄清楚界面的其余部分。日志设备必须响应writeclose(而不是puts)。只要MultiIO 响应这些并将它们代理到真正的 IO 对象,这应该可以工作。

【讨论】:

  • 如果您查看 logger 的 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
  • 注意在 Ruby 2.2 中,@targets.each(&:close) 已贬值。
  • 为我工作,直到我意识到我需要定期调用 log_file 上的 :close 以获取 log_file 以更新记录器记录的内容(本质上是“保存”)。 STDOUT 不喜欢 :close 被调用,有点挫败了 MultoIO 的想法。添加了一个 hack 以跳过 :close 类文件除外,但希望我有一个更优雅的解决方案。
【解决方案2】:

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

【讨论】:

  • 您能否解释一下,与大卫建议的普通方法相比,这种方法更好或增强了哪些实用程序
  • 这是关注点分离。 MultiDelegator 只知道将调用委托给多个目标。记录设备需要写入和关闭方法这一事实在调用者中实现。这使得 MultiDelegator 可用于日志记录以外的其他情况。
  • 不错的解决方案。我试图用它来将我的 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 类一样...
  • 我想出了一个类似于 Delegator 的实现,我称之为 DelegatorToAll。这样您就不必列出要委托的所有方法,因为它将委托委托类 (IO) 中定义的所有方法: class Tee FILE}.log", "a")) 详情请见gist.github.com/TylerRick/4990898
  • 我真的很喜欢你的解决方案,但它作为一个可以多次使用的通用委托器并不好,因为每个委托都会用新方法污染所有实例。我在下面发布了一个答案(stackoverflow.com/a/36659911/123376)来解决这个问题。我发布了答案而不是编辑,因为我还发布了示例,看到这两种实现之间的差异可能具有教育意义。
【解决方案3】:

如果您使用的是 Rails 3 或 4,正如this blog post 指出的那样,Rails 4 has this functionality built in。所以你可以这样做:

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

【讨论】:

  • 这适用于导轨之外,还是仅适用于导轨?
  • 它基于 ActiveSupport,所以如果你已经有了这个依赖,你可以extend 任何ActiveSupport::Logger 实例,如上所示。
  • 谢谢,很有帮助。
  • 我认为这是最简单和最有效的答案,尽管我在环境配置中使用 config.logger.extend() 时有些奇怪。相反,我在我的环境中将config.logger 设置为STDOUT,然后在不同的初始化程序中扩展了记录器。
【解决方案4】:

对于喜欢简单的人:

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

source

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

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

【讨论】:

  • "| tee test.log" 将覆盖旧输出,可能是"| tee -a test.log"
  • No such file or directory @ rb_sysopen
【解决方案5】:

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

【讨论】:

    【解决方案6】:

    虽然我非常喜欢其他建议,但我发现我遇到了同样的问题,但希望能够为 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) 简单,并且 (2) 鼓励您重用 Logger 类,而不是假设所有内容都保存在文件中。就我而言,我想登录到 STDOUT 和 Graylog 的 GELF appender。拥有像@dsz 所描述的MultiLogger 非常合适。感谢分享!
    • 添加部分来处理伪变量(setters/getters)
    【解决方案7】:

    这是另一个实现,灵感来自 @jonas054 的回答。

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

    class Tee < DelegateToAllClass(IO)
    end
    
    $stdout = Tee.new(STDOUT, File.open("#{__FILE__}.log", "a"))
    

    您应该也可以将它与 Logger 一起使用。

    delegate_to_all.rb 可从此处获得:https://gist.github.com/TylerRick/4990898

    【讨论】:

      【解决方案8】:

      又快又脏(参考: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')
      

      【讨论】:

        【解决方案9】:

        @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 
        

        以上一切都很好。 tee 有一个 write 方法,但没有预期的 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
        

        【讨论】:

          【解决方案10】:

          我编写了一个小 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

          【讨论】:

          • 谢谢。这很干净。
          【解决方案11】:

          您是否仅限于标准记录器?

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

          一个优点:您还可以为标准输出和文件定义不同的日志级别。

          【讨论】:

            【解决方案12】:

            我采用了其他人已经探索过的“将所有方法委托给子元素”的相同想法,但我为每个人返回了最后一次调用该方法的返回值。 如果我不这样做,它会破坏 logger-colors ,它期待 Integer 并且地图返回 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"
            

            【讨论】:

              【解决方案13】:

              另一种方式。 如果您正在使用标记日志记录并且还需要另一个日志文件中的标记,您可以这样做

              # 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(...) 一起使用。
              【解决方案14】:

              如果您可以使用ActiveSupport,那么我强烈建议您查看ActiveSupport::Logger.broadcast,这是一种向记录器添加额外日志目标的绝佳且非常简洁的方法。

              事实上,如果您使用的是 Rails 4+(截至this commit),您不需要做任何事情来获得所需的行为 - 至少如果您使用的是rails console。每当您使用 rails console 时,Rails automatically 都会扩展 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
              

              由于一些未知且不幸的原因,此方法为 undocumented,但您可以参考 source code 或博客文章了解其工作原理或查看示例。

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

              【讨论】:

                【解决方案15】:

                还有一个选项 ;-)

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

                【讨论】:

                  【解决方案16】:

                  我喜欢 MultiIO 方法。它适用于 Ruby Logger。如果你使用 pure IO 它会停止工作,因为它缺少一些 IO 对象应该有的方法。 管道在此之前提到过:How can I have ruby logger log output to stdout as well as file?。以下是最适合我的方法。

                  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 的输出时,我都会遇到这个线程。所以,我希望你也觉得这很有用。

                  【讨论】:

                    【解决方案17】:

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

                    【讨论】:

                      【解决方案18】:

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

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

                      正是您要找的。​​p>

                      【讨论】:

                        【解决方案19】:

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

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

                        也就是说,我还没有实现所有标准 IO 方法,但那些遵循 IO 语义的方法(例如,#write 返回写入所有底层 IO 目标的字节数之和)。

                        【讨论】:

                          【解决方案20】:

                          您可以继承Logger 并覆盖write 方法:

                          class LoggerWithStdout < Logger
                            def initialize(*)
                              super
                          
                              def @logdev.write(msg)
                                super
                          
                                puts msg
                              end
                            end
                          end
                          
                          logger = LoggerWithStdout.new('path/to/log_file.log')
                          

                          【讨论】:

                            【解决方案21】:

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

                            所以我用

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

                            去log debug和定期记录,然后写了几个

                              puts "doing stuff..."
                            

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

                            呸,只是我的 10 美分 :-)

                            【讨论】:

                              猜你喜欢
                              • 2017-04-13
                              • 1970-01-01
                              • 2016-09-24
                              • 1970-01-01
                              • 1970-01-01
                              • 2012-12-13
                              • 2021-10-31
                              • 2020-10-12
                              • 1970-01-01
                              相关资源
                              最近更新 更多