#!/usr/bin/env perl
use strict;
use warnings;
use autodie;

use IO::File;

use HTTP::Date;

# For each file specified on the command line we store in this hash the
# corresponding open file handle, the last line read from this file and its
# corresponding timestamp.
my %files;

# Return the time stamp from a line supposed to be roughly in Apache combined
# log format.
sub extract_timestamp
{
    my ($line) = @_;

    # Check that we have something resembling Apache combined log format.
    return undef if $line !~ /^(?:\S+ ){3}\[([^\]]+)\] "/;

    return HTTP::Date::str2time($1);
}

if (!@ARGV) {
    die <<EOF
Usage: $0 <apache-log-file...>

Combines several Apache log files in the combined log format (or any other,
provided that the fourth field of each line is the date string) in a single
file sorted in chronological order on output.
EOF
}

# Open all files.
foreach my $fname (@ARGV) {
    my $fh = IO::File->new($fname, "r");
    $files{$fname}{fh} = $fh;
}

# The main loop, outputting all the lines in order of their timestamps.
while (1) {
    # Find the earliest (i.e. the one with the smallest timestamp) line.
    my $fname_next;
    FILE: foreach my $fname (keys %files) {
        # Read the next (or possible the first) line from this file.
        LINE: while (!defined($files{$fname}{line})) {
            my $line = $files{$fname}{fh}->getline();
            if (!defined($line)) {
                # We exhausted the file without finding more lines in it.
                delete $files{$fname};
                next FILE
            }

            my $t = extract_timestamp($line);
            if (!defined($t)) {
                chomp($line);
                warn "Ignoring malformed line \"$line\" at $fname:$..\n";
                next LINE
            }

            $files{$fname}{line} = $line;
            $files{$fname}{time} = $t;
        }

        if (!defined($fname_next) ||
                $files{$fname}{time} < $files{$fname_next}{time} ) {
            $fname_next = $fname;
        }
    }

    last if !%files;

    # Output and invalidate it.
    print $files{$fname_next}{line};
    $files{$fname_next}{line} = undef;
}
