Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Log4perl: How do I dynamically load appenders at runtime?

I'd like to have modules managing their logging at runtime, but without having everything referring to a single monolithic config file. When dealing with processes running under different permissions, I really don't want to deal with each process needing to be able to access every log on the system when they're only writing to a subset of them.

However, I'm not finding much documentation in the Log4perl manual on how to initialize additional appenders from a configuration file at runtime. http://metacpan.org/pod/Log::Log4perl::Appender references an add_appender method, but that works on instantiated appender objects instead of conf files. It also doesn't define the logger objects and the logger->appender relations.

I tried having each package init from its own conf, but that simply clobbers the existing config each time it's initalized. What I'd like to do is something along the lines of:

my $foo = Foo->new() ## Checks Log::Log4perl::initialized(), sees that it
                     ## hasn't been initalized yet, inits Log4perl from foo.conf
my $bar = Bar->new() ## Checks Log::Log4perl::initialized(), sees that it
                     ## has been initalized. Adds appenders and loggers defined
                     ## in bar.conf into the initialized configuration

How can I parse and add the configuration into the current config?

Edit: Probalem with using a package variable is that this is just a Moose role being consumed by various classes, pretty much just a MooseX::Role::Parameterized version of Ether's answer in Making self-logging modules with Log::Log4perl. Thus, my logger is getting composed into the library consuming it, and I don't have a global variable I can work on each time I use it.

Though..

If I declare a global variable outside of the MooseX::Role::Parameterized role block, would each and every class that consumes the role be using that same conf variable?

like image 338
Oesor Avatar asked Jan 17 '11 17:01

Oesor


2 Answers

You can remember what config files was already loaded (%log_configs hash in code below). When new class arrives, you can reread all configs, merge it together and init Log::Log4perl again using string reference parameter to init.

I generally prefer having a single log configuration per application, because of easier maintenance and reload capability.

package Logger;
use Moose::Role;
use Log::Log4perl;

our %log_configs = ();

around BUILDARGS => sub {
    my $orig  = shift;
    my $class = shift;

    my $config_name = lc($class) . '.conf';

    # if the config is not integrated yet
    if(! defined $log_configs{$config_name}) {
        $log_configs{$config_name} = 1;

        # reload all configs including new one
        my $config_text = '';
        for my $file (sort keys %log_configs) {
            $config_text .= "\n" . do {
                local $/;   # slurp
                unless(open my $fh, "<", $file) {
                    warn "$file could not be open\n";
                    '';
                }
                else {
                    <$fh>
                }
            };
        }

        # refresh config
        Log::Log4perl::init(\$config_text);
    }

    return $class->$orig(@_);
};


package Foo;
use Moose;
with 'Logger';
use Log::Log4perl ':easy';

sub BUILD {
    ERROR 'Foo reporting';
}


package Bar;
use Moose;
with 'Logger';
use Log::Log4perl ':easy';

sub BUILD {
    INFO 'Bar reporting';
}


package main;

my $foo = Foo->new;
my $bar = Bar->new;
like image 131
bvr Avatar answered Nov 15 '22 02:11

bvr


While I was hoping to avoid it, if I parse the config files myself I can then access the configuration in perl via the API documented in http://search.cpan.org/perldoc?Log::Log4perl. Namely,

  ########################
  # Initialization section
  ########################
  use Log::Log4perl;
  use Log::Log4perl::Layout;
  use Log::Log4perl::Level;

     # Define a category logger
  my $log = Log::Log4perl->get_logger("Foo::Bar");

     # Define a layout
  my $layout = Log::Log4perl::Layout::PatternLayout->new("[%r] %F %L %m%n");

     # Define a file appender
  my $file_appender = Log::Log4perl::Appender->new(
                          "Log::Log4perl::Appender::File",
                          name      => "filelog",
                          filename  => "/tmp/my.log");

     # Define a stdout appender
  my $stdout_appender =  Log::Log4perl::Appender->new(
                          "Log::Log4perl::Appender::Screen",
                          name      => "screenlog",
                          stderr    => 0);

     # Have both appenders use the same layout (could be different)
  $stdout_appender->layout($layout);
  $file_appender->layout($layout);

  $log->add_appender($stdout_appender);
  $log->add_appender($file_appender);
  $log->level($INFO);

While the other method works, there's too many caveats for me to be comfortable using it (Gee I used this library, why'd my logging stop?) -- it's just too surprising for my tastes.

Instead, I think I'm going to see if I can't get from config file to Log::Log4perl state by looking through how to use Log::Log4perl::Config::PropertyConfigurator, which is delegated to by ->init when parsing a config file is needed. If I go over the data structure that returns, i can compare changes to the initialization on a logger-by-logger and appender-by-appender basis and modify the initialized state appropriately, handle namespace collisions properly, etc.

like image 37
Oesor Avatar answered Nov 15 '22 04:11

Oesor