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!
Although an old question, I thought it worthwhile to document a different approach.
Building on Jacob's answer, I would suggest a module that you can add in as and when needed.
My version is this:
I have this saved into a library of handy modules, and I would use it like this:
I find this a lot easier and more versatile than other options I've looked at so far, so I hope it helps you with yours.
Inspired by this thread I created the easy_logging gem.
It combines all the features discussed such as:
Installation:
Usage:
Output
More details on GitHub.
How about wrapping the logger in a singleton then you could access it using MyLogger.instance
As Zenagray points out, logging from class methods was left out of Jacob's answer. A small addition solves that:
The intended use is via "include":
Produces:
Note the id of the logger is the same in all four cases. If you want a different instance for each class, then don't use
Logging.logger
, rather useself.class.logger
:The same program now produces:
Note that the first two id's are the same but are different from the 2nd two ids showing that we have two instances -- one for each class.
The may be some weird Ruby magic that could let you avoid it, but there's a fairly simple solution that doesn't need weird. Just put the logger into the module and access it directly, with a mechanism to set it. If you want to be cool about it, define a "lazy logger" that keeps a flag to say if it has a logger yet, and either silently drops messages until the logger is set, throws an exception of something is logged before the logger is set, or adds the log message to a list so it can be logged once the logger is defined.
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:Then, in your classes:
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:Your
Widget
now logs messages with its classname, and didn't need to change one bit :)