File Coverage

blib/lib/Mojolicious/Plugin/AccessLog.pm
Criterion Covered Total %
statement 138 160 86.2
branch 44 70 62.8
condition 28 46 60.8
subroutine 37 39 94.8
pod 1 1 100.0
total 248 316 78.4


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::AccessLog;
2              
3 9     9   10012 use Mojo::Base 'Mojolicious::Plugin';
  9         12  
  9         59  
4 9     9   1636 use Mojo::IOLoop;
  9         12  
  9         79  
5              
6 9     9   169 use File::Spec;
  9         9  
  9         199  
7 9     9   37 use IO::File;
  9         14  
  9         1888  
8 9     9   50 use POSIX qw(setlocale strftime LC_ALL);
  9         10  
  9         77  
9 9     9   743 use Scalar::Util qw(blessed reftype weaken);
  9         16  
  9         520  
10 9     9   38 use Socket qw(inet_aton AF_INET);
  9         13  
  9         501  
11 9     9   42 use Time::HiRes qw(gettimeofday tv_interval);
  9         11  
  9         78  
12              
13             our $VERSION = '0.009';
14              
15             my $DEFAULT_FORMAT = 'common';
16             my %FORMATS = (
17             $DEFAULT_FORMAT => '%h %l %u %t "%r" %>s %b',
18             combined => '%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i"',
19             combinedio => '%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i" %I %O',
20             );
21              
22             # some systems (Windows) don't support %z correctly
23             my $TZOFFSET = strftime('%z', localtime) !~ /^[+-]\d{4}$/ && do {
24             require Time::Local;
25             my $t = time;
26             my $d = (Time::Local::timegm(localtime($t)) - $t) / 60;
27             sprintf '%+03d%02u', int($d / 60), $d % 60;
28             };
29             # some systems (Windows) don't support %s
30             my $NOEPOCHSECS = strftime('%s', localtime) !~ /^\d+$/;
31              
32             sub register {
33 9     9 1 578 my ($self, $app, $conf) = @_;
34 9   66     49 my $log = $conf->{log} // $app->log->handle;
35 9         135 my ($pkg, $f, $l) = caller 2; # :-/
36 9         22 my $fh;
37              
38 9 50       43 unless ($log) { # somebody cleared $app->log->handle?
39             # Log a warning nevertheless - there might be an event handler.
40 0         0 $app->log->warn(__PACKAGE__ . ': Log handle is not defined');
41 0         0 return;
42             }
43              
44 9   100     49 my $reftype = reftype $log // '';
45 9         13 my $logger;
46              
47 9 100 66     71 if ($reftype eq 'GLOB') {
    100 66        
    100 33        
    50          
48 4         6 $fh = $log;
49 4         6 eval { $fh->autoflush(1) };
  4         38  
50 4     31   187 $logger = sub { $fh->print($_[0]) };
  31         717  
51             }
52             elsif (blessed($log) and my $l = $log->can('print') || $log->can('info')) {
53 2     13   9 $logger = sub { $l->($log, $_[0]) };
  13         55  
54             }
55             elsif ($reftype eq 'CODE') {
56 2         3 $logger = $log;
57             }
58             elsif (defined $log and not ref $log) {
59 1 50       12 File::Spec->file_name_is_absolute($log)
60             or $log = $app->home->rel_file($log);
61              
62 1 50       7 $fh = IO::File->new($log, '>>')
63             or die <<"";
64             Can't open log file "$log": $! at $f line $l.
65              
66 1         342 $fh->autoflush(1);
67 1     7   57 $logger = sub { $fh->print($_[0]) };
  7         45  
68             }
69             else {
70 0         0 $app->log->error(__PACKAGE__ . ': not a valid "log" value');
71 0         0 return;
72             }
73              
74 9 50       28 if ($conf->{uname_helper}) {
75 0         0 warn <<"";
76             uname_helper is DEPRECATED in favor of \$c->req->env->{REMOTE_USER} at $f line $l.
77              
78              
79 0         0 my $helper_name = $conf->{uname_helper};
80              
81 0 0       0 $helper_name = 'set_username' if $helper_name !~ /^[\_A-za-z]\w*$/;
82              
83             $app->helper(
84 0     0   0 $helper_name => sub { $_[0]->req->env->{REMOTE_USER} = $_[1] }
85 0         0 );
86             }
87              
88 9         15 my @handler;
89             my $strftime = sub {
90 69     69   241 my ($fmt, @time) = @_;
91 69 50       188 $fmt =~ s/%z/$TZOFFSET/g if $TZOFFSET;
92 69 50       153 $fmt =~ s/%s/time()/ge if $NOEPOCHSECS;
  0         0  
93 69         730 my $old_locale = setlocale(LC_ALL);
94 69         304 setlocale(LC_ALL, 'C');
95 69         1661 my $out = strftime($fmt, @time);
96 69         288 setlocale(LC_ALL, $old_locale);
97 69         466 return $out;
98 9         32 };
99 9   66     43 my $format = $FORMATS{$conf->{format} // $DEFAULT_FORMAT};
100 9         12 my $safe_re;
101              
102 9 100       23 if ($format) {
103             # Apache default log formats don't quote username, which might
104             # have spaces.
105 6         23 $safe_re = qr/([^[:print:]]|\s)/;
106             }
107             else {
108             # For custom log format appropriate quoting is the user's responsibility.
109 3         8 $format = $conf->{format};
110             }
111              
112             # each handler is called with following parameters:
113             # 0: $tx, 1: $tx->req, 2: $tx->res, 3: $tx->req->url,
114             # 4: $request_start_time, 5: $process_time, 6: $bytes_in, 7: $bytes_out
115              
116             my $block_handler = sub {
117 22     22   66 my ($block, $type) = @_;
118              
119 104   100     1170 return sub { _safe($_[1]->headers->header($block) // '-') }
120 22 100       90 if $type eq 'i';
121              
122 18   50     309 return sub { $_[2]->headers->header($block) // '-' }
123 7 100       13 if $type eq 'o';
124              
125             return sub {
126 24 50       146 return $_[4][0]
127             if $block eq 'sec';
128 24 50       43 return sprintf "%u%03u", $_[4][0], int($_[4][1] / 1000)
129             if $block eq 'msec';
130 24 50       43 return sprintf "%u%06u", @{$_[4]}
  0         0  
131             if $block eq 'usec';
132 24 100       106 return sprintf('%03u', $_[4][1] / 1000)
133             if $block eq 'msec_frac';
134 18 50       190 return sprintf('%06u', $_[4][1])
135             if $block eq 'usec_frac';
136 18         529 return $strftime->($block, localtime($_[4][0]));
137             }
138 4 50       20 if $type eq 't';
139              
140 0   0     0 return sub { _safe($_[1]->cookie($block // '')) }
141 0 0       0 if $type eq 'C';
142              
143 0   0     0 return sub { _safe($_[1]->env->{$block // ''}) }
144 0 0       0 if $type eq 'e';
145              
146 0         0 $app->log->error("{$block}$type not supported");
147              
148 0         0 return '-';
149 9         31 };
150              
151 9 50   14   22 my $servername_cb = sub { $_[3]->base->host || '-' };
  14         213  
152 9 50   58   19 my $remoteaddr_cb = sub { $_[0]->remote_address || '-' };
  58         208  
153             my %char_handler = (
154             '%' => '%',
155             a => $remoteaddr_cb,
156 7   50 7   141 A => sub { $_[0]->local_address // '-' },
157             b => sub {
158 60 100 100 60   767 $_[7] && ($_[7] - $_[2]->header_size - $_[2]->start_line_size) || '-'
159             },
160             B => sub {
161 16 100   16   429 $_[7] ? $_[7] - $_[2]->header_size - $_[2]->start_line_size : '0'
162             },
163 7     7   125 D => sub { int($_[5] * 1000000) },
164             h => $remoteaddr_cb,
165 7     7   96 H => sub { 'HTTP/' . $_[1]->version },
166 19     19   198 I => sub { $_[6] },
167             l => '-',
168 7     7   60 m => sub { $_[1]->method },
169 19     19   692 O => sub { $_[7] },
170 7     7   50 p => sub { $_[0]->local_port },
171 7     7   63 P => sub { $$ },
172             q => sub {
173 7 100   7   24 my $s = $_[3]->query->to_string or return '';
174 2         184 return '?' . $s;
175             },
176 51     51   436 r => sub { substr($_[1]->build_start_line, 0, -2) },
177 66   100 66   2555 s => sub { $_[2]->code // '-' },
178             t => sub {
179 51     51   2077 $strftime->('[%d/%b/%Y:%H:%M:%S %z]', localtime($_[4][0]))
180             },
181 7     7   28 T => sub { int $_[5] },
182             u => sub {
183 51     51   1041 my $env = $_[1]->env;
184 51 50 50     623 my $user =
    100 100        
185             exists($env->{REMOTE_USER}) ?
186             length($env->{REMOTE_USER} // '') ?
187             $env->{REMOTE_USER} : '-' :
188             (split ':', $_[3]->base->userinfo || '-:')[0];
189              
190 51         660 return _safe($user, $safe_re)
191             },
192 7     7   24 U => sub { $_[3]->path },
193 9         276 v => $servername_cb,
194             V => $servername_cb,
195             );
196              
197 9 50       31 if ($conf->{hostname_lookups}) {
198             $char_handler{h} = sub {
199 0 0   0   0 my $ip = $_[0]->remote_address or return '-';
200 0         0 return gethostbyaddr(inet_aton($ip), AF_INET);
201 0         0 };
202             }
203              
204             my $char_handler = sub {
205 70     70   74 my $char = shift;
206 70         100 my $cb = $char_handler{$char};
207              
208 70 50       151 return $char_handler{$char} if $char_handler{$char};
209              
210 0         0 $app->log->error("\%$char not supported.");
211              
212 0         0 return '-';
213 9         26 };
214              
215 9         83 $format =~ s~
216             (?:
217             \%\{(.+?)\}([a-z]) |
218             \%(?:[<>])?([a-zA-Z\%])
219             )
220             ~
221 92 100       193 push @handler, $1 ? $block_handler->($1, $2) : $char_handler->($3);
222 92         223 '%s';
223             ~egx;
224              
225 9         41 chomp $format;
226 9   33     62 $format .= $conf->{lf} // $/ // "\n";
      50        
227              
228             $app->hook(after_build_tx => sub {
229 66     66   415594 my $tx = $_[0];
230 66         144 my $bcr = my $bcw = 0;
231 66         100 my ($r, $s, $t, $w);
232              
233             $tx->on(connection => sub {
234 66         2024 my ($tx, $connection) = @_;
235              
236 66         296 $t = [gettimeofday];
237 66         291 $s = Mojo::IOLoop->stream($connection);
238 66         1013 $r = $s->on(read => sub { $bcr += length $_[1] });
  523         10958552  
239 66         569 $w = $s->on(write => sub { $bcw += length $_[1] });
  81         2041363  
240 66         612 });
241              
242             # watch for the right moment to fetch the un-expanded start-line
243 66         503 $tx->req->once(progress => sub { $_[0]->build_start_line });
  66         54102  
244              
245             $tx->on(finish => sub {
246 66         507954 my $tx = shift;
247 66         212 my $dt = tv_interval($t);
248              
249 66         825 $s->unsubscribe(read => $r);
250 66         798 $s->unsubscribe(write => $w);
251 66         778 $logger->(_log($tx, $format, \@handler, $t, $dt, $bcr, $bcw));
252 66         2295 });
253 9         85 });
254             }
255              
256             sub _log {
257 66     66   183 my ($tx, $format, $handler) = (shift, shift, shift);
258 66         207 my $req = $tx->req;
259 66         405 my @args = ($tx, $req, $tx->res, $req->url, @_);
260              
261 66 100 50     907 sprintf $format, map(ref() ? ($_->(@args))[0] // '' : $_, @$handler);
262             }
263              
264             sub _safe {
265 155     155   1725 my $string = shift;
266 155   66     774 my $re = shift // qr/([^[:print:]])/;
267              
268 155 50       1079 $string =~ s/$re/'\x' . unpack('H*', $1)/eg
  16         139  
269             if defined $string;
270              
271 155         1007 return $string;
272             }
273              
274             1;
275              
276             __END__