chaos_cat has asked for the wisdom of the Perl Monks concerning the following question:

I've been using Log::Log4perl for a few months at work now, and in that time I've noticed that if the configuration file specifies logging to a directory that doesn't exist, Log::Log4perl dies, taking every application using that logging config file with it. The error looks something like:
Can't open logs/BOGUS/foo.log (No such file or directory) at C:/Perl/s +ite/lib/Log/Log4perl/Appender/File.pm line 102. Compilation failed in require at...
Since one of my main goals in using Log::Log4perl is to centralize my logging configurations, this means that large subsets of my code base can all go down at once from a typo in that config file. This was an acceptable risk when I was the only developer using the package, and I could trust myself to add the directory to the SVN archive in the same commit as I added the config file change (my logging paths are all under my svn root). Now that most of my team is using Log::Log4perl, there is getting to be too much risk of human failure in that approach.

I already have Log::Log4perl thinly wrapped, so I'm going to add some logic to the wrapper to attempt the initilization in an eval and, if it dies, re-attempt to initilize with a known safe (meaning empty) config file. This way, config file issues will only disable logging, not crash applications. I would like a way to automatically create that directory, however. I've thought about hacking the log4perl code to add this functionality, but I'd like to keep that as a last resort. I'm wondering how other monks have solved this problem before me.

Thanks in advance for your replies.

Replies are listed 'Best First'.
Re: Graceful handling of Log::Log4perl errors
by kyle (Abbot) on May 28, 2008 at 15:29 UTC

    I think your use of eval to implement a fail safe is a good way to go. One thing you could add to that is trying to figure out what directory it wants to use and then create it.

    eval { bork_bork_bork }; if ( $@ =~ m{ \A Can't \s open \s ( .+ ) / ( [^/]+ ) \s }xms ) { my ( $missing_dir, $log_filename ) = ( $1, $2 ); if ( ! -d $missing_dir ) { # try to make the directory } }

    If you manage to get the broken config working, the first thing you'll want to do is log that.

      Thanks for the suggestion. I built the following code based on your idea, which seems to do the trick. It makes repeated attempts to load the config file and correct for missing path errors. If it encounters an error it doesn't recognize, or if it can't get a path created, it gives up and loads a blank config file (which it creates then and there to be sure it's blank). So far, it's passed all my tests but if anyone sees something broken here please drop me a note.
      my %paths = (); my $init_fail = 0; INIT: while (1) { eval {Log::Log4perl->init_once($LOGGER_CONF_FILE)}; if ($@) { # Config file error of some kind # Send some alert message here # if we get a missing directory error, try some (sane) recover +y if ($@ =~ m{Can't\sopen\s/my/base/path/(.+)\s\(No such file or + directory\)}) { my $path = $1; if ( $paths{$path}++ ) { # only try once to create a give +n path. This prevents infinite loops here $init_fail = 1; last INIT; } my $dir = "/my/base/path"; # if the base path isn't th +ere, creating it is above this module's pay grade my @dir_list = split '/', $path; my $file = pop @dir_list; foreach my $sub_dir (@dir_list) { $dir .= "/$sub_dir"; if (! -d$dir) { eval {mkdir ($dir)}; # not too concerned with catc +hing errors here, the next round will bail on duplicate path attempt } } } # for other config file errors, throw our hands up in disgust +and move on else { $init_fail = 1; last INIT; } } else { $init_fail = 0; last INIT; } } # if we couldn't get the config file to initlize, load a blank one and + move on without logging if ($init_fail) { warn "Couldn't initilize logger, defaulting to blank config file"; my $default_config_file = "/my/base/path/etc/default.logger.conf"; open (my $fh, ">", $default_config_file); close ($fh); Log::Log4perl::init_once($default_config_file); }

        That looks pretty good! I'm gratified that you took my suggestion and ran with it. Thanks for sharing it with us! Looking at what you have, I think it could be made a bit more compact.

        use File::Path qw( mkpath ); my $base_path = '/my/base/path'; my $init_done = 0; my %paths = (); INIT: while ( ! $init_done ) { $init_done = eval { Log::Log4perl->init_once($LOGGER_CONF_FILE); 1 }; # If there's an error, and it's THIS one, if ( $@ =~ m{\A Can't \s open \s \Q$base_path\E (?: / ( \S+ ) )? / [^/]+ \s \( No \s such \s file \s or \s directory \)}xms ) { # This is the path that's missing my $target_path = "$base_path/$1"; # If we've already tried to create it once, don't try again if ( $paths{ $target_path }++ ) { last INIT; } # Try to create the path eval { mkpath( $target_path ) }; if ( ! -d $target_path ) { warn "Can't create missing directory '$target_path': $@\n" +; } } } # if we couldn't get the config file to initlize, load a blank one and + move on without logging if ( ! $init_done ) { warn "Couldn't initilize logger, defaulting to blank config file"; Log::Log4perl->init( \'' ); }

        Note the differences, though (good and bad).

        • I use File::Path::mkpath instead of just mkdir. This will try to create every necessary directory right up to the root. Yours won't try to create anything above /my/base/path. If attempting to create directories above /my/base/path will be a problem, you should go back to your own code.
        • Instead of tracking $init_fail, I track $init_done. This way the while condition is more natural (and doesn't look infinite), and I get to set it in only one place inside the loop.
        • I use Log::Log4perl->init with an empty string instead of an empty file I just created. It's shorter, and I don't run the risk of failing to create the empty file.
        • I re-complicated the regular expression (in particular adding the /x form back).
        • I haven't tested this. There could even be a syntax error.

        Thanks again for posting your work.