File Coverage

blib/lib/Maypole/Plugin/Trace.pm
Criterion Covered Total %
statement 33 154 21.4
branch 0 74 0.0
condition 0 15 0.0
subroutine 11 19 57.8
pod 1 1 100.0
total 45 263 17.1


line stmt bran cond sub pod time code
1             package Maypole::Plugin::Trace;
2              
3 1     1   42988 use warnings;
  1         4  
  1         31  
4 1     1   7 use strict;
  1         1  
  1         35  
5              
6 1     1   1102 use NEXT;
  1         6716  
  1         39  
7 1     1   1072 use Class::ISA();
  1         3276  
  1         27  
8 1     1   1150 use Class::Inspector();
  1         5647  
  1         25  
9 1     1   22015 use Data::Dumper();
  1         9998  
  1         29  
10 1     1   9 use Scalar::Util();
  1         2  
  1         164  
11              
12 1     1   6 use base 'Class::Data::Inheritable';
  1         2  
  1         11423  
13              
14             our $VERSION = '0.1';
15              
16             # default to the most useful level
17             __PACKAGE__->mk_classdata(trace_level => 2);
18             __PACKAGE__->mk_classdata(only_trace_exported => 0);
19             __PACKAGE__->mk_classdata('extra_trace_classes');
20             __PACKAGE__->mk_classdata('trace_path');
21              
22             =head1 NAME
23              
24             Maypole::Plugin::Trace - trace calls in Maypole
25              
26             =cut
27              
28             =head1 SYNOPSIS
29              
30             use Maypole::Application qw/Trace/;
31            
32             # options:
33             __PACKAGE__->trace_level(3);
34             __PACKAGE__->only_trace_exported(1);
35              
36             =head1 DESCRIPTION
37              
38             Prints a trace of method entries and exits to C.
39              
40             B.
41              
42             =over
43              
44             =item trace_level
45              
46             __PACKAGE__->trace_level(1);
47            
48             The default C is set to 2.
49              
50             The trace level must be set B>.
51              
52             Level Output
53             ======================================================================
54             0 none
55             1 method entry and exit
56             2 as above, but prints method arguments and return values
57             3 uses Data::Dumper to expand method arguments and return values
58             within Exported methods
59             4 uses Data::Dumper to expand method arguments and return values
60             within all methods
61             5 as 2, but also reports private methods (single leading _ in name)
62             6 as 3, but also reports private methods (single leading _ in name)
63             7 as 4, but also reports private methods (single leading _ in name)
64            
65             Tracing is implemented for packages in the Maypole namespace, and in your
66             application's namespace.
67              
68             The characters C are printed in the left margin to indicate when an exported
69             method is being processed.
70              
71             At trace level 2, objects, e.g. in class C, are represented as
72             C. This is to avoid potential overloaded stringification, which
73             causes deep recursion errors.
74              
75             B: trace output is only generated for exported methods when they are
76             called via Maypole's own controller mechanism. So, for example, if a custom
77             method directly calls an exported method, the entry to and exit from the
78             exported method will not be registered in the trace output. This is a known bug,
79             suggestions for how to fix it would be great.
80              
81             =item only_trace_exported
82              
83             __PACKAGE__->only_trace_exported(1)
84            
85             Turn off tracing except within Exported methods. Default is 0 - trace all
86             methods.
87              
88             =item extra_trace_classes
89              
90             __PACKAGE__->extra_trace_classes('Some::Problem::Package');
91            
92             # or
93             __PACKAGE__->extra_trace_classes( [ 'Some::Problem::Package',
94             'Another::Buggy::Monster',
95             ] );
96            
97             Adds the specified package(s) to the list of traced packages.
98              
99             =item trace_path
100              
101             True or false, default false.
102              
103             Shows the request path in trace output.
104              
105             No path is shown for methods that do not include the Maypole request object in
106             their parameters. This includes methods run before or after a request, most
107             methods in non-Maypole packages, and some methods within the Maypole stack.
108             Also, the path is not available until after C has returned.
109              
110             =item setup
111              
112             Configures tracing.
113              
114             =back
115              
116             =cut
117              
118             sub setup
119             {
120 0     0 1   my $class = shift;
121            
122             # load models etc first
123 0           $class->NEXT::DISTINCT::setup(@_);
124            
125             # our version manually traces Exported methods
126             {
127 1     1   3301 no warnings 'redefine';
  1         3  
  1         3247  
  0            
128 0           *Maypole::Model::Base::process = \&__process;
129             }
130            
131 0 0         my $trace_level = $class->trace_level or return;
132 0           my $show_private;
133 0 0         if ($trace_level == 5)
134             {
135 0           $show_private = 1;
136 0           $trace_level = 2;
137             }
138 0 0         if ($trace_level == 6)
139             {
140 0           $show_private = 1;
141 0           $trace_level = 3;
142             }
143 0 0         if ($trace_level == 7)
144             {
145 0           $show_private = 1;
146 0           $trace_level = 4;
147             }
148            
149 0           my @classes = Class::ISA::self_and_super_path($class);
150 0           push @classes, Class::ISA::self_and_super_path($class->config->model);
151 0           push @classes, @{$class->config->classes};
  0            
152 0           push @classes, Class::ISA::self_and_super_path($class->config->view);
153            
154 0           my @extra_trace_classes;
155 0 0         if (my $extras = $class->extra_trace_classes)
156             {
157 0 0         @extra_trace_classes = ref($extras) ? @$extras : ($extras);
158 0           push @classes, @extra_trace_classes;
159             }
160            
161 0           my %done; # ensure no subs are traced more than once
162            
163 0           foreach my $trace_class (@classes)
164             {
165             # 'expanded' gives an arrayref for each function:
166             # [0] - full name
167             # [1] - class
168             # [2] - function name
169             # [3] - coderef
170 0 0         my @public = $show_private ? () : ('public');
171 0           my $functions = Class::Inspector->methods($trace_class, @public, 'expanded');
172            
173             # never trace super-private methods - in particular, don't trace
174             # the trace methods. Might revisit this.
175 0           @$functions = grep { $_->[2] !~ /^__/ } @$functions;
  0            
176            
177             # don't trace stuff outside our app, or Maypole, or extra requested packages
178 0           my @our_functions = grep { $_->[1] =~ /(?:Maypole|MVC|$class)/ } @$functions;
  0            
179            
180 0 0         if (@extra_trace_classes)
181             {
182 0           foreach my $extra_class (@extra_trace_classes)
183             {
184 0           foreach my $function (@$functions)
185             {
186 0 0         push(@our_functions, $function) if $function->[1] eq $extra_class;
187             }
188             }
189             }
190            
191 0           @$functions = @our_functions;
192            
193 0           foreach my $function (@$functions)
194             {
195 0 0         next if $done{ $function->[0] }++;
196 0           $class->__traceize( $function->[1],
197             $function->[2],
198             $trace_level,
199             $class->only_trace_exported,
200             $class->trace_path,
201             );
202             }
203             }
204            
205             #warn "Tracing these subs:\n", join "\n", sort keys %done;
206             }
207              
208             sub __traceize
209             {
210 0     0     my ($class, $namespace, $function, $level, $only_exported, $show_path) = @_;
211            
212 0           my $coderef = $namespace->can($function);
213            
214             my $traced = sub
215             {
216 0     0     __trace_entry(0, $level, $only_exported, $show_path, $namespace, $function, @_);
217              
218 0 0         if (wantarray) # list context
    0          
219             {
220 0           my @return = $coderef->(@_);
221 0           __trace_exit($level, $only_exported, $show_path, $namespace, $function, @return);
222 0           return @return;
223             }
224             elsif(defined wantarray) # scalar context
225             {
226 0           my $return = $coderef->(@_);
227 0           __trace_exit($level, $only_exported, $show_path, $namespace, $function, $return);
228 0           return $return;
229             }
230             else # void context
231             {
232 0           $coderef->(@_);
233 0           __trace_exit($level, $only_exported, $show_path, $namespace, $function);
234 0           return;
235             }
236 0           };
237            
238             # replace original sub with the traced version
239             # TODO: don't know how to preserve attributes
240 0 0         return if $class->config->model->method_attrs($function);
241            
242             {
243 1     1   10 no strict 'refs';
  1         2  
  1         63  
  0            
244 1     1   6 no warnings 'redefine';
  1         2  
  1         5844  
245 0           *{"$namespace\::$function"} = $traced;
  0            
246             }
247             }
248              
249             {
250             # note - these functions are also called from Mp::Model::Base::process()
251            
252             my $indent = 0;
253             my $in_exported = '';
254             my $path = '';
255            
256             # not a method
257             sub __trace_entry
258             {
259 0     0     my ($is_exported, $level, $only_exported, $show_path, $namespace, $function, @args) = @_;
260            
261 0 0 0       if (ref($args[0]) and UNIVERSAL::isa($args[0], 'Maypole'))
262             {
263             # NOTE: this *must* be by direct hash access, otherwise we're
264             # calling a traced method, and infinitely recurse
265 0           $path = $args[0]->{path};
266             }
267             else
268             {
269 0           $path = '';
270             }
271            
272 0 0         $in_exported = "$namespace\::$function" if $is_exported;
273              
274 0 0 0       return if ($only_exported and not length $in_exported);
275            
276 0           my $msg = " " x $indent++ . "==> $namespace\::$function";
277 0 0         $msg =~ s/^../E:/ if $in_exported;
278 0 0 0       $msg = "$path: $msg" if $show_path and $path;
279            
280 0 0         if ($level == 2)
    0          
    0          
281             {
282 0           @args = __prep_args2(@args);
283 0           $msg .= '( '.join(', ', @args)." )\n";
284             }
285             elsif ($level == 3)
286             {
287 0           @args = __prep_args3(@args);
288 0           $msg .= '( '.join(', ', @args)." )\n";
289             }
290             elsif ($level > 3)
291             {
292 0           local $Data::Dumper::Terse = 1;
293 0           local $Data::Dumper::Indent = 1;
294 0           $msg .= '( '.Data::Dumper::Dumper(\@args)." )\n";
295             }
296            
297 0           warn $msg;
298             }
299            
300             # not a method
301             sub __trace_exit
302             {
303 0     0     my ($level, $only_exported, $show_path, $namespace, $function, @args) = @_;
304            
305 0 0 0       return if ($only_exported and not length $in_exported);
306            
307 0           my $msg = " " x --$indent . "<== $namespace\::$function";
308 0 0         $msg =~ s/^../E:/ if $in_exported;
309 0 0 0       $msg = "$path: $msg" if $show_path and $path;
310            
311 0 0         if ($level == 2)
    0          
    0          
312             {
313 0           @args = __prep_args2(@args);
314 0           $msg .= ' return( '.join(', ', @args)." )\n";
315             }
316             elsif ($level == 3)
317             {
318 0           @args = __prep_args3(@args);
319 0           $msg .= '( '.join(', ', @args)." )\n";
320             }
321             elsif ($level > 3)
322             {
323 0           local $Data::Dumper::Terse = 1;
324 0           local $Data::Dumper::Indent = 1;
325 0           $msg .= ' return( '.Data::Dumper::Dumper(\@args)." )\n";
326             }
327            
328             # completed processing an exported method
329 0 0         $in_exported = '' if "$namespace\::$function" eq $in_exported;
330            
331 0           warn $msg;
332             }
333              
334             # expand args inside Exported method
335             sub __prep_args3
336             {
337 0     0     my @args = @_;
338            
339 0 0         if ($in_exported)
340             {
341 0           local $Data::Dumper::Terse = 1;
342 0           local $Data::Dumper::Indent = 1;
343 0           @args = (Data::Dumper::Dumper(\@args));
344             }
345             else
346             {
347 0           @args = __prep_args2(@args);
348             }
349            
350 0           return @args;
351             }
352             }
353              
354             sub __prep_args2
355             {
356 0     0     my @args = @_;
357            
358 0           map {
359 0           my $str = $_;
360 0 0         if (defined $str)
361             {
362 0 0         if (! ref $str)
    0          
363             {
364 0           $str = "'$str'";
365             }
366             elsif(Scalar::Util::blessed($str))
367             {
368             # avoid calling overloaded stringification -
369             # causes deep recursion
370 0           $str = ref($str).'(OBJECT)';
371             }
372             }
373             else
374             {
375 0           $str = 'undef'
376             };
377              
378             #Text::Elide::elide($_, 50)
379 0           substr $str, 0, 50, "...'";
380             } @args;
381             }
382              
383             # we replace Maypole::Model::Base::process with this
384             sub __process {
385 0     0     my ( $class, $r ) = @_;
386 0           my $method = $r->action;
387 0 0         return if $r->{template}; # Authentication has set this, we're done.
388              
389 0           $r->{template} = $method;
390 0           my $obj = $class->fetch_objects($r);
391 0 0         $r->objects([$obj]) if $obj;
392            
393             # have to trace manually, because can't replace Exported methods with
394             # self-traceing versions - see trace methods in Maypole.pm
395             # The '1' indicates the method is exported.
396 0 0         my $trace_level = $r->trace_level if $r->can('trace_level');
397 0 0         Maypole::Plugin::Trace::__trace_entry(1, $trace_level, $r->only_trace_exported, $r->trace_path, $class, $method, $r, $obj, @{ $r->args } )
  0            
398             if $trace_level;
399            
400 0           $class->$method( $r, $obj, @{ $r->{args} } );
  0            
401            
402 0 0         Maypole::Plugin::Trace::__trace_exit($trace_level, $r->only_trace_exported, $r->trace_path, $class, $method, $r, $obj, @{ $r->args } )
  0            
403             if $trace_level;
404            
405 0           return; # previously, would implicitly return whatever the $method call
406             # returned, and the return value was ignored
407             }
408              
409              
410              
411             =head1 AUTHOR
412              
413             David Baird, C<< >>
414              
415             =head1 BUGS
416              
417             Please report any bugs or feature requests to
418             C, or through the web interface at
419             L.
420             I will be notified, and then you'll automatically be notified of progress on
421             your bug as I make changes.
422              
423             =head1 ACKNOWLEDGEMENTS
424              
425             =head1 COPYRIGHT & LICENSE
426              
427             Copyright 2005 David Baird, All Rights Reserved.
428              
429             This program is free software; you can redistribute it and/or modify it
430             under the same terms as Perl itself.
431              
432             =cut
433              
434             1; # End of Maypole::Plugin::Trace