File Coverage

blib/lib/HTTPD/Log/Filter.pm
Criterion Covered Total %
statement 131 137 95.6
branch 40 52 76.9
condition 4 5 80.0
subroutine 15 18 83.3
pod 4 10 40.0
total 194 222 87.3


line stmt bran cond sub pod time code
1             package HTTPD::Log::Filter;
2              
3             #------------------------------------------------------------------------------
4             #
5             # Standard pragmas
6             #
7             #------------------------------------------------------------------------------
8              
9 10     10   15622 use strict;
  10         21  
  10         406  
10 10     10   55 use warnings;
  10         19  
  10         485  
11              
12             #------------------------------------------------------------------------------
13             #
14             # ModuleS
15             #
16             #------------------------------------------------------------------------------
17              
18 10     10   10117 use IO::File;
  10         154012  
  10         1567  
19 10     10   12018 use IO::Zlib;
  10         885378  
  10         79  
20              
21             my $fields_order = {
22             CLF => [ qw(
23             host
24             ident
25             authexclude
26             date
27             request
28             status
29             bytes
30             ) ],
31             ELF => [ qw(
32             host
33             ident
34             authexclude
35             date
36             request
37             status
38             bytes
39             referer
40             agent
41             ) ],
42             SQUID => [ qw(
43             time
44             elapsed
45             remotehost
46             code_status
47             bytes
48             method
49             url
50             rfc931
51             peerstatus_peerhost
52             type
53             ) ],
54             UNSPECIFIED => [ qw(
55             host
56             ident
57             authexclude
58             date
59             request
60             status
61             bytes
62             referer
63             agent
64             junk
65             ) ],
66             XLF => [ qw(
67             host
68             ident
69             authexclude
70             date
71             request
72             status
73             bytes
74             referer
75             agent
76             junk
77             ) ],
78             };
79              
80             my @format_options = grep !/^UNSPECIFIED$/, keys %{$fields_order};
81             my $format_options_re = '(' . join( '|', @format_options ) . ')';
82              
83             my %in_braces = map { $_ => 1 } qw(
84             date
85             );
86              
87             my %in_quotes = map { $_ => 1 } qw(
88             request
89             referer
90             agent
91             );
92              
93             my $squid_status = '(?:' . join( '|', qw(
94             TCP_HIT
95             TCP_MISS
96             TCP_REFRESH_HIT
97             TCP_REF_FAIL_HIT
98             TCP_REFRESH_MISS
99             TCP_CLIENT_REFRESH_MISS
100             TCP_IMS_HIT
101             TCP_SWAPFAIL_MISS
102             TCP_NEGATIVE_HIT
103             TCP_MEM_HIT
104             TCP_DENIED
105             TCP_OFFLINE_HIT
106             UDP_HIT
107             UDP_MISS
108             UDP_DENIED
109             UDP_INVALID
110             UDP_MISS_NOFETCH
111             NONE
112             ERR_.*?
113             TCP_CLIENT_REFRESH
114             TCP_SWAPFAIL
115             TCP_IMS_MISS
116             UDP_HIT_OBJ
117             UDP_RELOADING
118             ) ) . ')';
119              
120             my @http_methods = qw(
121             GET
122             HEAD
123             POST
124             PUT
125             DELETE
126             TRACE
127             OPTIONS
128             CONNECT
129             );
130              
131             my @rfc2518_methods = qw(
132             PROPFIND
133             PROPATCH
134             MKCOL
135             MOVE
136             COPY
137             LOCK
138             UNLOCK
139             );
140              
141             my $methods_re = '(?:' . join( '|', @http_methods, @rfc2518_methods ) . ')';
142              
143             my @squid_methods = (
144             'ICP_QUERY',
145             'PURGE',
146             @http_methods,
147             @rfc2518_methods
148             );
149              
150             my @heirarchy_codes = qw(
151             NONE
152             DIRECT
153             SIBLING_HIT
154             PARENT_HIT
155             DEFAULT_PARENT
156             SINGLE_PARENT
157             FIRST_UP_PARENT
158             NO_PARENT_DIRECT
159             FIRST_PARENT_MISS
160             CLOSEST_PARENT_MISS
161             CLOSEST_PARENT
162             CLOSEST_DIRECT
163             NO_DIRECT_FAIL
164             SOURCE_FASTEST
165             ROUNDROBIN_PARENT
166             CACHE_DIGEST_HIT
167             CD_PARENT_HIT
168             CD_SIBLING_HIT
169             NO_CACHE_DIGEST_DIRECT
170             CARP
171             ANY_PARENT
172             INVALID CODE
173             );
174              
175             my $hierarchy_code_re = '(?:' . join( '|', @heirarchy_codes ) . ')';
176             my $squid_methods_re = '(?:' . join( '|', @squid_methods ) . ')';
177             my $url_re = '.*?';
178             my $host_re = '.*?';
179             my $mime_type_re = '(?:-|.*?/.*?)';
180             my $status_re = '\d{3}';
181              
182             my %generic_fields_re = (
183             host => $host_re,
184             ident => '\S+',
185             authexclude => '\S+',
186             date => '\d{2}/\w{3}/\d{4}:\d{2}:\d{2}:\d{2}\s[+-]\d{4}',
187             request => "$methods_re $url_re",
188             status => $status_re,
189             bytes => '(?:-|\d+)',
190             referer => '.*?',
191             agent => '.*?',
192             junk => '.*',
193             'time' => '\d+\.\d+',
194             elapsed => '\d+',
195             remotehost => '\S+',
196             code_status => "$squid_status/$status_re",
197             method => $squid_methods_re,
198             url => $url_re,
199             rfc931 => '.*?',
200             peerstatus_peerhost => "$hierarchy_code_re/$host_re",
201             type => $mime_type_re,
202             );
203              
204             my @options = qw(
205             exclusions_file
206             invert
207             );
208              
209 10     10   5158 use vars qw( $VERSION );
  10         23  
  10         15944  
210              
211             $VERSION = '1.08';
212              
213             #------------------------------------------------------------------------------
214             #
215             # Constructor
216             #
217             #------------------------------------------------------------------------------
218              
219             sub new
220             {
221 15     15 0 22242 my $class = shift;
222 15         110 my %args = @_;
223 15         64 my $self = bless {}, $class;
224 15         187 $self->{exclusions_file} = delete $args{exclusions_file};
225 15 100       71 if ( $self->{exclusions_file} )
226             {
227 4         54 $self->{efh} = new IO::File ">$self->{exclusions_file}";
228 4 50       946 die "can't write to $self->{exclusions_file}: $!\n" unless $self->{efh};
229             }
230 15         56 $self->{invert} = delete $args{invert};
231 15 50 66     300 die "format option should be $format_options_re\n"
232             if $args{format} and $args{format} !~ /^$format_options_re$/
233             ;
234 15   100     134 $self->{required_format} = delete $args{format} || 'UNSPECIFIED';
235 15         46 $self->{format} = $self->{required_format};
236 15         49 $self->{capture} = delete( $args{capture} );
237 15         38 $self->{regexes} = \%args;
238 15         78 $self->create_regexes( $self->{format} );
239 15         67 return $self;
240             }
241              
242             sub capture
243             {
244 1     1 1 8 my $self = shift;
245 1         2 my $capture = shift;
246              
247 1 50       212 if ( $capture )
248             {
249 1         3 $self->{capture} = $capture;
250 1         9 $self->create_regexes( $self->{format} );
251             }
252 1         11 return $self->{capture};
253             }
254              
255             sub format
256             {
257 1     1 1 7 my $self = shift;
258 1         3 my $format = shift;
259              
260 1 50       3 if ( $format )
261             {
262 1         3 $self->{format} = $format;
263 1         4 $self->create_regexes( $self->{format} );
264             }
265 1         3 return $self->{format};
266             }
267              
268             sub get_re_field
269             {
270 6935     6935 0 7804 my $field = shift;
271 6935         7039 my $re = shift;
272 6935         11592 my %capture = @_;
273              
274 6935 100       12328 $re = "($re)" if $capture{$field};
275 6935 100       12716 $re = "\"$re\"" if $in_quotes{$field};
276 6935 100       11877 $re = "\\[$re\\]" if $in_braces{$field};
277 6935         28411 return $re;
278             }
279              
280             sub create_regexes
281             {
282 280     280 0 346 my $self = shift;
283 280         322 my $format = shift;
284              
285 280         515 my @fields_order = @{$fields_order->{$format}};
  280         1190  
286 280         568 my %fields_order = map { $_ => 1 } @fields_order;
  2572         4951  
287 280         576 my %valid_fields = map { $_ . '_re' => 1 } @fields_order;
  2572         5170  
288 280         604 for ( keys %{$self->{regexes}} )
  280         940  
289             {
290 132         394 die
291             "$_ is not a valid option; please use one of:\n",
292 50 100       452 map { "\t$_\n" } keys( %valid_fields ), @options,
293             unless $valid_fields{$_}
294             }
295              
296 269         478 my %capture;
297              
298 269 100       768 if ( ref( $self->{capture} ) eq 'ARRAY' )
299             {
300 124         195 for ( @{$self->{capture}} )
  124         260  
301             {
302 220         546 die
303             "$_ is not a valid $format field name;",
304             "should be one of\n",
305 124 100       436 map { "\t$_\n" } @fields_order
306             unless $fields_order{$_};
307             }
308 102         142 %capture = map { $_ => 1 } @{$self->{capture}};
  102         274  
  102         189  
309 888         1481 $self->{capture_fields} =
310 102         159 [ grep { $capture{$_} } @fields_order ]
311             ;
312             }
313 2242         3013 my @generic_fields_re = map
314             {
315 247         391 my $re = $generic_fields_re{$_};
316 2242         4012 $re = get_re_field( $_, $re, %capture );
317 2242         5306 $re;
318             }
319             @fields_order
320             ;
321 247         951 $self->{generic_fields_re} = join( '\s', @generic_fields_re );
322 39         87 my %exclude_fields_re = (
323             %generic_fields_re,
324             map {
325 247         1625 my $re = $self->{regexes}{$_};
326 39         211 s/_re$//;
327 39         351 $_ => $re
328             }
329             grep /_re$/,
330 247         1072 keys %{$self->{regexes}}
331             );
332 4693         11410 %exclude_fields_re =
333 247         6691 map { $_ => get_re_field( $_, $exclude_fields_re{$_}, %capture ) }
334             keys %exclude_fields_re
335             ;
336 2242         5726 $self->{exclude_fields_re} =
337 247         1529 join( '\s', map( { $exclude_fields_re{$_} } @fields_order ) )
338             ;
339             }
340              
341             sub generic_re
342             {
343 0     0 0 0 my $self = shift;
344 0         0 return $self->{generic_fields_re};
345             }
346              
347             sub re
348             {
349 0     0 1 0 my $self = shift;
350 0         0 return $self->{exclude_fields_re};
351             }
352              
353             sub check_generic_re
354             {
355 364     364 0 412 my $self = shift;
356 364         439 my $line = shift;
357 364         25278 return $line =~ m{^$self->{generic_fields_re}$};
358             }
359              
360             sub detect_format
361             {
362 93     93 0 104 my $self = shift;
363 93         209 my %args = @_;
364              
365 93 100       198 if ( $args{filename} )
366             {
367 2         3 my $fh;
368 2 100       9 if ( $args{filename} =~ /\.gz$/ )
369             {
370 1 50       9 $fh = IO::Zlib->new( $args{filename}, "rb" )
371             or die "Can't open $args{filename}\n"
372             ;
373             }
374             else
375             {
376 1 50       9 $fh = IO::File->new( $args{filename} )
377             or die "Can't open $args{filename}\n"
378             ;
379             }
380 2         1799 $args{line} = <$fh>;
381             }
382 93 50       453 die "detect_format expects either a filename or a line from a logfile"
383             unless $args{line}
384             ;
385 93         198 for ( @format_options )
386             {
387 263         343 eval { $self->create_regexes( $_ ) };
  263         571  
388 263 100       1624 next if $@;
389 230 100       646 next unless $self->check_generic_re( $args{line} );
390 92         227 $self->{format} = $_;
391 92         243 return $self->{format};
392             }
393 1         14 die "Can't autodetect format\n";
394             }
395              
396             sub filter
397             {
398 135     135 1 3915 my $self = shift;
399 135         210 my $line = shift;
400              
401 135         185 my @captured;
402 135 100       578 $self->detect_format( line => $line )
403             if $self->{required_format} eq 'UNSPECIFIED'
404             ;
405 134         288 @captured = $self->check_generic_re( $line );
406 134 50       331 return undef unless @captured;
407 134 100       311 if ( $self->{capture} )
408             {
409 33         35 my @cfields = @{$self->{capture_fields}};
  33         79  
410 33         44 my %captured;
411 33         86 @captured{@cfields} = @captured;
412 33         100 $self->{captured} = \%captured;
413             }
414 134 50       300 if ( $self->{invert} )
415             {
416 0 0       0 return $line if $line !~ m{^$self->{exclude_fields_re}$};
417             }
418             else
419             {
420 134 100       1921 return $line if $line =~ m{^$self->{exclude_fields_re}$};
421             }
422 21 100       62 if ( $self->{efh} )
423             {
424 16         79 $self->{efh}->print( $line );
425             }
426 21         261 return '';
427             }
428              
429 0     0   0 sub DESTROY {}
430              
431             sub AUTOLOAD
432             {
433 28     28   252 my $self = shift;
434 10     10   74 use vars qw( $AUTOLOAD );
  10         21  
  10         1995  
435 28         47 my $field = $AUTOLOAD;
436 28         128 $field =~ s/.*:://;
437 28 50       89 die "$field method not defined\n" unless exists $self->{captured}{$field};
438 28         7591 return $self->{captured}{$field};
439             }
440              
441             #------------------------------------------------------------------------------
442             #
443             # Start of POD
444             #
445             #------------------------------------------------------------------------------
446              
447             =head1 NAME
448              
449             HTTPD::Log::Filter - a module to filter entries out of an httpd log.
450              
451             =head1 SYNOPSIS
452              
453             my $hlf = HTTPD::Log::Filter->new(
454             exclusions_file => $exclusions_file,
455             agent_re => '.*Mozilla.*',
456             format => 'ELF',
457             );
458              
459             while( <> )
460             {
461             my $ret = $hlf->filter( $_ );
462             die "Error at line $.: invalid log format\n" unless defined $ret;
463             print $_ if $ret;
464             }
465              
466             print grep { $hlf->filter( $_ ) } <>;
467              
468             $hlf = HTTPD::Log::Filter->new(
469             capture => [ qw(
470             host
471             ident
472             authexclude
473             date
474             request
475             status
476             bytes
477             ) ];
478             );
479              
480             while( <> )
481             {
482             next unless $hlf->filter( $_ );
483             print $hlf->host, "\n";
484             }
485              
486             print grep { $hlf->filter( $_ ) } <>;
487              
488             =head1 DESCRIPTION
489              
490             This module provide a simple interface to filter entries out of an httpd
491             logfile. The constructor can be passed regular expressions to match against
492             particular fields on the logfile. It does its filtering line by line, using a
493             filter method that takes a line of a logfile as input, and returns true if it
494             matches, and false if it doesn't.
495              
496             There are two possible non-matching (false) conditions; one is where the line
497             is a valid httpd logfile entry, but just doesn't happen to match the filter
498             (where "" is returned). The other is where it is an invalid entry according to
499             the format specified in the constructor.
500              
501             =head1 CONSTRUCTOR
502              
503             The constructor is passed a number of options as a hash. These are:
504              
505             =over 4
506              
507             =item exclusions_file
508              
509             This option can be used to specify a filename for entries that don't match the
510             filter to be written to.
511              
512             =item invert
513              
514             This option, is set to true, will invert the logic of the fliter; i.e. will
515             return only non-matching lines.
516              
517             =item format
518              
519             This should be one of:
520              
521             =over 4
522              
523             =item CLF
524              
525             Common Log Format (CLF):
526              
527             "%h %l %u %t \"%r\" %>s %b"
528              
529             =item ELF
530              
531             NCSA Extended/combined Log format:
532              
533             "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\""
534              
535             =item XLF
536              
537             Some bespoke format based on extended log format + some junk at the end:
538              
539             "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" %j
540              
541             where %j is .* in regex-speak.
542              
543             See L for more
544             information on log file formats.
545              
546             =item SQUID
547              
548             Logging format for Squid (v1.1+) caching / proxy servers. This is of the form:
549              
550             "%9d.%03d %6d %s %s/%03d %d %s %s %s %s%s/%s %s"
551              
552             where the fields are:
553              
554             time
555             elapsed
556             remotehost
557             code_status
558             bytes
559             method
560             url
561             rfc931
562             peerstatus_peerhost
563             type
564              
565              
566             (see L for more info).
567              
568             =back
569              
570             =item (host|ident|authexclude|date|request|status|bytes|referer|agent)_re
571              
572             This class of options specifies the regular expression or expressions which are
573             used to filter the logfile for httpd logs.
574              
575             =item (time|elapsed|remotehost|code_status|method|url|rfc931|peerstatus_peerhost|type)_re
576              
577             Ditto for Squid logs.
578              
579             =item capture [ , , ... ]
580              
581             This option requests the filter to capture the contents of given named fields
582             so that they can be examined if the filtering is successful. This is done by
583             simply putting capturing parentheses around the appropriate segment of the
584             filtering regex. Fields to be captured are passed as an array reference.
585             WARNING; do not try to insert your own capturing parentheses in the custom
586             field regexes, as this will have unpredictable results when combined with the
587             capture option.
588              
589             Captured fields can be accessed after each call to filter using a method call
590             with the same name as the captured field; e.g.
591              
592             my $filter = HTTPD::Logs::Filter->new(
593             capture => [ 'host', 'request' ]
594             );
595             while ( <> )
596             {
597             next unless $filter->filter( $_ );
598             print $filter->host, " requested ", $filter->request, "\n";
599             }
600              
601             =back
602              
603             =head1 METHODS
604              
605             =head2 filter
606              
607             Filters a line of a httpd logfile. returns true (the line) if it
608             matches, and false ("" or undef) if it doesn't.
609              
610             There are two possible non-matching (false) conditions; one is where the line
611             is a valid httpd logfile entry, but just doesn't happen to match the filter
612             (where "" is returned). The other is where it is an invalid entry according to
613             the format specified in the constructor.
614              
615             =head2 re
616              
617             Returns the current filter regular expression.
618              
619             =head2 format
620              
621             Returns the current format.
622              
623             =head2 (host|ident|authexclude|date|request|status|bytes|referer|agent|junk)
624              
625             If the capture option has been specified, these methods return the captured
626             string for each field as a result of the previous call to filter.
627              
628             =head1 AUTHOR
629              
630             Ave Wrigley
631              
632             =head1 COPYRIGHT
633              
634             Copyright (c) 2001 Ave Wrigley. All rights reserved. This program is free
635             software; you can redistribute it and/or modify it under the same terms as Perl
636             itself.
637              
638             =cut
639              
640             #------------------------------------------------------------------------------
641             #
642             # End of POD
643             #
644             #------------------------------------------------------------------------------
645              
646             #------------------------------------------------------------------------------
647             #
648             # True ...
649             #
650             #------------------------------------------------------------------------------
651              
652             1;