Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby - share logger instance among module/classes

Working on a little Ruby script that goes out to the web and crawls various services. I've got a module with several classes inside:

module Crawler   class Runner   class Options   class Engine end 

I want to share one logger among all those of those classes. Normally I'd just put this in a constant in the module and reference it like so:

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

The problem is that I can't create my logger instance until I know where the output is going. You start the crawler via command line and at that point you can tell it you want to run in development (log output goes to STDOUT) or production (log output goes to a file, crawler.log):

crawler --environment=production 

I have a class Options that parses the options passed in through the command line. Only at that point do I know how to instantiate the logger with the correct output location.

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

I could pass my logger instance to each new() call for every class instance I create, but I know there has to be a better, Rubyish way to do it. I'm imagining some weird class variable on the module that shared with class << self or some other magic. :)

A little more detail: Runner starts everything by passing the command line options to the Options class and gets back an object with a couple of 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 the code in Engine to be able to access the logger object (along with a couple more classes that are initialized inside Engine). Help!

All of this could be avoided if you could just dynamically change the output location of an already-instantiated Logger (similar to how you change the log level). I'd instantiate it to STDOUT and then change over to a file if I'm in production. I did see a suggestion somewhere about changing Ruby's $stdout global variable, which would redirect output somewhere other than STDOUT, but this seems pretty hacky.

Thanks!

like image 790
Rob Cameron Avatar asked May 27 '09 19:05

Rob Cameron


Video Answer


2 Answers

I like to have a logger method available in my classes, but I don't like sprinkling @logger = Logging.logger in all my initializers. Usually, I 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 

Because the Logging#logger method can access the instance that the module is mixed into, it is trivial to extend your logging module to record the classname 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 

Your Widget now logs messages with its classname, and didn't need to change one bit :)

like image 189
Jacob Avatar answered Sep 22 '22 05:09

Jacob


With the design you've laid out, it looks like the easiest solution is to give Crawler a module method that returns a module ivar.

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

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

module Crawler   class <<self     attr_accessor :logger   end end 

It does the exact same thing.

like image 44
Chuck Avatar answered Sep 20 '22 05:09

Chuck