少し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以外の場所に出力がリダイレクトされるという提案がどこかで見られましたが、これはかなりハックに思えます。
ありがとう!
レイアウトしたデザインで、最も簡単な解決策は、クローラーにモジュール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
まったく同じことを行います。
クラスで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
はメッセージをそのクラス名でログに記録するようになり、少し変更する必要がなくなりました:)
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つ)があることを示しています。
このスレッドに触発されて、私は easy_logging gemを作成しました。
次のようなすべての機能を組み合わせています。
インストール:
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 の詳細。
いくつかの奇妙なRuby魔法を使えばそれを回避できるかもしれませんが、奇妙なことを必要としないかなり簡単な解決策があります。ロガーをモジュールに挿入して直接アクセスするだけで、それを設定するメカニズム。それについてクールになりたい場合は、ロガーがまだあるかどうかを示すフラグを保持する「レイジーロガー」を定義し、ロガーが設定されるまでメッセージをサイレントにドロップし、何かの例外をスローします。ロガーを設定する前にログに記録するか、ログメッセージをリストに追加して、ロガーを定義するとログに記録できるようにします。
これがどのように機能するかを示すコードの小さな塊。私は単に新しい基本オブジェクトを作成しているので、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
助けてくれてありがとう!
ロガーをシングルトンでラップすると、MyLogger.instanceを使用してロガーにアクセスできますか?
あなたのコメントに基づいて
すでにインスタンス化されているロガーの出力場所を動的に変更できれば(ログレベルの変更方法と同様に)、これらすべてを回避できます。
デフォルトのロガーに制限されていない場合は、別の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つ:各クラスに単一のロガーを使用する場合は、ロガーの名前を変更するだけで済みます。
古い質問ですが、別のアプローチを文書化する価値があると思いました。
ジェイコブの答えに基づいて、必要に応じて追加できるモジュールを提案します。
私のバージョンはこれです:
# 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
これは、これまでに見てきた他のオプションよりもはるかに簡単で用途が広いと思うので、あなたの助けになることを願っています。