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 45

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
Я нахожу это намного проще и универсальнее, чем другие варианты, которые я рассматривал до сих пор, поэтому я надеюсь, что это поможет вам с вашим.