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!
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 :)