Ruby - 在模块/类之间共享记录器实例

时间:2009-05-27 19:23:37

标签: ruby logging class-design

使用一个小的Ruby脚本,该脚本可以访问Web并抓取各种服务。我有一个包含几个类的模块:

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之外的某个地方,但这看起来很糟糕。

谢谢!

9 个答案:

答案 0 :(得分:97)

我希望在我的课程中使用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现在使用其类名记录消息,并且不需要更改一位:)

答案 1 :(得分:22)

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

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

如果您愿意,也可以使用“class <<self magic”:

module Crawler
  class <<self
    attr_accessor :logger
  end
end

完全相同。

答案 2 :(得分:11)

正如Zenagray所指出的那样,从雅各布的答案中遗漏出来的课堂方法。一个小小的补充解决了:

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

预期用途是通过&#34; include&#34;:

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不同,表明我们有两个实例 - 每个类一个。

答案 3 :(得分:4)

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

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

  • 在任何地方添加日志记录功能,简短, 自我描述性命令
  • Logger适用于类和实例方法
  • 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的更多详情。

答案 4 :(得分:2)

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

答案 5 :(得分: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_id为4表示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

感谢帮助人员!

答案 6 :(得分:1)

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

访问它

答案 7 :(得分: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)像A::B这样的记录器名称的名称为B,并且是名为A的记录器的子项。据我所知,这不是对象继承。

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

答案 8 :(得分:0)

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

根据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

我发现这比我到目前为止所看到的其他选项更容易,更通用,所以我希望它可以帮助你。