package Log::Trace;
use Carp;
#Hires times if available
eval
{
require Time::HiRes;
};
use vars qw($VERSION @EXPORT);
@EXPORT = qw(TRACE_HERE TRACEF); # TRACE, DUMP
use strict qw(subs vars);
use Fcntl ':flock';
$VERSION = sprintf"%d.%03d", q$Revision: 1.70 $ =~ /: (\d+)\.(\d+)/;
#################################################
# Importing
#################################################
*{"_debug"} = 0 ? sub { warn __PACKAGE__ . " @_\n" } : sub {};
#Import into calling packages
sub import
{
my $pkg = shift;
my $callpkg = caller(0);
my @args = @_;
if (ref $args[1] eq 'HASH')
{
# e.g. 'import (print => {Verbose => 1})
@args = ($args[0], undef, @args[1..$#args])
}
$pkg->_import($callpkg, @args);
}
sub deep_import # deprecated. use 'import Log::Trace {Deep => 1, ...} ...'
{
my $pkg = shift;
my $callpkg = caller(0);
my $params = ref $_[0] ? shift : {};
$params->{Deep} = 1;
push @_, undef if @_ == 1; # deep_import 'print';
$pkg->_import($callpkg, @_, $params);
}
sub TRACEF
{
my $callpkg = caller();
my $trace = *{"$callpkg\::TRACE"};
my $params = ref $_[0] ? shift : {};
my $format = shift;
$trace->($params, sprintf($format, @_));
}
sub TRACE_HERE
{
my $callpkg = caller();
my $params = ref $_[0] ? shift : {};
# see 'perldoc -f caller'
# calling caller() from the DB package stores subroutine args in @DB::args
my @caller;
do {
package DB;
@caller = caller(1);
@caller = caller(0) unless $caller[0];
};
my $sub = $caller[3]; # the subroutine that called TRACE_HERE
my ($file, $line) = (caller(0))[1,2]; # the location of the TRACE_HERE
my $trace = *{"$callpkg\::TRACE"};
shift @DB::args if @DB::args && "$DB::args[0]" eq "$params";
$trace->($params, "In $sub(".join(",", @DB::args).") - line $line of $file");
}
#################################################
# Exporting
#################################################
sub _import
{
my $pkg = shift;
my (@packages) = shift;
my ($target, $arg, $params) = @_;
$target = '' unless defined $target;
if ($params->{Deep}) {
# extend the package list
push @packages, _deep_import_packages($params->{Everywhere});
}
if ($params->{AutoImport}) {
# override the default require() to catch new modules being loaded
_install_require($pkg, $params, $target, $arg)
}
# lookup: valid target > TRACE sub. These are also closures around '$arg'
my %import_targets = (
'print' => sub {_log_to_fh($arg, _log_normal(@_))},
'print-verbose' => sub {_log_to_fh($arg, _log_verbose(@_))},
'print-debug' => sub {_log_to_fh($arg, _log_debug(@_))},
'warn' => sub {warn _log_normal(@_)},
'warn-verbose' => sub {warn _log_verbose(@_)},
'warn-debug' => sub {warn _log_debug(@_)},
'buffer' => sub {$$arg .= _log_normal(@_)},
'buffer-verbose' => sub {$$arg .= _log_verbose(@_)},
'buffer-debug' => sub {$$arg .= _log_debug(@_)},
'file' => sub {_log_to_file($arg, _log_normal(@_))},
'file-verbose' => sub {_log_to_file($arg, _log_verbose(@_))},
'file-debug' => sub {_log_to_file($arg, _log_debug(@_))},
'log' => sub {_log_to_file($arg, _log_debug(@_))},
'syslog' => sub {_log_to_syslog($arg, _log_normal(@_))},
'syslog-verbose' => sub {_log_to_syslog($arg, _log_verbose(@_))},
'syslog-debug' => sub {_log_to_syslog($arg, _log_debug(@_))},
'custom' => $arg,
);
my $suffix = '';
$params->{Verbose} = 0 unless defined $params->{Verbose};
if ($params->{Verbose} == 1)
{
$suffix = '-verbose';
}
elsif ($params->{Verbose} == 2)
{
$suffix = '-debug';
}
$target = $import_targets{$target.$suffix} ? $target.$suffix : $target;
_debug("Initialising target: $target");
foreach my $export_to (@packages) {
# Check whether to export functions to the package
my $match = defined $params->{Match} ? $params->{Match} : '.';
next unless $export_to =~ /$match/;
my %exclude;
if (my $excl = $params->{Exclude}) {
%exclude = map {$_ => 1} ref $excl eq 'ARRAY' ? @$excl : $excl;
}
$exclude{+__PACKAGE__} = 1; # exclude ourselves
next if $exclude{$export_to};
_debug("Exporting target:$target to $export_to");
# set up the TRACE/DUMP functions
my ($trace, $dump);
if ($target && $import_targets{$target})
{
$trace = _trace_maker($export_to, $params, $import_targets{$target});
$dump = _dump_maker($export_to, $params, $trace);
}
else
{
# Just export stub functions
$trace = $dump = sub {};
carp "$pkg imported with unknown target $target" if $target;
}
# Now export ...
__replace_subroutine($export_to, 'TRACE', $trace);
__replace_subroutine($export_to, 'DUMP', $dump);
__replace_subroutine($export_to, 'TRACEF', \&TRACEF);
__replace_subroutine($export_to, 'TRACE_HERE', \&TRACE_HERE);
if ($params->{AllSubs})
{
# wrap all functions in package with calls to TRACE
_debug("wrapping all functions in $export_to");
_wrap_functions($export_to, $trace);
}
}
}
sub __replace_subroutine
{
my ($package, $sub, $coderef) = @_;
if (defined \&{"${package}::$sub"})
{
# quietly remove existing stub function
# This avoids unsightly "subroutine foo redefined" warnings
# 'no warnings "redefine"' doesn't work pre perl 5.6
eval "undef \$${package}::{'$sub'}";
}
*{"${package}::$sub"} = $coderef;
}
sub _trace_maker
{
my ($package, $params, $trace_sub) = @_;
my $trace_level = $params->{Level};
return sub
{
local $@; # in-case TRACE is called from &DESTROY
my $rv;
eval
{
my $level = shift->{Level} if ($_[0] && ref $_[0] eq 'HASH');
return unless _evaluate_level($package, $trace_level, $level);
$rv = 1 && $trace_sub->(@_);
};
if ($@)
{
warn __PACKAGE__ . " : $@";
}
return $rv;
}
}
sub _dump_maker
{
my ($package, $params, $trace_sub) = @_;
my $trace_level = $params->{Level};
return sub
{
# always return the dumped data regardless of level unless called in
# void context
my $context = wantarray();
local $@; # in-case DUMP is called from &DESTROY
my $rv;
eval
{
return $rv = _dump($params, @_) if defined $context;
my $level = undef;
if ($_[0] && ref $_[0] eq 'HASH' && defined $_[0]{Level})
{
$level = shift->{Level};
}
return unless _evaluate_level($package, $trace_level, $level);
my $dumped = _dump($params, @_);
$rv = 1 && $trace_sub->($dumped);
};
if ($@)
{
warn __PACKAGE__ . " : $@";
}
return $rv;
}
};
# returns a list of packages to export trace functions to
sub _deep_import_packages
{
my $all_packages = shift;
# Build the list of packages
my @packages;
foreach my $module (@{_list_all_packages()})
{
next if $module eq __PACKAGE__;
next unless $all_packages || defined (&{"$module\::TRACE"});
push @packages, $module;
}
return @packages;
}
my %_autowrap;
sub _wrap_functions {
my ($package, $trace) = @_;
return if $_autowrap{$package};
$_autowrap{$package} = {} unless defined $_autowrap{$package};
my $symbols = \%{$package . '::'};
# wrap coderefs in the caller's symbol table
foreach my $typeglob (keys %$symbols) {
# skip TRACE/DUMP and other potential deep recursions
next if $typeglob =~ /^(?:TRACE(?:F|_HERE)?|DUMP|AUTOLOAD)$/;
# only wrap code references
my $sub = *{$symbols->{$typeglob}}{CODE};
next unless (defined $sub and defined &$sub);
# skip if sub is already wrapped
next if $_autowrap{$package}{$typeglob}++;
# define wrapped subroutine body
my $sub_body = <<'WRAPPED';
my ($name) = "${package}::$typeglob";
my ($callpkg, $file, $line) = caller(1);
my $arg = $_[0] && ref($_[0]) ? ref($_[0]) . ', ...' : "";
$trace->( "${name}( $arg )" );
goto &$sub
WRAPPED
# wrap subroutine, preserving prototypes
my $wrapped_sub;
if(defined (my $proto = prototype($sub)))
{
$wrapped_sub = eval "sub ($proto) { $sub_body }";
}
else
{
$wrapped_sub = eval "sub { $sub_body }";
}
__replace_subroutine($package, $typeglob, $wrapped_sub);
}
}
# return a list of all defined packages in the symbol table
# we could use %INC, but we'd miss packages that are defined in other modules
sub _list_all_packages {
my ($package) = @_;
$package = '' unless defined $package;
my @packages;
# this is a recursive look in the symbol table:
# %main::
# CGI::
# Cookie::
# Data::
# Dumper::
# ...
my %symbols = %{$package . "::"};
foreach my $module (keys %symbols)
{
next unless $module =~ s/::$//;
# ignore 'main' (deep recursion) and all invalid package names
next if !$package && ($module eq 'main' || $module !~ /^[a-zA-Z_]\w*$/);
my $prefix = $package ? $package . '::' : '';
# Add this module
push @packages, $prefix . $module;
# and recurse to sub-packages
push @packages, @{_list_all_packages($prefix . $module)};
}
return \@packages;
}
# Override the built-in require()
# This is tricky because these are treated differently by perl:
# 1. require CGI
# 2. require "CGI"
# We have no way of distinguishing the two, so we make a best guess
#
# This only works since perl 5.6.1
#
# See 'perlsub' for more information about overriding built-ins
sub _install_require
{
my ($pkg, $params, $target, @args) = @_;
# CORE::require has prototype(;$), but we get "bareword foo not allowed"
# errors if we use that. prototype(*) works though
my $require = sub (*)
{
local $^W;
my $what = shift;
return 1 if $INC{$what};
_debug("require $what");
my $package;
if ($what =~ /^v?[\d_.]+$/) {
# take advantage of UNIVERSAL->VERSION($require) for a portable
# version check
local $_Log::Trace::PerlVersion::VERSION = $];
eval {_Log::Trace::PerlVersion->VERSION($what)};
if (my $error = $@) {
$error =~ s/_Log::Trace::PerlVersion/Perl/;
die $error; #rethrow exception
}
return 1;
} elsif ($what =~ /(.*)\.pm$/) {
# looks like a module name, get the main package from the filename
# (perl 5.8 & ActivePerl 5.6.1)
($package = $1) =~ s{/}{::}g;
} elsif ($what =~ /^[a-zA-Z_]\w*(?:::\w+)*$/i) {
# package name: vanilla perl 5.6.1, 5.6.2
$package = $what;
($what = "$what.pm") =~ s{::}{/}g;
}
my $rv = CORE::require $what;
if ($rv && $package)
{
# import Log::Trace into package
return unless $params->{Everywhere}
|| defined (&{"$package\::TRACE"});
$pkg->_import($package, $target, @args, $params);
}
return $rv;
};
# Override global require, silencing "... used only once" warnings
*CORE::GLOBAL::require = *CORE::GLOBAL::require = $require;
}
# Returns caller info for exported functions
sub __caller
{
# We need to look several frames back, so we keep going until we find
# something from outside this package
my @caller;
for (1 .. 8) {
my @c = caller($_);
last unless defined $c[0];
@caller = @c;
last unless $caller[0] eq __PACKAGE__
|| $caller[3] =~ /^@{[__PACKAGE__]}\::/o;
}
# because we don't seem to get a call frame for main::__ANON__
$caller[0] = 'main' if ($caller[0] eq __PACKAGE__);
$caller[3] =~ s/^@{[__PACKAGE__]}\::.*/main::__ANON__/;
return @caller;
}
#################################################
# TRACE guts
#################################################
sub _evaluate_level
{
my ($callpkg, $imported_level, $trace_level) = @_;
return 1 if ! defined $imported_level;
if (ref $imported_level eq 'CODE')
{
return $imported_level->($callpkg, $trace_level);
}
elsif (ref $imported_level eq 'ARRAY')
{
for (@$imported_level)
{
return 1 if (! defined($_) && ! defined($trace_level));
next unless defined($trace_level) && defined($_);;
return 1 if $_ == $trace_level;
}
}
elsif (!ref $imported_level)
{
return unless defined $trace_level;
return $imported_level >= $trace_level;
}
}
sub _log_normal
{
return join(",", @_)."\n";
}
sub _log_verbose
{
my ($pack,$file,$line,$sub) = __caller();
return "$sub ($line) :: " . join( ", ", @_ ) . "\n";
}
sub _log_debug
{
my ($pack,$file,$line,$sub) = __caller();
my $timestamp = _timestamp();
return "$file: $sub ($line) [$timestamp] " . join( ", ", @_ ) . "\n";
}
sub _log_to_fh
{
my ($fh, @output) = @_;
$fh = \*STDOUT unless $fh;
print $fh @output;
}
sub _log_to_file
{
my $filename = shift;
my ($pack,$file,$line,$sub) = __caller();
local *LOG_FILE;
if (open (LOG_FILE, ">> $filename"))
{
if (eval {flock LOG_FILE, LOCK_EX|LOCK_NB})
{
print LOG_FILE @_;
flock LOG_FILE, LOCK_UN;
close LOG_FILE;
}
else
{
die "couldn't get lock on $filename : $!";
}
}
else
{
die "Cannot open $filename : $!";
}
}
sub _log_to_syslog
{
my ($priority) = shift || 'debug';
return unless eval {require Sys::Syslog};
Sys::Syslog::openlog(__PACKAGE__, 'pid');
my $rv = Sys::Syslog::syslog($priority, join ",", @_);
Sys::Syslog::closelog();
return $rv;
}
sub _dump
{
my ($params, @args) = @_;
my $msg = ref $args[0] ? '' : shift @args;
$msg .= ": " if($msg && @args);
my $type;
eval
{
if ($params->{Dumper})
{
$type = 'Data::Serializer';
require Data::Serializer;
my $params = ref $params->{Dumper} ?
$params->{Dumper} : { serializer => $params->{Dumper} };
my $serialiser = Data::Serializer->new(%$params);
for (@args)
{
$msg .= $serialiser->raw_serialize($_) . "\n";
}
}
else
{
$type = 'Data::Dumper';
require Data::Dumper;
# avoid 'used $var only once' warning
local $Data::Dumper::Indent;
local $Data::Dumper::Sortkeys;
local $Data::Dumper::Quotekeys;
$Data::Dumper::Indent = 1;
$Data::Dumper::Sortkeys = 1;
$Data::Dumper::Quotekeys = 0;
$msg .= Data::Dumper::Dumper(@args);
}
};
die "$type not available: $@" if $@;
return $msg;
}
sub _gettimeofday()
{
return Time::HiRes::gettimeofday() if $INC{'Time/HiRes.pm'};
return (time(), undef);
}
#Provide localtime-style timestamp with microsecond resolution if Time::HiRes
#is available
sub _timestamp
{
my ($epoch, $usec) = _gettimeofday();
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($epoch);
$year+=1900; $mon+=1;
my $stamp = sprintf("%4d-%02d-%02d %02d:%02d:%02d",$year,$mon,$mday,$hour,$min,$sec);
$stamp .= sprintf(".%.6d",$usec) if(defined $usec);
return $stamp;
}
1;
=head1 NAME
Log::Trace - provides a unified approach to tracing
=head1 SYNOPSIS
# The tracing targets
use Log::Trace; # No output
use Log::Trace 'print'; # print to STDOUT
use Log::Trace log => '/var/log/foo.log'; # Output to log file
use Log::Trace print => { Level => 3 };
# Switch on/off logging with a constant
use Log::Trace;
import Log::Trace ('log' => LOGFILE) if TRACING;
# Set up tracing for all packages that advertise TRACE
use Foo;
use Bar;
use Log::Trace warn => { Deep => 1 };
# Sets up tracing in all subpackages excluding Foo
use Log::Trace warn => {Deep => 1, 'Exclude' => 'Foo'};
# Exported functions
TRACE("Record this...");
TRACE({Level => 2}, "Only shown if tracing level is 2 or higher");
TRACEF("A la printf: %d-%.2f", 1, 2.9999);
TRACE_HERE(); # Record where we are (file, line, sub, args)
DUMP(\@loh, \%hoh); # Trace out via Data::Dumper
DUMP("Title", \@loh); # Trace out via Data::Dumper
my $dump = DUMP(@args); # Dump is returned without being traced
=head1 DESCRIPTION
A module to provide a unified approach to tracing. A script can C<use
Log::Trace qw( E<lt> mode E<gt> )> to set the behaviour of the TRACE function.
By default, the trace functions are exported to the calling package only. You
can export the trace functions to other packages with the C<Deep> option. See
L<"OPTIONS"> for more information.
All exports are in uppercase (to minimise collisions with "real" functions).
=head1 FUNCTIONS
=over 4
=item TRACE(@args)
Output a message. Where the message actually goes depends on how you imported
Log::Trace (See L<"Importing/enabling Log::Trace">)
The first argument is an optional hashref of options:
TRACE('A simple message');
vs:
TRACE({ Level => 2.1 }, 'A message at a specified trace level');
=item TRACEF($format, @args)
C<printf()> equivalent of TRACE. Also accepts an optional hashref:
TRACEF('%d items', scalar @items);
TRACEF({ Level => 5 }, '$%1.2d', $value);
=item DUMP([$message,] @args)
Serialises each of @args, optionally prepended with $message. If called in a
non-void context, DUMP will return the serialised data rather than TRACE
it. This is useful if you want to DUMP a datastructure at a specific tracing
level.
DUMP('colours', [qw(red green blue)]); # outputs via TRACE
my $dump = DUMP('colours', [qw(red green blue)]); # output returned
=item TRACE_HERE()
TRACEs the current position on the call stack (file, line number, subroutine
name, subroutine args).
TRACE_HERE();
TRACE_HERE({Level => 99});
=back
=head1 Importing/enabling Log::Trace
=over 4
=item import($target, [$arg], [\%params])
Controls where TRACE messages go. This method is called automatically when you
call C<'use Log::Trace;'>, but you may explicitly call this method at
runtime. Compare the following:
use Log::Trace 'print';
which is the same as
BEGIN {
require Log::Trace;
Log::Trace->import('print');
}
Valid combinations of C<$target> and C<arg> are:
=over 4
=item print =E<gt> $filehandle
Prints trace messages to the supplied C<$filehandle>. Defaults to C<STDOUT>
if no file handle is specified.
=item warn
Prints trace messages via C<warn()>s to C<STDERR>.
=item buffer =E<gt> \$buffer
Appends trace messages to a string reference.
=item file =E<gt> $filename
Append trace messages to a file. If the file doesn't exist, it will be created.
=item log =E<gt> $filename
This is equivalent to:
use Log::Trace file => $filename, {Verbose => 2};
=item syslog =E<gt> $priority
Logs trace messages to syslog via C<Sys::Syslog>, if available.
You should consult your syslog configuration before using this option.
The default C<$priority> is 'C<debug>', and the C<ident> is set to
C<Log::Trace>. You can configure the C<priority>, but beyond that, you can
implement your own syslogging via the C<custom> trace target.
=item custom => \&custom_trace_sub
Trace messages are processed by a custom subroutine. E.g.
use Log::Trace custom => \&mylogger;
sub mylogger {
my @messages = @_;
foreach (@messages) {
# highly sensitive trace messages!
tr/a-zA-Z/n-za-mN-ZA-M/;
print;
}
}
=back
The import C<\%params> are optional. These two statements are functionally the
same:
import Log::Trace print => {Level => undef};
import Log::Trace 'print';
See L<"OPTIONS"> for more information.
B<Note:> If you use the C<custom> tracing option, you should be careful about
supplying a subroutine named C<TRACE>.
=back
=head1 OPTIONS
=over 4
=item AllSubs =E<gt> BOOL
Attaches a C<TRACE> statement to all subroutines in the package. This can be
used to track the execution path of your code. It is particularly useful when
used in conjunction with C<Deep> and C<Everywhere> options.
B<Note:> Anonymous subroutines and C<AUTOLOAD> are not C<TRACE>d.
=item AutoImport =E<gt> BOOL
By default, C<Log::Trace> will only set up C<TRACE> routines in modules that
have already been loaded. This option overrides C<require()> so that modules
loaded after C<Log::Trace> can automatically be set up for tracing.
B<Note>: This is an experimental feature. See the ENVIRONMENT NOTES
for information about behaviour under different versions of perl.
This option has no effect on perl E<lt> 5.6
=item Deep =E<gt> BOOL
Attaches C<Log::Trace> to all packages (that define a TRACE function). Any
TRACEF, DUMP and TRACE_HERE routines will also be overridden in these packages.
=item Dumper =E<gt> Data::Serializer backend
Specify a serialiser to be used for DUMPing data structures.
This should either be a string naming a Data::Serializer backend (e.g. "YAML")
or a hashref of parameters which will be passed to Data::Serializer, e.g.
{
serializer => 'XML::Dumper',
options => {
dtd => 'path/to/my.dtd'
}
}
Note that the raw_serialise() method of Data::Serializer is used. See L<Data::Serializer>
for more information.
If you do not have C<Data::Serializer> installed, leave this option undefined to use the
C<Data::Dumper> natively.
Default: undef (use standalone Data::Dumper)
=item Everywhere =E<gt> BOOL
When used in conjunction with the C<Deep> option, it will override the
standard behaviour of only enabling tracing in packages that define C<TRACE>
stubs.
Default: false
=item Exclude =E<gt> STRING|ARRAY
Exclude a module or list of modules from tracing.
=item Level =E<gt> NUMBER|LIST|CODE
Specifies which trace levels to display.
If no C<Level> is defined, all TRACE statements will be output.
If the value is numeric, only TRACEs that are at the specified level or below
will be output.
If the value is a list of numbers, only TRACEs that match the specified levels
are output.
The level may also be a code reference which is passed the package name and the
TRACE level. It mst return a true value if the TRACE is to be output.
Default: undef
=item Match =E<gt> REGEX
Exports trace functions to packages that match the supplied regular
expression. Can be used in conjunction with C<Exclude>. You can also use
C<Match> as an exclusion method if you give it a negative look-ahead.
For example:
Match => qr/^(?!Acme::)/ # will exclude every module beginning with Acme::
and
Match => qr/^Acme::/ # does the reverse
Default: '.' # everything
=item Verbose =E<gt> 0|1|2
You can use this option to prepend extra information to each trace message. The
levels represent increasing levels of verbosity:
0: the default*, don't add anything
1: adds subroutine name and line number to the trace output
2: As [1], plus a filename and timestamp (in ISO 8601 : 2000 format)
This setting has no effect on the C<custom> or C<log> targets.
* I<the log target uses 'Verbose' level 2>
=back
=head1 ENVIRONMENT NOTES
The AutoImport feature overrides C<CORE::require()> which requires perl 5.6, but you may see unexpected errors if you aren't using at
least perl 5.8. The AutoImport option has no effect on perl E<lt> 5.6.
In mod_perl or other persistent interpreter environments, different applications could trample on each other's
C<TRACE> routines if they use Deep (or Everywhere) option. For example application A could route all the trace output
from Package::Foo into "appA.log" and then application B could import Log::Trace over the top, re-routing all the trace output from Package::Foo
to "appB.log" for evermore. One way around this is to ensure you always import Log::Trace on every run in a persistent environment from all your
applications that use the Deep option. We may provide some more tools to work around this in a later version of C<Log::Trace>.
C<Log::Trace> has not been tested in a multi-threaded application.
=head1 DEPENDENCIES
Carp
Time::HiRes (used if available)
Data::Dumper (used if available - necessary for meaningful DUMP output)
Data::Serializer (optional - to customise DUMP output)
Sys::Syslog (loaded on demand)
=head1 RELATED MODULES
=over 4
=item Log::TraceMessages
C<Log::TraceMessages> is similar in design and purpose to C<Log::Trace>.
However, it only offers a subset of this module's functionality. Most notably,
it doesn't offer a mechanism to control the tracing output of an entire
application - tracing must be enabled on a module-by-module
basis. C<Log::Trace> also offers control over the output with the trace
levels and supports more output targets.
=item Log::Agent
C<Log::Agent> offers a procedural interface to logging. It strikes a good
balance between configurability and ease of use. It differs to C<Log::Trace> in
a number of ways. C<Log::Agent> has a concept of channels and priorities, while
C<Log::Trace> only offers levels. C<Log::Trace> also supports tracing code
execution path and the C<Deep> import option. C<Log::Trace> trades a certain
amount of configurability for increased ease-of use.
=item Log::Log4Perl
A feature rich perl port of the popular C<log4j> library for Java. It is
object-oriented and comprised of more than 30 modules. It has an impressive
feature set, but some people may be frightened of its complexity. In contrast,
to use C<Log::Trace> you need only remember up to 4 simple functions and a
handful of configuration options.
=back
=head1 SEE ALSO
L<Log::Trace::Manual> - A guide to using Log::Trace
=head1 VERSION
$Revision: 1.70 $ on $Date: 2005/11/01 11:32:59 $ by $Author: colinr $
=head1 AUTHOR
John Alden and Simon Flack with some additions by Piers Kent and Wayne Myers
<cpan _at_ bbc _dot_ co _dot_ uk>
=head1 COPYRIGHT
(c) BBC 2005. This program is free software; you can redistribute it and/or modify it under the GNU GPL.
See the file COPYING in this distribution, or http://www.gnu.org/licenses/gpl.txt
=cut