File Coverage

blib/lib/ApacheLog/Parser/Report.pm
Criterion Covered Total %
statement 12 195 6.1
branch 0 62 0.0
condition 0 28 0.0
subroutine 4 17 23.5
pod 7 7 100.0
total 23 309 7.4


line stmt bran cond sub pod time code
1             package ApacheLog::Parser::Report;
2             $VERSION = v0.0.1;
3              
4 1     1   1744 use warnings;
  1         2  
  1         31  
5 1     1   6 use strict;
  1         1  
  1         32  
6 1     1   5 use Carp;
  1         2  
  1         83  
7 1     1   6 use YAML;
  1         2  
  1         3566  
8              
9             =head1 NAME
10              
11             ApacheLog::Parser::Report - configurable report extraction
12              
13             =head1 SYNOPSIS
14              
15             my $rep = ApacheLog::Parser::Report->new(conf => \%config);
16             $rep->load_config($config_filename); # maybe
17             my $func = $rep->get_func;
18             while(...) {
19             $func->($array_ref);
20             }
21             $rep->write_report($filename);
22              
23             =cut
24              
25             =head2 new
26              
27             my $rep = ApacheLog::Parser::Report->new(conf => \%config);
28              
29             =cut
30              
31             sub new {
32 0     0 1   my $package = shift;
33 0   0       my $class = ref($package) || $package;
34 0           my $self = {@_};
35 0           bless($self, $class);
36 0 0         if(ref($package)) {
37 0           @$self{qw(conf config_hash)} = @$package{qw(conf config_hash)};
38             }
39             else {
40 0           $self->_setup_config;
41             }
42 0           return($self);
43             } # end subroutine new definition
44             ########################################################################
45              
46             =head2 _setup_config
47              
48             $self->_setup_config;
49              
50             =cut
51              
52             my $namify = sub {my $t = $_[0]; $t =~ s/ /_/g; lc($t);};
53             sub _setup_config {
54 0     0     my $self = shift;
55              
56 0 0         my $config = $self->{conf} or die "no config";
57 0           my @conf = @$config;
58 0           my $c = $self->{config_hash} = {};
59 0           foreach my $item (@conf) {
60 0 0 0       my $name = $item->{name} ||= $namify->($item->{title}) or
61             die "no name/title in ", join(", ", %$item);
62 0           $item->{opts} = {
63 0   0       map({$_ => 1} split(/ /, ($item->{options}||'')))
64             };
65 0 0         $c->{$name} and croak("duplicate name '$name'");
66 0           $c->{$name} = $item;
67             }
68             } # end subroutine _setup_config definition
69             ########################################################################
70              
71             =head2 load_config
72              
73             $rep->load_config($config_filename); # maybe
74              
75             =cut
76              
77             sub load_config {
78 0     0 1   my $self = shift;
79 0           die "nope";
80             } # end subroutine load_config definition
81             ########################################################################
82              
83             =head2 get_func
84              
85             my $func = $rep->get_func;
86              
87             =cut
88              
89             sub get_func {
90 0     0 1   my $self = shift;
91              
92 0           my @conf = @{$self->{conf}};
  0            
93              
94 0           my $s = $self->{store} = {};
95 0           my @preface;
96             my @codes;
97 0           foreach my $item (@conf) {
98 0           my $name = $item->{name};
99 0           $s->{$name} = {};
100 0 0         unless($item->{where}) {
101             #warn "$name is a stub\n";
102 0           next;
103             }
104             #warn "gen code for $name ($item->{title})\n";
105 0           my ($code, $pre) = $self->_code_for($item);
106 0           push(@codes, $code);
107 0 0         push(@preface, $pre) if($pre);
108 0           if(0) {
109             warn "#"x72, "\n";
110             warn "for $name\n$code", ($pre ? "\n\n$pre\n" : '');
111             }
112             }
113 0 0         $ENV{DBG} and warn join("\n", @preface), join("\n", @codes);
114 0           $self->_compile(join("\n", @preface), join("\n", @codes));
115             } # end subroutine get_func definition
116             ########################################################################
117             sub _compile {
118 0     0     my $s = $_[0]->{store};
119 0           my $func = eval("$_[1];
120             use ApacheLog::Parser qw(:fields);
121             sub {
122             my \$v = shift;
123             my \$p;
124             my \@ans;
125             $_[2]
126             no ApacheLog::Parser;
127             }
128             ");
129 0 0         $@ and croak("cannot compile $_[1]/\n$_[2]\n -- $@");
130 0           return($func);
131             }
132             sub _code_for {
133 0     0     my $self = shift;
134 0           my ($item) = @_;
135              
136 0           my $name = $item->{name};
137 0 0         $ENV{DBG} and warn "building rules for $name\n";
138             # need to work-out the pre-reqs
139 0           my $preface;
140             my $callcode;
141 0 0         if(my $code = $item->{code}) {
142 0           $callcode = '$_' . $name . '_code';
143 0           $preface = join("\n",
144             'my ' . $callcode . ' = sub {',
145             $code,
146             '};'
147             );
148             }
149             # then the total number of captures?
150             # bind everything to ^$ ?
151             # switch some to eq?
152             my $has_matches = sub {
153 0     0     my ($string) = @_;
154 0 0         defined($string) or die "no string";
155 0 0         return($string =~ m/(?
156 0           };
157 0           my $before;
158             my @code;
159 0           my @conds;
160 0           my $some_matches = 0;
161 0           foreach my $cond (@{$item->{where}}) {
  0            
162 0           my @subs;
163 0           foreach my $thing (sort(keys(%$cond))) {
164 0           my $re = $cond->{$thing};
165 0 0         if($thing eq 'params') {
166 0           $before =
167             '$p ||= {map({my @g = split(/=/, $_, 2); ($#g?@g:())}' .
168             ' split(/&/, $v->[params]))};';
169 0           foreach my $p (split(/ & /, $re)) {
170 0           my ($name, $want) = split(/=/, $p, 2);
171 0           push(@subs, ["(\$p->{$name}||'')", $want]);
172             }
173             }
174             else {
175             # the \$v->[$thing] =~ m#$re# bit
176 0           push(@subs, ["\$v->[$thing]", $re]);
177             }
178             }
179             # and-together all of the subconditions
180 0           my $had_match = 0;
181 0           my @pref = ('(@ans = ', 'push(@ans, ');
182 0           my @built;
183 0           foreach my $subc (@subs) {
184 0           my $start;
185 0 0         if($has_matches->($subc->[1])) {
186 0           $start = $pref[$had_match];
187 0           $some_matches = 1;
188 0           $had_match = 1;
189             }
190             else {
191 0           $start = '(';
192             }
193 0           push(@built, $start . $subc->[0] . ' =~ m#^' . $subc->[1] . '$#)');
194             }
195             # single subcondition
196 0 0         push(@conds, $#built ? join(' and ',map({"($_)"} @built)) : @built);
  0            
197             }
198             #warn "$name ", $some_matches ? 'yes' : 'no', "\n\n";
199             # or-together all of the where's
200 0           my $code = ($before ? "$before\n" : '') .
201             'if(' . (
202             $#conds ?
203 0 0         "\n " . join(" or\n", map({" ($_)"} @conds)) . "\n" :
    0          
    0          
    0          
    0          
204             $conds[0]
205             ) .
206             ") {\n " .
207             # must clear-out the answer slot if there were never any match vars
208             ($callcode ?
209             ($some_matches ? '' : '@ans = ();') . $callcode . '->(@ans)' :
210             "(\$s->{$name}{" . ($some_matches ? '$ans[0]' : q('') ) .
211             '}||=0)++'
212             ) . ';return' .
213             "\n}";
214 0           return($code, $preface);
215             }
216              
217             =head2 aggregate
218              
219             $rep->aggregate($data);
220              
221             =cut
222              
223             sub aggregate {
224 0     0 1   my $self = shift;
225 0           my ($data) = @_;
226              
227 0 0         $data or croak('usage: aggregate(\%data)');
228              
229 0   0       my $s = $self->{store} ||= {};
230 0   0       my $t = $self->{totals} ||= {};
231              
232 0           my @conf = @{$self->{conf}};
  0            
233              
234 0           my %data = %$data;
235              
236 0           foreach my $item (@conf) {
237 0           my $name = $item->{name};
238 0   0       $s->{$name} ||= {};
239 0 0         my $got = $data{$name} or next;
240 0           foreach my $k (keys(%$got)) {
241 0   0       ($s->{$name}{$k}||=0) += $got->{$k};
242             # and the totals
243 0 0         unless($item->{opts}{no_total}) {
244 0   0       ($t->{$name}||=0)+= $got->{$k};
245             }
246 0 0         if(my $dest = $item->{sum_into}) {
247 0           $dest = $namify->($dest);
248 0   0       ($t->{$dest}||=0)+= $got->{$k};
249             }
250             }
251             }
252 0           return($t, $s);
253             } # end subroutine aggregate definition
254             ########################################################################
255              
256             # XXX actually YAML::Syck doesn't always play-nice, so ...
257             my $dumper = eval{require YAML::Syck} ?
258             sub {YAML::Syck::Dump($_[0])} :
259             sub {YAML::Dump($_[0])};
260              
261             =head2 print_report
262              
263             my $string = $rep->print_report;
264              
265             =cut
266              
267             sub print_report {
268 0     0 1   my $self = shift;
269 0           my $string = "";
270              
271 0           my $s = $self->{store};
272 0           my $t = $self->{totals};
273 0           my $c = $self->{config_hash};
274              
275 0 0         open(my $fh, '>', \$string) or die "gah";
276              
277             my $get_width = sub {
278 0     0     length((sort({length($b) <=> length($a)} @_))[0]);
  0            
279 0           };
280 0           my $max_l = $get_width->(map({$c->{$_}{title}} keys(%$t)));
  0            
281 0           $max_l++;
282              
283 0           print $fh join("\n ", 'Totals:',
284 0           map({sprintf("%-${max_l}s %10d",
285             $c->{$_}{title} . ':', $t->{$_})} sort(keys(%$t)))
286             ), "\n\n";
287              
288 0           my $gh = $self->_greatest_hits;
289 0           print $fh "Greatest Hits\n";
290 0           foreach my $k (sort(keys(%$gh))) {
291 0           my $d = $gh->{$k};
292 0           print $fh " $c->{$k}{title}:\n";
293 0           my @rows = sort({$d->{$b} <=> $d->{$a}} keys(%$d));
  0            
294 0           my $max_w = $get_width->(@rows);
295 0           $max_w++;
296 0           print $fh join("\n",
297 0           map({sprintf(" %-${max_w}s %10d", $_ . ':', $d->{$_})} @rows)
298             ), "\n";
299             }
300              
301 0           close($fh);
302 0           my $yaml = $dumper->({
303             totals => $self->{totals},
304             greatest_hits => $gh
305             });
306              
307 0           return($string, $yaml);
308             } # end subroutine print_report definition
309             ########################################################################
310              
311             =head2 table_report
312              
313             $rep->table_report(@files);
314              
315             =cut
316              
317             sub table_report {
318 0     0 1   my $self = shift;
319 0           my (@files) = @_;
320              
321 0 0         my $ref = ref($files[0]) ? shift(@files) : undef;
322              
323 0           my $c = $self->{config_hash};
324              
325 0           my $collect;
326 0           foreach my $file (@files) {
327 0           my $t;
328 0 0         if($ref) {
329 0           my $agg = $self->new;
330 0           foreach my $f (@{$ref->{$file}}) {
  0            
331 0           my $data = YAML::Syck::LoadFile($f);
332 0   0       $data ||= {}; # XXX is silence golden?
333 0           ($t) = $agg->aggregate($data);
334             }
335             }
336             else {
337 0           my $data = YAML::Syck::LoadFile($file);
338 0           $t = $data->{totals};
339             }
340 0           foreach my $k (keys(%$t)) {
341 0   0       my $dest = $collect->{$k} ||= {};
342 0           $dest->{$file} = $t->{$k};
343             }
344             }
345              
346 0           my @rows = sort(keys(%$collect));
347 0           my @col0 = map({$c->{$_}{title}} @rows);
  0            
348              
349 0           my @table;
350 0           push(@table, []) for(@rows);
351              
352 0           foreach my $file (@files) {
353 0           my $r = 0;
354 0           foreach my $row (@rows) {
355 0   0       push(@{$table[$r++]}, $collect->{$row}{$file} || 0);
  0            
356             }
357             }
358             {
359 0           my $r = 0;
  0            
360 0           unshift(@{$table[$r++]}, shift(@col0)) for(@rows);
  0            
361             }
362 0           return(@table);
363             } # end subroutine table_report definition
364             ########################################################################
365              
366             =head2 _greatest_hits
367              
368             $self->_greatest_hits;
369              
370             =cut
371              
372             sub _greatest_hits {
373 0     0     my $self = shift;
374              
375 0           my $c = $self->{config_hash};
376 0           my $s = $self->{store};
377 0           my %o;
378 0           foreach my $k (keys(%$s)) {
379 0           my $d = $s->{$k};
380 0           my @got = sort({$d->{$b} <=> $d->{$a}} keys(%$d));
  0            
381 0 0         (@got > 1) or next;
382 0   0       my $max = ($c->{$k}{top} || 10) - 1;
383 0 0         $#got = $max if($#got > $max);
384             #warn "@got\n";
385 0           $o{$k} = {map({$_ => $d->{$_}} @got)};
  0            
386             }
387 0           return(\%o);
388             } # end subroutine _greatest_hits definition
389             ########################################################################
390              
391             # TODO sum_into is just deferred until write_report time?
392              
393             =head2 write_report
394              
395             $rep->write_report($filename);
396              
397             =cut
398              
399             sub write_report {
400 0     0 1   my $self = shift;
401 0           my ($filename) = @_;
402              
403 0 0         open(my $fh, '>', $filename) or die "cannot write '$filename' $!";
404 0           print $fh $dumper->($self->{store});
405 0 0         close($fh) or die "cannot close '$filename' $!";
406             } # end subroutine write_report definition
407             ########################################################################
408              
409              
410              
411              
412             =head1 AUTHOR
413              
414             Eric Wilhelm @
415              
416             http://scratchcomputing.com/
417              
418             =head1 BUGS
419              
420             If you found this module on CPAN, please report any bugs or feature
421             requests through the web interface at L. I will be
422             notified, and then you'll automatically be notified of progress on your
423             bug as I make changes.
424              
425             If you pulled this development version from my /svn/, please contact me
426             directly.
427              
428             =head1 COPYRIGHT
429              
430             Copyright (C) 2007 Eric L. Wilhelm, All Rights Reserved.
431              
432             =head1 NO WARRANTY
433              
434             Absolutely, positively NO WARRANTY, neither express or implied, is
435             offered with this software. You use this software at your own risk. In
436             case of loss, no person or entity owes you anything whatsoever. You
437             have been warned.
438              
439             =head1 LICENSE
440              
441             This program is free software; you can redistribute it and/or modify it
442             under the same terms as Perl itself.
443              
444             =cut
445              
446             # vi:ts=2:sw=2:et:sta
447             1;