The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Dancer::Logger::Abstract;

use strict;
use warnings;
use Carp;
use base 'Dancer::Engine';

use Dancer::SharedData;
use Dancer::Timer;
use Dancer::Config 'setting';
use POSIX qw/strftime/;

# This is the only method to implement by logger engines.
# It receives the following arguments:
# $msg_level, $msg_content, it gets called only if the configuration allows
# a message of the given level to be logged.
sub _log { confess "_log not implemented" }

my $levels = {

    # levels < 0 are for core only
    core => -10,

    # levels > 0 are for end-users only
    debug   => 1,
    info    => 2,
    warn    => 3,
    warning => 3,
    error   => 4,
};

my $log_formats = {
    simple  => '[%P] %L @%D> %i%m in %f l. %l',
};

sub _log_format {
    my $config = setting('logger_format');

    if ( !$config ) {
        return $log_formats->{simple};
    }

    exists $log_formats->{$config}
      ? return $log_formats->{$config}
      : return $config;
}

sub _should {
    my ($self, $msg_level) = @_;
    my $conf_level = setting('log') || 'debug';

    if (!exists $levels->{$conf_level}) {
        setting('log' => 'debug');
        $conf_level = 'debug';
    }

    return $levels->{$conf_level} <= $levels->{$msg_level};
}

sub format_message {
    my ($self, $level, $message) = @_;
    chomp $message;

    if (!Encode::is_utf8($message) && setting('charset')) {
        $message = Encode::encode(setting('charset'), $message);
    }

    $level = 'warn' if $level eq 'warning';
    $level = sprintf('%5s', $level);

    my $r     = Dancer::SharedData->request;
    my @stack = caller(3);

    my $block_handler = sub {
        my ( $block, $type ) = @_;
        if ( $type eq 't' ) {
            return "[" . strftime( $block, localtime ) . "]";
        }
        elsif ( $type eq 'h' ) {
            return '-' unless defined $r;
            return scalar $r->header($block) || '-';
        }
        else {
            Carp::carp("{$block}$type not supported");
            return "-";
        }
    };

    my $chars_mapping = {
        h => sub {
            defined $r
              ? $r->env->{'HTTP_X_REAL_IP'} || $r->env->{'REMOTE_ADDR'}
              : '-';
        },
        t => sub { Encode::decode(setting('charset') || 'utf8',
                                  POSIX::strftime( "%d/%b/%Y %H:%M:%S", localtime )) },
        T => sub { POSIX::strftime( "%Y-%m-%d %H:%M:%S", localtime  ) },
        u => sub { Encode::decode(setting('charset') || 'utf8',
                                  POSIX::strftime( "%d/%b/%Y %H:%M:%S", gmtime )) },
        U => sub { POSIX::strftime( "%Y-%m-%d %H:%M:%S", gmtime  ) },
        P => sub { $$ },
        L => sub { $level },
        D => sub {
            my $time = Dancer::SharedData->timer->tick;
            return $time;
        },
        m => sub { $message },
        f => sub { $stack[1] || '-' },
        l => sub { $stack[2] || '-' },
        i => sub {
            defined $r ? "[hit #" . $r->id . "]" : "";
        },
    };

    my $char_mapping = sub {
        my $char = shift;

        my $cb = $chars_mapping->{$char};
        unless ($cb) {
            Carp::carp "\%$char not supported.";
            return "-";
        }
        $cb->($char);
    };

    my $fmt = $self->_log_format();

    $fmt =~ s^
        (?:
            \%\{(.+?)\}([a-z])|
            \%([a-zA-Z])
        )
    ^ $1 ? $block_handler->($1, $2) : $char_mapping->($3) ^egx;

    return $fmt."\n";
}

sub core    { $_[0]->_should('core')    and $_[0]->_log('core',    $_[1]) }
sub debug   { $_[0]->_should('debug')   and $_[0]->_log('debug',   $_[1]) }
sub info    { $_[0]->_should('info')    and $_[0]->_log('info',    $_[1]) }
sub warning { $_[0]->_should('warning') and $_[0]->_log('warning', $_[1]) }
sub error   { $_[0]->_should('error')   and $_[0]->_log('error',   $_[1]) }

1;

__END__

=head1 NAME

Dancer::Logger::Abstract - Abstract logging engine for Dancer

=head1 SYNOPSIS

In your configuration file:

    # default
    logger_format: simple
    # [1234] debug @0.12> [hit #123]message from your log in File.pm line 12

    # custom
    logger_format: %m %{%H:%M}t [%{accept_type}h]
    # message from your log [11:59] [text/html]

=head1 DESCRIPTION

This is an abstract logging engine that provides loggers with basic
functionality and some sanity checking.

=head1 CONFIGURATION

=head2 logger_format

This is a format string (or a preset name) to specify the log format.

The possible values are:

=over 4

=item %h

host emitting the request

=item %t

date (local timezone, formatted like %d/%b/%Y %H:%M:%S)

=item %T

date (local timezone, formatted like %Y-%m-%d %H:%M:%S)

=item %u

date (UTC timezone, formatted like %d/%b/%Y %H:%M:%S)

=item %U

date (UTC timezone, formatted like %Y-%m-%d %H:%M:%S)

=item %P

PID

=item %L

log level

=item %D

timer

=item %m

message

=item %f

file name that emit the message

=item %l

line from the file

=item %i

request ID

=item %{$fmt}t

timer formatted with a valid time format

=item %{header}h

header value

=back

There is two preset possible:

=over 4

=item simple

will format the message like: [%P] %L @%D> %m in %f l. %l

=item with_id

will format the message like: [%P] %L @%D> [hit #%i] %m in %f l. %l

=back

=head1 METHODS

=head2 format_message

Provides a common message formatting.

=head2 core

Logs messages as core.

=head2 debug

Logs messages as debug.

=head2 warning

Logs messages as warning.

=head2 error

Logs messages as error.

=head2 info

Logs messages as info.

=head2 _log

A method to override. If your logger does not provide this, it will cause the
application to die.

=head2 _should

Checks a certain level number against a certain level type (core, debug, info
warning, error).

=head1 AUTHOR

Alexis Sukrieh

=head1 LICENSE AND COPYRIGHT

Copyright 2009-2010 Alexis Sukrieh.

This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.