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.04';
4             ## use critic (RequireUseStrict)
5 11     11   45519 use strict;
  11         16  
  11         370  
6 11     11   39 use warnings;
  11         12  
  11         274  
7 11     11   35 use parent 'Plack::Middleware';
  11         12  
  11         58  
8              
9 11     11   514 use Carp qw(croak);
  11         13  
  11         453  
10 11     11   423 use HTTP::Request;
  11         13579  
  11         115  
11 11     11   894 use IO::File;
  11         1336  
  11         1152  
12 11     11   4049 use IO::String;
  11         17612  
  11         127  
13 11     11   875 use Sereal qw(encode_sereal);
  11         725  
  11         447  
14 11     11   38 use Fcntl qw(:flock);
  11         10  
  11         1085  
15 11     11   3834 use Scope::Guard;
  11         3047  
  11         333  
16 11     11   802 use namespace::clean;
  11         20485  
  11         65  
17              
18 11     11   3501 use Plack::Util::Accessor qw/active start_url stop_url/;
  11         13  
  11         71  
19              
20             sub prepare_app {
21 27     27 1 66825 my ( $self ) = @_;
22              
23 27 100       64 $self->active(1) unless defined $self->active;
24 27 100       1115 $self->start_url('/recorder/start') unless defined $self->start_url;
25 27 100       185 $self->stop_url('/recorder/stop') unless defined $self->stop_url;
26              
27 27         195 my $output = delete $self->{'output'};
28 27 100       73 croak "output parameter required" unless defined $output;
29              
30 26 50       54 if (ref $output) {
31 0         0 $self->{'output_fh'} = $output;
32 0         0 $output->autoflush(1);
33             } else {
34 26 50       435 unless(-w $output) {
35 0         0 croak "$output is not writable";
36             }
37 26         71 $self->{'output_filename'} = $output;
38             }
39             }
40              
41             sub _output_fh {
42 36     36   45 my ( $self, $env ) = @_;
43 36 100       103 unless ($self->{'output_fh'}) {
44 25 100       61 my $mode = $env->{'psgi.run_once'} ? 'a' : 'w';
45 25         133 $self->{'output_fh'} = IO::File->new($self->{'output_filename'}, $mode);
46 25         2129 $self->{'output_fh'}->autoflush(1);
47             }
48 36         792 return $self->{'output_fh'};
49             }
50              
51             sub env_to_http_request {
52 36     36 1 66 my ( $self, $env ) = @_;
53              
54 36         119 my $request = HTTP::Request->new;
55 36         950 $request->method($env->{'REQUEST_METHOD'});
56 36         220 $request->uri($env->{'REQUEST_URI'});
57 36         1189 $request->header(Content_Length => $env->{'CONTENT_LENGTH'});
58 36         1123 $request->header(Content_Type => $env->{'CONTENT_TYPE'});
59 36         1074 foreach my $header (grep { /^HTTP_/ } keys %$env) {
  880         671  
60 44         284 my $value = $env->{$header};
61 44         90 $header =~ s/^HTTP_//;
62 44         64 $header = uc($header);
63 44         77 $header =~ s/\b([a-z])/uc $!/ge;
  0         0  
64              
65 44         88 $request->header($header, $value);
66             }
67              
68 36         1078 my $input = $env->{'psgi.input'};
69 36         162 my $body = IO::String->new;
70 36         1026 my $buffer = '';
71 36         155 while($input->read($buffer, 1024) > 0) {
72 5         47 print $body $buffer;
73             }
74              
75 36         413 $body->setpos(0);
76 36         329 $env->{'psgi.input'} = $body;
77 36         39 $request->content(${ $body->string_ref });
  36         79  
78              
79 36         788 return $request;
80             }
81              
82             sub call {
83 58     58 1 168632 my ( $self, $env ) = @_;
84              
85 58         189 my $app = $self->app;
86 58         263 my $start_url = $self->start_url;
87 58         205 my $stop_url = $self->stop_url;
88 58         323 my $path = $env->{'PATH_INFO'};
89              
90 58         80 $env->{__PACKAGE__ . '.start_url'} = $start_url;
91 58         75 $env->{__PACKAGE__ . '.stop_url'} = $stop_url;
92              
93 58 100       432 if($path =~ m!\Q$start_url\E!) {
    100          
    100          
94 4         13 $self->active(1);
95 4         19 $env->{__PACKAGE__ . '.active'} = $self->active;
96             return [
97 4         33 200,
98             ['Content-Type' => 'text/plain'],
99             [ 'Request recording is ON' ],
100             ];
101             } elsif($path =~ m!\Q$stop_url\E!) {
102 7         22 $self->active(0);
103 7         32 $env->{__PACKAGE__ . '.active'} = $self->active;
104             return [
105 7         100 200,
106             ['Content-Type' => 'text/plain'],
107             [ 'Request recording is OFF' ],
108             ];
109             } elsif($self->active) {
110 36         173 my $req = $self->env_to_http_request($env);
111 36         551 my $frozen = encode_sereal($req);
112              
113 36         93 my $fh = $self->_output_fh($env);
114             # $guard looks unused, but it's unlocking the file upon its
115             # destruction
116 36         74 my $guard = $self->_create_concurrency_lock($fh, $env);
117 36 50       87 if($guard) {
118 36         280 $fh->write(pack('Na*', length($frozen), $frozen));
119 36         70159 $fh->flush;
120             }
121             }
122              
123 47         158 $env->{__PACKAGE__ . '.active'} = $self->active;
124              
125 47         254 return $app->($env);
126             }
127              
128             sub _create_concurrency_lock {
129 36     36   45 my ( $self, $fh, $env ) = @_;
130              
131 36 100 66     162 return 1 if !$env->{'psgi.multithread'} && !$env->{'psgi.multiprocess'};
132              
133 2 50       2 my $locked = eval { flock($fh, LOCK_EX) || die "$!\n" };
  2         13  
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   16 return Scope::Guard->new( sub { flock($fh, LOCK_UN) });
  2         33  
149             }
150              
151             1;
152              
153             # ABSTRACT: Plack middleware that records your client-server interactions
154              
155             __END__