package Plack::Middleware::AxsLog;

use strict;
use warnings;
use parent qw/Plack::Middleware/;
use Plack::Util;
use Time::HiRes qw/gettimeofday/;
use Plack::Util::Accessor qw/response_time combined ltsv error_only long_response_time logger/;
use POSIX qw//;
use Time::Local qw//;
use HTTP::Status qw//;

our $VERSION = '0.03';

## copy from Plack::Middleware::AccessLog
my $tzoffset = POSIX::strftime("%z", localtime);
if ( $tzoffset !~ /^[+-]\d{4}$/ ) {
    my @t = localtime(time);
    my $s = Time::Local::timegm(@t) - Time::Local::timelocal(@t);
    $tzoffset = sprintf '%+03d%02u', int($s/3600), $s % 3600;
}
my @abbr = qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec );

sub prepare_app {
    my $self = shift;
    $self->combined(1) if ! defined $self->combined;
    $self->response_time(0) if ! defined $self->response_time;
    $self->error_only(0) if ! defined $self->error_only;
    $self->long_response_time(0) if ! defined $self->long_response_time;
}

sub call {
    my $self = shift;
    my($env) = @_;

    my $t0 = [gettimeofday];

    my $res = $self->app->($env);
    if ( ref($res) && ref($res) eq 'ARRAY' ) {
        my $length = Plack::Util::content_length($res->[2]);
        if ( defined $length ) {
            $self->log_line($t0, $env,$res,$length);
            return $res;
        }        
    }
    Plack::Util::response_cb($res, sub {
        my $res = shift;
        my $length = Plack::Util::content_length($res->[2]);
        if ( defined $length ) {
            $self->log_line($t0, $env,$res,$length);
            return;
        }
        return sub {
            my $chunk = shift;
            if ( ! defined $chunk ) {
                $self->log_line($t0, $env,$res,$length);
                return;
            }
            $length += length($chunk);
            return $chunk;
        };	
    });
}

sub log_line {
    my $self = shift;
    my ($t0, $env, $res, $length) = @_;

    my $elapsed = int(Time::HiRes::tv_interval($t0) * 1_000_000);

    unless (
         ( $self->{long_response_time} == 0 && !$self->{error_only} )
      || ( $self->{long_response_time} != 0 && $elapsed >= $self->{long_response_time} ) 
      || ( $self->{error_only} && HTTP::Status::is_error($res->[0]) ) 
    ) {
        return;
    }

    my @lt = localtime($t0->[0]);
    my $t = sprintf '%02d/%s/%04d:%02d:%02d:%02d %s', $lt[3], $abbr[$lt[4]], $lt[5]+1900, 
        $lt[2], $lt[1], $lt[0], $tzoffset;
    my $log_line;

    if ( $self->{ltsv} ) {
        #host:%h user:%u time:%t req:%r status:%>s size:%b referer:%{Referer}i ua:%{User-agent}i
        $log_line =
            "host:" . _string($env->{REMOTE_ADDR}) . "\t"
          . "user:" . _string($env->{REMOTE_USER}) . "\t"
          . "time:[" .$t . "]\t"
          . "req:" . _safe($env->{REQUEST_METHOD} . " " . $env->{REQUEST_URI} . " " . $env->{SERVER_PROTOCOL}) . "\t"
          . "status:" . $res->[0] . "\t"
          . "size:" . (defined $length ? "$length" : '-') . "\t"
          . "referer:" . _string($env->{HTTP_REFERER}) . "\t"
          . "ua:" .  _string($env->{HTTP_USER_AGENT}) 
          . ( $self->{response_time} ? "\ttaken:$elapsed" : '')
          . "\n";
    }
    else {
        $log_line =  _string($env->{REMOTE_ADDR}) . " "
            . '- '
            . _string($env->{REMOTE_USER}) . " "
            . q![!. $t . q!] !
            . _safe(q!"! . $env->{REQUEST_METHOD} . " " . $env->{REQUEST_URI} . " " . $env->{SERVER_PROTOCOL} . q!" !)
            . $res->[0] . " "
            . (defined $length ? "$length" : '-')
            . ($self->{combined} ? q! "! . _string($env->{HTTP_REFERER}) . q!" ! : '')
            . ($self->{combined} ? q!"! . _string($env->{HTTP_USER_AGENT}) . q!"! : '')
            . ($self->{response_time} ? " $elapsed" : '')
            . "\n";
    }

    if ( ! $self->{logger} ) {
        $env->{'psgi.errors'}->print($log_line);
    }
    else {
        $self->{logger}->($log_line);
    }
}

sub _safe {
    my $string = shift;
    $string =~ s/([^[:print:]])/"\\x" . unpack("H*", $1)/eg
        if defined $string;
    $string;
}

sub _string {
    my $string = shift;
    return '-' if ! defined $string;
    return '-' if ! length $string;
    _safe($string);
}

1;
__END__

=head1 NAME

Plack::Middleware::AxsLog - Fixed format but Fast AccessLog Middleware

=head1 SYNOPSIS

  use Plack::Builder;

  builder {
      enable 'AxsLog',
        combined => 1,
        response_time => 1,
        error_only => 1,
      $app
  };

=head1 DESCRIPTION

Alternative implementation of Plack::Middleware::AccessLog.
Only supports combined, common and ltsv format, but 3x-4x faster than Plack::Middleware::AccessLog 
in micro benchmarking.
AxsLog also supports filter logs by response_time and status code.

=head1 LOG FORMAT

AxsLog supports combined, common and ltsv format. And adds elapsed time in microseconds to last of log line

=over 4

=item combined (NCSA extended/combined log format)

  %h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\" %D
  => 127.0.0.1 - - [23/Aug/2012:00:52:15 +0900] "GET / HTTP/1.1" 200 645 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.79 Safari/537.1" 10941

=item common (Common Log Format)

  %h %l %u %t \"%r\" %>s %b %D 
  => 127.0.0.1 - - [23/Aug/2012:00:52:15 +0900] "GET / HTTP/1.0" 200 645 10941

=item LTSV (Labeled Tab-separated Values)

  host:%h<TAB>user:%u<TAB>time:%t<TAB>req:%r<TAB>status:%>s<TAB>size:%b<TAB>referer:%{Referer}i<TAB>ua:%{User-agent}i<TAB>taken:%D
  => host:127.0.0.1<TAB>user:-<TAB>time:[23/Aug/2012:00:52:15 +0900]<TAB>req:GET / HTTP/1.1<TAB>status:200<TAB>size:645<TAB>"referer:-<TAB>ua:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.79 Safari/537.1<TAB>taken:10941

See also L<http://ltsv.org/>

=back

=head1 CONFIGURATION

=over 4

=item combined: Bool

log format. if disabled, "common" format used. default: 1 (combined format used)

=item ltsv: Bool

use ltsv log format. default: 0

=item response_time: Bool

Adds time taken to serve the request. default: 0

=item error_only: Bool

Display logs if response status is error (4xx or 5xx). default: 0

=item long_response_time: Int (microseconds)

Display log if time taken to serve the request is above long_response_time. default: 0 (all request logged)

=item logger: Coderef

Callback to print logs. default:none ( output to psgi.errors )

  use File::RotateLogs;
  my $logger = File::RotateLogs->new();

  builder {
      enable 'AxsLog',
        logger => sub { $logger->print(@_) }
      $app
  };

=back

=head1 AUTHOR

Masahiro Nagano E<lt>kazeburo {at} gmail.comE<gt>

=head1 SEE ALSO

L<Plack::Middleware::AccessLog>

=head1 LICENSE

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut
