Voterbox

Ruby Dual Output / Logging

Summary:

Logging in Ruby is a fairly straight forward process, however I came across an interesting issue. I found that I was constantly doing both a 'log' statement, as well as a 'puts' statement to output to console. Well, this clutters code especially once your script turns into a multi-layered application. Additionally, most of the time what I am logging is the same thing I want to output to console.

In walks Log4r.

Log4r is a comprehensive and flexible logging library written in Ruby for use in Ruby programs. It features a hierarchical logging system of any number of levels, custom level names, logger inheritance, multiple output destinations, execution tracing, custom formatting, thread safteyness, XML and YAML configuration, and more."



Logging to Console:

The most generally place you want to see output from a script is to the console. Our first setup is going to configure Log4r to log to console:

sample.rb:

#!/usr/bin/env/ruby

require('rubygems')
require('log4r')
include Logr4

log = Logger.new 'sample application'
format = PatternFormatter.new(:pattern => "%l:\t %m")
log.add Log4r::StdoutOutputter.new('console', :formatter=>format)

log.info 'sample application logging to console'

You can see that we have configured out 'log' object, and then added a 'format' to it with the 'log.add' method. Here you can specify any format pattern that you want. I generally like to keep the log level in the output for clarity ('%l').

Our output looks like:

wdierkes$ ruby sample.rb
INFO: sample application logging to console



Logging to a file:

Now, say we didn't want to log to console, we only want to log to a file. This is a perfectly legit scenario. We would simply change our logger around:

#!/usr/bin/env/ruby

require('rubygems')
require('log4r')
include Log4r

log_file = './sample.log'

log = Logger.new 'sample application'
format = PatternFormatter.new(:pattern => "[ %d ] %l\t %m")
log.add FileOutputter.new('fileOutputter', :filename => log_file, :trunc => false, :formatter=>format)

puts "logging to #{log_file}"
log.info 'sample application logging to console'

Only a few changes... we are now adding a bit more to the format pattern as do most log files. We are also now using the FileOutputter class.

Lets see the new output:

wdierkes$ ruby sample.rb
logging to ./sample.log

wdierkes$ cat sample.log
[ 2007-09-03 15:51:04 ] INFO sample application logging to console

Perfect!



Combining Console And Logfile Output in One Logger:

Here we arrive at my original dilemma. I constantly want to log the same thing to console, and the log file.. however keeping two separate formatter patterns for each. This turned out to be a lot easier than I had anticipated.

sample.rb:

#!/usr/bin/env/ruby

require('rubygems')
require('log4r')
include Log4r

log_file = './sample.log'

log = Logger.new 'sample application'
console_format = PatternFormatter.new(:pattern => "%l:\t %m")
log.add Log4r::StdoutOutputter.new('console', :formatter=>console_format)

file_format = PatternFormatter.new(:pattern => "[ %d ] %l\t %m")
log.add FileOutputter.new('fileOutputter', :filename => log_file, :trunc => false, :formatter=>file_format)

log.info 'sample application logging to console'

All we need to do is add both Outputters to our log object, and that is it. Lets check this bad boy out:

wdierkes$ ruby sample.rb
INFO: Im logging to both console and ./sample.log now

wdierkes$ cat sample.log
[ 2007-09-03 15:51:04 ] INFO sample application logging to console
[ 2007-09-03 15:56:05 ] INFO Im logging to both console and ./sample.log now

Looking nice... real nice.



Toggle Debug

Most of my scripts have a '--debug' flag that will set the global $debug to 'true'. If not set, then $debug should be 'nil'. We can easily add the following to our logger to enable debug:

if($debug)
   log.level = DEBUG
else
   log.level = INFO
end

There are 5 default levels when you create your Logger object:

DEBUG < INFO < WARN < ERROR < FATAL

I usually only toggle between INFO and DEBUG since all other levels will be displayed in 'INFO' mode.



One Logger for all my classes?:

Being that our main goal is world domination, with limited lines of code why not combine all of your classes to use one logger? In most cases, multiple classes are working together to accomplish a single task. In generally, you might want to break up your logs if you are running: multiple daemons, multiple threads, etc.

The following lays out a standard way of accessing the same log object with multiple classes. The concept is to have a 'base' or 'core' class that is passed between the other classes. In this example, the 'base' class is used to maintain common methods that all other classes will likely access.

sample.rb:

#!/usr/bin/env/ruby

require('rubygems')
require('log4r')
include Log4r

$debug = true

module Sample
   class Base
     def initialize(log_file)
       @log = Logger.new 'sample application'
       console_format = PatternFormatter.new(:pattern => "%l:\t %m")
       self.log.add Log4r::StdoutOutputter.new('console', :formatter=>console_format)
       file_format = PatternFormatter.new(:pattern => "[ %d ] %l\t %m")
       self.log.add FileOutputter.new('fileOutputter', :filename => log_file, :trunc => false, :formatter=>file_format)

       if($debug)
         self.log.level = DEBUG
       else
         self.log.level = INFO
       end
     end

     def log=(log_obj)
       @log = log_obj
     end

     def log()
       @log
     end
   end

   class ClassA
     def initialize(base)
       @base = base
     end

     def base=(base_obj)
       @base = base_obj
     end

     def base()
       @base
     end
   end

   class ClassB
     def initialize(base)
       @base = base
     end

     def base=(base_obj)
       @base = base_obj
     end

     def base()
       @base
     end
   end

end

log_file = './sample.log'
base = Sample::Base.new(log_file)
base.log.info 'accessing the logger from the base object'

objA = Sample::ClassA.new(base)
objA.base.log.info 'accessing the logger from ClassA (same object)'

objB = Sample::ClassB.new(base)
objB.base.log.debug 'accessing the logger from ClassB (same obj)'

Note that the base, objA, and objB are all logging to the Logger established in the Sample::Base class. Lets see what we have:

wdierkes$ ruby sample.rb
INFO: accessing the logger from the base object
INFO: accessing the logger from ClassA (same object)
DEBUG: accessing the logger from ClassB (same obj)

wdierkes$ cat sample.log
[ 2007-09-03 16:15:40 ] INFO accessing the logger from the base object
[ 2007-09-03 16:15:40 ] INFO accessing the logger from ClassA (same object)
[ 2007-09-03 16:15:40 ] DEBUG accessing the logger from ClassB (same obj)

You like?


Reference:

Log4r: http://log4r.sourceforge.net/



All content generally available under the GPL unless otherwise noted.
5dollarwhitebox.org is not responsible in anyway for actions performed based on information found on this site.

Powered by Drupal - Design by Artinet