root/trunk/util/server_daily_maint @ 1083

Revision 1083, 40.8 kB (checked in by max, 5 years ago)

stats: wasn't rolling over day properly

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