| 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 | |
| | 95 | sub 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 | |
| | 108 | sub 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 | |
| | 127 | sub 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 | |
| | 209 | sub 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 | |
| | 235 | sub 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 | |
| | 251 | sub 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 | |
| | 278 | sub parse_feedback |
| | 279 | { |
| 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 |
| 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 | |
| | 371 | sub 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 | |
| | 390 | sub 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 | |
| | 410 | sub 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 | |
| | 422 | sub 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 | |
| | 487 | sub 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 | |
| | 540 | sub 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 | |
| | 586 | sub 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> |
| | 597 | a { color: #F6B620; text-decoration: none; border-bottom: 1px dotted #F6B620; } |
| | 598 | a:hover { border: 1px dotted #FFEE40; } |
| | 599 | html, body, table { margin: 0; padding: 0; color: #c0bcb0; font: normal 12px Verdana; } |
| | 600 | body { text-align: left; margin: 1em 10 10 20; } |
| | 601 | body.home { background: #474642; } |
| | 602 | body.home h1 { font: bold 36px/.8em Slyphaen, Times, serif; color: #F6B620; border-bottom: 3px solid #F6B620; padding: 0; clear: both; text-align: left; } |
| | 603 | body.home h2 { font: normal 24px/.8em Slyphaen, Times, serif; color: #DFB020; padding: 0; clear: both; margin: 20 0 10 0; } |
| | 604 | body.home p { clear: both; } |
| | 605 | input { 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> |
| | 613 | EOF |
| | 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 | |