File Coverage

blib/lib/Plack/Middleware/Recorder.pm
Criterion Covered Total %
statement 104 113 92.0
branch 25 32 78.1
condition 2 5 40.0
subroutine 18 18 100.0
pod 3 3 100.0
total 152 171 88.8


line stmt bran cond sub pod time code
1             ## no critic (RequireUseStrict)
2             package Plack::Middleware::Recorder;
3             $Plack::Middleware::Recorder::VERSION = '0.06';
4             ## use critic (RequireUseStrict)
5 11     11   78412 use strict;
  11         23  
  11         412  
6 11     11   57 use warnings;
  11         18  
  11         406  
7 11     11   56 use parent 'Plack::Middleware';
  11         18  
  11         88  
8              
9 11     11   815 use Carp qw(croak);
  11         19  
  11         762  
10 11     11   541 use HTTP::Request;
  11         22397  
  11         204  
11 11     11   1670 use IO::File;
  11         2448  
  11         2302  
12 11     11   6983 use IO::String;
  11         32180  
  11         203  
13 11     11   1336 use Sereal qw(encode_sereal);
  11         1027  
  11         725  
14 11     11   80 use Fcntl qw(:flock);
  11         15  
  11         1693  
15 11     11   6970 use Scope::Guard;
  11         5230  
  11         552  
16 11     11   1024 use namespace::clean;
  11         30815  
  11         102  
17              
18 11     11   5855 use Plack::Util::Accessor qw/active start_url stop_url/;
  11         20  
  11         121  
19              
20             sub prepare_app {
21 27     27 1 118672 my ( $self ) = @_;
22              
23 27 100       97 $self->active(1) unless defined $self->active;
24 27 100       783 $self->start_url('/recorder/start') unless defined $self->start_url;
25 27 100       322 $self->stop_url('/recorder/stop') unless defined $self->stop_url;
26              
27 27         294 my $output = delete $self->{'output'};
28 27 100       126 croak "output parameter required" unless defined $output;
29              
30 26 50       74 if (ref $output) {
31 0         0 $self->{'output_fh'} = $output;
32 0         0 $output->autoflush(1);
33             } else {
34 26 50       676 unless(-w $output) {
35 0         0 croak "$output is not writable";
36             }
37 26         117 $self->{'output_filename'} = $output;
38             }
39             }
40              
41             sub _output_fh {
42 36     36   64 my ( $self, $env ) = @_;
43 36 100       172 unless ($self->{'output_fh'}) {
44 25 100       89 my $mode = $env->{'psgi.run_once'} ? 'a' : 'w';
45 25         180 $self->{'output_fh'} = IO::File->new($self->{'output_filename'}, $mode);
46 25         2968 $self->{'output_fh'}->autoflush(1);
47             }
48 36         1125 return $self->{'output_fh'};
49             }
50              
51             sub env_to_http_request {
52 36     36 1 52 my ( $self, $env ) = @_;
53              
54 36         171 my $request = HTTP::Request->new;
55 36         1341 $request->method($env->{'REQUEST_METHOD'});
56 36         316 $request->uri($env->{'REQUEST_URI'});
57 36         1726 $request->header(Content_Length => $env->{'CONTENT_LENGTH'});
58 36         1570 $request->header(Content_Type => $env->{'CONTENT_TYPE'});
59 36         1371 foreach my $header (grep { /^HTTP_/ } keys %$env) {
  880         896  
60 44         344 my $value = $env->{$header};
61 44         148 $header =~ s/^HTTP_//;
62 44         80 $header = uc($header);
63 44         127 $header =~ s/\b([a-z])/uc $!/ge;
  0         0  
64              
65 44         131 $request->header($header, $value);
66             }
67              
68 36         1451 my $input = $env->{'psgi.input'};
69 36         223 my $body = IO::String->new;
70 36         1456 my $buffer = '';
71 36         269 while($input->read($buffer, 1024) > 0) {
72 5         70 print $body $buffer;
73             }
74              
75 36         551 $body->setpos(0);
76 36         409 $env->{'psgi.input'} = $body;
77 36         50 $request->content(${ $body->string_ref });
  36         108  
78              
79 36         874 return $request;
80             }
81              
82             sub call {
83 58     58 1 252883 my ( $self, $env ) = @_;
84              
85 58         264 my $app = $self->app;
86 58         396 my $start_url = $self->start_url;
87 58         312 my $stop_url = $self->stop_url;
88 58         252 my $path = $env->{'PATH_INFO'};
89              
90 58         104 $env->{__PACKAGE__ . '.start_url'} = $start_url;
91 58         102 $env->{__PACKAGE__ . '.stop_url'} = $stop_url;
92              
93 58 100       673 if($path =~ m!\Q$start_url\E!) {
    100          
    100          
94 4         26 $self->active(1);
95 4         37 $env->{__PACKAGE__ . '.active'} = $self->active;
96             return [
97 4         67 200,
98             ['Content-Type' => 'text/plain'],
99             [ 'Request recording is ON' ],
100             ];
101             } elsif($path =~ m!\Q$stop_url\E!) {
102 7         33 $self->active(0);
103 7         49 $env->{__PACKAGE__ . '.active'} = $self->active;
104             return [
105 7         94 200,
106             ['Content-Type' => 'text/plain'],
107             [ 'Request recording is OFF' ],
108             ];
109             } elsif($self->active) {
110 36         249 my $req = $self->env_to_http_request($env);
111 36         804 my $frozen = encode_sereal($req);
112              
113 36         133 my $fh = $self->_output_fh($env);
114             # $guard looks unused, but it's unlocking the file upon its
115             # destruction
116 36         107 my $guard = $self->_create_concurrency_lock($fh, $env);
117 36 50       119 if($guard) {
118 36         412 $fh->write(pack('Na*', length($frozen), $frozen));
119 36         105946 $fh->flush;
120             }
121             }
122              
123 47         242 $env->{__PACKAGE__ . '.active'} = $self->active;
124              
125 47         364 return $app->($env);
126             }
127              
128             sub _create_concurrency_lock {
129 36     36   56 my ( $self, $fh, $env ) = @_;
130              
131 36 100 66     243 return 1 if !$env->{'psgi.multithread'} && !$env->{'psgi.multiprocess'};
132              
133 2 50       3 my $locked = eval { flock($fh, LOCK_EX) || die "$!\n" };
  2         17  
134              
135 2 50       5 if(!$locked) {
136 0 0       0 if(my $log = $env->{'psgix.logger'}) {
137 0   0     0 my $error = $@ || 'Unknown error';
138 0         0 chomp $error;
139              
140 0         0 $log->({
141             level => 'warn',
142             message => "Unable to lock filehandle in multiprocess environment ($error); skipping recording",
143             });
144             }
145 0         0 return;
146             }
147              
148 2     2   17 return Scope::Guard->new( sub { flock($fh, LOCK_UN) });
  2         28  
149             }
150              
151             1;
152              
153             # ABSTRACT: Plack middleware that records your client-server interactions
154              
155             __END__