File Coverage

blib/lib/Apache/Log/Parser.pm
Criterion Covered Total %
statement 156 163 95.7
branch 77 88 87.5
condition 17 33 51.5
subroutine 15 16 93.7
pod 0 8 0.0
total 265 308 86.0


line stmt bran cond sub pod time code
1             # -*- coding: utf-8 -*-
2              
3             package Apache::Log::Parser;
4              
5 2     2   2074 use strict;
  2         5  
  2         75  
6 2     2   48 use 5.008001;
  2         7  
  2         88  
7             our $VERSION = '0.02';
8              
9 2     2   18 use Carp;
  2         2  
  2         147  
10 2     2   10 use List::Util qw( reduce );
  2         3  
  2         4566  
11              
12             our @FAST_COMMON_FIELDS = qw( rhost logname user datetime date time timezone request method path proto status bytes );
13             our @FAST_COMBINED_FIELDS = qw( referer agent );
14             our @FAST_DEBUG_FIELDS = qw( referer agent duration );
15              
16             my $COMMON = [" ", [qw(rhost logname user datetime request status bytes)], undef];
17             my $COMBINED = [" ", [qw(rhost logname user datetime request status bytes referer agent)], sub{my $x=shift; defined($x->{agent}) and defined($x->{referer})}];
18             my $DEBUG = [" ", [qw(rhost logname user datetime request status bytes referer agent duration)], sub{my $x=shift; defined($x->{agent}) and defined($x->{referer}) and defined($x->{duration}) and $x->{duration} =~ m!^\d+!o}];
19             my $VHOST_COMMON = [" ", [qw( vhost rhost logname user datetime request status bytes )], undef];
20              
21             my $STRICT_DEFAULT_FORMATS = [$DEBUG, $COMBINED, $COMMON, $VHOST_COMMON];
22              
23             our @REQUIRED_FIELDS = qw( rhost logname user datetime request status bytes );
24             our @FIELDS_ALWAYS_RETURNED = qw( host logname user datetime date time timezone request method path proto status bytes );
25              
26             sub new {
27 10     10 0 17477 my $class = shift;
28 10         33 my $self = bless {}, $class;
29              
30 10         26 my %args = @_;
31 10 50 50     65 croak "only one option from 'strict' or 'fast' required." unless ($args{strict} xor $args{fast});
32              
33 10 50       43 $self->{verbose} = exists($args{verbose}) ? $args{verbose} : 0;
34              
35 10 100       21 if ($args{strict}) {
36 5 100       17 my @formats = (ref($args{strict}) ? @{$args{strict}} : ());
  2         7  
37 5 100       11 if (scalar(@formats) > 0) {
38 2         9 for (my $i = 0; $i < scalar(@formats); $i++) {
39 9 100       43 if ($formats[$i] eq 'common') {
    100          
    100          
    100          
40 2         5 $formats[$i] = $COMMON;
41             }
42             elsif ($formats[$i] eq 'combined') {
43 2         6 $formats[$i] = $COMBINED;
44             }
45             elsif ($formats[$i] eq 'debug') {
46 1         4 $formats[$i] = $DEBUG;
47             }
48             elsif ($formats[$i] eq 'vhost_common') {
49 1         3 $formats[$i] = $VHOST_COMMON;
50             }
51             else {
52 3 50 33     23 unless (length($formats[$i]->[0]) > 0 and
  252         387  
53 36     36   42 scalar(reduce{$a += scalar(grep{$_ eq $b}@REQUIRED_FIELDS)} 0, @{$formats[$i]->[1]}) == scalar(@REQUIRED_FIELDS)) {
  3         14  
54 0         0 croak "specified fields not enough";
55             }
56             }
57             }
58 2         5 $self->{try_maps} = \@formats;
59             }
60             else {
61 3         8 $self->{try_maps} = $STRICT_DEFAULT_FORMATS;
62             }
63 5         9 $self->{strict} = 1;
64             }
65             else {
66 5         11 $self->{strict} = 0;
67 5 100 66     21 if (ref($args{fast}) and scalar(@{$args{fast}}) > 0) {
  2         8  
68 2         4 my @args = @{$args{fast}};
  2         5  
69 2         4 my @fields = ();
70 2         6 foreach my $arg (@args) {
71 7 100       23 if ($arg eq 'common') {
    100          
    100          
    50          
72 2         6 push @fields, [0, []];
73             }
74             elsif ($arg eq 'combined') {
75 2         6 push @fields, [scalar(@FAST_COMBINED_FIELDS), \@FAST_COMBINED_FIELDS];
76             }
77             elsif ($arg eq 'debug') {
78 1         3 push @fields, [scalar(@FAST_DEBUG_FIELDS), \@FAST_DEBUG_FIELDS];
79             }
80             elsif (ref($arg)) {
81 2         2 my @matchers = @{$arg};
  2         5  
82 2         6 push @fields, [scalar(@matchers), \@matchers];
83             }
84             else {
85 0         0 croak "unknow definition for fast parse";
86             }
87             }
88 2         4 $self->{field_lists} = \@fields;
89             }
90             else {
91 3         14 $self->{field_lists} = [
92             [scalar(@FAST_COMBINED_FIELDS), \@FAST_COMBINED_FIELDS],
93             [0, []]
94             ];
95             }
96 5         9 my $part = q{\s*(?:"([^"]*)"|([^\s]+))?};
97 5         11 my $common = q{([^\s]*)\s+([^\s]*)\s+([^\s]*)\s+\[(([^: ]+):([^ ]+) ([-+0-9]+))\]\s+"(([^\s]+) ([^\s]+)( ([^\s"]*))?)"\s+([^\s]*)\s+([^\s]*)};
98 5         7 my $common_parts = 14;
99 5 100   13   19 my $max_match_parts = reduce {$a > $b ? $a : $b} 0, map {$_->[0]} @{$self->{field_lists}};
  13         27  
  13         38  
  5         11  
100 5         25 my $regex = $common . ($part x $max_match_parts);
101 5         436 $self->{fastpattern} = qr/\A$regex/;
102             }
103 10         36 return $self;
104             }
105              
106             # our @FAST_COMMON_FIELDS = qw( rhost logname user datetime date time timezone request method path proto status bytes );
107             sub parse_fast {
108 38     38 0 69 my ($self, $line) = @_;
109 38         59 chomp $line;
110 38         53 my $pairs = {};
111 38         50 my $dummy;
112             my @values;
113 38         1051 ($pairs->{rhost}, $pairs->{logname}, $pairs->{user}, $pairs->{datetime},
114             $pairs->{date}, $pairs->{time}, $pairs->{timezone}, $pairs->{request},
115             $pairs->{method}, $pairs->{path}, $dummy, $pairs->{proto}, $pairs->{status},
116             $pairs->{bytes}, @values) = ($line =~ $self->{fastpattern});
117              
118 38 50 66     552 unless (defined($pairs->{status}) and $pairs->{status} ne '' and
      66        
      33        
      33        
      33        
119             defined($pairs->{request}) and $pairs->{request} ne '' and
120             defined($pairs->{datetime}) and $pairs->{datetime} ne '') {
121 5 50       17 carp "unknown format: $line" if $self->{verbose};
122 5         36 return undef;
123             }
124              
125 33         42 foreach my $ref (@{$self->{field_lists}}) {
  33         72  
126 63         68 my %result = (%{$pairs}); # copy not to break $pairs while trying to parse
  63         450  
127 63         211 for (my $i = $ref->[0] - 1; $i >= 0; $i--) {
128 90         110 my $x = $i * 2;
129 90 100       174 my $v = defined($values[$x]) ? $values[$x] : $values[$x + 1];
130 90 100       153 last if not defined $v;
131 60         201 $result{$ref->[1]->[$i]} = $v;
132             }
133 63 100       485 return \%result if scalar(keys %result) >= $ref->[0] + 13;
134             }
135 0 0       0 carp "unknown format: $line" if $self->{verbose};
136 0         0 return undef;
137             }
138              
139             sub dequote {
140 262     262 0 686 my $str = shift;
141 262 100       538 return $str unless substr($str, 0, 1) eq '"';
142 259         359 my $body = substr($str, 1, length($str) - 2);
143 259         344 $body =~ s/\\"/"/g;
144 259         459 $body;
145             }
146              
147             sub has_unquoted_tail_doublequote {
148 640     640 0 1024 my $part = shift;
149 640 100       1656 return 0 if substr($part, -1, 1) ne '"';
150 263 100       739 return 1 if substr($part, -2, 1) ne '\\';
151              
152 5         16 my $reversed_body = substr(reverse($part), 1);
153 5         8 my $backslash_count = 0;
154 5 100 66     32 foreach my $c (split(//, $reversed_body)) { $c eq '\\' and $backslash_count += 1 or last; }
  20         104  
155 5         42 not ($backslash_count % 2);
156             }
157              
158             sub separate_log_items {
159 134     134 0 145863 my ($sep, $buf) = @_;
160 134         149 my $continuing = 0;
161 134         166 my $flag_in_quoted = 1;
162 134         123 my $flag_in_bracket = 2;
163 134         158 my @items = ();
164 134         140 my @item_parts_in_quote = ();
165 134         136 my @item_parts_in_brackets = ();
166              
167 134         985 foreach my $part (split($sep, $buf)) {
168 1526 100       2240 unless ($continuing) {
169 1039 100       2095 if (substr($part, 0, 1) eq '"') {
    100          
170 257 100       371 if (has_unquoted_tail_doublequote($part)) {
171 108         149 push @items, dequote($part);
172             }
173             else {
174 149         155 $continuing = $flag_in_quoted;
175 149         213 push @item_parts_in_quote, $part;
176             }
177             } elsif (substr($part, 0, 1) eq '[') {
178 123 100       187 if (substr($part, -1, 1) eq ']') {
179 10         23 push @items, substr($part, 1, length($part) - 2);
180             }
181             else {
182 113         112 $continuing = $flag_in_bracket;
183 113         123 push @item_parts_in_brackets, $part;
184             }
185             } else {
186 659         801 push @items, $part;
187             }
188             next
189 1039         1246 }
190              
191 487 100       672 if ($continuing == $flag_in_quoted) {
    50          
192 374         420 push @item_parts_in_quote, $part;
193 374 100       527 next unless has_unquoted_tail_doublequote($part);
194              
195 149         383 push @items, dequote(join(' ', @item_parts_in_quote));
196 149         239 @item_parts_in_quote = ();
197 149         215 $continuing = 0;
198             }
199             elsif ($continuing == $flag_in_bracket) {
200 113         123 push @item_parts_in_brackets, $part;
201 113 50       254 next if substr($part, -1 , 1) ne ']';
202              
203 113         215 my $s = join(' ', @item_parts_in_brackets);
204 113         217 push @items, substr($s, 1, length($s) - 2);
205 113         150 @item_parts_in_brackets = ();
206 113         178 $continuing = 0;
207             } else {
208 0         0 croak "invalid status about continuing: '$continuing'.";
209             }
210             }
211 134         796 return @items;
212             }
213              
214             sub set_strict_mode {
215 0     0 0 0 my $self = shift;
216 0   0     0 $self->{strict} = shift || 0;
217             }
218              
219             sub parse_strict {
220 42     42 0 54 my ($self, $line) = @_;
221 42         53 chomp $line;
222 42         44 foreach my $rule (@{$self->{try_maps}}) {
  42         91  
223 112         338 my @values = separate_log_items($rule->[0], $line);
224 112 100       262 next if scalar(@values) < 2;
225              
226 101         135 my $pairs = {};
227 101         103 my $fnum = 0;
228 101         114 foreach my $f (@{$rule->[1]}) {
  101         186  
229 960         1357 $pairs->{$f} = $values[$fnum];
230 960         960 $fnum += 1;
231             }
232 101 100   707   552 next unless reduce { $a and defined($pairs->{$b}) } 1, qw( bytes status request datetime user logname rhost );
  707 100       2119  
233              
234 100         241 my $req;
235 100         670 ($pairs->{date}, $pairs->{time}, $pairs->{timezone}) = ($pairs->{datetime} =~ m!^([^: ]+):([^ ]+)\s([-+0-9]+)$!);
236 100 100       410 if ($pairs->{request} =~ m!^(.*) (HTTP/\d\.\d)$!) {
237 84         189 $pairs->{proto} = $2;
238 84         119 $req = $1;
239             }
240             else {
241 16         22 $pairs->{proto} = undef;
242 16         27 $req = $pairs->{request};
243             }
244 100         342 ($pairs->{method}, $pairs->{path}) = split(/\s+/, $req, 2);
245 100 100   300   430 next unless reduce { $a and defined($pairs->{$b}) } 1, qw( path time date );
  300 100       1046  
246              
247 90 100 100     365 next if defined($rule->[2]) and not $rule->[2]->($pairs);
248              
249 41         251 return $pairs;
250             }
251 1 50       4 carp "unknown format: $line" if $self->{verbose};
252 1         5 return undef;
253             }
254              
255             sub parse {
256 80     80 0 36805 my ($self, $line) = @_;
257 80 100       198 if ($self->{strict}) {
258 42         92 return $self->parse_strict($line);
259             }
260 38         101 $self->parse_fast($line);
261             }
262              
263             1;
264              
265             __END__