File Coverage

blib/lib/Log/Any/Proxy/WithStackTrace.pm
Criterion Covered Total %
statement 48 58 82.7
branch 12 20 60.0
condition 6 9 66.6
subroutine 16 17 94.1
pod 3 3 100.0
total 85 107 79.4


line stmt bran cond sub pod time code
1 1     1   5155 use 5.008001;
  1         2  
2 1     1   4 use strict;
  1         1  
  1         16  
3 1     1   3 use warnings;
  1         1  
  1         52  
4              
5             package Log::Any::Proxy::WithStackTrace;
6              
7             # ABSTRACT: Log::Any proxy to upgrade string errors to objects with stack traces
8             our $VERSION = '1.718';
9              
10 1     1   4 use Log::Any::Proxy;
  1         1  
  1         49  
11             our @ISA = qw/Log::Any::Proxy/;
12              
13 1     1   4 use Devel::StackTrace 2.00;
  1         15  
  1         34  
14 1     1   4 use Log::Any::Adapter::Util ();
  1         1  
  1         21  
15 1     1   3 use Scalar::Util qw/blessed reftype/;
  1         1  
  1         43  
16 1     1   3 use overload;
  1         2  
  1         4  
17              
18             #pod =head1 SYNOPSIS
19             #pod
20             #pod use Log::Any qw( $log, proxy_class => 'WithStackTrace' );
21             #pod
22             #pod # Allow stack trace call stack arguments to be logged:
23             #pod use Log::Any qw( $log, proxy_class => 'WithStackTrace',
24             #pod proxy_show_stack_trace_args => 1 );
25             #pod
26             #pod # Configure some adapter that knows how to:
27             #pod # 1) handle structured data, and
28             #pod # 2) handle message objects which have a "stack_trace" method:
29             #pod Log::Any::Adapter->set($adapter);
30             #pod
31             #pod $log->error("Help!"); # stack trace gets automatically added,
32             #pod # starting from this line of code
33             #pod
34             #pod =head1 DESCRIPTION
35             #pod
36             #pod Some log adapters, like L, are able to
37             #pod take advantage of being passed message objects that contain a stack
38             #pod trace. However if a stack trace is not available, and fallback logic is
39             #pod used to generate one, the resulting trace can be confusing if it begins
40             #pod relative to where the log adapter was called, and not relative to where
41             #pod the logging method was originally called.
42             #pod
43             #pod With this proxy in place, if any logging method is called with a log
44             #pod message that is a non-reference scalar (i.e. a string), that log message
45             #pod will be upgraded into a C object with a
46             #pod C method, and that method will return a trace relative to
47             #pod where the logging method was called. A string overload is provided on
48             #pod the object to return the original log message.
49             #pod
50             #pod Additionally, any call stack arguments in the stack trace will be
51             #pod deleted before logging, to avoid accidentally logging sensitive data.
52             #pod This happens both for message objects that were auto-generated from
53             #pod string messages, as well as for message objects that were passed in
54             #pod directly (if they appear to have a stack trace method). This default
55             #pod argument scrubbing behavior can be turned off by specifying a true value
56             #pod for the C import flag.
57             #pod
58             #pod B This proxy should be used with a L that
59             #pod is configured to handle structured data. Otherwise the object created
60             #pod here will just get stringified before it can be used to access the stack
61             #pod trace.
62             #pod
63             #pod =cut
64              
65             {
66             package # hide from PAUSE indexer
67             Log::Any::MessageWithStackTrace;
68              
69 1     1   90 use overload '""' => \&stringify;
  1         3  
  1         5  
70              
71             sub new
72             {
73 6     6   23 my ($class, $message, %opts) = @_;
74              
75             return bless {
76             message => $message,
77             stack_trace => Devel::StackTrace->new(
78             # Filter e.g "Log::Any::Proxy", "My::Log::Any::Proxy", etc.
79             ignore_package => [ qr/(?:^|::)Log::Any(?:::|$)/ ],
80             no_args => $opts{no_args},
81 6         57 ),
82             }, $class;
83             }
84              
85 16     16   2834 sub stringify { $_[0]->{message} }
86              
87 4     4   6044 sub stack_trace { $_[0]->{stack_trace} }
88             }
89              
90             #pod =head1 METHODS
91             #pod
92             #pod =head2 maybe_upgrade_with_stack_trace
93             #pod
94             #pod @args = $self->maybe_upgrade_with_stack_trace(@args);
95             #pod
96             #pod This is an internal-use method that will convert a non-reference scalar
97             #pod message into a C object with a
98             #pod C method. A string overload is provided to return the
99             #pod original message.
100             #pod
101             #pod Stack trace args are scrubbed out in case they contain sensitive data,
102             #pod unless the C option has been set.
103             #pod
104             #pod =cut
105              
106             sub maybe_upgrade_with_stack_trace
107             {
108 14     14 1 34 my ($self, @args) = @_;
109              
110             # We expect a message, optionally followed by a structured data
111             # context hashref. Bail if we get anything other than that rather
112             # than guess what the caller might be trying to do:
113 14 100 100     85 return @args unless @args == 1 ||
      100        
114             ( @args == 2 && ref $args[1] eq 'HASH' );
115              
116 10 100       31 if (ref $args[0]) {
117             $self->maybe_delete_stack_trace_args($args[0])
118 4 100       19 unless $self->{proxy_show_stack_trace_args};
119             }
120             else {
121             $args[0] = Log::Any::MessageWithStackTrace->new(
122             $args[0],
123             no_args => !$self->{proxy_show_stack_trace_args},
124 6         110 );
125             }
126              
127 10         1411 return @args;
128             }
129              
130             #pod =head2 maybe_delete_stack_trace_args
131             #pod
132             #pod $self->maybe_delete_stack_trace_args($arg);
133             #pod
134             #pod This is an internal-use method that, given a single argument that is a
135             #pod reference, tries to figure out whether the argument is an object with a
136             #pod stack trace, and if so tries to delete any stack trace args.
137             #pod
138             #pod The logic is based on L.
139             #pod
140             #pod It specifically looks for objects with a C method (which
141             #pod should catch anything that does L, including anything
142             #pod that does L), or a C method (used by
143             #pod L and L and friends).
144             #pod
145             #pod It specifically ignores L objects, because their stack
146             #pod traces don't contain any call stack args.
147             #pod
148             #pod =cut
149              
150             sub maybe_delete_stack_trace_args
151             {
152 2     2 1 6 my ($self, $arg) = @_;
153              
154 2 100       8 return unless blessed $arg;
155              
156 1 50       22 if ($arg->can('stack_trace')) {
    50          
    50          
157             # This should catch anything that does StackTrace::Auto,
158             # including anything that does Throwable::Error.
159 0         0 my $trace = $arg->stack_trace;
160 0         0 $self->delete_args_from_stack_trace($trace);
161             }
162             elsif ($arg->isa('Mojo::Exception')) {
163             # Skip these, they don't have args in their stack traces.
164             }
165             elsif ($arg->can('trace')) {
166             # This should catch Exception::Class and Moose::Exception and
167             # friends. Make sure to check for the "trace" method *after*
168             # skipping the Mojo::Exception objects, because those also have
169             # a "trace" method.
170 0         0 my $trace = $arg->trace;
171 0         0 $self->delete_args_from_stack_trace($trace);
172             }
173              
174 1         3 return;
175             }
176              
177             my %aliases = Log::Any::Adapter::Util::log_level_aliases();
178              
179             # Set up methods/aliases and detection methods/aliases
180             foreach my $name ( Log::Any::Adapter::Util::logging_methods(), keys(%aliases) )
181             {
182             my $super_name = "SUPER::" . $name;
183 1     1   460 no strict 'refs';
  1         2  
  1         229  
184             *{$name} = sub {
185 14     14   17411 my ($self, @args) = @_;
186 14         48 @args = $self->maybe_upgrade_with_stack_trace(@args);
187 14         70 my $response = $self->$super_name(@args);
188 14 50       37 return $response if defined wantarray;
189 14         81 return;
190             };
191             }
192              
193             #pod =head2 delete_args_from_stack_trace($trace)
194             #pod
195             #pod $self->delete_args_from_stack_trace($trace)
196             #pod
197             #pod To scrub potentially sensitive data from C arguments,
198             #pod this method deletes arguments from all of the C
199             #pod in the trace.
200             #pod
201             #pod =cut
202              
203             sub delete_args_from_stack_trace
204             {
205 0     0 1   my ($self, $trace) = @_;
206              
207 0 0 0       return unless $trace && $trace->can('frames');
208              
209 0           foreach my $frame ($trace->frames) {
210 0 0         next unless $frame->{args};
211 0           $frame->{args} = [];
212             }
213              
214 0           return;
215             }
216              
217             1;
218              
219             __END__