Подтвердить что ты не робот

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, но это кажется довольно взломанным.

Спасибо!

4b9b3361

Ответ 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

Я нахожу это намного проще и универсальнее, чем другие варианты, на которые я смотрел, поэтому я надеюсь, что это поможет вам с вашей стороны.