File Coverage

blib/lib/Exception/Reporter/Sender/Dir.pm
Criterion Covered Total %
statement 76 77 98.7
branch 10 14 71.4
condition 6 14 42.8
subroutine 13 13 100.0
pod 1 2 50.0
total 106 120 88.3


line stmt bran cond sub pod time code
1 1     1   347 use strict;
  1         1  
  1         21  
2 1     1   2 use warnings;
  1         1  
  1         34  
3             package Exception::Reporter::Sender::Dir;
4             # ABSTRACT: a report sender that writes to directories on the filesystem
5             $Exception::Reporter::Sender::Dir::VERSION = '0.013';
6 1     1   3 use parent 'Exception::Reporter::Sender';
  1         1  
  1         3  
7              
8             #pod =head1 SYNOPSIS
9             #pod
10             #pod my $sender = Exception::Reporter::Sender::Dir->new({
11             #pod root => '/var/error/my-app',
12             #pod });
13             #pod
14             #pod =head1 OVERVIEW
15             #pod
16             #pod This report sender writes reports to the file system. Given a report with
17             #pod bunch dumpable items, the Dir sender will make a directory and write each item
18             #pod to a file in it, using the ident when practical, and a generated filename
19             #pod otherwise.
20             #pod
21             #pod =cut
22              
23 1     1   28 use Digest::MD5 ();
  1         1  
  1         10  
24 1     1   592 use JSON ();
  1         7893  
  1         18  
25 1     1   688 use Path::Tiny;
  1         7549  
  1         44  
26 1     1   414 use String::Truncate;
  1         3269  
  1         4  
27 1     1   116 use Try::Tiny;
  1         1  
  1         65  
28              
29             sub new {
30 1     1 0 8 my ($class, $arg) = @_;
31              
32 1   33     4 my $root = $arg->{root} || Carp::confess("missing 'root' argument");
33              
34 1     1   3 use Path::Tiny;
  1         1  
  1         482  
35              
36 1         3 $root = path($root);
37              
38 1 50 33     17 if (-e $root && ! -d $root) {
39 0         0 Carp::confess("given root <$root> is not a writable directory");
40             }
41              
42 1 50       26 $root->mkpath unless -e $root;
43              
44 1         15 return bless {
45             root => $root,
46             }, $class;
47             }
48              
49             #pod =head2 send_report
50             #pod
51             #pod $email_reporter->send_report(\@summaries, \%arg, \%internal_arg);
52             #pod
53             #pod This method makes a subdirectory for the report and writes it out.
54             #pod
55             #pod C<%arg> is the same set of arguments given to Exception::Reporter's
56             #pod C method. Arguments that will have an effect include:
57             #pod
58             #pod reporter - the name of the program reporting the exception
59             #pod handled - if true, the reported exception was handled and the user
60             #pod saw a simple error message; sets X-Exception-Handled header
61             #pod and adds a text part at the beginning of the report,
62             #pod calling out the "handled" status"
63             #pod
64             #pod C<%internal_arg> contains data produced by the Exception::Reporter using this
65             #pod object. It includes the C of the report and the C calling the
66             #pod reporter.
67             #pod
68             #pod The return value of C is not defined.
69             #pod
70             #pod =cut
71              
72             my $JSON;
73              
74             sub send_report {
75 1     1 1 1 my ($self, $summaries, $arg, $internal_arg) = @_;
76              
77             # ?!? Presumably this can't really happen, but... you know what they say
78             # about zero-summary incidents, right? -- rjbs, 2012-07-03
79 1 50       5 Carp::confess("can't report a zero-summary incident!") unless @$summaries;
80              
81             # We always use this file for internal use.
82 1         4 my %manifest = ('report.json' => { description => 'report metadata' });
83             my %report = (
84             guid => $internal_arg->{guid},
85 1         3 manifest => \%manifest,
86             );
87              
88 1         2 my $n = 1;
89              
90             my $safename = sub {
91 9     9   12 my ($name) = @_;
92             # Surely this is sub-optimal: -- rjbs, 2016-07-19
93 9         14 $name =~ s{\.\.}{DOTDOT}g;
94 9         5 $name =~ s{/}{BACKSLASH}g;
95 9         15 $name =~ s{[^-_0-9a-z.]}{-}gi;
96              
97 9         60 my $base = $name;
98 9         23 $name = "$base-" . $n++ while $manifest{$name};
99              
100 9         24 return $name;
101 1         4 };
102              
103 1         9 my $root = $self->{root}->child($internal_arg->{guid});
104 1         36 $root->mkpath;
105              
106 1         163 my @parts;
107 1         3 GROUP: for my $summary (@$summaries) {
108 6         1297 my @these_parts;
109              
110 6         11 my $t_path = \&path;
111 6 100       4 if (@{ $summary->[1] } > 1) {
  6         18  
112 1         2 my $name = $safename->($summary->[0]);
113 1         3 $manifest{$name} = { ident => $summary->[0] };
114              
115 1         3 $root->child($name)->mkpath;
116 1         128 my $target_path = path($name);
117              
118 1     3   18 $t_path = sub { $target_path->child($_[0]) };
  3         9  
119             }
120              
121 6         5 for my $inner (@{ $summary->[1] }) {
  6         11  
122             my $file = $t_path->(
123 8   50     5955 $safename->( $inner->{filename} || 'inner' )
124             );
125              
126             $manifest{$file} = {
127             filename => $inner->{filename},
128             content_type => $inner->{mimetype},
129              
130             (($inner->{body_is_bytes} && $inner->{charset})
131             ? (charset => $inner->{charset})
132 8 100 66     205 : ()),
133             };
134              
135 8 100       30 my $method = $inner->{body_is_bytes} ? 'spew_raw' : 'spew_utf8';
136 8         15 $root->child($file)->$method($inner->{body});
137             }
138             }
139              
140 1 50       243 if ($arg->{handled}) {
141 1         3 $report{handled} = \1;
142             }
143              
144 1         1 my ($package, $filename, $line) = @{ $internal_arg->{caller} };
  1         2  
145              
146 1         3 $report{reporter} = $arg->{reporter};
147 1         3 $report{caller} = "$filename line $line ($package)";
148              
149 1   33     30 $JSON ||= JSON->new->canonical->pretty;
150 1         34 my $json = $JSON->encode(\%report);
151              
152 1         3 $root->child('report.json')->spew_utf8($json);
153              
154 1         246 return;
155             }
156              
157             1;
158              
159             __END__