File Coverage

blib/lib/Catalyst/Plugin/Devel/InPageLogs.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Catalyst::Plugin::Devel::InPageLogs;
2              
3 2     2   35013 use strict;
  2         6  
  2         76  
4 2     2   13 use warnings;
  2         4  
  2         60  
5              
6 2     2   1261 use Catalyst::Plugin::Devel::InPageLogs::Log;
  0            
  0            
7             use NEXT;
8              
9             our $VERSION = '0.01_02';
10              
11             # hash key to use when saving plugin data into context
12             our $plugin_dataname = 'inpagelogs';
13              
14              
15             =head1 NAME
16              
17             Catalyst::Plugin::Devel::InPageLogs - append request debug messages to HTML web page output
18              
19             =head1 SYNOPSIS
20              
21             use Catalyst 'Devel::InPageLogs';
22              
23             # These are the default values
24             __PACKAGE__->config->{inpagelogs} = {
25             enabled => 1,
26             passthru => 1,
27             addcaller => 1,
28             shortnames => 'dbg,dbgf',
29             };
30              
31             # In MyApp::C::SuperHero
32             $c->dbgf( "Leaped over %d tall buildings", $count );
33              
34              
35             =head1 DESCRIPTION
36              
37             This plugin module for the Catalyst framework provides a means to capture
38             debugging messages and append them to the end of web page output.
39             The automatic display of debug messages in the same web page with
40             normal HTML output can be very convenient during development.
41              
42             One way to think about this plugin is to compare it with the Unix
43             'tee' command. Debug log messages continue to display using the
44             core logger Catalyst::Log, but are also captured and displayed
45             in the browser.
46              
47             Almost all debug/info/etc. messages created during processing of
48             one HTTP request are returned in the generated HTML page, grouped together
49             and appended to the end of the displayed HTML. A few core debug messages
50             are missed at end of a request (see L<"LIMITATIONS">).
51              
52             Please note that B<only during processing of requests> are debug messages
53             are captured and displayed.
54             This means that only debug calls from controller, model, and view
55             routines will be handled.
56             This is not a real limitation as only these messages I<could>
57             be added to the web page.
58              
59             If care for security issues is taken then this facility could even be
60             enabled at will using URL parameters. This could be I<very> helpful
61             when only the beta tester's browser is close at hand.
62              
63             In addition to the normal debug, warn, etc. routines documented for
64             L<Catalyst::Log>, this plugin adds two convenience methods.
65             These two methods combine shorter names with added information noting
66             the calling location. One of these add-on methods also conveniently
67             handles L<sprintf> formatting.
68              
69             =cut
70              
71             =head1 CONFIGURATION OPTIONS
72              
73             Some plugin behavior can be controlled by configuration options. You can
74             modify the defaults by specifying values using the
75             C<__PACKAGE__-E<gt>config-E<gt>{inpagelogs}> key.
76              
77             =head2 enabled
78              
79             The plugin can be disabled by setting this value to zero.
80             You may want to do this to leave the code unchanged but prevent
81             debug output from being seen when not in development.
82             A warning log message will be generated noting that the plugin is
83             installed but disabled.
84              
85             =head2 passthru
86              
87             The plugin defaults to 'tee' mode - passing calls to debug/warn/etc.
88             to the core logger after capturing them. Messages will be displayed
89             in both the normal log and in the web page.
90              
91             If you want debug messages displayed only in the web page you can
92             set this config value to zero.
93              
94             =head2 shortnames
95              
96             As a convenience for the developer, the plugin will define short name
97             aliases for the add-on debug routines.
98              
99             You may change the short name symbol definitions used if the defaults
100             would conflict with existing other code. Set the config value to a
101             string of two name symbols separated by a comma:
102              
103             shortname => "bugout,bugfmt",
104              
105             The first name is aliased to the "capture a set of messages" routine.
106             The second name is aliased to the "format a message and capture it"
107             routine.
108              
109             =head2 addcaller
110              
111             The add-on debug routines (normally 'dbg' and 'dbgf') will record
112             caller information in each message.
113             The calling filename and linenumber will be added after the timestamp.
114              
115             If you do not want this added information, set this config value to zero.
116              
117             =head1 METHODS
118              
119             =head2 EXTENDED METHODS
120              
121             =head3 prepare_request
122              
123             Setup plugin-specific data area in the current request.
124              
125             This plugin method will create and attach a work area to the current
126             request context. The work area will contain the array used to
127             collect the captured debug messages.
128             The existing core logger object reference is saved before installing
129             our own logger object.
130              
131             =cut
132              
133             # This plugin method is the first point at which we can execute during
134             # processing of one request. The context at this point is an Engine
135             # object, which will be discarded at end of request processing.
136             #
137             # We attach our data area to the context object using the hash key
138             # 'inpagelogs', in keeping with the methods employed by other plugins.
139              
140              
141             sub prepare_request {
142             my ( $c ) = shift;
143              
144             unless( $c->is_inpagelogs_enabled ) {
145             $c->log->warn( "InPageLogs plugin is disabled by config" );
146             return $c->NEXT::prepare_request( @_ );
147             }
148              
149             # Determine whether shortnames are enabled and what they are
150             #
151             # We use defaults of enabled and shortnames 'dbg' and 'dbgf'
152             # - if no config is specified
153             # - if no 'shortnames' config value is specified
154             # - if config value is 'yes'
155             # We are disabled if config value is present and equals 'no'
156             # We use shortnames from the config value if two name strings
157             # are present in the value
158             # Otherwise we complain and disable ourselves
159              
160             my @shortnames = ( 'dbg', 'dbgf' );
161            
162             my $cfg_value = $c->inpagelogs_config('shortnames');
163             if( defined $cfg_value ) {
164              
165             my $shortnames = 'yes';
166              
167             if ( my @newnames = $cfg_value =~ m/^\s*(\w+)\s*,\s*(\w+)\s*$/ ) {
168             if( @newnames == 2 ) {
169             @shortnames = @newnames;
170             }
171             else {
172             $shortnames = 'bad'; # disabled by bad 'shortnames' config value
173             }
174             }
175             elsif ( $cfg_value =~ m/^ \s* no \s* $/ix ) {
176             @shortnames = ();
177             }
178             elsif ( $cfg_value =~ m/^ \s* yes \s* $/ix ) {
179             ;
180             }
181             else {
182             $shortnames = 'bad'; # disabled by bad 'shortnames' config value
183             }
184              
185             if ( $shortnames eq 'bad' ) {
186             $c->log->warn( "InPageLogs plugin 'shortnames' config value '$cfg_value' is invalid" );
187             @shortnames = ();
188             }
189             }
190              
191             # Create our new logger object
192             my $new_log_obj = Catalyst::Plugin::Devel::InPageLogs::Log->new( $c );
193            
194             # Create plugin-specific data area, storing array ref for captured
195             # debug messages, and saving the current logging object.
196             my %data_area = (
197             buffer => [],
198             old_log_obj => $c->log,
199             new_log_obj => $new_log_obj,
200             );
201              
202             $c->{$plugin_dataname} = \%data_area;
203              
204             # Replace current log object for use during this request
205             $c->log( $new_log_obj );
206              
207             # If convenience short names are enabled, create those definitions
208             if( @shortnames ) {
209             no strict 'refs';
210             *{ ref($c) . '::' . $shortnames[0] } = \&inpagelogs_log_msg;
211             *{ ref($c) . '::' . $shortnames[1] } = \&inpagelogs_log_msgf;
212             }
213              
214             # Done here, continue the plugin chain
215             $c->NEXT::prepare_request( @_ );
216             }
217              
218              
219             =head3 finalize
220              
221             This plugin method will check whether captured debug messages can be
222             appended to the current output body. Only content type 'text/html'
223             output will be updated.
224              
225             The saved previous logger object will be restored at this point.
226              
227             =cut
228              
229             # This is the last possible point during finalization of a response
230             # for us intervene, before the generated output is actually sent to
231             # the browser.
232             #
233             # As our data area reference is held within the engine context object
234             # we shouldn't need to take extra efforts to delete the hash, but can
235             # let discarding the engine context at end of request do that for us.
236              
237              
238             sub finalize {
239             my ( $c ) = shift;
240              
241             unless ( $c->response->body ) {
242             return $c->NEXT::finalize;
243             }
244              
245             unless ( $c->response->content_type =~ m!^text/html!i ) {
246             return $c->NEXT::finalize;
247             }
248            
249             my $data_area = $c->inpagelogs_data;
250             unless ( defined $data_area ) {
251             return $c->NEXT::finalize;
252             }
253              
254             # If there are captured messages in our save area
255             if ( defined $data_area->{buffer} ) {
256             my $ra = $c->inpagelogs_data->{buffer};
257             $c->res->body( $c->res->body . '<pre>' . join('',@$ra) . '</pre>' );
258             }
259              
260             # Restore the original log object
261             if ( defined $data_area->{old_log_obj} ) {
262             $c->log( $data_area->{old_log_obj} );
263             }
264              
265             # Allow other plugins/core to finish generating output body
266             return $c->NEXT::finalize;
267             }
268              
269              
270             =head2 INTERNAL METHODS
271              
272             =head3 inpagelogs_data - access to plugin-specific data area
273              
274             $data_area = $c->inpagelogs_data;
275              
276             Return reference to work area for this plugin during this request.
277             If no work area was created (perhaps because plugin is disabled)
278             then C<undef> is returned.
279              
280             =cut
281              
282             sub inpagelogs_data {
283             my ( $c ) = @_;
284              
285             return $c->{$plugin_dataname};
286             }
287              
288             =head3 inpagelogs_config - access to plugin-specific config area
289              
290             $config_area = $c->inpagelogs_config;
291              
292             Return reference to config hash section for this plugin, if present.
293             Otherwise C<undef> will be returned.
294              
295             =cut
296              
297             sub inpagelogs_config {
298             my ( $c ) = @_;
299              
300             my $our_config = $c->config->{$plugin_dataname};
301              
302             # If a specific config value is requested, return that
303             if( defined $our_config && @_ > 1 ) {
304             return $our_config->{ $_[1] };
305             }
306              
307             return $our_config;
308             }
309              
310             =head3 is_inpagelogs_enabled - check config flag
311              
312             return unless $c->is_inpagelogs_enabled;
313              
314             The default is to assume the installed plugin is enabled, unless
315              
316             =over 8
317              
318             =item 'inpagelogs' config section is present, and
319              
320             =item 'enabled' flag value is present, and
321              
322             =item the value is set to zero
323              
324             =back
325              
326             =cut
327              
328             sub is_inpagelogs_enabled {
329             my ( $c ) = shift;
330              
331             my $enabled = $c->inpagelogs_config('enabled');
332             # Default to 'enabled' if installed but no config set
333             # Default to 'enabled' if config doesn't mention flag
334             return 1 unless defined $enabled;
335              
336             # Otherwise return configured enable flag value
337             return $enabled;
338             }
339              
340              
341             =head2 PUBLIC METHODS
342              
343             =head3 inpagelogs_add_msg - add messages to our capture array
344              
345             $c->inpagelogs_add_msg(
346             'Whoa! What they said!',
347             " parameter was '$he_said_she_said'" );
348              
349             This method will take one or more strings and save them in the capture buffer
350             for later display.
351              
352             The only formatting done is to add a "\n" to the end of every string
353             that does not already end with "\n".
354              
355             =cut
356              
357             sub inpagelogs_add_msg {
358             my( $c ) = shift;
359              
360             return unless @_ > 0;
361              
362             my $data_area = $c->inpagelogs_data;
363             return unless defined $data_area;
364              
365             my $buffer = $data_area->{buffer};
366             return unless defined $buffer;
367              
368             foreach my $msg ( @_ ) {
369             if( $msg =~ m/\n\z/ ) {
370             push @{$buffer}, $msg;
371             } else {
372             push @{$buffer}, $msg . "\n";
373             }
374             }
375             }
376              
377             # The add-on convenience methods for debugging with added information
378              
379             =head3 inpagelogs_log_msg - capture debug messages
380              
381             Add a list of strings to captured debug messages.
382              
383             =cut
384              
385             sub inpagelogs_log_msg {
386             my $c = shift;
387             my( $filename, $line ) = ( caller() )[1,2];
388             $c->inpagelogs_log_msgsub( $filename, $line, @_ );
389             }
390              
391             =head3 inpagelogs_log_msgf - sprintf format parameters and capture debug message
392              
393             $c->inpagelogs_log_msgf( "I saw a huge number '%12.3g'\n", $value );
394              
395             Process a format and parameters using sprintf, then add result
396             to captured debug messages.
397              
398             =cut
399              
400             sub inpagelogs_log_msgf {
401             my $c = shift;
402             my $msg = sprintf shift, @_;
403             my( $filename, $line ) = ( caller() )[1,2];
404             $c->inpagelogs_log_msgsub( $filename, $line, $msg );
405             }
406              
407              
408             =head3 inpagelogs_log_msgsub - internal debug message capture routine
409              
410             This routine handles the final formatting of messages added to the
411             capture array.
412              
413             The formatted current time will prefix the first message. The time is
414             formatted using overridable routine C<_log_time_formatter>.
415              
416             By default the caller information, filename and line number, will be
417             formatted and also added before the first message. This can be controlled
418             by configuration option C<addcaller>.
419              
420             =cut
421              
422             sub inpagelogs_log_msgsub {
423             my $c = shift;
424             my $filename = shift;
425             my $line = shift;
426              
427             my $time_string = _log_time_formatter();
428              
429             my $addcaller = $c->inpagelogs_config('addcaller');
430             if( ! defined $addcaller || $addcaller ) {
431             # While running using stand-alone server, remove leading home path
432             my $home = $c->config->{home};
433             $home =~ s/\\/\//g;
434             if( substr($filename,0,length $home) eq $home ) {
435             $filename = substr($filename,length $home);
436             }
437             # Remove some more repeated stuff if present
438             $home = '/script/../lib/';
439             if( substr($filename,0,length $home) eq $home ) {
440             $filename = substr($filename,length $home);
441             }
442            
443             my $hdr = "${time_string}: ($filename,$line)\n";
444             $c->inpagelogs_add_msg( $hdr, @_ );
445             }
446             else {
447             my $hdr = "${time_string}: " . shift;
448             $c->inpagelogs_add_msg( $hdr, @_ );
449             }
450             }
451              
452              
453             {
454             # Return local epoch date/time in format MMDDpHHmmSS (e.g. 0109.191550)
455              
456             # A small bit of memoizing of results. We are assuming that the
457             # log times will be in forward sequence, with each input time
458             # value repeated a number of times.
459             # If a new time input value is same as previous, then we can simply
460             # return the previously formatted string result.
461              
462             # Previous epoch time value input received
463             my $prev_time;
464             # Previous formatted string result
465             my $prev_string;
466              
467             sub _log_time_formatter {
468             my( $time ) = shift || time;
469              
470             unless( defined $prev_time && $prev_time == $time ) {
471             $prev_time = $time;
472             my( $sec, $min, $hour, $mday, $mon, $year ) = localtime($prev_time);
473             $prev_string = sprintf( "%02d%02d.%02d%02d%02d",
474             $mon+1, $mday, $hour, $min, $sec );
475             }
476              
477             $prev_string;
478             }
479             }
480              
481              
482              
483             =head1 LIMITATIONS
484              
485             =head2 MISSED MESSAGES
486              
487             Due to the sequence of Catalyst internal operations and calls to the
488             plugin methods, some debug messages at the very end of processing
489             for a request cannot be seen by this plugin.
490              
491             Specifically (and most regretably) the displayed output table showing
492             the actions executed for the request are not captured, e.g.
493              
494             [Wed Aug 3 16:30:39 2005] [catalyst] [info] Request took 0.27s (3.70/s)
495             .=---------------------------------------------+----------=.
496             | Action | Time |
497             |=---------------------------------------------+----------=|
498             | /begin | 0.000955s |
499             | -> /user/upld/phase2page | 0.000614s |
500             | /user/upld/phase1 | 0.002515s |
501             | -> Widget::V::TT->process | 0.228791s |
502             | /user/upld/end | 0.230610s |
503             '=---------------------------------------------+----------='
504              
505             will not be seen except in the core logger output.
506              
507             =head2 NOT INTEGRATED WITH DEFAULT ERROR PAGE
508              
509             The 'pretty' error page displayed by Catalyst upon an exception
510             does not include any debug messages captured by this plugin.
511              
512             =head1 TODOS
513              
514             =over 4
515              
516             =item Figure out how to add our messages to Catalyst exception page
517              
518             =item Propose patch to move logging of action execution summary earlier?
519              
520             =item Use check "if ( $c->isa('Catalyst::Plugin::Devel::InPageLogs') )" ?
521              
522             =back
523              
524              
525             =head1 SEE ALSO
526              
527             L<Catalyst>, L<Catalyst::Log>.
528              
529             =head1 AUTHOR
530              
531             Thomas L. Shinnick <tshinnic@cpan.org>
532              
533             =head1 LICENSE
534              
535             This library is free software . You can redistribute it and/or modify
536             it under the same terms as perl itself.
537              
538             =cut
539              
540             1;
541              
542             __END__
543              
544             Todos:
545              
546             Should the removed leading caller filename string be configurable
547             in inpagelogs_log_msgsub() ?
548              
549             Use config flags to control behaviors
550              
551             Move the logging inner class to own module
552              
553             Do I need something like:
554             if ( $c->isa('Catalyst::Plugin::Devel::InPageLogs') ) {
555              
556             Why is finalize_body not being called on (?)
557             - non text/html requests?
558             - static data requests? status = 304 not modified
559              
560             What facilities in Catalyst to process arguments meant for plugins
561             Can it correctly process
562             use Catalyst qw{ -log=MyLogger InPageLogs=passthru };
563             * only the -Debug and -opt options show any evidence of allowing
564             arguments with 'plugin' names
565             * however, there is an "instant plugin" (dynamic plugin?) that
566             shows handling of arguments to a 'new()' call
567            
568              
569             ## xshortnames: no defaults defined
570             ## shortnames: dbg, dbgf works as though defaulted
571             ## shortnames: yes works as though defaulted
572             ## shortnames: xdbg, xdbgf dies with: Can't locate object method "dbgf"
573             ## shortnames: foo dies with: Can't locate object method "dbgf"
574             ## shortnames: no dies with: Can't locate object method "dbgf"
575              
576              
577              
578             # vim:ft=perl:ts=4:sw=4:et:is:hls:ss=10: