Changeset 326

Show
Ignore:
Timestamp:
12/02/06 05:25:08 (6 years ago)
Author:
lincoln
Message:

drop RRD from server daily script and use our own home-grown graphing

Files:
1 modified

Legend:

Unmodified
Added
Removed
  • util/server_daily_maint

    r321 r326  
    11#!/usr/bin/perl 
    22 
    3 # server-side daily maintenance script 
     3# server-side maintenance script 
     4#  * can be run daily or multiple times per day 
     5#  * best if run (say) every 15 or 30 minutes (more granular data) 
     6# 
    47#  - deletes old feedback 
    5 #  - updates RRD of some common statistics (users, failures, duration, time-of-day) etc. 
    6  
     8#  - updates store of some common statistics (users, failures, duration, time-of-day) etc. 
     9#  - generates pretty graphs and html 
     10 
     11 
     12# settings 
    713my $days_to_keep = 14;          # keep 14 days 
    8 my $store_dir = $ENV{HOME} . "/whuffy.com/dev/stats";    # where we store our feedback 
     14my $store_dir = $ENV{HOME} . "/whuffy.com/dev/stats";    # where we retrieve our feedback 
    915my $graph_dir = $ENV{HOME} . "/whuffy.com/dev/feedback"; # where we store our graphs 
    10  
    11 my $debug = 0; 
     16my $debug = 1; 
    1217 
    1318use strict; 
    14 use RRD::Simple; 
    1519use Data::Dumper; 
     20use POSIX qw(strftime); 
     21use SVG::TT::Graph::TimeSeries; 
     22use SVG::TT::Graph::Bar; 
     23 
     24my $stats_file = $store_dir . "/stats.config"; 
     25my $store = { }; 
     26my $starttime = time; 
    1627 
    1728print "Running at " . localtime() . ".\n"; 
    1829 
    19 &delete_old_files; 
    20  
    21 my $rrd = RRD::Simple->new(); 
    22 my $last_updated = 0; 
    23  
    24 &check_rrd; 
    25 &update_stats; 
    26 &generate_graphs; 
     30&set_initial_values; 
     31&read_config;           # read in our existing stats 
     32 
     33print "Last run " . localtime($store->{last_run}) . ".\n"; 
     34 
     35&rollover_statistics;   # rotate statistics 
     36 
     37&delete_old_files;      # delete old feedback files 
     38&parse_feedback;        # parse any (new) feedback files 
     39 
     40&aggregate_stats;       # use last 7 days to build last week, etc. 
     41 
     42&generate_reports;      # generate reports and graphs 
     43 
     44$store->{last_run} = $starttime; # update our last_run time 
     45&write_config;          # write out our updated stats 
    2746 
    2847print "Done.\n"; 
     
    4160        foreach my $file (@files) { 
    4261                next if (!-f $file);                    # only work on normal files 
    43                 next if ($file !~ /^\d+$/);     # filename should be all digits 
     62                next if ($file !~ /^\d+\.done$/);       # only delete processed files 
    4463 
    4564                if ((-M $file) > $days_to_keep) { 
     
    5675 
    5776############################################################################### 
    58  
    59 sub check_rrd 
    60 { 
    61         # create usage RRD if it doesn't exist 
    62         if (!-e "$graph_dir/usage.rrd") { 
    63                 $rrd->create("$graph_dir/usage.rrd", "3years", 
    64                         "visits" => "ABSOLUTE:86400", 
    65                         "unique_visits" => "ABSOLUTE:86400", 
    66                         "success_total" => "ABSOLUTE:86400", 
    67                         "fail_total" => "ABSOLUTE:86400", 
    68                         "duration" => "ABSOLUTE:86400"); 
     77# read in our saved statistics 
     78 
     79sub read_config 
     80{ 
     81        if (-r $stats_file) { 
     82                local (@ARGV, $/) = ($stats_file); 
     83                no warnings 'all'; eval <>; die "$@" if $@; 
    6984        } else { 
    70                 # set $last_updated based on last update time of usage.rrd 
    71                 $last_updated = $rrd->last("$graph_dir/usage.rrd"); 
    72         } 
    73  
    74         # create time-of-day-usage if it doesn't exist 
    75         if (!-e "$graph_dir/timeofday.rrd") { 
    76                 $rrd->create("$graph_dir/timeofday.rrd", "3years", 
    77                         "starttime" => "ABSOLUTE:86400", 
    78                         "duration" => "ABSOLUTE:86400"); 
    79         } 
    80  
    81         printf "Gathering statistics for data since ".localtime($last_updated)."\n" if ($debug); 
    82 } 
    83  
    84 ############################################################################### 
    85  
    86 sub update_stats 
    87 { 
    88         my %daily_stats; 
    89         $daily_stats{visits} = 0; 
    90         $daily_stats{unique_visits} = 0; 
    91         $daily_stats{success_total} = 0; 
    92         $daily_stats{fail_total} = 0; 
    93         $daily_stats{duration} = 0; 
    94  
    95         my %seen_sysid_list; 
    96         my %timeofday_start; 
    97         my %timeofday_duration; 
    98  
     85                printf "WARNING: no config file $stats_file - ok if this is the first time running!\n"; 
     86 
     87                # try to write to it - if we can't then this will cause us to barf 
     88                &write_config; 
     89        } 
     90} 
     91 
     92############################################################################### 
     93# write out our saved statistics 
     94 
     95sub write_config 
     96{ 
     97        $Data::Dumper::Sortkeys = 1; 
     98        $Data::Dumper::Indent = 1; 
     99        open(F,">$stats_file") || die "can't write to $stats_file: $!\n"; 
     100        print F Data::Dumper->Dump([$store], ["store"]); 
     101        close F; 
     102} 
     103 
     104############################################################################### 
     105# define some initial settings to basic values 
     106# reading the config file may override these 
     107 
     108sub set_initial_values 
     109{ 
     110        $store->{last_run} = 0; 
     111 
     112        $store->{num_hours} = 1; 
     113        $store->{num_days} = 0; 
     114        $store->{num_weeks} = 0; 
     115        $store->{num_months} = 0; 
     116 
     117        $store->{max_hours} = 72;       # keep per-hour stats for 72 hours (need min 24) 
     118        $store->{max_days} = 30;        # keep per-day stats for 30 days (need min 30) 
     119        $store->{max_weeks} = 12;       # keep per-week stats for 12 weeks 
     120        $store->{max_months} = 36;      # keep per-month stats for 36 months 
     121} 
     122 
     123############################################################################### 
     124# rollover 'daily' / 'weekly' / 'monthly' statistics if day/week/month is  
     125# different from the last time we were run 
     126 
     127sub rollover_statistics 
     128{ 
     129        # NOTE: this logic is working on GMT+10 time not whatever timezone 
     130        # this server is running in! 
     131         
     132        # 1. work out GMT offset 
     133        my $tzstring = strftime("%z", localtime(time)); 
     134        $store->{gmt_offset} = (60*60) * int(substr($tzstring,1,2));     # hr 
     135        $store->{gmt_offset} += (60 * int(substr($tzstring,3,2)));       # min 
     136        $store->{gmt_offset} *= -1 if (substr($tzstring,0,1) eq "-");    # +/- 
     137 
     138        # 2. work out difference from this timezone to GMT+10 
     139        $store->{tzdiff} = (10*60*60) - $store->{gmt_offset}; 
     140 
     141        # 3. correct starttime and last_run times 
     142        my $gmt10_starttime = $starttime + $store->{tzdiff}; 
     143        my $gmt10_last_run = $store->{last_run} + $store->{tzdiff}; 
     144 
     145        # 4. new month 
     146        if (strftime("%m", localtime($gmt10_last_run)) != strftime("%m", localtime($gmt10_starttime))) { 
     147                for (my $i = $store->{num_months}; $i > 0; $i--) { 
     148                        $store->{per_month}->[$i+1] = undef; 
     149                        $store->{per_month}->[$i+1] = $store->{per_month}->[$i] 
     150                                if (defined $store->{per_month}->[$i]) 
     151                } 
     152                if ($store->{num_months} >= $store->{max_months}) { 
     153                        delete $store->{per_month}->[($store->{num_months})]; 
     154                } else { 
     155                        $store->{num_months}++; 
     156                } 
     157        } 
     158 
     159        # 5. new week 
     160        if (strftime("%U", localtime($gmt10_last_run)) != strftime("%U", localtime($gmt10_starttime))) { 
     161                for (my $i = $store->{num_weeks}; $i > 0; $i--) { 
     162                        $store->{per_week}->[$i+1] = undef; 
     163                        $store->{per_week}->[$i+1] = $store->{per_week}->[$i] 
     164                                if (defined $store->{per_week}->[$i]); 
     165                } 
     166                if ($store->{num_weeks} >= $store->{max_weeks}) { 
     167                        delete $store->{per_week}->[($store->{num_weeks})]; 
     168                } else { 
     169                        $store->{num_weeks}++; 
     170                } 
     171        } 
     172 
     173        # 6. new day 
     174        if (strftime("%w", localtime($gmt10_last_run)) != strftime("%w", localtime($gmt10_starttime))) { 
     175                for (my $i = $store->{num_days}; $i > 0; $i--) { 
     176                        $store->{per_day}->[$i+1] = undef; 
     177                        $store->{per_day}->[$i+1] = $store->{per_day}->[$i] 
     178                                if (defined $store->{per_day}->[$i]); 
     179                } 
     180                if ($store->{num_days} >= $store->{max_days}) { 
     181                        delete $store->{per_day}->[($store->{num_days})]; 
     182                } else { 
     183                        $store->{num_days}++; 
     184                } 
     185        } 
     186 
     187        # 7. new hour 
     188        if (strftime("%H", localtime($gmt10_last_run)) != strftime("%H", localtime($gmt10_starttime))) { 
     189                for (my $i = $store->{num_hours}; $i > 0; $i--) { 
     190                        $store->{per_hour}->[$i+1] = undef; 
     191                        $store->{per_hour}->[$i+1] = $store->{per_hour}->[$i] 
     192                                if (defined $store->{per_hour}->[$i]); 
     193                } 
     194                if ($store->{num_hours} >= $store->{max_hours}) { 
     195                        delete $store->{per_hour}->[($store->{num_hours})]; 
     196                } else { 
     197                        $store->{num_hours}++; 
     198                } 
     199        } 
     200 
     201} 
     202 
     203############################################################################### 
     204# aggregate stats: 
     205#  - last 24 hours = day 0 statistics, 
     206#  - last 7 days = week 0 statistics, 
     207#  - last 30 days = month 0 statistics 
     208 
     209sub aggregate_stats 
     210{ 
     211        # build day 0 based on last 24 hours 
     212        delete $store->{per_day}->[0]; 
     213        for (my $i = 0; $i < 24; $i++) { 
     214                next if (!defined $store->{per_hour}->[$i]); 
     215                $store->{per_day}->[0] = &merge_in_stats($store->{per_day}->[0], $store->{per_hour}->[$i]); 
     216        } 
     217 
     218        # build week 0 based on last 7 days 
     219        delete $store->{per_week}->[0]; 
     220        for (my $i = 0; $i < 7; $i++) { 
     221                next if (!defined $store->{per_day}->[$i]); 
     222                $store->{per_week}->[0] = &merge_in_stats($store->{per_week}->[0], $store->{per_day}->[$i]); 
     223        } 
     224 
     225        # build month 0 based on last 30 days 
     226        delete $store->{per_month}->[0]; 
     227        for (my $i = 0; $i < 30; $i++) { 
     228                next if (!defined $store->{per_day}->[$i]); 
     229                $store->{per_month}->[0] = &merge_in_stats($store->{per_month}->[0], $store->{per_day}->[$i]); 
     230        } 
     231} 
     232 
     233############################################################################### 
     234 
     235sub record_duration 
     236{ 
     237        my ($where, $dur) = @_; 
     238        $where->{duration} += $dur; 
     239        $where->{duration_max} = $dur if (!defined $where->{duration_max}); 
     240        $where->{duration_max} = $dur if ($dur > $where->{duration_max}); 
     241        $where->{duration_min} = $dur if (!defined $where->{duration_min}); 
     242        $where->{duration_min} = $dur if ($dur < $where->{duration_max}); 
     243} 
     244 
     245############################################################################### 
     246# merge statistics in an intelligent manner 
     247#  - can recursively descend HASH lists 
     248#  - accumulates totals 
     249#  - fields with _min/_max suffix take the minimum/maximum value 
     250 
     251sub merge_in_stats 
     252{ 
     253        my ($to, $from) = @_; 
     254 
     255        foreach my $field (keys %{($from)}) { 
     256                if (ref($from->{$field}) eq "HASH") { 
     257                        $to->{$field} = &merge_in_stats($to->{$field}, $from->{$field}); 
     258                } elsif (defined $from->{$field}) { 
     259                        if (!defined $to->{$field}) { 
     260                                $to->{$field} = $from->{$field}; 
     261                        } else { 
     262                                # fields ending in _min and _max are special 
     263                                if ($field =~ /_min$/) { 
     264                                        $to->{$field} = $from->{$field} if ($from->{$field} < $to->{$field}); 
     265                                } elsif ($field =~ /_max$/) { 
     266                                        $to->{$field} = $from->{$field} if ($from->{$field} > $to->{$field}); 
     267                                } else { 
     268                                        $to->{$field} += $from->{$field}; 
     269                                } 
     270                        } 
     271                } 
     272        } 
     273        return $to; 
     274} 
     275 
     276############################################################################### 
     277 
     278sub parse_feedback 
     279{ 
    99280        chdir($store_dir) || die "can't chdir $store_dir: $!\n"; 
    100281        opendir(DIR, $store_dir) || die "can't opendir $store_dir: $!\n"; 
     
    104285        foreach my $file (@files) { 
    105286                next if (!-f $file);                    # only work on normal files 
    106                 next if ($file !~ /^\d+$/);      
    107                 next if ((stat(_))[9] < $last_updated); # only look at newly modified files 
    108  
    109                 my $starttime = (stat(_))[9]; 
    110                 printf " - gathering statistics from $store_dir/$file ($starttime)\n" if ($debug); 
     287                next if ($file !~ /^\d+$/);             # only look at non-processed files 
     288                next if ((stat(_))[9] < $store->{last_updated}); # only look at newly modified files 
     289 
     290                my $file_modtime = (stat(_))[9]; 
     291                printf " - gathering statistics from $store_dir/$file ($file_modtime)\n" if ($debug); 
    111292 
    112293                open(F,"<$store_dir/$file") || die "could not open $store_dir/$file: $!\n"; 
     
    120301                                # usage statistics 
    121302                                # 
    122                                 $daily_stats{visits}++; 
    123                                 $daily_stats{duration} += ($duration/60);               # convert to minutes 
    124                                 if (!defined $seen_sysid_list{$sysid}) { 
    125                                         $seen_sysid_list{$sysid} = 1; 
    126                                         $daily_stats{unique_visits}++; 
    127                                 } 
    128  
    129                                 # 
    130                                 # time of day statistics 
    131                                 # (note: we're using starttime based on FILE MODIFICATION time and not 
    132                                 #  what the user tells us. this is because RRD can only add values) 
    133                                 $timeofday_start{$starttime}++; 
    134                                 $timeofday_duration{$starttime} += ($duration/60);      # convert to minutes 
    135  
    136                         } elsif ($_ =~ /^(\S+)\t(\S+)\t/) { 
    137                                 my ($component, $status) = ($1, $2); 
    138  
    139                                 # 
    140                                 # success/fail usage statistics 
    141                                 # 
    142                                 if ($status eq "SUCCESS") { 
    143                                         $daily_stats{success_total}++; 
    144                                 } elsif ($status eq "FAIL") { 
    145                                         $daily_stats{fail_total}++; 
    146                                 } 
     303                                $store->{per_hour}->[0]->{visits}++;             # total # of visits seen 
     304                                $store->{total}->{visits}++; 
     305 
     306                                # shepherd's duration (total count, min, max, average from duration/visits) 
     307                                &record_duration($store->{per_hour}->[0], $duration); 
     308                                &record_duration($store->{total}, $duration); 
     309 
     310                                # list of sysid's seen (used to derive # unique users) 
     311                                $store->{per_hour}->[0]->{seen_sysid_list}->{$sysid}++; 
     312                                $store->{total}->{seen_sysid_list}->{$sysid}++; 
     313 
     314                                # where user is 
     315                                my $gmt_offset_string = sprintf "%s%02d%02d", 
     316                                        ($gmt_offset < 0 ? "-" : "+"), 
     317                                        int($gmt_offset / 3600), 
     318                                        int(($gmt_offset / 3600) / 60); 
     319                                $store->{per_hour}->[0]->{gmt_offset_list}->{$gmt_offset_string}++; 
     320 
     321                                # when shepherd was run (in GMT+10), normalized to 15 minute buckets 
     322                                my $when_run = strftime("%H:%M", localtime(($file_modtime-($file_modtime%(15*60))) + $store->{tzdiff})); 
     323                                $store->{per_hour}->[0]->{when_run_list}->{"$when_run"}++; 
     324                                $store->{total}->{when_run_list}->{"$when_run"}++; 
     325 
     326                                # what components were used (and in what order?) 
     327                                $store->{per_hour}->[0]->{components_used_order_list}->{$components_used}++; 
     328 
     329                        } elsif ($_ =~ /^(\S+)\tSUCCESS\t(\S+)\t(\S+)\t(\S+)\t(\S+)/) { 
     330                                # success line 
     331                                my ($c, $retcode, $c_start, $c_dur, $c_ver) = ($1, $2, $3, $4, $5); 
     332 
     333                                my $g = "c_".$c."_success"; 
     334                                $store->{per_hour}->[0]->{$g}->{counter}++; 
     335                                $store->{total}->{$g}->{counter}++; 
     336 
     337                                # duration (total count, min, max, average from duration/counter) 
     338                                &record_duration($store->{per_hour}->[0]->{$g}, $c_dur); 
     339                                &record_duration($store->{total}->{$g}, $c_dur); 
     340 
     341                                # version count 
     342                                $store->{per_hour}->[0]->{$g}->{version_list}->{$c_ver}++; 
     343                        } elsif ($_ =~ /^(\S+)\tFAIL\t(\S+)\t(\S+)\t(\S+)\t(\S+)\t(\S+)/) { 
     344                                # fail line 
     345                                my ($c, $retcode, $c_start, $c_dur, $c_ver, $num_failures) = ($1, $2, $3, $4, $5, $6); 
     346 
     347                                my $g = "c_".$c."_fail"; 
     348                                $store->{per_hour}->[0]->{$g}->{counter}++; 
     349                                $store->{total}->{$g}->{counter}++; 
     350 
     351                                # duration (total count, min, max, average from duration/counter) 
     352                                &record_duration($store->{per_hour}->[0]->{$g}, $c_dur); 
     353                                &record_duration($store->{total}->{$g}, $c_dur); 
     354 
     355                                # version count 
     356                                $store->{per_hour}->[0]->{$g}->{version_list}->{$c_ver}++; 
     357                        } elsif ($_ =~ /^(\S+)\tstats\t(.*)$/) { 
     358                                # component statistics line 
     359                                ; # ignore for now 
    147360                        } 
    148361                } 
    149362                close(F); 
    150         } 
    151  
    152         # average 
    153         if ($daily_stats{visits} > 0) { 
    154                 $daily_stats{duration} /= $daily_stats{visits}; 
    155                 $daily_stats{success_total} /= $daily_stats{visits}; 
    156                 $daily_stats{fail_total} /= $daily_stats{visits}; 
    157         } 
    158  
    159         # update usage statistics 
    160         $rrd->update("$graph_dir/usage.rrd", time, 
    161                 "visits" => $daily_stats{visits}, 
    162                 "unique_visits" => $daily_stats{unique_visits}, 
    163                 "success_total" => $daily_stats{success_total}, 
    164                 "fail_total" => $daily_stats{fail_total}, 
    165                 "duration" => $daily_stats{duration}); 
    166         printf "updated usage statistics:\n".Dumper(%daily_stats) if ($debug); 
    167  
    168         # update time-of-day statistics 
    169         foreach my $epoch (sort keys %timeofday_start) { 
    170                 $timeofday_duration{$epoch} /= $timeofday_start{$epoch}; 
    171                 $rrd->update("$graph_dir/timeofday.rrd", $epoch, 
    172                         "starttime" => $timeofday_start{$epoch}, 
    173                         "duration" => $timeofday_duration{$epoch}); 
    174                 printf "updated time-of-day statistics: start=%d, count=%d, duration=%d\n", 
    175                   $epoch, $timeofday_start{$epoch}, $timeofday_duration{$epoch} if ($debug); 
    176         } 
    177 } 
    178  
    179 ############################################################################### 
    180  
    181 sub generate_graphs 
    182 { 
    183         # usage-daily.png, usage-weekly.png, 
    184         # usage-monthly.png, usage-annual.png 
    185         $rrd->graph("$graph_dir/usage.rrd", 
    186                 destination => "$graph_dir", 
    187                 basename => "usage", 
    188                 extended_legend => 1); 
    189  
    190         # timeofday-daily.png, timeofday-weekly.png, 
    191         # timeofday-monthly.png, timeofday-annual.png 
    192         $rrd->graph("$graph_dir/timeofday.rrd", 
    193                 destination => "$graph_dir", 
    194                 basename => "timeofday"); 
    195 } 
    196  
    197 ############################################################################### 
    198  
    199  
     363 
     364                # rename processed files so we don't re-process them 
     365                rename("$file", "$file.done") if (!$debug); 
     366        } 
     367} 
     368 
     369############################################################################### 
     370 
     371sub numvar 
     372{ 
     373        my ($var,$field,$notdef) = @_; 
     374        $notdef = "n/a" if (!defined $notdef); 
     375        $field = "" if (!defined $field); 
     376 
     377        return $notdef if (!defined $var); 
     378 
     379        if ($field =~ /_list$/) { 
     380                # fields which end in _list are HASHes - return # of keys 
     381                my $count = keys %{$var}; 
     382                return $count; 
     383        } else { 
     384                return $var; 
     385        } 
     386} 
     387 
     388############################################################################### 
     389 
     390sub report_count_data 
     391{ 
     392        my ($field) = @_; 
     393        my $r = ""; 
     394 
     395        $r .= sprintf "<li>in last 60 minutes: <b>%s</b> (%s previous)<br>\n", 
     396                numvar($store->{per_hour}->[0]->{$field},$field), numvar($store->{per_hour}->[1]->{$field},$field); 
     397        $r .= sprintf "<li>in last 24 hours: <b>%s</b> (previous %s)<br>\n", 
     398                numvar($store->{per_day}->[0]->{$field},$field), numvar($store->{per_day}->[1]->{$field},$field); 
     399        $r .= sprintf "<li>in last 7 days: <b>%s</b> (previous %s)<br>\n", 
     400                numvar($store->{per_week}->[0]->{$field},$field), numvar($store->{per_week}->[1]->{$field},$field); 
     401        $r .= sprintf "<li>in last 30 days: <b>%s</b> (previous %s)<br>\n",  
     402                numvar($store->{per_month}->[0]->{$field},$field), numvar($store->{per_month}->[1]->{$field},$field); 
     403        $r .= sprintf "<li><b>TOTAL SEEN: <u>%s</u></b><br>\n", numvar($store->{total}->{$field},$field); 
     404 
     405        return $r; 
     406} 
     407 
     408############################################################################### 
     409 
     410sub sec2hms 
     411{ 
     412        my $sec = shift; 
     413 
     414        return sprintf "%dh %02dm %02ds", 
     415                int($sec / 3600), 
     416                int(($sec % 3600) / 60), 
     417                $sec % 60; 
     418} 
     419 
     420############################################################################### 
     421 
     422sub graph_hash_data 
     423{ 
     424        my ($field, $when, $startwhen, $endwhen, $title, $gheight, $gwidth) = @_; 
     425 
     426        my $gname = $field."_".$when."_".$startwhen."_".$endwhen; 
     427 
     428        # gather up data 
     429        my %data; 
     430        for (my $i = $startwhen; $i < $endwhen; $i++) { 
     431                if (defined $store->{$when}->[$i]->{$field}) { 
     432                        foreach my $k (keys %{($store->{$when}->[$i]->{$field})}) { 
     433                                $data{$k} += $store->{$when}->[$i]->{$field}->{$k}; 
     434                        } 
     435                } 
     436        } 
     437 
     438        # sort it into key order 
     439        my $min, my $max; 
     440        my @sorted_keys, my @sorted_data; 
     441        foreach my $k (sort keys %data) { 
     442                push(@sorted_keys,$k); 
     443                push(@sorted_data,$data{$k}); 
     444 
     445                $min = $data{$k} if (!defined $min); 
     446                $max = $data{$k} if (!defined $max); 
     447                $min = $data{$k} if ($data{$k} < $min); 
     448                $max = $data{$k} if ($data{$k} > $min); 
     449        } 
     450 
     451        # work out our scaling 
     452        my $scalediv = ($max - $min) / 3; 
     453        $scalediv = $max if ($scalediv < 1); 
     454 
     455        # only graph if we have some data 
     456        return if (($min == 0) && ($max == 0)); 
     457 
     458        print "Generating bar graph $gname ..\n" if ($debug); 
     459 
     460        my $graph = SVG::TT::Graph::Bar->new({ 
     461                'show_graph_title' => 1, 
     462                'graph_title' => $title, 
     463                'bar_gap' => 0, 
     464                'show_data_values' => 0, 
     465                'height' => $gheight, 
     466                'width' => $gwidth, 
     467                'stagger_x_labels' => 1, 
     468                'show_x_labels' => 1, 
     469                'bar_gap' => 0, 
     470                'show_data_values' => 1, 
     471                'scale_divisions' => $scalediv, 
     472                'fields' => \@sorted_keys, 
     473                }); 
     474        $graph->add_data({ 
     475                'data' => \@sorted_data, 
     476                }); 
     477 
     478        open(SVG,">$graph_dir/$gname".".svg") || die "could not create $graph_dir/$gname".".svg: $!\n"; 
     479        print SVG $graph->burn(); 
     480        close(SVG); 
     481 
     482        return sprintf "<object type='image/svg+xml' data='%s.svg' height='%d' width='%d'></object>\n",$gname,$gheight,$gwidth; 
     483} 
     484 
     485############################################################################### 
     486 
     487sub graph_one_count_data 
     488{ 
     489        my ($gname, $data, $title, $time_format, $timediv) = @_; 
     490        my $gheight = 120; 
     491        my $gwidth = 250; 
     492 
     493        print "Generating graph $gname ..\n" if ($debug); 
     494 
     495        # work out scale.. 
     496        my $min, my $max, my $valnum = 0; 
     497        foreach my $val (@$data) { 
     498                $valnum++; 
     499                next if ($valnum % 2 == 1); 
     500 
     501                $min = $val if (!defined $min); 
     502                $max = $val if (!defined $max); 
     503                $min = $val if ($val < $min); 
     504                $max = $val if ($val > $min); 
     505        } 
     506        my $scalediv = ($max - $min) / 3; 
     507        $scalediv = 1 if ($scalediv < 1); 
     508 
     509        return if (($min == 0) && ($max == 0)); 
     510 
     511        my $graph = SVG::TT::Graph::TimeSeries->new({ 
     512                'show_graph_title' => 1, 
     513                'graph_title' => $title, 
     514                'bar_gap' => 0, 
     515                'show_data_values' => 0, 
     516                'height' => $gheight, 
     517                'width' => $gwidth, 
     518                'stagger_x_labels' => 1, 
     519                'area_fill' => 1, 
     520                'rollover_values' => 1, 
     521                'show_data_values' => 1, 
     522                'x_label_format' => $time_format, 
     523                'scale_divisions' => $scalediv, 
     524                'timescale_divisions' => $timediv, 
     525                }); 
     526        $graph->add_data({ 
     527                'data' => $data, 
     528                }); 
     529 
     530        open(SVG,">$graph_dir/$gname".".svg") || die "could not create $graph_dir/$gname".".svg: $!\n"; 
     531        print SVG $graph->burn(); 
     532        close(SVG); 
     533 
     534        return sprintf "<object type='image/svg+xml' data='%s.svg' height='%d' width='%d'></object>\n",$gname,$gheight,$gwidth; 
     535} 
     536 
     537############################################################################### 
     538# produces up to 4 sets of graph 
     539 
     540sub graph_count_data 
     541{ 
     542        my ($field, $max_hrs, $max_days, $max_weeks, $max_months) = @_; 
     543        my $r = ""; 
     544 
     545        # hours 
     546        my @d1; 
     547        for (my $i = ($max_hrs-1); $i >= 0; $i--) { 
     548                my $val = numvar($store->{per_hour}->[$i]->{$field},$field,0); 
     549                my $time_offset = $starttime + $store->{tzdiff} - (60*60*$i); 
     550                push(@d1, strftime("%Y-%m-%d %H:%M:%S", localtime($time_offset)), $val); 
     551        } 
     552 
     553        # days 
     554        my @d2; 
     555        for (my $i = ($max_days-1); $i >= 0; $i--) { 
     556                my $val = numvar($store->{per_day}->[$i]->{$field},$field,0); 
     557                my $time_offset = $starttime + $store->{tzdiff} - (60*60*24*$i); 
     558                push(@d2, strftime("%Y-%m-%d %H:%M:%S", localtime($time_offset)), $val); 
     559        } 
     560 
     561        # weeks 
     562        my @d3; 
     563        for (my $i = ($max_weeks-1); $i >= 0; $i--) { 
     564                my $val = numvar($store->{per_week}->[$i]->{$field},$field,0); 
     565                my $time_offset = $starttime + $store->{tzdiff} - (60*60*24*7*$i); 
     566                push(@d3, strftime("%Y-%m-%d %H:%M:%S", localtime($time_offset)), $val); 
     567        } 
     568 
     569        # months 
     570        my @d4; 
     571        for (my $i = ($max_months-1); $i >= 0; $i--) { 
     572                my $val = numvar($store->{per_week}->[$i]->{$field},$field,0); 
     573                my $time_offset = $starttime + $store->{tzdiff} - (60*60*24*30*$i); 
     574                push(@d4, strftime("%Y-%m-%d %H:%M:%S", localtime($time_offset)), $val); 
     575        } 
     576 
     577        $r .= &graph_one_count_data($field."_hr", \@d1, "Last $max_hrs hours", "%a %H:%M", "6 hours") if (defined $max_hrs); 
     578        $r .= &graph_one_count_data($field."_day", \@d2, "Last $max_days days", "%a %e %b", "5 days") if (defined $max_days); 
     579        $r .= &graph_one_count_data($field."_wk", \@d3, "Last $max_weeks weeks", "%e %b %y", "3 weeks") if (defined $max_weeks); 
     580        $r .= &graph_one_count_data($field."_mn", \@d4, "Last $max_months months", "%b %y", "4 months") if (defined $max_months); 
     581        return $r; 
     582} 
     583 
     584############################################################################### 
     585 
     586sub generate_reports 
     587{ 
     588        open(F,">$graph_dir/main.html") || die "couldn't open $graph_dir for writing: $!\n"; 
     589 
     590        my $now = localtime($starttime); 
     591 
     592        print F <<EOF 
     593<html> 
     594<head> 
     595<title>Shepherd</title> 
     596<style> 
     597a { color: #F6B620; text-decoration: none; border-bottom: 1px dotted #F6B620; } 
     598a:hover { border: 1px dotted #FFEE40; } 
     599html, body, table { margin: 0; padding: 0; color: #c0bcb0; font: normal 12px Verdana; } 
     600body { text-align: left; margin: 1em 10 10 20; } 
     601body.home { background: #474642; } 
     602body.home h1 { font: bold 36px/.8em Slyphaen, Times, serif; color: #F6B620; border-bottom: 3px solid #F6B620; padding: 0; clear: both; text-align: left; } 
     603body.home h2 { font: normal 24px/.8em Slyphaen, Times, serif; color: #DFB020; padding: 0; clear: both; margin: 20 0 10 0; } 
     604body.home p { clear: both; } 
     605input { font: bold 12px Arial; } 
     606</style> 
     607</head> 
     608 
     609<body class="home"> 
     610<h1>Shepherd Overall statistics <font size=-1>(as of $now)</font></h1> 
     611<table style='border: 1px dotted; border-collapse: separate; border-spacing: 1px dotted; padding: 3px; cellpadding: 3px;' align=left> 
     612<tr> 
     613EOF 
     614; 
     615        print F "<tr'><td align=left valign=top nowrap><h2>Shepherd usage</h2>\n"; 
     616        print F "Number of times shepherd reported in.<br>\n"; 
     617        print F report_count_data("visits"); 
     618        print F "</td><td nowrap colspan=4><br>"; 
     619        print F graph_count_data("visits",24,21,12,24); 
     620        print F "</td></tr>\n"; 
     621 
     622        print F "<tr><td align=left valign=top nowrap><h2>Unique users</h2>\n"; 
     623        print F "Number of unique systems seen.<br>\n"; 
     624        print F report_count_data("seen_sysid_list"); 
     625        print F "</td><td nowrap colspan=4><br>"; 
     626        print F graph_count_data("seen_sysid_list",24,21,12,24); 
     627        print F "</td></tr>\n"; 
     628 
     629        print F "<tr><td align=left valign=top nowrap><h2>Start time</h2>\n"; 
     630        print F "When shepherd checked in.<br>\n"; 
     631        print F "(in 15 minute groups, time in GMT+10)"; 
     632        print F "</td><td nowrap colspan=2><br>"; 
     633        print F graph_hash_data("when_run_list", "per_day", 0, 7, "Last 7 days", 120, 500); 
     634        print F "</td><td nowrap colspan=2><br>"; 
     635        print F graph_hash_data("when_run_list", "per_month", 0, 1, "Last 30 days", 120, 500); 
     636        print F "</td></tr>\n"; 
     637 
     638        print F "<tr><td align=left valign=top nowrap><h2>Duration</h2>\n"; 
     639        print F "How long shepherd took to run.<br>\n"; 
     640        print F "</td><td nowrap align=left valign=top><br>"; 
     641        printf F "<li>in last 60 minutes:</b><ul><li>%s average (mean)<li>%s min<li>%s max</ul>\n", 
     642                sec2hms(numvar($store->{per_hour}->[0]->{duration},"",0) / $store->{per_hour}->[0]->{visits}), 
     643                sec2hms(numvar($store->{per_hour}->[0]->{duration_min},"",0)), 
     644                sec2hms(numvar($store->{per_hour}->[0]->{duration_max},"",0)) 
     645                if ((defined $store->{per_hour}->[0]->{visits}) && ($store->{per_hour}->[0]->{visits} > 0)); 
     646        print F "</td><td nowrap align=left valign=top><br>"; 
     647        printf F "<li>in last 7 days:</b><ul><li>%s average (mean)<li>%s min<li>%s max</ul>\n", 
     648                sec2hms(numvar($store->{per_day}->[0]->{duration},"",0) / $store->{per_day}->[0]->{visits}), 
     649                sec2hms(numvar($store->{per_day}->[0]->{duration_min},"",0)), 
     650                sec2hms(numvar($store->{per_day}->[0]->{duration_max},"",0)) 
     651                if ((defined $store->{per_day}->[0]->{visits}) && ($store->{per_day}->[0]->{visits} > 0)); 
     652        print F "</td><td nowrap align=left valign=top><br>"; 
     653        printf F "<li>in last 30 days:</b><ul><li>%s average (mean)<li>%s min<li>%s max</ul>\n", 
     654                sec2hms(numvar($store->{per_month}->[0]->{duration},"",0) / $store->{per_month}->[0]->{visits}), 
     655                sec2hms(numvar($store->{per_month}->[0]->{duration_min},"",0)), 
     656                sec2hms(numvar($store->{per_month}->[0]->{duration_max},"",0)) 
     657                if ((defined $store->{per_month}->[0]->{visits}) && ($store->{per_month}->[0]->{visits} > 0)); 
     658        print F "</td><td nowrap align=left valign=top><br>"; 
     659        printf F "<li>OVERALL:</b><ul><li>%s average (mean)<li>%s min<li>%s max</ul>\n", 
     660                sec2hms(numvar($store->{total}->{duration},"",0) / $store->{total}->{visits}), 
     661                sec2hms(numvar($store->{total}->{duration_min},"",0)), 
     662                sec2hms(numvar($store->{total}->{duration_max},"",0)) 
     663                if ((defined $store->{total}->{visits}) && ($store->{total}->{visits} > 0)); 
     664        print F "</td></tr>\n"; 
     665 
     666        print F "<tr><td align=left valign=top nowrap><h2>Timezones</h2>\n"; 
     667        print F "Timezone setting on hosts.<br>\n"; 
     668        print F "</td><td nowrap colspan=4><br>"; 
     669        print F graph_hash_data("gmt_offset_list", "per_day", 0, 7, "Last 7 days", 120, 500); 
     670        print F graph_hash_data("gmt_offset_list", "per_month", 0, 1, "Last 30 days", 120, 500); 
     671        print F "</td></tr>\n"; 
     672 
     673 
     674# SHEPHERD 
     675# what components were used (and in what order?) 
     676# $store->{per_hour}->[0]->{components_used_order_list}->{$components_used}++; 
     677 
     678 
     679# PER-COMPONENT 
     680# my $g = "c_".$c."_success"; 
     681# my $g = "c_".$c."_fail"; 
     682# $store->{per_hour}->[0]->{$g}->{counter}++; 
     683# $store->{total}->{$g}->{counter}++; 
     684 
     685# duration (total count, min, max, average from duration/counter) 
     686# &record_duration($store->{per_hour}->[0]->{$g}, $c_dur); 
     687# &record_duration($store->{total}->{$g}, $c_dur); 
     688 
     689# # version count 
     690# $store->{per_hour}->[0]->{$g}->{version_list}->{$c_ver}++; 
     691 
     692        print F "</table><br clear=all>\n"; 
     693 
     694        print F "</body></html>\n"; 
     695 
     696} 
     697 
     698############################################################################### 
     699 
     700