Ruby-общий экземпляр регистратора среди модулей / классов
Работа над небольшим скриптом Ruby, который выходит в сеть и обходит различные службы. У меня есть модуль с несколькими классами внутри:
module Crawler
class Runner
class Options
class Engine
end
Я хочу разделить один регистратор среди всех этих классов. Обычно я просто помещаю это в константу в модуле и ссылаюсь на нее следующим образом:
Crawler::LOGGER.info("Hello, world")
Проблема в том, что я не могу создать свой экземпляр logger, пока не узнаю, куда идет вывод. Вы запускаете искатель через командную строку, и в этот момент Вы можете сказать ему, что вы хотите работать в разработке (выход журнала идет в stdout) или производства (выпуск журнала выходит в файл, гусеничные.журнал):
crawler --environment=production
У меня есть класс Options
, который анализирует параметры, передаваемые через командную строку. Только в этот момент я знаю, как создать экземпляр регистратора с правильным местоположением вывода.
Итак, мой вопрос: как / где я могу поместить свой объект logger, чтобы все мои классы имели к нему доступ?
Я мог бы передать экземпляр logger каждому вызову new()
для каждого класса пример я создаю, но я знаю, что должен быть лучший, рубиновый способ сделать это. Я представляю себе какую-то странную переменную класса в модуле, который поделился с 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
, чтобы иметь доступ к объекту logger (наряду с несколькими другими классами, которые инициализируются внутри Engine
). Помогите!
Всего этого можно было бы избежать, если бы вы могли просто динамически изменять выходное местоположение уже созданного регистратора (аналогично тому, как вы изменяете уровень журнала). Я бы скопировал его в 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
Предполагаемое использование-через "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
Примечание идентификатор регистратора одинаков во всех четырех случаях. Если вы хотите иметь разные экземпляры для каждого класса, то не используйте
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-й два идентификатора, показывающие, что у нас есть два экземпляра-по одному для каждого класса.
Вдохновленный этой нитью, я создалeasy_logging gem.
Он сочетает в себе все рассмотренные особенности, такие как:
- добавляет функциональность ведения журнала в любом месте с одним, коротким, самоописательная команда
- 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
Спасибо за помощь, ребята!
Как насчет упаковки регистратора в синглет, тогда вы можете получить к нему доступ с помощью MyLogger.Пример
Основываясь на вашем Комментарии
Всего этого можно было бы избежать, если бы вы могли просто динамически изменять выходное местоположение уже созданного регистратора (аналогично тому, как вы изменяете уровень журнала).
Если вы не ограничены регистратором по умолчанию, вы можете использовать другой 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
. Насколько мне известно, это не наследование объектов.Одно из преимуществ этого подхода: Если вы хотите использовать один регистратор для каждого класса, вам нужно только изменить имя регистратора.
Хотя это старый вопрос, я подумал, что стоит документировать другой подход.
Основываясь на ответе Джейкоба, я бы предложил модуль, который вы можете добавить по мере необходимости.Моя версия такова:
# 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