Search code examples
rubyloggingclass-design

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!


Solution

  • 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.