A couple of weeks ago, I posted RFC: A simple and badly-named logging module. Given the feedback and a few days to ponder on it, I decided I had approached a number of things in a flawed manner, prompting a re-write from scratch.
The goals haven't changed. The goals were:
I will address three likely classes of comments off the bat. First, I used Exporter. Yes, I know that "Exporter is evil" to many people, and the fact that I used a custom import function that forces the core subroutines into to be exported will also tickle a few noses. However, given the goals, I think it was the right approach.
Second, I used prototypes on the exported functions. I have my reasons: I wanted the debug/trace/info statements to work in the same way as warn/die do. Consistency of interface trumped prototype concerns, and I think this is one of the few cases where they are sensible.
Third, I'm sure some will see this as "reinventing the wheel" and ask why I didn't just use an existing logger. I addressed this in detail in the previous RFC, but I'll summarize here: things like Log::Simple and Log::Log4Perl are great. The former fills a great niche, and the latter is extremely powerful. It came down to the fact that I wanted something in-between for certain applications, and which satisfied the goals above.
This RFC is one the steps along the way for me to release this on CPAN, so please pay particular attention to anything that would prevent you from using this in production code.
Thanks in advance!
$Id: Painless.pod,v 1.1 2006-02-27 09:58:51 radiant Exp $ $Revision: 1.1 $
This document describes the Log::Painless module.
Log::Painless (hereafter, LP) is a module designed to allow reasonably configurable, yet painless logging. Everything from reasonable defaults to the interface, to the configuration system is meant to be as painless as possible without sacrificing too much functionality.
This module abuses Exporter to create the info, debug, and trace subroutines in the importing package. There is no way to turn this off: it's kind of the whole point.
Warnings and exceptions are handled via signal handlers attached to the perl built-in functions warn and die. See SIGNAL HANDLERS for more on these.
Examples:
#!/bin/perl use Log::Painless; # sends warnings, info, and exceptions to m +ain.log info "Logging begun"; warn "Hello, there!"; #behaves like warn, but logs warning as +well. die "Program done."; #behaves like die, but logs exception as + well.
LP is configured during import (see CONFIGURATION):
use Log::Painless { file => 'myscript.log', level => 'debug' } +;
Any logging calls that are supressed by the current logging level are empty subs, so performance should not be impacted by peppering code with trace calls.
There are also three shortcuts (enter, leave, caught) imported by default to make tracing/debugging easier:
use Log::Painless { level => 'debug' }; sub test_log { debug enter; # puts 'Entered subroutine main::test_log' t +o log eval { # .. something which could die.. }; if ($@) { # logs 'Caught exception [$@] 1 evals deep in main::tes +t_log' info caught; # .. handle the exception } debug leave; # as enter, above, but "Left subroutine" } test_log();
The caught call removes the 'at file.pl line ##.' from the caught exception message, for clarity.
info 'Starting to connect to data source: '.$dsn;
Records an info-level log message.
Takes one scalar argument as a message to record to the log. This and all other exported functions are prototyped so that only one argument will be accepted (and parentheses are not required) -- this is much like the behavior of the functions warn and die.
eval { $dbh = DBI->connect('dbi:SQLite2:dbname=test.db','','' +) or die ("Can't connect!"); }; if ($@) { info caught; warn "Falling back to CSV file..." # do stuff. }
Might result in the following log:
2006-03-14T13:21:32 4508 E (myscript.pl/11):Can't connect! 2006-03-14T13:21:32 4508 I Caught exception [Can't connect!] 1 + evals deep in main 2006-03-14T13:21:32 4508 W (myscript.pl/16):Falling back to CS +V file...
Note that nested evals are noted, and either the package name or the subroutine that generates the exception is noted.
sub test_sub { trace enter; # .. some stuff }
Might result in a the following log:
2006-03-14T13:21:33 4509 T Entered subroutine main::test_sub
Note that the package name is included.
sub test_sub { # .. some stuff trace leave; }
Might result in a the following log:
2006-03-14T13:21:35 4509 T Left subroutine main::test_sub
Almost all configuration is done during import by passing a single hashref. The exceptions to this rule are covered in DUPLICATING AND DIVERTING LOGS.
Following are the options for import-time configuration:
By default, it is the package name followed by '.log';
use Log::Painless { file => 'messages.log' };
use Log::Painless { level => 'debug' }; # only trace will be s +upressed
use Log::Painless { shortcuts => [ 'caught' ] }; # only import + caught()
Using only import-time configuration, only one log file may be used. However, it is commonly useful to have particular classes of messages be directed to separate log files. For example, an implementor may wish to have all debug messages directed to a file named 'debug.log'.
LP supports this functionality through runtime configuration, allowing specific-level messages to be duplicated to several logs or diverted to a separate log.
For example, to divert all debug-level messages to 'debug.log':
Log::Painless->divert('debug' => 'debug.log');
Either a filehandle typeglob or filename may be given to divert. In some cases, an implementor wishes to duplicate messages to a secondary target:
Log::Painless->duplicate('debug' => *STDERR);
This will log all debug messages as previously configured, but also log them to STDERR. This call can be repeated to cause messages to be written to a theoretically limitless number of logs. Duplicating to more than two or three targets is, though, strongly discouraged for practical reasons.
These calls can be sensibly combined, as well. For example, if the default log target is 'main.log', but debug messages are to go only to 'debug.log' and STDERR:
Log::Painless->divert('debug' => 'debug.log'); Log::Painless->duplicate('debug' => *STDERR);
It's worth noting that divert will disable all previous targets for the given message level.
Warnings and exceptions are handled via overriding the signal handlers for the built-in warn and die functions. That is, $SIG{__WARN__} and $SIG{__DIE__} are universally overridden. If there are signal handlers already existing at import time, these will be automatically chained onto the logging handlers.
For warnings, CORE::warn will be called to propagate warnings unless quiet_warn is set. For exceptions, CORE::die will be called for propagation under all circumstances. This means all calls to die -- even those inside eval{} blocks -- will be logged. It is a good idea to use caught to note when such exceptions are handled internally.
To preserve logging capabilities when overriding these handlers at runtime, merely chain them:
use Log::Painless; { my $old_warn = $SIG{__WARN__}; $SIG{__WARN__} = sub { # my own signal handling; $old_warn->(@_); }; }
See perlvar under %SIG for more details about this.
The logfile format is:
Date-Time PID Type-Char Message With default format settings, this results in something like:
2006-03-14T14:22:32 4856 E (testlog.pl/10):the death
The Type-Char is one of Trace, Debug, Info, Warning, Exception. The line format may be altered through the logformat and timeformat configuration directives. Additionally, the lineformat directive can be used to specify a CODEref that will reformat each log line before it is written.
For example, to elimitate the PID from the log:
use Log::Painless { lineformat => sub { my $m = shift; $m=~s/^(.*?) \d+/$1 +/; return $m }, };
Any newlines found within Message are converted to the ASCII Field Separator char (0x1F).
Copyright (c) 2006 Darren Meyer <darren.meyer@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ``Software''), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package Log::Painless; #===================================================================== +========== # $Id: Painless.pm,v 1.1 2006-02-27 09:58:51 radiant Exp $ # Painless, simple logging facility #--------------------------------------------------------------------- +---------- # (c) 2006 RadiantMatrix, under an MIT License (see LICENSE doc sectio +n) #===================================================================== +========== use strict; #use warnings; #__Modules__# require Exporter; use Fcntl; use IO::Handle; use Data::Dumper (); use POSIX qw[strftime]; use vars qw[@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION]; #__setup__# $VERSION = sprintf "%d.%03d", q$Revision: 1.1 $ =~ /: (\d+)\.(\d+)/; + #CVS @ISA = qw[Exporter]; @EXPORT = qw[info debug trace]; @EXPORT_OK = qw[enter leave caught]; my %Config; my %Lvl = ( trace => 5, debug => 4, info => 3, warning => 2, exception + => 1 ); #__configuration and import__# my $LE = "\n"; # ( $^O eq 'MSWin32' ? "\x0D\x0A" : "\x0A" ); #Win line endings or + Unix? sub trace ($) { } sub debug ($) { } sub info ($) { } #_____________________________________________________________________ + import()_ sub import { # import ( \%config ) my $self = shift; my $config = shift; (my $dlog = caller().'.log') =~ s/\W+/_/g; my %default = ( file => $dlog, level => 'info', logformat => '%s %d %s %s', timeformat => '%Y-%m-%dT%H:%M:%S', shortcuts => [ qw(enter leave caught) ], quiet_warn => 0, ); foreach (keys %default) { $config->{$_} = $default{$_} unless exists $config->{$_}; } unless ( exists $Lvl{ $config->{level} } ) { die "Bad log level: $$config{level}" ; } no warnings 'redefine'; foreach ( sort { $Lvl{$a} <=> $Lvl{$b} } keys %Lvl ) { $Config{$_}{files} = []; $self->divert( $_ => $config->{file} ); if ($Lvl{$config->{level}} >= $Lvl{$_}) { eval '*'.$_.'=\&_'.$_.';'; } else { eval '*'.$_.'=\&_empty;'; } } for (qw [logformat timeformat lineformat level quiet_warn] ) { $Config{$_} = $config->{$_} if exists $config->{$_}; } my ($sig_warn, $sig_die) = ($SIG{__WARN__}, $SIG{__DIE__}); $SIG{__WARN__} = sub { CORE::warn(@_) unless $Config{quiet_warn}; warning ( @_ ); if (defined $sig_warn && ref $sig_warn eq 'CODE') { $sig_warn- +>(@_) } }; $SIG{__DIE__} = sub { exception( @_ ); if (defined $sig_die && ref $sig_die eq 'CODE') { $sig_die->(@ +_) } }; $self->export_to_level(1, $self, @EXPORT); if (defined $config->{shortcuts}) { $self->export_to_level(1, $self, @{ $config->{shortcuts} } ); } } #__________________________________________________________________ du +plicate()_ sub duplicate { # duplicate ( %cfg ) my $self = shift; my %cfg = @_; foreach ( keys %cfg ) { next unless exists $Config{$_}{files}; my $fh; if ( $cfg{$_} =~ /^\*/ ) { $fh = $cfg{$_}; # die("handle not open for writing while duplicating log '$ +_'") # unless ( O_WRONLY | O_RDWR ) & fcntl( $fh, F_GETFL, my +$slush ); } else { open $fh, '>>', $cfg{$_} or die("Can't append to $cfg{$_} while duplicating log ' +$_'"); } autoflush $fh 1; push @{ $Config{$_}{files} }, $fh; } return 1; } #_____________________________________________________________________ + divert()_ sub divert { # divert ( %cfg ) my $self = shift; my %cfg = @_; my %sav; foreach ( keys %cfg ) { next unless exists $Config{$_}{files}; $sav{$_} = $Config{$_}{files} unless exists $sav{$_}; my $fh; if ( $cfg{$_} =~ /^\*/ ) { $fh = $cfg{$_}; # die("handle not open for writing while diverting log '$_' +") # unless ( O_WRONLY | O_RDWR ) & fcntl( $fh, F_GETFL, my +$slush ); } else { open $fh, '>>', $cfg{$_} or die("Can't append to $cfg{$_} while diverting log '$_ +'"); } autoflush $fh 1; $Config{$_}{files} = [$fh]; } return \%sav; } #_____________________________________________________________________ + _write()_ sub _write { # _write ( $level, $msg ) my ( $level, $msg ) = @_; $msg =~ s/[\r\n]/\x1F/g; # change line-breaks to field-seps. my $line = sprintf $Config{'logformat'}, strftime($Config{'timeformat'}, localtime), $$, uc substr($level,0,1), $msg; # call global custom formater if it exists $line = $Config{'lineformat'}->($line) if ( exists $Config{'lineformat'} && ref $Config{'lineformat'} eq 'CODE' ); # call level custom formater if it exists $line = $Config{$level}{'lineformat'}->($line) if ( exists $Config{$level}{'lineformat'} && ref $Config{$level}{'lineformat'} eq 'CODE' ); # write to log for ( @{ $Config{$level}{files} } ) { print $_ $line,$LE; } return 1; } #__ shortcuts __# #_____________________________________________________________________ +_ enter()_ sub enter () { # enter ( ) - generates a sub entrance message. my ($pack, $file, $line, $sub) = caller(1); return 'Entered subroutine '.$sub; } #_____________________________________________________________________ +_ leave()_ sub leave () { # leave ( ) - generates a sub departure message. my ($pack, $file, $line, $sub) = caller(1); return 'Left subroutine '.$sub; } #_____________________________________________________________________ + caught()_ sub caught () { # caught ( ) - invoked as, e.g. info caught; my $cl = 1; my ($pack, $file, $sub); do { ($pack,$file, undef, $sub) = caller($cl++); } until ($sub ne '(eval)'); $cl--; (my $exc = $@) =~ s/(.*) at .*$/$1/s; #trim of 'at file line ##' m +sg. $exc=~ s/[\r\n]/\x1F/gs; #replace line endings. return 'Caught exception ['.$exc.'] ' .($cl>0 ? "$cl evals deep " : '').'in ' .($sub ? $sub : 'main'); } #_____________________________________________________________________ + _empty()_ sub _empty ($) { } # executed when a log level is to be skipped #__ interfaces __# #_____________________________________________________________________ + _trace()_ sub _trace ($) { # trace ( $msg ) _write('trace', @_) } #_____________________________________________________________________ + _debug()_ sub _debug ($) { # debug ( $msg ) _write('debug', @_) } #_____________________________________________________________________ + _info()_ sub _info ($) { # info ( $msg ) _write('info', @_) } #___________________________________________________________________ _ +warning()_ sub _warning ($) { # warning ( $msg ) (my $msg = shift) =~ m/(.*)\s+at (.*?) line (\d+).*$/; _write('warning', "($2\/$3):$1"); #- CORE::warn($msg) if ($Lvl{$Config{level}} >= $Lvl{warning}); } #_________________________________________________________________ _ex +ception()_ sub _exception ($) { # exception ( $msg ) (my $msg = shift) =~ m/(.*)\s+at (.*?) line (\d+).*$/; _write('exception', "($2\/$3):$1"); CORE::die($msg); } 1;
Updates:
|
|---|
| Replies are listed 'Best First'. | |
|---|---|
|
Re: RFC: Log::Painless
by xdg (Monsignor) on Mar 15, 2006 at 17:17 UTC | |
by radiantmatrix (Parson) on Mar 15, 2006 at 19:36 UTC | |
|
Re: RFC: Log::Painless
by rhesa (Vicar) on Mar 15, 2006 at 18:07 UTC | |
by radiantmatrix (Parson) on Mar 15, 2006 at 19:47 UTC | |
|
Re: RFC: Log::Painless
by PodMaster (Abbot) on Mar 16, 2006 at 09:35 UTC | |
by radiantmatrix (Parson) on Mar 16, 2006 at 16:59 UTC |