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