The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Thorium::Roles::Trace;
{
  $Thorium::Roles::Trace::VERSION = '0.510';
}
BEGIN {
  $Thorium::Roles::Trace::AUTHORITY = 'cpan:AFLOTT';
}

# ABSTRACT: Add code tracing and argument dumping to your class

use Thorium::Protection;

use MooseX::Role::Strict;

# core
use Data::Dumper;
use Scalar::Util qw();

# Attributes

has 'tracing' => (
    'is'              => 'rw',
    'isa'             => 'Bool',
    'default'         => 0,
    'trigger'         => \&_set_tracing,
    'documentation' => 'Turn tracing on or off by setting this attribute to true.'
);

has 'trace_meta' => (
    'is'              => 'rw',
    'isa'             => 'Bool',
    'default'         => 0,
    'documentation' => 'Include calls to the objects meta method (Class::MOP) in trace output.'
);

has 'dump_args' => (
    'is'      => 'rw',
    'isa'     => 'Bool',
    'default' => 0,
    'trigger' => \&_set_args_dump,
    'documentation' =>
      'Dump arguments being passed in and out of every method. Note, argument dumping will turn on tracing as well.'
);

has 'dump_args_in' => (
    'is'              => 'rw',
    'isa'             => 'Bool',
    'default'         => 0,
    'trigger'         => \&_set_args_dump_in,
    'documentation' => 'Dump arguments being passed in to a method.'
);

has 'dump_args_out' => (
    'is'              => 'rw',
    'isa'             => 'Bool',
    'default'         => 0,
    'trigger'         => \&_set_args_dump_out,
    'documentation' => 'Dump arguments being passed out of a method.'
);

has 'dump_maxdepth' => (
    'is'              => 'rw',
    'isa'             => 'Maybe[Int]',
    'default'         => undef,
    'documentation' => 'Maximum depth of argument dump - sets C<$Data::Dumper::Maxdepth> locally.'
);

has 'dump_skip_self' => (
    'is'      => 'rw',
    'isa'     => 'Bool',
    'default' => 1,
    'documentation' =>
'Do not include C<$self> in dump. This is true by default. Note, this just blindly skips the first argument in @_!'
);

has 'trace_dbi_calls' => (
    'is'              => 'rw',
    'isa'             => 'Bool',
    'default'         => 0,
    'trigger'         => \&_set_dbi_handlers,
    'documentation' => 'Add simple tracing callbacks to some DBI methods.'
);

has '_dbh_to_trace' => (
    'is'      => 'rw',
    'isa'     => 'Maybe[Object]',
    'default' => undef,
);

# Builders

# Triggers

# these need to be skipped to avoid deep recursion
my @_skip_methods = qw(
  tracing
  trace_meta
  dump_args
  dump_args_in
  dump_args_out
  dump_maxdepth
  dump_skip_self
  log
);

sub _set_tracing {
    my $self = shift;
    my ($new) = @_;

    # if tracing is being set to false, short-circuit and return
    # also, we only need to do this once the first time set to true
    # XXX: is there a way to drop method modifiers?
    return unless $new;

    # Get methods
    my $class   = ref $self;
    my $meta    = $self->meta;
    my @methods = $meta->get_all_method_names();

    # do we have a log object as an attribute?
    my $is_logging = 0;
    $is_logging = 1 if 'log' ~~ @methods && Scalar::Util::blessed $self->log eq 'Thorium::Log';

    if ($meta->is_immutable) {
        $self->meta->make_mutable;
    }

    # go over methods and declare before and after method modifiers that handle
    # tracing and method dumping.
    for my $method (@methods) {

        # avoid deep recursion
        next if $method ~~ @_skip_methods;
        next if $method eq 'meta' && !$self->trace_meta;
        push(@_skip_methods, $method);    # don't add next time

        $meta->add_around_method_modifier(
            $method,
            sub {
                my $next = shift;
                my $self = shift;

                my $msg = "##### Entering $class" . "::$method #####\n";

                if ($self->dump_args_in) {
                    local $Data::Dumper::Maxdepth = $self->dump_maxdepth;
                    $msg .= "*** $method args ***\n";
                    $msg .= $self->dump_skip_self ? Dumper(\@_) : Dumper([ $self, @_ ]);
                }

                $is_logging && $self->log->enabled ? $self->log->trace($msg) : warn $msg;

                my (@rl, $rs);

                if (wantarray) {
                    @rl = $self->$next(@_);
                }
                else {
                    $rs = $self->$next(@_);
                }

                $msg = "##### Leaving $class" . "::$method #####\n";

                if ($self->dump_args_out) {
                    local $Data::Dumper::Maxdepth = $self->dump_maxdepth;
                    $msg .= "*** $method returned ***\n";
                    (wantarray) ? $msg .= Dumper(\@rl) : $msg .= Dumper($rs);
                }

                $is_logging && $self->log->enabled ? $self->log->trace($msg) : warn $msg;

                (wantarray) ? return @rl : return $rs;
            }
        );
    }

}    # _set_tracing

sub _set_args_dump {
    my $self = shift;
    my ($new) = @_;

    # if we're turning on arg dumping turn on tracing too
    $self->tracing($new) if $new;

    $self->dump_args_in($new);
    $self->dump_args_out($new);
}

sub _set_args_dump_in {
    my $self = shift;
    $self->tracing($_[0]) if $_[0];
}

sub _set_args_dump_out {
    my $self = shift;
    $self->tracing($_[0]) if $_[0];
}

sub _set_dbi_handlers {
    my $self = shift;

    return unless ($INC{'DBI.pm'} && $DBI::VERSION > 1.5);

    my ($dbh) = $self->_dbh_to_trace;

    my $meta    = $self->meta;
    my @methods = $meta->get_all_method_names();

    # do we have a log object as an attribute?
    my $is_logging = 0;
    $is_logging = 1 if 'log' ~~ @methods && Scalar::Util::blessed $self->log eq 'Thorium::Log';

    $dbh->{'Callbacks'} = {
        'prepare' => sub {
            my (undef, $query) = @_;

            my $msg = "DBI Preparing SQL: $query";

            $is_logging ? $self->log->trace($msg) : warn $msg;
            return;
        },
        'do' => sub {
            my (undef, $query) = @_;
            my $msg = "DBI do: $query";

            $is_logging ? $self->log->trace($msg) : warn $msg;
            return;
        },
        'connect' => sub {
            my $msg = 'DBI Connected';

            $is_logging ? $self->log->trace($msg) : warn $msg;
            return;
        },
        'disconnect' => sub {
            my $msg = 'DBI Disconnected';

            $is_logging ? $self->log->trace($msg) : warn $msg;
            return;
        },
    };

    return;
}

# Method modifiers
around 'trace_dbi_calls' => sub {
    my $orig = shift;
    my $self = shift;
    my $dbh  = shift;

    if (!$dbh) {
        return $self->$orig(0);
    }
    else {
        $self->_dbh_to_trace($dbh);
        return $self->$orig(1);
    }
};

no Moose::Role;

1;



=pod

=head1 NAME

Thorium::Roles::Trace - Add code tracing and argument dumping to your class

=head1 VERSION

version 0.510

=head1 SYNOPSIS

    package MyModule;
    
    use Moose;
    with qw(Thorium::Roles::Trace);
    
    # ... methods, attributes and such

Then when using an object of your class

    use MyModule;
    
    # new object, with tracing turned on
    
    my $obj = MyModule->new(tacing => 1);
    
    $obj->method(); # method and all calls from $self interally are traced
    # ##### Entering MyModule::method #####
    # ##### Leaving MyModule::method #####
    
    $obj->dump_args_in(1);
    
    $obj->method2({one => 1}); # dump arguments being passed into methods
    # ##### Entering MyModule::method2 #####
    # *** method2 args ***
    # $VAR1 = [                    
    #           {
    #             'one' => 1
    #           }
    #         ];
    # ##### Leaving MyModule::method2 #####

Note that methods added after tracing is set will not be logged until tracing is
set again. Methods set with C<*MyModule::method = sub {}> will never be seen; use
C<$meta->add_method> instead!

=head1 DESCRIPTION

This role adds tracing to arguments, sub-routines entering/leaving and returned
data to L<Thorium::Log> logging or C<STDERR> if no logging sub-system found.

=head1 WARNING!

Do B<not> keep tracing enabled in production. It has measurable performance
penalties! Tracing should be a temporary debugging action. Proper logging is a
permanent debugging action.

=head1 MORE WARNING!

Once you turn on tracing it is not possible to turn off as the original
references to the sub-routines are embedded into new sub-routines and as a
result, lost. It is technically possible add the ability to turn off tracing,
but for the sake of simplicity and for the note listed in L</"WARNING!"> the
feature is absent.

=head1 ATTRIBUTES

=head2 Optional Attributes

=over

=item * tracing (rw, Bool)

Turn tracing on or off by setting this attribute to true. Defaults to false.

=item * trace_meta (rw, Bool)

Include calls to the objects meta method (Class::MOP) in trace output. Defaults
to false.

=item * dump_args (rw, Bool)

Dump arguments being passed in and out of every method. Note, argument dumping
will turn on tracing as well. Defaults to false.

=item * dump_args_in (rw, Bool)

Dump arguments being passed in to a method. Defaults to false.

=item * dump_args_out (rw, Bool)

Dump arguments being passed out of a method. Defaults to false.

=item * dump_maxdepth (rw, Maybe[Int])

Maximum depth of argument dump - sets C<$Data::Dumper::Maxdepth> locally. Defaults to false.

=item * dump_skip_self (rw, Bool)

Do not include C<$self> in dump. This is true by default. Note, this just blindly
skips the first argument in @_!

=item * trace_dbi_calls (rw, DBH|Bool)

Add simple tracing callbacks to some L<DBI> methods:

=over

=item * connect

=item * prepare

=item * do

=item * disconnect

=back

=back

=head1 PUBLIC API METHODS

None. This is a L<Moose::Role>.

=head1 AUTHOR

Adam Flott <adam@npjh.com>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2011 by Adam Flott <adam@npjh.com>, CIDC.

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


__END__