File Coverage

blib/lib/Mojolicious/Plugin/AccessLog.pm
Criterion Covered Total %
statement 138 160 86.2
branch 44 70 62.8
condition 30 49 61.2
subroutine 37 39 94.8
pod 1 1 100.0
total 250 319 78.3


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::AccessLog;
2              
3 9     9   12653 use Mojo::Base 'Mojolicious::Plugin';
  9         27  
  9         72  
4 9     9   1999 use Mojo::IOLoop;
  9         30  
  9         92  
5              
6 9     9   259 use File::Spec;
  9         22  
  9         196  
7 9     9   51 use IO::File;
  9         17  
  9         1738  
8 9     9   75 use POSIX qw(setlocale strftime LC_ALL);
  9         20  
  9         94  
9 9     9   925 use Scalar::Util qw(blessed reftype weaken);
  9         29  
  9         622  
10 9     9   100 use Socket qw(inet_aton AF_INET);
  9         40  
  9         561  
11 9     9   66 use Time::HiRes qw(gettimeofday tv_interval);
  9         19  
  9         110  
12              
13             our $VERSION = '0.010001';
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 475 my ($self, $app, $conf) = @_;
34 9   66     61 my $log = $conf->{log} // $app->log->handle;
35 9         148 my ($pkg, $f, $l) = caller 2; # :-/
36              
37 9 50       51 unless ($log) { # somebody cleared $app->log->handle?
38             # Log a warning nevertheless - there might be an event handler.
39 0         0 $app->log->warn(__PACKAGE__ . ': Log handle is not defined');
40 0         0 return;
41             }
42              
43 9   100     56 my $reftype = reftype $log // '';
44 9         24 my $logger;
45              
46 9 100 66     77 if ($reftype eq 'GLOB') {
    100 66        
    100 33        
    50          
47 4         39 eval { $log->autoflush(1) };
  4         43  
48 4     31   342 $logger = sub { $log->print($_[0]) };
  31         953  
49             }
50             elsif (blessed($log) and my $l = $log->can('print') || $log->can('info')) {
51 2     13   35 $logger = sub { $l->($log, $_[0]) };
  13         59  
52             }
53             elsif ($reftype eq 'CODE') {
54 2         25 $logger = $log;
55             }
56             elsif (defined $log and not ref $log) {
57 1 50       23 File::Spec->file_name_is_absolute($log)
58             or $log = $app->home->rel_file($log);
59              
60 1 50       8 my $fh = IO::File->new($log, '>>')
61             or die <<"";
62             Can't open log file "$log": $! at $f line $l.
63              
64 1         349 $fh->autoflush(1);
65 1     7   63 $logger = sub { $fh->print($_[0]) };
  7         40  
66             }
67             else {
68 0         0 $app->log->error(__PACKAGE__ . ': not a valid "log" value');
69 0         0 return;
70             }
71              
72 9 50       43 if ($conf->{uname_helper}) {
73 0         0 warn <<"";
74             uname_helper is DEPRECATED in favor of \$c->req->env->{REMOTE_USER} at $f line $l.
75              
76              
77 0         0 my $helper_name = $conf->{uname_helper};
78              
79 0 0       0 $helper_name = 'set_username' if $helper_name !~ /^[\_A-za-z]\w*$/;
80              
81             $app->helper(
82 0     0   0 $helper_name => sub { $_[0]->req->env->{REMOTE_USER} = $_[1] }
83 0         0 );
84             }
85              
86 9         26 my @handler;
87             my $strftime = sub {
88 69     69   339 my ($fmt, @time) = @_;
89 69 50       212 $fmt =~ s/%z/$TZOFFSET/g if $TZOFFSET;
90 69 50       176 $fmt =~ s/%s/time()/ge if $NOEPOCHSECS;
  0         0  
91 69         389 my $old_locale = setlocale(LC_ALL);
92 69         898 setlocale(LC_ALL, 'C');
93 69         2142 my $out = strftime($fmt, @time);
94 69         731 setlocale(LC_ALL, $old_locale);
95 69         577 return $out;
96 9         41 };
97 9   66     94 my $format = $FORMATS{$conf->{format} // $DEFAULT_FORMAT};
98 9         20 my $safe_re;
99              
100 9 100       27 if ($format) {
101             # Apache default log formats don't quote username, which might
102             # have spaces.
103 6         46 $safe_re = qr/([^[:print:]]|\s)/;
104             }
105             else {
106             # For custom log format appropriate quoting is the user's responsibility.
107 3         7 $format = $conf->{format};
108             }
109              
110             # each handler is called with following parameters:
111             # 0: $tx, 1: $tx->req, 2: $tx->res, 3: $tx->req->url,
112             # 4: $request_start_time, 5: $process_time, 6: $bytes_in, 7: $bytes_out
113             # 8: HTTP request start line
114              
115             my $block_handler = sub {
116 22     22   70 my ($block, $type) = @_;
117              
118 104   100     1317 return sub { _safe($_[1]->headers->header($block) // '-') }
119 22 100       91 if $type eq 'i';
120              
121 18   50     252 return sub { $_[2]->headers->header($block) // '-' }
122 7 100       19 if $type eq 'o';
123              
124             return sub {
125 24 50       157 return $_[4][0]
126             if $block eq 'sec';
127 24 50       48 return sprintf "%u%03u", $_[4][0], int($_[4][1] / 1000)
128             if $block eq 'msec';
129 24 50       54 return sprintf "%u%06u", @{$_[4]}
  0         0  
130             if $block eq 'usec';
131 24 100       86 return sprintf('%03u', $_[4][1] / 1000)
132             if $block eq 'msec_frac';
133 18 50       38 return sprintf('%06u', $_[4][1])
134             if $block eq 'usec_frac';
135 18         388 return $strftime->($block, localtime($_[4][0]));
136             }
137 4 50       17 if $type eq 't';
138              
139 0   0     0 return sub { _safe($_[1]->cookie($block // '')) }
140 0 0       0 if $type eq 'C';
141              
142 0   0     0 return sub { _safe($_[1]->env->{$block // ''}) }
143 0 0       0 if $type eq 'e';
144              
145 0         0 $app->log->error("{$block}$type not supported");
146              
147 0         0 return '-';
148 9         42 };
149              
150 9 50   14   29 my $servername_cb = sub { $_[3]->base->host || '-' };
  14         181  
151 9 50   58   25 my $remoteaddr_cb = sub { $_[0]->remote_address || '-' };
  58         206  
152             my %char_handler = (
153             '%' => '%',
154             a => $remoteaddr_cb,
155 7   50 7   144 A => sub { $_[0]->local_address // '-' },
156             b => sub {
157 60 100 100 60   709 $_[7] && ($_[7] - $_[2]->header_size - $_[2]->start_line_size) || '-'
158             },
159             B => sub {
160 16 100   16   438 $_[7] ? $_[7] - $_[2]->header_size - $_[2]->start_line_size : '0'
161             },
162 7     7   152 D => sub { int($_[5] * 1000000) },
163             h => $remoteaddr_cb,
164 7     7   110 H => sub { 'HTTP/' . $_[1]->version },
165 19     19   222 I => sub { $_[6] },
166             l => '-',
167 7     7   64 m => sub { $_[1]->method },
168 19     19   220 O => sub { $_[7] },
169 7     7   54 p => sub { $_[0]->local_port },
170 7     7   57 P => sub { $$ },
171             q => sub {
172 7 100   7   27 my $s = $_[3]->query->to_string or return '';
173 2         564 return '?' . $s;
174             },
175 51     51   380 r => sub { $_[8] },
176 66   100 66   290 s => sub { $_[2]->code // '-' },
177             t => sub {
178 51     51   1709 $strftime->('[%d/%b/%Y:%H:%M:%S %z]', localtime($_[4][0]))
179             },
180 7     7   33 T => sub { int $_[5] },
181             u => sub {
182 51     51   1053 my $env = $_[1]->env;
183             my $user =
184             exists($env->{REMOTE_USER}) ?
185             length($env->{REMOTE_USER} // '') ?
186 51 50 50     576 $env->{REMOTE_USER} : '-' :
    100 100        
187             (split ':', $_[3]->base->userinfo || '-:')[0];
188              
189 51         654 return _safe($user, $safe_re)
190             },
191 7     7   29 U => sub { $_[3]->path },
192 9         270 v => $servername_cb,
193             V => $servername_cb,
194             );
195              
196 9 50       37 if ($conf->{hostname_lookups}) {
197             $char_handler{h} = sub {
198 0 0   0   0 my $ip = $_[0]->remote_address or return '-';
199 0         0 return gethostbyaddr(inet_aton($ip), AF_INET);
200 0         0 };
201             }
202              
203             my $char_handler = sub {
204 70     70   128 my $char = shift;
205 70         124 my $cb = $char_handler{$char};
206              
207 70 50       201 return $char_handler{$char} if $char_handler{$char};
208              
209 0         0 $app->log->error("\%$char not supported.");
210              
211 0         0 return '-';
212 9         34 };
213              
214 9         104 $format =~ s~
215             (?:
216             \%\{(.+?)\}([a-z]) |
217             \%(?:[<>])?([a-zA-Z\%])
218             )
219             ~
220 92 100       300 push @handler, $1 ? $block_handler->($1, $2) : $char_handler->($3);
221 92         364 '%s';
222             ~egx;
223              
224 9         44 chomp $format;
225 9   33     80 $format .= $conf->{lf} // $/ // "\n";
      50        
226              
227             $app->hook(after_build_tx => sub {
228 66     66   421366 my $tx = $_[0];
229              
230             $tx->on(connection => sub {
231 66         2272 my ($tx, $connection) = @_;
232 66         134 my $bcr = my $bcw = 0;
233 66         115 my $sl;
234 66         265 my $t = [gettimeofday];
235 66         298 my $s = Mojo::IOLoop->stream($connection);
236             my $r = $s->on(read => sub {
237             # get the unmodified HTTP request start line
238 971   66     11437517 $sl //= substr($_[1], 0, index($_[1], "\r\n"));
239 971         2182 $bcr += length $_[1];
240 66         1101 });
241 66         514 my $w = $s->on(write => sub { $bcw += length $_[1] });
  81         2038252  
242              
243 66         504 weaken $s;
244 66         204 weaken $r;
245 66         165 weaken $w;
246              
247             $tx->on(finish => sub {
248 66         30109396 my $tx = shift;
249 66         261 my $dt = tv_interval($t);
250              
251 66         1219 $s->unsubscribe(read => $r);
252 66         1522 $s->unsubscribe(write => $w);
253 66         1202 $logger->(_log($tx, $format, \@handler, $t, $dt, $bcr, $bcw, $sl));
254 66         341 });
255 66         626 });
256 9         114 });
257             }
258              
259             sub _log {
260 66     66   221 my ($tx, $format, $handler) = (shift, shift, shift);
261 66         219 my $req = $tx->req;
262 66         410 my @args = ($tx, $req, $tx->res, $req->url, @_);
263              
264 66 100 50     788 sprintf $format, map(ref() ? ($_->(@args))[0] // '' : $_, @$handler);
265             }
266              
267             sub _safe {
268 155     155   1963 my $string = shift;
269 155   66     706 my $re = shift // qr/([^[:print:]])/;
270              
271 155 50       1015 $string =~ s/$re/'\x' . unpack('H*', $1)/eg
  16         131  
272             if defined $string;
273              
274 155         1042 return $string;
275             }
276              
277             1;
278              
279             __END__