代码之家  ›  专栏  ›  技术社区  ›  Rob Cameron

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

  •  46
  • Rob Cameron  · 技术社区  · 16 年前

    正在编写一个小Ruby脚本,该脚本将发布到Web并爬行各种服务。我有一个模块,里面有几个类:

    module Crawler
      class Runner
      class Options
      class Engine
    end
    

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

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

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

    crawler --environment=production
    

    我有一节课 Options 它解析通过命令行传入的选项。只有在这一点上,我才知道如何用正确的输出位置实例化记录器。

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

    我可以将日志实例传递给每个 new() 调用我创建的每个类实例,但我知道必须有一种更好的、普通的方法来完成它。我想象在与共享的模块上有一些奇怪的类变量 class << self 或者其他魔法。:)

    再详细一点: Runner 通过将命令行选项传递给 选项 类并返回一个带有几个实例变量的对象:

    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 为了能够访问logger对象(以及一些在内部初始化的类) 发动机 )救命!

    如果可以动态地更改已经实例化的记录器的输出位置(类似于更改日志级别的方式),那么可以避免所有这些问题。我将它实例化为stdout,然后在生产中转换为一个文件。我确实在某个地方看到了一个关于更改Ruby的$stdout全局变量的建议,它会将输出重定向到stdout之外的其他地方,但这看起来很糟糕。

    谢谢!

    9 回复  |  直到 7 年前
        1
  •  22
  •   Chuck    16 年前

    根据您所设计的,看起来最简单的解决方案是给爬行器一个模块方法,它返回一个模块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
    

    它的作用完全相同。

        2
  •  97
  •   Jacob    13 年前

    我喜欢 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 现在用它的类名记录消息,不需要更改一位:)

        3
  •  11
  •   pedz    11 年前

    正如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不同,这两个ID显示我们有两个实例——每个类一个实例。

        4
  •  4
  •   thisismydesign    8 年前

    灵感来源于这条线,我创造了 easy_logging 宝石。

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

    • 在任意位置添加日志记录功能, 自我描述命令
    • 记录器在类和实例方法中都工作
    • 记录器特定于类并包含类名

    安装:

    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 .

        5
  •  2
  •   Charlie Martin    16 年前

    可能是一些奇怪的红宝石魔法,可以让你避免它,但有一个相当简单的解决方案,不需要奇怪。只需将记录器放入模块并直接访问它,通过一种机制设置它。如果你想对它保持冷静,定义一个“懒惰的记录器”,保持一个标志来表示它是否有一个记录器,或者悄悄地丢弃消息直到记录器被设置,在记录器被设置之前抛出一个异常的东西被记录,或者将日志消息添加到一个列表中,这样一旦记录器被定义,它就可以被记录。

        6
  •  2
  •   Rob Cameron    16 年前

    一小段代码来演示这是如何工作的。我只是创建一个新的基本对象,这样我就可以观察到对象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
    

    谢谢你们的帮助!

        7
  •  1
  •   Mike West    15 年前

    如何将记录器包装在一个单独的实例中,然后您可以使用mylogger.instance访问它

        8
  •  0
  •   knut    9 年前

    根据你的意见

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

    如果您不局限于默认的日志记录器,那么您可以使用另一个日志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 . 据我所知,这不是对象继承。

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

        9
  •  0
  •   Kitebuggy    7 年前

    虽然这是一个老问题,但我认为有必要记录下不同的方法。

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

    我的版本是:

    # 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
    

    我发现这比我目前看到的其他选择更容易和更通用,所以我希望它能帮助你解决你的问题。