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.
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:
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
.
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:
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.
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 ) } );
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.
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 |
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.
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 );
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__ );
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.
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.
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.
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.