Chapter 13. Logging

Logging records messages from my program so I can watch its progress or look at what happened later. This is much larger than warnings or errors from my program since I can also emit messages when things are going well. Along with configuration, logging is one of the features most missing from Perl applications, or bolted on as an after thought. I can easily add logging from the start of development or retrofit a legacy application.

Recording Errors and Other Information

Web applications already have it made. They can send things to STDERR (through whichever mechanism or interface the program might use) and it shows up in the web server error log. Other programs have to work harder. In general, logging is not as simple as opening a file, appending some information, and closing the file. That might work if the program won’t run more than once at the same time and definitely finishes before it will run again. For any other case, it’s possible that two runs of the program running at the same time will try to write to the same file. Output buffering and the race for control of the output file means that somebody is going to win and not all of the output may make it into the file.

Logging, however, doesn’t necessarily mean merely adding text to a file. Maybe I want to put the messages into a database, display them on the screen, send them to a system logger (such as syslogd), or more than one of those. I might want to send them directly to my email or pager. Indeed, if something is going wrong on one machine, to ensure that I see the message I might want to send it to another machine. I might want to send a message to several places at the same time. Not only should it show up in the log file, but it should show up on my screen. I might want different parts of my program to log things differently. Application errors might go to customer service people while database problems go to the database administrators.

If that’s not complicated enough, I might want different levels of logging. By assigning messages an importance, I can decide which messages I want to remember and which I want to ignore. While I’m debugging, I want to get a lot of information out of my application, but when it’s in production, I don’t need to see all of that. I should be able to have it both ways at once.

Putting all of that together, I need:

Output to multiple destinations

Different logging for different program parts

Different levels of logging

There are several modules that can handle all of those, including Michael Schilli’s Log::Log4perl and Dave Rolsky’s Log::Dispatch, but I’m only going to talk about one of them. Once I have the basic concept, I do the same thing with only minor interface details. Indeed, parts of Log::Log4perl use Log::Dispatch.

Log4perl

The Apache / Jakarta project created a Java logging mechanism called http://logging.apache.org/log4j, which has all of the features I mentioned in the last section. Language wars aside, it’s a nice package. It’s so nice, in fact, that Mike Schilli and Kevin Goess ported it to Perl.

Start simply. This short example loads Log::Log4perl with the :easy import tag, which gives me $ERROR, a constant to denote the logging level, and ERROR as the function to log messages for that level. I use easy_init to set up the default logging by telling it what sort of messages that I want to log, in this case those of type $ERROR. After that, I can use ERROR. Since I haven’t said anything about where the logging output should go, Log::Log4perl sends it to my terminal:

#!/usr/bin/perl
# log4perl-easy1.pl

use Log::Log4perl qw(:easy);

Log::Log4perl->easy_init( $ERROR );

ERROR( "I've got something to say!" );

The error message I see on the screen has a date and time stamp, as well as the message I sent.

2006/10/22 19:26:20 I've got something to say!

If I don’t want that output to go to the screen, I can give easy_init some extra information to let it know what I want it to do. I use an anonymous hash to specify the level of logging and the file I want it to go to. Since I want to append to my log, I use a >> before the filename just as I would with Perl’s open. This example does the same thing as the previous one save that its output goes to error_log. In Log::Log4perl parlance, I’ve configured an appender:

#!/usr/bin/perl
# log4perl-easy2.pl

use Log::Log4perl qw(:easy);

Log::Log4perl->easy_init(
    {
    level => $ERROR,
    file  => '>> error_log',
    }
    );

ERROR( "I've got something to say!" );

I can change my program a bit. Perhaps I want to have some debugging messages. I can use the DEBUG function for those. When I set the target for the message, I use the special filename STDERR, which stands in for standard error:

#!/usr/bin/perl
# log4perl-easy3.pl

use strict;
use warnings;

use Log::Log4perl qw(:easy);

Log::Log4perl->easy_init(
    {
    level => $ERROR,
    file  => ">> error_log",
    },

    {
    level => $DEBUG,
    file  => "STDERR",
    }
    );

ERROR( "I've got something to say!" );

DEBUG( "Hey! What's going on in there?" );

My error messages go to the file and my debugging messages go to the standard error. However, I get both on the screen!

2006/10/22 20:02:45 I've got something to say!
2006/10/22 20:02:45 Hey! What's going on in there?

The messages are hierarchical, therefore configuring a message level, such as $DEBUG, means that messages for that level and all lower levels reach that appender. Log::Log4perl defines five levels, where debugging is the highest level (i.e. I get the most output from it). The DEBUG level gets the messages for all levels, whereas the ERROR level gets the messages for itself and FATAL. Here’s the levels and their hierarchy:

TRACE

DEBUG

INFO

WARN

ERROR

FATAL

Keep in mind, though, that I’m really configuring the appenders, all of which get a chance to log the output. Each appender looks at the message and figures out if it should log it. In the previous example, both the error_log and STDERR appenders knew that they logged messages at the ERROR level, so the ERROR messages showed up in both places. Only the STDERR appender thinks it should log messages at the DEBUG level, so the DEBUG messages only show up on screen.

Subroutine Arguments

I don’t have to be content with simply logging messages, though. Instead of a string argument, I give the logging routines an anonymous subroutine to execute. This subroutine will only run when I’m logging at that level. I can do anything I like in the subroutine, and the return value becomes the log message:

#!/usr/bin/perl
# log4perl-runsub.pl

use strict;
use warnings;

use Log::Log4perl qw(:easy);

Log::Log4perl->easy_init(
    {
    level => $DEBUG,
    file  => "STDERR",
    }
    );

DEBUG( sub {
    print "Here I was!";      # To STDOUT
    return "I was debugging!" # the log message
    } );

I can also use a subroutine reference to avoid creating a large string argument. For instance, I might want to use Data::Dumper to show a big data structure. If I call Dumper in the argument list, Perl calls it, creates the big string, then uses that as the argument to DEBUG even before it knows if DEBUG will do anything:

use Data::Dumper;

# always calls Dumper()
DEBUG( Dumper( \%big_data_structure ) );

If I use a subroutine reference, <DEBUG> only runs the subroutine reference if it’s going to log the message:

DEBUG( sub { Dumper( \%big_data_structure ) } );

Configuring Log4perl

The “easy” technique I used earlier defined a default logger that it used. For more control, I can create my own directly. In most applications, this is what I’m going to do. This happens in two parts. First, I’ll load Log::Log4perl and configure it. Second, I’ll get a logger instance.

To load my own configuration, I replace my earlier call to easy_init with init, which takes a filename argument. Once I’ve initialized my logger, I still need to get an instance of the logger (since I can have several instances going at the same time) by calling get_logger. The easy_init method did this for me, but now that I want more flexibility I have a bit more work to do. I put my instance in $logger and have to call the message methods, such as error, on that instance:

#!/usr/bin/perl
# root-logger.pl

use Log::Log4perl;

Log::Log4perl::init( 'root-logger.conf' );

my $logger = Log::Log4perl->get_logger;

$logger->error( "I've got something to say!" );

Now I have to configure Log::Log4perl. Instead of easy_init making all of the decisions for me, I do it myself. For now, I’m going to stick with a single root logger. Log::Log4perl can have different loggers in the same program, but I’m going to ignore those. Here’s a simple configuration file that mimics what I was doing before: appending messages at or below the ERROR level to a file error_log:

# root-logger.conf
log4perl.rootLogger               = ERROR, myFILE

log4perl.appender.myFILE          = Log::Log4perl::Appender::File
log4perl.appender.myFILE.filename = error_log
log4perl.appender.myFILE.mode     = append
log4perl.appender.myFILE.layout   = Log::Log4perl::Layout::PatternLayout
log4perl.appender.myFILE.layout.ConversionPattern = [%p] (%F line %L) %m%n

The first line configures rootLogger. The first argument is the logging level, and the second argument is the appender to use. I make up a name that I want to use, and myFILE seems good enough.

After I configure the logger, I configure the appender. Before I start, there is no appender even though I’ve named one (myFILE), and although I’ve given it a name, nothing really happens. I have to tell Log4perl what myFile should do.

First, I configure myFile to use Log::Log4perl::Appender::File. I could use any of the appenders that come with the module (and there are many), but I’ll keep it simple. Since I want to send it to a file, I have to tell Log::Log4perl::Appender::File which file to use and which mode it should use. As with my easy example, I want to append to error_log. I also have to tell it what to write to the file.

I tell the appender to use Log::Log4perl::Layout::PatternLayout so I can specify my own format for the message, and since I want to use that, I need to specify the pattern. The pattern string is similar to sprintf but Log::Log4perl takes care of the % placeholders for me. From the placeholders in the documentation, I choose the pattern:

[%p] (%F line %L) %m%n

This pattern gives me an error message that includes the error level (%p for priority), the filename and line number that logged the message (%F and %L), the message (%m), and finally a newline %n:

[ERROR] (root-logger.pl line 10) I've got something to say!

I have to remember that newline because the module doesn’t make any assumptions about what I’m doing with the message. There has been a recurring debate about this, and I think the module does it right: it does what I say rather than making me adapt to it. I just have to remember to add newlines myself, either in the format or in the messages.

Table 13-1. PatternLayout placeholders
Placeholder Description
%c category of the message
%C Package (class) name of the caller
%d date-time as YYYY/MM/DD HH:MM:SS
%F filename
%H hostname
%l shortcut for %c %f (%L)
%L line number
%m the message
%M method or function name
%n newline
%p priority (e.g. ERROR, DEBUG, INFO)
%P process ID
%r milliseconds since program start
%R number of milliseconds elapsed from last logging event to current logging event
%T A stack trace of functions called
%x The topmost “Nested Diagnostic Context”
%X{key} The entry ‘key’ of the “Mapped Diagnostic Context”
%% A literal percent (%) sign

Adding My Own Information

I can put anything I like in my log message and use the %m placeholder to insert that string into the log, but I just get the single string. I can’t expand the placeholder to define my own, but I can use the Nested Diagnostic Context (NDC) or the Mapped Diagnostic Context (MDC) to place information with %x and %X{key} respectively.

The Nested Diagnostic Context is a stack. I can add strings to this stack and access them in my log pattern. With just %x, I get all the strings I’ve added, separated by spaces. I create this configuration:

log4perl.appender.Screen.layout.ConversionPattern = [%x] %m%n

In my program, I use Log::Log4perl::NDC-push> to add a string to the NDC stack. In this program, I only add one string:

use Log::Log4perl;

Log::Log4perl->init( 'log4perl.conf' );

my $logger = Log::Log4perl->get_logger;
Log::Log4perl::NDC->push( 'Buster' );

$logger->info( 'info level' );

The log output shows Buster replaced %x:

[Buster] info level

I can add more strings and they will all show up, separated by spaces:

use Log::Log4perl;

Log::Log4perl->init( 'log4perl.conf' );

my $logger = Log::Log4perl->get_logger;
Log::Log4perl::NDC->push( 'Buster' );
Log::Log4perl::NDC->push( 'Mimi' );

Now I see both strings:

[Buster Mimi] info level

Log::Log4perl has methods to manipulate the stack, too. I don’t like the stack for most things. I use it when each item in the stack is the same sort of information, such as all usernames, that can appear multiple times rather than trying to keep track of different information, such as IP addresses and baseball scores, at the same time.

I’d rather have a hash, which is what the MDC does. To place a value from the MDC into a log message, I use %X{key}:

log4perl.appender.Screen.layout.ConversionPattern = [%X{Cat}] %m%n

In this program, I have two keys in the MDC:

use Log::Log4perl;

Log::Log4perl->init( 'log4perl.conf' );

my $logger = Log::Log4perl->get_logger;
Log::Log4perl::MDC->put( 'Cat', 'Buster' );
Log::Log4perl::MDC->put( 'Dog', 'Droopy' );


$logger->info( 'info level' );

Since I only access the Cat keep in the my log pattern, my output only shows that:

[Buster] info level

This way, I can track many different things and let the logger decide later which ones it wants to use.

Persistent Configuration

If I’m using this in a persistent program, such as something run under mod_perl, I don’t need to load the configuration file every time. I can tell Log::Log4perl not to reload it if it’s already done it. The init_once method loads the configuation one time only:

Log::Log4perl::init_once( 'logger.conf' );

Alternatively, I might want Log::Log4perl to continually check the configuration file and reload it when it changes. That way, by changing the configuration file, and the configuration file only (remember that I promised I could do this without changing the program), I can affect the logging behavior while the program is still running. For instance, I might want to crank up the logging level to see more messages (or send them to different places). The second argument to init_and_watch is the refresh time, in seconds:

Log::Log4perl::init_and_watch( 'logger.conf', 30 );

Logging Categories

When I turn on logging, I don’t always want to turn it on for the entire program. Perhaps I need to pay attention to only one part of the program. Instead of using the rootLogger, I can define categories and configure them separately. A category is really a logger that’s confined to a particular component.

In the configuration file, I replace rootLogger with a name that I choose, and configure it as I did earlier. Different categories can share the same appender. In this example, I defined three categories, two of which share the CSV appender:

# category-logger.conf
log4perl.someCategory = WARN, CSV
log4perl.someOtherCategory = FATAL, CSV
log4perl.anotherCategory = INFO, DBI

...CSV appender config...

...DBI appender config...

In my program, I access these category loggers by specifiying their name in my call to get_logger:

my $someCategory = Log::Log4perl->get_logger( 'someCategory' );
$someCategory->debug( 'This is from someCategory' );

my $anotherCategory = Log::Log4perl->get_logger( 'anotherCategory' );
$anotherCategory->debug( 'This is from anotherCategory' );

I like to name my logging instance variables after the category name, and since the configuration file looks like Java (coming from log4j), some CamelCase sneaks into my program.

I like to create categories for each of the modules in my application. I can turn on logging per module. Log::Log4perl lets me use the :: as a separator in the configuration:

log4perl.My::Module = WARN, CSV
log4perl.Our::Module = FATAL, CSV
log4perl.Your::Module = INFO, DBI

The colons are the same as dots, so this configuration is the same:

log4perl.My.Module = WARN, CSV
log4perl.Our.Module = FATAL, CSV
log4perl.Your.Module = INFO, DBI

Inside my program, I can then get the module logger without explicitly stating the package name since I can use the __PACKAGE__ token:

my $logger = Log::Log4perl->get_logger( __PACKAGE__ );

Categories are hierarchical

When I define categories with either the :: or . separators in the name, I create hierarchies. When I log a message in a particular category, Log::Log4perl will apply the particular category when it logs, but after that it will look for other categories that it thinks that category inherits from.

This configuration defines two categories, Foo and Foo::Bar. The second category uses the . to separate Foo and Bar, so it inherits from Foo. Messages to Foo.Bar will also log in Foo. This configuration defines separate appenders for each of them with the difference only being the ConversionPattern in the last line of configuration for each appender:

log4perl.logger.Foo                  = DEBUG, FooScreen
log4perl.logger.Foo.Bar              = INFO, FooBarScreen

log4perl.appender.FooScreen  = Log::Log4perl::Appender::Screen
log4perl.appender.FooScreen.layout   = Log::Log4perl::Layout::PatternLayout
log4perl.appender.FooScreen.layout.ConversionPattern = FooScreen %c [%p] %m%n

log4perl.appender.FooBarScreen  = Log::Log4perl::Appender::Screen
log4perl.appender.FooBarScreen.layout   = Log::Log4perl::Layout::PatternLayout
log4perl.appender.FooBarScreen.layout.ConversionPattern = FooBarScreen %c [%p] %m%n

Here’s a program that logs each level (except FATAL!) in both categories. I access each category logger into its own variable and calls each level:

#!/usr/bin/perl
# hierarchial logging

my $file = 'categories.conf';

Log::Log4perl->init( $file );

my $foo = Log::Log4perl->get_logger( 'Foo' );
my $foo_bar = Log::Log4perl->get_logger( 'Foo::Bar' );

$foo->error( 'error from foo' );
$foo->warn( 'warn from foo' );
$foo->info( 'info from foo' );
$foo->debug( 'debug from foo' );
$foo->trace( 'trace from foo' );

$foo_bar->error( 'error from foo_bar' );
$foo_bar->warn( 'warn from foo_bar' );
$foo_bar->info( 'info from foo_bar' );
$foo_bar->debug( 'debug from foo_bar' );
$foo_bar->trace( 'trace from foo_bar' );

The output shows that some log messages show up twice. First, the FooScreen messages show up. Those are from the messages I log with $foo. After those, I see my first line that starts with FooBarScreen. That’s from my first call with $foo_bar. Right after that line is the same log message but from FooScreen. After logging in Foo.Bar, Log::Log4perl walked up the hierarchy to log the same thing in Foo:

FooScreen Foo [ERROR] error from foo
FooScreen Foo [WARN] warn from foo
FooScreen Foo [INFO] info from foo
FooScreen Foo [DEBUG] debug from foo
FooBarScreen Foo.Bar [ERROR] error from foo_bar
FooScreen Foo.Bar [ERROR] error from foo_bar
FooBarScreen Foo.Bar [WARN] warn from foo_bar
FooScreen Foo.Bar [WARN] warn from foo_bar
FooBarScreen Foo.Bar [INFO] info from foo_bar
FooScreen Foo.Bar [INFO] info from foo_bar

In that example, the level in Foo was DEBUG, so it was a lower level than that in Foo.Bar. Still, the Foo category, when inherited, logged the same messages as the original category despite its own setting. It’s the bottom category that decides the level.

Other Log::Log4perl Features

I’ve only shown the very basics of Log::Log4perl. It’s much more powerful than that, and there are already many excellent tutorials out there. Since Log::Log4perl started life as Log4j, a Java library, it maintains a lot of the same interface and configuration details, so you might read the Log4j documentation or tutorials too.

Having said that, I want to mention one last feature. Since Log::Log4perl is written in Perl, I can use Perl hooks in my configuration to dynamically affect the configuration. For instance, Log::Log4perl::Appender::DBI sends messages to a database, but I’ll usually need a username and password to write to the database. I don’t want to store those in a file, so I grab them from the environment. In this example of an appender, I pull that information from %ENV. When Log::Log4perl sees that I’ve wrapped a configuration value in sub { }, it executes it as Perl code:

# dbi-logger.conf
log4perl.rootLogger = WARN, CSV
log4perl.appender.CSV                 = Log::Log4perl::Appender::DBI
log4perl.appender.CSV.datasource      = DBI:CSV:f_dir=.
log4perl.appender.CSV.username        = sub { $ENV{CSV_USERNAME} }
log4perl.appender.CSV.password        = sub { $ENV{CSV_PASSWORD} }
log4perl.appender.CSV.sql             = \
   insert into csvdb                   \
   (pid, level, file, line, message) values (?,?,?,?,?)

log4perl.appender.CSV.params.1        = %P
log4perl.appender.CSV.params.2        = %p
log4perl.appender.CSV.params.3        = %F
log4perl.appender.CSV.params.4        = %L
log4perl.appender.CSV.usePreparedStmt = 1

log4perl.appender.CSV.layout          = Log::Log4perl::Layout::NoopLayout
log4perl.appender.CSV.warp_message    = 0

My program to use this new logger is the same as before, although I add some initialization in a BEGIN block to create the database file if it isn’t already there:

#!/usr/bin/perl
# log4perl-dbi.pl

use Log::Log4perl;

Log::Log4perl::init( 'dbi-logger.conf' );

my $logger = Log::Log4perl->get_logger;

$logger->warn( "I've got something to say!" );

BEGIN {
    # create the database if it doesn't already exist
    unless( -e 'csvdb' ) {
        use DBI;

        $dbh = DBI->connect("DBI:CSV:f_dir=.")
                        or die "Cannot connect: " . $DBI::errstr;
        $dbh->do(<<"HERE") or die "Cannot prepare: " . $dbh->errstr();
            CREATE TABLE csvdb (
                pid INTEGER,
                level   CHAR(64),
                file    CHAR(64),
                line    INTEGER,
                message CHAR(64)
                )
            HERE

        $dbh->disconnect();
        }
    }

I can do much more complex things with the Perl hooks available in the configuration files as long as I can write the code to do it.

Summary

I can easily add logging to my programs with Log::Log4perl, a Perl version of the Log4j package. I can use the easy configuration, or specify my own complex configuration. Once configured, I call subroutines or methods to make a message available to Log::Log4perl, which decides where to send the message or if it should ignore it. I just have to send it the message.

Further Reading

The Log4perl project at Sourceforge, http://log4perl.sourceforge.net/, has Log4Perl FAQs, tutorials, and other support resources for the package. Most of the basic questions about using the module, such as “How do I rotate log files automatically”. The project is also in GitHub at https://github.com/mschilli/log4perl.

Michael Schilli wrote about Log4perl on Perl.com, “Retire Your Debugger, Log Smartly with Log::Log4perl!”, http://www.perl.com/pub/2002/09/11/log4perl.html.

Log4Perl is closely related to Log4j http://logging.apache.org/log4j, the Java logging library, so you do things the same way in each. You can find good tutorials and documentation for Log4j that you might be able to apply to Log4perl too.