perllogginglog4perl

log4perl: grouping messages


I'm using log4perl to log messages from a perl script. With mwe.pl as below, then I receive the following (desired) output in test.log

INFO: some information
      more information

My current implementation uses:

my $logmessage = "some information\n";
$logmessage .= "more information";
$logger->info($logmessage);

Notice in particular that I have specified the line break manually using \n, which I'd like to avoid.

Is there a way that I can achieve my desired output (test.log), without having to scaffold my logging input?

mwe.pl

#!/usr/bin/env perl
use strict;
use warnings;
use Log::Log4perl qw(get_logger :levels);

my $logger = get_logger();
$logger->level($INFO);

my $layout = Log::Log4perl::Layout::PatternLayout->new("%p: %m{indent}%n");
my $appender = Log::Log4perl::Appender->new(
    "Log::Dispatch::File",
    filename => "test.log",
    mode     => "write",
);

$appender->layout($layout);
$logger->add_appender($appender);

my $logmessage = "some information\n";
$logmessage .= "more information";
$logger->info($logmessage);

exit(0);

Solution

  • One way is to add custom "cspecs" to your PatternLayout. Using an API

    Log::Log4perl::Layout::PatternLayout::add_global_cspec(
        'A', sub { ... }
    );                    # can now use %A
    

    where this needs to come before the call to new which can then use the %A specifier.

    This can be set up in configuration instead, as shown in linked docs. Or add_global_cspec method can be called on the $layout object (but I couldn't figure out the interface.)

    The anonymous sub receives

    ($layout, $message, $category, $priority, $caller_level)  
    

    layout: the PatternLayout object that called it
    message: the logging message (%m)
    category: e.g. groceries.beverages.adult.beer.schlitz
    priority: e.g. DEBUG|WARN|INFO|ERROR|FATAL
    caller_level: how many levels back up the call stack you have to go to find the caller

    what can be used to implement criteria for formatting the prints.

    Here is a simple example custom-specifying the whole format

    use strict;
    use warnings;
    use Log::Log4perl qw(get_logger :levels);
    
    my $logger = get_logger();
    $logger->level($INFO);
    
    Log::Log4perl::Layout::PatternLayout::add_global_cspec( 
        'A', sub { return ( 
            $_[1] !~ /^more/                 # /^more/ taken to indicate 
               ?  "$_[3]: "                  # the continuation criterion,
               :  ' ' x length $_[3] . '  '  # or start with 'INFO: '
        ) . $_[1]
    });
    
    my $layout = Log::Log4perl::Layout::PatternLayout->new("%A%n");
    
    my $appender = Log::Log4perl::Appender->new(
        "Log::Dispatch::File",
        filename => "new_test.log",
        mode     => "write",
    );
    
    $logger->info('some info');
    $logger->info('more info');
    
    $logger->info('info');
    $logger->info('more and more info');
    

    which prints

    INFO: some info
          more info
    INFO: info
          more and more info
    

    Such a custom specifier can of course be combined with the provided ones.

    Since the list in info(...) is joined by the logger into a string that is passed along to appender(s) we can decide on the heading in the caller with an apparent interface

    $logger->info('*', "... message ...");  # * for heading (add INFO:)
    

    where the first string above is whatever the regex in our cspec looks for.

    This formats each log line based on its content. A more rounded option is to write your own appender (FAQ), which is a rather simple class where you can keep and manipulate lines as needed. See an example of bundling messages (FAQ), for instance.

    Finally, a proper way to fine-tune how messages are selected is by adding a category. Then you can pull a new logger and configure it to display INFO: (for the header line), while the rest of messages in that group go by the other logger, configured to not display it. See this post for a simple example.

    The downside is that there is now a lot more to do with loggers, appenders, and layout, and all that for only a small tweak in this case.

    If these don't fit the bill please clarify how you decide which prints are to be grouped.