52

编写一个小 Ruby 脚本,该脚本可以访问网络并爬取各种服务。我有一个模块,里面有几个类:

module Crawler
  class Runner
  class Options
  class Engine
end

我想在所有这些类中共享一个记录器。通常我只是把它放在模块中的一个常量中并像这样引用它:

Crawler::LOGGER.info("Hello, world")

问题是我无法创建我的记录器实例,直到我知道输出的去向。您通过命令行启动爬虫,此时您可以告诉它您要在开发(日志输出到 STDOUT)或生产(日志输出到文件 crawler.log)中运行:

crawler --environment=production

我有一个类Options可以解析通过命令行传入的选项。只有到那时我才知道如何使用正确的输出位置实例化记录器。

所以,我的问题是:我如何/在哪里放置我的记录器对象,以便我的所有类都可以访问它?

我可以将我的记录器实例传递给new()我创建的每个类实例的每个调用,但我知道必须有一种更好的 Rubyish 方式来做到这一点。我想象模块上有一些奇怪的类变量与class << self其他魔法共享。:)

更多细节:Runner通过将命令行选项传递给Options类来启动一切,并返回一个带有几个实例变量的对象:

module Crawler
  class Runner
    def initialize(argv)
      @options = Options.new(argv)
      # feels like logger initialization should go here
      # @options.log_output => STDOUT or string (log file name)
      # @options.log_level => Logger::DEBUG or Logger::INFO
      @engine = Engine.new()
    end
    def run
      @engine.go
    end
  end
end

runner = Runner.new(ARGV)
runner.run

我需要代码Engine才能访问记录器对象(以及在内部初始化的更多类Engine)。帮助!

如果您可以动态更改已经实例化的 Logger 的输出位置(类似于更改日志级别的方式),那么所有这些都可以避免。如果我在生产中,我会将其实例化为 STDOUT,然后转换为文件。我确实在某处看到了有关更改 Ruby 的 $stdout 全局变量的建议,该变量会将输出重定向到 STDOUT 以外的其他位置,但这似乎很 hacky。

谢谢!

4

9 回答 9

107

我喜欢logger在我的类中有一个可用的方法,但我不喜欢@logger = Logging.logger在我所有的初始化程序中撒上。通常,我这样做:

module Logging
  # This is the magical bit that gets mixed into your classes
  def logger
    Logging.logger
  end

  # Global, memoized, lazy initialized instance of a logger
  def self.logger
    @logger ||= Logger.new(STDOUT)
  end
end

然后,在您的课程中:

class Widget
  # Mix in the ability to log stuff ...
  include Logging

  # ... and proceed to log with impunity:
  def discombobulate(whizbang)
    logger.warn "About to combobulate the whizbang"
    # commence discombobulation
  end
end

因为该Logging#logger方法可以访问该模块混入的实例,所以扩展您的日志记录模块以使用日志消息记录类名是微不足道的:

module Logging
  def logger
    @logger ||= Logging.logger_for(self.class.name)
  end

  # Use a hash class-ivar to cache a unique Logger per class:
  @loggers = {}

  class << self
    def logger_for(classname)
      @loggers[classname] ||= configure_logger_for(classname)
    end

    def configure_logger_for(classname)
      logger = Logger.new(STDOUT)
      logger.progname = classname
      logger
    end
  end
end

Widget现在使用其类名记录消息,并且不需要更改一点:)

于 2011-07-20T20:34:00.843 回答
22

根据您布置的设计,看起来最简单的解决方案是为 Crawler 提供一个返回模块 ivar 的模块方法。

module Crawler
  def self.logger
    @logger
  end
  def self.logger=(logger)
    @logger = logger
  end
end

或者,class <<self如果您愿意,可以使用“魔法”:

module Crawler
  class <<self
    attr_accessor :logger
  end
end

它做同样的事情。

于 2009-05-27T21:25:33.063 回答
15

正如 Zenagray 指出的那样,Jacob 的回答中忽略了类方法的日志记录。一个小补充解决了这个问题:

require 'logger'

module Logging
  class << self
    def logger
      @logger ||= Logger.new($stdout)
    end

    def logger=(logger)
      @logger = logger
    end
  end

  # Addition
  def self.included(base)
    class << base
      def logger
        Logging.logger
      end
    end
  end

  def logger
    Logging.logger
  end
end

预期用途是通过“包括”:

class Dog
  include Logging

  def self.bark
    logger.debug "chirp"
    puts "#{logger.__id__}"
  end

  def bark
    logger.debug "grrr"
    puts "#{logger.__id__}"
  end
end

class Cat
  include Logging

  def self.bark
    logger.debug "chirp"
    puts "#{logger.__id__}"
  end

  def bark
    logger.debug "grrr"
    puts "#{logger.__id__}"
  end
end

Dog.new.bark
Dog.bark
Cat.new.bark
Cat.bark

产生:

D, [2014-05-06T22:27:33.991454 #2735] DEBUG -- : grrr
70319381806200
D, [2014-05-06T22:27:33.991531 #2735] DEBUG -- : chirp
70319381806200
D, [2014-05-06T22:27:33.991562 #2735] DEBUG -- : grrr
70319381806200
D, [2014-05-06T22:27:33.991588 #2735] DEBUG -- : chirp
70319381806200

请注意,记录器的 id 在所有四种情况下都是相同的。如果您想为每个类使用不同的实例,请不要使用Logging.logger,而是使用self.class.logger

require 'logger'

module Logging
  def self.included(base)
    class << base
      def logger
        @logger ||= Logger.new($stdout)
      end

      def logger=(logger)
        @logger = logger
      end
    end
  end

  def logger
    self.class.logger
  end
end

相同的程序现在产生:

D, [2014-05-06T22:36:07.709645 #2822] DEBUG -- : grrr
70350390296120
D, [2014-05-06T22:36:07.709723 #2822] DEBUG -- : chirp
70350390296120
D, [2014-05-06T22:36:07.709763 #2822] DEBUG -- : grrr
70350390295100
D, [2014-05-06T22:36:07.709791 #2822] DEBUG -- : chirp
70350390295100

请注意,前两个 id 相同,但与第二个两个 id 不同,这表明我们有两个实例——每个类一个。

于 2014-05-07T03:40:59.873 回答
4

受此线程的启发,我创建了easy_logging gem。

它结合了所有讨论的功能,例如:

  • 使用一个简短的自描述命令在任何地方添加日志记录功能
  • Logger 适用于类和实例方法
  • 记录器特定于类并包含类名

安装:

gem install 'easy_logging

用法:

require 'easy_logging'

class YourClass
  include EasyLogging

  def do_something
    # ...
    logger.info 'something happened'
  end
end

class YourOtherClass
  include EasyLogging

  def self.do_something
    # ...
    logger.info 'something happened'
  end
end

YourClass.new.do_something
YourOtherClass.do_something

输出

I, [2017-06-03T21:59:25.160686 #5900]  INFO -- YourClass: something happened
I, [2017-06-03T21:59:25.160686 #5900]  INFO -- YourOtherClass: something happened

更多细节在GitHub 上

于 2017-06-03T20:31:48.797 回答
2

这可能是一些奇怪的 Ruby 魔法,可以让你避免它,但有一个相当简单的解决方案,不需要奇怪。只需将记录器放入模块并直接访问它,并具有设置它的机制。如果您想对此保持冷静,请定义一个“惰性记录器”,该记录器保留一个标志以说明它是否有记录器,并且要么静默丢弃消息,直到设置记录器,要么在记录器被记录之前抛出异常记录是设置,或将日志消息添加到列表中,以便在定义记录器后对其进行记录。

于 2009-05-27T20:08:55.870 回答
2

一小段代码来演示它是如何工作的。我只是创建一个新的基本对象,以便我可以观察到 object_id 在整个调用过程中保持不变:

module M

  class << self
    attr_accessor :logger
  end

  @logger = nil

  class C
    def initialize
      puts "C.initialize, before setting M.logger: #{M.logger.object_id}"
      M.logger = Object.new
      puts "C.initialize, after setting M.logger: #{M.logger.object_id}"
      @base = D.new
    end
  end

  class D
    def initialize
      puts "D.initialize M.logger: #{M.logger.object_id}"
    end
  end
end

puts "M.logger (before C.new): #{M.logger.object_id}"
engine = M::C.new
puts "M.logger (after C.new): #{M.logger.object_id}"

此代码的输出如下所示(object_id4 表示nil):

M.logger (before C.new): 4
C.initialize, before setting M.logger: 4
C.initialize, after setting M.logger: 59360
D.initialize M.logger: 59360
M.logger (after C.new): 59360

谢谢你们的帮助!

于 2009-05-27T23:20:24.530 回答
1

如何将记录器包装在单例中,然后您可以使用 MyLogger.instance 访问它

于 2009-10-16T10:58:58.603 回答
1

虽然是一个老问题,但我认为值得记录一种不同的方法。

基于 Jacob 的回答,我建议您在需要时添加一个模块。

我的版本是这样的:

# saved into lib/my_log.rb

require 'logger'

module MyLog

  def self.logger
    if @logger.nil?
      @logger = Logger.new( STDERR)
      @logger.datetime_format = "%H:%M:%S "
    end
    @logger
  end

  def self.logger=( logger)
    @logger = logger
  end

  levels = %w(debug info warn error fatal)
  levels.each do |level|
    define_method( "#{level.to_sym}") do |msg|
      self.logger.send( level, msg)
    end
  end
end

include MyLog

我把它保存到一个方便的模块库中,我会这样使用它:

#! /usr/bin/env ruby
#

require_relative '../lib/my_log.rb'

MyLog.debug "hi"
# => D, [19:19:32 #31112] DEBUG -- : hi

MyLog.warn "ho"
# => W, [19:20:14 #31112]  WARN -- : ho

MyLog.logger.level = Logger::INFO

MyLog.logger = Logger.new( 'logfile.log')

MyLog.debug 'huh'
# => no output, sent to logfile.log instead

我发现这比我迄今为止看到的其他选项更容易、更通用,所以我希望它对你有所帮助。

于 2017-11-18T19:25:53.390 回答
0

根据您的评论

如果您可以动态更改已经实例化的 Logger 的输出位置(类似于更改日志级别的方式),那么所有这些都可以避免。

如果您不限于默认记录器,则可以使用另一个 log-gem。

log4r 为例

require 'log4r' 

module Crawler
  LOGGER = Log4r::Logger.new('mylog')
  class Runner
    def initialize
        LOGGER.info('Created instance for %s' % self.class)
    end
  end
end

ARGV << 'test'  #testcode

#...
case ARGV.first
  when 'test'
    Crawler::LOGGER.outputters = Log4r::StdoutOutputter.new('stdout')
  when 'prod'
    Crawler::LOGGER.outputters = Log4r::FileOutputter.new('file', :filename => 'test.log') #append to existing log
end
#...
Crawler::Runner.new

在 prod 模式下,日志数据存储在一个文件中(附加到现有文件,但有创建新日志文件或实施滚动日志文件的选项)。

结果:

 INFO main: Created instance for Crawler::Runner

如果您使用 log4r (a) 的继承机制,您可以为每个类定义一个记录器(或者在我下面的示例中为每个实例)并共享输出器。

这个例子:

require 'log4r' 

module Crawler
  LOGGER = Log4r::Logger.new('mylog')
  class Runner
    def initialize(id)
      @log = Log4r::Logger.new('%s::%s %s' % [LOGGER.fullname,self.class,id])
      @log.info('Created instance for %s with id %s' % [self.class, id])
    end
  end
end

ARGV << 'test'  #testcode

#...
case ARGV.first
  when 'test'
    Crawler::LOGGER.outputters = Log4r::StdoutOutputter.new('stdout')
  when 'prod'
    Crawler::LOGGER.outputters = Log4r::FileOutputter.new('file', :filename => 'test.log') #append to existing log
end
#...
Crawler::Runner.new(1)
Crawler::Runner.new(2)

结果:

 INFO Runner 1: Created instance for Crawler::Runner with id 1
 INFO Runner 2: Created instance for Crawler::Runner with id 2

(a) 记录器名称 likeA::B具有 nameB并且是具有 name 的记录器的子级A。据我所知,这不是对象继承。

这种方法的一个优点是:如果您想为每个类使用一个记录器,您只需要更改记录器的名称。

于 2015-11-10T22:03:57.160 回答