Ruby - делить экземпляр логгера между модулем / классами
Работает над небольшим скриптом 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, но это выглядит довольно глупо.
Спасибо!
9 ответов
С выложенным дизайном, похоже, самое простое решение - дать 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
Это делает то же самое.
Мне нравится иметь 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
теперь регистрирует сообщения со своим именем класса, и не нужно менять один бит:)
Как указывает Зенагрей, запись из методов класса была исключена из ответа Джейкоба. Небольшое дополнение решает, что:
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
Обратите внимание, что идентификатор логгера одинаков во всех четырех случаях. Если вы хотите разные экземпляры для каждого класса, не используйте 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
Обратите внимание, что первые два идентификатора одинаковы, но отличаются от двух вторых идентификаторов, показывающих, что у нас есть два экземпляра - по одному для каждого класса.
Вдохновленный этой темой, я создал гем easy_logging.
Он сочетает в себе все обсуждаемые функции, такие как:
- Добавляет функциональность ведения журнала в любом месте с помощью одной короткой самоописательной команды
- 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.
Небольшой кусок кода, чтобы продемонстрировать, как это работает. Я просто создаю новый базовый объект, чтобы я мог наблюдать, что 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
Спасибо за помощь, ребята!
Это может быть какая-то странная магия Ruby, которая может позволить вам избежать этого, но есть довольно простое решение, которое не нуждается в странных. Просто вставьте регистратор в модуль и получите к нему доступ напрямую, используя механизм для его настройки. Если вы хотите быть крутым в этом, определите "ленивый регистратор", который сохраняет флаг, чтобы сказать, есть ли у него регистратор, и либо молча отбрасывает сообщения до тех пор, пока регистратор не установлен, выдает исключение чего-то зарегистрированного, прежде чем регистратор будет установить или добавить сообщение журнала в список, чтобы его можно было зарегистрировать после определения регистратора.
Как насчет того, чтобы обернуть регистратор в одиночку, тогда вы можете получить к нему доступ, используя MyLogger.instance
Хотя старый вопрос, я подумал, что стоит документировать другой подход.
Основываясь на ответе Джейкоба, я бы предложил модуль, который вы можете добавлять по мере необходимости.
Моя версия такова:
# 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
Я считаю, что это намного проще и более универсально, чем другие варианты, на которые я смотрел до сих пор, поэтому я надеюсь, что это поможет вам в вашей.
На основании вашего комментария
Всего этого можно было бы избежать, если бы вы могли просто динамически изменять местоположение вывода уже созданного экземпляра 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::B
имеет имя B
и является потомком регистратора с именем A
, Насколько я знаю, это не наследование объектов.
Одно из преимуществ этого подхода: если вы хотите использовать один регистратор для каждого класса, вам нужно только изменить имя регистратора.