File Coverage

blib/lib/Plack/Middleware/TrafficLog.pm
Criterion Covered Total %
statement 85 86 98.8
branch 40 46 86.9
condition 7 8 87.5
subroutine 17 18 94.4
pod 2 2 100.0
total 151 160 94.3


line stmt bran cond sub pod time code
1             package Plack::Middleware::TrafficLog;
2              
3             =head1 NAME
4              
5             Plack::Middleware::TrafficLog - Log headers and body of HTTP traffic
6              
7             =head1 SYNOPSIS
8              
9             # In app.psgi
10             use Plack::Builder;
11              
12             builder {
13             enable "TrafficLog", with_body => 1;
14             };
15              
16             =head1 DESCRIPTION
17              
18             This middleware logs the request and response messages with detailed
19             information about headers and body.
20              
21             The example log:
22              
23             [08/Aug/2012:16:59:47 +0200] [164836368] [127.0.0.1 -> 0:5000] [Request ]
24             |GET / HTTP/1.1|Connection: TE, close|Host: localhost:5000|TE: deflate,gzi
25             p;q=0.3|User-Agent: lwp-request/6.03 libwww-perl/6.03||
26             [08/Aug/2012:16:59:47 +0200] [164836368] [127.0.0.1 <- 0:5000] [Response]
27             |HTTP/1.0 200 OK|Content-Type: text/plain||Hello World
28              
29             This module works also with applications which have delayed response. In that
30             case each chunk is logged separately and shares the same unique ID number and
31             headers.
32              
33             The body of request and response is not logged by default. For streaming
34             responses only first chunk is logged by default.
35              
36             =for readme stop
37              
38             =cut
39              
40              
41 2     2   30517 use 5.008;
  2         8  
42              
43 2     2   12 use strict;
  2         3  
  2         51  
44 2     2   12 use warnings;
  2         7  
  2         94  
45              
46             our $VERSION = '0.0401';
47              
48              
49 2     2   778 use parent 'Plack::Middleware';
  2         294  
  2         15  
50              
51 2         13 use Plack::Util::Accessor qw(
52             with_request with_response with_date with_body with_all_chunks eol body_eol logger
53             _counter _call_id _strftime
54 2     2   17477 );
  2         4  
55              
56              
57 2     2   213 use Plack::Util;
  2         2  
  2         41  
58              
59 2     2   1616 use Plack::Request;
  2         112358  
  2         66  
60 2     2   1541 use Plack::Response;
  2         6555  
  2         119  
61              
62 2     2   1659 use POSIX ();
  2         13691  
  2         48  
63 2     2   1687 use POSIX::strftime::Compiler ();
  2         18576  
  2         64  
64 2     2   19 use Scalar::Util ();
  2         5  
  2         2567  
65              
66              
67             sub prepare_app {
68 13     13 1 29645 my ($self) = @_;
69              
70             # the default values
71 13 100       46 $self->with_request(Plack::Util::TRUE) unless defined $self->with_request;
72 13 100       228 $self->with_response(Plack::Util::TRUE) unless defined $self->with_response;
73 13 100       144 $self->with_date(Plack::Util::TRUE) unless defined $self->with_date;
74 13 100       120 $self->with_body(Plack::Util::FALSE) unless defined $self->with_body;
75 13 100       125 $self->with_all_chunks(Plack::Util::FALSE) unless defined $self->with_all_chunks;
76 13 100       155 $self->body_eol(defined $self->eol ? $self->eol : ' ') unless defined $self->body_eol;
    100          
77 13 100       213 $self->eol('|') unless defined $self->eol;
78              
79 13         174 $self->_strftime(POSIX::strftime::Compiler->new('%d/%b/%Y:%H:%M:%S %z'));
80              
81 13         4487 $self->_counter(0);
82             };
83              
84              
85             sub _log_message {
86 25     25   55 my ($self, $type, $env, $status, $headers, $body) = @_;
87              
88 25   50 0   67 my $logger = $self->logger || sub { $env->{'psgi.errors'}->print(@_) };
  0         0  
89              
90 25         198 my $server_addr = sprintf '%s:%s', $env->{SERVER_NAME}, $env->{SERVER_PORT};
91             my $remote_addr = defined $env->{REMOTE_PORT}
92             ? sprintf '%s:%s', $env->{REMOTE_ADDR}, $env->{REMOTE_PORT}
93 25 50       113 : $env->{REMOTE_ADDR};
94              
95 25         67 my $eol = $self->eol;
96 25         136 my $body_eol = $self->body_eol;
97 25 50       222 $body =~ s/\015?\012/$body_eol/gs if defined $body_eol;
98              
99 25 100       67 my $date = $self->with_date
100             ? ('['. $self->_strftime->to_string(localtime) . '] ')
101             : '';
102              
103 25 50       1626 $logger->( sprintf "%s[%s] [%s %s %s] [%s] %s%s%s%s%s%s\n",
    100          
104             $date,
105             $self->_call_id,
106              
107             $remote_addr,
108             $type eq 'Request ' ? '->' : $type eq 'Response' ? '<-' : '--',
109             $server_addr,
110              
111             $type,
112              
113             $eol,
114             $status,
115             $eol,
116             $headers->as_string($eol),
117             $eol,
118             $body,
119             );
120             };
121              
122              
123             sub _log_request {
124 12     12   85 my ($self, $env) = @_;
125              
126 12         73 my $req = Plack::Request->new($env);
127              
128 12         118 my $status = sprintf '%s %s %s', $req->method, $req->request_uri, $req->protocol;
129 12         261 my $headers = $req->headers;
130 12 100       1424 my $body = $self->with_body ? $req->content : '';
131              
132 12         8546 $self->_log_message('Request ', $env, $status, $headers, $body);
133             };
134              
135              
136             sub _log_response {
137 13     13   17 my ($self, $env, $ret) = @_;
138              
139 13         79 my $res = Plack::Response->new(@$ret);
140              
141 13         769 my $status_code = $res->status;
142 13         72 my $status_message = HTTP::Status::status_message($status_code);
143              
144 13 100       79 my $status = sprintf 'HTTP/1.0 %s %s', $status_code, defined $status_message ? $status_message : '';
145 13         36 my $headers = $res->headers;
146 13         74 my $body = '';
147 13 100       33 if ($self->with_body) {
148 12         86 $body = $res->content;
149 12 50       83 $body = '' unless defined $body;
150 12 50       39 $body = join '', grep { defined $_ } @$body if ref $body eq 'ARRAY';
  12         30  
151             }
152              
153 13         34 $self->_log_message('Response', $env, $status, $headers, $body);
154             };
155              
156              
157             sub call {
158 13     13 1 25568 my ($self, $env) = @_;
159              
160             $self->_call_id(sprintf '%015d',
161             time % 2**16 * 2**32 +
162 13 50       98 (Scalar::Util::looks_like_number $env->{REMOTE_PORT} ? $env->{REMOTE_PORT} : int rand 2**16) % 2**16 * 2**16 +
163             $self->_counter % 2**16);
164 13         162 $self->_counter($self->_counter + 1);
165              
166             # Preprocessing
167 13 100       114 $self->_log_request($env) if $self->with_request;
168              
169             # $self->app is the original app
170 13         824 my $res = $self->app->($env);
171              
172             # Postprocessing
173             return $self->with_response ? $self->response_cb($res, sub {
174 12     12   361 my ($ret) = @_;
175 12         16 my $seen;
176             return sub {
177 25         554 my ($chunk) = @_;
178 25 100 100     112 return if $seen and not defined $chunk;
179 14 100 100     41 return $chunk if $seen and not $self->with_all_chunks;
180 13         58 $self->_log_response($env, [ $ret->[0], $ret->[1], [$chunk] ]);
181 13         499 $seen = Plack::Util::TRUE;
182 13         47 return $chunk;
183 12         50 };
184 13 100       134 }) : $res;
185             };
186              
187              
188             1;
189              
190              
191             =head1 CONFIGURATION
192              
193             =over 4
194              
195             =item logger
196              
197             # traffic.l4p
198             log4perl.logger.traffic = DEBUG, LogfileTraffic
199             log4perl.appender.LogfileTraffic = Log::Log4perl::Appender::File
200             log4perl.appender.LogfileTraffic.filename = traffic.log
201             log4perl.appender.LogfileTraffic.layout = PatternLayout
202             log4perl.appender.LogfileTraffic.layout.ConversionPattern = %m{chomp}%n
203              
204             # app.psgi
205             use Log::Log4perl qw(:levels get_logger);
206             Log::Log4perl->init('traffic.l4p');
207             my $logger = get_logger('traffic');
208              
209             enable "Plack::Middleware::TrafficLog",
210             logger => sub { $logger->log($INFO, join '', @_) };
211              
212             Sets a callback to print log message to. It prints to C output
213             stream by default.
214              
215             =item with_request
216              
217             The false value disables logging of request message.
218              
219             =item with_response
220              
221             The false value disables logging of response message.
222              
223             =item with_date
224              
225             The false value disables logging of current date.
226              
227             =item with_body
228              
229             The true value enables logging of message's body.
230              
231             =item with_all_chunks
232              
233             The true value enables logging of every chunk for streaming responses.
234              
235             =item eol
236              
237             Sets the line separator for message's headers and body. The default value is
238             the pipe character C<|>.
239              
240             =item body_eol
241              
242             Sets the line separator for message's body only. The default is the space
243             character C< >. The default value is used only if B is also undefined.
244              
245             =back
246              
247             =for readme continue
248              
249             =head1 SEE ALSO
250              
251             L, L.
252              
253             =head1 BUGS
254              
255             This module has unstable API and it can be changed in future.
256              
257             The log file can contain the binary data if the PSGI server provides binary
258             files.
259              
260             If you find the bug or want to implement new features, please report it at
261             L
262              
263             The code repository is available at
264             L
265              
266             =head1 AUTHOR
267              
268             Piotr Roszatycki
269              
270             =head1 LICENSE
271              
272             Copyright (c) 2012, 2014-2015 Piotr Roszatycki .
273              
274             This is free software; you can redistribute it and/or modify it under
275             the same terms as perl itself.
276              
277             See L