Ruby - share logger instance among module/classes
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.
Share global logger among module/classes
Add a constant in the module:
module Foo
Logger = Logger.new
class A
class B
class C
...
class Z
end
Then you can do Logger.log('blah')
in your classes. Since we're shadowing the global constant Logger
with Foo::Logger
, this means that if you want to refer to the Logger
class within the Foo
module, you have to use the scope resolution: ::Logger
.
How to define a common module logging shared among others module
Just after require you can add this piece of code
$FILE_LOG = Logging.create_log(File.expand_path('LoggingFile.log'), Logger::DEBUG)
Explanation of the above line : It is calling a function in Logging Module, to create File , Level of Logging is debug.
Below is the piece of code for Module
module Logging
def self.create_log(output_location level)
log = Logger.new(output_location, 'weekly').tap do |l|
next unless l
l.level = level
l.progname = File.basename($0)
l.datetime_format = DateTime.iso8601
l.formatter = proc { |severity, datetime, progname, msg| "#{severity}: #{datetime} - (#{progname}) : #{msg}\n" }
end
log.level = level if level < log.level
log
end
def self.log(msg, level)
# Here I am just logging only FATAL and DEBUG, similarly you can add in different level of logs
if level == :FATAL
$FILE_LOG.fatal(msg)
elsif level == :DEBUG
$FILE_LOG.debug(msg)
end
end
end
Then in Every method every Ruby file, we can use this logging as follows
Logging.log("Message",:FATAL)
Logger default for class and new file for subclasses
You tell ruby to do exactly what you need. But beware, multiple loggers on the same file will not play nice with each other, so you'd better set the logger on the class object.
class ModelClass
def logger
self.class.logger
end
def self.logger
@logger ||= begin
target = self == ModelClass ? STDOUT : "#{name}.log"
Logger.new(target)
end
end
end
You will have one logger per class, and all of them will go to files except the base class, which will go to STDOUT.
But why being so complex? You could just write all of them to files, and then tail -f YourFile.log
to see the log you want on stdout.
Ruby: partially share initialization logic using Module
super
works just fine with modules. Use **
to ignore additional keyword parameters.
module Being
def initialize(age: , name: , **)
@age = age.to_i
@name = name.to_sym
end
end
class Person
include Being
def initialize(height:, **)
super
@height = height
end
end
class Dog
include Being
def initialize(breed: , **)
super
@breed = breed
end
end
#<Dog:0x00007fb0fe80f7f8 @age=6, @name=:"Good Boy", @breed="Good Dog">
#<Person:0x00007fb0fe80f2a8 @age=42, @name=:Bront, @height="6' 2\"">
p Dog.new(age: 6, name: "Good Boy", breed: "Good Dog")
p Person.new(age: 42, name: "Bront", height: %q{6' 2"})
You can get yourself into some trouble mixing super
with Modules because it's not always clear which ancestor method super
will call. You can check your full inheritance tree with Module#ancestors
. This includes Classes because all Classes are Modules.
# [Dog, Being, Object, Kernel, BasicObject]
# [Person, Being, Object, Kernel, BasicObject]
p Dog.ancestors
p Person.ancestors
To avoid this, use composition. Compose your class of several different objects and delegate method calls to them. In this case, have a Being object and delegate method calls to it. We'll use Forwardable
to forward method calls to a Being object.
require 'forwardable'
class Being
attr_accessor :age, :name
def initialize(age:, name:)
@age = age.to_i
@name = name.to_sym
end
def greeting
"Hello, my name is #{name} and I am #{age} years old."
end
end
class Person
extend Forwardable
def_delegators :@being, :greeting
def initialize(height:, **args)
@being = Being.new(**args)
@height = height
end
def to_s
self
end
end
class Dog
extend Forwardable
def_delegators :@being, :greeting
def initialize(breed:, **args)
@being = Being.new(**args)
@breed = breed
end
def to_s
self
end
end
#<Dog:0x00007fb87702c060 @being=#<Being:0x00007fb87702e400 @age=6, @name=:"Good Boy">, @breed="Good Dog">
#<Person:0x00007fb87a02f870 @being=#<Being:0x00007fb87a02f7f8 @age=42, @name=:Bront>, @height="6' 2\"">
p dog = Dog.new(age: 6, name: "Good Boy", breed: "Good Dog")
p person = Person.new(age: 42, name: "Bront", height: %q{6' 2"})
# Hello, my name is Good Boy and I am 6 years old.
# Hello, my name is Bront and I am 42 years old.
puts dog.greeting
puts person.greeting
# [Dog, Being, Object, Kernel, BasicObject]
# [Person, Being, Object, Kernel, BasicObject]
p Dog.ancestors
p Person.ancestors
def_delegators :@being, :greeting
says that when greeting
is called, call @being.greeting
instead.
Inheritance is easy, but it can lead to hard to find complications. Composition takes a bit more work, but it is more obvious what is happening, and it allows for more flexible classes. You can swap out what is being delegated to.
For example, say you need to fetch things off the web. You could inherit from Net::HTTP. Or you could delegate to a Net::HTTP object. Then in testing you can replace the Net::HTTP object with one that does dummy network calls.
Related Topics
Why Should We Avoid Using Class Variables @@ in Rails
Rails: Skinny Controller Vs. Fat Model, or Should I Make My Controller Anorexic
Is There a "Phpmyadmin" for Ruby on Rails
Passing Parameters from View to Controller
Why Won't Heroku Accept My Gemfile.Lock in Windows
Can't Access Rubygems - Possibly Due to Ssl
Read and Write Yaml Files Without Destroying Anchors and Aliases
Can't Install Ruby Rvm on Ubuntu 16.04 Due to Gpg Bug
What's the Precedence of Ruby'S Method Call
How to Use the Debugger with Ruby 2.0
How to Switch to Ruby 1.9.3 Installed Using Homebrew
How to Get the Name of a Ruby Class
How to Run Untrusted Ruby Code Inside a Safe Sandbox