perllogginglog4perl

Is it possible to register a function to preprocess log messages with Log::Log4perl?


In this example:

$logger->debug({
    filter => \&Data::Dumper::Dumper,
    value  => $ref
});

I can pretty print my references instead of ARRAY(0xFFDFKDJ). But it's too boring to type that long code every time. I just want:

$logger->preprocessor({
    filter => \&Data::Dumper::Dumper,
    value  => $ref
});

$logger->debug( $ref, $ref2 );
$logger->info( $array );

And $ref, $ref2, and $array will be dumped by Data::Dumper.

It there a way to do this?

UPD
With help of your answers I do the patch

Now you just:

log4perl.appender.A1.layout=FallbackLayout
log4perl.appender.A1.layout.chain=PatternLayout
log4perl.appender.A1.layout.chain.ConversionPattern=%m%n
log4perl.appender.A1.warp_message = sub { $#_ = 2 if @_ > 3; \
                                       return @_; }
# OR
log4perl.appender.A1.warp_message = main::warp_my_message

sub warp_my_message {
    my( @chunks ) =  @_;

    use Data::Dump qw/ pp /;
    for my $msg ( @chunks ) {
        $msg =  pp $msg   if ref $msg;
    }

    return @chunks;
}

UPD2

Or you can use this small module

log4perl.appender.SomeAPP.warp_message  = Preprocess::Messages::msg_filter
log4perl.appender.SomeAPP.layout        = Preprocess::Messages

package Preprocess::Messages;

sub msg_filter {
    my @chunks =  @_;

    for my $msg ( @chunks ) {
        $msg =  pp $msg   if ref $msg;
    }

    return @chunks;
};

sub render {
    my $self =  shift;

    my $layout =  Log::Log4perl::Layout::PatternLayout->new(
        '%d %P %p> %c %F:%L %M%n  %m{indent=2}%n%n'
    );

    $_[-1] += 1; # increase level of the caller
    return $layout->render( join $Log::Log4perl::JOIN_MSG_ARRAY_CHAR, @{ shift() }, @_ );
}


sub new {
    my $class = shift;
    $class = ref ($class) || $class;

    return bless {}, $class;
}

1;

Yes, of course you can set 'warp_message = 0' and combine msg_filter and render together.

log4perl.appender.SomeAPP.warp_message  = 0
log4perl.appender.SomeAPP.layout        = Preprocess::Messages

sub render {
    my($self, $message, $category, $priority, $caller_level) = @_;

    my $layout =  Log::Log4perl::Layout::PatternLayout->new(
        '%d %P %p> %c %F:%L %M%n  %m{indent=2}%n%n'
    );

    for my $item ( @{ $message } ) {
        $item =  pp $item   if ref $item;
    }

    $message =  join $Log::Log4perl::JOIN_MSG_ARRAY_CHAR, @$message;
    return $layout->render( $message, $category, $priority, $caller_level+1 );
}

Solution

  • The easy way: use warp_message

    The easiest way to do this is to create a custom appender and set the warp_message parameter so you can get the original references that were passed to the logger:

    package DumpAppender;
    use strict;
    use warnings;
    
    use Data::Dumper;
    
    $Data::Dumper::Indent = 0;
    $Data::Dumper::Terse  = 1;
    
    sub new {
        bless {}, $_[0];
    }
    
    sub log {
        my($self, %params) = @_;
        print ref($_) ? Dumper($_) : $_ for @{ $params{message} };
        print "\n";
    }
    
    package main;   
    use strict;
    use warnings;
    
    use Log::Log4perl;
    
    Log::Log4perl->init(\q{
        log4perl.rootLogger=DEBUG,Dump
        log4perl.appender.Dump=DumpAppender
        log4perl.appender.Dump.layout=NoopLayout
        log4perl.appender.Dump.warp_message=0
    });
    
    my $logger = Log::Log4perl->get_logger;
    
    $logger->debug(
        'This is a string, but this is a reference: ',
        { foo => 'bar' },
    );
    

    Output:

    This is a string, but this is a reference: {'foo' => 'bar'}
    

    Unfortunately, if you take this approach, you're stuck writing your own code to handle layouts, open files, etc. I wouldn't take this approach except for very simple projects that only need to print to screen.

    A better way: composite appender

    A better approach is to write your own composite appender. A composite appender forwards messages on to another appender after manipulating them somehow, e.g. filtering or caching them. With this approach, you can write only the code for dumping the references and let an existing appender do the heavy lifting.

    The following shows how to write a composite appender. Some of this is explained in the docs for Log::Log4perl::Appender, but I copied much of it from Mike Schilli's Log::Log4perl::Appender::Limit:

    package DumpAppender;   
    use strict;
    use warnings;
    
    our @ISA = qw(Log::Log4perl::Appender);
    
    use Data::Dumper;
    
    $Data::Dumper::Indent = 0;
    $Data::Dumper::Terse  = 1;
    
    sub new {
        my ($class, %options) = @_;
    
        my $self = {
            appender => undef,
            %options
        };
    
        # Pass back the appender to be limited as a dependency to the configuration
        # file parser.
        push @{ $options{l4p_depends_on} }, $self->{appender};
    
        # Run our post_init method in the configurator after all appenders have been
        # defined to make sure the appenders we're connecting to really exist.
        push @{ $options{l4p_post_config_subs} }, sub { $self->post_init() };
    
        bless $self, $class;
    }
    
    sub log {
        my ($self, %params) = @_;
    
        # Adjust call stack so messages are reported with the correct caller and
        # file
        local $Log::Log4perl::caller_depth = $Log::Log4perl::caller_depth + 2;
    
        # Dump all references with Data::Dumper
        $_ = ref($_) ? Dumper($_) : $_ for @{ $params{message} };
    
        $self->{app}->SUPER::log(
            \%params,
            $params{log4p_category},
            $params{log4p_level}
        );
    }
    
    sub post_init {
        my ($self) = @_;
    
        if(! exists $self->{appender}) {
            die "No appender defined for " . __PACKAGE__;
        }
    
        my $appenders = Log::Log4perl->appenders();
        my $appender = Log::Log4perl->appenders()->{$self->{appender}};
    
        if(! defined $appender) {
            die "Appender $self->{appender} not defined (yet) when " .
                __PACKAGE__ . " needed it";
        }
    
        $self->{app} = $appender;
    }
    
    package main;
    
    use strict;
    use warnings;
    
    use Log::Log4perl;
    
    Log::Log4perl->init(\q{
        log4perl.rootLogger=DEBUG, Dump
    
        log4perl.appender.Dump=DumpAppender
        log4perl.appender.Dump.appender=SCREEN
    
        log4perl.appender.SCREEN=Log::Log4perl::Appender::Screen
        log4perl.appender.SCREEN.layout=PatternLayout
        log4perl.appender.SCREEN.layout.ConversionPattern=%d %p %m%n
    });
    
    my $logger = Log::Log4perl->get_logger;
    
    $logger->debug(
        'This is a string, but this is a reference: ',
        { foo => 'bar' },
    );
    

    Output:

    2015/09/14 13:38:47 DEBUG This is a string, but this is a reference: {'foo' => 'bar'}
    

    Note that you have to take some extra steps if you initialize Log::Log4perl via the API instead of via a file. This is documented in the composite appenders section of the Log::Log4perl::Appender documentation.