1 # Copyright (c) 2010 Zmanda, Inc. All Rights Reserved.
3 # This program is free software; you can redistribute it and/or modify it
4 # under the terms of the GNU General Public License version 2 as published
5 # by the Free Software Foundation.
7 # This program is distributed in the hope that it will be useful, but
8 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
9 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12 # You should have received a copy of the GNU General Public License along
13 # with this program; if not, write to the Free Software Foundation, Inc.,
14 # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16 # Contact information: Zmanda Inc., 465 S Mathlida Ave, Suite 300
17 # Sunnyvale, CA 94085, USA, or: http://www.zmanda.com
20 package Amanda::Report::human;
28 use Amanda::Config qw(:getconf config_dir_relative);
29 use Amanda::Util qw(:constants quote_string );
32 use Amanda::Debug qw( debug );
33 use Amanda::Util qw( quote_string );
37 ## constants that define the column specification output format.
39 use constant COLSPEC_NAME => 0; # column name; used internally
40 use constant COLSPEC_PRE_SPACE => 1; # prefix spaces
41 use constant COLSPEC_WIDTH => 2; # column width
42 use constant COLSPEC_PREC => 3; # post-decimal precision
43 use constant COLSPEC_MAXWIDTH => 4; # resize if set
44 use constant COLSPEC_FORMAT => 5; # sprintf format
45 use constant COLSPEC_TITLE => 6; # column title
47 use constant PROGRAM_ORDER =>
48 qw(amdump planner amflush amvault driver dumper chunker taper reporter);
59 : ( ($q = $a / $b) > 99999.95 ) ? "#####"
60 : ( $q > 999.95 ) ? sprintf( "%5.0f", $q )
61 : sprintf( "%5.1f", $q );
70 : ( ($q = $a / $b) > 9999999.95 ) ? "#######"
71 : ( $q > 99999.95 ) ? sprintf( "%7.0f", $q )
72 : sprintf( "%7.1f", $q );
77 my ( $a, $b, $col ) = @_;
80 : sprintf( $col->[5], $col->[2], $col->[3], ( $a / $b ) );
85 my ( $format, @args ) = @_;
87 formline( $format, @args );
93 my ( $max, @args ) = @_; # first element starts as max
95 foreach my $elt (@args) {
96 $max = $elt if $elt > $max;
103 my ( $min, @args ) = @_; # first element starts as min
105 foreach my $elt (@args) {
106 $min = $elt if $elt < $min;
114 $sec += 30; # round up
115 my ( $hr, $mn ) = ( int( $sec / ( 60 * 60 ) ), int( $sec / 60 ) % 60 );
116 return sprintf( '%d:%02d', $hr, $mn );
122 $sec += 0.5; # round up
123 my ( $mn, $sc ) = ( int( $sec / (60) ), int( $sec % 60 ) );
124 return sprintf( '%d:%02d', $mn, $sc );
129 # return $val/$unit_divisor as a a floating-point number
132 my ($val, %params) = @_;
134 return $params{'zero'} if ($val == 0 and exists $params{'zero'});
136 # $orig_size and $out_size are bigints, which must be stringified to cast
137 # them to floats. We need floats, because they round nicely. This is
138 # ugly and hard to track down.
139 my $flval = $val.".0";
140 my $flunit = $self->{'unit_div'}.".0";
141 return $flval / $flunit;
148 my ($class, $report, $fh, $config_name, $logfname) = @_;
153 config_name => $config_name,
154 logfname => $logfname,
157 disp_unit => getconf($CNF_DISPLAYUNIT),
158 unit_div => getconf_unit_divisor(),
164 dumpdisks => [ 0, 0 ], # full_count, incr_count
165 tapedisks => [ 0, 0 ],
166 tapeparts => [ 0, 0 ],
169 if (defined $report) {
171 my (@errors, @stranges, @notes);
174 map { @{ $report->get_program_info($_, "errors", []) }; }
176 ## prepend program name to notes lines.
177 foreach my $program (PROGRAM_ORDER) {
179 map { "$program: $_" }
180 @{ $report->get_program_info($program, "notes", []) };
183 $self->{errors} = \@errors;
184 $self->{notes} = \@notes;
194 my $fh = $self->{fh};
195 my $report = $self->{report};
197 # TODO: the hashes are a cheap fix. fix these.
198 my @dles = $report->get_dles();
199 my $full_stats = $self->{full_stats};
200 my $incr_stats = $self->{incr_stats};
201 my $total_stats = $self->{total_stats};
202 my $dumpdisks = $self->{dumpdisks};
203 my $tapedisks = $self->{tapedisks};
204 my $tapeparts = $self->{tapeparts};
206 ## initialize all relevant fields to 0
207 map { $incr_stats->{$_} = $full_stats->{$_} = 0; }
208 qw/dumpdisk_count tapedisk_count tapepart_count outsize origsize
209 tapesize coutsize corigsize taper_time dumper_time/;
211 foreach my $dle_entry (@dles) {
213 # $dle_entry = [$hostname, $disk]
214 my $dle = $report->get_dle_info(@$dle_entry);
215 my $alldumps = $dle->{'dumps'};
217 while( my ($timestamp, $tries) = each %$alldumps ) {
218 foreach my $try ( @$tries ) {
220 my $level = exists $try->{dumper} ? $try->{dumper}{'level'} :
221 exists $try->{taper} ? $try->{taper}{'level'} :
223 my $stats = ($level > 0) ? $incr_stats : $full_stats;
225 # compute out size, skipping flushes (tries without a dumper run)
227 if (exists $try->{dumper}
228 && exists $try->{chunker} && defined $try->{chunker}->{kb}
229 && ( $try->{chunker}{status} eq 'success'
230 || $try->{chunker}{status} eq 'partial')) {
231 $outsize = $try->{chunker}->{kb};
232 } elsif (exists $try->{dumper}
233 && exists $try->{taper} && defined $try->{taper}->{kb}
234 && ( $try->{taper}{status} eq 'done'
235 || $try->{taper}{status} eq 'partial')) {
236 $outsize = $try->{taper}->{kb};
239 # compute orig size, again skipping flushes
241 if ( exists $try->{dumper}
242 && ( $try->{dumper}{status} eq 'success'
243 || $try->{dumper}{status} eq 'strange')) {
245 $origsize = $try->{dumper}{orig_kb};
246 $stats->{dumper_time} += $try->{dumper}{sec};
247 $stats->{dumpdisk_count}++; # count this as a dumped filesystem
248 $dumpdisks->[$try->{dumper}{'level'}]++; #by level count
249 } elsif (exists $try->{dumper}
250 && exists $try->{taper} && defined $try->{taper}->{kb}
251 && ( $try->{taper}{status} eq 'done'
252 || $try->{taper}{status} eq 'partial')) {
253 # orig_kb doesn't always exist (older logfiles)
254 if ($try->{taper}->{orig_kb}) {
255 $origsize = $try->{taper}->{orig_kb};
259 if ( exists $try->{taper}
260 && ( $try->{taper}{status} eq 'done'
261 || $try->{taper}{status} eq 'partial')) {
263 $stats->{tapesize} += $try->{taper}{kb};
264 $stats->{taper_time} += $try->{taper}{sec};
265 $stats->{tapepart_count} += @{ $try->{taper}{parts} }
266 if $try->{taper}{parts};
267 $stats->{tapedisk_count}++;
269 $tapedisks->[ $try->{taper}{level} ]++; #by level count
270 $tapeparts->[$try->{taper}{level}] += @{ $try->{taper}{parts} }
271 if $try->{taper}{parts};
274 # add those values to the stats
275 $stats->{'origsize'} += $origsize;
276 $stats->{'outsize'} += $outsize;
278 # if the sizes differ, then we have a compressed dump, so also add it to
280 $stats->{'corigsize'} += $origsize;
281 $stats->{'coutsize'} += $outsize;
286 %$total_stats = map { $_ => $incr_stats->{$_} + $full_stats->{$_} }
289 $total_stats->{planner_time} =
290 $report->get_program_info("planner", "time", 0);
292 if ($report->get_flag("got_finish")) {
293 $total_stats->{total_time} =
294 $report->get_program_info("driver", "time", 0)
295 || $report->get_program_info("amflush", "time", 0);
297 $total_stats->{total_time} =
298 $total_stats->{taper_time} + $total_stats->{planner_time};
301 $total_stats->{idle_time} =
302 ( $total_stats->{total_time} - $total_stats->{planner_time} ) -
303 $total_stats->{taper_time};
305 # TODO: tape info is very sparse. There either needs to be a
306 # function that collects and fills in tape info post-processing in
307 # Amanda::Report, or it needs to be done here.
311 sub print_human_amreport
313 my ( $self, $fh ) = @_;
316 || die "error: no file handle given to print_human_amreport\n";
318 ## collect statistics
319 $self->calculate_stats();
321 ## print the basic info header
322 $self->print_header();
324 ## print out statements about past and predicted tape usage
325 $self->output_tapeinfo();
327 ## print out error messages from the run
328 $self->output_error_summaries();
330 ## print out aggregated statistics for the whole dump
331 $self->output_stats();
333 ## print out statistics for each tape used
334 $self->output_tape_stats();
336 ## print out all errors & comments
337 $self->output_details();
339 ## print out dump statistics per DLE
340 $self->output_summary();
344 "(brought to you by Amanda version $Amanda::Constants::VERSION)\n";
353 my $report = $self->{report};
354 my $fh = $self->{fh};
355 my $config_name = $self->{config_name};
357 my $hostname = $report->{hostname};
358 my $org = getconf($CNF_ORG);
360 # TODO: this should be a shared method somewhere
361 my $timestamp = $report->get_timestamp();
362 my ($year, $month, $day) = ($timestamp =~ m/^(\d\d\d\d)(\d\d)(\d\d)/);
363 my $date = POSIX::strftime('%B %e, %Y', 0, 0, 0, $day, $month - 1, $year - 1900);
364 $date =~ s/ / /g; # get rid of intervening space
366 print $fh "*** THE DUMPS DID NOT FINISH PROPERLY!\n\n"
367 unless ($report->{flags}{got_finish});
369 my $header_format = <<EOF;
370 @<<<<<<<: @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<...
374 print $fh swrite($header_format, "Hostname", $hostname);
375 print $fh swrite($header_format, "Org", $org);
376 print $fh swrite($header_format, "Config", $config_name);
377 print $fh swrite($header_format, "Date", $date);
387 my $report = $self->{report};
388 my $fh = $self->{fh};
389 my $logfname = $self->{logfname};
391 my $taper = $report->get_program_info("taper");
392 my $tapes = $taper->{tapes} || {};
393 my $tape_labels = $taper->{tape_labels} || [];
395 my %full_stats = %{ $self->{full_stats} };
396 my %incr_stats = %{ $self->{incr_stats} };
397 my %total_stats = %{ $self->{total_stats} };
399 if (@$tape_labels > 0) {
401 # slightly different sentence depending on the run type
403 if ($report->get_flag("amflush_run")) {
404 $tapelist_str = "The dumps were flushed ";
405 } elsif ($report->get_flag("amvault_run")) {
406 $tapelist_str = "The dumps were vaulted ";
408 $tapelist_str = "These dumps were ";
410 $tapelist_str .= (@$tape_labels > 1) ? "to tapes " : "to tape ";
411 $tapelist_str .= join(", ", @$tape_labels) . ".\n";
412 print $fh $tapelist_str;
416 $report->get_program_info("taper", "tape_error", undef)) {
418 if ($report->get_program_info("taper", "failure_from", undef) eq "config") {
419 # remove leading [ and trailling ]
420 $tape_error =~ s/^\[//;
421 $tape_error =~ s/\]$//;
422 print $fh "Not using all tapes because $tape_error.\n";
424 print $fh "*** A TAPE ERROR OCCURRED: $tape_error.\n";
426 #$tape_error =~ s{^no-tape }{};
429 ## if this is a historical report, do not generate holding disk
430 ## information. If this dump is the most recent, output holding
432 if ($report->get_flag("historical")) {
433 print $fh "Some dumps may have been left in the holding disk.\n\n"
434 if $report->get_flag("degraded_mode")
438 my @holding_list = Amanda::Holding::get_files_for_flush();
440 foreach my $holding_file (@holding_list) {
441 $h_size += (0 + Amanda::Holding::file_size($holding_file, 1));
445 sprintf("%.0f%s", $self->tounits($h_size), $self->{disp_unit});
449 "There are $h_size_u of dumps left in the holding disk.\n";
451 (getconf($CNF_AUTOFLUSH))
452 ? print $fh "They will be flushed on the next run.\n\n"
453 : print $fh "Run amflush to flush them to tape.\n\n";
455 } elsif ($report->get_flag("degraded_mode")) {
456 print $fh "No dumps are left in the holding disk.\n\n";
461 my $run_tapes = getconf($CNF_RUNTAPES);
465 ? print $fh "The next $run_tapes tapes Amanda expects to use are: "
466 : print $fh "The next tape Amanda expects to use is: ";
470 foreach my $i ( 0 .. ( $run_tapes - 1 ) ) {
472 if ( my $tape_label =
473 Amanda::Tapelist::get_last_reusable_tape_label($i) ) {
476 print $fh ", " if !$first;
477 print $fh "$nb_new_tape new tape"
478 . ( $nb_new_tape > 1 ? "s" : "" );
493 print $fh ", " if !$first;
494 print $fh "$nb_new_tape new tape"
495 . ( $nb_new_tape > 1 ? "s" : "" );
499 my $new_tapes = Amanda::Tapelist::list_new_tapes(getconf($CNF_RUNTAPES));
500 print $fh "$new_tapes\n" if $new_tapes;
505 sub output_error_summaries
508 my $errors = $self->{errors};
509 my $report = $self->{report};
511 my @dles = $report->get_dles();
513 my @fatal_failures = ();
514 my @error_failures = ();
515 my @missing_failures = ();
516 my @driver_failures = ();
517 my @planner_failures = ();
518 my @dump_failures = ();
521 foreach my $program (PROGRAM_ORDER) {
523 push @fatal_failures,
524 map { "$program: FATAL $_" }
525 @{ $report->get_program_info($program, "fatal", []) };
526 push @error_failures,
527 map { "$program: ERROR $_" }
528 @{ $report->get_program_info($program, "errors", []) };
531 foreach my $dle_entry (@dles) {
533 my ($hostname, $disk) = @$dle_entry;
534 my $alldumps = $report->get_dle_info(@$dle_entry, "dumps");
535 my $dle = $report->get_dle_info($hostname, $disk);
536 my $qdisk = quote_string($disk);
538 if ($report->get_flag('results_missing') and
539 !defined($alldumps->{$report->{run_timestamp}}) and
541 push @missing_failures, "$hostname $qdisk RESULTS MISSING";
544 if ( exists $dle->{driver}
545 && exists $dle->{driver}->{error}) {
546 push @driver_failures, "$hostname $qdisk lev $dle->{driver}->{level} FAILED $dle->{driver}->{error}";
549 if ( exists $dle->{planner}
550 && exists $dle->{planner}->{error}) {
551 push @planner_failures, "$hostname $qdisk lev $dle->{planner}->{level} FAILED $dle->{planner}->{error}";
554 while( my ($timestamp, $tries) = each %$alldumps ) {
556 foreach my $try (@$tries) {
557 if (exists $try->{dumper} &&
558 $try->{dumper}->{status} &&
559 $try->{dumper}->{status} eq 'fail') {
560 push @dump_failures, "$hostname $qdisk lev $try->{dumper}->{level} FAILED $try->{dumper}->{error}";
563 if (exists $try->{chunker} &&
564 $try->{chunker}->{status} eq 'fail') {
565 push @dump_failures, "$hostname $qdisk lev $try->{chunker}->{level} FAILED $try->{chunker}->{error}";
568 if ( exists $try->{taper}
569 && ( $try->{taper}->{status} eq 'fail'
570 || ( $try->{taper}->{status} eq 'partial'))) {
572 $flush = "FAILED" if exists $try->{dumper} && !exists $try->{chunker};
573 if ($flush ne "FLUSH" or !defined $try->{taper}->{failure_from}
574 or $try->{taper}->{failure_from} ne 'config') {
575 if ($try->{taper}->{status} eq 'partial') {
576 # if the error message is omitted, then the taper only got a partial
577 # dump from the dumper/chunker, rather than failing with a taper error
578 my $errmsg = $try->{taper}{error} || "successfully taped a partial dump";
579 $flush = "partial taper: $errmsg";
581 $flush .= " " . $try->{taper}{error};
584 push @dump_failures, "$hostname $qdisk lev $try->{taper}->{level} $flush";
589 # detect retried dumps
591 && exists $try->{dumper}
592 && ( $try->{dumper}->{status} eq "success"
593 || $try->{dumper}->{status} eq "strange")
594 && ( !exists $try->{chunker}
595 || $try->{chunker}->{status} eq "success")
596 && ( !exists $try->{taper}
597 || $try->{taper}->{status} eq "done")) {
598 push @dump_failures, "$hostname $qdisk lev $try->{dumper}->{level} was successfully retried";
602 # detect dumps re-flushed from holding
604 && !exists $try->{dumper}
605 && !exists $try->{chunker}
606 && exists $try->{taper}
607 && $try->{taper}->{status} eq "done") {
608 push @dump_failures, "$hostname $qdisk lev $try->{taper}->{level} was successfully re-flushed";
613 "$hostname $qdisk lev $try->{dumper}->{level} STRANGE (see below)"
614 if (defined $try->{dumper}
615 && $try->{dumper}->{status} eq 'strange');
619 push @failures, @fatal_failures, @error_failures, @missing_failures,
620 @driver_failures, @planner_failures, @dump_failures;
622 $self->print_if_def(\@failures, "FAILURE DUMP SUMMARY:");
623 $self->print_if_def(\@stranges, "STRANGE DUMP SUMMARY:");
633 # start at level 1 - don't include fulls
634 foreach my $i (1 .. (@$count - 1)) {
635 push @lc, "$i:$count->[$i]" if defined $count->[$i] and $count->[$i] > 0;
637 return join(' ', @lc);
643 my $fh = $self->{fh};
644 my $report = $self->{report};
650 Total Full Incr. Level:#
651 -------- -------- -------- --------
654 my $st_format = <<EOF;
655 @<<<<<<<<<<<<<<<<<<<<<<@>>>>>>>> @>>>>>>>> @>>>>>>>> @<<<<<<<<<<<<<<<<<<
658 # TODO: the hashes are a cheap fix. fix these.
659 my $full_stats = $self->{full_stats};
660 my $incr_stats = $self->{incr_stats};
661 my $total_stats = $self->{total_stats};
663 my ( $ttyp, $tt, $tapesize, $marksize );
664 $ttyp = getconf($CNF_TAPETYPE);
665 $tt = lookup_tapetype($ttyp) if $ttyp;
667 if ( $ttyp && $tt ) {
669 $tapesize = "".tapetype_getconf( $tt, $TAPETYPE_LENGTH );
670 $marksize = "".tapetype_getconf( $tt, $TAPETYPE_FILEMARK );
673 # these values should never be zero; assign defaults
674 $tapesize = 100 * 1024 * 1024 if !$tapesize;
675 $marksize = 1 * 1024 * 1024 if !$marksize;
681 "Estimate Time (hrs:min)",
682 hrmn( $total_stats->{planner_time} ),
688 "Run Time (hrs:min)",
689 hrmn( $total_stats->{total_time} ),
695 "Dump Time (hrs:min)",
696 hrmn( $total_stats->{dumper_time} ),
697 hrmn( $full_stats->{dumper_time} ),
698 hrmn( $incr_stats->{dumper_time} ),
705 sprintf( "%8.1f", $total_stats->{outsize}/1024 ),
706 sprintf( "%8.1f", $full_stats->{outsize}/1024 ),
707 sprintf( "%8.1f", $incr_stats->{outsize}/1024 ),
713 "Original Size (meg)",
714 sprintf( "%8.1f", $total_stats->{origsize}/1024 ),
715 sprintf( "%8.1f", $full_stats->{origsize}/1024 ),
716 sprintf( "%8.1f", $incr_stats->{origsize}/1024 ),
720 my $comp_size = sub {
722 return divzero(100 * $stats->{outsize}, $stats->{origsize});
727 "Avg Compressed Size (%)",
728 $comp_size->($total_stats),
729 $comp_size->($full_stats),
730 $comp_size->($incr_stats),
737 sprintf("%4d", $total_stats->{dumpdisk_count}),
738 sprintf("%4d", $full_stats->{dumpdisk_count}),
739 sprintf("%4d", $incr_stats->{dumpdisk_count}),
740 (has_incrementals($self->{dumpdisks}) ? by_level_count($self->{dumpdisks}) : "")
745 "Avg Dump Rate (k/s)",
746 divzero_wide( $total_stats->{outsize}, $total_stats->{dumper_time} ),
747 divzero_wide( $full_stats->{outsize}, $full_stats->{dumper_time} ),
748 divzero_wide( $incr_stats->{outsize}, $incr_stats->{dumper_time} ),
755 "Tape Time (hrs:min)",
756 hrmn( $total_stats->{taper_time} ),
757 hrmn( $full_stats->{taper_time} ),
758 hrmn( $incr_stats->{taper_time} ),
765 sprintf( "%8.1f", $total_stats->{tapesize}/1024 ),
766 sprintf( "%8.1f", $full_stats->{tapesize}/1024 ),
767 sprintf( "%8.1f", $incr_stats->{tapesize}/1024 ),
771 my $tape_usage = sub {
776 ($stat_ref->{tapedisk_count} + $stat_ref->{tapepart_count}) +
777 $stat_ref->{tapesize}
786 $tape_usage->($total_stats),
787 $tape_usage->($full_stats),
788 $tape_usage->($incr_stats),
793 my @incr_dle = @{$self->{tapedisks}};
794 foreach my $level (1 .. $#incr_dle) {
795 $nb_incr_dle += $incr_dle[$level];
800 $self->{tapedisks}[0] + $nb_incr_dle,
801 $self->{tapedisks}[0],
804 (has_incrementals($self->{tapedisks}))
805 ? by_level_count($self->{tapedisks})
810 # NOTE: only print out the per-level tapeparts if there are
811 # incremental tapeparts
815 sprintf("%4d", $total_stats->{tapepart_count}),
816 sprintf("%4d", $full_stats->{tapepart_count}),
817 sprintf("%4d", $incr_stats->{tapepart_count}),
819 $self->{tapeparts}[1] > 0
820 ? by_level_count($self->{tapeparts})
827 "Avg Tp Write Rate (k/s)",
828 divzero_wide( $total_stats->{tapesize}, $total_stats->{taper_time} ),
829 divzero_wide( $full_stats->{tapesize}, $full_stats->{taper_time} ),
830 divzero_wide( $incr_stats->{tapesize}, $incr_stats->{taper_time} ),
842 for ($a = 1; $a < @$array; $a+=1) {
843 return 1 if $array->[$a] > 0;
848 sub output_tape_stats
851 my $fh = $self->{fh};
852 my $report = $self->{report};
854 my $taper = $report->get_program_info("taper");
855 my $tapes = $taper->{tapes} || {};
856 my $tape_labels = $taper->{tape_labels} || [];
858 # if no tapes used, do nothing
859 return if (!@$tape_labels);
861 my $label_length = 19;
862 foreach my $label (@$tape_labels) {
863 $label_length = length($label) if length($label) > $label_length;
866 . '<' x ($label_length - 1)
867 . "@>>>> @>>>>>>>>>>> @>>>>> @>>>> @>>>>\n";
869 print $fh "USAGE BY TAPE:\n";
870 print $fh swrite($ts_format, "Label", "Time", "Size", "%", "DLEs", "Parts");
872 my $tapetype_name = getconf($CNF_TAPETYPE);
873 my $tapetype = lookup_tapetype($tapetype_name);
874 my $tapesize = "" . tapetype_getconf($tapetype, $TAPETYPE_LENGTH);
875 my $marksize = "" . tapetype_getconf($tapetype, $TAPETYPE_FILEMARK);
877 foreach my $label (@$tape_labels) {
879 my $tape = $tapes->{$label};
881 my $tapeused = $tape->{'kb'};
882 $tapeused += $marksize * (1 + $tape->{'files'});
887 hrmn($tape->{time}), # time
888 sprintf("%.0f", $self->tounits($tape->{kb})) . $self->{disp_unit}, # size
889 divzero(100 * $tapeused, $tapesize), # % usage
890 int($tape->{dle}), # # of dles
891 int($tape->{files}) # # of parts
900 ## takes no arguments
902 my $fh = $self->{fh};
903 my $errors = $self->{errors};
904 my $notes = $self->{notes};
905 my $report = $self->{report};
906 my $stranges = $report->{stranges};
908 my $disp_unit = $self->{disp_unit};
910 my @failed_dump_details;
911 my @strange_dump_details;
913 my @dles = $report->get_dles();
915 foreach my $dle_entry (@dles) {
917 my ($hostname, $disk) = @$dle_entry;
918 my $dle = $report->get_dle_info(@$dle_entry);
919 my $alldumps = $dle->{'dumps'} || {};
920 my $qdisk = quote_string($disk);
923 while( my ($timestamp, $tries) = each %$alldumps ) {
924 foreach my $try (@$tries) {
927 # check for failed dumper details
929 if (defined $try->{dumper}
930 && $try->{dumper}->{status} eq 'fail') {
932 push @failed_dump_details,
933 "/-- $hostname $qdisk lev $try->{dumper}->{level} FAILED $try->{dumper}->{error}",
934 @{ $try->{dumper}->{errors} },
937 if ($try->{dumper}->{nb_errors} > 100) {
938 my $nb = $try->{dumper}->{nb_errors} - 100;
940 push @failed_dump_details,
941 "$nb lines follow, see the corresponding log.* file for the complete list",
947 # check for strange dumper details
949 if (defined $try->{dumper}
950 && $try->{dumper}->{status} eq 'strange') {
952 push @strange_dump_details,
953 "/-- $hostname $qdisk lev $try->{dumper}->{level} STRANGE",
954 @{ $try->{dumper}->{stranges} },
957 if ($try->{dumper}->{nb_stranges} > 100) {
958 my $nb = $try->{dumper}->{nb_stranges} - 100;
959 push @strange_dump_details,
960 "$nb lines follow, see the corresponding log.* file for the complete list",
965 # note: copied & modified from calculate_stats.
967 exists $try->{dumper}
968 && exists $try->{taper}
969 && defined $try->{taper}->{kb}
970 && ( $try->{taper}{status} eq 'done'
971 || $try->{taper}{status} eq 'partial')
973 $outsize = $try->{taper}->{kb};
975 exists $try->{dumper}
976 && exists $try->{chunker}
977 && defined $try->{chunker}->{kb}
978 && ( $try->{chunker}{status} eq 'success'
979 || $try->{chunker}{status} eq 'partial')
981 $outsize = $try->{chunker}->{kb};
987 # check for bad estimates
990 if (exists $dle->{estimate} && defined $outsize) {
991 my $est = $dle->{estimate};
994 "big estimate: $hostname $qdisk $dle->{estimate}{level}",
995 sprintf(' est: %.0f%s out %.0f%s',
996 $est->{ckb}, $disp_unit, $outsize, $disp_unit)
997 if (defined $est->{'ckb'} && ($est->{ckb} * .9 > $outsize)
998 && ($est->{ckb} - $outsize > 1.0e5));
1002 $self->print_if_def(\@failed_dump_details, "FAILED DUMP DETAILS:");
1003 $self->print_if_def(\@strange_dump_details, "STRANGE DUMP DETAILS:");
1004 $self->print_if_def($notes, "NOTES:");
1012 ## takes no arguments
1014 my $fh = $self->{fh};
1015 my $report = $self->{report};
1019 sort { ( $a->[0] cmp $b->[0] ) || ( $a->[1] cmp $b->[1] ) }
1020 $report->get_dles();
1022 ## set the col_spec, which is the configuration for the summary
1024 my $col_spec = $self->set_col_spec();
1026 ## collect all the output line specs (see get_summary_info)
1027 my @summary_linespecs = ();
1028 foreach my $dle (@dles) {
1029 push @summary_linespecs, $self->get_summary_info($dle, $report, $col_spec);
1032 # shift off the first element of each tuple
1033 my @summary_linedata =
1034 map { my @x = @$_; shift @x; [ @x ] } @summary_linespecs;
1036 ## get the summary format. this is based on col_spec, but may
1037 ## expand maxwidth columns if they have large fields. Note that
1038 ## this modifies $col_spec in place. Ordering is important: the summary
1039 ## format must be generated before the others.
1040 my $title_format = get_summary_format($col_spec, 'title', @summary_linedata);
1041 my $summary_format = get_summary_format($col_spec, 'full', @summary_linedata);
1042 my $missing_format = get_summary_format($col_spec, 'missing', @summary_linedata);
1043 my $noflush_format = get_summary_format($col_spec, 'noflush', @summary_linedata);
1044 my $nodump_PARTIAL_format = get_summary_format($col_spec, 'nodump-PARTIAL', @summary_linedata);
1045 my $nodump_FAILED_format = get_summary_format($col_spec, 'nodump-FAILED', @summary_linedata);
1046 my $nodump_FLUSH_format = get_summary_format($col_spec, 'nodump-FLUSH', @summary_linedata);
1047 my $skipped_format = get_summary_format($col_spec, 'skipped', @summary_linedata);
1049 ## print the header names
1051 $col_spec->[0]->[COLSPEC_WIDTH] +
1052 $col_spec->[1]->[COLSPEC_PRE_SPACE] +
1053 $col_spec->[1]->[COLSPEC_WIDTH] +
1054 $col_spec->[2]->[COLSPEC_PRE_SPACE] +
1055 $col_spec->[2]->[COLSPEC_WIDTH];
1057 $col_spec->[3]->[COLSPEC_WIDTH] +
1058 $col_spec->[4]->[COLSPEC_PRE_SPACE] +
1059 $col_spec->[4]->[COLSPEC_WIDTH] +
1060 $col_spec->[5]->[COLSPEC_PRE_SPACE] +
1061 $col_spec->[5]->[COLSPEC_WIDTH] +
1062 $col_spec->[6]->[COLSPEC_PRE_SPACE] +
1063 $col_spec->[6]->[COLSPEC_WIDTH] +
1064 $col_spec->[7]->[COLSPEC_PRE_SPACE] +
1065 $col_spec->[7]->[COLSPEC_WIDTH];
1067 $col_spec->[8]->[COLSPEC_WIDTH] +
1068 $col_spec->[9]->[COLSPEC_PRE_SPACE] +
1069 $col_spec->[9]->[COLSPEC_WIDTH];
1072 ## use perl's ancient formatting support for the header, since we get free string
1074 my $summary_header_format =
1075 ' ' x ($col_spec->[0]->[COLSPEC_PRE_SPACE] +
1076 $hdl + $col_spec->[4]->[COLSPEC_PRE_SPACE])
1077 . '@' . '|' x ($ds - 1)
1078 . ' ' x $col_spec->[9]->[COLSPEC_PRE_SPACE]
1079 . '@'. '|' x ($ts - 1) . "\n";
1080 my $summary_header = swrite($summary_header_format, "DUMPER STATS", "TAPER STATS");
1082 my $summary_dashes =
1083 ' ' x $col_spec->[0]->[COLSPEC_PRE_SPACE]
1085 . ' ' x $col_spec->[4]->[COLSPEC_PRE_SPACE]
1087 . ' ' x $col_spec->[9]->[COLSPEC_PRE_SPACE]
1090 print $fh "DUMP SUMMARY:\n";
1091 print $fh $summary_header;
1092 print $fh sprintf($title_format, map { $_->[COLSPEC_TITLE] } @$col_spec);
1093 print $fh $summary_dashes;
1095 ## write out each output line
1096 for (@summary_linespecs) {
1097 my ($type, @data) = @$_;
1098 if ($type eq 'full') {
1099 print $fh sprintf($summary_format, @data);
1100 } elsif ($type eq 'nodump-PARTIAL') {
1101 print $fh sprintf($nodump_PARTIAL_format, @data);
1102 } elsif ($type eq 'nodump-FAILED') {
1103 print $fh sprintf($nodump_FAILED_format, @data);
1104 } elsif ($type eq 'nodump-FLUSH') {
1105 print $fh sprintf($nodump_FLUSH_format, @data);
1106 } elsif ($type eq 'missing') {
1107 print $fh sprintf($missing_format, @data[0..2]);
1108 } elsif ($type eq 'noflush') {
1109 print $fh sprintf($noflush_format, @data[0..2]);
1110 } elsif ($type eq 'skipped') {
1111 print $fh sprintf($skipped_format, @data[0..2]);
1119 ## output_summary helper functions. mostly for formatting, but some
1120 ## for data collection. Returns an 12-tuple matching one of
1122 ## ('full', host, disk, level, orig, out, comp%, dumptime, dumprate,
1123 ## tapetime, taperate, taperpartial)
1124 ## ('missing', host, disk, '' ..) # MISSING -----
1125 ## ('noflush', host, disk, '' ..) # NO FILE TO FLUSH ------
1126 ## ('nodump-$msg', host, disk, level, '', out, '--', '',
1127 ## '', tapetime, taperate, taperpartial) # ... {FLUSH|FAILED|PARTIAL} ...
1128 ## ('skipped', host, disk, '' ..) # SKIPPED -----
1130 ## the taperpartial column is not covered by the columnspec, and "hangs off"
1131 ## the right side. It's usually empty, but set to " PARTIAL" when the taper
1132 ## write was partial
1134 sub get_summary_info
1137 my ( $dle, $report, $col_spec ) = @_;
1138 my ( $hostname, $disk ) = @$dle;
1141 my $dle_info = $report->get_dle_info(@$dle);
1143 my $tail_quote_trunc = sub {
1144 my ($str, $len) = @_;
1146 my $q_str = quote_string($str);
1149 if (length($q_str) > $len) {
1151 $qt_str = substr($q_str, length($q_str) - $len, $len);
1152 if ($q_str eq $str) {
1155 $qt_str =~ s{^..}{"-};
1165 ($col_spec->[1]->[COLSPEC_MAXWIDTH])
1166 ? quote_string($disk)
1167 : $tail_quote_trunc->($disk, $col_spec->[1]->[COLSPEC_WIDTH]);
1169 my $alldumps = $dle_info->{'dumps'};
1170 if ($dle_info->{'planner'} &&
1171 $dle_info->{'planner'}->{'status'} eq 'fail') {
1173 push @rv, 'nodump-FAILED';
1174 push @rv, $hostname;
1175 push @rv, $disk_out;
1176 push @rv, ("",) x 9;
1178 } elsif ($dle_info->{'planner'} &&
1179 $dle_info->{'planner'}->{'status'} eq 'skipped') {
1181 push @rv, 'skipped';
1182 push @rv, $hostname;
1183 push @rv, $disk_out;
1184 push @rv, ("",) x 8;
1186 } elsif (keys %{$alldumps} == 0) {
1188 push @rv, $report->get_flag("amflush_run")? 'noflush' : 'missing';
1189 push @rv, $hostname;
1190 push @rv, $disk_out;
1191 push @rv, ("",) x 8;
1195 while( my ($timestamp, $tries) = each %$alldumps ) {
1196 my $last_try = $tries->[-1];
1198 exists $last_try->{taper} ? $last_try->{taper}{level}
1199 : exists $last_try->{chunker} ? $last_try->{chunker}{level}
1200 : $last_try->{dumper}{level};
1202 my $orig_size = undef;
1204 # find the try with the successful dumper entry
1206 foreach my $try (@$tries) {
1207 if ( exists $try->{dumper}
1208 && exists $try->{dumper}{status}
1209 && ( $try->{dumper}{status} eq "success"
1210 || $try->{dumper}{status} eq "strange")) {
1211 $dumper = $try->{dumper};
1215 $orig_size = $dumper->{orig_kb}
1218 my ( $out_size, $dump_time, $dump_rate, $tape_time, $tape_rate ) = (0) x 5;
1219 my ($dumper_status) = "";
1220 my $saw_dumper = 0; # no dumper will mean this was a flush
1221 my $taper_partial = 0; # was the last taper run partial?
1223 ## Use this loop to set values
1224 foreach my $try ( @$tries ) {
1226 ## find the outsize for the output summary
1229 exists $try->{taper}
1230 && ( $try->{taper}{status} eq "done"
1231 || $try->{taper}{status} eq "part+partial" )
1234 $orig_size = $try->{taper}{orig_kb} if !defined($orig_size);
1235 $out_size = $try->{taper}{kb};
1236 $tape_time = $try->{taper}{sec};
1237 $tape_rate = $try->{taper}{kps};
1238 } elsif ( exists $try->{taper}
1239 && ( $try->{taper}{status} eq "partial" ) ) {
1242 $orig_size = $try->{taper}{orig_kb} if !defined($orig_size);
1243 $out_size = $try->{taper}{kb};
1244 $tape_time = $try->{taper}{sec} if !$tape_time;
1245 $tape_rate = $try->{taper}{kps} if !$tape_rate;
1246 } elsif (exists $try->{taper} && ( $try->{taper}{status} eq "fail")) {
1252 exists $try->{chunker}
1253 && ( $try->{chunker}{status} eq "success"
1254 || $try->{chunker}{status} eq "partial" )
1256 $out_size = $try->{chunker}{kb};
1260 exists $try->{dumper}) {
1261 $out_size = $try->{dumper}{kb};
1264 if ( exists $try->{dumper}) {
1266 $dumper_status = $try->{dumper}{status};
1269 ## find the dump time
1270 if ( exists $try->{dumper}
1271 && exists $try->{dumper}{status}
1272 && ( $try->{dumper}{status} eq "success"
1273 || $try->{dumper}{status} eq "strange")) {
1275 $dump_time = $try->{dumper}{sec};
1276 $dump_rate = $try->{dumper}{kps};
1280 # sometimes the driver logs an orig_size of -1, which makes the
1281 # compression percent very large and negative
1282 $orig_size = 0 if ($orig_size < 0);
1284 # pre-format the compression column, with '--' replacing 100% (i.e.,
1287 if (!defined $orig_size || $orig_size == $out_size) {
1288 $compression = '--';
1291 divzero_col((100 * $out_size), $orig_size, $col_spec->[5]);
1294 ## simple formatting macros
1296 my $fmt_col_field = sub {
1297 my ( $column, $data ) = @_;
1300 $col_spec->[$column]->[COLSPEC_FORMAT],
1301 $col_spec->[$column]->[COLSPEC_WIDTH],
1302 $col_spec->[$column]->[COLSPEC_PREC], $data
1306 my $format_space = sub {
1307 my ( $column, $data ) = @_;
1309 return sprintf("%*s",$col_spec->[$column]->[COLSPEC_WIDTH], $data);
1314 if ( !$orig_size && !$out_size && (!defined($tape_time) || !$tape_time)) {
1315 push @rv, $report->get_flag("amflush_run")? 'noflush' : 'missing';
1316 push @rv, $hostname;
1317 push @rv, $disk_out;
1318 push @rv, ("",) x 8;
1319 } elsif ($saw_dumper and ($dumper_status eq 'success' or $dumper_status eq 'strange')) {
1321 push @rv, $hostname;
1322 push @rv, $disk_out;
1323 push @rv, $fmt_col_field->(2, $level);
1324 push @rv, $orig_size ? $fmt_col_field->(3, $self->tounits($orig_size)) : '';
1325 push @rv, $out_size ? $fmt_col_field->(4, $self->tounits($out_size)) : '';
1326 push @rv, $compression;
1327 push @rv, $dump_time ? $fmt_col_field->(6, mnsc($dump_time)) : "PARTIAL";
1328 push @rv, $dump_rate ? $fmt_col_field->(7, $dump_rate) : "";
1329 push @rv, $fmt_col_field->(8,
1330 (defined $tape_time) ?
1331 $tape_time ? mnsc($tape_time) : ""
1333 push @rv, (defined $tape_rate) ?
1335 $fmt_col_field->(9, $tape_rate)
1336 : $format_space->(9, "")
1337 : $format_space->(9, "FAILED");
1338 push @rv, $taper_partial? " PARTIAL" : ""; # column 10
1340 my $message = $saw_dumper?
1341 ($dumper_status eq 'failed') ? 'FAILED' : 'PARTIAL'
1343 push @rv, "nodump-$message";
1344 push @rv, $hostname;
1345 push @rv, $disk_out;
1346 push @rv, $fmt_col_field->(2, $level);
1347 push @rv, $orig_size ? $fmt_col_field->(4, $self->tounits($orig_size)) :'';
1348 push @rv, $out_size ? $fmt_col_field->(4, $self->tounits($out_size)) : '';
1349 push @rv, $compression;
1352 push @rv, $fmt_col_field->(8,
1353 (defined $tape_time) ?
1354 $tape_time ? mnsc($tape_time) : ""
1356 push @rv, (defined $tape_rate) ?
1358 $fmt_col_field->(9, $tape_rate)
1359 : $format_space->(9, "")
1360 : $format_space->(9, "FAILED");
1361 push @rv, $taper_partial? " PARTIAL" : "";
1368 sub get_summary_format
1370 my ($col_spec, $type, @summary_lines) = @_;
1371 my @col_format = ();
1373 if ($type eq 'full' || $type eq 'title') {
1374 foreach my $i ( 0 .. ( @$col_spec - 1 ) ) {
1376 get_summary_col_format( $i, $col_spec->[$i],
1377 map { $_->[$i] } @summary_lines );
1380 # first two columns are the same
1381 foreach my $i ( 0 .. 1 ) {
1383 get_summary_col_format( $i, $col_spec->[$i],
1384 map { $_->[$i] } @summary_lines );
1387 # some of these have a lovely text rule, just to be difficult
1389 $col_spec->[3]->[COLSPEC_WIDTH] +
1390 $col_spec->[4]->[COLSPEC_PRE_SPACE] +
1391 $col_spec->[4]->[COLSPEC_WIDTH] +
1392 $col_spec->[5]->[COLSPEC_PRE_SPACE] +
1393 $col_spec->[5]->[COLSPEC_WIDTH] +
1394 $col_spec->[6]->[COLSPEC_PRE_SPACE] +
1395 $col_spec->[6]->[COLSPEC_WIDTH] +
1396 $col_spec->[7]->[COLSPEC_PRE_SPACE] +
1397 $col_spec->[7]->[COLSPEC_WIDTH] +
1398 $col_spec->[8]->[COLSPEC_PRE_SPACE] +
1399 $col_spec->[8]->[COLSPEC_WIDTH] +
1400 $col_spec->[9]->[COLSPEC_PRE_SPACE] +
1401 $col_spec->[9]->[COLSPEC_WIDTH];
1403 if ($type eq 'missing') {
1404 # add a blank level column and the space for the origkb column
1405 push @col_format, ' ' x $col_spec->[2]->[COLSPEC_PRE_SPACE];
1406 push @col_format, ' ' x $col_spec->[2]->[COLSPEC_WIDTH];
1407 push @col_format, ' ' x $col_spec->[3]->[COLSPEC_PRE_SPACE];
1408 my $str = "MISSING ";
1409 $str .= '-' x ($rulewidth - length($str));
1410 push @col_format, $str;
1411 } elsif ($type eq 'noflush') {
1412 # add a blank level column and the space for the origkb column
1413 push @col_format, ' ' x $col_spec->[2]->[COLSPEC_PRE_SPACE];
1414 push @col_format, ' ' x $col_spec->[2]->[COLSPEC_WIDTH];
1415 push @col_format, ' ' x $col_spec->[3]->[COLSPEC_PRE_SPACE];
1417 my $str = "NO FILE TO FLUSH ";
1418 $str .= '-' x ($rulewidth - length($str));
1419 push @col_format, $str;
1420 } elsif ($type =~ /^nodump-(.*)$/) {
1423 # nodump has level, origkb, outkb, and comp% although origkb is usually blank and
1425 foreach my $i ( 2 .. 5 ) {
1427 get_summary_col_format( $i, $col_spec->[$i],
1428 map { $_->[$i] } @summary_lines );
1431 # and then the message is centered across columns 6 and 7, which are both blank
1432 push @col_format, ' ' x $col_spec->[6]->[COLSPEC_PRE_SPACE];
1434 $col_spec->[6]->[COLSPEC_WIDTH] +
1435 $col_spec->[7]->[COLSPEC_PRE_SPACE] +
1436 $col_spec->[7]->[COLSPEC_WIDTH];
1438 my $str = ' ' x (($width - length($msg))/2);
1440 $str .= ' ' x ($width - length($str));
1441 push @col_format, $str;
1442 push @col_format, "%s%s"; # consume empty columns 6 and 7
1444 # and finally columns 8 and 9 as usual
1445 foreach my $i ( 8 .. 9 ) {
1447 get_summary_col_format( $i, $col_spec->[$i],
1448 map { $_->[$i] } @summary_lines );
1450 } elsif ($type eq 'skipped') {
1451 # add a blank level column and the space for the origkb column
1452 push @col_format, ' ' x $col_spec->[2]->[COLSPEC_PRE_SPACE];
1453 push @col_format, ' ' x $col_spec->[2]->[COLSPEC_WIDTH];
1454 push @col_format, ' ' x $col_spec->[3]->[COLSPEC_PRE_SPACE];
1455 my $str = "SKIPPED ";
1456 $str .= '-' x ($rulewidth - length($str));
1457 push @col_format, $str;
1461 # and format the hidden 10th column. This is not part of the columnspec,
1462 # so its width is not counted in any of the calculations here.
1463 push @col_format, "%s" if $type ne 'title';
1465 return join( "", @col_format ) . "\n";
1468 sub get_summary_col_format
1470 my ( $i, $col, @entries ) = @_;
1472 my $col_width = $col->[COLSPEC_WIDTH];
1473 my $left_align = ($i == 0 || $i == 1); # first 2 cols left-aligned
1474 my $limit_width = ($i == 0 || $i == 1); # and not allowed to overflow
1476 ## if necessary, resize COLSPEC_WIDTH to the maximum widht
1478 if ($col->[COLSPEC_MAXWIDTH]) {
1480 push @entries, $col->[COLSPEC_TITLE];
1481 my $strmax = max( map { length $_ } @entries );
1482 $col_width = max($strmax, $col_width);
1483 # modify the spec in place, so the headers and
1484 # whatnot all add up .. yuck!
1485 $col->[COLSPEC_WIDTH] = $col_width;
1488 # put together a "%s" format for this column
1489 my $rv = ' ' x $col->[COLSPEC_PRE_SPACE]; # space on left
1491 $rv .= '-' if $left_align;
1493 $rv .= ".$col_width" if $limit_width;
1497 ## col_spec functions. I want to deprecate this stuff so bad it hurts.
1502 my $report = $self->{report};
1503 my $disp_unit = $self->{disp_unit};
1505 $self->{col_spec} = [
1506 [ "HostName", 0, 12, 12, 0, "%-*.*s", "HOSTNAME" ],
1507 [ "Disk", 1, 11, 11, 0, "%-*.*s", "DISK" ],
1508 [ "Level", 1, 1, 1, 0, "%*.*d", "L" ],
1509 [ "OrigKB", 1, 7, 0, 1, "%*.*f", "ORIG-" . $disp_unit . "B" ],
1510 [ "OutKB", 1, 7, 0, 1, "%*.*f", "OUT-" . $disp_unit . "B" ],
1511 [ "Compress", 1, 6, 1, 1, "%*.*f", "COMP%" ],
1512 [ "DumpTime", 1, 7, 7, 1, "%*.*s", "MMM:SS" ],
1513 [ "DumpRate", 1, 6, 1, 1, "%*.*f", "KB/s" ],
1514 [ "TapeTime", 1, 6, 6, 1, "%*.*s", "MMM:SS" ],
1515 [ "TapeRate", 1, 6, 1, 1, "%*.*f", "KB/s" ]
1518 $self->apply_col_spec_override();
1519 return $self->{col_spec};
1522 sub apply_col_spec_override
1525 my $col_spec = $self->{col_spec};
1527 my %col_spec_override = read_col_spec_override();
1529 foreach my $col (@$col_spec) {
1530 if ( my $col_override = $col_spec_override{ $col->[COLSPEC_NAME] } ) {
1532 my $override_col_val_if_def = sub {
1533 my ( $field, $or_num ) = @_;
1534 if ( defined $col_override->[$or_num]
1535 && !( $col_override->[$or_num] eq "" ) ) {
1536 $col->[$field] = $col_override->[$or_num];
1540 $override_col_val_if_def->( COLSPEC_PRE_SPACE, 0 );
1541 $override_col_val_if_def->( COLSPEC_WIDTH, 1 );
1542 $override_col_val_if_def->( COLSPEC_PREC, 2 );
1543 $override_col_val_if_def->( COLSPEC_MAXWIDTH, 3 );
1548 sub read_col_spec_override
1550 ## takes no arguments
1551 my $col_spec_str = getconf($CNF_COLUMNSPEC) || return;
1552 my %col_spec_override = ();
1554 foreach (split(",", $col_spec_str)) {
1556 $_ =~ m/^(\w+) # field name
1557 =([-:\d]+) # field values
1559 or die "error: malformed columnspec string:$col_spec_str";
1562 my @field_values = split ':', $2;
1565 die "error: malformed columnspec string:$col_spec_str"
1566 if (@field_values > 3);
1568 # all values *should* be in the right place. If not enough
1569 # were given, pad the array.
1570 push @field_values, "" while (@field_values < 4);
1572 # if the second value is negative, that means MAXWIDTH=1, so
1573 # sort that out now. Yes, this is pretty ugly. Imagine this in C!
1574 if ($field_values[1] ne '') {
1575 if ($field_values[1] =~ /^-/) {
1576 $field_values[1] =~ s/^-//;
1577 $field_values[3] = 1;
1579 $field_values[3] = 0;
1583 $col_spec_override{$field} = \@field_values;
1586 return %col_spec_override;
1591 my ($self, $msgs, $header) = @_;
1592 my $fh = $self->{fh};
1594 @$msgs or return; # do not print section if no messages
1596 print $fh "$header\n";
1597 foreach my $msg (@$msgs) {
1598 print $fh " $msg\n";