package MooseX::Role::Loggable;
$MooseX::Role::Loggable::VERSION = '0.113';
# ABSTRACT: Extensive, yet simple, logging role using Log::Dispatchouli

use strict;
use warnings;

use Carp ();
use Safe::Isa;
use Moo::Role;
use MooX::Types::MooseLike::Base qw<Bool Str>;
use Sub::Quote 'quote_sub';
use Log::Dispatchouli;
use namespace::sweep;

my %attr_meth_map = (
    'logger_facility' => 'facility',
    'logger_ident'    => 'ident',
    'log_to_file'     => 'to_file',
    'log_to_stdout'   => 'to_stdout',
    'log_to_stderr'   => 'to_stderr',
    'log_fail_fatal'  => 'fail_fatal',
    'log_muted'       => 'muted',
    'log_quiet_fatal' => 'quiet_fatal',
);

has 'debug' => (
    'is'      => 'ro',
    'isa'     => Bool,
    'default' => sub {0},
);

has 'logger_facility' => (
    'is'      => 'ro',
    'isa'     => Str,
    'default' => sub {'local6'},
);

has 'logger_ident' => (
    'is'      => 'ro',
    'isa'     => Str,
    'default' => sub { ref shift },
);

has 'log_to_file' => (
    'is'      => 'ro',
    'isa'     => Bool,
    'default' => sub {0},
);

has 'log_to_stdout' => (
    'is'      => 'ro',
    'isa'     => Bool,
    'default' => sub {0},
);

has 'log_to_stderr' => (
    'is'      => 'ro',
    'isa'     => Bool,
    'default' => sub {0},
);

has 'log_file' => (
    'is'        => 'ro',
    'isa'       => Str,
    'predicate' => 'has_log_file',
);

has 'log_path' => (
    'is'        => 'ro',
    'isa'       => Str,
    'predicate' => 'has_log_path',
);

has 'log_pid' => (
    'is'      => 'ro',
    'isa'     => Bool,
    'default' => sub {1},
);

has 'log_fail_fatal' => (
    'is'      => 'ro',
    'isa'     => Bool,
    'default' => sub {1},
);

has 'log_muted' => (
    'is'      => 'ro',
    'isa'     => Bool,
    'default' => sub {0},
);

has 'log_quiet_fatal' => (
    'is'  => 'ro',
    'isa' => quote_sub(
        q{
        use Safe::Isa;
        $_[0] || $_[0]->$_isa( ref [] )
            or die "$_[0] must be a string or arrayref"
    }
    ),
    'default' => sub {'stderr'},
);

has 'logger' => (
    'is'  => 'lazy',
    'isa' => quote_sub(
        q{
        use Safe::Isa;
        $_[0]->$_isa('Log::Dispatchouli')        ||
        $_[0]->$_isa('Log::Dispatchouli::Proxy')
            or die "$_[0] must be a Log::Dispatchouli object";
    }
    ),

    'handles' => [
        qw/
            log log_fatal log_debug
            set_debug clear_debug set_prefix clear_prefix set_muted clear_muted
            /
    ],
);

sub _build_logger {
    my $self = shift;
    my %optional;

    foreach my $option (qw<log_file log_path>) {
        my $method = "has_$option";
        if ( $self->$method ) {
            $optional{$option} = $self->$option;
        }
    }

    my $logger = Log::Dispatchouli->new(
        {
            'debug'       => $self->debug,
            'ident'       => $self->logger_ident,
            'facility'    => $self->logger_facility,
            'to_file'     => $self->log_to_file,
            'to_stdout'   => $self->log_to_stdout,
            'to_stderr'   => $self->log_to_stderr,
            'log_pid'     => $self->log_pid,
            'fail_fatal'  => $self->log_fail_fatal,
            'muted'       => $self->log_muted,
            'quiet_fatal' => $self->log_quiet_fatal,
            %optional,
        }
    );

    return $logger;
}

# if we already have a logger, use its values
sub BUILDARGS {
    my $class = shift;
    my %args  = @_;
    my @items = qw<
        debug logger_facility logger_ident
        log_to_file log_to_stdout log_to_stderr log_file log_path
        log_pid log_fail_fatal log_muted log_quiet_fatal
    >;

    if ( exists $args{'logger'} ) {
        $args{'logger'}->$_isa('Log::Dispatchouli')
            || $args{'logger'}->$_isa('Log::Dispatchouli::Proxy')
            or Carp::croak('logger must be a Log::Dispatchouli object');

        foreach my $item (@items) {

            # if value is overridden, don't touch it
            my $attr
                = exists $attr_meth_map{$item}
                ? $attr_meth_map{$item}
                : $item;

            if ( exists $args{$item} ) {

                # override logger configuration
                $args{'logger'}{$attr} = $args{$item};
            } else {

                # override our attributes if it's in logger
                exists $args{'logger'}{$attr}
                    and $args{$item} = $args{'logger'}{$attr};
            }
        }
    }

    return {%args};
}

sub log_fields {
    my $self = shift;
    my $warning
        = '[MooseX::Role::Loggable] Calling ->log_fields() is deprecated, '
        . 'it will be removed in the next version';

    $self->log( { 'level' => 'warning' }, $warning );
    Carp::carp($warning);

    return ( 'logger' => $self->logger );
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

MooseX::Role::Loggable - Extensive, yet simple, logging role using Log::Dispatchouli

=head1 VERSION

version 0.113

=head1 SYNOPSIS

    package My::Object;

    use Moose; # or Moo
    with 'MooseX::Role::Loggable';

    sub do_this {
        my $self = shift;
        $self->set_prefix('[do_this] ');
        $self->log_debug('starting...');
        ...
        $self->log_debug('more stuff');
        $self->clear_prefix;
    }

=head1 DESCRIPTION

This is a role to provide logging ability to whoever consumes it using
L<Log::Dispatchouli>. Once you consume this role, you have attributes and
methods for logging defined automatically.

    package MyObject;
    use Moose # Moo works too
    with 'MooseX::Role::Loggable';

    sub run {
        my $self = shift;

        $self->log('Trying to do something');

        # this only gets written if debug flag is on
        $self->log_debug('Some debugging output');

        $self->log(
            { level => 'critical' },
            'Critical log message',
        );

        $self->log_fatal('Log and die');
    }

This module uses L<Moo> so it takes as little resources as it can by default,
and can seamlessly work with both L<Moo> or L<Moose>.

=head1 Propagating logging definitions

Sometimes your objects create additional object which might want to log
using the same settings. You can simply give them the same logger object.

    package Parent;
    use Moose;
    with 'MooseX::Role::Loggable';

    has child => (
        is      => 'ro',
        isa     => 'Child',
        lazy    => 1,
        builder => '_build_child',
    );

    sub _build_child {
        my $self = shift;
        return Child->new( logger => $self->logger );
    }

=head1 ATTRIBUTES

=head2 debug

A boolean for whether you're in debugging mode or not.

Default: B<no>.

Read-only.

=head2 logger_facility

The facility the logger would use. This is useful for syslog.

Default: B<local6>.

=head2 logger_ident

The ident the logger would use. This is useful for syslog.

Default: B<calling object's class name>.

Read-only.

=head2 log_to_file

A boolean that determines if the logger would log to a file.

Default location of the file is in F</tmp>.

Default: B<no>.

Read-only.

=head2 log_to_stdout

A boolean that determines if the logger would log to STDOUT.

Default: B<no>.

=head2 log_to_stderr

A boolean that determines if the logger would log to STDERR.

Default: B<no>.

=head2 log_file

The leaf name for the log file.

Default: B<undef>

=head2 log_path

The path for the log file.

Default: B<undef>

=head2 log_pid

Whether to append the PID to the log filename.

Default: B<yes>

=head2 log_fail_fatal

Whether failure to log is fatal.

Default: B<yes>

=head2 log_muted

Whether only fatals are logged.

Default: B<no>

=head2 log_quiet_fatal

From L<Log::Dispatchouli>:
I<'stderr' or 'stdout' or an arrayref of zero, one, or both fatal log messages
will not be logged to these>.

Default: B<stderr>

=head2 logger

A L<Log::Dispatchouli> object.

=head1 METHODS

All methods here are imported from L<Log::Dispatchouli>. You can read its
documentation to understand them better.

=head2 log

Log a message.

=head2 log_debug

Log a message only if in debug mode.

=head2 log_fatal

Log a message and die.

=head2 set_debug

Set the debug flag.

=head2 clear_debug

Clear the debug flag.

=head2 set_prefix

Set a prefix for all next messages.

=head2 clear_prefix

Clears the prefix for all next messages.

=head2 set_muted

Sets the mute property, which makes only fatal messages logged.

=head2 clear_muted

Clears the mute property.

=head2 BUILDARGS

You shouldn't care about this. It takes care of propagating attributes
from a given logger (if you provided one) to the attributes this role provides.

=head2 log_fields

B<DEPRECATED>.

Please pass the logger attribute instead:

    SomeObject->new( logger => $parent->logger );

=head1 DEBUGGING

Occassionally you might encounter the following error:

    no ident specified when using Log::Dispatchouli at Loggable.pm line 117.

The problem does not stem from L<MooseX::Role::Loggable>, but from a builder
calling a logging method before the logger is built. Since L<Moo> and L<Moose>
do not assure order of building attributes, some attributes might not yet
exist by the time you need them.

This specific error happens when the C<ident> attribute isn't built by the
time a builder runs. In order to avoid it, the attribute which uses the builder
should be made lazy, and then called in the C<BUILD> method. Here is an
example:

    package Stuff;

    use Moose;
    with 'MooseX::Role::Logger';

    has db => (
        is      => 'ro',
        lazy    => 1,
        builder => '_build_db',
    }

    sub _build_db {
        my $self = shift;
        $self->log_debug('Building DB');
        ...
    }

    sub BUILD {
        my $self = shift;
        $self->db;
    }

This makes the C<db> attribute non-lazy, but during run-time. This will assure
that all the logging attributes are created B<before> you build the C<db>
attribute and call C<log_debug>.

=head1 AUTHOR

Sawyer X <xsawyerx@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2016 by Sawyer X.

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

=cut
