File Coverage

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


line stmt bran cond sub pod time code
1 1     1   501 use strict;
  1         2  
  1         29  
2 1     1   5 use warnings;
  1         2  
  1         42  
3             package Exception::Reporter::Sender::Dir 0.015;
4             # ABSTRACT: a report sender that writes to directories on the filesystem
5              
6 1     1   5 use parent 'Exception::Reporter::Sender';
  1         3  
  1         5  
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   43 use Digest::MD5 ();
  1         2  
  1         13  
24 1     1   666 use JSON ();
  1         10498  
  1         42  
25 1     1   933 use Path::Tiny;
  1         12447  
  1         84  
26 1     1   556 use String::Truncate;
  1         4597  
  1         7  
27 1     1   235 use Try::Tiny;
  1         3  
  1         86  
28              
29             sub new {
30 1     1 0 16 my ($class, $arg) = @_;
31              
32 1   33     5 my $root = $arg->{root} || Carp::confess("missing 'root' argument");
33              
34 1     1   7 use Path::Tiny;
  1         2  
  1         699  
35              
36 1         4 $root = path($root);
37              
38 1 50 33     30 if (-e $root && ! -d $root) {
39 0         0 Carp::confess("given root <$root> is not a writable directory");
40             }
41              
42 1 50       43 $root->mkpath unless -e $root;
43              
44 1         46 return bless {
45             root => $root,
46             }, $class;
47             }
48              
49             #pod =head2 send_report
50             #pod
51             #pod $dir_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; adds C<< "handled":true >>
61             #pod to the JSON body of the report
62             #pod
63             #pod C<%internal_arg> contains data produced by the Exception::Reporter using this
64             #pod object. It includes the C of the report and the C calling the
65             #pod reporter.
66             #pod
67             #pod The return value of C is not defined.
68             #pod
69             #pod =cut
70              
71             my $JSON;
72              
73             sub send_report {
74 1     1 1 5 my ($self, $summaries, $arg, $internal_arg) = @_;
75              
76             # ?!? Presumably this can't really happen, but... you know what they say
77             # about zero-summary incidents, right? -- rjbs, 2012-07-03
78 1 50       6 Carp::confess("can't report a zero-summary incident!") unless @$summaries;
79              
80             # We always use this file for internal use.
81 1         8 my %manifest = ('report.json' => { description => 'report metadata' });
82             my %report = (
83             guid => $internal_arg->{guid},
84 1         8 manifest => \%manifest,
85             );
86              
87 1         3 my $n = 1;
88              
89             my $safename = sub {
90 9     9   26 my ($name) = @_;
91             # Surely this is sub-optimal: -- rjbs, 2016-07-19
92 9         22 $name =~ s{\.\.}{DOTDOT}g;
93 9         18 $name =~ s{/}{BACKSLASH}g;
94 9         22 $name =~ s{[^-_0-9a-z.]}{-}gi;
95              
96 9         13 my $base = $name;
97 9         33 $name = "$base-" . $n++ while $manifest{$name};
98              
99 9         25 return $name;
100 1         7 };
101              
102 1         15 my $root = $self->{root}->child($internal_arg->{guid});
103 1         123 $root->mkpath;
104              
105 1         314 my @parts;
106 1         4 GROUP: for my $summary (@$summaries) {
107 6         2245 my @these_parts;
108              
109 6         18 my $t_path = \&path;
110 6 100       7 if (@{ $summary->[1] } > 1) {
  6         23  
111 1         5 my $name = $safename->($summary->[0]);
112 1         4 $manifest{$name} = { ident => $summary->[0] };
113              
114 1         5 $root->child($name)->mkpath;
115 1         196 my $target_path = path($name);
116              
117 1     3   34 $t_path = sub { $target_path->child($_[0]) };
  3         11  
118             }
119              
120 6         11 for my $inner (@{ $summary->[1] }) {
  6         14  
121             my $file = $t_path->(
122 8   50     2169 $safename->( $inner->{filename} || 'inner' )
123             );
124              
125             $manifest{$file} = {
126             filename => $inner->{filename},
127             content_type => $inner->{mimetype},
128              
129             (($inner->{body_is_bytes} && $inner->{charset})
130             ? (charset => $inner->{charset})
131 8 100 100     306 : ()),
132             };
133              
134 8 100       47 my $method = $inner->{body_is_bytes} ? 'spew_raw' : 'spew_utf8';
135 8         20 $root->child($file)->$method($inner->{body});
136             }
137             }
138              
139 1 50       429 if ($arg->{handled}) {
140 1         3 $report{handled} = \1;
141             }
142              
143 1         3 my ($package, $filename, $line) = @{ $internal_arg->{caller} };
  1         4  
144              
145 1         6 $report{reporter} = $arg->{reporter};
146 1         5 $report{caller} = "$filename line $line ($package)";
147              
148 1   33     37 $JSON ||= JSON->new->canonical->pretty;
149 1         34 my $json = $JSON->encode(\%report);
150              
151 1         5 $root->child('report.json')->spew_utf8($json);
152              
153 1         489 return;
154             }
155              
156             1;
157              
158             __END__