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/;-)
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).
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.
- 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 toWARN
, it won't logDEBUG
, andINFO
log events.ALL
andOFF
are special boundary levels. Setting a logger toALL
will let it see evey log event while setting it toOFF
will disable all log events. It's not possible to log atALL
andOFF
.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 wantFoo
,Bar
, andBaz
. 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 manyOutputters
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 anIO
somehow chokes, the Outputter will set itself toOFF
and close theIO
.- 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 areERROR
andFATAL
. To have one logger inherit another, specify the name of the parent in the argument toLogger.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 infoo::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
Outputter
s of its parent. That is, whenmychild
performs a logging event, it also calls the appropriate logging event formylogger
. This behavior is entirely optional and can be turned off by setting a logger'sadditive
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-formatsExceptions
. Changing the way the data is formatted is just a matter of rolling up your ownFormatter
class that defines the methodformat
: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
totrue
. It's up to theFormatter
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.
root.level=OFF
after defining some loggers,
none of the loggers will change their level to OFF
,
Outputter
's IO
is closed, the
Outputter
changes its level to OFF
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.