Using the Ruby Logger

Posted on August 10, 2013 - Subscribe - Home

Ruby has a logging built in. Simply require logger and you're off to the races. Don't waste your time with log4r or anything else. The standard logger is good enough for almost every use case I've heard of. Less dependencies are always important. Use the standard library when you can.

The interface is easy to use. You've probably seen this:

logger = Logger.new File.new('test.log')
logger.debug "debugging info"
logger.info "general logs"
logger.warn "oh my…this isn't good"
logger.error "boom!"
logger.fatal "oh crap…"

Each method is tied to a specific log level. There are 6 levels: Logger::DEBUG, Logger::INFO, Logger::WARN, Logger::Error, Logger::FATAL, and Logger::UNKOWN. You can set the log level by writing logger.level = Logger::DEBUG.

You can also provide a progname. This is inserted into each log entry. Have you ever done anything like this? logger.debug "[worker] doing something…" or logger.info "[email] sending invitation". I think pretty much everyone has. This is exactly what progname is for. Here's an example:

logger.info 'worker' { 'doing hard work' }

The syntax is weird, but it gets the job done. The initial argument is the name, and the block returns the message. You can also set the progname attribute. This is perfect for when you have a logger for various subsystems.

logger = Logger.new some_file

# all messages will include "sync"
logger.progname = 'sync'

logger.info 'doing some stuff'
logger.error 'boom!'

This is a much better solution. You can still change the progname with the block form.

You can also change the formatter. This object that defines what a log message looks like. It must respond to call. The signature looks like this:

call(severity, time, progname, msg)

You can implement a simple formatter like this:

logger.formatter = proc do |serverity, time, progname, msg|
  msg
end

You can also create a Formatter object if that's more your style. I recommend this approach because you can subclass Logger::Formatter and get error handling for free. What do I mean? Take a look at this common code:

logger.error "Something blew up!"
logger.error "#{err.message}"
logger.error err.backtrace.join("\n")

3 lines to print some error information! The the default formatter handles exceptions by default It will print the error and the backtrace for you. Those lines can be reduced to:

logger.error "Something blew up!"
logger.error ex

This also has annoyed me, but once I learned that it handled errors by default I was able to clean up a lot of code. I recommend you go with something like this:

class CustomFormater < Logger::Formater
  def call(severity, time, progname, msg)
   # msg2str is the internal helper that handles different msgs correctly
    "#{time} - #{msg2str(msg)}"
  end
end

It is more verbose but you only see it once so it's not such a big deal.

Tips & Tricks

Have you ever needed to test a logger or redirect it's output to something you can interact with? Perhaps StringIO works for you. Logger#initialize expects an IO objects. Anyone will do. Here's an example:

require 'stringio'
require 'logger'
file = StringIO.new
logger = Logger.new file

# do logging stuff

# now get the log's contents
log = file.rewind.read

Voilla, no file dependencies!

Here's another scenario. Have you ever needed to simply tell a library to be quiet? This is very common in tests. The best way is to substitute a null object. Google the null object pattern if you're not familiar with this. This is better than changing the log level or using a temp file, insert other solution here. You can create a null logger by overriding two methods:

class NullLoger < Logger
  def initialize(*args)
  end

  def add(*args, &block)
  end
end

add is the internal method all the other methods go through. initialize is also overridden. This means you can pass whatever you want because it makes no difference. You could chose to use the same method signature if you wanted, but I don't think it really matters. Now the logger will do nothing and you don't have see any output or worry about log files.

Here's the final tip. This is my preferred way to create loggers.

App.logger = Logger.new($stdout).tap do |log|
  log.progname = 'name-of-subsystem'
end

I always set progname. It makes it easier to track the logs. It's set in one place and everything uses it.

I've also released a gem called "better_logger". It provides a few simple usability enhancements to the stdlib logger. You can check it out on github.

Happy logging!

— Adam Hawkins