Log4r - A Powerful Logger for Ruby

Inspired by the Apache Log4j project

At a Glance

Log4r features an extremely flexible logging library for Ruby. Killer features include a heiarchial logging system of any number of levels, logger inheritance, multiple output destinations, tracing, custom formatting and more.

Log4r was inspired by the very excellent Apache Log4j Project . Log4r provides the defining features of Log4j and some of its own features that just might make Log4j users envious. ;-)

The project is hosted on SourceForge: http://sourceforge.net/projects/log4r/

Download

Latest version is Log4r 0.9.8. All versions are stable. Newer ones have more features.

The API is available in a Javadoc-like format (thanks go to the RDoc project).

But why?

Please read the Introduction to Apache Log4j for answers. Personally, I find that a logging system such as Log4r is essential when writing distributed applications, like for a game that has a client/server design. Also, the ability to leave logging statements in the code without commenting them out is quite handy.

Features

Easy to use

Thanks to Ruby, Log4r is an extremely simple tool to use. It should become evident as you read further. ;)

Multiple loggers

You can have as many loggers as desired. Upon creation, they get stored in a Singleton repository from which they can be retrieved at any point.

  Log4r::Logger.new('mylogger')         # create a logger 'mylogger'
  Log4r::Logger['mylogger']             # get 'mylogger' back

Heiarchial logging

Log4r provides five levels of logging: DEBUG, INFO, WARN, ERROR, FATAL. A logger with a certain level will not perform logging for all levels below it (less important, if you will). Hence, if a logger is set to WARN, it won't log DEBUG, and INFO log events. ALL and OFF are special boundary levels. Setting a logger to ALL will let it see evey log event while setting it to OFF will disable all log events. It's not possible to log at ALL and OFF.

  include Log4r                      # include for brevity

  log = Logger['mylogger']
  log.level = WARN                   # set log level to Log4r::WARN

  log.debug  "DEBUG log event"       # won't show up
  log.info   "INFO log event"        # ditto
  log.warn   "WARN log event"        # will show up, and all that follow
  log.error  "ERROR log event"
  log.fatal  "FATAL log event"

You can dynamically reassign a logger's level if you want.

Custom levels

And now, something that will really make Log4j users envious: You can change the number and names of the heiarchial logging levels easily. Suppose we don't like having 5 levels named DEBUG, INFO, etc. Instead, we want Foo, Bar, and Baz. Here's how we do it:

  Log4r::Logger.custom_levels 'Foo', 'Bar', 'Baz'

Thereafter, the logging methods will be named after your custom levels:

  log.level = Log4r::Bar 
  log.foo?                               => false
  log.bar?                               => true
  log.bar "this is bar"                  => <Bar> this is bar
  log.baz "this is baz"                  => <Baz> this is baz

Multiple output destinations

The second block of code won't do anything because mylogger does not have anything to write to. In order to log somewhere, we have to create an Outputter and assign it to our logger. We can give our logger as many Outputters as we want:

  # continuing from second block

  f = FileOutputter.new(:filename => './tmp.log')
  so = StdoutOutputter.new
  se = StderrOutputter.new
  ex = Outputter.new(ExoticIO.new)   # outputter with an IO of our own make
  log.add(f, so, se, ex)
  log.error "A test error"           # writes to all 4 IOs

If an IO somehow chokes, the Outputter will set itself to OFF and close the IO.

Root logger, global threshold and Logger inheritance

A logger can inherit another logger. It turns out that every logger that you create normally is a child of the root logger. The root logger does two things: Provide the global logging level and the default level for its immediate children.

  Logger.root.level = ERROR       # set global level to ERROR
  Logger.new("alog")              # alog inherits ERROR

From this point on, the only log events (by alog and mylogger) that can show up are ERROR and FATAL.

To have one logger inherit another, specify the name of the parent in the argument to Logger.new as follows: "parent::child". Specifying 'root' is optional. Because of namespace collision concerns, you need to specify the full path to the logger as in foo::bar::baz. The same thing must be done for retrieving loggers from the repository:

  Logger.new('mylogger::mychild')       # mychild is a child of mylogger
  Logger['mylogger::mychild']           # get mychild back

A child logger that does not define its level during creation will inherit the level of its parent.

Outputter inheritance

In addition to level, a logger inherits the Outputters of its parent. That is, when mychild performs a logging event, it also calls the appropriate logging event for mylogger. This behavior is entirely optional and can be turned off by setting a logger's additive to false:

  Logger['mylogger::mychild'].additive = false

Hencenforth, any logging events to mychild will not be sent to mylogger. The buck stops here.

Custom formatting

By default, Log4r is capable of formatting any object passed to it by calling its inspect method. It also pre-formats Exceptions. Changing the way the data is formatted is just a matter of rolling up your own Formatter class that defines the method format:

  class MyFormatter < Formatter
    def format(level, logger, tracer, data)
      # level is the integer level of the log event
      # logger is the logger that called it
      # tracer is the execution stack returned by caller at the log event
      # data is what was passed into a logging method
    end
  end

  afile = FileOutputter.new(:file=>'./prettyformat')
  afile.formatter = MyFormatter.new
  # add it to 'mychild' dynamically
  Logger['mylogger::mychild'].add afile

Formatters are not inherited, they are assigned to specific outputters.

Tracing

By default, loggers don't record the execution stack. You can turn on tracing by setting a logger's trace to true. It's up to the Formatter to handle this information. BasicFormatter displays the line number and file of the log event.

Ways around parameter evaluation

Avoiding parameter evaluation at the log method invocation can be critical at times. One way to do this is to pass the object in and let the formatter inspect it. The formatter won't be called for non-loggable levels. If parameter evaluation is unavoidable, you can querry the logger to see if it's logging at a particular level. The following querry methods are provided:

  Logger.root.level          => ERROR
  log = Logger['mylogger']
  log.level                  => WARN
  log.debug?                 => false
  log.warn?                  => false
  log.info?                  => false
  log.error?                 => true
  log.fatal?                 => true
  log.off?                   => false   # true only if OFF is set
  log.all?                   => false   # true only if ALL is set

Even more flexibility: Outputter thresholds

Sometimes it's prudent to fix a certain outputter's level in stone, or keep it at some independent level with respect to any loggers. (Yes, you can share an outputter among loggers.) This can be done fairly easily by setting the level of the outputter:

  # console log for mychild that filters out anything less important than ERROR
	
  screen = StdoutOutputter.new(:level=>ERROR)
  Logger['mychild'].add(screen)

And that's not all, we can also tell an Outputter to log only specific levels. Here's how:

  # only DEBUG and FATAL on screen
  screen.only_at DEBUG, FATAL

Thread safe

Logging is thread safe. The formatting and writing to an output are synchronized by a simple mutex in each outputter.

Fast enough!

Profiling has revealed that log4r is typically an order of magnitude or two slower than log4j. However, this is still damn fast! In particular, if a logger is set to OFF, the overhead of checking to see if a log event should be logged nearly vanishes. This was accomplished by dynamically redefining the unloggable logging methods to do nothing.

I'd like to point out that if one needs to use something like log4r in a performance-critical application, one should recondsider why Ruby is being used at all. As far as anyone should be concerned, log4r is Fast Enough (TM) for casual and moderate use :)

Of course, this doesn't mean that we shouldn't improve the performance of log4r! When the time comes, it will be written as a C extension to Ruby.

Gotchas

If you are using Log4r, there are a few gotchas that you should be aware of:

The Future

Log4r is mostly done. It was written in about 3 days and does enough to be useful for most cases. There is still room for improvement performance-wise and maybe the C extension is nigh.

Obligatory SourceForge Link

SourceForge Logo


leon@ugcs.caltech.edu $Id: manual.html,v 1.1 2002/01/16 12:27:05 cepheus Exp $