| 1 | | | | | package Apache::LogFormat::Compiler; |
| 2 | | | | | |
| 3 | | | | | use strict; |
| 4 | | | | | use warnings; |
| 5 | | | | | use 5.008004; |
| 6 | | | | | use Carp; |
| 7 | | | | | use POSIX::strftime::Compiler qw//; |
| 8 | | | | | use constant { |
| 9 | | | | | ENVS => 0, |
| 10 | | | | | RES => 1, |
| 11 | | | | | LENGTH => 2, |
| 12 | | | | | REQTIME => 3, |
| 13 | | | | | TIME => 4, |
| 14 | | | | | }; |
| 15 | | | | | |
| 16 | | | | | our $VERSION = '0.30'; |
| 17 | | | | | |
| 18 | | | | | # copy from Plack::Middleware::AccessLog |
| 19 | | | | | our %formats = ( |
| 20 | | | | | common => '%h %l %u %t "%r" %>s %b', |
| 21 | | | | | combined => '%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-agent}i"', |
| 22 | | | | | ); |
| 23 | | | | | |
| 24 | | | | | sub _safe { |
| 25 | | | | | my $string = shift; |
| 26 | | | | | return unless defined $string; |
| 27 | | | | | $string =~ s/([^[:print:]])/"\\x" . unpack("H*", $1)/eg; |
| 28 | | | | | return $string; |
| 29 | | | | | } |
| 30 | | | | | |
| 31 | | | | | sub _string { |
| 32 | | | | | my $string = shift; |
| 33 | | | | | return '-' if ! defined $string; |
| 34 | | | | | return '-' if ! length $string; |
| 35 | | | | | $string =~ s/([^[:print:]])/"\\x" . unpack("H*", $1)/eg; |
| 36 | | | | | return $string; |
| 37 | | | | | } |
| 38 | | | | | |
| 39 | | | | | sub header_get { |
| 40 | | | | | my ($headers, $key) = @_; |
| 41 | | | | | $key = lc $key; |
| 42 | | | | | my @headers = @$headers; # copy |
| 43 | | | | | my $value; |
| 44 | | | | | while (my($hdr, $val) = splice @headers, 0, 2) { |
| 45 | | | | | if ( lc $hdr eq $key ) { |
| 46 | | | | | $value = $val; |
| 47 | | | | | last; |
| 48 | | | | | } |
| 49 | | | | | } |
| 50 | | | | | return $value; |
| 51 | | | | | } |
| 52 | | | | | |
| 53 | | | | | my $psgi_reserved = { CONTENT_LENGTH => 1, CONTENT_TYPE => 1 }; |
| 54 | | | | | |
| 55 | | | | | my $block_handler = sub { |
| 56 | | | | | my($block, $type, $extra) = @_; |
| 57 | | | | | my $cb; |
| 58 | | | | | if ($type eq 'i') { |
| 59 | | | | | $block =~ s/-/_/g; |
| 60 | | | | | $block = uc($block); |
| 61 | | | | | $block = "HTTP_${block}" unless $psgi_reserved->{$block}; |
| 62 | | | | | $cb = q!_string($_[ENVS]->{'!.$block.q!'})!; |
| 63 | | | | | } elsif ($type eq 'o') { |
| 64 | | | | | $cb = q!_string(header_get($_[RES]->[1],'!.$block.q!'))!; |
| 65 | | | | | } elsif ($type eq 't') { |
| 66 | | | | | $cb = q!"[" . POSIX::strftime::Compiler::strftime('!.$block.q!', @lt) . "]"!; |
| 67 | | | | | } elsif (exists $extra->{$type}) { |
| 68 | | | | | $cb = q!_string($extra_block_handlers->{'!.$type.q!'}->('!.$block.q!',$_[ENVS],$_[RES],$_[LENGTH],$_[REQTIME]))!; |
| 69 | | | | | } else { |
| 70 | | | | | Carp::croak("{$block}$type not supported"); |
| 71 | | | | | $cb = "-"; |
| 72 | | | | | } |
| 73 | | | | | return q|! . | . $cb . q| |
| 74 | | | | | . q!|; |
| 75 | | | | | }; |
| 76 | | | | | |
| 77 | | | | | our %char_handler = ( |
| 78 | | | | | '%' => q!'%'!, |
| 79 | | | | | h => q!($_[ENVS]->{REMOTE_ADDR} || '-')!, |
| 80 | | | | | l => q!'-'!, |
| 81 | | | | | u => q!($_[ENVS]->{REMOTE_USER} || '-')!, |
| 82 | | | | | t => q!'[' . $t . ']'!, |
| 83 | | | | | r => q!_safe($_[ENVS]->{REQUEST_METHOD}) . " " . _safe($_[ENVS]->{REQUEST_URI}) . |
| 84 | | | | | " " . $_[ENVS]->{SERVER_PROTOCOL}!, |
| 85 | | | | | s => q!$_[RES]->[0]!, |
| 86 | | | | | b => q!(defined $_[LENGTH] ? $_[LENGTH] : '-')!, |
| 87 | | | | | T => q!(defined $_[REQTIME] ? int($_[REQTIME]*1_000_000) : '-')!, |
| 88 | | | | | D => q!(defined $_[REQTIME] ? $_[REQTIME] : '-')!, |
| 89 | | | | | v => q!($_[ENVS]->{SERVER_NAME} || '-')!, |
| 90 | | | | | V => q!($_[ENVS]->{HTTP_HOST} || $_[ENVS]->{SERVER_NAME} || '-')!, |
| 91 | | | | | p => q!$_[ENVS]->{SERVER_PORT}!, |
| 92 | | | | | P => q!$$!, |
| 93 | | | | | m => q!_safe($_[ENVS]->{REQUEST_METHOD})!, |
| 94 | | | | | U => q!_safe($_[ENVS]->{PATH_INFO})!, |
| 95 | | | | | q => q!(($_[ENVS]->{QUERY_STRING} ne '') ? '?' . _safe($_[ENVS]->{QUERY_STRING}) : '' )!, |
| 96 | | | | | H => q!$_[ENVS]->{SERVER_PROTOCOL}!, |
| 97 | | | | | |
| 98 | | | | | ); |
| 99 | | | | | |
| 100 | | | | | my $char_handler = sub { |
| 101 | | | | | my ($char, $extra) = @_; |
| 102 | | | | | my $cb = $char_handler{$char}; |
| 103 | | | | | if (!$cb && exists $extra->{$char}) { |
| 104 | | | | | $cb = q!_string($extra_char_handlers->{'!.$char.q!'}->($_[ENVS],$_[RES],$_[LENGTH],$_[REQTIME]))!; |
| 105 | | | | | } |
| 106 | | | | | unless ($cb) { |
| 107 | | | | | Carp::croak "\%$char not supported."; |
| 108 | | | | | return "-"; |
| 109 | | | | | } |
| 110 | | | | | q|! . | . $cb . q| |
| 111 | | | | | . q!|; |
| 112 | | | | | }; |
| 113 | | | | | |
| 114 | | | | | sub new { |
| 115 | | | | | my $class = shift; |
| 116 | | | | | |
| 117 | | | | | my $fmt = shift || "combined"; |
| 118 | | | | | $fmt = $formats{$fmt} if exists $formats{$fmt}; |
| 119 | | | | | |
| 120 | | | | | my %opts = @_; |
| 121 | | | | | |
| 122 | | | | | my ($code_ref, $code) = compile($fmt, $opts{block_handlers} || {}, $opts{char_handlers} || {}); |
| 123 | | | | | bless [$code_ref, $code], $class; |
| 124 | | | | | } |
| 125 | | | | | |
| 126 | | | | | sub compile { |
| 127 | | | | | my $fmt = shift; |
| 128 | | | | | my $extra_block_handlers = shift; |
| 129 | | | | | my $extra_char_handlers = shift; |
| 130 | | | | | $fmt =~ s/!/\\!/g; |
| 131 | | | | | $fmt =~ s! |
| 132 | | | | | (?: |
| 133 | | | | | \%\{(.+?)\}([a-zA-Z]) | |
| 134 | | | | | \%(?:[<>])?([a-zA-Z\%]) |
| 135 | | | | | ) |
| 136 | | | | | ! $1 ? $block_handler->($1, $2, $extra_block_handlers) : $char_handler->($3, $extra_char_handlers) !egx; |
| 137 | | | | | |
| 138 | | | | | my @abbr = qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec ); |
| 139 | | | | | my $c = {}; |
| 140 | | | | | $fmt = q~sub { |
| 141 | | | | | $_[TIME] = time() if ! defined $_[TIME]; |
| 142 | | | | | my @lt = localtime($_[TIME]); |
| 143 | | | | | if ( ! exists $c->{tz_cache} || ! exists $c->{isdst_cache} || $lt[8] != $c->{isdst_cache} ) { |
| 144 | | | | | $c->{tz_cache} = POSIX::strftime::Compiler::strftime('%z',@lt); |
| 145 | | | | | $c->{isdst_cache} = $lt[8]; |
| 146 | | | | | } |
| 147 | | | | | my $t = sprintf '%02d/%s/%04d:%02d:%02d:%02d %s', $lt[3], $abbr[$lt[4]], $lt[5]+1900, |
| 148 | | | | | $lt[2], $lt[1], $lt[0], $c->{tz_cache}; |
| 149 | | | | | q!~ . $fmt . q~! |
| 150 | | | | | }~; |
| 151 | 1 | 4.16ms | | | my $code_ref = eval $fmt; ## no critic |
| 152 | | | | | die $@ . "\n===\n" . $fmt if $@; |
| 153 | | | | | wantarray ? ($code_ref, $fmt) : $code_ref; |
| 154 | | | | | } |
| 155 | | | | | |
| 156 | | | | | sub log_line { |
| 157 | | | | | my $self = shift; |
| 158 | | | | | $self->[0]->(@_) . "\n"; |
| 159 | | | | | } |
| 160 | | | | | |
| 161 | | | | | sub code { |
| 162 | | | | | my $self = shift; |
| 163 | | | | | $self->[1]; |
| 164 | | | | | } |
| 165 | | | | | |
| 166 | | | | | sub code_ref { |
| 167 | | | | | my $self = shift; |
| 168 | | | | | $self->[0]; |
| 169 | | | | | } |
| 170 | | | | | |
| 171 | | | | | 1; |
| 172 | | | | | __END__ |
| 173 | | | | | |
| 174 | | | | | =encoding utf8 |
| 175 | | | | | |
| 176 | | | | | =head1 NAME |
| 177 | | | | | |
| 178 | | | | | Apache::LogFormat::Compiler - Compile a log format string to perl-code |
| 179 | | | | | |
| 180 | | | | | =head1 SYNOPSIS |
| 181 | | | | | |
| 182 | | | | | use Apache::LogFormat::Compiler; |
| 183 | | | | | |
| 184 | | | | | my $log_handler = Apache::LogFormat::Compiler->new("combined"); |
| 185 | | | | | my $log = $log_handler->log_line( |
| 186 | | | | | $env, |
| 187 | | | | | $res, |
| 188 | | | | | $length, |
| 189 | | | | | $reqtime, |
| 190 | | | | | $time |
| 191 | | | | | ); |
| 192 | | | | | |
| 193 | | | | | =head1 DESCRIPTION |
| 194 | | | | | |
| 195 | | | | | Compile a log format string to perl-code. For faster generation of access_log lines. |
| 196 | | | | | |
| 197 | | | | | =head1 METHOD |
| 198 | | | | | |
| 199 | | | | | =over 4 |
| 200 | | | | | |
| 201 | | | | | =item new($fmt:String) |
| 202 | | | | | |
| 203 | | | | | Takes a format string (or a preset template C<combined> or C<custom>) |
| 204 | | | | | to specify the log format. This module implements a subset of |
| 205 | | | | | L<Apache's LogFormat templates|http://httpd.apache.org/docs/2.0/mod/mod_log_config.html>: |
| 206 | | | | | |
| 207 | | | | | %% a percent sign |
| 208 | | | | | %h REMOTE_ADDR from the PSGI environment, or - |
| 209 | | | | | %l remote logname not implemented (currently always -) |
| 210 | | | | | %u REMOTE_USER from the PSGI environment, or - |
| 211 | | | | | %t [local timestamp, in default format] |
| 212 | | | | | %r REQUEST_METHOD, REQUEST_URI and SERVER_PROTOCOL from the PSGI environment |
| 213 | | | | | %s the HTTP status code of the response |
| 214 | | | | | %b content length of the response |
| 215 | | | | | %T custom field for handling times in subclasses |
| 216 | | | | | %D custom field for handling sub-second times in subclasses |
| 217 | | | | | %v SERVER_NAME from the PSGI environment, or - |
| 218 | | | | | %V HTTP_HOST or SERVER_NAME from the PSGI environment, or - |
| 219 | | | | | %p SERVER_PORT from the PSGI environment |
| 220 | | | | | %P the worker's process id |
| 221 | | | | | %m REQUEST_METHOD from the PSGI environment |
| 222 | | | | | %U PATH_INFO from the PSGI environment |
| 223 | | | | | %q QUERY_STRING from the PSGI environment |
| 224 | | | | | %H SERVER_PROTOCOL from the PSGI environment |
| 225 | | | | | |
| 226 | | | | | In addition, custom values can be referenced, using C<%{name}>, |
| 227 | | | | | with one of the mandatory modifier flags C<i>, C<o> or C<t>: |
| 228 | | | | | |
| 229 | | | | | %{variable-name}i HTTP_VARIABLE_NAME value from the PSGI environment |
| 230 | | | | | %{header-name}o header-name header in the response |
| 231 | | | | | %{time-format]t localtime in the specified strftime format |
| 232 | | | | | |
| 233 | | | | | =item log_line($env:HashRef, $res:ArrayRef, $length:Integer, $reqtime:Integer, $time:Integer): $log:String |
| 234 | | | | | |
| 235 | | | | | Generates log line. |
| 236 | | | | | |
| 237 | | | | | $env PSGI env request HashRef |
| 238 | | | | | $res PSGI response ArrayRef |
| 239 | | | | | $length Content-Length |
| 240 | | | | | $reqtime The time taken to serve request in microseconds. optional |
| 241 | | | | | $time Time the request was received. optional. If $time is undefined. current timestamp is used. |
| 242 | | | | | |
| 243 | | | | | Sample psgi |
| 244 | | | | | |
| 245 | | | | | use Plack::Builder; |
| 246 | | | | | use Time::HiRes; |
| 247 | | | | | use Apache::LogFormat::Compiler; |
| 248 | | | | | |
| 249 | | | | | my $log_handler = Apache::LogFormat::Compiler->new( |
| 250 | | | | | '%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-agent}i" %D' |
| 251 | | | | | ); |
| 252 | | | | | my $compile_log_app = builder { |
| 253 | | | | | enable sub { |
| 254 | | | | | my $app = shift; |
| 255 | | | | | sub { |
| 256 | | | | | my $env = shift; |
| 257 | | | | | my $t0 = [gettimeofday]; |
| 258 | | | | | my $res = $app->(); |
| 259 | | | | | my $reqtime = int(Time::HiRes::tv_interval($t0) * 1_000_000); |
| 260 | | | | | $env->{psgi.error}->print($log_handler->log_line( |
| 261 | | | | | $env,$res,6,$reqtime, $t0->[0])); |
| 262 | | | | | } |
| 263 | | | | | }; |
| 264 | | | | | $app |
| 265 | | | | | }; |
| 266 | | | | | |
| 267 | | | | | =back |
| 268 | | | | | |
| 269 | | | | | =head1 ABOUT POSIX::strftime::Compiler |
| 270 | | | | | |
| 271 | | | | | This module uses L<POSIX::strftime::Compiler> for generate datetime string. POSIX::strftime::Compiler provides GNU C library compatible strftime(3). But this module will not affected by the system locale. This feature is useful when you want to write loggers, servers and portable applications. |
| 272 | | | | | |
| 273 | | | | | =head1 ADD CUSTOM FORMAT STRING |
| 274 | | | | | |
| 275 | | | | | Apache::LogFormat::Compiler allows one to add a custom format string |
| 276 | | | | | |
| 277 | | | | | my $log_handler = Apache::LogFormat::Compiler->new( |
| 278 | | | | | '%z %{HTTP_X_FORWARDED_FOR|REMOTE_ADDR}Z', |
| 279 | | | | | char_handlers => +{ |
| 280 | | | | | 'z' => sub { |
| 281 | | | | | my ($env,$req) = @_; |
| 282 | | | | | return $env->{HTTP_X_FORWARDED_FOR}; |
| 283 | | | | | } |
| 284 | | | | | }, |
| 285 | | | | | block_handlers => +{ |
| 286 | | | | | 'Z' => sub { |
| 287 | | | | | my ($block,$env,$req) = @_; |
| 288 | | | | | # block eq 'HTTP_X_FORWARDED_FOR|REMOTE_ADDR' |
| 289 | | | | | my ($main, $alt) = split('\|', $args); |
| 290 | | | | | return exists $env->{$main} ? $env->{$main} : $env->{$alt}; |
| 291 | | | | | } |
| 292 | | | | | }, |
| 293 | | | | | ); |
| 294 | | | | | |
| 295 | | | | | Any single letter can be used, other than those already defined by Apache::LogFormat::Compiler. |
| 296 | | | | | Your sub is called with two or three arguments: the content inside the C<{}> |
| 297 | | | | | from the format (block_handlers only), the PSGI environment (C<$env>), |
| 298 | | | | | and the ArrayRef of the response. It should return the string to be logged. |
| 299 | | | | | |
| 300 | | | | | =head1 AUTHOR |
| 301 | | | | | |
| 302 | | | | | Masahiro Nagano E<lt>kazeburo@gmail.comE<gt> |
| 303 | | | | | |
| 304 | | | | | =head1 SEE ALSO |
| 305 | | | | | |
| 306 | | | | | L<Plack::Middleware::AccessLog>, L<http://httpd.apache.org/docs/2.2/mod/mod_log_config.html> |
| 307 | | | | | |
| 308 | | | | | =head1 LICENSE |
| 309 | | | | | |
| 310 | | | | | Copyright (C) Masahiro Nagano |
| 311 | | | | | |
| 312 | | | | | This library is free software; you can redistribute it and/or modify |
| 313 | | | | | it under the same terms as Perl itself. |
| 314 | | | | | |
| 315 | | | | | =cut |