Ruby - делить экземпляр логгера между модулем / классами - PullRequest
46 голосов
/ 27 мая 2009

Работа над небольшим скриптом на 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, а затем переключился бы на файл, если я в работе. Я где-то видел предложение об изменении глобальной переменной $ stdout в Ruby, которая перенаправляла бы вывод куда-то, кроме STDOUT, но это выглядит довольно глупо.

Спасибо!

Ответы [ 9 ]

97 голосов
/ 21 июля 2011

Мне нравится иметь метод 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 теперь регистрирует сообщения под своим именем класса, и не нужно менять один бит:)

22 голосов
/ 28 мая 2009

С выложенным дизайном, похоже, самое простое решение - дать 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

Он делает то же самое.

12 голосов
/ 07 мая 2014

Как указывает Зенагрей, ведение журнала из методов класса было исключено из ответа Джейкоба. Небольшое дополнение решает, что:

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

Обратите внимание, что первые два идентификатора одинаковы, но отличаются от двух вторых идентификаторов, показывая, что у нас есть два экземпляра - по одному для каждого класса.

4 голосов
/ 03 июня 2017

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

2 голосов
/ 28 мая 2009

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

Спасибо за помощь, ребята!

2 голосов
/ 28 мая 2009

Может быть какая-то странная магия Ruby, которая может позволить вам избежать этого, но есть довольно простое решение, которое не нуждается в странных. Просто вставьте регистратор в модуль и получите к нему доступ напрямую, используя механизм для его настройки. Если вы хотите быть крутым об этом, определите «ленивый регистратор», который сохраняет флаг, чтобы сказать, есть ли у него регистратор, и либо молча отбрасывает сообщения до тех пор, пока регистратор не установлен, выдает исключение чего-либо зарегистрированного, прежде чем регистратор будет установить или добавить сообщение журнала в список, чтобы его можно было зарегистрировать после определения регистратора.

1 голос
/ 16 октября 2009

Как насчет того, чтобы обернуть регистратор в одиночный файл, чтобы получить к нему доступ, используя MyLogger.instance

0 голосов
/ 18 ноября 2017

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

Основываясь на ответе Джейкоба, я бы предложил модуль, который вы можете добавлять по мере необходимости.

Моя версия такова:

# 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

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

0 голосов
/ 11 ноября 2015

На основании вашего комментария

Всего этого можно было бы избежать, если бы вы могли просто динамически изменять расположение вывода уже созданного экземпляра 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) Имя регистратора, например A::B, имеет имя B и является дочерним по отношению к регистратору с именем A. Насколько я знаю, это не наследование объектов.

Одно из преимуществ этого подхода: если вы хотите использовать один регистратор для каждого класса, вам нужно только изменить имя регистратора.

...