web-dev-qa-db-ja.com

Ruby-モジュール/クラス間でロガーインスタンスを共有する

少しRuby Webに出てさまざまなサービスをクロールするスクリプトに取り組んでいます。内部にいくつかのクラスを持つモジュールがあります。

_module Crawler
  class Runner
  class Options
  class Engine
end
_

これらのクラスのすべてで1つのロガーを共有したいと思います。通常、これをモジュールの定数に入れて、次のように参照します。

_Crawler::LOGGER.info("Hello, world")
_

問題は、出力先がわかるまでロガーインスタンスを作成できないことです。コマンドラインを介してクローラーを起動し、その時点で、開発(ログ出力はSTDOUTに出力)または本番(ログ出力はcrawler.logファイルに出力)で実行することを指定できます。

_crawler --environment=production
_

コマンドラインから渡されたオプションを解析するクラスOptionsがあります。その時点で初めて、正しい出力場所でロガーをインスタンス化する方法がわかります。

だから、私の質問は:すべてのクラスがそれにアクセスできるように、ロガーオブジェクトをどのように/どこに置くか?

ロガーインスタンスを、作成するすべてのクラスインスタンスの各new()呼び出しに渡すことができますが、それを行うにはより良いRubyの方法が必要であることはわかっています。 _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内で初期化されるいくつかのクラスとともに)。助けて!

すでにインスタンス化されているロガーの出力場所を動的に変更できれば(ログレベルの変更方法と同様に)、これらすべてを回避できます。 STDOUTにインスタンス化してから、運用環境にある場合はファイルに切り替えます。 Rubyの$ stdoutグローバル変数を変更すると、STDOUT以外の場所に出力がリダイレクトされるという提案がどこかで見られましたが、これはかなりハックに思えます。

ありがとう!

46
Rob Cameron

レイアウトしたデザインで、最も簡単な解決策は、クローラーにモジュール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

まったく同じことを行います。

22
Chuck

クラスで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はメッセージをそのクラス名でログに記録するようになり、少し変更する必要がなくなりました:)

97
Jacob

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

使用目的は「include」を介したものです。

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は4つのケースすべてで同じであることに注意してください。クラスごとに異なるインスタンスが必要な場合は、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

最初の2つのIDは同じですが、2つ目の2つのIDとは異なり、2つのインスタンス(クラスごとに1つ)があることを示しています。

12
pedz

このスレッドに触発されて、私は easy_logging gemを作成しました。

次のようなすべての機能を組み合わせています。

  • 1つの短い自己記述的なコマ​​ンドで、どこにでもロギング機能を追加します
  • ロガーはクラスメソッドとインスタンスメソッドの両方で動作します
  • ロガーはクラスに固有であり、クラス名を含みます

インストール:

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
thisismydesign

いくつかの奇妙なRuby魔法を使えばそれを回避できるかもしれませんが、奇妙なことを必要としないかなり簡単な解決策があります。ロガーをモジュールに挿入して直接アクセスするだけで、それを設定するメカニズム。それについてクールになりたい場合は、ロガーがまだあるかどうかを示すフラグを保持する「レイジーロガー」を定義し、ロガーが設定されるまでメッセージをサイレントにドロップし、何かの例外をスローします。ロガーを設定する前にログに記録するか、ログメッセージをリストに追加して、ロガーを定義するとログに記録できるようにします。

2
Charlie Martin

これがどのように機能するかを示すコードの小さな塊。私は単に新しい基本オブジェクトを作成しているので、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}"

このコードの出力は(an object_id of 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

助けてくれてありがとう!

2
Rob Cameron

ロガーをシングルトンでラップすると、MyLogger.instanceを使用してロガーにアクセスできますか?

1
Mike West

あなたのコメントに基づいて

すでにインスタンス化されているロガーの出力場所を動的に変更できれば(ログレベルの変更方法と同様に)、これらすべてを回避できます。

デフォルトのロガーに制限されていない場合は、別の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という名前のロガーの子です。私の知る限り、これはオブジェクトの継承ではありません。

このアプローチの利点の1つ:各クラスに単一のロガーを使用する場合は、ロガーの名前を変更するだけで済みます。

0
knut

古い質問ですが、別のアプローチを文書化する価値があると思いました。

ジェイコブの答えに基づいて、必要に応じて追加できるモジュールを提案します。

私のバージョンはこれです:

# 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

これは、これまでに見てきた他のオプションよりもはるかに簡単で用途が広いと思うので、あなたの助けになることを願っています。

0
Kitebuggy