Tagged Logging with Class and method - ruby

I want to log using the Ruby Logger but with the classes and methods that are in the call stack.
Things I have tried:
ActiveSupport::TaggedLogger
Kernel#caller and Kernel#caller_locations - only seems to give me the file and line number
TracePoint
Has anyone done something like this or have different ways to use the above? I was thinking there might be something with Binding that might have some information.
Example scenario:
logger = Logger.new(STDOUT)
class A
def initialize
#b = B.new
end
def call_b
b.meth_name('hello world')
end
end
class B
def meth_name(val)
logger.log_with_classes val #made up method name
end
end
A.new.call_b
"[A#call_b][B#meth_name] hello world"
=> nil

Related

Carry logger instance between classes [duplicate]

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.
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.
Inspired by this thread I created the easy_logging gem.
It combines all the features discussed such as:
Adds logging functionality anywhere with one, short,
self-descriptive command
Logger works in both class and instance methods
Logger is specific to class and contains class name
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.
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!
How about wrapping the logger in a singleton then you could access it using MyLogger.instance
Based on your comment
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).
If you are not restricted to the default logger you may use another log-gem.
As an example with log4r:
require 'log4r'
module Crawler
LOGGER = Log4r::Logger.new('mylog')
class Runner
def initialize
LOGGER.info('Created instance for %s' % self.class)
end
end
end
ARGV << 'test' #testcode
#...
case ARGV.first
when 'test'
Crawler::LOGGER.outputters = Log4r::StdoutOutputter.new('stdout')
when 'prod'
Crawler::LOGGER.outputters = Log4r::FileOutputter.new('file', :filename => 'test.log') #append to existing log
end
#...
Crawler::Runner.new
In prod mode the logging data are stored in a file (attached to existing file, but there are options to create new log files or implement rolling log files).
The result:
INFO main: Created instance for Crawler::Runner
If you use the inheritance mechanism of log4r (a), you may define a logger for each class (or in my following example for each instance) and share the outputter.
The example:
require 'log4r'
module Crawler
LOGGER = Log4r::Logger.new('mylog')
class Runner
def initialize(id)
#log = Log4r::Logger.new('%s::%s %s' % [LOGGER.fullname,self.class,id])
#log.info('Created instance for %s with id %s' % [self.class, id])
end
end
end
ARGV << 'test' #testcode
#...
case ARGV.first
when 'test'
Crawler::LOGGER.outputters = Log4r::StdoutOutputter.new('stdout')
when 'prod'
Crawler::LOGGER.outputters = Log4r::FileOutputter.new('file', :filename => 'test.log') #append to existing log
end
#...
Crawler::Runner.new(1)
Crawler::Runner.new(2)
The result:
INFO Runner 1: Created instance for Crawler::Runner with id 1
INFO Runner 2: Created instance for Crawler::Runner with id 2
(a) A logger name like A::B has the name B and is a child of the logger with the name A. As far as I know this is no object inheritance.
One advantage of this approach: If you want to use a single logger for each class, you need only to change the name of the logger.
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.

Understanding ruby TOPLEVEL_BINDING

I understand that TOPLEVEL_BINDING is the Binding object for main. The following code confirms it:
def name
:outer
end
module Test
class Binder
def self.name
:inner
end
def self.test_it
eval 'name', TOPLEVEL_BINDING
end
end
end
p Test::Binder.test_it # => :outer
I got confused while looking at the source for rack. The problem was in understanding this code in the file lib/rack/builder.rb
def self.new_from_string(builder_script, file="(rackup)")
eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
TOPLEVEL_BINDING, file, 0
end
def run(app)
end
The new_from_string method is passed the contents of a config.ru file which will be something like
run DemoApp::Application
Here it seems like TOPLEVEL_BINDING is referring to a Builder object, since the method run is defined for Builder but not for Object. However the earlier experiment establishes that TOPLEVEL_BINDING refers to main's binding. I do not understand how run method is working here. Please help me in understanding this code.
TOPLEVEL_BINDING is the top level binding.
That method is passing the string "run ..." into Builder.new { run ... }
Builder.new then does an instance_eval (https://github.com/rack/rack/blob/df1506b0825a096514fcb3821563bf9e8fd52743/lib/rack/builder.rb#L53-L55) on the block, thereby giving the code inside the block direct access to the instance's methods.
def initialize(default_app = nil,&block)
#use, #map, #run, #warmup = [], nil, default_app, nil
instance_eval(&block) if block_given?
end
run is an instance method of the Builder class, defined here -> https://github.com/rack/rack/blob/df1506b0825a096514fcb3821563bf9e8fd52743/lib/rack/builder.rb#L103-L105
def run(app)
#run = app
end
In short, "run DemoApp::Application" becomes:
Rack::Builder.new {
run DemoApp::Application
}.to_app
Edit: A simple example to illustrate the point:
class Builder
def initialize(&block)
instance_eval(&block)
end
def run(what)
puts "Running #{what}"
end
end
TOPLEVEL_BINDING.eval "Builder.new { run 10 }"
prints
Running 10
TOPLEVEL_BINDING gives you access to context in which the first file was executed:
a.rb:
a = 1
p TOPLEVEL_BINDING.local_variables #=> [:a]
require_relative 'b'
b.rb:
b = 1
p TOPLEVEL_BINDING.local_variables #=> [:a]
$ ruby a.rb
What rack developers are apparently trying to achieve is to isolate the builder line/script from all what is accessible from Rack::Builder.new_from_string (local variables, methods, you name it).
There's also a comment there these days:
# Evaluate the given +builder_script+ string in the context of
# a Rack::Builder block, returning a Rack application.
def self.new_from_string(builder_script, file = "(rackup)")
# We want to build a variant of TOPLEVEL_BINDING with self as a Rack::Builder instance.
# We cannot use instance_eval(String) as that would resolve constants differently.
binding, builder = TOPLEVEL_BINDING.eval('Rack::Builder.new.instance_eval { [binding, self] }')
eval builder_script, binding, file
return builder.to_app
end

How can I have ruby logger log output to stdout as well as file?

Someting like a tee functionality in logger.
You can write a pseudo IO class that will write to multiple IO objects. Something like:
class MultiIO
def initialize(*targets)
#targets = targets
end
def write(*args)
#targets.each {|t| t.write(*args)}
end
def close
#targets.each(&:close)
end
end
Then set that as your log file:
log_file = File.open("log/debug.log", "a")
Logger.new MultiIO.new(STDOUT, log_file)
Every time Logger calls puts on your MultiIO object, it will write to both STDOUT and your log file.
Edit: I went ahead and figured out the rest of the interface. A log device must respond to write and close (not puts). As long as MultiIO responds to those and proxies them to the real IO objects, this should work.
#David's solution is very good. I've made a generic delegator class for multiple targets based on his code.
require 'logger'
class MultiDelegator
def initialize(*targets)
#targets = targets
end
def self.delegate(*methods)
methods.each do |m|
define_method(m) do |*args|
#targets.map { |t| t.send(m, *args) }
end
end
self
end
class <<self
alias to new
end
end
log_file = File.open("debug.log", "a")
log = Logger.new MultiDelegator.delegate(:write, :close).to(STDOUT, log_file)
If you're in Rails 3 or 4, as this blog post points out, Rails 4 has this functionality built in. So you can do:
# config/environment/production.rb
file_logger = Logger.new(Rails.root.join("log/alternative-output.log"))
config.logger.extend(ActiveSupport::Logger.broadcast(file_logger))
Or if you're on Rails 3, you can backport it:
# config/initializers/alternative_output_log.rb
# backported from rails4
module ActiveSupport
class Logger < ::Logger
# Broadcasts logs to multiple loggers. Returns a module to be
# `extended`'ed into other logger instances.
def self.broadcast(logger)
Module.new do
define_method(:add) do |*args, &block|
logger.add(*args, &block)
super(*args, &block)
end
define_method(:<<) do |x|
logger << x
super(x)
end
define_method(:close) do
logger.close
super()
end
define_method(:progname=) do |name|
logger.progname = name
super(name)
end
define_method(:formatter=) do |formatter|
logger.formatter = formatter
super(formatter)
end
define_method(:level=) do |level|
logger.level = level
super(level)
end
end
end
end
end
file_logger = Logger.new(Rails.root.join("log/alternative-output.log"))
Rails.logger.extend(ActiveSupport::Logger.broadcast(file_logger))
For those who like it simple:
log = Logger.new("| tee test.log") # note the pipe ( '|' )
log.info "hi" # will log to both STDOUT and test.log
source
Or print the message in the Logger formatter:
log = Logger.new("test.log")
log.formatter = proc do |severity, datetime, progname, msg|
puts msg
msg
end
log.info "hi" # will log to both STDOUT and test.log
I'm actually using this technique to print to a log file, a cloud logger service (logentries) and if it's dev environment - also print to STDOUT.
You can also add multiple device logging functionality directly into the Logger:
require 'logger'
class Logger
# Creates or opens a secondary log file.
def attach(name)
#logdev.attach(name)
end
# Closes a secondary log file.
def detach(name)
#logdev.detach(name)
end
class LogDevice # :nodoc:
attr_reader :devs
def attach(log)
#devs ||= {}
#devs[log] = open_logfile(log)
end
def detach(log)
#devs ||= {}
#devs[log].close
#devs.delete(log)
end
alias_method :old_write, :write
def write(message)
old_write(message)
#devs ||= {}
#devs.each do |log, dev|
dev.write(message)
end
end
end
end
For instance:
logger = Logger.new(STDOUT)
logger.warn('This message goes to stdout')
logger.attach('logfile.txt')
logger.warn('This message goes both to stdout and logfile.txt')
logger.detach('logfile.txt')
logger.warn('This message goes just to stdout')
While I quite like the other suggestions, I found I had this same issue but wanted the ability to have different logging levels for STDERR and the file.
I ended up with a routing strategy that multiplexes at the logger level rather than at the IO level, so that each logger could then operate at independent log-levels:
class MultiLogger
def initialize(*targets)
#targets = targets
end
%w(log debug info warn error fatal unknown).each do |m|
define_method(m) do |*args|
#targets.map { |t| t.send(m, *args) }
end
end
end
stderr_log = Logger.new(STDERR)
file_log = Logger.new(File.open('logger.log', 'a'))
stderr_log.level = Logger::INFO
file_log.level = Logger::DEBUG
log = MultiLogger.new(stderr_log, file_log)
Here's another implementation, inspired by #jonas054's answer.
This uses a pattern similar to Delegator. This way you don't have to list all the methods you want to delegate, since it will delegate all methods that are defined in any of the target objects:
class Tee < DelegateToAllClass(IO)
end
$stdout = Tee.new(STDOUT, File.open("#{__FILE__}.log", "a"))
You should be able to use this with Logger as well.
delegate_to_all.rb is available from here: https://gist.github.com/TylerRick/4990898
Quick and dirty (ref: https://coderwall.com/p/y_b3ra/log-to-stdout-and-a-file-at-the-same-time)
require 'logger'
ll=Logger.new('| tee script.log')
ll.info('test')
#jonas054's answer above is great, but it pollutes the MultiDelegator class with every new delegate. If you use MultiDelegator several times, it will keep adding methods to the class, which is undesirable. (See bellow for example)
Here is the same implementation, but using anonymous classes so the methods don't pollute the delegator class.
class BetterMultiDelegator
def self.delegate(*methods)
Class.new do
def initialize(*targets)
#targets = targets
end
methods.each do |m|
define_method(m) do |*args|
#targets.map { |t| t.send(m, *args) }
end
end
class <<self
alias to new
end
end # new class
end # delegate
end
Here is an example of the method pollution with the original implementation, contrasted with the modified implementation:
tee = MultiDelegator.delegate(:write).to(STDOUT)
tee.respond_to? :write
# => true
tee.respond_to? :size
# => false
All is good above. tee has a write method, but no size method as expected. Now, consider when we create another delegate:
tee2 = MultiDelegator.delegate(:size).to("bar")
tee2.respond_to? :size
# => true
tee2.respond_to? :write
# => true !!!!! Bad
tee.respond_to? :size
# => true !!!!! Bad
Oh no, tee2 responds to size as expected, but it also responds to write because of the first delegate. Even tee now responds to size because of the method pollution.
Contrast this to the anonymous class solution, everything is as expected:
see = BetterMultiDelegator.delegate(:write).to(STDOUT)
see.respond_to? :write
# => true
see.respond_to? :size
# => false
see2 = BetterMultiDelegator.delegate(:size).to("bar")
see2.respond_to? :size
# => true
see2.respond_to? :write
# => false
see.respond_to? :size
# => false
I have written a little RubyGem that allows you to do several of these things:
# Pipe calls to an instance of Ruby's logger class to $stdout
require 'teerb'
log_file = File.open("debug.log", "a")
logger = Logger.new(TeeRb::IODelegate.new(log_file, STDOUT))
logger.warn "warn"
$stderr.puts "stderr hello"
puts "stdout hello"
You can find the code on github: teerb
Are you restricted to the standard logger?
If not you may use log4r:
require 'log4r'
LOGGER = Log4r::Logger.new('mylog')
LOGGER.outputters << Log4r::StdoutOutputter.new('stdout')
LOGGER.outputters << Log4r::FileOutputter.new('file', :filename => 'test.log') #attach to existing log-file
LOGGER.info('aa') #Writs on STDOUT and sends to file
One advantage: You could also define different log-levels for stdout and file.
If you're okay with using ActiveSupport, then I would highly recommend checking out ActiveSupport::Logger.broadcast, which is an excellent and very concise way to add additional log destinations to a logger.
In fact, if you are using Rails 4+ (as of this commit), you don't need to do anything to get the desired behavior — at least if you're using the rails console. Whenever you use the rails console, Rails automatically extends Rails.logger such that it outputs both to its usual file destination (log/production.log, for example) and STDERR:
console do |app|
…
unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDERR, STDOUT)
console = ActiveSupport::Logger.new(STDERR)
Rails.logger.extend ActiveSupport::Logger.broadcast console
end
ActiveRecord::Base.verbose_query_logs = false
end
For some unknown and unfortunate reason, this method is undocumented but you can refer to the source code or blog posts to learn how it works or see examples.
https://www.joshmcarthur.com/til/2018/08/16/logging-to-multiple-destinations-using-activesupport-4.html has another example:
require "active_support/logger"
console_logger = ActiveSupport::Logger.new(STDOUT)
file_logger = ActiveSupport::Logger.new("my_log.log")
combined_logger = console_logger.extend(ActiveSupport::Logger.broadcast(file_logger))
combined_logger.debug "Debug level"
…
I went to the same idea of "Delegating all methods to sub-elements" that other people already explored, but am returning for each of them the return value of the last call of the method.
If I didn't, it broke logger-colors which were expecting an Integer and map was returning an Array.
class MultiIO
def self.delegate_all
IO.methods.each do |m|
define_method(m) do |*args|
ret = nil
#targets.each { |t| ret = t.send(m, *args) }
ret
end
end
end
def initialize(*targets)
#targets = targets
MultiIO.delegate_all
end
end
This will redelegate every method to all targets, and return only the return value of the last call.
Also, if you want colors, STDOUT or STDERR must be put last, since it's the only two were colors are supposed to be output. But then, it will also output colors to your file.
logger = Logger.new MultiIO.new(File.open("log/test.log", 'w'), STDOUT)
logger.error "Roses are red"
logger.unknown "Violets are blue"
One more way.
If you're using tagged logging and need tags in another logfile as well, you could do it in this way
# backported from rails4
# config/initializers/active_support_logger.rb
module ActiveSupport
class Logger < ::Logger
# Broadcasts logs to multiple loggers. Returns a module to be
# `extended`'ed into other logger instances.
def self.broadcast(logger)
Module.new do
define_method(:add) do |*args, &block|
logger.add(*args, &block)
super(*args, &block)
end
define_method(:<<) do |x|
logger << x
super(x)
end
define_method(:close) do
logger.close
super()
end
define_method(:progname=) do |name|
logger.progname = name
super(name)
end
define_method(:formatter=) do |formatter|
logger.formatter = formatter
super(formatter)
end
define_method(:level=) do |level|
logger.level = level
super(level)
end
end # Module.new
end # broadcast
def initialize(*args)
super
#formatter = SimpleFormatter.new
end
# Simple formatter which only displays the message.
class SimpleFormatter < ::Logger::Formatter
# This method is invoked when a log event occurs
def call(severity, time, progname, msg)
element = caller[4] ? caller[4].split("/").last : "UNDEFINED"
"#{Thread.current[:activesupport_tagged_logging_tags]||nil } # {time.to_s(:db)} #{severity} #{element} -- #{String === msg ? msg : msg.inspect}\n"
end
end
end # class Logger
end # module ActiveSupport
custom_logger = ActiveSupport::Logger.new(Rails.root.join("log/alternative_#{Rails.env}.log"))
Rails.logger.extend(ActiveSupport::Logger.broadcast(custom_logger))
After this you'll get uuid tags in alternative logger
["fbfea87d1d8cc101a4ff9d12461ae810"] 2015-03-12 16:54:04 INFO logger.rb:28:in `call_app' --
["fbfea87d1d8cc101a4ff9d12461ae810"] 2015-03-12 16:54:04 INFO logger.rb:31:in `call_app' -- Started POST "/psp/entrypoint" for 192.168.56.1 at 2015-03-12 16:54:04 +0700
Hope that helps someone.
One more option ;-)
require 'logger'
class MultiDelegator
def initialize(*targets)
#targets = targets
end
def method_missing(method_sym, *arguments, &block)
#targets.each do |target|
target.send(method_sym, *arguments, &block) if target.respond_to?(method_sym)
end
end
end
log = MultiDelegator.new(Logger.new(STDOUT), Logger.new(File.open("debug.log", "a")))
log.info('Hello ...')
This is a simplification of #rado's solution.
def delegator(*methods)
Class.new do
def initialize(*targets)
#targets = targets
end
methods.each do |m|
define_method(m) do |*args|
#targets.map { |t| t.send(m, *args) }
end
end
class << self
alias for new
end
end # new class
end # delegate
It has all the same benefits as his without the need of the outer class wrapper. Its a useful utility to have in a separate ruby file.
Use it as a one-liner to generate delegator instances like so:
IO_delegator_instance = delegator(:write, :read).for(STDOUT, STDERR)
IO_delegator_instance.write("blah")
OR use it as a factory like so:
logger_delegator_class = delegator(:log, :warn, :error)
secret_delegator = logger_delegator_class(main_logger, secret_logger)
secret_delegator.warn("secret")
general_delegator = logger_delegator_class(main_logger, debug_logger, other_logger)
general_delegator.log("message")
You can use Loog::Tee object from loog gem:
require 'loog'
logger = Loog::Tee.new(first, second)
Exactly what you are looking for.
I also has this need recently so I implemented a library that does this. I just discovered this StackOverflow question, so I'm putting it out there for anyone that needs it: https://github.com/agis/multi_io.
Compared to the other solutions mentioned here, this strives to be an IO object of its own, so it can be used as a drop-in replacement for other regular IO objects (files, sockets etc.)
That said, I've not yet implemented all the standard IO methods, but those that are, follow the IO semantics (e.g. for example, #write returns the sum of the number of bytes written to all the underlying IO targets).
You can inherit Logger and override the write method:
class LoggerWithStdout < Logger
def initialize(*)
super
def #logdev.write(msg)
super
puts msg
end
end
end
logger = LoggerWithStdout.new('path/to/log_file.log')
I like the MultiIO approach. It works well with Ruby Logger. If you use pure IO it stops working because it lacks some methods that IO objects are expected to have. Pipes were mentioned before here: How can I have ruby logger log output to stdout as well as file?. Here is what works best for me.
def watch(cmd)
output = StringIO.new
IO.popen(cmd) do |fd|
until fd.eof?
bit = fd.getc
output << bit
$stdout.putc bit
end
end
output.rewind
[output.read, $?.success?]
ensure
output.close
end
result, success = watch('./my/shell_command as a String')
Note I know this doesn't answer the question directly but it is strongly related. Whenever I searched for output to multiple IOs I came across this thread.So, I hope you find this useful too.
I think your STDOUT is used for critical runtime info and errors raised.
So I use
$log = Logger.new('process.log', 'daily')
to log debug and regular logging, and then wrote a few
puts "doing stuff..."
where I need to see STDOUT information that my scripts were running at all!
Bah, just my 10 cents :-)

Ruby - share logger instance among module/classes

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.
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.
Inspired by this thread I created the easy_logging gem.
It combines all the features discussed such as:
Adds logging functionality anywhere with one, short,
self-descriptive command
Logger works in both class and instance methods
Logger is specific to class and contains class name
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.
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!
How about wrapping the logger in a singleton then you could access it using MyLogger.instance
Based on your comment
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).
If you are not restricted to the default logger you may use another log-gem.
As an example with log4r:
require 'log4r'
module Crawler
LOGGER = Log4r::Logger.new('mylog')
class Runner
def initialize
LOGGER.info('Created instance for %s' % self.class)
end
end
end
ARGV << 'test' #testcode
#...
case ARGV.first
when 'test'
Crawler::LOGGER.outputters = Log4r::StdoutOutputter.new('stdout')
when 'prod'
Crawler::LOGGER.outputters = Log4r::FileOutputter.new('file', :filename => 'test.log') #append to existing log
end
#...
Crawler::Runner.new
In prod mode the logging data are stored in a file (attached to existing file, but there are options to create new log files or implement rolling log files).
The result:
INFO main: Created instance for Crawler::Runner
If you use the inheritance mechanism of log4r (a), you may define a logger for each class (or in my following example for each instance) and share the outputter.
The example:
require 'log4r'
module Crawler
LOGGER = Log4r::Logger.new('mylog')
class Runner
def initialize(id)
#log = Log4r::Logger.new('%s::%s %s' % [LOGGER.fullname,self.class,id])
#log.info('Created instance for %s with id %s' % [self.class, id])
end
end
end
ARGV << 'test' #testcode
#...
case ARGV.first
when 'test'
Crawler::LOGGER.outputters = Log4r::StdoutOutputter.new('stdout')
when 'prod'
Crawler::LOGGER.outputters = Log4r::FileOutputter.new('file', :filename => 'test.log') #append to existing log
end
#...
Crawler::Runner.new(1)
Crawler::Runner.new(2)
The result:
INFO Runner 1: Created instance for Crawler::Runner with id 1
INFO Runner 2: Created instance for Crawler::Runner with id 2
(a) A logger name like A::B has the name B and is a child of the logger with the name A. As far as I know this is no object inheritance.
One advantage of this approach: If you want to use a single logger for each class, you need only to change the name of the logger.
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.

suppresing output to console with ruby

I am writing some unit tests like the following:
def executing_a_signal
a_method(a_signal.new, a_model, a_helper);
assert_equal(new_state, a_model.state)
end
The tests work fine, but the method which runs just before the assertion to execute the logic prints various messages to the console, mainly via puts.
Is there a quick, perhaps built-in, way to suppress that output to the console? I am only interested in the final effect of the method on the model object, and for the sake of keeping the console clean basically, I was hoping to find a way to simply prevent all output to the console without re-writing or commenting out those puts statements just for my tests.
It is definitely not a critical issue, but would very much like to hear any thoughts or ideas (or workaround) on it.
I use the following snippet in tests to capture and test STDOUT
def capture_stdout(&block)
original_stdout = $stdout
$stdout = fake = StringIO.new
begin
yield
ensure
$stdout = original_stdout
end
fake.string
end
With this method, the above would become:
def executing_a_signal
capture_stdout { a_method(a_signal.new, a_model, a_helper) }
assert_equal(new_state, a_model.state)
end
A slightly cleaner take on #cldwalker's solution:
def silenced
$stdout = StringIO.new
yield
ensure
$stdout = STDOUT
end
silenced do
something_that_prints
end
There's two solutions: redirecting where puts writes to (the solution given by #cldwalker above), or overwriting the puts method itself to be a no-op. (The implementation should be obvious: module Kernel; def puts(*args) end end).
However, in this case, what would really be the best solution is "listening to your tests". Because, oftentimes when something is awkward to test, your tests are really trying to tell you that something is wrong with your design. In this case, I smell a violation of the Single Responsibility Principle: why the heck does a Model object need to know how to write to the console? Its responsibility is representing a Domain Concept, not logging! That's what Logger objects are for!
So, an alternative solution would be to have the Model object delegate the responsibility for logging to a Logger object, and use dependency injection to inject the Model object with a suitable Logger object. That way, you can simply inject a fake logger for the test. Here's an example:
#!/usr/bin/env ruby
class SomeModel
def initialize(logger=Kernel) #logger = logger end
def some_method_that_logs; #logger.puts 'bla' end
end
require 'test/unit'
require 'stringio'
class TestQuietLogging < Test::Unit::TestCase
def setup; #old_stdout, $> = $>, (#fake_logdest = StringIO.new) end
def teardown; $> = #old_stdout end
def test_that_default_logging_is_still_noisy
SomeModel.new.some_method_that_logs
assert_equal "bla\n", #fake_logdest.string
end
def test_that_logging_can_be_made_quiet
fake_logger = Object.new
def fake_logger.puts *args; end
SomeModel.new(fake_logger).some_method_that_logs
assert_equal '', #fake_logdest.string
end
end
At the very least, the Model object should take the IO object that it is logging to as an argument, so that you can simply inject StringIO.new into it for the test:
#!/usr/bin/env ruby
class SomeModel
def initialize(logdest=$>) #logdest = logdest end
def some_method_that_logs; #logdest.puts 'bla' end
end
require 'test/unit'
require 'stringio'
class TestQuietLogging < Test::Unit::TestCase
def setup; #old_stdout, $> = $>, (#fake_logdest = StringIO.new) end
def teardown; $> = #old_stdout end
def test_that_default_logging_is_still_noisy
SomeModel.new.some_method_that_logs
assert_equal "bla\n", #fake_logdest.string
end
def test_that_logging_can_be_made_quiet
fake_logdest = (#fake_logdest = StringIO.new)
SomeModel.new(fake_logdest).some_method_that_logs
assert_equal '', #fake_logdest.string
assert_equal "bla\n", fake_logdest.string
end
end
If you still want to be able to just say puts whatever in your Model or you are afraid that someone might forget to call puts on the logger object, you can provide your own (private) puts method:
class SomeModel
def initialize(logdest=$>) #logdest = logdest end
def some_method_that_logs; puts 'bla' end
private
def puts(*args) #logdest.puts *args end
end
reopen '/dev/null'
Another option is to redirecting to /dev/null with:
STDOUT.reopen('/dev/null', 'w')
STDERR.reopen('/dev/null', 'w')
This technique is used on WEBrick::Daemon of the stdlib (toggle source).
It has the advantage of being more efficient than StringIO.new since it does not store the stdout on a StringIO, but it is less portable.
I just used the below code at the beginning of my .rb file.. so it disable all console print statements..
def puts(*args) end

Resources