root/trunk/util/server_daily_maint @ 1078

Revision 1078, 40.7 kB (checked in by max, 5 years ago)

server_daily_maint: stats logging fixes

  • Property svn:executable set to *
Line 
1#!/usr/bin/perl
2
3# server-side maintenance script
4#  * can be run daily or multiple times per day
5#  * best if run (say) every 3 hours or 12 hours
6#    (typically no need to run more often - it will still generate per-hour data)
7#
8#  - deletes old feedback
9#  - updates store of some common statistics (users, failures, duration, time-of-day) etc.
10#  - generates pretty graphs and html
11#  - generates graphs showing when components have missing data (...Max to add this part. :) )
12
13
14# settings
15my $days_to_keep = 14;          # keep 14 days
16my $missing_interval = (5*60);  # record missing data in 5 minute intervals
17my $store_dir = $ENV{HOME} . "/whuffy.com/dev/stats";    # where we retrieve our feedback
18my $graph_dir = $ENV{HOME} . "/whuffy.com/dev/feedback"; # where we store our graphs
19my $template_dir = $ENV{HOME} . "/shepherd/util";  # where we store our templates
20my $stylesheet = '/svgstyles.css';
21my $debug = 0;
22
23use strict;
24use Data::Dumper;
25use POSIX qw(strftime);
26use SVG::TT::Graph::TimeSeries;
27use SVG::TT::Graph::Bar;
28use SVG::TT::Graph::Pie;
29use Storable;
30use HTML::Template;
31use Time::Local;
32use Date::Manip;
33
34my $stats_file = $store_dir . "/stats.config";
35my $store = { };
36my $starttime = time;
37
38print "Running at:\n";
39print strftime "- %a %b %e %H:%M:%S %Y %z (system time)\n", localtime;
40# Set timezone to Australian EST
41$ENV{TZ}="EST-10EDT-11,M10.5.0/02:00,M3.5.0/02:00";
42strftime("%z", localtime);  # This is necessary to force the change
43print strftime "- %a %b %e %H:%M:%S %Y %z (Australian EST)\n", localtime;
44
45$debug = 1 if (grep ($_ eq '--debug', @ARGV));
46
47&read_config;           # read in our existing stats
48&set_default_values;
49
50&rollover_statistics;   # rotate statistics
51
52&delete_old_files;      # delete old feedback files
53
54$store->{last_run} = $starttime;
55&write_config;          # need to do this in case we barf during
56                        # stats generation -- we've renamed/deleted
57                        # old stats files so keep config file in
58                        # sync
59
60&parse_feedback;        # parse any (new) feedback files
61
62&aggregate_stats;       # use last 7 days to build last week, etc.
63
64if (grep ($_ eq '--dump', @ARGV))
65{
66    print Dumper($store);
67    exit;
68}
69
70&generate_reports;      # generate reports and graphs
71
72&write_config;          # write out our updated stats
73
74print "Done.\n";
75
76exit(0);
77
78###############################################################################
79
80sub delete_old_files
81{
82        chdir($store_dir) || die "can't chdir $store_dir: $!\n";
83        opendir(DIR, $store_dir) || die "can't opendir $store_dir: $!\n";
84        my @files = readdir(DIR);
85        closedir DIR;
86
87        foreach my $file (@files) {
88                next if (!-f $file);                    # only work on normal files
89                next if ($file !~ /^\d+\.done$/);       # only delete processed files
90
91                if ((-M $file) > $days_to_keep) {
92                        if ($debug) {
93                                print "would have tried to delete $store_dir/$file if debug wasn't set\n";
94                        } else {
95                                if ((unlink "$store_dir/$file") == 0) {
96                                        print "failed to delete $store_dir/$file: $!\n";
97                                }
98                        }
99                }
100        }
101}
102
103###############################################################################
104# read in our saved statistics
105
106sub read_config
107{
108        if (-r $stats_file) {
109                $store = Storable::retrieve($stats_file);
110        } else {
111                printf "WARNING: no config file $stats_file - ok if this is the first time running!\n";
112
113                # try to write to it - if we can't then this will cause us to barf
114                &write_config;
115        }
116}
117
118###############################################################################
119# write out our saved statistics
120
121sub write_config
122{
123        Storable::store($store, $stats_file);
124}
125
126###############################################################################
127# define some initial settings to basic values if they weren't already set
128# (have to do this _after_ reading config file, as Storable overwrites)
129
130sub set_default_values
131{
132        $store->{last_run} = 0 if (!defined $store->{last_run});
133
134        $store->{num_hours} = 1 if (!defined $store->{num_hours});
135        $store->{num_days} = 0 if (!defined $store->{num_days});
136        $store->{num_weeks} = 0 if (!defined $store->{num_weeks});
137        $store->{num_months} = 0 if (!defined $store->{num_months});
138
139        $store->{max_hours} = 72 if (!defined $store->{max_hours});     # keep per-hour stats for 72 hours (need min 24)
140        $store->{max_days} = 30 if (!defined $store->{max_days});       # keep per-day stats for 30 days (need min 30)
141        $store->{max_weeks} = 12 if (!defined $store->{max_weeks});     # keep per-week stats for 12 weeks
142        $store->{max_months} = 36 if (!defined $store->{max_months});   # keep per-month stats for 36 months
143}
144
145###############################################################################
146# rollover 'daily' / 'weekly' / 'monthly' statistics if day/week/month is
147# different from the last time we were run
148
149sub rollover_statistics
150{
151
152    return unless ($store->{last_run});
153
154    my $last_run = &Date::Manip::ParseDateString("epoch " . $store->{last_run});
155    my $now = &Date::Manip::ParseDate("epoch $starttime");
156    my $weekdiffstr = &Date::Manip::DateCalc($last_run, $now);
157    my $monthdiffstr = &Date::Manip::DateCalc($last_run, $now, undef, 1);
158
159    my $months_since = (($monthdiffstr =~ /(\d+):(\d+):/) && ($1 * 12 + $2));
160    &rollover_units('month', $months_since);
161
162    my $weeks_since = (($weekdiffstr =~ /0:0:(\d+):/) && $1);
163    &rollover_units('week', $weeks_since);
164
165    my $days_since = (($weekdiffstr =~ /0:0:(\d+):(\d+):/) && ($1 * 7 + $2));
166    &rollover_units('day', $days_since);
167
168    my $hours_since = (($weekdiffstr =~ /0:0:(\d+):(\d+):(\d+):/) && ($1 * 168 + $2 * 24 + $3));
169    &rollover_units('hour', $hours_since);
170
171#    print "Finished; per_hour 1 is: " . Dumper($store->{per_hour}->[1]) . "\n"; die;
172}
173
174# Helper function called by rollover_stats; abstracts the code for
175# rolling over months/weeks/days/hours.
176sub rollover_units
177{
178    my ($unit, $units_since) = @_;
179
180    if ($units_since)
181    {
182        print "$units_since $unit since last stats rollover\n"; # if ($debug);
183
184        my $num_units = 'num_' . $unit . 's';
185        my $max_units = 'max_' . $unit . 's';
186        my $per_unit = 'per_' . $unit;
187        for (my $i = ($store->{$num_units}-1); $i >= 0; $i--)
188        {
189            if ($i + $units_since < $store->{$max_units})
190            {
191                $store->{$per_unit}->[$i + $units_since] = $store->{$per_unit}->[$i];
192                print "store -> $per_unit -> [ $i + $units_since ] copied from $i\n";
193            }
194            $store->{$per_unit}->[$i] = undef;
195        }
196        $store->{$num_units} += $units_since;
197        $store->{$num_units} = $store->{$max_units} - 1 if ($store->{$num_units} >= $store->{$max_units});
198    }
199}
200
201###############################################################################
202# aggregate stats:
203#  - last 24 hours = day 0 statistics,
204#  - last 7 days = week 0 statistics,
205#  - last 30 days = month 0 statistics
206
207sub aggregate_stats
208{
209        # build day 0 based on last 24 hours
210        delete $store->{per_day}->[0];
211        for (my $i = 0; $i < 24; $i++) {
212                next if (!defined $store->{per_hour}->[$i]);
213                $store->{per_day}->[0] = &merge_in_stats($store->{per_day}->[0], $store->{per_hour}->[$i]);
214        }
215
216        # build week 0 based on last 7 days
217        delete $store->{per_week}->[0];
218        for (my $i = 0; $i < 7; $i++) {
219                next if (!defined $store->{per_day}->[$i]);
220                $store->{per_week}->[0] = &merge_in_stats($store->{per_week}->[0], $store->{per_day}->[$i]);
221        }
222
223        # build month 0 based on last 30 days
224        delete $store->{per_month}->[0];
225        for (my $i = 0; $i < 30; $i++) {
226                next if (!defined $store->{per_day}->[$i]);
227                $store->{per_month}->[0] = &merge_in_stats($store->{per_month}->[0], $store->{per_day}->[$i]);
228        }
229}
230
231###############################################################################
232
233sub record_duration
234{
235        my ($where, $suffix, $dur) = @_;
236        $suffix .= "_" if ($suffix ne "");
237
238        $where->{$suffix."duration"} += $dur;
239        $where->{$suffix."duration_count"} ++;
240        $where->{$suffix."duration_max"} = $dur if (!defined $where->{$suffix."duration_max"});
241        $where->{$suffix."duration_max"} = $dur if ($dur > $where->{$suffix."duration_max"});
242        $where->{$suffix."duration_min"} = $dur if (!defined $where->{$suffix."duration_min"});
243        $where->{$suffix."duration_min"} = $dur if ($dur < $where->{$suffix."duration_min"});
244}
245
246###############################################################################
247# merge statistics in an intelligent manner
248#  - can recursively descend HASH lists
249#  - accumulates totals
250#  - fields with _min/_max suffix take the minimum/maximum value
251
252sub merge_in_stats
253{
254        my ($to, $from) = @_;
255
256        foreach my $field (keys %{($from)}) {
257                if (ref($from->{$field}) eq "HASH") {
258                        $to->{$field} = &merge_in_stats($to->{$field}, $from->{$field});
259                } elsif (defined $from->{$field}) {
260                        if (!defined $to->{$field}) {
261                                $to->{$field} = $from->{$field};
262                        } else {
263                                # fields ending in _min and _max are special
264                                if ($field =~ /_min$/) {
265                                        $to->{$field} = $from->{$field} if ($from->{$field} < $to->{$field});
266                                } elsif ($field =~ /_max$/) {
267                                        $to->{$field} = $from->{$field} if ($from->{$field} > $to->{$field});
268                                } else {
269                                        $to->{$field} += $from->{$field};
270                                }
271                        }
272                }
273        }
274        return $to;
275}
276
277###############################################################################
278
279sub parse_feedback
280{
281        chdir($store_dir) || die "can't chdir $store_dir: $!\n";
282        opendir(DIR, $store_dir) || die "can't opendir $store_dir: $!\n";
283        my @files = readdir(DIR);
284        closedir DIR;
285
286        foreach my $file (@files) {
287                next if (!-f $file);                    # only work on normal files
288                next if ($file !~ /^\d+$/);             # only look at non-processed files
289                next if ((stat(_))[9] < $store->{last_updated}); # only look at newly modified files
290
291                my $file_modtime = (stat(_))[9];
292                my $hr_num = int((time - $file_modtime) / 3600);
293                $hr_num = $store->{max_hours} if ($hr_num > $store->{max_hours});
294                $hr_num = 0 if ($hr_num < 0);
295
296                printf " - gathering statistics from $store_dir/$file ($file_modtime -> hr $hr_num)\n"; # if ($debug);
297
298                my $parsed = 0;
299                $store->{per_hour}->[$hr_num]->{visits}++;               # total # of visits seen
300                $store->{total}->{visits}++;
301
302#               printf "   : visits hr $hr_num: %d\n", $store->{per_hour}->[$hr_num]->{visits};
303
304                my $sid;
305
306                open(F,"<$store_dir/$file") || die "could not open $store_dir/$file: $!\n";
307                while(<F>) {
308                        chop;
309                        if ($_ =~ /^shepherd\t(SUCCESS|FAIL)\t([0-9\.]+)\t(\d+)\t(\d+)\t(\d+)\t(.*)$/) {
310                                # shepherd status response
311                                my ($status, $sysid, $user_starttime, $duration, $region, $components_used) = ($1,$2,$3,$4,$5,$6);
312                                $parsed = 1;
313                                $sid = $sysid;
314                                $sid = undef if ($sid eq '');
315                                $status = lc($status);
316
317                                #
318                                # usage statistics
319                                #
320
321                                # success and failure by unique ID
322                                if ($status eq 'success' or $status eq 'fail')
323                                {
324                                    $store->{per_hour}->[$hr_num]->{$status.'_sysid_list'}->{$sid}++;
325                                    $store->{total}->{$status.'_sysid_list'}->{$sid}++;
326                                }
327
328                                # shepherd's duration (total count, min, max, average from duration/visits)
329                                if ($status eq 'success')
330                                {
331                                    &record_duration($store->{per_hour}->[$hr_num], "", $duration);
332                                    &record_duration($store->{total}, "", $duration);
333                                }
334
335                                # list of sysid's seen (used to derive # unique users)
336                                $store->{per_hour}->[$hr_num]->{seen_sysid_list}->{$sysid}++;
337                                $store->{total}->{seen_sysid_list}->{$sysid}++;
338
339                                # where user is
340                                if ($region > 0 and $region < 200)
341                                {
342                                    $store->{per_hour}->[$hr_num]->{region_list}->{$region}->{$sysid}++;
343                                }
344
345                                # when shepherd was run, normalized to 15 minute buckets
346                                my $when_run = strftime("%H:%M", localtime(($file_modtime-($file_modtime%(15*60)))));
347                                $store->{per_hour}->[$hr_num]->{when_run_list}->{"$when_run"}++;
348                                $store->{total}->{when_run_list}->{"$when_run"}++;
349
350                                # what components were used (and in what order?)
351                                $components_used =~ s/\(.*?\)//g;   # ignore version numbers
352                                $store->{per_hour}->[$hr_num]->{components_used_order_list}->{$components_used}++;
353
354                        } elsif ($_ =~ /^(\S+)\tSUCCESS\t(\S+)\t(\S+)\t(\S+)\t(\S+)\t(\S+)/) {
355                                # success line
356                                my ($c, $retcode, $c_start, $c_dur, $c_ver, $c_grab) = ($1, $2, $3, $4, $5, $6);
357
358                                $c = $1 if ($c =~ /^(.*)-\d+$/);
359                                my $g = "c_".$c."_success";
360                                $store->{per_hour}->[$hr_num]->{'c_'.$c.'_runs'}++;
361                                $store->{total}->{'c_'.$c.'_runs'}++;
362                                $store->{per_hour}->[$hr_num]->{$g."_sysid_list"}->{$sid}++ if ($sid);
363
364                                # duration (total count, min, max, average from duration/counter)
365                                &record_duration($store->{per_hour}->[$hr_num], $g, $c_dur);
366                                &record_duration($store->{total}, $g, $c_dur);
367
368                                # version count
369                                $store->{per_hour}->[$hr_num]->{$g."_version_list"}->{$c_ver}++;
370
371                                # data download count
372                                if ($c_grab)
373                                {
374                                    $store->{per_hour}->[$hr_num]->{mins_grabbed_list}->{$c} += $c_grab;
375                                }
376                        } elsif ($_ =~ /^(\S+)\tFAIL\t(.+)\t(\S+)\t(\S+)\t(\S+)\t(\S+)/) {
377                                # fail line
378                                my ($c, $retcode, $c_start, $c_dur, $c_ver, $num_failures) = ($1, $2, $3, $4, $5, $6);
379
380                                $c = $1 if ($c =~ /^(.*)-\d+$/);
381                                my $g = "c_".$c."_fail";
382                                $store->{per_hour}->[$hr_num]->{'c_'.$c.'_runs'}++;
383                                $store->{total}->{'c_'.$c.'_runs'}++;
384                                $store->{per_hour}->[$hr_num]->{$g."_sysid_list"}->{$sid}++ if ($sid);
385                                if ($retcode =~ /(.*?):(.*)/)
386                                {
387                                    my ($code, $msg) = ($1, $2);
388                                    my $shortsid = $1 if ($sid =~ /\d+\.(\d+)/);
389                                    unshift @{$store->{failcodes}->{$c}}, { timestamp => $starttime, sysid => $shortsid, file => $file, code => $code, msg => $msg };
390                                }
391
392                                # duration (total count, min, max, average from duration/counter)
393                                &record_duration($store->{per_hour}->[$hr_num], $g, $c_dur);
394                                &record_duration($store->{total}, $g, $c_dur);
395
396                                # version count
397                                $store->{per_hour}->[$hr_num]->{$g."_version_list"}->{$c_ver}++;
398                        } elsif ($_ =~ /^(\S+)\tMISSING_DATA\t(.*)$/) {
399                                # component didn't return as much data as we expected it to
400                                #  record as: $store->{missing_data}->{$channel}->{$component_name}->{$epoch} = count
401                                # note that old missing_data is aged out in read_config().
402
403                                my ($c, $missing) = ($1, $2);
404                                $c = $1 if ($c =~ /^(.*)-\d+$/);
405                                my $g = ($c eq 'shepherd' ? '' : 'c_'.$c.'_') . 'missing_sysid_list';
406                                $store->{per_hour}->[$hr_num]->{$g}->{$sid}++;
407                                $store->{total}->{$g}->{$sid}++;
408                                unless ($c eq 'shepherd')
409                                {
410                                    foreach my $chgroup (split(/\t/,$missing)) 
411                                    {
412                                        my ($ch, $missing_times) = split(/:/,$chgroup);
413                                        foreach my $missing_interval (split(/,/,$missing_times)) 
414                                        {
415                                            my ($miss_start, $miss_stop) = split(/\-/,$missing_interval);
416                                            $store->{missing_data}->{$ch}->{"$miss_start-$miss_stop"}->{$c}->{$sid} = $starttime;
417                                        }
418                                    }
419                                }
420                        } elsif ($_ =~ /^(\S+)\tstats\t(.*)$/) {
421                                # component statistics line
422
423                                # November 11 2007: skipping STATS line because
424                                # our data file (stats.config) is frigging huge
425                                # and causing timeouts. Could turn this
426                                # back on if we're more selective about the data
427                                # we retain, or if we hunt down some of the more
428                                # bloated areas.
429                                next;
430
431                                # parse stats from line if in the form of:
432                                #   (delim1) (space) (number) (space) (varname) (delim)
433                                #
434                                # (delim1) has to be a comma or dot
435                                # (number) must be a number - can be a float or integer
436                                # (varname) must be either end-of-line or be followed by a . or ,
437                                # (delim2) must be either end-of-line or be a . or ,
438
439                                # e.g. a STATS line of:
440                                #  sbsnews_website v0.03 completed in 1.23 seconds, 51887 bytes_fetched, 1 http_successful_requests, 236 progs
441                                # will pick up the statistics:
442                                #    field 'bytes_fetched' with a value of 51887
443                                #    field 'http_successful_requests' with a value of 1
444                                #    field 'progs' with a value of 236
445
446                                # skip if any '/' or '`' chars in name
447                                next if (($1 =~ /\//) || ($1 =~ /`/));
448                                next if (($2 =~ /\//) || ($2 =~ /`/));
449
450                                my $c = $1;
451                                my $stats_string = $2 . ".";
452
453                                $c = $1 if ($c =~ /^(.*)-\d+$/);
454
455                                while ($stats_string ne "") {
456                                        # find delim1
457                                        while ((substr($stats_string,0,1) !~ /[,\.]/) && (length($stats_string) > 0)) {
458                                                $stats_string = substr($stats_string,1,1000);
459                                        }
460                                        last if (length($stats_string) < 2);
461
462                                        if ($stats_string =~ /^[\,\.]\s([0-9\.]+)\s(\S+)[\,\.]/) {
463                                                my $count = $1;
464                                                my $var = "cs.".$c.".".$2;
465                                                &record_duration($store->{per_hour}->[$hr_num],$var,$count);
466                                                &record_duration($store->{total},$var,$count);
467                                        }
468
469                                        $stats_string = substr($stats_string,2,1000);
470                                }
471                        } else {
472                            print "WARNING: unknown line in $file:\n$_\n";
473                        }
474                }
475                close(F);
476
477                if (!$parsed) {
478                        # anonymous, old version or no details
479                        $store->{per_hour}->[$hr_num]->{visits_with_no_details}++;
480                        $store->{total}->{visits_with_no_details}++;
481                }
482
483                # rename processed files so we don't re-process them
484                rename("$file", "$file.done") if (!$debug);
485        }
486}
487
488###############################################################################
489
490sub numvar
491{
492        my ($var,$field,$notdef) = @_;
493        $notdef = 0 if (!defined $notdef);
494        $field = "" if (!defined $field);
495
496        return $notdef if (!defined $var);
497
498        if ($field =~ /_list$/) {
499                # fields which end in _list are HASHes - return # of keys
500                my $count = keys %{$var};
501                return $count;
502        } else {
503                return $var;
504        }
505}
506
507###############################################################################
508
509sub report_count_data
510{
511        my ($template, $varname, $field, $reptype, $ignore_zero) = @_;
512        $reptype = "all" unless (defined $reptype);
513
514        my @ret = ();
515        my @loop = ($reptype eq 'all' ?  ( 'hour', 'day', 'week', 'month' ) :  ( 'day', 'week' ));
516
517        foreach my $timeslice (@loop)
518        {
519                next if ($ignore_zero and !numvar($store->{"per_$timeslice"}->[0]->{$field},$field));
520                my %h;
521                $h{'TIMESLICE'} = $timeslice;
522                $h{'SEEN'} = numvar($store->{"per_$timeslice"}->[0]->{$field},$field);
523                $h{'PREVIOUS'} = numvar($store->{"per_$timeslice"}->[1]->{$field},$field);
524
525                push (@ret, \%h);
526        }
527
528        my $total = numvar($store->{total}->{$field},$field);
529
530        if ($template)
531        {
532                $template->param($varname => \@ret);
533                $template->param($varname. '_TOTAL' => $total);
534        }
535        else
536        {
537                return (\@ret, $total);
538        }
539}
540
541sub calc_success_rate
542{
543        my ($c, $timeslice) = @_;
544
545        my $s = numvar($store->{"per_$timeslice"}->[0]->{'c_'.$c.'_success_sysid_list'},'c_'.$c.'_success_sysid_list');
546        my $f = numvar($store->{"per_$timeslice"}->[0]->{'c_'.$c.'_fail_sysid_list'},'c_'.$c.'_fail_sysid_list');
547
548        return '-' unless ($s or $f);
549        my $n = int(100*$s/($s+$f));
550        my $color = ($n < 75 ? "red" : $n < 90 ? "orange" : "lightgreen");
551        return "<font color=\"$color\">$n%</font>";
552}
553
554sub report_failure_msgs
555{
556    my ($template, $c) = @_;
557
558    return unless $store->{failcodes}->{$c};
559
560    my @msgs;
561    for (my $i = 0; $i < @{$store->{failcodes}->{$c}}; $i++)
562    {
563        my %h = %{$store->{failcodes}->{$c}[$i]};
564
565        # Remove any entries older than 1 week
566        if ($h{timestamp} + 604800 < $starttime)
567        {
568            splice(@{$store->{failcodes}->{$c}}, $i);
569            last;
570        }
571        $h{timestamp} = strftime("%a %d %b %H:%M", localtime($h{timestamp}));
572        $h{msg} = '(no msg)' unless $h{msg};
573        push @msgs, { %h };
574    }
575
576    $template->param(FAILURE_MSGS => \@msgs);
577}
578
579###############################################################################
580
581sub sec2hms
582{
583        my $sec = shift;
584
585        return sprintf "%.1f", ($sec/60);
586}
587
588###############################################################################
589
590sub graph_runtime
591{
592        my ($when, $startwhen, $endwhen, $title ) = @_;
593
594        my $field = 'when_run_list';
595        my $gname = $field."_".$when."_".$startwhen."_".$endwhen;
596
597        # gather up data
598        my %data; 
599        for (my $i = $startwhen; $i < $endwhen; $i++) {
600                if (defined $store->{$when}->[$i]->{$field}) {
601                        foreach my $k (keys %{($store->{$when}->[$i]->{$field})}) {
602                                $data{"2000-1-1 $k"} += $store->{$when}->[$i]->{$field}->{$k};
603                        }
604                }
605        }
606
607        # fill in empty timeslots with zeros
608        for (my $i = 0; $i < 24; $i++)
609        {
610                for (my $j = 0; $j < 60; $j += 15)
611                {
612                        my $key = sprintf "2000-1-1 %02d:%02d", $i, $j;
613                        $data{$key} = 0 unless defined $data{$key};
614                }
615        }
616
617        # sort it into key order
618        my ($min, $max, @sorted_data);
619        foreach my $k (sort keys %data) 
620        {
621                push(@sorted_data, $k, $data{$k});
622
623                $min = $data{$k} if (!defined $min or $data{$k} < $min);
624                $max = $data{$k} if (!defined $max or $data{$k} > $max);
625        }
626
627        # work out our scaling
628        my $scalediv = ($max - $min) / 3;
629        $scalediv = $max if ($scalediv < 1);
630
631        # only graph if we have some data
632        return if ($min == 0 and $max == 0);
633
634        return graph_one_count_data($gname, $title, '%l:%M %p', '2 hours', [ \@sorted_data ], 0, 505 );
635}
636
637sub gather_region_data
638{
639    my ($when, $startwhen, $endwhen, $long_ids) = @_;
640       
641    my %short_regions = (
642        126 => "ACT", 73 => "Sydney", 184 => "NSW", 66 => "NSW", 
643        67 => "NSW", 63 => "NSW", 69 => "NSW",  71 => "NSW",
644        106 => "NSW", 74 => "Darwin", 108 => "NT", 75 => "Brisbane",
645        78 => "QLD", 79 => "QLD", 114 => "QLD", 81 => "Adelaide",
646        82 => "SA", 83 => "SA", 85 => "SA", 86 => "SA", 107 => "SA",
647        88 => "Tasmania", 94 => "Melbourne", 93 => "VIC",
648        90 => "VIC", 95 => "VIC", 98 => "VIC", 101 => "Perth",
649        102 => "WA" );
650    my %long_regions = (
651       126 => "ACT", 73 => "Sydney", 184 => "Newcastle",
652        66 => "Central Coast NSW", 67 => "Griffith", 63 => "Broken Hill",
653        69 => "Northern NSW", 71 => "Southern NSW", 106 => "NSW Remote",
654        74 => "Darwin", 108 => "NT Remote", 75 => "Brisbane",
655        78 => "Gold Coast", 79 => "QLD Regional", 114 => "QLD Remote",
656        81 => "Adelaide", 82 => "Renmark", 83 => "Riverland", 
657        85 => "South East SA", 86 => "Spencer Gulf", 
658        107 => "SA Remote", 88 => "Tasmania", 94 => "Melbourne",
659        93 => "Geelong", 90 => "Eastern VIC", 95 => "Mildura/Sunraysia",
660        98 => "Western VIC", 101 => "Perth", 102 => "WA: Regional");
661
662    my $data;
663    for (my $i = $startwhen; $i < $endwhen; $i++) 
664    {
665        my $h = $store->{$when}->[$i]->{'region_list'}; 
666        if ($h)
667        {
668            foreach my $k (keys %$h)
669            {
670                my $label = ($long_ids ? $long_regions{$k} : $short_regions{$k});
671                foreach my $sysid (keys %{$h->{$k}})
672                {
673                    push (@{$data->{$label}}, $sysid) unless (grep($_ eq $sysid, @{$data->{$label}}));
674                }
675            }
676        }
677    }
678
679    # convert to # of unique sysids
680    foreach (keys %$data)
681    {
682        $data->{$_} = scalar(@{$data->{$_}});
683    }
684    return $data;
685}
686
687sub graph_regions
688{
689        my ($when, $startwhen, $endwhen, $title) = @_;
690
691        my $gname = "region_list_".$when."_".$startwhen."_".$endwhen;
692
693        my $rdata = &gather_region_data($when, $startwhen, $endwhen, 0);
694        return unless ($rdata);
695
696        my $gheight = 220;
697        my $gwidth = 360;
698        print "Generating pie graph $gname ..\n" if ($debug);
699        my $graph = SVG::TT::Graph::Pie->new({
700                'show_graph_title' => 1,
701                'graph_title' => $title,
702                'height' => $gheight,
703                'width' => $gwidth,
704                'show_shadow' => 1,
705                'shadow_offset' => 5,
706                'shadow_size' => 2,
707                'expanded' => 0,
708                'show_percent' => 0,
709                'show_data_labels' => 1,
710                'fields' => [ keys %$rdata ],
711                'style_sheet' => $stylesheet
712                });
713        $graph->add_data({
714                'data' => [ values %$rdata ],
715                });
716
717        open(SVG,">$graph_dir/$gname".".svg") || die "could not create $graph_dir/$gname".".svg: $!\n";
718        print SVG $graph->burn();
719        close(SVG);
720
721        return sprintf "<object type='image/svg+xml' data='%s.svg' height='%d' width='%d'></object>\n",$gname,$gheight,$gwidth;
722}
723
724# Pretty nasty stuff in here... sorry!
725sub graph_missing_data
726{
727    my ($template, $varname, $component) = @_;
728
729    my $day = 60*60*24; # 1 day
730    my $interval = 60 * 60; # 1 hour
731
732    # Figure out what "start of today" to "end of the 7th day from now"
733    # in localtime is in epoch seconds
734    my @now = localtime();
735    @now = (0, 0, 0, splice(@now, 3)); # reset to start of today
736    my $start = timelocal(@now);
737    my $end = $start + (7 * $day);
738
739    my $data;   # missing by channel
740    my $gdata;  # missing by grabber
741    foreach my $ch (keys %{$store->{missing_data}})
742    {
743        foreach my $slice (keys %{$store->{missing_data}->{$ch}})
744        {
745            my ($s, $f) = split(/-/,$slice);
746            $s = $s - $s%(60*60);
747
748            # We treat each unique combination of grabber and sysid ("csid") as
749            # 1 unit. That is, if 5 unique users all have a yahoo7widget hole, that's
750            # 5, and so is 1 unique user who has 5 different grabbers with a hole.
751            my @csids;
752            foreach my $c (keys %{$store->{missing_data}->{$ch}->{$slice}})
753            {
754                next if ($component and $component ne $c);
755                foreach my $sysid (keys %{$store->{missing_data}->{$ch}->{$slice}->{$c}})
756                {
757                    # Age out missing_data received more than 24 hours ago, since it's
758                    # probably stale; we want to allow newer information to shine through
759                    if ($store->{missing_data}->{$ch}->{$slice}->{$c}->{$sysid} + $day < $starttime)
760                    {
761                        delete $store->{missing_data}->{$ch}->{$slice}->{$c}->{$sysid};
762                        next;
763                    }
764                    my $csid = $c.'_'.$sysid;
765                    push @csids, $csid unless (grep ($_ eq $csid, @csids));
766                }
767            }
768            for (my $i = $s; $i < $f; $i += $interval)
769            {
770                next if ($i < $start or $i > $end);
771                my $n = ((($i - $start) / $interval) * 2) + 1;
772
773                foreach my $csid (@csids)
774                {
775                    push @{$data->{$ch}->[$n]}, $csid unless (grep ($_ eq $csid, @{$data->{$ch}->[$n]}));
776                    if (!$component and $csid =~ /(.+)_(.+)/)
777                    {
778                        my $c = $1;
779                        my $sid = $2;
780                        push @{$gdata->{$c}->[$n]}, $sid unless (grep $_ eq $sid, @{$gdata->{$c}->[$n]});
781                    }
782                }
783            }
784        }
785    } 
786
787    my @graphs;
788    &pad_missing_data_hash($data, $start, $end, $interval);
789    my $gname = ($component ? 'c_'.$component.'_' : '') . 'missing_by_channel';
790    my $gtitle = 'Missing ' . ($component ? "$component " : '') . 'Data by Channel';
791    push @graphs, { GRAPH => &graph_one_count_data($gname, $gtitle, "%a %H:%M", '12 hours', [ values %$data ], 220, 1010, 0, [ keys %$data ]) };
792
793    unless ($component)
794    {
795        &pad_missing_data_hash($gdata, $start, $end, $interval);
796        push @graphs, { GRAPH => &graph_one_count_data('missing_by_grabber', 'Missing Data by Grabber', "%a %H:%M", '12 hours', [ values %$gdata ], 220, 1010, 0, [ keys %$gdata ]) };
797    }
798
799    $template->param($varname => \@graphs);
800}
801
802# Pad out the hash of 'missing data' with timestamps and 0s to make
803# a timeseries array suitable for graphing
804sub pad_missing_data_hash
805{
806    my ($h, $start, $end, $interval) = @_;
807
808    foreach my $key (keys %$h)
809    {
810        for (my $n = 0; $n <= @{$h->{$key}}; $n += 2)
811        {
812            my $t = $start + ($n / 2 * $interval);
813            last if ($t > $end);
814            $h->{$key}->[$n] = strftime("%Y-%m-%d %H:%M:%S", localtime($t));
815            if (!defined $h->{$key}->[$n+1])
816            {
817                $h->{$key}->[$n+1] = 0;
818            }
819            else
820            {
821                $h->{$key}->[$n+1] = scalar(@{$h->{$key}->[$n+1]});
822            }
823        }
824    }
825}
826
827###############################################################################
828
829sub graph_mins_grabbed
830{
831    my ($when, $startwhen, $endwhen, $title, $gheight, $gwidth) = @_;
832
833    my $gname = "mins_grabbed_".$when."_".$startwhen."_".$endwhen;
834
835    # gather up data
836    my %data;
837    for (my $i = $startwhen; $i < $endwhen; $i++) 
838    {
839        foreach my $c (keys %{$store->{$when}->[$i]->{mins_grabbed_list}})
840        {
841            $data{$c} += $store->{$when}->[$i]->{mins_grabbed_list}->{$c};
842        }
843    }
844
845    # sort it into key order
846    my $min, my $max;
847    my @sorted_keys, my @sorted_data;
848    foreach my $k (sort { $data{$b} <=> $data{$a} } keys %data) 
849    {
850        push(@sorted_keys,$k);
851        push(@sorted_data, int($data{$k}/60));
852
853        $min = $data{$k} if (!defined $min);
854        $max = $data{$k} if (!defined $max);
855        $min = $data{$k} if ($data{$k} < $min);
856        $max = $data{$k} if ($data{$k} > $max);
857    }
858
859    # work out our scaling
860    my $scalediv = ($max - $min) / 3;
861    $scalediv = $max if ($scalediv < 1);
862
863    # only graph if we have some data
864    return if (($min == 0) && ($max == 0));
865
866    print "Generating bar graph $gname ..\n" if ($debug);
867    my $graph = SVG::TT::Graph::Bar->new({
868                'show_graph_title' => 1,
869                'graph_title' => $title,
870                'show_data_values' => 1,
871                'height' => $gheight,
872                'width' => $gwidth,
873                'stagger_x_labels' => 1,
874                'show_x_labels' => 1,
875                'show_y_labels' => 0,
876                'show_data_values' => 1,
877                'scale_divisions' => $scalediv,
878                'style_sheet' => $stylesheet,
879                'fields' => \@sorted_keys,
880                });
881        $graph->add_data({
882                'data' => \@sorted_data,
883                });
884
885        open(SVG,">$graph_dir/$gname".".svg") || die "could not create $graph_dir/$gname".".svg: $!\n";
886        print SVG $graph->burn();
887        close(SVG);
888
889        return sprintf "<object type='image/svg+xml' data='%s.svg' height='%d' width='%d'></object>\n",$gname,$gheight,$gwidth;
890}
891
892###############################################################################
893
894sub graph_one_count_data
895{
896        my ($gname, $title, $time_format, $timediv, $data, $gheight, $gwidth, $nofill, $keys) = @_;
897
898        $gheight ||= 120;
899        $gwidth ||= 250;
900        $nofill = 0 unless $nofill;
901
902        print "Generating graph $gname ($title)...\n" if ($debug);
903        # work out scale..
904        my $min, my $max;
905        foreach my $dset (@$data) {
906                my $valnum = 0;
907                foreach my $val (@$dset) {
908                        $valnum++;
909                        next if ($valnum % 2 == 1);
910
911                        $min = $val if (!defined $min);
912                        $max = $val if (!defined $max);
913                        $min = $val if ($val < $min);
914                        $max = $val if ($val > $max);
915                }
916        }
917        my $scalediv = ($max-$min)/3;
918        $scalediv = 1 if ($scalediv < 1);
919
920        return if (($min == 0) && ($max == 0));
921
922        my $graph = SVG::TT::Graph::TimeSeries->new({
923                'show_graph_title' => 1,
924                'graph_title' => $title,
925                'height' => $gheight,
926                'width' => $gwidth,
927                'stagger_x_labels' => 1,
928                'area_fill' => !$nofill,
929                'rollover_values' => 1,
930                'show_data_values' => 1,
931                'x_label_format' => $time_format,
932                'scale_divisions' => $scalediv,
933                'timescale_divisions' => $timediv,
934                'key' => ($keys ? 1 : 0),
935                'key_position' => 'bottom',
936                'style_sheet' => $stylesheet
937                });
938
939        foreach my $dset (@$data) {
940                my $key = shift @$keys;
941                $graph->add_data({
942                        'data' => \@$dset,
943                        'title' => $key
944                        });
945        }
946
947        open(SVG,">$graph_dir/$gname".".svg") || die "could not create $graph_dir/$gname".".svg: $!\n";
948        print SVG $graph->burn();
949        close(SVG);
950
951        return sprintf "<object type='image/svg+xml' data='%s.svg' height='%d' width='%d'></object>\n",$gname,$gheight,$gwidth;
952}
953
954###############################################################################
955# produces sets of graphs, from requests like "day week 6month".
956
957sub graph_count_data
958{
959        my ($template, $varname, $request, $gheight, $gwidth, $show_key, @fields) = @_;
960
961        my @requests = split(/ /, $request);
962        my @graphs;
963
964        my $tdata = {
965                        hour => { n => 3600, tformat => "%a %H:%M", abbrev => "hr" },
966                        day =>  { n => 86400, tformat => "%a %e %b", abbrev => "day", section => "24hour" },
967                        week => { n => 604800, tformat => "%e %b %y", abbrev => "wk", section => "7day" },
968                        month =>{ n => 2592000, tformat => "%b %y", abbrev => "mn" },
969                        year => { section => "12month" }
970                    };
971
972        foreach my $r (@requests)
973        {
974                my $title;
975                if ($tdata->{$r})
976                {
977                        $title = "Last $r";
978                        $r = $tdata->{$r}->{section};
979                }
980                next unless ($r =~ /(\d+)(\w+)/);
981                my $max = $1;
982                my $timeslice = $2;
983                $title = "Last $max $timeslice" . 's' unless ($title);
984
985                my $fieldnum = 0, my $d, my $fielddesc = "", my @keys;
986                foreach my $field (@fields) 
987                {
988                        my $anydata = 0;
989                        my @data;
990                        for (my $i = ($max-1); $i >= 0; $i--) 
991                        {
992                                my $val = numvar($store->{"per_$timeslice"}->[$i]->{$field},$field,0);
993                                if ($field =~ /duration/)
994                                {
995                                    $val /= 60; # convert to minutes
996                                    if ($field =~ /duration$/)  # average
997                                    {
998                                        $val /= numvar($store->{"per_$timeslice"}->[$i]->{duration_count},$field,1);
999                                    }
1000                                    $val = int($val + 0.49);
1001                                }
1002                                my $time_offset = $starttime - ($i * $tdata->{$timeslice}->{n});
1003                                push(@data, strftime("%Y-%m-%d %H:%M:%S", localtime($time_offset)), $val);
1004                                $anydata = 1 if ($val);
1005                        }
1006                        $fielddesc .= $field."_";
1007
1008                        # Prevent unnecessary entries in keyed graphs
1009                        next if ($show_key and !$anydata);
1010
1011                        push (@{$d->[$fieldnum]}, @data);
1012                        $fieldnum++;
1013                        if ($show_key)
1014                        {
1015                            my $k = $field;
1016                            $k = $1 if ($k =~ /^c_(.+)_fail/);
1017                            push (@keys, $k);
1018                        }
1019                }
1020                $fielddesc = substr($fielddesc, 0, 150) . "-more-" if (length($fielddesc) > 160);
1021                my $ts = int($max > 20 ? $max / 4 : $max > 6 ? 3 : 2);
1022                push @graphs, { GRAPH => &graph_one_count_data($fielddesc.$tdata->{$timeslice}->{abbrev}, 
1023                                                               $title, 
1024                                                               $tdata->{$timeslice}->{tformat},
1025                                                               "$ts $timeslice" . 's',
1026                                                               $d,
1027                                                               $gheight,
1028                                                               $gwidth,
1029                                                               ($varname =~ /duration/i ? 1 : 0),
1030                                                               ($show_key ? \@keys : undef) ) }; 
1031        }
1032
1033        if ($template)
1034        {
1035            $template->param($varname => \@graphs);
1036        }
1037        else
1038        {
1039            return \@graphs;
1040        }
1041}
1042
1043###############################################################################
1044
1045sub generate_reports
1046{
1047        my $template = HTML::Template->new(filename => "$template_dir/server_daily_maint_main.tmpl");
1048        $template->param(NOW => strftime "%a %b %e %H:%M:%S %Y %z", localtime($starttime));
1049
1050        &report_count_data($template, 'SUCCESS', 'success_sysid_list', 'all');
1051        &report_count_data($template, 'MISSING', 'missing_sysid_list', 'all');
1052        &report_count_data($template, 'FAILURE', 'fail_sysid_list', 'all');
1053        &graph_count_data($template, 'GRAPHS_USAGE', 'day week 12week year', 0, 0, 0, 'success_sysid_list', 'missing_sysid_list', 'fail_sysid_list');
1054
1055        &graph_missing_data($template, 'GRAPHS_MISSING');
1056
1057        &report_count_data($template, 'RUNS_SEEN', "visits", "all");
1058        &graph_count_data($template, 'GRAPHS_RUNS', "day week 12week year", 0, 0, 0, "visits");
1059
1060        my @graphs = ();
1061        push @graphs, { GRAPH => &graph_runtime("per_day", 0, 7, "Last week") };
1062        push @graphs, { GRAPH => &graph_runtime("per_month", 0, 1, "Last month") };
1063        $template->param('GRAPHS_RUNTIME', \@graphs);
1064
1065        my @duration;
1066        foreach my $timeslice ( 'week', 'month' )
1067        {
1068                my $h;
1069                $h->{'TIMESLICE'} = $timeslice;
1070                $h->{'AVG'} = sec2hms(numvar($store->{"per_$timeslice"}->[0]->{duration},"",0) / $store->{"per_$timeslice"}->[0]->{duration_count}) if ($store->{"per_$timeslice"}->[0]->{duration_count});
1071                $h->{'MIN'} = sec2hms(numvar($store->{"per_$timeslice"}->[0]->{duration_min},"",0));
1072                $h->{'MAX'} = sec2hms(numvar($store->{"per_$timeslice"}->[0]->{duration_max},"",0));
1073                push @duration, $h;
1074        }
1075        $template->param('DURATION' => \@duration);
1076
1077        &graph_count_data($template, 'GRAPHS_DURATION', "day week 12week year", 0, 0, 0, "duration_min", "duration", "duration_max");
1078
1079        @graphs = ( );
1080        my $rdata = &gather_region_data("per_day", 0, 7, 1);
1081        my @regions = ();
1082        my $top = 10; # list top 5
1083        foreach (sort { $rdata->{$b} <=> $rdata->{$a} } keys %$rdata)
1084        {
1085            push @regions, { REGION => $_, SEEN => $rdata->{$_} };
1086            $top--;
1087            last if ($top < 1);
1088        }
1089        $template->param('REGIONS', \@regions);
1090        push @graphs, { GRAPH => graph_regions("per_day", 0, 7, "Last week") };
1091        push @graphs, { GRAPH => graph_regions("per_month", 0, 6, "Last 6 months") };
1092        $template->param('GRAPHS_REGION', \@graphs);
1093
1094        my @components_used;
1095        foreach my $timeslice ( 'day', 'week' )
1096        {
1097                my $h;
1098                my @cu_timeslices;
1099                $top = 5;  # only show top 5
1100                $h->{'TIMESLICE'} = $timeslice;
1101                foreach my $c ( 
1102                        sort { $store->{"per_$timeslice"}->[0]->{components_used_order_list}->{$b} <=> $store->{"per_$timeslice"}->[0]->{components_used_order_list}->{$a} }
1103                        keys %{($store->{"per_$timeslice"}->[0]->{components_used_order_list})}) 
1104                {
1105                        my $h2;
1106                        my $val = $store->{"per_$timeslice"}->[0]->{components_used_order_list}->{$c};
1107                        $h2->{'TIMES'} = $val . " time" . ($val != 1 ? "s" : "");
1108                        $h2->{'DETAIL'} = $c;
1109                        push @cu_timeslices, $h2;
1110                        $top--;
1111                        last if ($top < 1);
1112                }
1113                $h->{'COMPONENTS_USED_TIMESLICES'} = \@cu_timeslices;
1114                push @components_used, $h;
1115        }
1116        $template->param('COMPONENTS_USED' => \@components_used);
1117
1118        # build a component list
1119        my %clist;
1120        foreach my $c (sort keys %{($store->{total})}) {
1121                if ($c =~ /^c_(.*)_runs$/) {
1122                        # skip if any '/' or '`' chars in name
1123                        next if (($1 =~ /\//) || ($1 =~ /`/));
1124                        $clist{$1}++;
1125                }
1126        }
1127
1128        my (@components, @cfailures, @counters);
1129        my $ccount = 0;
1130        foreach my $c (sort keys %clist) {
1131                my $h;
1132                my @details;
1133                $h->{'NAME'} = $c;
1134                $h->{'FOURTH'} = 1 if (($ccount % 4 == 0) && ($ccount > 0));
1135                my @ret = report_count_data(0, 0, "c_".$c."_success_sysid_list", "all");
1136                $h->{'COMPONENT_SUCCESS_RUNS'} = $ret[0];
1137                @ret = report_count_data(0, 0, "c_".$c."_fail_sysid_list", "all", "ignore-zero");
1138                $h->{'COMPONENT_FAIL_RUNS'} = $ret[0];
1139
1140                $h->{'SUCCESS_RATE_DAY'} = calc_success_rate($c, "day");
1141                $h->{'SUCCESS_RATE_WEEK'} = calc_success_rate($c, "week");
1142
1143                push @components, $h;
1144                my @uniques = report_count_data(0, 0, "c_".$c."_fail_sysid_list", "brief", "ignore-zero");
1145                push @cfailures, { NAME => $c, TIMESLICES => $uniques[0] } if (@{$uniques[0]});
1146                push @counters, "c_".$c."_fail_sysid_list";
1147
1148                $ccount++;
1149        }
1150        $template->param('COMPONENTS', \@components);
1151        $template->param('CFAILURES', \@cfailures);
1152        &graph_count_data($template, 'CFAILURE_GRAPHS', 'day week', 220, 1010, 1, @counters);
1153
1154        @graphs = ();
1155        push @graphs, { GRAPH => &graph_mins_grabbed('per_hour', 0, 24, "Last day", 120, 505) };
1156        push @graphs, { GRAPH => &graph_mins_grabbed('per_day', 0, 7, "Last week", 120, 505) };
1157        $template->param('MINS_GRABBED_GRAPHS', \@graphs);
1158
1159        print "Generating index.html...\n\n" if ($debug);
1160        open(F,">$graph_dir/index.html") || die "couldn't open $graph_dir/index.html for writing: $!\n";
1161        $template->output(print_to => *F);
1162        close(F);
1163
1164        #
1165        # iterate through every component we have seen, sorted alphabetically
1166        # generate per-component statistics
1167        #
1168
1169        foreach my $c (sort keys %clist) 
1170        {
1171
1172                next if ($c eq 'shepherd'); # shouldn't be there; is sneaking in via parsing error
1173
1174                $template = HTML::Template->new(filename => "$template_dir/server_daily_maint_component.tmpl");
1175                $template->param(NOW => "". localtime($starttime));
1176                $template->param('NAME', $c);
1177
1178                &report_count_data($template, 'SUCCESS', 'c_'.$c.'_success_sysid_list', 'all');
1179                &report_count_data($template, 'MISSING', 'c_'.$c.'_missing_sysid_list', 'all');
1180                &report_count_data($template, 'FAILURE', 'c_'.$c.'_fail_sysid_list', 'all');
1181                &graph_count_data($template, 'GRAPHS_USAGE', 'day week 12week year', 0, 0, 0, 'c_'.$c.'_success_sysid_list', 'c_'.$c.'_missing_sysid_list', 'c_'.$c.'_fail_sysid_list');
1182
1183                &graph_missing_data($template, 'GRAPHS_MISSING', $c);
1184
1185                &report_count_data($template, 'RUNS_SEEN', 'c_'.$c.'_runs', "all");
1186                &graph_count_data($template, 'GRAPHS_RUNS', "day week 12week year", 0, 0, 0, 'c_'.$c.'_runs');
1187
1188                &report_failure_msgs($template, $c);
1189
1190                my @sf; 
1191                foreach my $w ("success", "failure") 
1192                {
1193                        my $h;
1194                        $h->{NAME} = $w;
1195
1196                        my $g = "c_".$c."_".$w;
1197
1198                        my @timeslices;
1199                        foreach my $timeslice ("day", "month")
1200                        {
1201                            my @versions;
1202                            foreach my $c 
1203                            (
1204                                sort { $store->{"per_$timeslice"}->[0]->{$g."_version_list"}->{$b} <=> $store->{"per_$timeslice"}->[0]->{$g."_version_list"}->{$a} }
1205                                keys %{($store->{"per_$timeslice"}->[0]->{$g."_version_list"})}) {
1206                                    my $val = $store->{"per_$timeslice"}->[0]->{$g."_version_list"}->{$c};
1207                                    my $h2;
1208                                    $h2->{'TIMES'} = sprintf("%d time%s", $val, ($val != 1 ? "s" : ""));
1209                                    $h2->{'VERSION'} = $c;
1210                                    push @versions, $h2;
1211                            }
1212                            push @timeslices, { TIMESLICE => $timeslice, VERSIONS => \@versions };
1213                        }
1214                        $h->{'SFV_TIMESLICES'} = \@timeslices;
1215
1216                        my @durations = ( );
1217                        foreach my $timeslice ("day", "month")
1218                        {
1219                            if  ((defined $store->{"per_$timeslice"}->[0]->{$g."_duration_count"}) && ($store->{"per_$timeslice"}->[0]->{$g."_duration_count"} > 0))
1220                            {
1221                                push @durations, { AVG => sec2hms(numvar($store->{"per_$timeslice"}->[0]->{$g."_duration"},"",0) / $store->{"per_$timeslice"}->[0]->{$g."_duration_count"}),
1222                                                MIN => sec2hms(numvar($store->{"per_$timeslice"}->[0]->{$g."_duration_min"},"",0)),
1223                                                MAX => sec2hms(numvar($store->{"per_$timeslice"}->[0]->{$g."_duration_max"},"",0)) };
1224                            }
1225                        }
1226                        $h->{'DURATIONS'} = \@durations;
1227
1228                        $h->{'GRAPHS_DURATIONS'} = &graph_count_data(undef, undef, "day week 12week year", 0, 0, 0, $g."_duration", $g."_duration_min", $g."_duration_max");
1229                        push @sf, $h;
1230                }
1231                $template->param(SUCCESS_FAILURE => \@sf);
1232
1233                # iterate across every component statistic seen from this component
1234                my @statistics;
1235
1236# Nov 11 2007: removed individual stats to reduce load
1237if (0) {
1238   
1239                foreach my $s (sort keys %{($store->{total})}) {
1240                        next if ($s !~ /^cs\.(.*)\.(.*)_duration$/);
1241                        next if ($1 ne $c);
1242                        my $sname = $2;
1243                        my $svar = "cs\.".$c.".".$sname;
1244
1245                        my @timeslices = ();
1246                        foreach my $timeslice ("day", "month")
1247                        {
1248                            if ((defined $store->{"per_$timeslice"}->[0]->{$svar."_duration_count"}) && ($store->{"per_$timeslice"}->[0]->{$svar."_duration_count"} > 0))
1249                            {
1250                                my $h;
1251                                $h->{TIMESLICE} = $timeslice;
1252                                $h->{AVG} = nformat(numvar($store->{"per_$timeslice"}->[0]->{$svar."_duration"},"",0) / $store->{"per_$timeslice"}->[0]->{$svar."_duration_count"});
1253                                $h->{MIN} = nformat(numvar($store->{"per_$timeslice"}->[0]->{$svar."_duration_min"},"",0));
1254                                $h->{MAX} = nformat(numvar($store->{"per_$timeslice"}->[0]->{$svar."_duration_max"},"",0));
1255                                push @timeslices, $h;
1256                            }
1257                        }
1258                        push @statistics, { STATNAME => $sname, TIMESLICES => [ @timeslices ], GRAPHS_STATISTICS => &graph_count_data(undef, undef, "72hour 12week", 0, 0, 0, $svar."_duration", $svar."_duration_min", $svar."_duration_max")};
1259                }
1260
1261}
1262
1263                $template->param(STATISTICS => \@statistics);
1264
1265                open(F,">$graph_dir/$c.html") || die "couldn't open $graph_dir/$c for writing: $!\n";
1266                $template->output(print_to => *F); 
1267                close(F);
1268        }
1269}
1270
1271sub nformat
1272{
1273    my $n = shift;
1274    return sprintf("%0.2f", $n);
1275}
1276
1277###############################################################################
1278
Note: See TracBrowser for help on using the browser.