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
Inspired by this thread I created the easy_logging gem.
It combines all the features discussed such as:
Installation:
gem install 'easy_logging
Usage:
require 'easy_logging'
class YourClass
include EasyLogging
def do_something
# ...
logger.info 'something happened'
end
end
class YourOtherClass
include EasyLogging
def self.do_something
# ...
logger.info 'something happened'
end
end
YourClass.new.do_something
YourOtherClass.do_something
Output
I, [2017-06-03T21:59:25.160686 #5900] INFO -- YourClass: something happened
I, [2017-06-03T21:59:25.160686 #5900] INFO -- YourOtherClass: something happened
More details on GitHub.
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.
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.
As Zenagray points out, logging from class methods was left out of Jacob's answer. A small addition solves that:
require 'logger'
module Logging
class << self
def logger
@logger ||= Logger.new($stdout)
end
def logger=(logger)
@logger = logger
end
end
# Addition
def self.included(base)
class << base
def logger
Logging.logger
end
end
end
def logger
Logging.logger
end
end
The intended use is via "include":
class Dog
include Logging
def self.bark
logger.debug "chirp"
puts "#{logger.__id__}"
end
def bark
logger.debug "grrr"
puts "#{logger.__id__}"
end
end
class Cat
include Logging
def self.bark
logger.debug "chirp"
puts "#{logger.__id__}"
end
def bark
logger.debug "grrr"
puts "#{logger.__id__}"
end
end
Dog.new.bark
Dog.bark
Cat.new.bark
Cat.bark
Produces:
D, [2014-05-06T22:27:33.991454 #2735] DEBUG -- : grrr
70319381806200
D, [2014-05-06T22:27:33.991531 #2735] DEBUG -- : chirp
70319381806200
D, [2014-05-06T22:27:33.991562 #2735] DEBUG -- : grrr
70319381806200
D, [2014-05-06T22:27:33.991588 #2735] DEBUG -- : chirp
70319381806200
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 use self.class.logger
:
require 'logger'
module Logging
def self.included(base)
class << base
def logger
@logger ||= Logger.new($stdout)
end
def logger=(logger)
@logger = logger
end
end
end
def logger
self.class.logger
end
end
The same program now produces:
D, [2014-05-06T22:36:07.709645 #2822] DEBUG -- : grrr
70350390296120
D, [2014-05-06T22:36:07.709723 #2822] DEBUG -- : chirp
70350390296120
D, [2014-05-06T22:36:07.709763 #2822] DEBUG -- : grrr
70350390295100
D, [2014-05-06T22:36:07.709791 #2822] DEBUG -- : chirp
70350390295100
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.
A little chunk of code to demonstrate how this works. I'm simply creating a new basic Object so that I can observe that the object_id remains the same throughout the calls:
module M
class << self
attr_accessor :logger
end
@logger = nil
class C
def initialize
puts "C.initialize, before setting M.logger: #{M.logger.object_id}"
M.logger = Object.new
puts "C.initialize, after setting M.logger: #{M.logger.object_id}"
@base = D.new
end
end
class D
def initialize
puts "D.initialize M.logger: #{M.logger.object_id}"
end
end
end
puts "M.logger (before C.new): #{M.logger.object_id}"
engine = M::C.new
puts "M.logger (after C.new): #{M.logger.object_id}"
The output of this code looks like (an object_id
of 4 means nil
):
M.logger (before C.new): 4
C.initialize, before setting M.logger: 4
C.initialize, after setting M.logger: 59360
D.initialize M.logger: 59360
M.logger (after C.new): 59360
Thanks for the help guys!
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:
# saved into lib/my_log.rb
require 'logger'
module MyLog
def self.logger
if @logger.nil?
@logger = Logger.new( STDERR)
@logger.datetime_format = "%H:%M:%S "
end
@logger
end
def self.logger=( logger)
@logger = logger
end
levels = %w(debug info warn error fatal)
levels.each do |level|
define_method( "#{level.to_sym}") do |msg|
self.logger.send( level, msg)
end
end
end
include MyLog
I have this saved into a library of handy modules, and I would use it like this:
#! /usr/bin/env ruby
#
require_relative '../lib/my_log.rb'
MyLog.debug "hi"
# => D, [19:19:32 #31112] DEBUG -- : hi
MyLog.warn "ho"
# => W, [19:20:14 #31112] WARN -- : ho
MyLog.logger.level = Logger::INFO
MyLog.logger = Logger.new( 'logfile.log')
MyLog.debug 'huh'
# => no output, sent to logfile.log instead
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.