#!/usr/bin/perl -w use strict; # Copyright (c) 2002/2003 Eike Frost (btrackalyzer@kefro.st) # This is Free Software under the terms of the General Public License (GPL). # Of course donations never go amiss, but that's up to you ;) # This program parses a BitTorrent tracker logfile and extracts various # data from it. It's neither perfect nor pretty. # Oh, and it's sufficiently slow, too. # $Id: trackerlyze.pl,v 1.18 2003/09/06 06:07:10 eike Exp $ # Homepage : http://ei.kefro.st/projects/btrackalyzer/ use Time::Local; use POSIX qw(strftime); use Storable; # Some configuration settings my $reannounce_interval_max = 30*60; # This is the reannounce interval set; controls backdrift my $checkpoint_interval = 5*60; # How often the graph is updated my $timeout_downloaders_interval = 45*60; # after this much time, we stop considering peers my $drop_downloaders_interval = 800*60; # after this much time, we drop peers my $drop_overdue_stats = 1; # If stats don't fit into writestats_time_back, drop previous data my $writestats_time_back = 30*60; # only generate stats for data this old my $reannounce_interval_avg_samples = 5000; # do the stats on the last X sampls my $do_reannounce_stats = 0; # do reannounce stats. SLOW. my $reannounce_age = 0; # 0 = consider ALL reannounces, 1 = only past $checkpoint_interval my $display_file_stats = 0; # display statistics while running my $fetchnamesize = 0; # Try to fetch filenames/sizes from IO my $verbose = 1; # Be verbose on errors & current status my $always_do_filestats = 0; # do filestats even if the data isn't current. SLOW ! my $always_do_peakstats = 1; # do peakstats on historic data. SLOWER. my $autosaveperiods = 36; # Save state every X graphing intervals my $printstateperiods = 72; # Print state every X processed intervals my $allowpeerstats = 1; # Allow complete peerstats on per-file basis my $alwaysgraph = 0; # Graph even on historic data (SLOW !) my $statefilename = 'btrackalyze.state'; #my $io = new trackalyze::IO::file # Object to fetch/save stats to files # ({statusdir => './status', # Directory to put data into # connectionstats => 0, # Save connection duration stats # completionstats => 0, # Save completion duration stats # peerstats => 0, # Do/Save peerstats # }); my $io = new trackalyze::IO::sql # use this for SQL ({dbname => 'btrackalyzer', # Database to use dbuser => 'btrackalazer', # Database username dbpass => 'secret', # Database Password statstable => 'btstats', # Database Statistics table misctable => 'btmisc', # Database Miscellaneous stats table peerstatstable => 'btpeerstats', # Table for Peerstats, if desired metadatatable => 'btmetadata', # Table for supplying metadata peerstats => 0, # Do/Save peerstats }); #my $io = new trackalyze::IO::multiplex # Of course you can also use more # (new trackalyze::IO::file, # than one module if your heart so # new trackalyze::IO::sql); # desires / your machine handles it. #my $io = new trackalyze::IO::Dummy; # This blackholes statswriting my $graph = new trackalyze::Graph::RRDs # Shared rrdtool for graphing ({rrd => 'torrent.rrd', # RRDatabase to use outputdir => './graphs', # Output directory (graphs) graphtitle => 'BitTorrent tracker -', # Title prefix }); #my $graph = new trackalyze::Graph::rrdtool # External rrdtool for graphing # ({rrd => 'torrent.rrd', # RRDatabase to use # outputdir => './graphs', # Output directory (graphs) # graphtitle => 'BitTorrent tracker - ', # Title prefix # useexternal => './graph.sh'}); # Use external script for graphing? #my $graph = new trackalyze::Graph::Dummy; # This blackholes graphing # Below this comes non-configurable stuff ... my (%peers, %files); # Variables to be used my $inittime = # date/time of first read line my $upped = # graphed data (usually < transferred data) my $lasttime = # date of last read line my $totalmaxconnected = # maximum of connected users of all time my $totalcompleted = # Total completed downloads thus far my $terminate = # terminate the loop ? my $proclines = # Processed lines in this run my $parts = # counter of intervals processed my $finishedpeers = # peers we have served my $totalconnected = # currently connected peers my $totalleechers = # Total Leechers my $totalseeds = # Total Seeds my $totalgzip = # Total GZIP-encoded announces my $totalannounces = # Total Announces my $totalannouncesize = # Total size of answered announces my $totalscrapes = # Total scrapes my $totalscrapesize = # Total scrapes' size my $totalothersize = # Total size of all other responses my $firsttime = # Timestamp of first log-line my $showstate = # Show current status on next line my $nextgraph = # Create a Graph at next opportunity my $nextfilestats = # Write out Filestats at next opportunity my $nosave = # Save state at end ? 0; # Timeslots to consider my $nslots = int($reannounce_interval_max / $checkpoint_interval); my @slots = (); # for reannounce interval stats my %reannounce; $reannounce{number} = 0; $reannounce{current} = 0; # Slots start out empty for (my $i = $nslots-1; $i >=0 ; $i--) { $slots[$i]{timestamp} = 0; $slots[$i]{upped} = 0; } loadruntime (); # Variables used in each iteration -- declared here. my ($ip, $gzip, $timestamp, $getrequest, $fgetrequest, $getmethod, $getparam, $statuscode, $size, $tday, $tmonth, $tyear, $thour, $tminute, $tsecond, $tadjust, $line, %r); # Should we catch a sigint, stop after processing the current line $SIG{INT} = sub { $terminate++; }; $SIG{HUP} = sub { $showstate = 1; }; $SIG{USR1} = sub { $nextgraph = 1; }; $SIG{USR2} = sub { $nextfilestats = 1; }; # To convert from HTTP Log Timestamps to real timestamps my %months = ('Jan'=>0,'Feb'=>1,'Mar'=>2,'Apr'=>3,'May'=>4,'Jun'=>5, 'Jul'=>6,'Aug'=>7,'Sep'=>8,'Oct'=>9,'Nov'=>10,'Dec'=>11); my $lineregex = qr/ ((?:\d{1,3}\.){3}\d{1,3})\s # IP $1 ([^ ]+)\s [^ ]+\s # Ident, Username $2 \[([^\]]+)\]\s # Timestamp $3 \"([^"\s]+)\s # Method $4 ([^"\s]+)\s # Request $5 ([^"]+)"\s # Parameters $6 (\d)+\s # Status Code $7 (\d)+ # Size returned $8 /x; my $begin = time; if (defined $ARGV[0]) { if ($ARGV[0] eq '--writeout') { $terminate = 1; $nosave = 1; $always_do_filestats = 1; $nextfilestats = 1; $graph->graph ($lasttime - $reannounce_interval_max); filespeeds (); filestats (); } } while ((! $terminate) and ($line = <STDIN>)) { # Split the line into its parts ($line =~ m/$lineregex/o) or next; my ($ip, $gzip, $timestamp, $getmethod, $fgetrequest, $getparam, $statuscode, $size) = ($1, $2, $3, $4, $5, $6, $7, $8); # Convert timestamp. This used to be done by Date::Parse, which is # way too slow in this case. eval { ($tday, $tmonth, $tyear, $thour, $tminute, $tsecond, $tadjust) = unpack "A2xA3xA4xA2xA2xA2", $timestamp; $timestamp = timegm ($tsecond, $tminute, $thour, $tday, $months{$tmonth}, $tyear); }; if ($@) { print "The previous error message was due to a malformed line ". "but is non-fatal..\n"; print "Line in question : $line\n"; next; } if ($timestamp < $lasttime) { next; } # we only process announces in the further lines. If any other other # lines are to be processed, process them here. (undef, $getrequest) = split '/announce\?', $fgetrequest, 2; unless (defined $getrequest) { (undef, $getrequest) = split '/scrape', $fgetrequest, 2; if (defined $getrequest) { $totalscrapes++; $totalscrapesize+=$size; } else { if (defined $size) { $totalothersize+=$size; } } next; } my @r = map { split '=' } split '&', $getrequest; if (scalar @r % 2 == 0) { %r = @r; } else { next; } # test whether needed parameters have been given (defined $r{peer_id}) and (defined $r{uploaded}) and (defined $r{downloaded}) and (defined $r{left}) and (defined $r{info_hash}) or next; # let's do some sanity checks (! $r{peer_id} =~ /%/) and next; (! $r{info_hash} =~ /%/) and next; # Convert hashes into readable form $r{info_hash} =~ s/%([a-fA-F0-9]{2})/chr(hex($1))/ge; $r{info_hash} = unpack ("H*", $r{info_hash}); $r{peer_id} =~ s/%([a-fA-F0-9]{2})/chr(hex($1))/ge; $r{peer_id} = unpack ("H*", $r{peer_id}); # let's do some more sanity checks (length ($r{peer_id}) != 40) and next; (length ($r{info_hash}) != 40) and next; # Make sure numbers really ARE numbers ... $r{uploaded} =~ tr/0-9//cd; $r{downloaded} =~ tr/0-9//cd; $r{left} =~ tr/0-9//cd; # Count announces and gzips ($gzip eq 'gzip') and $totalgzip++; $totalannounces++; $totalannouncesize += $size; # Sometimes, the client submits its own IP instead of the originating host if (defined $r{$ip}) { $ip = $r{$ip}; } # If we don't really know this peer (yet), create some empty hashes (! exists $peers{$r{peer_id}}) and createpeerentry ($r{peer_id}, $timestamp, $r{info_hash}); (! exists $files{$r{info_hash}}) and createfileentry ($r{info_hash}, $timestamp); # Set some data about the peer from the current line $peers{$r{peer_id}}{ip} = $ip; $peers{$r{peer_id}}{left} = $r{left}; # Did this announce contain an event ? If so, handle it accordingly if (defined $r{event}) { if ($r{event} eq 'started') { ($r{downloaded} > 0) and next; $files{$r{info_hash}}{started}++; $peers{$r{peer_id}}{started} = $timestamp; } elsif ($r{event} eq 'stopped') { $peers{$r{peer_id}}{stopped} = $timestamp; } elsif ($r{event} eq 'completed') { ($r{left} > 0) and next; $files{$r{info_hash}}{completed}++; $peers{$r{peer_id}}{completedat} = $timestamp; $totalcompleted++; } } # Grab some info about files/torrents from the line and take note of them $files{$r{info_hash}}{lastseen} = $timestamp; # So, how do the transfers look ? if (defined $peers{$r{peer_id}}{up}) { my $diff = $r{uploaded} - $peers{$r{peer_id}}{up}; # some sanity limits if (($diff > 0) and ($diff < 2**32*$nslots)) { # The traffic difference is over this long a timespan ... my $timediff = $timestamp - $peers{$r{peer_id}}{lastseen}; # ... so we distribute it over this many timeslots ... my $takeslots = int ($timediff / $checkpoint_interval); ($takeslots < 1) and $takeslots = 1; ($takeslots > $nslots) and (! $drop_overdue_stats) and $takeslots = $nslots; # ... which gives us this much per timeslot my $taketraffic = int ($diff / $takeslots); # sanity-check, again if (! ($taketraffic > 2**32)) { # increment each slot for (my $i = $nslots-1 ; ($i > $nslots-1-$takeslots) and ($i >= 0); $i--) { $slots[$i]{upped} += $taketraffic; } # Note the difference for the file $files{$r{info_hash}}{up} += $diff; } else { # Speed is way higher than anticipated; either fake or errors # note that this can and will also happen when starting # in the middle of a log ... if (($diff > 0) and ($verbose)) { print "HUGE traffic : diff : $diff, peer : $r{peer_id}, " . "ip : $ip, time : $timestamp; " . "Logline : \n $line \n"; } } } } else { # This is a new peer (?) if (($peers{$r{peer_id}}{firstseen} == $timestamp) and ((! defined $r{event}) or ($r{event} ne 'started'))) { print "Not counting record from unknown peer that has not\n", "\"started\"; This is normal if starting in the middle.\n"; } else { if (($r{uploaded} < 2**32) and ($r{uploaded} > 0)) { $slots[$nslots-1]{upped} += $r{uploaded}; $files{$r{info_hash}}{up} += $r{uploaded}; } else { if (($r{uploaded} > 0) and ($verbose)) { print "HUGE traffic (disregard if starting in the middle " . "of a log): diff : $r{uploaded}, " . "peer : $r{peer_id}, ip : $ip, time : " . "$timestamp; Logline : \n $line \n"; } } } } # If we have seen this peer before, it's a reannounce if (exists $peers{$r{peer_id}}{lastseen}) { my $announce_interval = $timestamp - $peers{$r{peer_id}}{lastseen}; # These are done over the trackers life or over the past # writeout-interval # Create new average if ($do_reannounce_stats) { $reannounce{current} = ($reannounce{current} * $reannounce{number} + $announce_interval); $reannounce{current} /= ++$reannounce{number}; #/ } # If the client has been seen < 1 second prior to this, avoid # divisions by zero if ($announce_interval == 0) { $announce_interval++; } # Current upload speed of this peer $peers{$r{peer_id}}{speedup} = (($r{uploaded} - $peers{$r{peer_id}}{up}) / $announce_interval); if ($peers{$r{peer_id}}{speedup} < 0) { $peers{$r{peer_id}}{speedup} = 0; } # Current download speed of this peer $peers{$r{peer_id}}{speeddown} = (($r{downloaded} - $peers{$r{peer_id}}{down}) / $announce_interval); if ($peers{$r{peer_id}}{speeddown} < 0) { $peers{$r{peer_id}}{speeddown} = 0; } } else { # We don't know anything about speeds, yet. $peers{$r{peer_id}}{speedup} = 0; $peers{$r{peer_id}}{speeddown} = 0; } # Some more data we might be interested in, later. $peers{$r{peer_id}}{lastseen} = $timestamp; $peers{$r{peer_id}}{up} = $r{uploaded}; $peers{$r{peer_id}}{down} = $r{downloaded}; # The graphing magic. Ouch. # if we are just starting, set up some stuff if ($inittime == 0) { $inittime = $timestamp; $firsttime = $timestamp; # slots for before the big bang for (my $i = 0; $i < $nslots; $i++) { $slots[$i]{timestamp} = $timestamp - (($nslots-$i)*$checkpoint_interval); } $graph->setup ($slots[0]{timestamp} - $checkpoint_interval); } else { # else, we may have stuff to graph # but only if a checkpoint_interval has passed ... if ($inittime + $checkpoint_interval-1 < $timestamp) { # ... and as often as is needed to get to the current interval while ($inittime + $checkpoint_interval-1 < $timestamp) { $inittime += $checkpoint_interval; # move slots back one my %curslot; # this, we will work on later. $curslot{timestamp} = $slots[0]{timestamp}; $curslot{upped} = $slots[0]{upped}; for (my $i = 0; $i < $nslots-1; $i++) { $slots[$i]{timestamp} = $slots[$i+1]{timestamp}; $slots[$i]{upped} = $slots[$i+1]{upped}; } $slots[$nslots-1]{timestamp} = $inittime; $slots[$nslots-1]{upped} = 0; # increase $upped with value from moved-out slot $upped += $curslot{upped}; # update the difference in the rrdtool/graphing database $graph->update ($curslot{timestamp}, $upped, $totalconnected, $totalseeds); # If there's need for graphing, graph. if ($alwaysgraph or $nextgraph or (time - $timestamp < $writestats_time_back)) { $nextgraph = 0; $graph->graph ($timestamp - $reannounce_interval_max); } # Give some state information if so instructed if (((time - $timestamp < $writestats_time_back) or ($parts % $printstateperiods == 0)) and $verbose) { $showstate = 1; } # Just in case, save state information. if ($parts++ % $autosaveperiods == 0) { saveruntime (); } } # Clean up after dead & dropped peers peerclean (); # If used, calculate filespeeds & write file stats if ((time - $timestamp < $writestats_time_back) or $always_do_filestats or $always_do_peakstats or $nextfilestats) { filespeeds (); filestats (); } # set back reannounce-stats if ($reannounce_age) { $reannounce{number} = 0; $reannounce{current} = 0; } } } $proclines++; # Let's give the user some feedback how far along we are in parsing # (showstate is being set to 1 by a sighandler ...) if ($showstate == 1) { $showstate = 0; print "processed $proclines lines in ". readablespan (time - $begin + 1) . " (". sprintf ("%.2f", ($proclines / (time - $begin + 1))). " lines/s). \n"; print "current line's timestamp : " . strftime ("%a %b %e %H:%M:%S %Y", localtime ($timestamp)), "\n"; print "total transferred so far : ", readable($upped), " in (". readablespan ($timestamp - $firsttime).")\n"; } # This line is done for, save the time $lasttime = $timestamp; } # The loop ended; now let's see what final stats we have for this run, # and clean up after ourselves print "Total uploaded and graphed : " . readable($upped) . " ($upped)\n"; print "Final reannounce avg : " . $reannounce{current} . "\n"; print "Connected clients : " . $totalconnected . "\n"; print "Clients in recent memory : " . scalar(keys %peers) . "\n"; print "Log lines in this run : " . $proclines . "\n"; print "Time taken : " . (time () - $begin) . "\n"; print "Lines/sec : " . ($proclines / (time - $begin + 1)) . "\n"; unless ($nosave) { print "Calculating Final filestats ...\n"; filestats (); print "Saving current runtime state\n"; saveruntime (); } print "All done.\n"; exit 0; # Taking care of the hashes # This writes out statistics for files. Clunky, slow. sub filestats { # Gathering of filenames/sizes if ($display_file_stats and $fetchnamesize) { foreach my $file (keys %files) { if ($files{$file}{filename} eq '') { $files{$file}{filename} = $io->getfilename($file); } if ($files{$file}{filesize} == 0) { $files{$file}{filesize} = $io->getfilesize($file); } } } my @recs; # Fetch / deduce statistics for each file foreach my $file (keys %files) { my %record; # Use the max speed as speed indicator (figures should be close, # anyway. Also calculate max. $record{speed} = $files{$file}{speedup} > $files{$file}{speeddown} ? $files{$file}{speedup} : $files{$file}{speeddown}; # These peakcalculations are done on always_do_peakstats ... if ($record{speed} > $files{$file}{peakspeed}) { $files{$file}{peakspeed} = $record{speed}; } if ($files{$file}{clients} > $files{$file}{peakclients}) { $files{$file}{peakclients} = $files{$file}{clients}; } if ($files{$file}{seeds} > $files{$file}{peakseeds}) { $files{$file}{peakseeds} = $files{$file}{seeds}; } if ($files{$file}{clients} - $files{$file}{seeds} > $files{$file}{peakleechers}) { $files{$file}{peakleechers} = $files{$file}{clients} - $files{$file}{seeds}; } # ... no need to write out unless asked to, though. if (! ($display_file_stats or (time - $files{$file}{lastseen} < $writestats_time_back) or $always_do_filestats or $nextfilestats)) { next; } $record{speedfmt} = readablemetric ($record{speed}) . "/s"; $record{peakspeed} = $files{$file}{peakspeed}; $record{peakspeedfmt} = readablemetric ($files{$file}{peakspeed}) . "/s"; $record{hash} = $file; # Prepare some information from the hashes $record{up} = $files{$file}{up}; $record{upfmt} = sprintf("%10s", readable($files{$file}{up})); $record{completed} = $files{$file}{completed}; $record{completedfmt} = sprintf ("%6s", $files{$file}{completed}); $record{firstseen} = $files{$file}{firstseen}; $record{firstseenfmt} = strftime ("%m%d %H:%M", localtime ($files{$file}{firstseen})); $record{lastseen} = $files{$file}{lastseen}; $record{lastseenfmt} = strftime ("%m%d %H:%M", localtime ($files{$file}{lastseen})); $record{span} = $files{$file}{lastseen} - $files{$file}{firstseen}; $record{spanfmt} = readablespan ($files{$file}{lastseen} - $files{$file}{firstseen}); $record{peercompleteavg} = $files{$file}{peercomplduraverage}; $record{peercompletemax} = $files{$file}{peercompldurmax}; $record{peercompletemin} = $files{$file}{peercompldurmin}; $record{peercompleteavgfmt} = readablespan ($files{$file}{peercomplduraverage}); $record{peercompletemaxfmt} = readablespan ($files{$file}{peercompldurmax}); $record{peercompleteminfmt} = readablespan ($files{$file}{peercompldurmin}); $record{peerconnavg} = $files{$file}{peerconnduraverage}; $record{peerconnmax} = $files{$file}{peerconndurmax}; $record{peerconnavgfmt} = readablespan ($files{$file}{peerconnduraverage}); $record{peerconnmaxfmt} = readablespan ($files{$file}{peerconndurmax}); $record{seedsfmt} = pad(3, $files{$file}{seeds}) . "/" . pad(3, $files{$file}{clients}); $record{seeds} = $files{$file}{seeds}; $record{clients} = $files{$file}{clients}; $record{leechers} = $files{$file}{clients} - $files{$file}{seeds}; $record{peakseeds} = $files{$file}{peakseeds}; $record{peakclients} = $files{$file}{peakclients}; $record{peakleechers} = $files{$file}{peakleechers}; # only do peer statistics if it's actually requested by IO if ($allowpeerstats and $io->dopeerstats($file)) { $record{peerstats} = ""; for (my $i = 0; $i < 2; $i++) { foreach my $peer (@{$files{$file}{peers}}) { if ((defined $peers{$peer}{killed}) and ($i != 1)) { next; } if ((! defined $peers{$peer}{killed}) and ($i == 1)) { next; } $record{peerstats} .= "IP : " . sprintf ("%15s", $peers{$peer}{ip}); (defined $peers{$peer}{killed}) and $record{peerstats}.='D'; $record{peerstats} .= " :: "; $record{peerstats} .= "speedup : " . sprintf ("%9s", readable ($peers{$peer}{speedup})) . "/s :: " ; $record{peerstats} .= "speeddown : " . sprintf ("%9s", readable ($peers{$peer}{speeddown})) . "/s :: "; $record{peerstats} .= "left : " . sprintf ("%8s", readable ($peers{$peer}{left})) . " : (" . sprintf ("%5s", sprintf ("%.1f", $peers{$peer}{left}/($peers{$peer}{down} + $peers{$peer}{left} + 1)*100)) . "%) :: "; $record{peerstats} .= "uploaded : ". sprintf ("%8s", readable ($peers{$peer}{up})) . " :: "; $record{peerstats} .= "downloaded : " . sprintf ("%8s", readable ($peers{$peer}{down})) . " :: "; $record{peerstats} .= "firstseen : ". $peers{$peer}{firstseen} . " :: "; $record{peerstats} .= "lastseen : " . $peers{$peer}{lastseen} . " :: "; $record{peerstats} .= "duration : " . readablespan ($peers{$peer}{lastseen} - $peers{$peer}{firstseen}) . " :: "; if (defined $peers{$peer}{completedat}) { my $duration = $peers{$peer}{completedat} - $peers{$peer}{firstseen}; $record{peerstats} .= "completed : " . readable ($peers{$peer}{down}) . " in " . readablespan ($duration) . " @ " . sprintf ("%9s", readable ($peers{$peer}{down} / ($duration+1))) . "/s :: "; } $record{peerstats} .= "\n"; } } } # either way, hand the data over to IO if ((time - $record{lastseen} < $writestats_time_back) or $always_do_filestats or $nextfilestats) { $io->save (\%record); } if ($display_file_stats) { push @recs, \%record; } } if ((time - $lasttime < $writestats_time_back) or $always_do_filestats or $nextfilestats) { $nextfilestats = 0; # Some global stats that might be of interest $io->savemisc (totalconnected => $totalconnected, totalmaxconnected => $totalmaxconnected, reannounce_interval_avg => $reannounce{current}, totalleechers => $totalleechers, totalseeds => $totalseeds, totalupped => $upped, totaluppedfmt => sprintf("%10s", readable($upped)), totalcompleted => $totalcompleted, totalgzip => $totalgzip, totalannounces => $totalannounces, totalannouncesize => $totalannouncesize, totalscrapes => $totalscrapes, totalscrapesize => $totalscrapesize, totalothersize => $totalothersize, ); } # if we want to display stats on screen, sort them first, then give columns if ($display_file_stats) { @recs = sort {${$a}{up} <=> ${$b}{up}} @recs; print "Hash/Filename ". "upped dls firstseen timespan src/con\n"; foreach my $re (@recs) { my %r = %{$re}; # If we know a filename, use that. If not, use the hash. if ($files{$r{hash}}{filename} ne '') { $r{name} = $files{$r{hash}}{filename}; } else { $r{name} = $r{hash}; } # shrink name, print out my $fname; if (length ($r{name}) > 40) { $fname = substr ($r{name},0,20) . '...' . substr ($r{name}, -17); } else { $fname = $r{name}; } print $fname . " " . $r{upfmt} . " ". $r{completedfmt} . " " . $r{firstseenfmt} . " " . $r{spanfmt} . " " . $r{seedsfmt} . "\n"; } } } # calculates current up/down speeds sub filespeeds { foreach my $file (keys %files) { $files{$file}{speedup} = 0; $files{$file}{speeddown} = 0; } foreach my $peer (keys %peers) { $files{$peers{$peer}{hash}}{speedup} += $peers{$peer}{speedup}; $files{$peers{$peer}{hash}}{speeddown} += $peers{$peer}{speeddown}; } } # calculates the current connections/file sub connectionsperfile { foreach my $file (keys %files) { $files{$file}{clients} = 0; $files{$file}{seeds} = 0; $files{$file}{peers} = (); } foreach my $peer (keys %peers) { if (! defined $peers{$peer}{killed}) { $files{$peers{$peer}{hash}}{clients}++; if ((defined $peers{$peer}{left}) and ($peers{$peer}{left} == 0)) { $files{$peers{$peer}{hash}}{seeds}++; } } if ($allowpeerstats) { push @{$files{$peers{$peer}{hash}}{peers}}, $peer; } } } # cleans up the peers hash (kill dead peers, tally up stats) sub peerclean { $totalconnected = 0; $totalseeds = 0; $totalleechers = 0; foreach my $peer (keys %peers) { if (($peers{$peer}{lastseen} + $timeout_downloaders_interval > $lasttime) and (defined $peers{$peer}{killed})) { # client came back to life after timeout delete $peers{$peer}{killed}; $peers{$peer}{nostat} = 1; $finishedpeers--; } elsif (($peers{$peer}{lastseen} + $drop_downloaders_interval < $lasttime) and (defined $peers{$peer}{killed})) { # drop time has been reached delete $peers{$peer}; } elsif ((! defined $peers{$peer}{killed}) and ($peers{$peer}{lastseen} + $timeout_downloaders_interval < $lasttime) or (defined $peers{$peer}{stopped})) { # client exceeded timeout $finishedpeers++; # Has this peer been looked at before and come back from the dead ? if (! defined $peers{$peer}{nostat}) { # How long have we tracked this peer my $peerlifetime = $peers{$peer}{lastseen} - $peers{$peer}{firstseen}; # What's the current average for the current torrent ? my $avg = $files{$peers{$peer}{hash}}{peerconnduraverage}; # How many peers have finished this torrent before ? my $fpeers = $files{$peers{$peer}{hash}}{finishedpeers}++; # Calculate new average $files{$peers{$peer}{hash}}{peerconnduraverage} = int (($fpeers * $avg + $peerlifetime) / ($fpeers + 1)); # Is this the new king for longest lifetime ? ($files{$peers{$peer}{hash}}{peerconndurmax} < $peerlifetime) and $files{$peers{$peer}{hash}}{peerconndurmax} = $peerlifetime; # if we have seen both start and complete events, we can do some more stuff if (defined $peers{$peer}{completedat} and defined $peers{$peer}{started}) { # if we know the filesize of this torrent, we can deduce # whether more than 90% were transferred. If so, proceed, # otherwise just drop it if (($files{$peers{$peer}{hash}}{filesize} == 0) or (($files{$peers{$peer}{hash}}{filesize} != 0) and (abs ($peers{$peer}{down} - $files{$peers{$peer}{hash}}{filesize}) < $files{$peers{$peer}{hash}}{filesize}*0.1))) { # Calculate new average for duration for completion $files{$peers{$peer}{hash}}{peercomplduraverage} = int (($files{$peers{$peer}{hash}}{completedpeers} * $files{$peers{$peer}{hash}}{peercomplduraverage} + $peerlifetime) / (++$files{$peers{$peer}{hash}}{completedpeers})); # new winner for maximum ? ($files{$peers{$peer}{hash}}{peercompldurmax} < $peerlifetime) and $files{$peers{$peer}{hash}}{peercompldurmax} = $peerlifetime; # and what about the minimum ? (($files{$peers{$peer}{hash}}{peercompldurmin} > $peerlifetime) or ($files{$peers{$peer}{hash}}{peercompldurmin} == 0)) and $files{$peers{$peer}{hash}}{peercompldurmin} = $peerlifetime; } } } if (defined $peers{$peer}{stopped}) { # if a "stopped" event was received, no need to keep the # peer around delete $peers{$peer}; } else { # Do not consider this peer for anything anymore; keep it around # in case it comes back (connection problems and the like) $peers{$peer}{killed} = 1; } } else { if (! defined $peers{$peer}{killed}) { $totalconnected++; if ((defined $peers{$peer}{left}) and ($peers{$peer}{left} == 0)) { $totalseeds++; } else { $totalleechers++; } } } } if ($totalconnected > $totalmaxconnected) { $totalmaxconnected = $totalconnected; } connectionsperfile (); } # initialize fields for a new/unknown torrent/file sub createfileentry { my $info_hash = shift; my $timestamp = shift; $files{$info_hash} = {}; $files{$info_hash}{peerconnduraverage} = 0; # peer connection duration average $files{$info_hash}{peerconndurmax} = 0; # peer connection duration maximum $files{$info_hash}{finishedpeers} = 0; # peers that have stopped coming back $files{$info_hash}{completedpeers} = 0; # peers that have downloaded the whole file $files{$info_hash}{peercomplduraverage} = 0; # peer completion duration average $files{$info_hash}{peercompldurmax} = 0; # peer completion duration maximum $files{$info_hash}{peercompldurmin} = 0; # peer completion duration minimum (>90%) $files{$info_hash}{filename} = ''; # filename associated with this torrent $files{$info_hash}{filesize} = 0; # filesize associated with this torrent $files{$info_hash}{completed} = 0; # number of completed downloads $files{$info_hash}{up} = 0; # number of transferred bytes $files{$info_hash}{speedup} = 0; # current upload-speed $files{$info_hash}{speeddown} = 0; # current download-speed $files{$info_hash}{started} = 0; # How many clients have started to download ? $files{$info_hash}{completed} = 0; # ... and how many have finished ? $files{$info_hash}{clients} = 0; # How many clients are currently on the file ? $files{$info_hash}{seeds} = 0; # ... how many of those are seeds ? $files{$info_hash}{peakspeed} = 0; # Peak speed seen on this file $files{$info_hash}{peakseeds} = 0; # Peak number of seeds $files{$info_hash}{peakleechers} = 0; # Peak number of leeches $files{$info_hash}{peakclients} = 0; # Peak number of clients $files{$info_hash}{firstseen} = $timestamp; # We just created the record ... $files{$info_hash}{lastseen} = $timestamp; # We just created the record ... } # initialize fields for a new/unknown peer sub createpeerentry { my $peer_id = shift; my $timestamp = shift; my $info_hash = shift; $peers{$peer_id} = {}; $peers{$peer_id}{firstseen} = $timestamp; $peers{$peer_id}{hash} = $info_hash; } # Auxiliary Routines # Saves runtime data for picking up later sub saveruntime { my %save = ( 'version' => 1, 'peers' => \%peers, 'files' => \%files, 'lasttime' => $lasttime, 'upped' => $upped, 'inittime' => $inittime, 'slots' => \@slots, 'totalmaxconnected' => $totalmaxconnected, 'totalconnected' => $totalconnected, 'totalleechers' => $totalleechers, 'totalseeds' => $totalseeds, 'totalcompleted' => $totalcompleted, 'totalgzip' => $totalgzip, 'totalannounces' => $totalannounces, 'totalannouncesize' => $totalannouncesize, 'totalscrapes' => $totalscrapes, 'totalscrapesize' => $totalscrapesize, 'totalothersize' => $totalothersize, 'firsttime' => $firsttime, 'finishedpeers' => $finishedpeers, 'reannounce' => \%reannounce); store \%save, $statefilename; } # Loads runtime data from previous runs sub loadruntime { if (-e $statefilename) { my $retrieve = retrieve($statefilename); my %retrieve = %$retrieve; %peers = %{$retrieve{peers}}; %files = %{$retrieve{files}}; %reannounce = %{$retrieve{reannounce}}; $lasttime = $retrieve{lasttime}; $inittime = $retrieve{inittime}; $totalmaxconnected = $retrieve{totalmaxconnected}; $totalconnected = $retrieve{totalconnected}; $finishedpeers = $retrieve{finishedpeers}; $upped = $retrieve{upped}; @slots = @{$retrieve{slots}}; if (defined $retrieve{version} and $retrieve{version} >= 1) { $totalseeds = $retrieve{totalseeds}; $totalleechers = $retrieve{totalleechers}; $totalcompleted = $retrieve{totalcompleted}; $totalgzip = $retrieve{totalgzip}; $totalannounces = $retrieve{totalannounces}; $totalannouncesize = $retrieve{totalannouncesize}; $totalscrapes = $retrieve{totalscrapes}; $totalscrapesize = $retrieve{totalscrapesize}; $totalothersize = $retrieve{totalothersize}; $firsttime = $retrieve{firsttime}; } else { print "The statefile was created with an earlier version of\n", "this script. A couple of statistics will seem weird for\n", "a while, but you /should/ be able to continue using the\n", "old data, although there will be warnings and possibly\n", "some errors for the currently analyzed peers. Also, the\n", "RRD either needs a change or to be set up anew. See\n", "the webpage for further notes ...\n"; $totalseeds = 0; $totalleechers = 0; $totalcompleted = 0; $totalgzip = 0; $totalannounces = 0; $totalannouncesize = 0; $totalscrapes = 0; $totalscrapesize = 0; $totalothersize = 0; $firsttime = 0; } } } # spits out some more readable numbers for byte figures sub readable { my $d = shift; ($d > 1024**5) and return (sprintf ("%.2f", $d/(1024**5))) . "PiB"; ($d > 1024**4) and return (sprintf ("%.2f", $d/(1024**4))) . "TiB"; ($d > 1024**3) and return (sprintf ("%.2f", $d/(1024**3))) . "GiB"; ($d > 1024**2) and return (sprintf ("%.2f", $d/(1024**2))) . "MiB"; ($d > 1024) and return (sprintf ("%.2f", $d/(1024))) . "KiB"; return sprintf ("%.2f", $d) . "b"; } # Spits out some more readable numbers for metric bit figures sub readablemetric { my $d = shift; ($d > 1000**5) and return (sprintf ("%.2f", $d/(1000**5))) . "PB"; ($d > 1000**4) and return (sprintf ("%.2f", $d/(1000**4))) . "TB"; ($d > 1000**3) and return (sprintf ("%.2f", $d/(1000**3))) . "GB"; ($d > 1000**2) and return (sprintf ("%.2f", $d/(1000**2))) . "MB"; ($d > 1000) and return (sprintf ("%.2f", $d/(1000))) . "kB"; return sprintf ("%.2f", $d) . "b"; } # 0-pad numbers to x columns sub pad { my $cols = shift; my $num = my $temp = shift; if (! defined $temp) { $temp = $num = 0; } while ($temp >= 10) { $temp /= 10; $cols--; } return $cols > 0 ? ('0' x --$cols).$num : $num; } # a more readable timespan sub readablespan { my $span = shift; my $days = int ($span / 86400); my $hours = int (($span % 86400) / 3600); my $minutes = int ((($span % 86400) % 3600) / 60); return pad(2, $days) . 'd' . pad(2, $hours) . 'h' . pad(2, $minutes) . 'm'; } # Handle IO (with files) package trackalyze::IO::file; no strict 'refs'; sub new { my $proto = shift; my $class = ref($proto) || $proto; my $p = shift; if (! defined $p) { $p = {}; } my $self = { statusdir => defined $p->{statusdir} ? $p->{statusdir} : 'status', miscsuffix => defined $p->{miscsuffix} ? $p->{miscsuffix} : '', connectionstats => defined $p->{connectionstats} ? $p->{connectionstats} : 1, completionstats => defined $p->{completionstats} ? $p->{completionstats} : 1, peerstats => defined $p->{peerstats} ? $p->{peerstats} : 1, peakstats => defined $p->{peakstats} ? $p->{peakstats} : 1, }; bless ($self, $class); return $self; } # returns filename for torrent-hash sub getfilename ($) { my $self = shift; my $file = shift; if (-e "${$self}{statusdir}/$file.filename") { return `cat ${$self}{statusdir}/$file.filename`; } return ''; } # returns filesize for torrent-hash sub getfilesize ($) { my $self = shift; my $file = shift; if (-e "${$self}{statusdir}/$file.filename") { return `cat ${$self}{statusdir}/$file.filesize`; } return 0; } # saves data about one torrent sub save ($) { my $self = shift; my $rrecord = shift; my %r = %{$rrecord}; my $statusdir = ${$self}{statusdir}; open TR, ">$statusdir/$r{hash}.transferred"; print TR $r{upfmt}; close TR; open TR, ">$statusdir/$r{hash}.completed"; print TR $r{completed}; close TR; open TR, ">$statusdir/$r{hash}.duration"; print TR $r{spanfmt}; close TR; open TR, ">$statusdir/$r{hash}.connected"; print TR $r{clients}; close TR; open TR, ">$statusdir/$r{hash}.sources"; print TR $r{seeds}; close TR; open TR, ">$statusdir/$r{hash}.leechers"; print TR $r{leechers}; close TR; open TR, ">$statusdir/$r{hash}.speed"; print TR $r{speedfmt}; close TR; open TR, ">$statusdir/$r{hash}.consolidated"; print TR $r{clients} . ":" . $r{seeds} . ":" . $r{completed} . ":" . $r{upfmt} . ":" . $r{spanfmt} . ":" . $r{speed}; close TR; if ($self->{peakstats}) { open TR, ">$statusdir/$r{hash}.peakseeds"; print TR $r{peakseeds}; close TR; open TR, ">$statusdir/$r{hash}.peakleechers"; print TR $r{peakleechers}; close TR; open TR, ">$statusdir/$r{hash}.peakclients"; print TR $r{peakclients}; close TR; open TR, ">$statusdir/$r{hash}.peakspeed"; print TR $r{peakspeedfmt}; close TR; } if ($self->{completionstats}) { open TR, ">$statusdir/$r{hash}.peercompleteavg"; print TR $r{peercompleteavgfmt}; close TR; open TR, ">$statusdir/$r{hash}.peercompletemax"; print TR $r{peercompletemaxfmt}; close TR; open TR, ">$statusdir/$r{hash}.peercompletemin"; print TR $r{peercompleteminfmt}; close TR; } if ($self->{connectionstats}) { open TR, ">$statusdir/$r{hash}.peerconnectionaverage"; print TR $r{peerconnavgfmt}; close TR; open TR, ">$statusdir/$r{hash}.peerconnectiomax"; print TR $r{peerconnmaxfmt}; close TR; } if ($self->{peerstats}) { open TR, ">$statusdir/$r{hash}.peerstats"; print TR $r{peerstats}; close TR; } } # saves miscellaneous data sub savemisc ($) { my $self = shift; my %r = @_; my $statusdir = ${$self}{statusdir}; open TR, ">$statusdir/total.connected" . $self->{miscsuffix}; print TR $r{totalconnected}; close TR; open TR, ">$statusdir/total.maxconnected" . $self->{miscsuffix}; print TR $r{totalmaxconnected}; close TR; open TR, ">$statusdir/announce_interval_avg" . $self->{miscsuffix}; print TR $r{reannounce_interval_avg}; close TR; open TR, ">$statusdir/total.leechers" . $self->{miscsuffix}; print TR $r{totalleechers}; close TR; open TR, ">$statusdir/total.seeds" . $self->{miscsuffix}; print TR $r{totalseeds}; close TR; open TR, ">$statusdir/total.transferred" . $self->{miscsuffix}; print TR $r{totaluppedfmt}; close TR; open TR, ">$statusdir/total.completed" . $self->{miscsuffix}; print TR $r{totalcompleted}; close TR; open TR, ">$statusdir/total.gzip" . $self->{miscsuffix}; print TR $r{totalgzip}; close TR; open TR, ">$statusdir/total.announces" . $self->{miscsuffix}; print TR $r{totalannounces}; close TR; open TR, ">$statusdir/total.announcesize" . $self->{miscsuffix}; print TR $r{totalannouncesize}; close TR; open TR, ">$statusdir/total.scrapes" . $self->{miscsuffix}; print TR $r{totalscrapes}; close TR; open TR, ">$statusdir/total.scrapesize" . $self->{miscsuffix}; print TR $r{totalscrapesize}; close TR; open TR, ">$statusdir/total.othersize" . $self->{miscsuffix}; print TR $r{totalothersize}; close TR; } # should peerstats be generated for file X ? sub dopeerstats ($) { my $self = shift; return $self->{peerstats}; } package trackalyze::IO::sql; # Provisions for saving all this data to SQL tables # If you want peerstats to be generated for a specific hash, # insert a line into the peerstats table with the hash specified. # there is also a table for metadata, which isn't touched by this # code; you can store metadata there that may be used here, though. # [Note : this may be removed soon, the use is dubious at best :/] no strict 'refs'; # Only try to load DBI and DBD::mysql modules if they are available BEGIN { eval('use DBI;'); eval('use DBD::mysql;'); } sub new { my $proto = shift; my $class = ref($proto) || $proto; my $p = shift; if (! defined $p) { $p = {}; } if (not exists $INC{'DBI.pm'}) { print "Sorry, DBI module could NOT be loaded (is it installed ?).\n"; exit 0; } my $self = { dbname => defined $p->{dbname} ? $p->{dbname} : 'trackalyzer', dbuser => defined $p->{dbuser} ? $p->{dbuser} : 'trackalyzer', dbpass => defined $p->{dbpass} ? $p->{dbpass} : 'secret', statstable => defined $p->{statstable} ? $p->{statstable} : 'torrents', misctable => defined $p->{misctable} ? $p->{misctable} : 'misc', peerstatstable => defined $p->{peerstatstable} ? $p->{peerstatstable} : 'peerstats', metadatatable => defined $p->{metadatatable} ? $p->{metadatatable} : 'metadata', peerstats => defined $p->{peerstats} ? $p->{peerstats} : 1, sthandles => {}, }; $self->{sqlhandle} = DBI->connect ("dbi:mysql:" . $self->{dbname}, $self->{dbuser}, $self->{dbpass}) or die $self->{sqlhandle}->errstr(); my $st_handle = $self->{sqlhandle}->prepare ('show tables from ' . $self->{dbname}) or die $self->{sqlhandle}->errstr(); my $result = $st_handle->execute () or die $self->{sqlhandle}->errstr(); my $arrayref = $st_handle->fetchall_arrayref () or die $self->{sqlhandle}->errstr(); my @rows = @$arrayref; $result = 0; foreach my $key (@rows) { if (@$key[0] eq $self->{statstable}) { $result++; } } if ($result eq 0) { my $st_handle = $self->{sqlhandle}->prepare ('create table ' . $self->{statstable} . '(info_hash CHAR(40) not null unique, time INT unsigned, transferred BIGINT unsigned, completed INT unsigned, duration INT unsigned, peercompletemin INT unsigned, peercompleteaverage INT unsigned, peercompletemax INT unsigned, peerconnectionaverage INT unsigned, peerconnectionmax INT unsigned, clients INT unsigned, seeds INT unsigned, speed FLOAT unsigned, peakclients INT unsigned, peakseeds INT unsigned, peakspeed FLOAT unsigned, primary key (info_hash) )') or die $self->{sqlhandle}->errstr(); my $result = $st_handle->execute () or die $self->{sqlhandle}->errstr(); $st_handle = $self->{sqlhandle}->prepare ('create table ' . $self->{misctable} . '(name VARCHAR(255) not null unique, value VARCHAR(255), primary key (name) )') or die $self->{sqlhandle}->errstr(); $result = $st_handle->execute () or die $self->{sqlhandle}->errstr(); $st_handle = $self->{sqlhandle}->prepare ('create table ' . $self->{peerstatstable} . '(info_hash CHAR(40) not null unique, peerstats mediumblob, primary key (info_hash) )') or die $self->{sqlhandle}->errstr(); $result = $st_handle->execute () or die $self->{sqlhandle}->errstr(); $st_handle = $self->{sqlhandle}->prepare ('create table ' . $self->{metadatatable} . '(info_hash CHAR(40) not null unique, filename VARCHAR(255), filesize BIGINT unsigned, tracker VARCHAR(255), piecesize INT unsigned, primary key (info_hash) )') or die $self->{sqlhandle}->errstr(); $result = $st_handle->execute () or die $self->{sqlhandle}->errstr(); } bless ($self, $class); return $self; } # returns filename for torrent-hash sub getfilename ($) { my $self = shift; my $info_hash = shift; unless (defined $self->{sthandles}->{getfn}) { $self->{sthandles}->{getfn} = $self->{sqlhandle}->prepare ('select filename from ' . $self->{metadatatable} . ' where info_hash=?') or die $self->{sqlhandle}->errstr(); } my $result = $self->{sthandles}->{getfn}->execute ($info_hash) or die $self->{sqlhandle}->errstr(); my $arrayref = $self->{sthandles}->{getfn}->fetchall_arrayref () or die $self->{sqlhandle}->errstr(); my @rows = @$arrayref; return scalar @rows != 0 ? ${$rows[0]}[0] : '' } # returns filesize for torrent-hash sub getfilesize ($) { my $self = shift; my $info_hash = shift; unless (defined $self->{sthandles}->{getfs}) { $self->{sthandles}->{getfs} = $self->{sqlhandle}->prepare ('select filesize from ' . ${$self}{metadatatable} . ' where info_hash=?') or die $self->{sqlhandle}->errstr(); } my $result = $self->{sthandles}->{getfs}->execute ($info_hash) or die $self->{sqlhandle}->errstr(); my $arrayref = $self->{sthandles}->{getfs}->fetchall_arrayref () or die $self->{sqlhandle}->errstr(); my @rows = @$arrayref; return scalar @rows != 0 ? ${$rows[0]}[0] : '' } # saves data about one torrent sub save ($) { my $self = shift; my $rrecord = shift; my %r = %{$rrecord}; unless (defined $self->{sthandles}->{save}) { $self->{sthandles}->{save} = $self->{sqlhandle}->prepare ('insert ignore into ' . $self->{statstable} . ' (info_hash, time, transferred, completed, duration, peercompletemin, peercompleteaverage, peercompletemax, peerconnectionaverage, peerconnectionmax, clients, seeds, speed, peakclients, peakseeds, peakspeed) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' ) or die $self->{sqlhandle}->errstr(); } my $result = $self->{sthandles}->{save}->execute ( $r{hash}, $r{lastseen}, $r{up}, $r{completed}, $r{span}, $r{peercompletemin},$r{peercompleteavg}, $r{peercompletemax}, $r{peerconnavg}, $r{peerconnmax}, $r{clients}, $r{seeds}, $r{speed}, $r{peakclients}, $r{peakseeds}, $r{peakspeed}) or die $!; if ($result eq '0E0') { unless (defined $self->{sthandles}->{saveupdate}) { $self->{sthandles}->{saveupdate} = $self->{sqlhandle}->prepare ('update ' . $self->{statstable} . ' set time=?, transferred=?, completed=?, duration=?, peercompletemin=?, peercompleteaverage=?, peercompletemax=?, peerconnectionaverage=?, peerconnectionmax=?, clients=?, seeds=?, speed=?, peakclients=?, peakseeds=?, peakspeed=? where info_hash=?' ) or die $self->{sqlhandle}->errstr(); } $result = $self->{sthandles}->{saveupdate}->execute ( $r{lastseen}, $r{up}, $r{completed}, $r{span}, $r{peercompletemin}, $r{peercompleteavg}, $r{peercompletemax}, $r{peerconnavg}, $r{peerconnmax}, $r{clients}, $r{seeds}, $r{speed}, $r{peakclients}, $r{peakseeds}, $r{peakspeed}, $r{hash}) or die $self->{sqlhandle}->errstr(); } if ($self->dopeerstats ($r{hash})) { unless (defined $self->{sthandles}->{savepeerstats}) { $self->{sthandles}->{savepeerstats} = $self->{sqlhandle}->prepare ('update ' . $self->{peerstatstable} . ' set peerstats=? where info_hash=?;') or die $self->{sqlhandle}->errstr(); } $result = $self->{sthandles}->{savepeerstats}->execute ($r{peerstats}, $r{hash}) or die $self->{sqlhandle}->errstr(); } } # saves miscellaneous data sub savemisc ($) { my $self = shift; my %r = @_; my $statusdir = ${$self}{statusdir}; my $result; unless (defined $self->{sthandles}->{misc}) { $self->{sthandles}->{misc} = $self->{sqlhandle}->prepare ('insert ignore into ' . $self->{misctable} . ' (name, value) values (?, ?)') or die $self->{sqlhandle}->errstr(); } $result = $self->{sthandles}->{misc}->execute ('totalclients', $r{totalconnected}) or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{misc}->execute ('totalmaxclients', $r{totalmaxconnected}) or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{misc}->execute ('reannounce_interval_avg', $r{reannounce_interval_avg}) or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{misc}->execute ('totalleechers', $r{totalleechers}) or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{misc}->execute ('totalseeds', $r{totalseeds}) or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{misc}->execute ('totaltransfer', $r{totalupped}) or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{misc}->execute ('totalcompleted', $r{totalcompleted}) or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{misc}->execute ('totalgzip', $r{totalgzip}) or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{misc}->execute ('totalannounces', $r{totalannounces}) or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{misc}->execute ('totalannouncesize', $r{totalannouncesize}) or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{misc}->execute ('totalscrapes', $r{totalscrapes}) or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{misc}->execute ('totalscrapesize', $r{totalscrapesize}) or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{misc}->execute ('totalothersize', $r{totalothersize}) or die $self->{sqlhandle}->errstr(); if ($result eq '0E0') { unless (defined $self->{sthandles}->{miscupdate}) { $self->{sthandles}->{miscupdate} = $self->{sqlhandle}->prepare ('update ' . $self->{misctable} . ' set value=? where name=?;') or die $self->{sqlhandle}->errstr(); } $result = $self->{sthandles}->{miscupdate}->execute ($r{totalconnected}, 'totalclients') or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{miscupdate}->execute ($r{totalmaxconnected}, 'totalmaxclients') or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{miscupdate}->execute ($r{reannounce_interval_avg}, 'reannounce_interval_avg') or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{miscupdate}->execute ($r{totalleechers}, 'totalleechers') or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{miscupdate}->execute ($r{totalseeds}, 'totalseeds') or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{miscupdate}->execute (sprintf ("%.0f", $r{totalupped}), 'totaltransfer') or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{miscupdate}->execute ($r{totalcompleted}, 'totalcompleted') or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{miscupdate}->execute ($r{totalgzip}, 'totalgzip') or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{miscupdate}->execute ($r{totalannounces}, 'totalannounces') or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{miscupdate}->execute ($r{totalannouncesize}, 'totalannouncesize') or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{miscupdate}->execute ($r{totalscrapes}, 'totalscrapes') or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{miscupdate}->execute ($r{totalscrapesize}, 'totalscrapesize') or die $self->{sqlhandle}->errstr(); $result = $self->{sthandles}->{miscupdate}->execute ($r{totalothersize}, 'totalothersize') or die $self->{sqlhandle}->errstr(); } } # should peerstats be generated for file X ? sub dopeerstats ($) { my $self = shift; my $info_hash = shift; if ($self->{peerstats}) { unless (defined $self->{sthandles}->{peerstatsquery}) { $self->{sthandles}->{peerstatsquery} = $self->{sqlhandle}->prepare ('select peerstats from ' . $self->{peerstatstable} . ' where info_hash=?') or die $self->{sqlhandle}->errstr(); } my $result = $self->{sthandles}->{peerstatsquery}->execute ($info_hash) or die $self->{sqlhandle}->errstr(); my $arrayref = $self->{sthandles}->{peerstatsquery}->fetchall_arrayref () or die $self->{sqlhandle}->errstr(); my @rows = @$arrayref; return (scalar @rows != 0); } } sub destroy { my $self = shift; $self->{sqlhandle}->disconnect () or die $self->{sqlhandle}->errstr(); } package trackalyze::IO::multiplex; # multiplexes two or more IO:: modules. no strict 'refs'; sub new { my $proto = shift; my $class = ref($proto) || $proto; my $self = { objects => \@_ }; bless ($self, $class); return $self; } # returns filename for torrent-hash; use first object. sub getfilename ($) { my $self = shift; return ${${$self}{objects}}[0]->getfilename (@_); } # returns filesize for torrent-hash sub getfilesize ($) { my $self = shift; return ${${$self}{objects}}[0]->getfilesize (@_); } # saves data about one torrent sub save ($) { my $self = shift; foreach my $object (@{${$self}{objects}}) { $object->save (@_); } } # saves miscellaneous data sub savemisc ($) { my $self = shift; foreach my $object (@{${$self}{objects}}) { $object->savemisc (@_); } } # should peerstats be generated for file X ? sub dopeerstats ($) { my $self = shift; return ${${$self}{objects}}[0]->dopeerstats (@_); } sub destroy { my $self = shift; foreach my $object (@{${$self}{objects}}) { $object->destroy (@_); } } package trackalyze::IO::Dummy; # Blackholes all IO requests no strict 'refs'; sub new { my $proto = shift; my $class = ref($proto) || $proto; my $self = { }; bless ($self, $class); return $self; } # returns filename for torrent-hash sub getfilename ($) { return undef; } # returns filesize for torrent-hash sub getfilesize ($) { return undef; } # saves data about one torrent sub save ($) { } # saves miscellaneous data sub savemisc ($) { } # should peerstats be generated for file X ? sub dopeerstats ($) { return 0; } package trackalyze::Graph::rrdtool; # Provides Graphing via external rrdtool use POSIX qw(strftime); no strict 'refs'; sub new { my $proto = shift; my $class = ref($proto) || $proto; my $p = shift; if (! defined $p) { $p = {}; } my $self = { rrd => defined $p->{rrd} ? $p->{rrd} : 'torrent.rrd', outputdir => defined $p->{outputdir} ? $p->{outputdir} : 'graphs', useexternal => defined $p->{useexternal} ? $p->{useexternal} : undef, graphtitle => defined $p->{graphtitle} ? $p->{graphtitle} : 'BitTorrent network traffic', }; bless ($self, $class); return $self; } sub setup { my $self = shift; my $ts = shift; if (! -e ${$self}{rrd}) { my $a = `rrdtool create ${$self}{rrd} --start $ts \\ DS:torrent:COUNTER:600:0:U \\ DS:users:GAUGE:600:0:U \\ DS:seeds:GAUGE:600:0:U \\ --step 300 \\ RRA:AVERAGE:0.5:1:2000 \\ RRA:AVERAGE:0.5:6:2000 \\ RRA:AVERAGE:0.5:24:2000 \\ RRA:AVERAGE:0.5:288:2000 \\ RRA:MAX:0.5:1:2000 \\ RRA:MAX:0.5:6:2000 \\ RRA:MAX:0.5:24:2000 \\ RRA:MAX:0.5:288:2000`; } } sub update { my $self = shift; my $ts = shift; my $upped = shift; my $users = shift; my $totalseeds = shift; my $uppedlong = sprintf ("%.0f", $upped); my $a = `rrdtool update ${$self}{rrd} $ts:$uppedlong:$users:$totalseeds`; } sub graph { my $self = shift; my $endtime = shift; unless (defined $self->{useexternal} and $self->{useexternal}) { my $daystart = $endtime - 86400; my $weekstart = $endtime - 604800; my $monthstart = $endtime - 2592000; my $yearstart = $endtime - 31536000; my $dayend = $endtime - 300; my $weekend = $endtime - 1800; my $monthend = $endtime - 7200; my $yearend = $endtime - 86400; my $enddateday = strftime('%D %H:%M:%S', localtime($endtime-300)); my $enddateweek = strftime('%D %H:%M:%S', localtime($endtime-1800)); my $enddatemonth = strftime('%D %H:%M:%S', localtime($endtime-7200)); my $enddateyear = strftime('%D %H:%M:%S', localtime($endtime-86400)); my $title = $self->{graphtitle}; my $date = strftime('%D %H:%M:%S', localtime(time)); # Bandwidth graphs ... my $a = `rrdtool graph $self->{outputdir}/torrent_day.png \\ --start $daystart \\ -e $dayend \\ DEF:torrent_in_bytes=$self->{rrd}:torrent:AVERAGE \\ "CDEF:torrent_in_bits=torrent_in_bytes,8,*" \\ "CDEF:torrent_bytes_in=torrent_in_bytes,0,1250000000,LIMIT,UN,0,torrent_in_bytes,IF,86400,*" \\ AREA:torrent_in_bits#00dd00:torrent \\ COMMENT:" +--------------------------\\n" \\ COMMENT:" maximum average current" \\ COMMENT:" | graphed $date\\n" \\ COMMENT:"in " \\ GPRINT:torrent_in_bits:MAX:"%7.2lf %sb/s" \\ GPRINT:torrent_in_bits:AVERAGE:"%7.2lf %Sb/s" \\ GPRINT:torrent_in_bits:LAST:"%7.2lf %Sb/s" \\ COMMENT:"| endtime $enddateday \\n" \\ COMMENT:" | Total in metric \(10^X\),\\n" \\ GPRINT:torrent_bytes_in:AVERAGE:"ROUGHLY %7.2lf %sB total" \\ COMMENT:" | not binary \(2^X\) units." \\ -v "bits/sec" \\ -t "$title traffic \(day\) 5 min avg" \\ -h 100 \\ -w 392 \\ -x "HOUR:1:HOUR:6:HOUR:2:0:%H" \\ -l 0 \\ -a "PNG"`; $a = `rrdtool graph $self->{outputdir}/torrent_week.png \\ --start $weekstart \\ -e $weekend \\ DEF:torrent_in_bytes=$self->{rrd}:torrent:AVERAGE \\ DEF:torrent_in_bytes_max=$self->{rrd}:torrent:MAX \\ "CDEF:torrent_in_bits=torrent_in_bytes,8,*" \\ "CDEF:torrent_bytes_in=torrent_in_bytes,0,1250000000,LIMIT,UN,0,torrent_in_bytes,IF,604800,*" \\ "CDEF:torrent_in_bits_max=torrent_in_bytes_max,8,*" \\ "CDEF:torrent_bytes_in_max=torrent_in_bytes_max,0,1250000000,LIMIT,UN,0,torrent_in_bytes_max,IF,604800,*" \\ "CDEF:torrent_in_bits_maxtop=torrent_in_bits_max,torrent_in_bits,-" \\ AREA:torrent_in_bits#00dd00:torrent \\ STACK:torrent_in_bits_maxtop#cccccc:peak \\ COMMENT:" +--------------------------\\n" \\ COMMENT:" maximum average current" \\ COMMENT:" | graphed $date\\n" \\ COMMENT:"in " \\ GPRINT:torrent_in_bits:MAX:"%7.2lf %sb/s" \\ GPRINT:torrent_in_bits:AVERAGE:"%7.2lf %Sb/s" \\ GPRINT:torrent_in_bits:LAST:"%7.2lf %Sb/s" \\ COMMENT:"| endtime $enddateweek\\n" \\ COMMENT:"5m-peak" \\ GPRINT:torrent_in_bits_max:MAX:"%7.2lf %sb/s" \\ COMMENT:" " \\ GPRINT:torrent_in_bits_max:LAST:"%7.2lf %Sb/s" \\ COMMENT:"| Total in metric \(10^X\),\\n" \\ GPRINT:torrent_bytes_in:AVERAGE:"ROUGHLY %7.2lf %sB total" \\ COMMENT:" | not binary \(2^X\) units." \\ -v "bits/sec" \\ -t "$title traffic \(week\) 30 min avg" \\ -h 100 \\ -w 392 \\ -x "HOUR:6:DAY:1:DAY:1:0:%a" \\ -l 0 \\ -a "PNG"`; $a = `rrdtool graph $self->{outputdir}/torrent_month.png \\ --start $monthstart \\ -e $monthend \\ DEF:torrent_in_bytes=$self->{rrd}:torrent:AVERAGE \\ DEF:torrent_in_bytes_max=$self->{rrd}:torrent:MAX \\ "CDEF:torrent_in_bits=torrent_in_bytes,8,*" \\ "CDEF:torrent_bytes_in=torrent_in_bytes,0,1250000000,LIMIT,UN,0,torrent_in_bytes,IF,2592000,*" \\ "CDEF:torrent_in_bits_max=torrent_in_bytes_max,8,*" \\ "CDEF:torrent_bytes_in_max=torrent_in_bytes_max,0,1250000000,LIMIT,UN,0,torrent_in_bytes_max,IF,2592000,*" \\ "CDEF:torrent_in_bits_maxtop=torrent_in_bits_max,torrent_in_bits,-" \\ AREA:torrent_in_bits#00dd00:torrent \\ STACK:torrent_in_bits_maxtop#cccccc:peak \\ COMMENT:" +--------------------------\\n" \\ COMMENT:" maximum average current" \\ COMMENT:" | graphed $date\\n" \\ COMMENT:"in " \\ GPRINT:torrent_in_bits:MAX:"%7.2lf %sb/s" \\ GPRINT:torrent_in_bits:AVERAGE:"%7.2lf %Sb/s" \\ GPRINT:torrent_in_bits:LAST:"%7.2lf %Sb/s" \\ COMMENT:"| endtime $enddatemonth\\n" \\ COMMENT:"5m-peak" \\ GPRINT:torrent_in_bits_max:MAX:"%7.2lf %sb/s" \\ COMMENT:" " \\ GPRINT:torrent_in_bits_max:LAST:"%7.2lf %Sb/s" \\ COMMENT:"| Total in metric \(10^X\),\\n" \\ GPRINT:torrent_bytes_in:AVERAGE:"ROUGHLY %7.2lf %sB total" \\ COMMENT:" | not binary \(2^X\) units." \\ -v "bits/sec" \\ -t "$title traffic \(month\) 2 hour avg" \\ -h 100 \\ -w 392 \\ -x "DAY:1:WEEK:1:WEEK:1:0:Week %W" \\ -l 0 \\ -a "PNG"`; $a = `rrdtool graph $self->{outputdir}/torrent_year.png \\ --start $yearstart \\ -e $yearend \\ DEF:torrent_in_bytes=$self->{rrd}:torrent:AVERAGE \\ DEF:torrent_in_bytes_max=$self->{rrd}:torrent:MAX \\ "CDEF:torrent_in_bits=torrent_in_bytes,8,*" \\ "CDEF:torrent_bytes_in=torrent_in_bytes,0,1250000000,LIMIT,UN,0,torrent_in_bytes,IF,31536000,*" \\ "CDEF:torrent_in_bits_max=torrent_in_bytes_max,8,*" \\ "CDEF:torrent_bytes_in_max=torrent_in_bytes_max,0,1250000000,LIMIT,UN,0,torrent_in_bytes_max,IF,31536000,*" \\ "CDEF:torrent_in_bits_maxtop=torrent_in_bits_max,torrent_in_bits,-" \\ AREA:torrent_in_bits#00dd00:torrent \\ STACK:torrent_in_bits_maxtop#cccccc:peak \\ COMMENT:" +--------------------------\\n" \\ COMMENT:" maximum average current" \\ COMMENT:" | graphed $date\\n" \\ COMMENT:"in " \\ GPRINT:torrent_in_bits:MAX:"%7.2lf %sb/s" \\ GPRINT:torrent_in_bits:AVERAGE:"%7.2lf %Sb/s" \\ GPRINT:torrent_in_bits:LAST:"%7.2lf %Sb/s" \\ COMMENT:"| endtime $enddateyear\\n" \\ COMMENT:"5m-peak" \\ GPRINT:torrent_in_bits_max:MAX:"%7.2lf %sb/s" \\ COMMENT:" " \\ GPRINT:torrent_in_bits_max:LAST:"%7.2lf %Sb/s" \\ COMMENT:"| Total in metric \(10^X\),\\n" \\ GPRINT:torrent_bytes_in:AVERAGE:"ROUGHLY %7.2lf %sB total" \\ COMMENT:" | not binary \(2^X\) units." \\ -v "bits/sec" \\ -t "$title traffic \(year\) 1 day avg" \\ -h 100 \\ -w 392 \\ -x "MONTH:1:MONTH:1:MONTH:1:0:%b" \\ -l 0 \\ -a "PNG"`; $a = `rrdtool graph $self->{outputdir}/users_day.png \\ --start $daystart \\ -e $dayend \\ DEF:tracker_users_unchecked=$self->{rrd}:users:AVERAGE \\ DEF:tracker_seeds_unchecked=$self->{rrd}:seeds:AVERAGE \\ "CDEF:tracker_users=tracker_users_unchecked,UN,0,tracker_users_unchecked,IF" \\ "CDEF:tracker_seeds=tracker_seeds_unchecked,UN,0,tracker_seeds_unchecked,IF" \\ "CDEF:tracker_leechers=tracker_users,tracker_seeds,-" \\ "CDEF:tracker_ratio_unchecked=tracker_seeds,tracker_leechers,/" \\ "CDEF:tracker_ratio=tracker_ratio_unchecked,UN,0,tracker_ratio_unchecked,IF" \\ AREA:tracker_seeds#0000ff:Seeds \\ STACK:tracker_leechers#0000aa:Leechers \\ COMMENT:" +--------------------------\\n" \\ COMMENT:" maximum average current" \\ COMMENT:" | graphed $date\\n" \\ COMMENT:"Seeds " \\ GPRINT:tracker_seeds:MAX:"%7.0lf" \\ GPRINT:tracker_seeds:AVERAGE:" %7.0lf" \\ GPRINT:tracker_seeds:LAST:" %7.0lf" \\ COMMENT:" | endtime $enddateday\\n" \\ COMMENT:"Leechers " \\ GPRINT:tracker_leechers:MAX:"%7.0lf" \\ GPRINT:tracker_leechers:AVERAGE:" %7.0lf" \\ GPRINT:tracker_leechers:LAST:" %7.0lf" \\ COMMENT:" | Average Seeder to Leecher\\n" \\ COMMENT:"All Clients" \\ GPRINT:tracker_users:MAX:"%7.0lf" \\ GPRINT:tracker_users:AVERAGE:" %7.0lf" \\ GPRINT:tracker_users:LAST:" %7.0lf" \\ GPRINT:tracker_ratio:AVERAGE:" | ratio is %3.2lf\\n" \\ COMMENT:" |\\n" \\ --alt-y-grid \\ -v users \\ -t "$title users (day) 5m avg" \\ -h 100 \\ -w 392 \\ -x "HOUR:1:HOUR:6:HOUR:2:0:%H" \\ -l 0 \\ --units-exponent 0 \\ -a "PNG"`; $a = `rrdtool graph $self->{outputdir}/users_week.png \\ --start $weekstart \\ -e $weekend \\ DEF:tracker_users_unchecked=$self->{rrd}:users:AVERAGE \\ DEF:tracker_seeds_unchecked=$self->{rrd}:seeds:AVERAGE \\ DEF:tracker_users_unchecked_max=$self->{rrd}:users:MAX \\ DEF:tracker_seeds_unchecked_max=$self->{rrd}:seeds:MAX \\ "CDEF:tracker_users=tracker_users_unchecked,UN,0,tracker_users_unchecked,IF" \\ "CDEF:tracker_seeds=tracker_seeds_unchecked,UN,0,tracker_seeds_unchecked,IF" \\ "CDEF:tracker_users_max=tracker_users_unchecked_max,UN,0,tracker_users_unchecked_max,IF" \\ "CDEF:tracker_seeds_max=tracker_seeds_unchecked_max,UN,0,tracker_seeds_unchecked_max,IF" \\ "CDEF:tracker_leechers=tracker_users,tracker_seeds,-" \\ "CDEF:tracker_leechers_max=tracker_users_max,tracker_seeds_max,-" \\ "CDEF:tracker_users_maxtop=tracker_users_max,tracker_users,-" \\ "CDEF:tracker_ratio_unchecked=tracker_seeds,tracker_leechers,/" \\ "CDEF:tracker_ratio=tracker_ratio_unchecked,UN,0,tracker_ratio_unchecked,IF" \\ AREA:tracker_seeds#0000ff:Seeds \\ STACK:tracker_leechers#0000aa:Leechers \\ STACK:tracker_users_maxtop#cccccc:peak \\ COMMENT:" +--------------------------\\n" \\ COMMENT:" maximum average current" \\ COMMENT:" | graphed $date\\n" \\ COMMENT:"Seeds " \\ GPRINT:tracker_seeds:MAX:"%7.0lf" \\ GPRINT:tracker_seeds:AVERAGE:" %7.0lf" \\ GPRINT:tracker_seeds:LAST:" %7.0lf" \\ COMMENT:" | endtime $enddateweek\\n" \\ COMMENT:"Leechers " \\ GPRINT:tracker_leechers:MAX:"%7.0lf" \\ GPRINT:tracker_leechers:AVERAGE:" %7.0lf" \\ GPRINT:tracker_leechers:LAST:" %7.0lf" \\ COMMENT:" | Average Seeder to Leecher\\n" \\ COMMENT:"All Clients" \\ GPRINT:tracker_users:MAX:"%7.0lf" \\ GPRINT:tracker_users:AVERAGE:" %7.0lf" \\ GPRINT:tracker_users:LAST:" %7.0lf" \\ GPRINT:tracker_ratio:AVERAGE:" | ratio is %3.2lf\\n" \\ COMMENT:"Peak " \\ GPRINT:tracker_users_max:MAX:"%7.0lf" \\ COMMENT:" " \\ GPRINT:tracker_users_max:LAST:" %7.0lf" \\ COMMENT:" | 5m peak of all clients\\n" \\ --alt-y-grid \\ -v users \\ -t "$title users (week) 30 min avg" \\ -h 100 \\ -w 392 \\ -x "HOUR:6:DAY:1:DAY:1:0:%a" \\ -l 0 \\ --units-exponent 0 \\ -a "PNG"`; $a = `rrdtool graph $self->{outputdir}/users_month.png \\ --start $monthstart \\ -e $monthend \\ DEF:tracker_users_unchecked=$self->{rrd}:users:AVERAGE \\ DEF:tracker_seeds_unchecked=$self->{rrd}:seeds:AVERAGE \\ DEF:tracker_users_unchecked_max=$self->{rrd}:users:MAX \\ DEF:tracker_seeds_unchecked_max=$self->{rrd}:seeds:MAX \\ "CDEF:tracker_users=tracker_users_unchecked,UN,0,tracker_users_unchecked,IF" \\ "CDEF:tracker_seeds=tracker_seeds_unchecked,UN,0,tracker_seeds_unchecked,IF" \\ "CDEF:tracker_users_max=tracker_users_unchecked_max,UN,0,tracker_users_unchecked_max,IF" \\ "CDEF:tracker_seeds_max=tracker_seeds_unchecked_max,UN,0,tracker_seeds_unchecked_max,IF" \\ "CDEF:tracker_leechers=tracker_users,tracker_seeds,-" \\ "CDEF:tracker_leechers_max=tracker_users_max,tracker_seeds_max,-" \\ "CDEF:tracker_users_maxtop=tracker_users_max,tracker_users,-" \\ "CDEF:tracker_ratio_unchecked=tracker_seeds,tracker_leechers,/" \\ "CDEF:tracker_ratio=tracker_ratio_unchecked,UN,0,tracker_ratio_unchecked,IF" \\ AREA:tracker_seeds#0000ff:Seeds \\ STACK:tracker_leechers#0000aa:Leechers \\ STACK:tracker_users_maxtop#cccccc:peak \\ COMMENT:" +--------------------------\\n" \\ COMMENT:" maximum average current" \\ COMMENT:" | graphed $date\\n" \\ COMMENT:"Seeds " \\ GPRINT:tracker_seeds:MAX:"%7.0lf" \\ GPRINT:tracker_seeds:AVERAGE:" %7.0lf" \\ GPRINT:tracker_seeds:LAST:" %7.0lf" \\ COMMENT:" | endtime $enddatemonth\\n" \\ COMMENT:"Leechers " \\ GPRINT:tracker_leechers:MAX:"%7.0lf" \\ GPRINT:tracker_leechers:AVERAGE:" %7.0lf" \\ GPRINT:tracker_leechers:LAST:" %7.0lf" \\ COMMENT:" | Average Seeder to Leecher\\n" \\ COMMENT:"All Clients" \\ GPRINT:tracker_users:MAX:"%7.0lf" \\ GPRINT:tracker_users:AVERAGE:" %7.0lf" \\ GPRINT:tracker_users:LAST:" %7.0lf" \\ GPRINT:tracker_ratio:AVERAGE:" | ratio is %3.2lf\\n" \\ COMMENT:"Peak " \\ GPRINT:tracker_users_max:MAX:"%7.0lf" \\ COMMENT:" " \\ GPRINT:tracker_users_max:LAST:" %7.0lf" \\ COMMENT:" | 5m peak of all clients\\n" \\ --alt-y-grid \\ -v users \\ -t "$title users (month) 2 hour avg" \\ -h 100 \\ -w 392 \\ -x "DAY:1:WEEK:1:WEEK:1:0:Week %W" \\ -l 0 \\ --units-exponent 0 \\ -a "PNG"`; $a = `rrdtool graph $self->{outputdir}/users_year.png \\ --start $yearstart \\ -e $yearend \\ DEF:tracker_users_unchecked=$self->{rrd}:users:AVERAGE \\ DEF:tracker_seeds_unchecked=$self->{rrd}:seeds:AVERAGE \\ DEF:tracker_users_unchecked_max=$self->{rrd}:users:MAX \\ DEF:tracker_seeds_unchecked_max=$self->{rrd}:seeds:MAX \\ "CDEF:tracker_users=tracker_users_unchecked,UN,0,tracker_users_unchecked,IF" \\ "CDEF:tracker_seeds=tracker_seeds_unchecked,UN,0,tracker_seeds_unchecked,IF" \\ "CDEF:tracker_users_max=tracker_users_unchecked_max,UN,0,tracker_users_unchecked_max,IF" \\ "CDEF:tracker_seeds_max=tracker_seeds_unchecked_max,UN,0,tracker_seeds_unchecked_max,IF" \\ "CDEF:tracker_leechers=tracker_users,tracker_seeds,-" \\ "CDEF:tracker_leechers_max=tracker_users_max,tracker_seeds_max,-" \\ "CDEF:tracker_users_maxtop=tracker_users_max,tracker_users,-" \\ "CDEF:tracker_ratio_unchecked=tracker_seeds,tracker_leechers,/" \\ "CDEF:tracker_ratio=tracker_ratio_unchecked,UN,0,tracker_ratio_unchecked,IF" \\ AREA:tracker_seeds#0000ff:Seeds \\ STACK:tracker_leechers#0000aa:Leechers \\ STACK:tracker_users_maxtop#cccccc:peak \\ COMMENT:" +--------------------------\\n" \\ COMMENT:" maximum average current" \\ COMMENT:" | graphed $date\\n" \\ COMMENT:"Seeds " \\ GPRINT:tracker_seeds:MAX:"%7.0lf" \\ GPRINT:tracker_seeds:AVERAGE:" %7.0lf" \\ GPRINT:tracker_seeds:LAST:" %7.0lf" \\ COMMENT:" | endtime $enddateyear\\n" \\ COMMENT:"Leechers " \\ GPRINT:tracker_leechers:MAX:"%7.0lf" \\ GPRINT:tracker_leechers:AVERAGE:" %7.0lf" \\ GPRINT:tracker_leechers:LAST:" %7.0lf" \\ COMMENT:" | Average Seeder to Leecher\\n" \\ COMMENT:"All Clients" \\ GPRINT:tracker_users:MAX:"%7.0lf" \\ GPRINT:tracker_users:AVERAGE:" %7.0lf" \\ GPRINT:tracker_users:LAST:" %7.0lf" \\ GPRINT:tracker_ratio:AVERAGE:" | ratio is %3.2lf\\n" \\ COMMENT:"Peak " \\ GPRINT:tracker_users_max:MAX:"%7.0lf" \\ COMMENT:" " \\ GPRINT:tracker_users_max:LAST:" %7.0lf" \\ COMMENT:" | 5m peak of all clients\\n" \\ --alt-y-grid \\ -v users \\ -t "$title users (year) 1 day avg" \\ -h 100 \\ -w 392 \\ -x "MONTH:1:MONTH:1:MONTH:1:0:%b" \\ -l 0 \\ --units-exponent 0 \\ -a "PNG"`; } else { my $a = `${$self}{useexternal}`; } } package trackalyze::Graph::RRDs; # Provides Graphing via shared rrdtool use POSIX qw(strftime); # Only try to load RRDs if it's actually available BEGIN { eval('use RRDs;'); } no strict 'refs'; sub new { my $proto = shift; my $class = ref($proto) || $proto; my $p = shift; if (! defined $p) { $p = {}; } if (not exists $INC{'RRDs.pm'}) { print "Sorry, RRDs module could not be loaded (is it installed ?).\n"; print "Please switch to external rrdtool module.\n"; exit 0; } my $self = { rrd => defined $p->{rrd} ? $p->{rrd} : 'torrent.rrd', outputdir => defined $p->{outputdir} ? $p->{outputdir} : 'graphs', useexternal => defined $p->{useexternal} ? $p->{useexternal} : undef, graphtitle => defined $p->{graphtitle} ? $p->{graphtitle} : 'BitTorrent network traffic', }; bless ($self, $class); return $self; } sub setup { my $self = shift; my $ts = shift; if (! -e $self->{rrd}) { RRDs::create($self->{rrd}, '--start' => $ts, 'DS:torrent:COUNTER:600:0:U', 'DS:users:GAUGE:600:0:U', 'DS:seeds:GAUGE:600:0:U', '--step' => 300, 'RRA:AVERAGE:0.5:1:2000', 'RRA:AVERAGE:0.5:6:2000', 'RRA:AVERAGE:0.5:24:2000', 'RRA:AVERAGE:0.5:288:2000', 'RRA:MAX:0.5:1:2000', 'RRA:MAX:0.5:6:2000', 'RRA:MAX:0.5:24:2000', 'RRA:MAX:0.5:288:2000'); } } sub update { my $self = shift; my $ts = shift; my $upped = shift; my $users = shift; my $totalseeds = shift; RRDs::update($self->{rrd}, $ts.':'.sprintf ("%.0f", $upped).':'.$users.':'. $totalseeds); } sub graph { my $self = shift; my $endtime = shift; unless (defined $self->{useexternal} and $self->{useexternal}) { my $daystart = $endtime - 86400; my $weekstart = $endtime - 604800; my $monthstart = $endtime - 2592000; my $yearstart = $endtime - 31536000; my $title = $self->{graphtitle}; my $date = strftime('%D %H:%M:%S', localtime(time)); my $graphdate = strftime('%D %H:%M:%S', localtime($endtime)); # Bandwidth graphs ... RRDs::graph( $self->{outputdir}.'/torrent_day.png', '--start' => $daystart, '-e' => $endtime-300, "DEF:torrent_in_bytes=$self->{rrd}:torrent:AVERAGE", 'CDEF:torrent_in_bits=torrent_in_bytes,8,*', 'CDEF:torrent_bytes_in=torrent_in_bytes,0,1250000000,LIMIT,UN,0,torrent_in_bytes,IF,86400,*', 'AREA:torrent_in_bits#00dd00:torrent', 'COMMENT: +--------------------------\n', 'COMMENT: maximum average current', 'COMMENT: | graphed '.$date.'\n', 'COMMENT:in ', 'GPRINT:torrent_in_bits:MAX:%7.2lf %sb/s', 'GPRINT:torrent_in_bits:AVERAGE:%7.2lf %Sb/s', 'GPRINT:torrent_in_bits:LAST:%7.2lf %Sb/s', 'COMMENT:| endtime '.strftime('%D %H:%M:%S', localtime($endtime-300)).'\n', 'COMMENT: | Total in metric (10^X),\n', 'GPRINT:torrent_bytes_in:AVERAGE:ROUGHLY %7.2lf %sB total', 'COMMENT: | not binary (2^X) units.', '-v' => "bits/sec", '-t' => $title . ' traffic (day) 5 min avg', '-h' => 100, '-w' => 392, '-x' => 'HOUR:1:HOUR:6:HOUR:2:0:%H', '-l' => 0, '-a' => 'PNG'); RRDs::graph( $self->{outputdir}.'/torrent_week.png', '--start' => $weekstart, '-e' => $endtime-1800, "DEF:torrent_in_bytes=$self->{rrd}:torrent:AVERAGE", "DEF:torrent_in_bytes_max=$self->{rrd}:torrent:MAX", 'CDEF:torrent_in_bits=torrent_in_bytes,8,*', 'CDEF:torrent_bytes_in=torrent_in_bytes,0,1250000000,LIMIT,UN,0,torrent_in_bytes,IF,604800,*', 'CDEF:torrent_in_bits_max=torrent_in_bytes_max,8,*', 'CDEF:torrent_bytes_in_max=torrent_in_bytes_max,0,1250000000,LIMIT,UN,0,torrent_in_bytes_max,IF,604800,*', 'CDEF:torrent_in_bits_maxtop=torrent_in_bits_max,torrent_in_bits,-', 'AREA:torrent_in_bits#00dd00:torrent', 'STACK:torrent_in_bits_maxtop#cccccc:peak', 'COMMENT: +--------------------------\n', 'COMMENT: maximum average current', 'COMMENT: | graphed '.$date.'\n', 'COMMENT:in ', 'GPRINT:torrent_in_bits:MAX:%7.2lf %sb/s', 'GPRINT:torrent_in_bits:AVERAGE:%7.2lf %Sb/s', 'GPRINT:torrent_in_bits:LAST:%7.2lf %Sb/s', 'COMMENT:| endtime '.strftime('%D %H:%M:%S', localtime($endtime-1800)).'\n', 'COMMENT:5m-peak', 'GPRINT:torrent_in_bits_max:MAX:%7.2lf %sb/s', 'COMMENT: ', 'GPRINT:torrent_in_bits_max:LAST:%7.2lf %Sb/s', 'COMMENT:| Total in metric (10^X),\n', 'GPRINT:torrent_bytes_in:AVERAGE:ROUGHLY %7.2lf %sB total', 'COMMENT: | not binary (2^X) units. ', '-v' => 'bits/sec', '-t' => $title . ' traffic (week) 30 min avg', '-h' => 100, '-w' => 392, '-x' => 'HOUR:6:DAY:1:DAY:1:0:%a', '-l' => 0, '-a' => 'PNG'); RRDs::graph( $self->{outputdir}.'/torrent_month.png', '--start' => $monthstart, '-e' => $endtime-7200, "DEF:torrent_in_bytes=$self->{rrd}:torrent:AVERAGE", "DEF:torrent_in_bytes_max=$self->{rrd}:torrent:MAX", 'CDEF:torrent_in_bits=torrent_in_bytes,8,*', 'CDEF:torrent_bytes_in=torrent_in_bytes,0,1250000000,LIMIT,UN,0,torrent_in_bytes,IF,2592000,*', 'CDEF:torrent_in_bits_max=torrent_in_bytes_max,8,*', 'CDEF:torrent_bytes_in_max=torrent_in_bytes_max,0,1250000000,LIMIT,UN,0,torrent_in_bytes_max,IF,2592000,*', 'CDEF:torrent_in_bits_maxtop=torrent_in_bits_max,torrent_in_bits,-', 'AREA:torrent_in_bits#00dd00:torrent', 'STACK:torrent_in_bits_maxtop#cccccc:peak', 'COMMENT: +--------------------------\n', 'COMMENT: maximum average current', 'COMMENT: | graphed '.$date.'\n', 'COMMENT:in ', 'GPRINT:torrent_in_bits:MAX:%7.2lf %sb/s', 'GPRINT:torrent_in_bits:AVERAGE:%7.2lf %Sb/s', 'GPRINT:torrent_in_bits:LAST:%7.2lf %Sb/s', 'COMMENT:| endtime '.strftime('%D %H:%M:%S', localtime($endtime-7200)).'\n', 'COMMENT:5m-peak', 'GPRINT:torrent_in_bits_max:MAX:%7.2lf %sb/s', 'COMMENT: ', 'GPRINT:torrent_in_bits_max:LAST:%7.2lf %Sb/s', 'COMMENT:| Total in metric (10^X),\n', 'GPRINT:torrent_bytes_in:AVERAGE:ROUGHLY %7.2lf %sB total', 'COMMENT: | not binary (2^X) units.', '-v' => 'bits/sec', '-t' => $title .' traffic (month) 2 hour avg', '-h' => 100, '-w' => 392, '-x' => 'DAY:1:WEEK:1:WEEK:1:0:Week %W', '-l' => 0, '-a' => 'PNG'); RRDs::graph( $self->{outputdir}.'/torrent_year.png', '--start' => $yearstart, '-e' => $endtime-86400, "DEF:torrent_in_bytes=$self->{rrd}:torrent:AVERAGE", "DEF:torrent_in_bytes_max=$self->{rrd}:torrent:MAX", 'CDEF:torrent_in_bits=torrent_in_bytes,8,*', 'CDEF:torrent_bytes_in=torrent_in_bytes,0,1250000000,LIMIT,UN,0,torrent_in_bytes,IF,31536000,*', 'CDEF:torrent_in_bits_max=torrent_in_bytes_max,8,*', 'CDEF:torrent_bytes_in_max=torrent_in_bytes_max,0,1250000000,LIMIT,UN,0,torrent_in_bytes_max,IF,31536000,*', 'CDEF:torrent_in_bits_maxtop=torrent_in_bits_max,torrent_in_bits,-', 'AREA:torrent_in_bits#00dd00:torrent', 'STACK:torrent_in_bits_maxtop#cccccc:peak', 'COMMENT: +--------------------------\n', 'COMMENT: maximum average current', 'COMMENT: | graphed '.$date.'\n', 'COMMENT:in ', 'GPRINT:torrent_in_bits:MAX:%7.2lf %sb/s', 'GPRINT:torrent_in_bits:AVERAGE:%7.2lf %Sb/s', 'GPRINT:torrent_in_bits:LAST:%7.2lf %Sb/s', 'COMMENT:| endtime '.strftime('%D %H:%M:%S', localtime($endtime-86400)).'\n', 'COMMENT:5m-peak', 'GPRINT:torrent_in_bits_max:MAX:%7.2lf %sb/s', 'COMMENT: ', 'GPRINT:torrent_in_bits_max:LAST:%7.2lf %Sb/s', 'COMMENT:| Total in metric (10^X),\n', 'GPRINT:torrent_bytes_in:AVERAGE:ROUGHLY %7.2lf %sB total', 'COMMENT: | not binary (2^X) units.', '-v' => 'bits/sec', '-t' => $title . ' traffic (year) 1 day avg', '-h' => 100, '-w' => 392, '-x' => 'MONTH:1:MONTH:1:MONTH:1:0:%b', '-l' => 0, '-a' => 'PNG'); # User Graphs ... RRDs::graph( $self->{outputdir}.'/users_day.png', '--start' => $daystart, '-e' => $endtime-300, "DEF:tracker_users_unchecked=$self->{rrd}:users:AVERAGE", "DEF:tracker_seeds_unchecked=$self->{rrd}:seeds:AVERAGE", 'CDEF:tracker_users=tracker_users_unchecked,UN,0,tracker_users_unchecked,IF', 'CDEF:tracker_seeds=tracker_seeds_unchecked,UN,0,tracker_seeds_unchecked,IF', 'CDEF:tracker_leechers=tracker_users,tracker_seeds,-', 'CDEF:tracker_ratio_unchecked=tracker_seeds,tracker_leechers,/', 'CDEF:tracker_ratio=tracker_ratio_unchecked,UN,0,tracker_ratio_unchecked,IF', 'AREA:tracker_seeds#0000ff:Seeds', 'STACK:tracker_leechers#0000aa:Leechers', 'COMMENT: +--------------------------\n', 'COMMENT: maximum average current', 'COMMENT: | graphed '.$date.'\n', 'COMMENT:Seeds ', 'GPRINT:tracker_seeds:MAX:%7.0lf', 'GPRINT:tracker_seeds:AVERAGE: %7.0lf', 'GPRINT:tracker_seeds:LAST: %7.0lf', 'COMMENT: | endtime '.strftime('%D %H:%M:%S', localtime($endtime-300)).'\n', 'COMMENT:Leechers ', 'GPRINT:tracker_leechers:MAX:%7.0lf', 'GPRINT:tracker_leechers:AVERAGE: %7.0lf', 'GPRINT:tracker_leechers:LAST: %7.0lf', 'COMMENT: | Average Seeder to Leecher\n', 'COMMENT:All Clients', 'GPRINT:tracker_users:MAX:%7.0lf', 'GPRINT:tracker_users:AVERAGE: %7.0lf', 'GPRINT:tracker_users:LAST: %7.0lf', 'GPRINT:tracker_ratio:AVERAGE: | ratio is %3.2lf\n', 'COMMENT: |\n', '--alt-y-grid', '-v' => 'users', '-t' => $title . ' users (day) 5m avg', '-h' => 100, '-w' => 392, '-x' => 'HOUR:1:HOUR:6:HOUR:2:0:%H', '-l' => 0, '--units-exponent' => 0, '-a' => 'PNG'); RRDs::graph( $self->{outputdir}.'/users_week.png', '--start' => $weekstart, '-e' => $endtime-1800, "DEF:tracker_users_unchecked=$self->{rrd}:users:AVERAGE", "DEF:tracker_seeds_unchecked=$self->{rrd}:seeds:AVERAGE", "DEF:tracker_users_unchecked_max=$self->{rrd}:users:MAX", "DEF:tracker_seeds_unchecked_max=$self->{rrd}:seeds:MAX", 'CDEF:tracker_users=tracker_users_unchecked,UN,0,tracker_users_unchecked,IF', 'CDEF:tracker_seeds=tracker_seeds_unchecked,UN,0,tracker_seeds_unchecked,IF', 'CDEF:tracker_users_max=tracker_users_unchecked_max,UN,0,tracker_users_unchecked_max,IF', 'CDEF:tracker_seeds_max=tracker_seeds_unchecked_max,UN,0,tracker_seeds_unchecked_max,IF', 'CDEF:tracker_leechers=tracker_users,tracker_seeds,-', 'CDEF:tracker_leechers_max=tracker_users_max,tracker_seeds_max,-', 'CDEF:tracker_users_maxtop=tracker_users_max,tracker_users,-', 'CDEF:tracker_ratio_unchecked=tracker_seeds,tracker_leechers,/', 'CDEF:tracker_ratio=tracker_ratio_unchecked,UN,0,tracker_ratio_unchecked,IF', 'AREA:tracker_seeds#0000ff:Seeds', 'STACK:tracker_leechers#0000aa:Leechers', 'STACK:tracker_users_maxtop#cccccc:peak', 'COMMENT: +--------------------------\n', 'COMMENT: maximum average current', 'COMMENT: | graphed '.$date.'\n', 'COMMENT:Seeds ', 'GPRINT:tracker_seeds:MAX:%7.0lf', 'GPRINT:tracker_seeds:AVERAGE: %7.0lf', 'GPRINT:tracker_seeds:LAST: %7.0lf', 'COMMENT: | endtime '.strftime('%D %H:%M:%S', localtime($endtime-1800)).'\n', 'COMMENT:Leechers ', 'GPRINT:tracker_leechers:MAX:%7.0lf', 'GPRINT:tracker_leechers:AVERAGE: %7.0lf', 'GPRINT:tracker_leechers:LAST: %7.0lf', 'COMMENT: | Average Seeder to Leecher\n', 'COMMENT:All Clients', 'GPRINT:tracker_users:MAX:%7.0lf', 'GPRINT:tracker_users:AVERAGE: %7.0lf', 'GPRINT:tracker_users:LAST: %7.0lf', 'GPRINT:tracker_ratio:AVERAGE: | ratio is %3.2lf\n', 'COMMENT:Peak ', 'GPRINT:tracker_users_max:MAX:%7.0lf', 'COMMENT: ', 'GPRINT:tracker_users_max:LAST: %7.0lf', 'COMMENT: | 5m peak of all clients\n', '--alt-y-grid', '-v' => 'users', '-t' => $title . ' users (week) 30 min avg', '-h' => 100, '-w' => 392, '-x' => 'HOUR:6:DAY:1:DAY:1:0:%a', '-l' => 0, '--units-exponent' => 0, '-a' => 'PNG'); RRDs::graph( $self->{outputdir}.'/users_month.png', '--start' => $monthstart, '-e' => $endtime-7200, "DEF:tracker_users_unchecked=$self->{rrd}:users:AVERAGE", "DEF:tracker_seeds_unchecked=$self->{rrd}:seeds:AVERAGE", "DEF:tracker_users_unchecked_max=$self->{rrd}:users:MAX", "DEF:tracker_seeds_unchecked_max=$self->{rrd}:seeds:MAX", 'CDEF:tracker_users=tracker_users_unchecked,UN,0,tracker_users_unchecked,IF', 'CDEF:tracker_seeds=tracker_seeds_unchecked,UN,0,tracker_seeds_unchecked,IF', 'CDEF:tracker_users_max=tracker_users_unchecked_max,UN,0,tracker_users_unchecked_max,IF', 'CDEF:tracker_seeds_max=tracker_seeds_unchecked_max,UN,0,tracker_seeds_unchecked_max,IF', 'CDEF:tracker_leechers=tracker_users,tracker_seeds,-', 'CDEF:tracker_leechers_max=tracker_users_max,tracker_seeds_max,-', 'CDEF:tracker_users_maxtop=tracker_users_max,tracker_users,-', 'CDEF:tracker_ratio_unchecked=tracker_seeds,tracker_leechers,/', 'CDEF:tracker_ratio=tracker_ratio_unchecked,UN,0,tracker_ratio_unchecked,IF', 'AREA:tracker_seeds#0000ff:Seeds', 'STACK:tracker_leechers#0000aa:Leechers', 'STACK:tracker_users_maxtop#cccccc:peak', 'COMMENT: +--------------------------\n', 'COMMENT: maximum average current', 'COMMENT: | graphed '.$date.'\n', 'COMMENT:Seeds ', 'GPRINT:tracker_seeds:MAX:%7.0lf', 'GPRINT:tracker_seeds:AVERAGE: %7.0lf', 'GPRINT:tracker_seeds:LAST: %7.0lf', 'COMMENT: | endtime '.strftime('%D %H:%M:%S', localtime($endtime-7200)).'\n', 'COMMENT:Leechers ', 'GPRINT:tracker_leechers:MAX:%7.0lf', 'GPRINT:tracker_leechers:AVERAGE: %7.0lf', 'GPRINT:tracker_leechers:LAST: %7.0lf', 'COMMENT: | Average Seeder to Leecher\n', 'COMMENT:All Clients', 'GPRINT:tracker_users:MAX:%7.0lf', 'GPRINT:tracker_users:AVERAGE: %7.0lf', 'GPRINT:tracker_users:LAST: %7.0lf', 'GPRINT:tracker_ratio:AVERAGE: | ratio is %3.2lf\n', 'COMMENT:Peak ', 'GPRINT:tracker_users_max:MAX:%7.0lf', 'COMMENT: ', 'GPRINT:tracker_users_max:LAST: %7.0lf', 'COMMENT: | 5m peak of all clients\n', '--alt-y-grid', '-v' => 'users', '-t' => $title . ' users (month) 2 hour avg', '-h' => 100, '-w' => 392, '-x' => 'DAY:1:WEEK:1:WEEK:1:0:Week %W', '-l' => 0, '--units-exponent' => 0, '-a' => 'PNG'); RRDs::graph( $self->{outputdir}.'/users_year.png', '--start' => $yearstart, '-e' => $endtime-86400, "DEF:tracker_users_unchecked=$self->{rrd}:users:AVERAGE", "DEF:tracker_seeds_unchecked=$self->{rrd}:seeds:AVERAGE", "DEF:tracker_users_unchecked_max=$self->{rrd}:users:MAX", "DEF:tracker_seeds_unchecked_max=$self->{rrd}:seeds:MAX", 'CDEF:tracker_users=tracker_users_unchecked,UN,0,tracker_users_unchecked,IF', 'CDEF:tracker_seeds=tracker_seeds_unchecked,UN,0,tracker_seeds_unchecked,IF', 'CDEF:tracker_users_max=tracker_users_unchecked_max,UN,0,tracker_users_unchecked_max,IF', 'CDEF:tracker_seeds_max=tracker_seeds_unchecked_max,UN,0,tracker_seeds_unchecked_max,IF', 'CDEF:tracker_leechers=tracker_users,tracker_seeds,-', 'CDEF:tracker_leechers_max=tracker_users_max,tracker_seeds_max,-', 'CDEF:tracker_users_maxtop=tracker_users_max,tracker_users,-', 'CDEF:tracker_ratio_unchecked=tracker_seeds,tracker_leechers,/', 'CDEF:tracker_ratio=tracker_ratio_unchecked,UN,0,tracker_ratio_unchecked,IF', 'AREA:tracker_seeds#0000ff:Seeds', 'STACK:tracker_leechers#0000aa:Leechers', 'STACK:tracker_users_maxtop#cccccc:peak', 'COMMENT: +--------------------------\n', 'COMMENT: maximum average current', 'COMMENT: | graphed '.$date.'\n', 'COMMENT:Seeds ', 'GPRINT:tracker_seeds:MAX:%7.0lf', 'GPRINT:tracker_seeds:AVERAGE: %7.0lf', 'GPRINT:tracker_seeds:LAST: %7.0lf', 'COMMENT: | endtime '.strftime('%D %H:%M:%S', localtime($endtime-86400)).'\n', 'COMMENT:Leechers ', 'GPRINT:tracker_leechers:MAX:%7.0lf', 'GPRINT:tracker_leechers:AVERAGE: %7.0lf', 'GPRINT:tracker_leechers:LAST: %7.0lf', 'COMMENT: | Average Seeder to Leecher\n', 'COMMENT:All Clients', 'GPRINT:tracker_users:MAX:%7.0lf', 'GPRINT:tracker_users:AVERAGE: %7.0lf', 'GPRINT:tracker_users:LAST: %7.0lf', 'GPRINT:tracker_ratio:AVERAGE: | ratio is %3.2lf\n', 'COMMENT:Peak ', 'GPRINT:tracker_users_max:MAX:%7.0lf', 'COMMENT: ', 'GPRINT:tracker_users_max:LAST: %7.0lf', 'COMMENT: | 5m peak of all clients\n', '--alt-y-grid', '-v' => 'users', '-t' => $title . ' users (year) 1 day avg', '-h' => 100, '-w' => 392, '-x' => 'MONTH:1:MONTH:1:MONTH:1:0:%b', '-l' => 0, '--units-exponent' => 0, '-a' => 'PNG'); } else { my $a = `$self->{useexternal}`; } } package trackalyze::Graph::Dummy; # Blackholes all Graphing requests no strict 'refs'; sub new { my $proto = shift; my $class = ref($proto) || $proto; my $self = { }; bless ($self, $class); return $self; } sub setup { my $self = shift; } sub update { } sub graph { }