#!/usr/bin/perl
#

=pod

=head1 SUMMARY

Works with multilog to analyse and summarise log entries generated by the logterse plugin.  It is designed
to be invoked by multilog at log-rotation time.  This is specified by an argument to multilog similar to:

=over 4

multilog t !/path/to/qplogsumm ./main

=back

When qplogsumm is invoked, each line will be echoed, meaning the stored log is unchanged, but summary
information will be written to fd 5 and so stored in the 'state' file by multilog.

This file is fed in on fd 4 at the beginning of the next log rotation, so running totals, etc can be maintained.

=head1 State file format:

One entry per line containing three fields separated by whitespace:

=over 4

=item 1. Disposition (plugin) name.

=item 2. tai64n timestamp recording the first time it was seen in a log.

=item 3. long-term running total.

=back

A disposition is effectively the plugin name that called DENY or the string 'queued' for
messages that made it through.

A line containing a disposition name of LOGFILE_EPOCH and a timestamp for the earliest known log entry.

Other derived data, such as percentages etc. can also appear in the file, commented
by a # character.  This will be ignored on the next intake.

=head1 AUTHOR

Charles Butcher

=head1 VERSION

This is release 1.0

=cut

use strict;
use POSIX qw(strftime);


my $FS = "\t";	# field separator used by logterse plugin
my %disp;   	# hash of dispositions

if (open PREVIOUS, "<&4")
{
    while (<PREVIOUS>)
    {
	chomp();
	next if m/^#/;
	next if m/^\s*$/;
	my ($plug_name, $plug_epoch, $plug_cumulative) = split /\s+/;
	my $c = { epoch => $plug_epoch, cum => $plug_cumulative, curr => 0 };
	$disp{$plug_name} = $c;
    }

    close PREVIOUS;
}

my $first_timestamp = 0;
my $last_timestamp  = 0;


while (<>)
{
    print;
    chomp;
    next unless m/terse plugin/;

    my ($timestamp_part, $log_part) = split '`';
    my ($current_timestamp) = split /\s/, $timestamp_part;
    $first_timestamp = $current_timestamp unless $first_timestamp;
    $last_timestamp = $current_timestamp;

    my (@log_items) = split $FS, $log_part;
    my $disposition = $log_items[5];
    next unless defined $disposition;
    
    if ($disp{$disposition})
    {
	$disp{$disposition}->{curr} += 1;
    }
    else  # a new plugin -- make a note of when it first appeared
    {
	my $c = { epoch => $current_timestamp, cum => 0, curr => 1 };
	$disp{$disposition} = $c;
    }
}


#
# Set overall epoch
#
if (!exists $disp{'LOGFILE_EPOCH'})
{
    my $c = { epoch => $first_timestamp, cum => 0, curr => 0};
    $disp{'LOGFILE_EPOCH'} = $c;
}

my $current_total    = 0;
my $cumulative_total = 0;

open HOLDOVER, ">&5" and select HOLDOVER;

#
# Output cumulative values for intake the next time a log is processed
#
for my $c (keys %disp)
{
    $disp{$c}->{cum} += $disp{$c}->{curr};
    $current_total += $disp{$c}->{curr};
    $cumulative_total += $disp{$c}->{cum};

    printf "%-30.30s %s %12d\n", $c, $disp{$c}->{epoch}, $disp{$c}->{cum};
}

#
# Output current logfile stats
#

my $current_elapsed = tai64diff($last_timestamp, $first_timestamp);

printf "#
# Most recent logfile
# -------------------
#
# Start  : %s
# Finish : %s
# Elapsed: %s
#
# Total transactions : %9d
# Average tx per hour: %9d
",
    tai64utc($first_timestamp),
    tai64utc($last_timestamp),
    seconds_to_days($current_elapsed),
    $current_total,
    $current_total / ($current_elapsed / 3600),
    ;

#
# Output cumulative log stats
#
my $cumulative_elapsed = tai64diff($last_timestamp, $disp{'LOGFILE_EPOCH'}->{epoch});

printf "#
# Cumulative Totals
# -----------------
#
# Start  : %s
# Finish : %s
# Elapsed: %s
#
# Total transactions : %12d
# Average tx per hour: %12d
",
    tai64utc($disp{'LOGFILE_EPOCH'}->{epoch}),
    tai64utc($last_timestamp),
    seconds_to_days($cumulative_elapsed),
    $cumulative_total,
    $cumulative_total / ($cumulative_elapsed / 3600),
    ;


# 
# Output per-plugin stats
#

print "#
#                                 Most Recent Logfile      Cumulative Totals
# Disposition (plugin)            Total       Avg/Day      Total       Avg/Day
# ----------------------------------------------------------------------------\n"; 

my $printf_format = "# %-30.30s %6d %3d%% %8d %10d %3d%% %8d\n";

foreach my $c (sort { $disp{$b}->{curr} <=> $disp{$a}->{curr} } keys %disp)
{
    next if ($c eq 'LOGFILE_EPOCH');

    printf $printf_format, 
    $c,
    $disp{$c}->{curr},
    $disp{$c}->{curr} / $current_total * 100,
    $disp{$c}->{curr} / ($current_elapsed / 86400),
    $disp{$c}->{cum},
    $disp{$c}->{cum} / $cumulative_total * 100,
    $disp{$c}->{cum} / (tai64diff($last_timestamp, $disp{$c}->{epoch}) / 86400),
    ;
}

print "# ----------------------------------------------------------------------------\n";
printf $printf_format, 
    'TOTALS',
    $current_total,
    100,
    $current_total / ($current_elapsed / 86400),
    $cumulative_total,
    100,
    $cumulative_total / ($cumulative_elapsed / 86400),
    ;

exit 0;


sub tai64utc {
    my ($s) = @_;

    # @400000003f6c7bc5253bf98c
    # 0123456789012345678901234
    # 0         1         2
    #   |-------------||------|
    if (substr($s, 0, 2) eq '@4') {
        my $ts = hex(substr($s, 2, 15));
        $s = strftime('%Y-%m-%d %H:%M:%S', gmtime($ts));
    }
    return $s;
}

#
# Return difference in seconds
#
sub tai64diff
{
    my ($s1, $s2) = @_;

    # @400000003f6c7bc5253bf98c
    # 0123456789012345678901234
    # 0         1         2
    #   |-------------||------|
    if (substr($s1, 0, 2) eq '@4' and substr($s2, 0, 2) eq '@4')
    {
        my $ts1 = hex(substr($s1, 2, 15));
        my $ts2 = hex(substr($s2, 2, 15));
	return $ts1 - $ts2;
    }
    else
    {
	return 0;
    }
}


#
# Return an english phrase representing a number of seconds
#
sub seconds_to_days
{
    my ($secs) = @_;

    my $phrase = sprintf "%d days, ", ($secs / 86400);
    $secs %= 86400;
    $phrase .= sprintf "%d hours, ", ($secs / 3600);
    $secs %= 3600;
    $phrase .= sprintf "%d mins, %d secs", ($secs / 60), ($secs % 60);
}
