1 # Copyright (c) 2010-2012 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;
29 use Amanda::Config qw(:getconf config_dir_relative);
30 use Amanda::Util qw(:constants quote_string );
33 use Amanda::Debug qw( debug );
34 use Amanda::Util qw( quote_string );
38 ## constants that define the column specification output format.
40 use constant COLSPEC_NAME => 0; # column name; used internally
41 use constant COLSPEC_PRE_SPACE => 1; # prefix spaces
42 use constant COLSPEC_WIDTH => 2; # column width
43 use constant COLSPEC_PREC => 3; # post-decimal precision
44 use constant COLSPEC_MAXWIDTH => 4; # resize if set
45 use constant COLSPEC_FORMAT => 5; # sprintf format
46 use constant COLSPEC_TITLE => 6; # column title
48 use constant PROGRAM_ORDER =>
49 qw(amdump planner amflush amvault driver dumper chunker taper reporter);
60 : ( ($q = $a / $b) > 99999.95 ) ? "#####"
61 : ( $q > 999.95 ) ? sprintf( "%5.0f", $q )
62 : sprintf( "%5.1f", $q );
71 : ( ($q = $a / $b) > 9999999.95 ) ? "#######"
72 : ( $q > 99999.95 ) ? sprintf( "%7.0f", $q )
73 : sprintf( "%7.1f", $q );
78 my ( $a, $b, $col ) = @_;
81 : sprintf( $col->[5], $col->[2], $col->[3], ( $a / $b ) );
86 my ( $format, @args ) = @_;
88 formline( $format, @args );
94 my ( $max, @args ) = @_; # first element starts as max
96 foreach my $elt (@args) {
97 $max = $elt if $elt > $max;
104 my ( $min, @args ) = @_; # first element starts as min
106 foreach my $elt (@args) {
107 $min = $elt if $elt < $min;
115 $sec += 30; # round up
116 my ( $hr, $mn ) = ( int( $sec / ( 60 * 60 ) ), int( $sec / 60 ) % 60 );
117 return sprintf( '%d:%02d', $hr, $mn );
123 $sec += 0.5; # round up
124 my ( $mn, $sc ) = ( int( $sec / (60) ), int( $sec % 60 ) );
125 return sprintf( '%d:%02d', $mn, $sc );
130 # return $val/$unit_divisor as a a floating-point number
133 my ($val, %params) = @_;
135 return $params{'zero'} if ($val == 0 and exists $params{'zero'});
137 # $orig_size and $out_size are bigints, which must be stringified to cast
138 # them to floats. We need floats, because they round nicely. This is
139 # ugly and hard to track down.
140 my $flval = $val.".0";
141 my $flunit = $self->{'unit_div'}.".0";
142 return $flval / $flunit;
149 my ($class, $report, $fh, $config_name, $logfname) = @_;
154 config_name => $config_name,
155 logfname => $logfname,
158 disp_unit => getconf($CNF_DISPLAYUNIT),
159 unit_div => getconf_unit_divisor(),
165 dumpdisks => [ 0, 0 ], # full_count, incr_count
166 tapedisks => [ 0, 0 ],
167 tapeparts => [ 0, 0 ],
170 if (defined $report) {
172 my (@errors, @stranges, @notes);
175 map { @{ $report->get_program_info($_, "errors", []) }; }
177 ## prepend program name to notes lines.
178 foreach my $program (PROGRAM_ORDER) {
180 map { "$program: $_" }
181 @{ $report->get_program_info($program, "notes", []) };
184 $self->{errors} = \@errors;
185 $self->{notes} = \@notes;
195 my $fh = $self->{fh};
196 my $report = $self->{report};
198 # TODO: the hashes are a cheap fix. fix these.
199 my @dles = $report->get_dles();
200 my $full_stats = $self->{full_stats};
201 my $incr_stats = $self->{incr_stats};
202 my $total_stats = $self->{total_stats};
203 my $dumpdisks = $self->{dumpdisks};
204 my $tapedisks = $self->{tapedisks};
205 my $tapeparts = $self->{tapeparts};
207 ## initialize all relevant fields to 0
208 map { $incr_stats->{$_} = $full_stats->{$_} = 0; }
209 qw/dumpdisk_count tapedisk_count tapepart_count outsize origsize
210 tapesize coutsize corigsize taper_time dumper_time/;
212 foreach my $dle_entry (@dles) {
214 # $dle_entry = [$hostname, $disk]
215 my $dle = $report->get_dle_info(@$dle_entry);
216 my $alldumps = $dle->{'dumps'};
218 while( my ($timestamp, $tries) = each %$alldumps ) {
219 foreach my $try ( @$tries ) {
221 my $level = exists $try->{dumper} ? $try->{dumper}{'level'} :
222 exists $try->{taper} ? $try->{taper}{'level'} :
224 my $stats = ($level > 0) ? $incr_stats : $full_stats;
226 # compute out size, skipping flushes (tries without a dumper run)
228 if (exists $try->{dumper}
229 && exists $try->{chunker} && defined $try->{chunker}->{kb}
230 && ( $try->{chunker}{status} eq 'success'
231 || $try->{chunker}{status} eq 'partial')) {
232 $outsize = $try->{chunker}->{kb};
233 } elsif (exists $try->{dumper}
234 && exists $try->{taper} && defined $try->{taper}->{kb}
235 && ( $try->{taper}{status} eq 'done'
236 || $try->{taper}{status} eq 'partial')) {
237 $outsize = $try->{taper}->{kb};
240 # compute orig size, again skipping flushes
242 if ( exists $try->{dumper}
243 && ( $try->{dumper}{status} eq 'success'
244 || $try->{dumper}{status} eq 'strange')) {
246 $origsize = $try->{dumper}{orig_kb};
247 $stats->{dumper_time} += $try->{dumper}{sec};
248 $stats->{dumpdisk_count}++; # count this as a dumped filesystem
249 $dumpdisks->[$try->{dumper}{'level'}]++; #by level count
250 } elsif (exists $try->{dumper}
251 && exists $try->{taper} && defined $try->{taper}->{kb}
252 && ( $try->{taper}{status} eq 'done'
253 || $try->{taper}{status} eq 'partial')) {
254 # orig_kb doesn't always exist (older logfiles)
255 if ($try->{taper}->{orig_kb}) {
256 $origsize = $try->{taper}->{orig_kb};
260 if ( exists $try->{taper}
261 && ( $try->{taper}{status} eq 'done'
262 || $try->{taper}{status} eq 'partial')) {
264 $stats->{tapesize} += $try->{taper}{kb};
265 $stats->{taper_time} += $try->{taper}{sec};
266 $stats->{tapepart_count} += @{ $try->{taper}{parts} }
267 if $try->{taper}{parts};
268 $stats->{tapedisk_count}++;
270 $tapedisks->[ $try->{taper}{level} ]++; #by level count
271 $tapeparts->[$try->{taper}{level}] += @{ $try->{taper}{parts} }
272 if $try->{taper}{parts};
275 # add those values to the stats
276 $stats->{'origsize'} += $origsize;
277 $stats->{'outsize'} += $outsize;
279 # if the sizes differ, then we have a compressed dump, so also add it to
281 $stats->{'corigsize'} += $origsize;
282 $stats->{'coutsize'} += $outsize;
287 %$total_stats = map { $_ => $incr_stats->{$_} + $full_stats->{$_} }
290 $total_stats->{planner_time} =
291 $report->get_program_info("planner", "time", 0);
293 if ($report->get_flag("got_finish")) {
294 $total_stats->{total_time} =
295 $report->get_program_info("driver", "time", 0)
296 || $report->get_program_info("amflush", "time", 0);
298 $total_stats->{total_time} =
299 $total_stats->{taper_time} + $total_stats->{planner_time};
302 $total_stats->{idle_time} =
303 ( $total_stats->{total_time} - $total_stats->{planner_time} ) -
304 $total_stats->{taper_time};
306 # TODO: tape info is very sparse. There either needs to be a
307 # function that collects and fills in tape info post-processing in
308 # Amanda::Report, or it needs to be done here.
312 sub print_human_amreport
314 my ( $self, $fh ) = @_;
317 || confess "error: no file handle given to print_human_amreport\n";
319 ## collect statistics
320 $self->calculate_stats();
322 ## print the basic info header
323 $self->print_header();
325 ## print out statements about past and predicted tape usage
326 $self->output_tapeinfo();
328 ## print out error messages from the run
329 $self->output_error_summaries();
331 ## print out aggregated statistics for the whole dump
332 $self->output_stats();
334 ## print out statistics for each tape used
335 $self->output_tape_stats();
337 ## print out all errors & comments
338 $self->output_details();
340 ## print out dump statistics per DLE
341 $self->output_summary();
345 "(brought to you by Amanda version $Amanda::Constants::VERSION)\n";
354 my $report = $self->{report};
355 my $fh = $self->{fh};
356 my $config_name = $self->{config_name};
358 my $hostname = $report->{hostname};
359 my $org = getconf($CNF_ORG);
361 # TODO: this should be a shared method somewhere
362 my $timestamp = $report->get_timestamp();
363 my ($year, $month, $day) = ($timestamp =~ m/^(\d\d\d\d)(\d\d)(\d\d)/);
364 my $date = POSIX::strftime('%B %e, %Y', 0, 0, 0, $day, $month - 1, $year - 1900);
365 $date =~ s/ / /g; # get rid of intervening space
367 print $fh "*** THE DUMPS DID NOT FINISH PROPERLY!\n\n"
368 unless ($report->{flags}{got_finish});
370 my $header_format = <<EOF;
371 @<<<<<<<: @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<...
375 print $fh swrite($header_format, "Hostname", $hostname);
376 print $fh swrite($header_format, "Org", $org);
377 print $fh swrite($header_format, "Config", $config_name);
378 print $fh swrite($header_format, "Date", $date);
388 my $report = $self->{report};
389 my $fh = $self->{fh};
390 my $logfname = $self->{logfname};
392 my $taper = $report->get_program_info("taper");
393 my $tapes = $taper->{tapes} || {};
394 my $tape_labels = $taper->{tape_labels} || [];
396 my %full_stats = %{ $self->{full_stats} };
397 my %incr_stats = %{ $self->{incr_stats} };
398 my %total_stats = %{ $self->{total_stats} };
400 if (@$tape_labels > 0) {
402 # slightly different sentence depending on the run type
404 if ($report->get_flag("amflush_run")) {
405 $tapelist_str = "The dumps were flushed ";
406 } elsif ($report->get_flag("amvault_run")) {
407 $tapelist_str = "The dumps were vaulted ";
409 $tapelist_str = "These dumps were ";
411 $tapelist_str .= (@$tape_labels > 1) ? "to tapes " : "to tape ";
412 $tapelist_str .= join(", ", @$tape_labels) . ".\n";
413 print $fh $tapelist_str;
417 $report->get_program_info("taper", "tape_error", undef)) {
419 if ($report->get_program_info("taper", "failure_from", undef) eq "config") {
420 # remove leading [ and trailling ]
421 $tape_error =~ s/^\[//;
422 $tape_error =~ s/\]$//;
423 print $fh "Not using all tapes because $tape_error.\n";
425 print $fh "*** A TAPE ERROR OCCURRED: $tape_error.\n";
427 #$tape_error =~ s{^no-tape }{};
430 ## if this is a historical report, do not generate holding disk
431 ## information. If this dump is the most recent, output holding
433 if ($report->get_flag("historical")) {
434 print $fh "Some dumps may have been left in the holding disk.\n\n"
435 if $report->get_flag("degraded_mode")
439 my @holding_list = Amanda::Holding::get_files_for_flush();
441 foreach my $holding_file (@holding_list) {
442 $h_size += (0 + Amanda::Holding::file_size($holding_file, 1));
446 sprintf("%.0f%s", $self->tounits($h_size), $self->{disp_unit});
450 "There are $h_size_u of dumps left in the holding disk.\n";
452 (getconf($CNF_AUTOFLUSH))
453 ? print $fh "They will be flushed on the next run.\n\n"
454 : print $fh "Run amflush to flush them to tape.\n\n";
456 } elsif ($report->get_flag("degraded_mode")) {
457 print $fh "No dumps are left in the holding disk.\n\n";
462 my $run_tapes = getconf($CNF_RUNTAPES);
466 ? print $fh "The next $run_tapes tapes Amanda expects to use are: "
467 : print $fh "The next tape Amanda expects to use is: ";
471 foreach my $i ( 0 .. ( $run_tapes - 1 ) ) {
473 if ( my $tape_label =
474 Amanda::Tapelist::get_last_reusable_tape_label($i) ) {
477 print $fh ", " if !$first;
478 print $fh "$nb_new_tape new tape"
479 . ( $nb_new_tape > 1 ? "s" : "" );
494 print $fh ", " if !$first;
495 print $fh "$nb_new_tape new tape"
496 . ( $nb_new_tape > 1 ? "s" : "" );
500 my $new_tapes = Amanda::Tapelist::list_new_tapes(getconf($CNF_RUNTAPES));
501 print $fh "$new_tapes\n" if $new_tapes;
506 sub output_error_summaries
509 my $errors = $self->{errors};
510 my $report = $self->{report};
512 my @dles = $report->get_dles();
514 my @fatal_failures = ();
515 my @error_failures = ();
516 my @missing_failures = ();
517 my @driver_failures = ();
518 my @planner_failures = ();
519 my @dump_failures = ();
522 foreach my $program (PROGRAM_ORDER) {
524 push @fatal_failures,
525 map { "$program: FATAL $_" }
526 @{ $report->get_program_info($program, "fatal", []) };
527 push @error_failures,
528 map { "$program: ERROR $_" }
529 @{ $report->get_program_info($program, "errors", []) };
532 foreach my $dle_entry (@dles) {
534 my ($hostname, $disk) = @$dle_entry;
535 my $alldumps = $report->get_dle_info(@$dle_entry, "dumps");
536 my $dle = $report->get_dle_info($hostname, $disk);
537 my $qdisk = quote_string($disk);
539 if ($report->get_flag('results_missing') and
540 !defined($alldumps->{$report->{run_timestamp}}) and
543 push @missing_failures, "$hostname $qdisk RESULTS MISSING";
546 if ( exists $dle->{driver}
547 && exists $dle->{driver}->{error}) {
548 push @driver_failures, "$hostname $qdisk lev $dle->{driver}->{level} FAILED $dle->{driver}->{error}";
551 if ( exists $dle->{planner}
552 && exists $dle->{planner}->{error}) {
553 push @planner_failures, "$hostname $qdisk lev $dle->{planner}->{level} FAILED $dle->{planner}->{error}";
556 while( my ($timestamp, $tries) = each %$alldumps ) {
558 foreach my $try (@$tries) {
559 if (exists $try->{dumper} &&
560 $try->{dumper}->{status} &&
561 $try->{dumper}->{status} eq 'fail') {
562 push @dump_failures, "$hostname $qdisk lev $try->{dumper}->{level} FAILED $try->{dumper}->{error}";
565 if (exists $try->{chunker} &&
566 $try->{chunker}->{status} eq 'fail') {
567 push @dump_failures, "$hostname $qdisk lev $try->{chunker}->{level} FAILED $try->{chunker}->{error}";
570 if ( exists $try->{taper}
571 && ( $try->{taper}->{status} eq 'fail'
572 || ( $try->{taper}->{status} eq 'partial'))) {
574 $flush = "FAILED" if exists $try->{dumper} && !exists $try->{chunker};
575 if ($flush ne "FLUSH" or !defined $try->{taper}->{failure_from}
576 or $try->{taper}->{failure_from} ne 'config') {
577 if ($try->{taper}->{status} eq 'partial') {
578 # if the error message is omitted, then the taper only got a partial
579 # dump from the dumper/chunker, rather than failing with a taper error
580 my $errmsg = $try->{taper}{error} || "successfully taped a partial dump";
581 $flush = "partial taper: $errmsg";
583 $flush .= " " . $try->{taper}{error};
586 push @dump_failures, "$hostname $qdisk lev $try->{taper}->{level} $flush";
591 # detect retried dumps
593 && exists $try->{dumper}
594 && ( $try->{dumper}->{status} eq "success"
595 || $try->{dumper}->{status} eq "strange")
596 && ( !exists $try->{chunker}
597 || $try->{chunker}->{status} eq "success")
598 && ( !exists $try->{taper}
599 || $try->{taper}->{status} eq "done")) {
600 push @dump_failures, "$hostname $qdisk lev $try->{dumper}->{level} was successfully retried";
604 # detect dumps re-flushed from holding
606 && !exists $try->{dumper}
607 && !exists $try->{chunker}
608 && exists $try->{taper}
609 && $try->{taper}->{status} eq "done") {
610 push @dump_failures, "$hostname $qdisk lev $try->{taper}->{level} was successfully re-flushed";
615 "$hostname $qdisk lev $try->{dumper}->{level} STRANGE (see below)"
616 if (defined $try->{dumper}
617 && $try->{dumper}->{status} eq 'strange');
621 push @failures, @fatal_failures, @error_failures, @missing_failures,
622 @driver_failures, @planner_failures, @dump_failures;
624 $self->print_if_def(\@failures, "FAILURE DUMP SUMMARY:");
625 $self->print_if_def(\@stranges, "STRANGE DUMP SUMMARY:");
635 # start at level 1 - don't include fulls
636 foreach my $i (1 .. (@$count - 1)) {
637 push @lc, "$i:$count->[$i]" if defined $count->[$i] and $count->[$i] > 0;
639 return join(' ', @lc);
645 my $fh = $self->{fh};
646 my $report = $self->{report};
652 Total Full Incr. Level:#
653 -------- -------- -------- --------
656 my $st_format = <<EOF;
657 @<<<<<<<<<<<<<<<<<<<<<<@>>>>>>>> @>>>>>>>> @>>>>>>>> @<<<<<<<<<<<<<<<<<<
660 # TODO: the hashes are a cheap fix. fix these.
661 my $full_stats = $self->{full_stats};
662 my $incr_stats = $self->{incr_stats};
663 my $total_stats = $self->{total_stats};
665 my ( $ttyp, $tt, $tapesize, $marksize );
666 $ttyp = getconf($CNF_TAPETYPE);
667 $tt = lookup_tapetype($ttyp) if $ttyp;
669 if ( $ttyp && $tt ) {
671 $tapesize = "".tapetype_getconf( $tt, $TAPETYPE_LENGTH );
672 $marksize = "".tapetype_getconf( $tt, $TAPETYPE_FILEMARK );
675 # these values should never be zero; assign defaults
676 $tapesize = 100 * 1024 * 1024 if !$tapesize;
677 $marksize = 1 * 1024 * 1024 if !$marksize;
683 "Estimate Time (hrs:min)",
684 hrmn( $total_stats->{planner_time} ),
690 "Run Time (hrs:min)",
691 hrmn( $total_stats->{total_time} ),
697 "Dump Time (hrs:min)",
698 hrmn( $total_stats->{dumper_time} ),
699 hrmn( $full_stats->{dumper_time} ),
700 hrmn( $incr_stats->{dumper_time} ),
707 sprintf( "%8.1f", $total_stats->{outsize}/1024 ),
708 sprintf( "%8.1f", $full_stats->{outsize}/1024 ),
709 sprintf( "%8.1f", $incr_stats->{outsize}/1024 ),
715 "Original Size (meg)",
716 sprintf( "%8.1f", $total_stats->{origsize}/1024 ),
717 sprintf( "%8.1f", $full_stats->{origsize}/1024 ),
718 sprintf( "%8.1f", $incr_stats->{origsize}/1024 ),
722 my $comp_size = sub {
724 return divzero(100 * $stats->{outsize}, $stats->{origsize});
729 "Avg Compressed Size (%)",
730 $comp_size->($total_stats),
731 $comp_size->($full_stats),
732 $comp_size->($incr_stats),
739 sprintf("%4d", $total_stats->{dumpdisk_count}),
740 sprintf("%4d", $full_stats->{dumpdisk_count}),
741 sprintf("%4d", $incr_stats->{dumpdisk_count}),
742 (has_incrementals($self->{dumpdisks}) ? by_level_count($self->{dumpdisks}) : "")
747 "Avg Dump Rate (k/s)",
748 divzero_wide( $total_stats->{outsize}, $total_stats->{dumper_time} ),
749 divzero_wide( $full_stats->{outsize}, $full_stats->{dumper_time} ),
750 divzero_wide( $incr_stats->{outsize}, $incr_stats->{dumper_time} ),
757 "Tape Time (hrs:min)",
758 hrmn( $total_stats->{taper_time} ),
759 hrmn( $full_stats->{taper_time} ),
760 hrmn( $incr_stats->{taper_time} ),
767 sprintf( "%8.1f", $total_stats->{tapesize}/1024 ),
768 sprintf( "%8.1f", $full_stats->{tapesize}/1024 ),
769 sprintf( "%8.1f", $incr_stats->{tapesize}/1024 ),
773 my $tape_usage = sub {
778 ($stat_ref->{tapedisk_count} + $stat_ref->{tapepart_count}) +
779 $stat_ref->{tapesize}
788 $tape_usage->($total_stats),
789 $tape_usage->($full_stats),
790 $tape_usage->($incr_stats),
795 my @incr_dle = @{$self->{tapedisks}};
796 foreach my $level (1 .. $#incr_dle) {
797 $nb_incr_dle += $incr_dle[$level];
802 $self->{tapedisks}[0] + $nb_incr_dle,
803 $self->{tapedisks}[0],
806 (has_incrementals($self->{tapedisks}))
807 ? by_level_count($self->{tapedisks})
812 # NOTE: only print out the per-level tapeparts if there are
813 # incremental tapeparts
817 sprintf("%4d", $total_stats->{tapepart_count}),
818 sprintf("%4d", $full_stats->{tapepart_count}),
819 sprintf("%4d", $incr_stats->{tapepart_count}),
821 $self->{tapeparts}[1] > 0
822 ? by_level_count($self->{tapeparts})
829 "Avg Tp Write Rate (k/s)",
830 divzero_wide( $total_stats->{tapesize}, $total_stats->{taper_time} ),
831 divzero_wide( $full_stats->{tapesize}, $full_stats->{taper_time} ),
832 divzero_wide( $incr_stats->{tapesize}, $incr_stats->{taper_time} ),
844 for ($a = 1; $a < @$array; $a+=1) {
845 return 1 if $array->[$a] > 0;
850 sub output_tape_stats
853 my $fh = $self->{fh};
854 my $report = $self->{report};
856 my $taper = $report->get_program_info("taper");
857 my $tapes = $taper->{tapes} || {};
858 my $tape_labels = $taper->{tape_labels} || [];
860 # if no tapes used, do nothing
861 return if (!@$tape_labels);
863 my $label_length = 19;
864 foreach my $label (@$tape_labels) {
865 $label_length = length($label) if length($label) > $label_length;
868 . '<' x ($label_length - 1)
869 . "@>>>> @>>>>>>>>>>> @>>>>> @>>>> @>>>>\n";
871 print $fh "USAGE BY TAPE:\n";
872 print $fh swrite($ts_format, "Label", "Time", "Size", "%", "DLEs", "Parts");
874 my $tapetype_name = getconf($CNF_TAPETYPE);
875 my $tapetype = lookup_tapetype($tapetype_name);
876 my $tapesize = "" . tapetype_getconf($tapetype, $TAPETYPE_LENGTH);
877 my $marksize = "" . tapetype_getconf($tapetype, $TAPETYPE_FILEMARK);
879 foreach my $label (@$tape_labels) {
881 my $tape = $tapes->{$label};
883 my $tapeused = $tape->{'kb'};
884 $tapeused += $marksize * (1 + $tape->{'files'});
889 hrmn($tape->{time}), # time
890 sprintf("%.0f", $self->tounits($tape->{kb})) . $self->{disp_unit}, # size
891 divzero(100 * $tapeused, $tapesize), # % usage
892 int($tape->{dle}), # # of dles
893 int($tape->{files}) # # of parts
902 ## takes no arguments
904 my $fh = $self->{fh};
905 my $errors = $self->{errors};
906 my $notes = $self->{notes};
907 my $report = $self->{report};
908 my $stranges = $report->{stranges};
910 my $disp_unit = $self->{disp_unit};
912 my @failed_dump_details;
913 my @strange_dump_details;
915 my @dles = $report->get_dles();
917 foreach my $dle_entry (@dles) {
919 my ($hostname, $disk) = @$dle_entry;
920 my $dle = $report->get_dle_info(@$dle_entry);
921 my $alldumps = $dle->{'dumps'} || {};
922 my $qdisk = quote_string($disk);
925 while( my ($timestamp, $tries) = each %$alldumps ) {
926 foreach my $try (@$tries) {
929 # check for failed dumper details
931 if (defined $try->{dumper}
932 && $try->{dumper}->{status} eq 'fail') {
934 push @failed_dump_details,
935 "/-- $hostname $qdisk lev $try->{dumper}->{level} FAILED $try->{dumper}->{error}",
936 @{ $try->{dumper}->{errors} },
939 if ($try->{dumper}->{nb_errors} > 100) {
940 my $nb = $try->{dumper}->{nb_errors} - 100;
942 push @failed_dump_details,
943 "$nb lines follow, see the corresponding log.* file for the complete list",
949 # check for strange dumper details
951 if (defined $try->{dumper}
952 && $try->{dumper}->{status} eq 'strange') {
954 push @strange_dump_details,
955 "/-- $hostname $qdisk lev $try->{dumper}->{level} STRANGE",
956 @{ $try->{dumper}->{stranges} },
959 if ($try->{dumper}->{nb_stranges} > 100) {
960 my $nb = $try->{dumper}->{nb_stranges} - 100;
961 push @strange_dump_details,
962 "$nb lines follow, see the corresponding log.* file for the complete list",
967 # note: copied & modified from calculate_stats.
969 exists $try->{dumper}
970 && exists $try->{chunker}
971 && defined $try->{chunker}->{kb}
972 && ( $try->{chunker}{status} eq 'success'
973 || $try->{chunker}{status} eq 'partial')
975 $outsize = $try->{chunker}->{kb};
977 exists $try->{dumper}
978 && exists $try->{taper}
979 && defined $try->{taper}->{kb}
980 && ( $try->{taper}{status} eq 'done'
981 || $try->{taper}{status} eq 'partial')
983 $outsize = $try->{taper}->{kb};
989 # check for bad estimates
992 if (exists $dle->{estimate} && defined $outsize) {
993 my $est = $dle->{estimate};
996 "big estimate: $hostname $qdisk $dle->{estimate}{level}",
997 sprintf(' est: %.0f%s out %.0f%s',
998 $self->tounits($est->{ckb}), $disp_unit,
999 $self->tounits($outsize), $disp_unit)
1000 if (defined $est->{'ckb'} && ($est->{ckb} * .9 > $outsize)
1001 && ($est->{ckb} - $outsize > 1.0e5));
1005 $self->print_if_def(\@failed_dump_details, "FAILED DUMP DETAILS:");
1006 $self->print_if_def(\@strange_dump_details, "STRANGE DUMP DETAILS:");
1007 $self->print_if_def($notes, "NOTES:");
1015 ## takes no arguments
1017 my $fh = $self->{fh};
1018 my $report = $self->{report};
1022 sort { ( $a->[0] cmp $b->[0] ) || ( $a->[1] cmp $b->[1] ) }
1023 $report->get_dles();
1025 ## set the col_spec, which is the configuration for the summary
1027 my $col_spec = $self->set_col_spec();
1029 ## collect all the output line specs (see get_summary_info)
1030 my @summary_linespecs = ();
1031 foreach my $dle (@dles) {
1032 push @summary_linespecs, $self->get_summary_info($dle, $report, $col_spec);
1035 # shift off the first element of each tuple
1036 my @summary_linedata =
1037 map { my @x = @$_; shift @x; [ @x ] } @summary_linespecs;
1039 ## get the summary format. this is based on col_spec, but may
1040 ## expand maxwidth columns if they have large fields. Note that
1041 ## this modifies $col_spec in place. Ordering is important: the summary
1042 ## format must be generated before the others.
1043 my $title_format = get_summary_format($col_spec, 'title', @summary_linedata);
1044 my $summary_format = get_summary_format($col_spec, 'full', @summary_linedata);
1045 my $missing_format = get_summary_format($col_spec, 'missing', @summary_linedata);
1046 my $noflush_format = get_summary_format($col_spec, 'noflush', @summary_linedata);
1047 my $nodump_PARTIAL_format = get_summary_format($col_spec, 'nodump-PARTIAL', @summary_linedata);
1048 my $nodump_FAILED_format = get_summary_format($col_spec, 'nodump-FAILED', @summary_linedata);
1049 my $nodump_FLUSH_format = get_summary_format($col_spec, 'nodump-FLUSH', @summary_linedata);
1050 my $skipped_format = get_summary_format($col_spec, 'skipped', @summary_linedata);
1052 ## print the header names
1054 $col_spec->[0]->[COLSPEC_WIDTH] +
1055 $col_spec->[1]->[COLSPEC_PRE_SPACE] +
1056 $col_spec->[1]->[COLSPEC_WIDTH] +
1057 $col_spec->[2]->[COLSPEC_PRE_SPACE] +
1058 $col_spec->[2]->[COLSPEC_WIDTH];
1060 $col_spec->[3]->[COLSPEC_WIDTH] +
1061 $col_spec->[4]->[COLSPEC_PRE_SPACE] +
1062 $col_spec->[4]->[COLSPEC_WIDTH] +
1063 $col_spec->[5]->[COLSPEC_PRE_SPACE] +
1064 $col_spec->[5]->[COLSPEC_WIDTH] +
1065 $col_spec->[6]->[COLSPEC_PRE_SPACE] +
1066 $col_spec->[6]->[COLSPEC_WIDTH] +
1067 $col_spec->[7]->[COLSPEC_PRE_SPACE] +
1068 $col_spec->[7]->[COLSPEC_WIDTH];
1070 $col_spec->[8]->[COLSPEC_WIDTH] +
1071 $col_spec->[9]->[COLSPEC_PRE_SPACE] +
1072 $col_spec->[9]->[COLSPEC_WIDTH];
1075 ## use perl's ancient formatting support for the header, since we get free string
1077 my $summary_header_format =
1078 ' ' x ($col_spec->[0]->[COLSPEC_PRE_SPACE] +
1079 $hdl + $col_spec->[4]->[COLSPEC_PRE_SPACE])
1080 . '@' . '|' x ($ds - 1)
1081 . ' ' x $col_spec->[9]->[COLSPEC_PRE_SPACE]
1082 . '@'. '|' x ($ts - 1) . "\n";
1083 my $summary_header = swrite($summary_header_format, "DUMPER STATS", "TAPER STATS");
1085 my $summary_dashes =
1086 ' ' x $col_spec->[0]->[COLSPEC_PRE_SPACE]
1088 . ' ' x $col_spec->[4]->[COLSPEC_PRE_SPACE]
1090 . ' ' x $col_spec->[9]->[COLSPEC_PRE_SPACE]
1093 print $fh "DUMP SUMMARY:\n";
1094 print $fh $summary_header;
1095 print $fh sprintf($title_format, map { $_->[COLSPEC_TITLE] } @$col_spec);
1096 print $fh $summary_dashes;
1098 ## write out each output line
1099 for (@summary_linespecs) {
1100 my ($type, @data) = @$_;
1101 if ($type eq 'full') {
1102 print $fh sprintf($summary_format, @data);
1103 } elsif ($type eq 'nodump-PARTIAL') {
1104 print $fh sprintf($nodump_PARTIAL_format, @data);
1105 } elsif ($type eq 'nodump-FAILED') {
1106 print $fh sprintf($nodump_FAILED_format, @data);
1107 } elsif ($type eq 'nodump-FLUSH') {
1108 print $fh sprintf($nodump_FLUSH_format, @data);
1109 } elsif ($type eq 'missing') {
1110 print $fh sprintf($missing_format, @data[0..2]);
1111 } elsif ($type eq 'noflush') {
1112 print $fh sprintf($noflush_format, @data[0..2]);
1113 } elsif ($type eq 'skipped') {
1114 print $fh sprintf($skipped_format, @data[0..2]);
1122 ## output_summary helper functions. mostly for formatting, but some
1123 ## for data collection. Returns an 12-tuple matching one of
1125 ## ('full', host, disk, level, orig, out, comp%, dumptime, dumprate,
1126 ## tapetime, taperate, taperpartial)
1127 ## ('missing', host, disk, '' ..) # MISSING -----
1128 ## ('noflush', host, disk, '' ..) # NO FILE TO FLUSH ------
1129 ## ('nodump-$msg', host, disk, level, '', out, '--', '',
1130 ## '', tapetime, taperate, taperpartial) # ... {FLUSH|FAILED|PARTIAL} ...
1131 ## ('skipped', host, disk, '' ..) # SKIPPED -----
1133 ## the taperpartial column is not covered by the columnspec, and "hangs off"
1134 ## the right side. It's usually empty, but set to " PARTIAL" when the taper
1135 ## write was partial
1137 sub get_summary_info
1140 my ( $dle, $report, $col_spec ) = @_;
1141 my ( $hostname, $disk ) = @$dle;
1144 my $dle_info = $report->get_dle_info(@$dle);
1146 my $tail_quote_trunc = sub {
1147 my ($str, $len) = @_;
1149 my $q_str = quote_string($str);
1152 if (length($q_str) > $len) {
1154 $qt_str = substr($q_str, length($q_str) - $len, $len);
1155 if ($q_str eq $str) {
1158 $qt_str =~ s{^..}{"-};
1168 ($col_spec->[1]->[COLSPEC_MAXWIDTH])
1169 ? quote_string($disk)
1170 : $tail_quote_trunc->($disk, $col_spec->[1]->[COLSPEC_WIDTH]);
1172 my $alldumps = $dle_info->{'dumps'};
1173 if (($dle_info->{'planner'} &&
1174 $dle_info->{'planner'}->{'status'} eq 'fail') or
1175 ($dle_info->{'driver'} &&
1176 $dle_info->{'driver'}->{'status'} eq 'fail')) {
1177 # Do not report driver error if we have a try
1178 if (!exists $alldumps->{$report->{'run_timestamp'}}) {
1180 push @rv, 'nodump-FAILED';
1181 push @rv, $hostname;
1182 push @rv, $disk_out;
1183 push @rv, ("",) x 9;
1186 } elsif ($dle_info->{'planner'} &&
1187 $dle_info->{'planner'}->{'status'} eq 'skipped') {
1189 push @rv, 'skipped';
1190 push @rv, $hostname;
1191 push @rv, $disk_out;
1192 push @rv, ("",) x 8;
1194 } elsif (keys %{$alldumps} == 0) {
1196 push @rv, $report->get_flag("amflush_run")? 'noflush' : 'missing';
1197 push @rv, $hostname;
1198 push @rv, $disk_out;
1199 push @rv, ("",) x 8;
1203 while( my ($timestamp, $tries) = each %$alldumps ) {
1204 my $last_try = $tries->[-1];
1206 exists $last_try->{taper} ? $last_try->{taper}{level}
1207 : exists $last_try->{chunker} ? $last_try->{chunker}{level}
1208 : $last_try->{dumper}{level};
1210 my $orig_size = undef;
1212 # find the try with the successful dumper entry
1214 foreach my $try (@$tries) {
1215 if ( exists $try->{dumper}
1216 && exists $try->{dumper}{status}
1217 && ( $try->{dumper}{status} eq "success"
1218 || $try->{dumper}{status} eq "strange")) {
1219 $dumper = $try->{dumper};
1223 $orig_size = $dumper->{orig_kb}
1226 my ( $out_size, $dump_time, $dump_rate, $tape_time, $tape_rate ) = (0) x 5;
1227 my ($dumper_status) = "";
1228 my $saw_dumper = 0; # no dumper will mean this was a flush
1229 my $taper_partial = 0; # was the last taper run partial?
1231 ## Use this loop to set values
1232 foreach my $try ( @$tries ) {
1234 ## find the outsize for the output summary
1237 exists $try->{taper}
1238 && ( $try->{taper}{status} eq "done"
1239 || $try->{taper}{status} eq "part+partial" )
1242 $orig_size = $try->{taper}{orig_kb} if !defined($orig_size);
1243 $out_size = $try->{taper}{kb};
1244 $tape_time = $try->{taper}{sec};
1245 $tape_rate = $try->{taper}{kps};
1246 } elsif ( exists $try->{taper}
1247 && ( $try->{taper}{status} eq "partial" ) ) {
1250 $orig_size = $try->{taper}{orig_kb} if !defined($orig_size);
1251 $out_size = $try->{taper}{kb};
1252 $tape_time = $try->{taper}{sec} if !$tape_time;
1253 $tape_rate = $try->{taper}{kps} if !$tape_rate;
1254 } elsif (exists $try->{taper} && ( $try->{taper}{status} eq "fail")) {
1260 exists $try->{chunker}
1261 && ( $try->{chunker}{status} eq "success"
1262 || $try->{chunker}{status} eq "partial" )
1264 $out_size = $try->{chunker}{kb};
1268 exists $try->{dumper}) {
1269 $out_size = $try->{dumper}{kb};
1272 if ( exists $try->{dumper}) {
1274 $dumper_status = $try->{dumper}{status};
1277 ## find the dump time
1278 if ( exists $try->{dumper}
1279 && exists $try->{dumper}{status}
1280 && ( $try->{dumper}{status} eq "success"
1281 || $try->{dumper}{status} eq "strange")) {
1283 $dump_time = $try->{dumper}{sec};
1284 $dump_rate = $try->{dumper}{kps};
1288 # sometimes the driver logs an orig_size of -1, which makes the
1289 # compression percent very large and negative
1290 $orig_size = 0 if (defined $orig_size && $orig_size < 0);
1292 # pre-format the compression column, with '--' replacing 100% (i.e.,
1295 if (!defined $orig_size || $orig_size == $out_size) {
1296 $compression = '--';
1299 divzero_col((100 * $out_size), $orig_size, $col_spec->[5]);
1302 ## simple formatting macros
1304 my $fmt_col_field = sub {
1305 my ( $column, $data ) = @_;
1308 $col_spec->[$column]->[COLSPEC_FORMAT],
1309 $col_spec->[$column]->[COLSPEC_WIDTH],
1310 $col_spec->[$column]->[COLSPEC_PREC], $data
1314 my $format_space = sub {
1315 my ( $column, $data ) = @_;
1317 return sprintf("%*s",$col_spec->[$column]->[COLSPEC_WIDTH], $data);
1322 if ( !$orig_size && !$out_size && (!defined($tape_time) || !$tape_time)) {
1323 push @rv, $report->get_flag("amflush_run")? 'noflush' : 'missing';
1324 push @rv, $hostname;
1325 push @rv, $disk_out;
1326 push @rv, ("",) x 8;
1327 } elsif ($saw_dumper and ($dumper_status eq 'success' or $dumper_status eq 'strange')) {
1329 push @rv, $hostname;
1330 push @rv, $disk_out;
1331 push @rv, $fmt_col_field->(2, $level);
1332 push @rv, $orig_size ? $fmt_col_field->(3, $self->tounits($orig_size)) : '';
1333 push @rv, $out_size ? $fmt_col_field->(4, $self->tounits($out_size)) : '';
1334 push @rv, $compression;
1335 push @rv, $dump_time ? $fmt_col_field->(6, mnsc($dump_time)) : "PARTIAL";
1336 push @rv, $dump_rate ? $fmt_col_field->(7, $dump_rate) : "";
1337 push @rv, $fmt_col_field->(8,
1338 (defined $tape_time) ?
1339 $tape_time ? mnsc($tape_time) : ""
1341 push @rv, (defined $tape_rate) ?
1343 $fmt_col_field->(9, $tape_rate)
1344 : $format_space->(9, "")
1345 : $format_space->(9, "FAILED");
1346 push @rv, $taper_partial? " PARTIAL" : ""; # column 10
1348 my $message = $saw_dumper?
1349 ($dumper_status eq 'failed') ? 'FAILED' : 'PARTIAL'
1351 push @rv, "nodump-$message";
1352 push @rv, $hostname;
1353 push @rv, $disk_out;
1354 push @rv, $fmt_col_field->(2, $level);
1355 push @rv, $orig_size ? $fmt_col_field->(4, $self->tounits($orig_size)) :'';
1356 push @rv, $out_size ? $fmt_col_field->(4, $self->tounits($out_size)) : '';
1357 push @rv, $compression;
1360 push @rv, $fmt_col_field->(8,
1361 (defined $tape_time) ?
1362 $tape_time ? mnsc($tape_time) : ""
1364 push @rv, (defined $tape_rate) ?
1366 $fmt_col_field->(9, $tape_rate)
1367 : $format_space->(9, "")
1368 : $format_space->(9, "FAILED");
1369 push @rv, $taper_partial? " PARTIAL" : "";
1376 sub get_summary_format
1378 my ($col_spec, $type, @summary_lines) = @_;
1379 my @col_format = ();
1381 if ($type eq 'full' || $type eq 'title') {
1382 foreach my $i ( 0 .. ( @$col_spec - 1 ) ) {
1384 get_summary_col_format( $i, $col_spec->[$i],
1385 map { $_->[$i] } @summary_lines );
1388 # first two columns are the same
1389 foreach my $i ( 0 .. 1 ) {
1391 get_summary_col_format( $i, $col_spec->[$i],
1392 map { $_->[$i] } @summary_lines );
1395 # some of these have a lovely text rule, just to be difficult
1397 $col_spec->[3]->[COLSPEC_WIDTH] +
1398 $col_spec->[4]->[COLSPEC_PRE_SPACE] +
1399 $col_spec->[4]->[COLSPEC_WIDTH] +
1400 $col_spec->[5]->[COLSPEC_PRE_SPACE] +
1401 $col_spec->[5]->[COLSPEC_WIDTH] +
1402 $col_spec->[6]->[COLSPEC_PRE_SPACE] +
1403 $col_spec->[6]->[COLSPEC_WIDTH] +
1404 $col_spec->[7]->[COLSPEC_PRE_SPACE] +
1405 $col_spec->[7]->[COLSPEC_WIDTH] +
1406 $col_spec->[8]->[COLSPEC_PRE_SPACE] +
1407 $col_spec->[8]->[COLSPEC_WIDTH] +
1408 $col_spec->[9]->[COLSPEC_PRE_SPACE] +
1409 $col_spec->[9]->[COLSPEC_WIDTH];
1411 if ($type eq 'missing') {
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];
1416 my $str = "MISSING ";
1417 $str .= '-' x ($rulewidth - length($str));
1418 push @col_format, $str;
1419 } elsif ($type eq 'noflush') {
1420 # add a blank level column and the space for the origkb column
1421 push @col_format, ' ' x $col_spec->[2]->[COLSPEC_PRE_SPACE];
1422 push @col_format, ' ' x $col_spec->[2]->[COLSPEC_WIDTH];
1423 push @col_format, ' ' x $col_spec->[3]->[COLSPEC_PRE_SPACE];
1425 my $str = "NO FILE TO FLUSH ";
1426 $str .= '-' x ($rulewidth - length($str));
1427 push @col_format, $str;
1428 } elsif ($type =~ /^nodump-(.*)$/) {
1431 # nodump has level, origkb, outkb, and comp% although origkb is usually blank and
1433 foreach my $i ( 2 .. 5 ) {
1435 get_summary_col_format( $i, $col_spec->[$i],
1436 map { $_->[$i] } @summary_lines );
1439 # and then the message is centered across columns 6 and 7, which are both blank
1440 push @col_format, ' ' x $col_spec->[6]->[COLSPEC_PRE_SPACE];
1442 $col_spec->[6]->[COLSPEC_WIDTH] +
1443 $col_spec->[7]->[COLSPEC_PRE_SPACE] +
1444 $col_spec->[7]->[COLSPEC_WIDTH];
1446 my $str = ' ' x (($width - length($msg))/2);
1448 $str .= ' ' x ($width - length($str));
1449 push @col_format, $str;
1450 push @col_format, "%s%s"; # consume empty columns 6 and 7
1452 # and finally columns 8 and 9 as usual
1453 foreach my $i ( 8 .. 9 ) {
1455 get_summary_col_format( $i, $col_spec->[$i],
1456 map { $_->[$i] } @summary_lines );
1458 } elsif ($type eq 'skipped') {
1459 # add a blank level column and the space for the origkb column
1460 push @col_format, ' ' x $col_spec->[2]->[COLSPEC_PRE_SPACE];
1461 push @col_format, ' ' x $col_spec->[2]->[COLSPEC_WIDTH];
1462 push @col_format, ' ' x $col_spec->[3]->[COLSPEC_PRE_SPACE];
1463 my $str = "SKIPPED ";
1464 $str .= '-' x ($rulewidth - length($str));
1465 push @col_format, $str;
1469 # and format the hidden 10th column. This is not part of the columnspec,
1470 # so its width is not counted in any of the calculations here.
1471 push @col_format, "%s" if $type ne 'title';
1473 return join( "", @col_format ) . "\n";
1476 sub get_summary_col_format
1478 my ( $i, $col, @entries ) = @_;
1480 my $col_width = $col->[COLSPEC_WIDTH];
1481 my $left_align = ($i == 0 || $i == 1); # first 2 cols left-aligned
1482 my $limit_width = ($i == 0 || $i == 1); # and not allowed to overflow
1484 ## if necessary, resize COLSPEC_WIDTH to the maximum widht
1486 if ($col->[COLSPEC_MAXWIDTH]) {
1488 push @entries, $col->[COLSPEC_TITLE];
1489 my $strmax = max( map { length $_ } @entries );
1490 $col_width = max($strmax, $col_width);
1491 # modify the spec in place, so the headers and
1492 # whatnot all add up .. yuck!
1493 $col->[COLSPEC_WIDTH] = $col_width;
1496 # put together a "%s" format for this column
1497 my $rv = ' ' x $col->[COLSPEC_PRE_SPACE]; # space on left
1499 $rv .= '-' if $left_align;
1501 $rv .= ".$col_width" if $limit_width;
1505 ## col_spec functions. I want to deprecate this stuff so bad it hurts.
1510 my $report = $self->{report};
1511 my $disp_unit = $self->{disp_unit};
1513 $self->{col_spec} = [
1514 [ "HostName", 0, 12, 12, 1, "%-*.*s", "HOSTNAME" ],
1515 [ "Disk", 1, 11, 11, 1, "%-*.*s", "DISK" ],
1516 [ "Level", 1, 1, 1, 1, "%*.*d", "L" ],
1517 [ "OrigKB", 1, 7, 0, 1, "%*.*f", "ORIG-" . $disp_unit . "B" ],
1518 [ "OutKB", 1, 7, 0, 1, "%*.*f", "OUT-" . $disp_unit . "B" ],
1519 [ "Compress", 1, 6, 1, 1, "%*.*f", "COMP%" ],
1520 [ "DumpTime", 1, 7, 7, 1, "%*.*s", "MMM:SS" ],
1521 [ "DumpRate", 1, 6, 1, 1, "%*.*f", "KB/s" ],
1522 [ "TapeTime", 1, 6, 6, 1, "%*.*s", "MMM:SS" ],
1523 [ "TapeRate", 1, 6, 1, 1, "%*.*f", "KB/s" ]
1526 $self->apply_col_spec_override();
1527 return $self->{col_spec};
1530 sub apply_col_spec_override
1533 my $col_spec = $self->{col_spec};
1535 my %col_spec_override = $self->read_col_spec_override();
1537 foreach my $col (@$col_spec) {
1538 if ( my $col_override = $col_spec_override{ $col->[COLSPEC_NAME] } ) {
1539 my $override_col_val_if_def = sub {
1540 my ( $field, $or_num ) = @_;
1541 if ( defined $col_override->[$or_num]
1542 && !( $col_override->[$or_num] eq "" ) ) {
1543 $col->[$field] = $col_override->[$or_num];
1547 $override_col_val_if_def->( COLSPEC_PRE_SPACE, 0 );
1548 $override_col_val_if_def->( COLSPEC_WIDTH, 1 );
1549 $override_col_val_if_def->( COLSPEC_PREC, 2 );
1550 $override_col_val_if_def->( COLSPEC_MAXWIDTH, 3 );
1555 sub read_col_spec_override
1559 my $col_spec_str = getconf($CNF_COLUMNSPEC) || return;
1560 my %col_spec_override = ();
1561 my $col_spec = $self->{col_spec};
1563 foreach (split(",", $col_spec_str)) {
1565 $_ =~ m/^(\w+) # field name
1566 =([-:\d]+) # field values
1568 or confess "error: malformed columnspec string:$col_spec_str";
1573 foreach my $col (@$col_spec) {
1574 if (lc $field eq lc $col->[0]) {
1580 die("Invalid field name: $field");
1583 my @field_values = split ':', $2;
1586 confess "error: malformed columnspec string:$col_spec_str"
1587 if (@field_values > 3);
1589 # all values *should* be in the right place. If not enough
1590 # were given, pad the array.
1591 push @field_values, "" while (@field_values < 4);
1593 # if the second value is negative, that means MAXWIDTH=1, so
1594 # sort that out now. Yes, this is pretty ugly. Imagine this in C!
1595 if ($field_values[1] ne '') {
1596 if ($field_values[1] =~ /^-/) {
1597 $field_values[1] =~ s/^-//;
1598 $field_values[3] = 1;
1600 $field_values[3] = 0;
1604 $col_spec_override{$field} = \@field_values;
1607 return %col_spec_override;
1612 my ($self, $msgs, $header) = @_;
1613 my $fh = $self->{fh};
1615 @$msgs or return; # do not print section if no messages
1617 print $fh "$header\n";
1618 foreach my $msg (@$msgs) {
1619 print $fh " $msg\n";