#!/usr/bin/perl
# ----------------------------------------------------------------------
# Copyright (C) 2005 Mark Lawrence <nomad@null.net>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# ----------------------------------------------------------------------
# parselog - A wrapper for various Log::Parse::* modules
# ----------------------------------------------------------------------
use strict;
use warnings;
use Getopt::Std;
use Sys::Syslog;
use LockFile::Simple;
use Log::Parse;
use debug;

our %opt;
my $conf;
my $lockmgr;
my $version = '0.09';

sub usage {
    print STDERR "usage: $0 [-d] [-c config] [-p]\n";
}

#
# Lifted and modified from the CPAN ConfigFile module:
#
# Version 1.1.0
# Copyright (c) 2002 Sebastien J. Gross. All rights reserved.
# Copyright (c) 2003 Gunnar Wolf. All rights reserved.
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GPL.
#
# Return a hashref from a file, replacing conf if given
#
sub read_config_file {
    my %args = (
        file => '',
        conf => {},
        @_,
    );


    if (!open(CONF, "< " . $args{file})) {
        warn "Could not open $args{file}: $!\n";
        return undef; 
    }

    my $Conf;
    foreach my $key (keys %{$args{conf}}) {
        $Conf->{$key} = $args{conf}->{$key};
    }

    while(my $line = <CONF>) {
        chomp($line);
        next if($line =~ m/^\s*#/);
        
        $line =~ s/[^\\]#.*$//; # remove inline comments
        $line =~ s/\\#/#/g;     # replace \# with #

        next if $line =~ m/^\s*$/; # ignore empty lines

        $line =~ s{\$(\w+)}{ exists($Conf->{$1}) ? $Conf->{$1} : "\$$1" }gsex;

        if ($line =~ m/\s*([^\s=]+)\s*=\s*(.*?)\s*$/) {
            eval '$Conf->{\'' . join('\'}->{\'', split /[][]+/, $1) .
                 '\'} = \'' . $2 . "'\n";
        }
    }
    close CONF;

    debug::log('Read Config File ',$args{file}) if(DEBUG);
    return $Conf;
}

sub init {
    $0 =~ s!.*/!!; # get rid of directory components

    if (!getopts('dc:pt:', \%opt)) {
        usage();
        exit 1;
    }
    if (@ARGV) {
        usage();
        exit 1;
    }

    if ($opt{d}) {
        debug->import qw(
            main
            Log::Parse
            Log::Read
            Log::Read::Apache
            Log::Read::Exim
            Log::Read::Syslog
        );
        require Log::Delta;
        debug::set_default_log_filter(sub {Log::Delta->instance->l(@_);});
        debug::log("$0 version $version");
    }

    my $config = '/etc/parselog/parselog.conf';
    if ($opt{c}) {
        $config = $opt{c};
    }

    $conf = read_config_file(file => $config);
    exit 1 unless($conf);
    if ($opt{d}) {
        $conf->{debug} = 1;
    }

    if (! -d $conf->{confdir}) {
        warn "Directory '$conf->{confdir}' does not exist / not readable";
        exit 1;
    }

    if (!open(TMP, '> ' . $conf->{libdir} . "/$$")) {
        warn "Can't open files in $conf->{libdir}: $!\n";
        exit 1;
    }
    close(TMP);
    unlink($conf->{libdir} . "/$$");

    $lockmgr = LockFile::Simple->make(
        -autoclean => 1,
        -format    => '%f.lck',
        -hold      => 900,
        -stale     => 1,
        -delay     => 1,
        -max       => 2,
        -nfs       => 0,
    );

    if (!$lockmgr->lock($conf->{lockdir} . "/$0")) {
        die "Cannot get lock on $conf->{lockdir}/$0";
    }
    return 1;
}


#
#
#
sub doit {
    my $conf = shift;

    if ($conf->{file} !~ m!^/!) {
        warn "Must use absolute filename (ie start with '/')\n";
        return undef;
    }

    my $lp = Log::Parse->new(%$conf);

    debug::log("Created Log::Parse with Handlers: $conf->{handlers}") if(DEBUG);

    if (!$lp) {
        print STDERR "Could not create Log::Parse(handlers => '$conf->{handlers}')\n";
        return undef;
    }

    $lp->parse();

    return 1;
}


#
# Start of main program
#
init();

foreach my $file (glob($conf->{confdir} . '/*')) {
    my $cur = read_config_file(file => $file, conf => $conf);
    next unless($cur->{handlers});
    doit($cur);
}

$lockmgr && $lockmgr->unlock($conf->{lockdir} . "/$0");


__END__

=head1 NAME

parselog - generate statistics from log files

=head1 SYNOPSIS

  parselog [-d] [-c config]

=head1 DESCRIPTION

B<parselog> is a generic tool for parsing log files and feeding statistics
into RRD files. It creates no statistics directly itself, but relies on
various perl Log::Parse::* modules and RRD to do the work.

This would normally be called at regular intervals by B<cron(8)>.

=head1 OPTIONS

=head2 -d

Turn on debugging

=head2 -c file

Specify an alternative configuration file instead of the default
/etc/parselog/parselog.conf.

=head1 GLOBAL CONFIGURATION

/etc/parselog/parselog.conf is the global configuration file. This
file contains settings which may be overriden by specific
configurations in /etc/parselog/conf.d/*.

All entries in configuration files are of the form 'key = value'.
Lines beginning with '#' and empty lines are ignored.

These are the allowed parameters:

=head2 confdir

Specifies the directory containing specific configuration files for
various logs. Normally /etc/parselog/conf.d

=head2 lockdir

Specifies the directory where parselog can create lock files. Usually
something like /var/run or /var/lock.

=head2 libdir

Specifies the directory where RRD, state and other permanent files can
be kept. Usually /var/lib/parselog.

=head2 step

The base measurement interval in seconds. Passed to rrdtool when
creating the RRDs. See rrdcreate(1) for details. This item can also
be overridden in log-specific configuration files.

=head2 rrdrra

A space separated list of RRA definitions. Passed to rrdtool when
creating the RRDs. See rrdcreate(1) for details.  This item can also
be overridden in log-specific configuration files.

=head2 domains

A space separated list of domains for which measurements will be taken.
Any data collected for domains which are not in this list will appear
in the 'Other' domain category. The domains Can be Perl regular expressions.

For instance 'domains = example.net' will match against www.example.net and
example.net.com, however 'domains = ^example.net$' will only match
exmample.net.

=cut
head2 report

Specifies the directory where RRD, state and other permanent files can
be kept. Usually /var/lib/parselog.

head2 reporturl

Specifies the RPC-XML url where data will be posted when B<-p> is
used. If this is not set then statistics will only be stored locally.
Even if this is set statistics will not be posted unless B<-p> is
given.

=head1 SPECIFIC CONFIGURATION FILES

/etc/parselog/conf.d (or the value of the 'confdir' setting) contains
configuration files which determine the log files to parse and with
which parser.

All entries in configuration files are of the form 'key = value'.
Lines beginning with '#' and empty lines are ignored.

These are the allowed parameters:

=head2 file

This must be the absolute path (ie beginning with a '/') to the
log file to be parsed.

=head2 filetype

This determines which Log::Read::<handler> module will be used to
read the log file. Current possible values are 'Syslog', 'Exim'
and 'Apache'.

=head2 handler

This determines which Log::Parse::<handler> module will be used to
parse the log file. Current possible values are 'Amavis', 'Exim'
and 'Apache'.

=head2 step

The base measurement interval in seconds. Passed to rrdtool when
creating the RRDs. See rrdcreate(1) for details. If this item is
not defined then the value from the GLOBAL configuration will be used.

=head2 rrdrra

A space separated list of RRA definitions. Passed to rrdtool when
creating the RRDs. See rrdcreate(1) for details. If this item is
not defined then the value from the GLOBAL configuration will be used.

=head1 APACHE CONFIGURATION

Because there is no target/hostname information in the standard Apache
access.log file, and there maybe several virtual server running on the
one host the following item is available for Apache handlers:

=head2 apache_domain

Specify the target of this logfile.

=head1 FILES

/etc/parselog/parselog.conf
/etc/parselog/conf.d/*

=head1 SEE ALSO

L<Log::Parse>,  L<Log::Read>.

B<parselog> grew out of the amavis-stats project at
http://rekudos.net/amavis-stats

=head1 AUTHOR

Mark Lawrence E<lt>nomad@null.netE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2004,2005 Mark Lawrence <nomad@null.net>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

=cut

