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 :)
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With