Ruby - instance of share instance among modules / classes

Work with a small Ruby script that goes online and scans various services. I have a module with several classes inside:

module Crawler class Runner class Options class Engine end 

I want to share one registrar among all these classes. Usually I just put this in a constant in the module and referenced like this:

 Crawler::LOGGER.info("Hello, world") 

The problem is that I can’t create my own copy of the magazine until I find out where the exit goes. You start the scanner through the command line, and at this point you can say that you want to run it in development (the output of the log goes to STDOUT) or to production (the output of the log goes to the file, crawler.log):

 crawler --environment=production 

I have an Options class that parses parameters passed through the command line. Only at this moment I know how to create a registrar instance with the correct output location.

So my question is: how / where do I put my logger object so that all my classes have access to it?

I can pass my log instance for every call to new() for every instance of the class I create, but I know that there must be a better, Rubyish way to do this. I imagine some weird class variable in a module that shares with class << self or some other magic. :)

A bit more detailed information: Runner starts everything by passing command-line options to the Options class and returns an object with several instance variables:

 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 

I need code in Engine to have access to the log object (along with a few more classes initialized inside Engine ). Help!

All this could have been avoided if you could just dynamically change the output location of the already created Logger (similar to how you change the log level). I would create it in STDOUT and then go to the file if I am in production. I saw somewhere a proposal to change the global variable Ruby $ stdout, which would redirect the output somewhere other than STDOUT, but it seems pretty hacked.

Thank!

+41
ruby logging class-design
May 27 '09 at 19:23
source share
9 answers

With the design you posted, it seems like the easiest solution is to provide Crawler with a modular method that returns the ivar module.

 module Crawler def self.logger @logger end def self.logger=(logger) @logger = logger end end 

Or you could use " class <<self magic" if you want:

 module Crawler class <<self attr_accessor :logger end end 

He does the same.

+20
May 27 '09 at 21:25
source share

I like to have the logger method available in my classes, but I don't like splashing @logger = Logging.logger in all of my initializers. I usually do this:

 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 

Then in your classes:

 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 

Since the Logging#logger method can access the instance into which the module is connected, it is trivial to expand the registration module to record the class name with log messages:

 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 

Now your Widget logs messages with its class name and you do not need to change one bit :)

+89
Jul 20 2018-11-11T00:
source share

As Zenagray points out, the entry from the class methods was excluded from Jacob's answer. A small addition decides that:

 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 

Intended use via "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 

It produces:

 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 

Note that the log id is the same in all four cases. If you need a different instance for each class, then do not use Logging.logger , rather use 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 

The same program now produces:

 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 

Please note that the first two id are the same, but different from the two two identifiers, indicating that we have two instances - one for each class.

+9
May 7 '14 at 3:40
source share

There may be some kind of weird Ruby magic that can allow you to avoid this, but there is a pretty simple solution that doesn't need a fantastic one. Just put the registrar in the module and get access to it directly, using the mechanism of its installation. If you want to be cool in this matter, define a "lazy logger" that holds the flag to say that it still has a logger, and either silently discards messages until a logger is installed and throws an exception if something It is registered before the registrar installs or adds a journal message to the list so that it can be registered after the registrar is defined.

+2
May 27 '09 at 20:08
source share

A bit of code snippet to demonstrate how this works. I just create a new base object so that I can notice that object_id remains unchanged during calls:

 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}" 

The output of this code looks like ( object_id of 4 means 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 

Thanks for helping the guys!

+2
May 27 '09 at 23:20
source share

How to wrap a logger in a singleton, then you can access it using MyLogger.instance

+1
Oct 16 '09 at 10:58
source share

Inspired by this thread, I created an easy_logging gem.

It combines all the features discussed, such as:

  • Adds logging features anywhere with one, self-descriptive command
  • Logger works in both classes and instances
  • Logger is class specific and contains the class name

Installation:

 gem install 'easy_logging 

Using:

 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 

Exit

 I, [2017-06-03T21:59:25.160686 #5900] INFO -- YourClass: something happened I, [2017-06-03T21:59:25.160686 #5900] INFO -- YourOtherClass: something happened 

Learn more about GitHub .

+1
Jun 03 '17 at 20:31 on
source share

Based on your comment

All this could be avoided if you could just dynamically change the output location of the already created Logger (similar to how you change the log level).

If you are not limited to the default logger, you can use a different log file.

As an example with 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 

In prod mode, the registration data is stored in a file (attached to an existing file, but there are options for creating new log files or implementing sliding log files).

Result:

  INFO main: Created instance for Crawler::Runner 

If you use the log4r (a) inheritance mechanism, you can define a logger for each class (or in my next example for each instance) and share the output.

Example:

 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) 

Result:

  INFO Runner 1: Created instance for Crawler::Runner with id 1 INFO Runner 2: Created instance for Crawler::Runner with id 2 

(a) The registrar name, for example A::B , has the name B and is a descendant of the registrar named A As far as I know, this is not object inheritance.

One of the advantages of this approach: if you want to use one registrar for each class, you only need to change the name of the registrar.

0
Nov 10 '15 at 22:03
source share

Although an old question, I thought it was worth documenting a different approach.

Based on Jacob's answer, I would suggest a module that you can add as needed.

My version is this:

 # 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 

I saved this in a library of handy modules, and I would use it as follows:

 #! /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 

I find it a lot simpler and more versatile than the other options I've looked at, so I hope this helps you.

0
Nov 18 '17 at 19:25
source share



All Articles