File Coverage

blib/lib/Log/Saftpresse/CountersOutput/Html.pm
Criterion Covered Total %
statement 21 294 7.1
branch 0 90 0.0
condition 0 27 0.0
subroutine 7 33 21.2
pod 0 26 0.0
total 28 470 5.9


line stmt bran cond sub pod time code
1             package Log::Saftpresse::CountersOutput::Html;
2              
3 1     1   856 use Moose;
  1         2  
  1         6  
4              
5             # ABSTRACT: plugin to output counters in HTML report
6             our $VERSION = '1.5'; # VERSION
7              
8             extends 'Log::Saftpresse::CountersOutput';
9              
10 1     1   4958 use Log::Saftpresse::Utils qw( adj_int_units get_smh);
  1         2  
  1         92  
11              
12 1     1   6 use JSON;
  1         3  
  1         6  
13 1     1   126 use Time::Piece;
  1         1  
  1         11  
14 1     1   961 use Template;
  1         18760  
  1         27  
15              
16 1     1   483 use Template::Stash;
  1         9041  
  1         71  
17              
18             $Template::Stash::LIST_OPS->{ type } = sub { return 'list'; };
19             $Template::Stash::SCALAR_OPS->{ type } = sub { return 'scalar'; };
20             $Template::Stash::HASH_OPS->{ type } = sub { return 'hash'; };
21              
22             sub version {
23 0     0 0   my $version;
24             {
25             ## no critic
26 1     1   8 no strict 'vars'; # is only declared in build
  1         2  
  1         2430  
  0            
27 0 0         $version = defined $VERSION ? $VERSION : '(git checkout)';
28             }
29 0           return( $version );
30             }
31              
32             sub tt {
33 0     0 0   my $self = shift;
34 0 0         if( ! defined $self->{_tt} ) {
35 0           my $tt = Template->new(
36             ABSOLUTE => 1,
37             EVAL_PERL => 1,
38             );
39              
40 0           my $code = $self->template_content;
41             # create a parsed object of our main template
42 0           my $doc = $tt->template( \$code );
43 0           my $blocks = $doc->blocks();
44 0           my $ctx = $tt->context;
45              
46             # copy all defined blocks over to the global context
47 0           foreach my $block ( keys %$blocks ) {
48 0           $ctx->define_block( $block, $blocks->{$block} );
49             }
50              
51 0           $self->{_tt} = $tt;
52             }
53 0           return( $self->{_tt} );
54             }
55              
56             sub json {
57 0     0 0   my $self = shift;
58 0 0         if( ! defined $self->{_json} ) {
59 0           $self->{_json} = JSON->new->pretty->utf8;
60             }
61 0           return( $self->{_json} );
62             }
63              
64             sub template_content {
65 0     0 0   my $self = shift;
66 0           my $c = '';
67 0           my $h;
68              
69 0 0         if( defined $self->{'_template_content'}) {
70 0           return( $self->{'_template_content'} );
71             }
72              
73 0 0         if( defined $self->{'template_file'} ) {
74 0 0         $h = IO::File->new($self->{'template_file'}, 'r')
75             or die('error opening output template: '.$!);
76             } else {
77 0 0         $h = IO::Handle->new_from_fd(*DATA,'r')
78             or die('error reading default template from __DATA__: '.$!);
79             }
80 0           while ( my $line = $h->getline ) {
81 0           $c .= $line;
82             }
83 0           $h->close;
84 0           return( $self->{'_template_content'} = $c );
85             }
86              
87             sub process {
88 0     0 0   my ( $self, $block, %vars ) = @_;
89 0           my $buf;
90 0           $vars{'self'} = $self;
91 0           my $tt = $self->tt;
92              
93             # create a wrapper script and execute it
94 0           my $eval = "[% INCLUDE $block -%]\n";
95 0 0         $tt->process( \$eval, \%vars, \$buf)
96             or die( $tt->error );
97              
98 0           return $buf;
99             }
100              
101             sub title {
102 0     0 0   my $self = shift;
103 0           return( "Postfix log summaries generated on ".Time::Piece->new->ymd );
104             }
105              
106             sub output {
107 0     0 0   my ( $self, $cnt ) = @_;
108              
109 0           print $self->process('header');
110            
111 0           $self->print_totals( $cnt );
112              
113 0 0         if( defined $cnt->{'PostfixSmtpdStats'} ) {
114 0           $self->print_smtpd_stats( $cnt->{'PostfixSmtpdStats'} );
115             }
116            
117             # TODO: fix problem report output
118             #$self->print_problems_reports( $cnt );
119              
120 0           $self->print_traffic_summaries( $cnt );
121              
122 0 0 0       if( defined $self->{'top_domains_cnt'}
123             && $self->{'top_domains_cnt'} != 0 ) {
124 0           $self->print_domain_summaries( $cnt );
125             }
126              
127 0 0         if( defined $cnt->{'PostfixSmtpdStats'} ) {
128 0           $self->print_smtpd_summaries( $cnt );
129             }
130              
131 0           $self->print_user_summaries( $cnt );
132              
133             # TODO: restore Message detail of pflogsumm?
134              
135 0 0         if( defined $cnt->{'TlsStatistics'} ) {
136 0           $self->print_tls_stats( $cnt );
137             }
138 0 0         if( defined $cnt->{'PostfixGeoStats'} ) {
139 0           $self->print_geo_stats( $cnt->{'PostfixGeoStats'} );
140             }
141              
142 0           print $self->process('footer');
143              
144 0           return;
145             }
146              
147             sub print_user_summaries {
148 0     0 0   my ( $self, $cnt ) = @_;
149 0           my $delivered = $cnt->{'PostfixDelivered'};
150              
151 0           my @tables = (
152             [ "Senders by message count" => 'sender',
153             0, 'recieved', 'by_sender' ],
154             [ "Recipients by message count" => 'recipient',
155             0, 'sent', 'by_rcpt' ],
156             [ "Senders by message size" => 'sender',
157             0, 'recieved', 'size', 'by_sender' ],
158             [ "Recipients by message size" => 'recipient',
159             0, 'sent', 'size', 'by_rcpt' ],
160             );
161              
162 0           foreach my $table ( @tables ) {
163 0           my ( $title, $legend, $total, @node ) = @$table;
164 0           my $values = $delivered->get_node(@node);
165 0 0         if( ! defined $values ) { next; }
  0            
166 0 0         print $self->hash_top_values( $values,
167             title => $title,
168             total => $total,
169             legend => $legend,
170             unit => $title =~ /size$/ ? 'byte' : 'count',
171             );
172             }
173              
174 0           return;
175             }
176              
177             sub print_totals {
178 0     0 0   my ( $self, $cnt ) = @_;
179 0           my $reject_cnt = $cnt->{'PostfixRejects'};
180 0           my $recieved_cnt = $cnt->{'PostfixRecieved'};
181 0           my $delivered_cnt = $cnt->{'PostfixDelivered'};
182 0           my $smtpdConnCnt = 0;
183              
184             # PostfixRejects
185 0           my $msgsRjctd = $reject_cnt->get_value_or_zero('total', 'reject');
186 0           my $msgsDscrdd = $reject_cnt->get_value_or_zero('total', 'discard');
187 0           my $msgsWrnd = $reject_cnt->get_value_or_zero('total', 'warning');
188 0           my $msgsHld = $reject_cnt->get_value_or_zero('total', 'hold');
189              
190             # PostfixRecieved
191 0           my $msgsRcvd = $recieved_cnt->get_value_or_zero('total');
192              
193 0           my $msgsDlvrd = $delivered_cnt->get_value_or_zero('sent', 'total');
194 0           my $msgsDfrd = $delivered_cnt->get_value_or_zero('deferred', 'total');
195 0           my $msgsFwdd = $delivered_cnt->get_value_or_zero('forwarded');
196 0           my $msgsBncd = $delivered_cnt->get_value_or_zero('bounced', 'total');
197              
198 0           my $sizeRcvd = $delivered_cnt->get_value_or_zero('recieved', 'size', 'total');
199 0           my $sizeDlvrd = $delivered_cnt->get_value_or_zero('sent', 'size', 'total');
200              
201 0           my $sendgUserCnt = $delivered_cnt->get_key_count('recieved', 'by_sender');
202 0           my $sendgDomCnt = $delivered_cnt->get_key_count('recieved', 'by_domain');
203 0           my $recipUserCnt =$delivered_cnt->get_key_count('sent', 'by_rcpt');
204 0           my $recipDomCnt = $delivered_cnt->get_key_count('sent', 'by_domain');
205              
206 0           my $msgsTotal = $msgsDlvrd + $msgsRjctd + $msgsDscrdd;
207              
208 0           print $self->headline(1, 'Grand Totals');
209              
210 0           print $self->key_value_table( "Messages", [
211             [ 'Received', $msgsRcvd ],
212             [ 'Delivered', $msgsDlvrd ],
213             [ 'Forwarded', $msgsFwdd ],
214             [ 'Deferred', $msgsDfrd ],
215             ] );
216              
217 0           print $self->key_value_table( "Rejects", [
218             [ 'Bounced', $msgsBncd ],
219             [ 'Rejected', $msgsRjctd, 'count', $msgsTotal ],
220             [ 'Rejected Warnings', $msgsWrnd ],
221             [ 'Held', $msgsHld ],
222             [ 'discarded', $msgsDscrdd, 'count', $msgsTotal ],
223             ] );
224              
225 0           print $self->key_value_table( "Traffic Volume", [
226             [ 'Bytes recieved', $sizeRcvd, 'byte' ],
227             [ 'Bytes delivered', $sizeDlvrd, 'byte' ],
228             [ 'Senders', $sendgUserCnt ],
229             [ 'Sending hosts/domains', $sendgDomCnt ],
230             [ 'Recipients', $recipUserCnt ],
231             [ 'Recipients hosts/domains', $recipDomCnt ],
232             ] );
233              
234 0           return;
235             }
236              
237             sub print_smtpd_stats {
238 0     0 0   my ( $self, $cnt ) = @_;
239 0           my $connections = $cnt->get_value_or_zero('total');
240 0           my $hosts_domains = int(keys %{$cnt->get_node('per_domain')});
  0            
241 0 0         my $avg_conn_time = $connections > 0 ?
242             ($cnt->get_value_or_zero('busy', 'total')
243             / $connections ) + .5 : 0;
244 0           my $total_conn_time = $cnt->get_value_or_zero('busy', 'total');
245              
246 0           print $self->headline(1, 'Smtpd Statistics');
247              
248 0           print $self->key_value_table( "Connections", [
249             [ 'Connections', $connections ],
250             [ 'Hosts/domains', $hosts_domains ],
251             [ 'Avg. connect time', $avg_conn_time ],
252             [ 'total connect time', $total_conn_time, 'interval' ],
253             ] );
254 0           return;
255             }
256              
257             sub print_smtpd_summaries {
258 0     0 0   my ( $self, $cnt ) = @_;
259 0           my $smtpd_stats = $cnt->{'PostfixSmtpdStats'};
260 0           my $params = {
261             'day' => [ 'Per-Day', 'per_day', 'string' ],
262             'hour' => [ 'Per-Hour', 'per_hr', 'decimal' ],
263             'domain' => [ 'Per-Domain', 'per_domain', [ 'connections', 'decimal', 20 ] ],
264             };
265              
266 0           foreach my $table ( 'day', 'hour', 'domain' ) {
267 0           my ( $title, $key, $sort ) = @{$params->{ $table }};
  0            
268 0           print $self->headline(1, "$title SMTPD Connection Summary");
269 0           print $self->statistics_from_hashes(
270             legend => $table,
271             sort => $sort,
272             rows => [
273             [ 'connections', $smtpd_stats->get_node($key) ],
274             [ 'time conn.', $smtpd_stats->get_node('busy', $key) ],
275             [ 'avg./conn.', $self->hash_calc_avg( 2,
276             $smtpd_stats->get_node('busy', $key),
277             $smtpd_stats->get_node($key),
278             ), ],
279             [ 'max. time', $smtpd_stats->get_node('busy', 'max_'.$key ), ],
280             ],
281             );
282             }
283 0           return;
284             }
285              
286             sub print_domain_summaries {
287 0     0 0   my ( $self, $cnt ) = @_;
288 0           my $top_cnt = $self->{'top_domains_cnt'};
289             $top_cnt = defined $top_cnt && $top_cnt >= 0 ?
290 0 0 0       $self->{'top_domains_cnt'} : 20;
291 0           my $delivered = $cnt->{'PostfixDelivered'};
292              
293 0           foreach my $table ( 'sent', 'recieved' ) {
294 0           print $self->headline(1, "Host/Domain Summary: Message Delivery (top $top_cnt $table)");
295 0 0         print $self->statistics_from_hashes(
296             legend => 'host/domain',
297             sort => [ 'sent cnt', 'decimal', $top_cnt ],
298             rows => [
299             [ 'sent cnt', $delivered->get_node($table, 'by_domain') ],
300             [ 'bytes', $delivered->get_node($table, 'size', 'by_domain') ],
301             $table eq 'sent' ? (
302             # TODO
303             #[ 'defers', $delivered->get_node('busy', 'per_day') ],
304             [ 'avg delay', $self->hash_calc_avg( 2,
305             $delivered->get_node($table, 'delay', 'by_domain'),
306             $delivered->get_node($table, 'by_domain'),
307             ), ],
308             [ 'max. delay', $delivered->get_node($table, 'max_delay', 'by_domain'), ],
309             ) : (),
310             ],
311             );
312             }
313              
314 0           return;
315             }
316             sub print_geo_stats {
317 0     0 0   my ( $self, $cnt ) = @_;
318 0           my $client = $cnt->get_node('client');
319 0 0         if( defined $client ) {
320 0           print $self->hash_top_values(
321             $client,
322             title => 'Client Countries',
323             count => 0,
324             legend => 'Country',
325             );
326             }
327 0           return;
328             }
329              
330             sub print_tls_stats {
331 0     0 0   my ( $self, $cnt ) = @_;
332 0           my $tls_cnt = $cnt->{'TlsStatistics'};
333 0           my $smtpd_cnt = $cnt->{'PostfixSmtpdStats'};
334 0           my $recieved_cnt = $cnt->{'PostfixRecieved'};
335 0           my $delivered_cnt = $cnt->{'PostfixDelivered'};
336 0           my $smtpdConnCnt;
337              
338 0 0         if( defined $smtpd_cnt ) {
339 0           $smtpdConnCnt = $smtpd_cnt->get_value_or_zero('total');
340             }
341 0           my $msgs_rcvd = $recieved_cnt->get_value_or_zero('total');
342 0           my $msgs_sent = $delivered_cnt->get_value_or_zero('sent', 'total');
343              
344 0           print $self->headline(1, "TLS Statistics");
345              
346 0           print $self->key_value_table( "Total", [
347             [ 'Incoming TLS connections',
348             $tls_cnt->get('smtpd', 'connections', 'total'),
349             'count', $smtpdConnCnt ],
350             [ 'Incoming TLS messages',
351             $tls_cnt->get('smtpd', 'messages', 'total'),
352             'count', $msgs_rcvd ],
353             [ 'Outgoing TLS connections',
354             $tls_cnt->get('smtp', 'connections', 'total'),
355             'count', $smtpdConnCnt ],
356             [ 'Outgoing TLS messages',
357             $tls_cnt->get('smtp', 'messages', 'total'),
358             'count', $msgs_sent ],
359             ] );
360              
361 0           my @tls_statistics = (
362             [ "Incoming TLS trust-level" => 'trust-level',
363             $smtpdConnCnt, 'smtpd', 'connections', 'level' ],
364             [ "Outgoing TLS trust-level" => 'trust-level',
365             0, 'smtp', 'connections', 'level' ],
366             [ "Incoming TLS Protocol Version" => 'protocol version',
367             $smtpdConnCnt, 'smtpd', 'connections', 'protocol' ],
368             [ "Outgoing TLS Protocol Version" => 'protocol version',
369             0, 'smtp', 'connections', 'protocol' ],
370             [ "Incoming TLS key length" => 'key length',
371             $smtpdConnCnt, 'smtpd', 'connections', 'keylen' ],
372             [ "Outgoing TLS key length" => 'key length',
373             0, 'smtp', 'connections', 'keylen' ],
374             [ "Incoming TLS Ciphers" => 'cipher',
375             $smtpdConnCnt, 'smtpd', 'connections', 'cipher' ],
376             [ "Outgoing TLS Ciphers" => 'cipher',
377             0, 'smtp', 'connections', 'cipher' ],
378             );
379              
380 0           foreach my $tls_stat ( @tls_statistics ) {
381 0           my ( $title, $legend, $total, @node ) = @$tls_stat;
382 0           my $values = $tls_cnt->get_node(@node);
383 0 0         if( ! defined $values ) { next; }
  0            
384 0           print $self->hash_top_values( $values,
385             title => $title,
386             total => $total,
387             legend => $legend,
388             );
389             }
390             }
391              
392             sub print_problems_reports {
393 0     0 0   my ( $self, $cnt ) = @_;
394              
395 0           my $delivered_cnt = $cnt->{'PostfixDelivered'};
396 0           my $reject_cnt = $cnt->{'PostfixRejects'};
397              
398 0 0         if($self->{'deferral_detail'} != 0) {
399             print $self->nested_top_values(
400             $delivered_cnt->get_node('deferred'),
401             title => "message deferral detail",
402 0           count => $self->{'deferral_detail'} );
403             }
404 0 0         if($self->{'bounce_detail'} != 0) {
405             print $self->nested_top_values(
406             $delivered_cnt->get_node('bounced'),
407             title => "message bounce detail (by relay)",
408 0           count => $self->{'bounce_detail'} );
409             }
410 0 0         if($self->{'reject_detail'} != 0) {
411 0           foreach my $key ( 'reject', 'warning', 'hold', 'discard') {
412             print $self->nested_top_values(
413             $reject_cnt->get_node($key),
414             title => "message $key detail",
415 0           count => $self->{'reject_detail'} );
416             }
417             }
418              
419 0 0         if( my $smtp_cnt = $cnt->{'PostfixSmtp'} ) {
420 0           my $messages = $smtp_cnt->get_node('messages');
421 0 0         if( defined $messages ) {
422             print $self->nested_top_values($messages,
423             title => "smtp delivery failures",
424 0           count => $self->{'smtp_detail'} );
425             }
426             }
427 0 0         if( my $msg_cnt = $cnt->{'PostfixMessages'} ) {
428 0 0         if($self->{'smtpd_warn_detail'} != 0) {
429             print $self->nested_top_values(
430             $msg_cnt->get_node('warning'),
431             title => "Warnings",
432 0           count => $self->{'smtpd_warn_detail'} );
433             }
434 0           print $self->nested_top_values(
435             $msg_cnt->get_node('fatal'),
436             title => "Fatal Errors" );
437 0           print $self->nested_top_values(
438             $msg_cnt->get_node('panic'),
439             title => "Panics" );
440 0           print $self->hash_top_values($msg_cnt->get_node('master'),
441             title => "Master daemon messages",
442             legend => 'Message',
443             );
444             }
445             }
446              
447             sub print_traffic_summaries {
448 0     0 0   my ( $self, $cnt ) = @_;
449 0           my $params = {
450             'day' => [ 'Per-Day', 'per_day', 'string' ],
451             'hour' => [ 'Per-Hour', 'per_hr', 'decimal' ],
452             };
453              
454 0           foreach my $table ('day', 'hour') {
455 0           my ( $title, $key, $sort ) = @{$params->{ $table }};
  0            
456 0           print $self->headline(1, 'Traffic Summary ('.$title.')');
457             print $self->statistics_from_hashes(
458             legend => $table,
459             sort => $sort,
460             chart => 1,
461             rows => [
462             [ 'recieved', $cnt->{'PostfixRecieved'}->get_node($key) ],
463             [ 'delivered', $cnt->{'PostfixDelivered'}->get_node('sent', $key) ],
464             [ 'deffered', $cnt->{'PostfixDelivered'}->get_node('deferred', $key), ],
465             [ 'bounced', $cnt->{'PostfixDelivered'}->get_node('bounced', $key), ],
466 0           [ 'rejected', $cnt->{'PostfixRejects'}->get_node($key) ],
467             ],
468             );
469             }
470              
471 0           return;
472             }
473              
474             sub hash_calc_avg {
475 0     0 0   my ( $self, $precision, $total, $count ) = @_;
476 0           my %avg;
477 0           my %uniq = map { $_ => 1 } ( keys %$total, keys %$count );
  0            
478 0           my @keys = keys %uniq;
479 0           foreach my $key ( @keys ) {
480 0           my $value;
481 0 0 0       if( defined $total->{$key} && $total->{$key} > 0
      0        
      0        
482             && defined $count->{$key} && $count->{$key} > 0 ) {
483 0           $value = $total->{$key} / $count->{$key};
484             }
485 0 0 0       if( defined $total->{$key} && $total->{$key} eq 0 ) {
486 0           $value = 0;
487             }
488 0 0         if( defined $value ) {
489 0           $avg{$key} = sprintf('%.'.$precision.'f', $value);
490             } else {
491 0           $avg{$key} = undef;
492             }
493             }
494 0           return \%avg;
495             }
496              
497             sub statistics_from_hashes {
498 0     0 0   my ( $self, %params ) = @_;
499 0           my @rows = @{$params{'rows'}};
  0            
500 0           my @head = map { $_->[0] } @rows;
  0            
501 0           my @hashes = map { $_->[1] } @rows;
  0            
502 0           my @yaxis;
503 0           my ( @series, @labeled_rows );
504              
505 0 0         if( ref($params{'sort'}) eq 'ARRAY' ) { # sort by a column value
506 0           my ( $sortby, $alg, $limit ) = @{$params{'sort'}};
  0            
507 0           my ( $row ) = grep { $_->[0] eq $sortby } @rows;
  0            
508 0           $row = $row->[1];
509 0 0         if( ! defined $row ) { die('cant find row '.$sortby.' for sorting'); }
  0            
510 0 0         if( $alg eq 'decimal' ) {
511 0           @yaxis = sort { $row->{$b} <=> $row->{$a} } keys %$row;
  0            
512             } else { # string
513 0           @yaxis = sort { $row->{$b} cmp $row->{$a} } keys %$row;
  0            
514             }
515 0 0 0       if( $limit > 0 && scalar @yaxis > $limit ) { @yaxis = @yaxis[0 .. ($limit-1) ] };
  0            
516             } else { # simple sort by key
517 0           my @all_keys = map { keys %$_ } @hashes;
  0            
518 0           my %uniq = map { $_ => 1 } @all_keys;
  0            
519 0 0         if( $params{'sort'} eq 'decimal' ) {
520 0           @yaxis = sort { $a <=> $b } keys %uniq;
  0            
521             } else { # string
522 0           @yaxis = sort { $a cmp $b } keys %uniq;
  0            
523             }
524             }
525              
526 0           foreach my $row ( @yaxis ) {
527 0           push(@labeled_rows, [ $row, map { $_->{$row} } @hashes ] );
  0            
528             }
529              
530 0           foreach my $row ( @rows ) {
531 0           my $name = $row->[0];
532 0           my $values = $row->[1];
533             push(@series, {
534             label => $name,
535 0           data => [ map { [
536             $_ =~ /\d{4}-\d{2}-\d{2}/ ?
537             Time::Piece->strptime($_, '%Y-%m-%d')->epoch
538             : $_,
539 0 0         defined $values->{$_} ? $values->{$_} : 0
    0          
540             ] } @yaxis ],
541             } );
542             }
543              
544             my %options = (
545 0           legend => $params{'legend'},
546             head => \@head,
547             labeled_rows => \@labeled_rows,
548             series => \@series,
549             yaxis => \@yaxis,
550             hashes => \@hashes,
551             );
552              
553 0           my $output = '';
554 0 0 0       if( defined $params{'chart'} && $params{'chart'} ) {
555 0           $output .= $self->statistics_chart( %options );
556             }
557 0           $output .= $self->statistics_table( %options );
558 0           return $output;
559             }
560              
561             sub statistics_table {
562 0     0 0   my $self = shift;
563 0           return $self->process('statistics_table', @_ );
564             }
565              
566             sub statistics_chart {
567 0     0 0   my $self = shift;
568 0           return $self->process('statistics_chart', @_ );
569             }
570              
571             sub get_element_id {
572 0     0 0   my $self = shift;
573 0 0         if( ! defined $self->{_cur_element_id} ) {
574 0           $self->{_cur_element_id} = 1;
575             }
576 0           return $self->{_cur_element_id}++;
577             }
578              
579             sub headline {
580 0     0 0   my ( $self, $level, $title ) = @_;
581 0 0         if( ! defined $self->{'_headlines'} ) {
582 0           $self->{'_headlines'} = [];
583             }
584 0           my $cur = $self->{'_headlines'};
585 0           my $cur_level = 1;
586 0           my $id = 'title-'.$self->get_element_id;
587 0           while( $cur_level < $level ) {
588 0 0 0       if( scalar(@$cur) == 0 || ref($cur->[-1]) ne 'ARRAY' ) {
589 0           push(@$cur, []);
590             }
591 0           $cur = $cur->[-1];
592 0           $cur_level++;
593             }
594 0           my %headline = (
595             title => $title,
596             id => $id,
597             level => $level,
598             );
599 0           push(@$cur, \%headline );
600 0           return $self->process('headline', %headline );
601             }
602              
603             sub navigation {
604 0     0 0   my ( $self, $depth ) = @_;
605             return $self->process('navigation',
606             nav => $self->{_headlines},
607 0           depth => $depth,
608             );
609 0           return;
610             }
611              
612             sub nested_top_values {
613 0     0 0   my ( $self, $hash ) = ( shift, shift );
614 0           my %args = (
615             'count' => 0,
616             'unit' => 'count',
617             'legend' => '',
618             @_,
619             );
620 0 0         if( ! defined $hash) { return ''; }
  0            
621            
622 0           return $self->process('nested_values_table',
623             %args,
624             'data' => $hash,
625             );
626             }
627              
628             sub hash_top_values {
629 0     0 0   my ( $self, $hash ) = ( shift, shift );
630 0           my %args = (
631             'count' => 0,
632             'unit' => 'count',
633             'legend' => '',
634             'total' => 0,
635             @_,
636             );
637 0 0         if( ! defined $hash) { return ''; }
  0            
638              
639 0 0         my @data = sort { $b->[0] <=> $a->[0] || $b->[1] cmp $a->[1] }
640 0           map { [ $hash->{$_} => $_ ] } keys %$hash;
  0            
641              
642 0           return $self->process('top_values_table',
643             %args,
644             'data' => \@data,
645             );
646             }
647              
648             sub key_value_table {
649 0     0 0   my ( $self, $name, $data ) = @_;
650 0           return $self->process('key_value_table',
651             'name' => $name,
652             'data' => $data,
653             );
654             }
655              
656              
657             1;
658              
659             =pod
660              
661             =encoding UTF-8
662              
663             =head1 NAME
664              
665             Log::Saftpresse::CountersOutput::Html - plugin to output counters in HTML report
666              
667             =head1 VERSION
668              
669             version 1.5
670              
671             =head1 AUTHOR
672              
673             Markus Benning <ich@markusbenning.de>
674              
675             =head1 COPYRIGHT AND LICENSE
676              
677             This software is Copyright (c) 1998 by James S. Seymour, 2015 by Markus Benning.
678              
679             This is free software, licensed under:
680              
681             The GNU General Public License, Version 2, June 1991
682              
683             =cut
684              
685             __DATA__
686             [% BLOCK header -%]
687             <!DOCTYPE html>
688             <html lang="en">
689             <head>
690             <meta charset="utf-8">
691             <meta http-equiv="X-UA-Compatible" content="IE=edge">
692             <meta name="viewport" content="width=device-width, initial-scale=1">
693             <meta name="description" content="[% self.title %]">
694              
695             <title>[% self.title %]</title>
696              
697             <link rel="stylesheet" href="https://markusbenning.de/js/bootstrap/css/bootstrap.min.css" />
698             <link rel="stylesheet" href="https://markusbenning.de/js/bootstrap/css/bootstrap-theme.min.css" />
699             <script src="https://markusbenning.de/js/jquery.min.js"></script>
700             <script src="https://markusbenning.de/js/bootstrap/js/bootstrap.min.js"></script>
701             <script src="https://markusbenning.de/js/numeral.min.js"></script>
702             <script src="https://markusbenning.de/js/flot/jquery.flot.min.js"></script>
703             <script>
704             $( document ).ready(function() {
705             $("span.unit-count").each( function( index ) {
706             $( this ).html( numeral( $(this).text() ).format('0,0') );
707             });
708             $("span.unit-byte").each(function( index ) {
709             $( this ).html( numeral( $(this).text() ).format('0 b') );
710             });
711             $("span.unit-percent").each(function( index ) {
712             $( this ).html( numeral( $(this).text() ).format('0.00%') );
713             });
714             $("span.unit-interval").each(function( index ) {
715             $( this ).html( numeral( $(this).text() ).format('00:00:00') );
716             });
717             });
718             </script>
719             </head>
720              
721             <body>
722              
723             <nav class="navbar navbar-inverse">
724             <div class="container-fluid">
725             <div class="navbar-header">
726             <a class="navbar-brand" href="#">Postfix Statistics</a>
727             </div>
728             <div id="navbar" class="navbar-collapse collapse">
729             <ul class="nav navbar-nav navbar-right">
730             <li><a href="https://markusbenning.de/">saftpresse</a></li>
731             </ul>
732             </div>
733             </div>
734             </nav>
735              
736             <div class="container-fluid">
737             <div class="row">
738             <div class="col-md-10 col-md-push-2 main">
739             <h1>[% self.title %]</h1>
740             <p class="lead">generated by saftpresse [% self.version %] log file analyzer</p>
741             [% END -%]
742              
743             [% BLOCK footer %]
744             </div>
745             [% self.navigation(2) %]
746             </div>
747             </div> <!-- /container -->
748              
749              
750             </body>
751             </html>
752             [% END %]
753              
754             [% BLOCK navigation %]
755             <div class="col-md-2 col-md-pull-10 sidebar">
756             <ul class="nav nav-sidebar nav-stacked">
757             [% INCLUDE nav_element nav=nav level=1 depth=depth %]
758             </ul>
759             </div>
760             [% END %]
761              
762             [% BLOCK nav_element %]
763             [% FOREACH element = nav -%]
764             [% IF element.type == 'list' -%]
765             [% IF level < depth -%]
766             <li><ul class="nav">
767             [% INCLUDE nav_element nav=element level=level+1 depth=depth %]
768             </ul></li>
769             [% END %]
770             [% ELSE -%]
771             <li><a href="#[% element.id %]">[% element.title %]</a></li>
772             [% END -%]
773             [% END -%]
774             [% END %]
775              
776             [% BLOCK headline %]
777             <h[% level + 1 %] id="[% id %]">[% title %]</h[% level + 1 %]>
778             [% END %]
779              
780             [% BLOCK key_value_table %]
781             [% self.headline( 2, name ) %]
782             <table class="table table-striped table-hover">
783             <thead>
784             <tr>
785             <th class="col-md-3">Key</th>
786             <th>Value</th>
787             </tr>
788             </thead>
789             <tbody>
790             [% FOREACH row = data -%]
791             <tr>
792             <td>[% row.shift %]</td>
793             <td>[% INCLUDE format_unit format=row %]</td>
794             </tr>
795             [% END -%]
796             </tbody>
797             </table>
798             [% END %]
799              
800             [% BLOCK format_unit %]
801             [% value = format.0; type = format.1 -%]
802             [% IF ! type ; type = 'count' ; END -%]
803             [% IF format.2 -%]
804             [% total = format.2 ; percent = value / total; -%]
805             <span class="unit-[% type %]">[% value %]</span>
806             (<span class="unit-percent">[% percent %]</span> of <span class="unit-[% type %]">[% total %]</span>)
807             [% ELSE -%]
808             <span class="unit-[% type %]">[% value %]</span>
809             [% END -%]
810             [% END %]
811              
812             [% BLOCK top_values_table %]
813             [% IF title ; self.headline( 2, title ) ; END -%]
814             <table class="table table-striped table-hover[% IF compact %] table-condensed[% END %]">
815             <thead>
816             <tr>
817             <th class="col-md-3">Count</th>
818             <th>[% legend %]</th>
819             </tr>
820             </thead>
821             <tbody>
822             [% FOREACH row = data -%]
823             <tr>
824             <td>[% INCLUDE format_unit format=[ row.0, unit, total ] %]</td>
825             <td>[% row.1 %]</td>
826             </tr>
827             [% END -%]
828             </tbody>
829             </table>
830             [% END %]
831              
832             [% BLOCK nested_values_table %]
833             [% self.headline( 2, title ) %]
834             [% FOREACH section = data -%]
835             [% IF title ; self.headline( 3, section.key ) ; END %]
836             <div class="panel-group">
837             [% FOREACH panel = section.value -%]
838             <div class="panel panel-default">
839             <div class="panel-heading">[% panel.key %]</div>
840             <div class="panel-body">
841             [% IF panel.value.values.0.type == 'scalar' -%]
842             [% self.hash_top_values(panel.value, 'title', '', 'compact', 1) -%]
843             [% ELSE -%]
844             [% INCLUDE nested_values_table data=panel.value title=undef -%]
845             [% END -%]
846             </div>
847             </div>
848             [% END %]
849             </div>
850             [% END %]
851             [% END %]
852              
853             [% BLOCK statistics_table %]
854             <table class="table table-striped table-hover table-condensed">
855             <thead>
856             <tr>
857             <th>[% legend %]</th>
858             [% FOREACH th = head -%]
859             <th>[% th %]</th>
860             [% END -%]
861             </tr>
862             </thead>
863             <tbody>
864             [% FOREACH row = labeled_rows -%]
865             <tr>
866             [% FOREACH td = row -%]
867             <td>[% td != '' ? td : '-' %]</td>
868             [% END -%]
869             </tr>
870             [% END -%]
871             </tbody>
872             </table>
873             [% END %]
874              
875             [% BLOCK statistics_chart %]
876             [% chartid = 'chart-' _ self.get_element_id -%]
877             <div id="[% chartid %]" style="width:100%;height:300px"></div>
878             <script>
879             $( document ).ready(function() {
880             var data = [% self.json.encode( series ) %];
881             var options = {
882             series: {
883             stack: 1,
884             lines: {
885             show: true,
886             },
887             points: {
888             show: true,
889             },
890             grid: {
891             hoverable: true,
892             clickable: true
893             }
894             }
895             };
896             $("#[% chartid %]").plot(data, options);
897             });
898             </script>
899             [% END %]