File Coverage

blib/lib/Exception/Reporter.pm
Criterion Covered Total %
statement 56 57 98.2
branch 8 12 66.6
condition 8 15 53.3
subroutine 9 9 100.0
pod 3 4 75.0
total 84 97 86.6


line stmt bran cond sub pod time code
1 1     1   13071 use strict;
  1         1  
  1         23  
2 1     1   3 use warnings;
  1         1  
  1         49  
3             package Exception::Reporter;
4             # ABSTRACT: a generic exception-reporting object
5             $Exception::Reporter::VERSION = '0.014';
6             #pod =head1 SYNOPSIS
7             #pod
8             #pod B This is an experimental refactoring of some long-standing internal
9             #pod code. It might get even more refactored. Once I've sent a few hundred
10             #pod thousand exceptions through it, I'll remove this warning...
11             #pod
12             #pod First, you create a reporter. Probably you stick it someplace globally
13             #pod accessible, like MyApp->reporter.
14             #pod
15             #pod my $reporter = Exception::Reporter->new({
16             #pod always_dump => { env => sub { \%ENV } },
17             #pod senders => [
18             #pod Exception::Reporter::Sender::Email->new({
19             #pod from => 'root',
20             #pod to => 'SysAdmins ',
21             #pod }),
22             #pod ],
23             #pod summarizers => [
24             #pod Exception::Reporter::Summarizer::Email->new,
25             #pod Exception::Reporter::Summarizer::File->new,
26             #pod Exception::Reporter::Summarizer::ExceptionClass->new,
27             #pod Exception::Reporter::Summarizer::Fallback->new,
28             #pod ],
29             #pod });
30             #pod
31             #pod Later, some exception has been thrown! Maybe it's an L-based
32             #pod exception, or a string, or a L object or who knows what.
33             #pod
34             #pod try {
35             #pod ...
36             #pod } catch {
37             #pod MyApp->reporter->report_exception(
38             #pod [
39             #pod [ exception => $_ ],
40             #pod [ request => $current_request ],
41             #pod [ uploading => Exception::Reporter::Dumpable::File->new($filename) ],
42             #pod ],
43             #pod );
44             #pod };
45             #pod
46             #pod The sysadmins will get a nice email report with all the dumped data, and
47             #pod reports will thread. Awesome, right?
48             #pod
49             #pod =head1 OVERVIEW
50             #pod
51             #pod Exception::Reporter takes a bunch of input (the I) and tries to
52             #pod figure out how to summarize them and build them into a report to send to
53             #pod somebody. Probably a human being.
54             #pod
55             #pod It does this with two kinds of plugins: summarizers and senders.
56             #pod
57             #pod The summarizers' job is to convert each dumpable into a simple hashref
58             #pod describing it. The senders' job is to take those hashrefs and send them to
59             #pod somebody who cares.
60             #pod
61             #pod =cut
62              
63 1     1   370 use Data::GUID guid_string => { -as => '_guid_string' };
  1         13133  
  1         5  
64              
65             #pod =method new
66             #pod
67             #pod my $reporter = Exception::Reporter->new(\%arg);
68             #pod
69             #pod This returns a new reporter. Valid arguments are:
70             #pod
71             #pod summarizers - an arrayref of summarizer objects; required
72             #pod senders - an arrayref of sender objects; required
73             #pod dumper - a Exception::Reporter::Dumper used for dumping data
74             #pod always_dump - a hashref of coderefs used to generate extra dumpables
75             #pod caller_level - if given, the reporter will look n frames up; see below
76             #pod
77             #pod The C hashref bears a bit more explanation. When
78             #pod C> is called, each entry in C will be
79             #pod evaluated and appended to the list of given dumpables. This lets you make your
80             #pod reporter always include some more useful information.
81             #pod
82             #pod I<...but remember!> The reporter is probably doing its job in a C
83             #pod block, which means that anything that might have been changed C-ly in
84             #pod your C block will I be the same when evaluated as part of the
85             #pod C code. This might not matter often, but keep it in mind when
86             #pod setting up your reporter.
87             #pod
88             #pod In real code, you're likely to create one Exception::Reporter object and make
89             #pod it globally accessible through some method. That method adds a call frame, and
90             #pod Exception::Reporter sometimes looks at C to get a default. If you want
91             #pod to skip those intermedite call frames, pass C. It will be used
92             #pod as the number of frames up the stack to look. It defaults to zero.
93             #pod
94             #pod =cut
95              
96             sub new {
97 3     3 1 4 my ($class, $arg) = @_;
98              
99             my $self = {
100             summarizers => $arg->{summarizers},
101             senders => $arg->{senders},
102             dumper => $arg->{dumper},
103             always_dump => $arg->{always_dump},
104 3   100     18 caller_level => $arg->{caller_level} || 0,
105             };
106              
107 3 100       8 if ($self->{always_dump}) {
108 2         2 for my $key (keys %{ $self->{always_dump} }) {
  2         7  
109             Carp::confess("non-coderef entry in always_dump: $key")
110 2 50       7 unless ref($self->{always_dump}{$key}) eq 'CODE';
111             }
112             }
113              
114 3   66     11 $self->{dumper} ||= do {
115 1         7 require Exception::Reporter::Dumper::YAML;
116 1         7 Exception::Reporter::Dumper::YAML->new;
117             };
118              
119             Carp::confess("entry in dumper is not an Exception::Reporter::Dumper")
120 3 50       14 unless $self->{dumper}->isa('Exception::Reporter::Dumper');
121              
122 3         4 for my $test (qw(Summarizer Sender)) {
123 6         9 my $class = "Exception::Reporter::$test";
124 6         10 my $key = "\L${test}s";
125              
126 6 50 33     11 Carp::confess("no $key given") unless $arg->{$key} and @{ $arg->{$key} };
  6         15  
127             Carp::confess("entry in $key is not an $class")
128 6 50       6 if grep { ! $_->isa($class) } @{ $arg->{$key} };
  14         48  
  6         9  
129             }
130              
131 3         5 bless $self => $class;
132              
133 3         4 $_->register_reporter($self) for $self->_summarizers;
134              
135 3         6 return $self;
136             }
137              
138 7     7   8 sub _summarizers { return @{ $_[0]->{summarizers} }; }
  7         24  
139 4     4   4 sub _senders { return @{ $_[0]->{senders} }; }
  4         8  
140              
141 6     6 0 18 sub dumper { return $_[0]->{dumper} }
142              
143             #pod =method report_exception
144             #pod
145             #pod $reporter->report_exception(\@dumpables, \%arg);
146             #pod
147             #pod This method makes the reporter do its job: summarize dumpables and send a
148             #pod report.
149             #pod
150             #pod Useful options in C<%arg> are:
151             #pod
152             #pod reporter - the program or authority doing the reporting; defaults to
153             #pod the calling package
154             #pod
155             #pod handled - this indicates that this exception has been handled and that
156             #pod the user has not seen a terrible crash; senders might use
157             #pod this to decide who needs to get woken up
158             #pod
159             #pod extra_rcpts - this can be an arrayref of email addresses to be used as
160             #pod extra envelope recipients by the Email sender
161             #pod
162             #pod Each entry in C<@dumpables> is expected to look like this:
163             #pod
164             #pod [ $short_name, $value, \%arg ]
165             #pod
166             #pod The short name is used for a few things, including identifying the dumps inside
167             #pod the report produced. It's okay to have duplicated short names.
168             #pod
169             #pod The value can, in theory, be I. It can be C, any kind of
170             #pod object, or whatever you want to stick in a scalar. It's possible that
171             #pod extremely exotic values could confuse the "fallback" summarizer of last resort,
172             #pod but for the most part, anything goes.
173             #pod
174             #pod The C<%arg> entry isn't used for anything by the core libraries that ship with
175             #pod Exception::Reporter, but you might want to use it for your own purposes. Feel
176             #pod free.
177             #pod
178             #pod The reporter will try to summarize each dumpable by asking each summarizer, in
179             #pod order, whether it C the dumpable. If it can, it will be asked
180             #pod to C the dumpable. The summaries are collected into a structure
181             #pod that looks like this:
182             #pod
183             #pod [
184             #pod [ dumpable_short_name => \@summaries ],
185             #pod ...
186             #pod ]
187             #pod
188             #pod If a given dumpable can't be dumped by any summarizer, a not-very-useful
189             #pod placeholder is put in its place.
190             #pod
191             #pod The arrayref constructed is passed to the C method of each sender,
192             #pod in turn.
193             #pod
194             #pod =cut
195              
196             sub report_exception {
197 4     4 1 7726 my ($self, $dumpables, $arg) = @_;
198 4   50     8 $dumpables ||= [];
199 4   50     6 $arg ||= {};
200              
201 4         8 my $guid = _guid_string;
202              
203 4         638 my @caller = caller( $self->{caller_level} );
204 4   33     9 $arg->{reporter} ||= $caller[0];
205              
206 4         8 my $summaries = $self->collect_summaries($dumpables);
207              
208 4         6 for my $sender ($self->_senders) {
209 4         17 $sender->send_report(
210             $summaries,
211             $arg,
212             {
213             guid => $guid,
214             caller => \@caller,
215             }
216             );
217             }
218              
219 4         31 return $guid;
220             }
221              
222             #pod =method collect_summaries
223             #pod
224             #pod $reporter->report_exception(\@dumpables);
225             #pod
226             #pod This method is used by L to convert dumpables into
227             #pod summaries. It may be called directly by summarizers through
228             #pod C<< $self->reporter->collect_summaries(\@dumpables); >> if your
229             #pod summarizers receive dumpables that may be handled by another summarizer. Be
230             #pod wary though, because you could possibly create an endless loop...
231             #pod
232             #pod =cut
233              
234             sub collect_summaries {
235 4     4 1 2 my ($self, $dumpables) = @_;
236              
237 4         5 my @sumz = $self->_summarizers;
238              
239 4         5 my @summaries;
240              
241 4         4 DUMPABLE: for my $dumpable (
242             @$dumpables,
243 3         8 map {; [ $_, $self->{always_dump}{$_}->() ] }
244 4         11 sort keys %{$self->{always_dump}}
245             ) {
246 15         23 for my $sum (@sumz) {
247 48 100       289 next unless $sum->can_summarize($dumpable);
248 15         73 push @summaries, [ $dumpable->[0], [ $sum->summarize($dumpable) ] ];
249 15         156 next DUMPABLE;
250             }
251              
252 0         0 push @summaries, [
253             $dumpable->[0],
254             [ {
255             ident => "UNKNOWN",
256             body => "the entry for <$dumpable->[0]> could not be summarized",
257             mimetype => 'text/plain',
258             filename => 'unknown.txt',
259             } ],
260             ];
261             }
262              
263 4         9 return \@summaries;
264             }
265              
266             1;
267              
268             __END__