#!/usr/local/bin/perl -w # dialup-digest # Digests the dialup log file # # Input: a file's path # Output: digested report # # 06/28/2001 Version 1.0 # Use and distribute this script as per the Artistic License # Copyright (C) 2001 Igor S. Livshits # Define some constants # $true= (1==1); $false= !$true; $unexpected= "unexpected"; # key for unexpected log lines $noARAPUser= ";"; # denotes a failed ARAP attempt $programYear= 99; # does not much matter since it is bogus @monthsToDays= # lookup table for days elapsed (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334); %monthsToNumeric= # lookup table for months as ordered ("Jan" => "01", "Feb" => "02", "Mar" => "03", "Apr" => "04", "May" => "05", "Jun" => "06", "Jul" => "07", "Aug" => "08", "Sep" => "09", "Oct" => "10", "Nov" => "11", "Dec" => "12" ); %daysEachMonth= # lookup table for days within each month ("01" => "31", "02" => "28", "03" => "31", "04" => "30", "05" => "31", "06" => "30", "07" => "31", "08" => "31", "09" => "30", "10" => "31", "11" => "30", "12" => "31" ); %secondsToMonths= # lookup table for months elapsed ( 0 => "01", 2678400 => "02", 5097600 => "03", 7776000 => "04", 10368000 => "05", 13046400 => "06", 15638400 => "07", 18316800 => "08", 20995200 => "09", 23587200 => "10", 26265600 => "11", 28857600 => "12" ); # Define storage for reduced data # %ports= (); # tracks disconnecting ports @isdnInterfaces= (); # a history of ISDN channel activity @modemInterfaces= (); # a history of modem activity %isdnChannels= (); # tracks active ISDN channels %userDurations= (); # tracks total user connect times %arapDurations= (); # tracks ARAP connect times per modem %arapUsers= (); # tracks ARAP users per modem %durations= (); # tracks all connect times %connections= (); # tracks user connect counts %userReceived= (); # tracks user data transfers %userTransmitted= (); # tracks user data transfers %received= (); # total data received per modem %transmitted= (); # total data transmitted per modem @configured= (); # all configuration changes %modemProblems= (); # problem counts for each modem %callerProblems= (); # problem counts for each calling number %unmatched= (); # unmatched log records %nameless= (); # nameless modem call records %namelessDurations= (); # total connect times for each nameless origin %abnormalTermUsers= (); # all abnormal disconnects per user and type %abnormalTermTypes= (); # abnormal disconnect type counts by type # Process command line options # use Getopt::Long; GetOptions("d|debug" => \$debug, "r|reverse" => \$reverse, "p|primer=s" => \$primerLog, "t|top" => \$top, ); $top= 10 unless $top; # report top ten by default # Remaining arguments must be files if ($reverse) { # sort files in reverse order @files= reverse sort @ARGV; # to accommodate the likes of logrotate } else { @files= sort @ARGV; } # Process the logs and report results # PrimeModemCounts($primerLog) if $primerLog; foreach $file (@files) { # process each log file ProcessLog($file); } Report(GetDates($files[0], $files[$#files])); # Clean exit # exit; # # Subroutines # # PrimeModemCounts # Find latest modem counts from a previous log file # sub PrimeModemCounts { my($file)= shift; print "Priming initial values from $file...\n\n" if $debug; open(Input, $file) or die "Cannot read from $file: $!"; while () { # prime initial values from a previous log if (/\%CALLRECORD-3-MCOM_TERSE_CALL_REC/) { # track modem transfers my($dso, $port, $callID, $userID, $ip, $calling, $called, $standard, $protocol, $compression, $initRate, $finalRate, $snr, $transferred)= split(", "); $port= 1 + ($1-1)*24 + $2 # translate into a modem (TTY) number if $port=~ /slot\/port=(\d+)\/(\d+)/; if ($transferred=~ /(\d+)\/(\d+)/) { $received{$port}= $1; # last chars received count $transmitted{$port}= $2; # last chars transmitted count } $arapUsers{$port}= $false # clear defunct ARAP statistics if exists($arapUsers{$port}); $arapDurations{$port}= $false if exists($arapDurations{$port}); next; # grab the next line; we're done with this one } if (/\%ISDN-6-CONNECT: Interface Serial(\d+):(\d+)/) { # track ISDN user connections my($interface)= $1 * 24 + $2; # serialize my($to)= /to (.+)/; if ($to) { my($calling, $userID)= split(" ", $to); if ($calling and $userID) { # looks like a valid ISDN call $calling= "21726" . $calling if $calling < 99999; # fully qualify the phone number $isdnChannels{$calling}= $userID; # track each user by channel print "New call from $calling by $userID\n" if $debug; } } next; # grab the next line; we're done with this one } if (/\%ARAP-6-ADDRFREE: TTY (\d+).+user ([^\s]+).+; (\d+) second/) { # process an end of an ARAP call $arapUsers{$1}= $2; # remember values for the subsequent $arapDurations{$1}= $3; # modem record without a user name $arapUsers{$1}=~ # designate as an ARAP user s/;$/ (a)/ if $2 ne $noARAPUser; next; # grab the next line; we're done with this one } } close(Input); } # ProcessLog # Iterate through records and reduce them # sub ProcessLog { my($file)= shift; my(%isdnDurations)= (); # tracks active ISDN durations print "Processing $file...\n\n" if $debug; open(Input, $file) or die "Cannot read from $file: $!"; while () { # accumulate records into lists next # LINK-3-*, LINK-5-* and if /\%LINEPROTO-5-UPDOWN/ # LINEPROTO-5-* records are not or /\%LINK-5-CHANGED/ # properly paired or /\%LINK-3-UPDOWN/; # so ignore them if (/\%CALLRECORD-3-MCOM_TERSE_CALL_REC/) { # process a call record entry (end of call) my($dso, $port, $callID, $userID, $ip, $calling, $called, # $standard, $protocol, $compression, $initRate, $finalRate, $snr, $transferred, $ec, $time, $finalState, $discRadius, $discLocal, $discRemote)= split(", "); my($disconnectTime)= /^(.{15})/; my($month, $day)= $disconnectTime=~ /^(.{3})\s+(\d+)/; $month= $monthsToNumeric{$month}; $day= "0$day" if $day < 10; $disconnectTime=~ s/^.{6}/$programYear$month$day/; $userID=~ s/userid=//; # drop tags and process fields if (length($userID) > 16) { # curtail the name to fit the column $userID=~ s/^(.{13}).*$/$1\.\.\./; } $time=~ s/time=//; $discRadius=~ s/disc\(radius\)=//; $port= 1 + ($1-1)*24 + $2 # translate into a modem (TTY) number if $port=~ /slot\/port=(\d+)\/(\d+)/; push(@modemInterfaces, # create a time history of modem use ConnectTime($disconnectTime, $time) . "; +$port"); push(@modemInterfaces, "$disconnectTime; -$port"); $dso= $1 * 10 + $2 * 24 + $3 # serialize if $dso=~ /=(\d+)\/(\d+)\/(\d+)/; $ports{$dso}--; # level 5 disconnect, negate by matching 3 my($myReceived)= 0; my($myTransmitted)= 0; if ($transferred=~ /(\d+)\/(\d+)/) { if (exists($received{$port})) { $myReceived= $1 - $received{$port}; } else { print "Initial received count for modem $port\n" if $debug; } $received{$port}= $1; # last chars received count if (exists($transmitted{$port})) { $myTransmitted= $2 - $transmitted{$port}; } else { print "Initial transmitted count for modem $port\n" if $debug; } $transmitted{$port}= $2; # last chars transmitted count } else { # malformed transferred token? print "Malformed transmission counts <$transferred>\n" if $debug; } if ($userID eq "(n/a)") { # no user name if($arapUsers{$port}) { # a possible ARAP record if ($arapUsers{$port} eq $noARAPUser) { # an ARAP connect attempt failure $calling= # just keep the number s/^calling=//; $calling= "unknown" unless $calling; $calling= "ARAP $calling"; $nameless{$calling}++; # track the number of such $namelessDurations{$calling}+= $time; } else { # an ARAP session $userID= $arapUsers{$port}; # grab remembered values if (length($userID) > 16) { # curtail the name to fit the column $userID=~ s/^(.{13}).*$/$1\.\.\./; } $time= $arapDurations{$port}; $connections{$userID}++; $durations{$time}= # remember the user for each duration $userID; $userDurations{$userID}+= # track each user's total connect time $time; $userReceived{$userID}+= # track each user's total bytes received $myReceived if $myReceived > 0; $userTransmitted{$userID}+= # track each user's total bytes sent $myTransmitted if $myTransmitted > 0; } } else { # non-ARAP record with a missing user name $calling= s/^calling=//; # just keep the number $calling= "unknown" unless $calling; $calling= "PPP $calling"; $nameless{$calling}++; # track the number of such $namelessDurations{$calling}+= $time; } } else { # track all identified disconnects $userID.= " [p]"; # designate as PPP users $connections{$userID}++; $durations{$time}= # remember the user for each duration $userID; $userDurations{$userID}+= # track each user's total connect time $time; $userReceived{$userID}+= # track each user's total bytes received $myReceived if $myReceived > 0; $userTransmitted{$userID}+= # track each user's total bytes transmitted $myTransmitted if $myTransmitted > 0; if ($discRadius ne "User Request/Received Terminate") { # abnormal disconnect $abnormalTermUsers{"$userID\t$discRadius"}++; $abnormalTermTypes{$discRadius}++; } print "Stranded ARAP record on port $port!" if $arapUsers{$port} and $debug; } $arapUsers{$port}= $false; # clear remembered values $arapDurations{$port}= $false; next; # grab the next line; we're done with this one } if (/\%CALLRECORD-3-MCOM_TERSE_CALL_FAILED_REC/) { # process a failed call record my($dso, $port, $callID, $calling, $called, $time, $finalState, $discLocal, $discRemote)= split(", "); my($disconnectTime)= /^(.{15})/; my($month, $day)= $disconnectTime=~ /^(.{3})\s+(\d+)/; $month= $monthsToNumeric{$month}; $day= "0".$day if $day < 10; $disconnectTime=~ s/^.{6}/$programYear$month$day/; $time=~ s/time=//; $calling=~ s/calling=//; # drop tags and process fields $port= 1 + ($1-1)*24 + $2 # translate into a modem (TTY) number if $port=~ /slot\/port=(\d+)\/(\d+)/; $time= 10 unless $time; # allot at least 10 seconds to failed calls push(@modemInterfaces, # create a time history of modem use ConnectTime($disconnectTime, $time) . "; +$port"); push(@modemInterfaces, "$disconnectTime; -$port"); $dso= $1 * 10 + $2 * 24 + $3 # serialize if $dso=~ /=(\d+)\/(\d+)\/(\d+)/; $ports{$dso}--; # level 5 disconnect, negate by matching 3 $modemProblems{$port}++; # remember the culprit modem $callerProblems{$calling}++; # remember the affected caller next; # grab the next line; we're done with this one } if (/\%ARAP-6-ADDRFREE: TTY (\d+).+user ([^\s]+).+; (\d+) second/) { # process an end of an ARAP call $arapUsers{$1}= $2; # remember values for the subsequent $arapDurations{$1}= $3; # modem record without a user name $arapUsers{$1}=~ # designate as an ARAP user s/;$/ (a)/ if $2 ne $noARAPUser; next; # grab the next line; we're done with this one } next # nothing useful within start of ARAP records if /\%ARAP-6-ADDRUSED/; if (/\%ISDN-6-DISCONNECT: Interface Serial(\d+):(\d+)/) { # process an end of an ISDN call my($interface)= $1 * 24 + $2; # serialize my($time)= /call lasted (\d+) second/; my($disconnectTime)= /^(.{15})/; my($month, $day)= $disconnectTime=~ /^(.{3})\s+(\d+)/; $month= $monthsToNumeric{$month}; $day= "0".$day if $day < 10; $disconnectTime=~ s/^.{6}/$programYear$month$day/; push(@isdnInterfaces, # create a time history of interface use ConnectTime($disconnectTime, $time) . "; +$interface"); push(@isdnInterfaces, "$disconnectTime; -$interface"); my($from)= /from ([^,]+)/; if ($from) { my($calling, $userID)= split(" ", $from); if ($userID) { if (length($userID) > 16) { # curtail the name to fit the column $userID=~ s/^(.{13}).*$/$1\.\.\./; } if ((exists($isdnChannels{$calling+1}) and $isdnChannels{$calling+1} eq $userID) or (exists($isdnChannels{$calling-1}) and $isdnChannels{$calling-1} eq $userID)) { # first channel of a dual-channel call print "First channel of a dual-channel call: ", "$userID via $calling\n" if $debug; $isdnDurations{$userID}= $time; $isdnChannels{$calling}= $false; } else { if ($isdnDurations{$userID}) { # second channel of a dual-channel call $time= $isdnDurations{$userID} if $time < $isdnDurations{$userID}; $isdnDurations{$userID}= $false; print "Second channel disconnecting of a dual-channel call:\n", "\t $userID via $calling lasting $time\n\n" if $debug; $userID.= " {2}"; # caller's user ID tagged for a 2-ch ISDN call } else { # single channel call print "Single channel disconnect from $calling by $userID ", "lasting $time\n" if $debug; $userID.= " {1}"; # caller's user ID tagged for an ISDN call } $connections{$userID}++; $durations{$time}= # remember the user for each duration $userID; $userDurations{$userID}+= # track each user's total connect time $time; } } else { # track unknown ISDN records separately $ports{$interface}++; # level 3 disconnect, negate by matching 5 } } next; # grab the next line; we're done with this one } if (/\%ISDN-6-CONNECT: Interface Serial(\d+):(\d+)/) { # track ISDN user connections my($interface)= $1 * 24 + $2; # serialize my($to)= /to (.+)/; if ($to) { my($calling, $userID)= split(" ", $to); if ($calling and $userID) { # looks like a valid ISDN call $calling= "21726" . $calling if $calling < 99999; # fully qualify the phone number $isdnChannels{$calling}= $userID; # track each user by channel print "New call from $calling by $userID\n" if $debug; } } next; # grab the next line; we're done with this one } if (/^(.{15,15}).+\%SYS-5-CONFIG_I:(.+)$/) { # process a configuration change push(@configured, "$1$2\n"); next; # grab the next line; we're done with this one } # Skip some uninteresting and rare records next if /-Traceback=/; next if /\%AT-6-NODEWRONG/; next if /\%ARAP-6-MNP4T401/; next if /\%ARAP-6-RESENDSLOW/; next if /\%ARAP-6-RCVGIANT/; next if /\%ARAP-6-MAXRESENDS/; next if /\%SYS-3-CPUHOG/; next if /\%AAAA-3-INVSTATE/; if (/%([^:]+):/) { # track unmatched records $unmatched{$1}++; } else { # also preserve irregular lines /^\s*(.*)\s*$/; # chop white spaces at terminals $unmatched{$1}= 0; $unmatched{$unexpected}++; } } close(Input); } # Report # Generate our report # sub Report { my($dateRange)= shift; # passed log date bounds my($index); # a loop counter my($value); # a given report value my($name); # a user's account or name my(@ranks); # an array of ranks for a given category my($stranded)= ""; # a report of stranded ports # Generate the title print "\nDial-up report: $dateRange\n"; print "=======================================================\n"; # Compute starting date and time my($month, $day)= $dateRange=~ /^(.{3})\s+(\d+)/; $month= $monthsToNumeric{$month}; $day= "0".$day if $day < 10; $dateRange=~ s/^.{6}/$programYear$month$day/; # Report tabulated summaries print "\n Peak Active ISDN Channels Each Hour\n\n", Tabulate($dateRange, sort(@isdnInterfaces)), "\n\n\n Peak Active Modems Each Hour\n\n", Tabulate($dateRange, sort(@modemInterfaces)), "\n\n"; # Failed calls and other problems print "\nStranded ports\n--------------\n"; foreach (sort { $a <=> $b; } (keys(%ports))) { next unless $ports{$_}; # skip balanced ports $stranded.= sprintf " Port %2d, state %2d\n", $_, $ports{$_}; } $stranded= "None!\n" unless $stranded; print $stranded; if (each(%modemProblems)) { # make sure we have modem problems print "\nFailed calls by modem\n---------------------\n"; @ranks= sort {$modemProblems{$a}<=>$modemProblems{$b};} (keys(%modemProblems)); while ($value= pop(@ranks)) { # list modems by descending problem counts printf " modem %2d: %3d failed call%s\n", $value, $modemProblems{$value}, $modemProblems{$value} == 1 ? "" : "s"; } } if (each(%callerProblems)) { # make sure we have caller problems print "\nFailed calls by caller\n----------------------\n"; @ranks= sort {$callerProblems{$a}<=>$callerProblems{$b};} (keys(%callerProblems)); while ($value= pop(@ranks)) { # list callers by descending problem counts if ($value eq "(n/a)") { printf " unknown: %3d failed call%s\n", $callerProblems{$value}, $callerProblems{$value} == 1 ? "" : "s"; } else { printf "%13s: %3d failed call%s\n", $value, $callerProblems{$value}, $callerProblems{$value} == 1 ? "" : "s"; } } } if (each(%nameless)) { # make sure we have nameless calls print "\nNameless call records\n---------------------\n"; foreach (sort {$nameless{$a}<=>$nameless{$b};} (keys(%nameless))) { # report all nameless calls if ($namelessDurations{$_}) { # we know the duration printf "%20s: %3d call%s averaging %s (total: %6ld s)\n", $_, $nameless{$_}, $nameless{$_} == 1 ? " " : "s", VerboseTime($namelessDurations{$_} / $nameless{$_} + .49), $namelessDurations{$_}; } else { # unknown duration printf "%20s: %3d call%s of unknown duration\n", $_, $nameless{$_}, $nameless{$_} == 1 ? " " : "s"; } } } if (each(%abnormalTermTypes)) { # make sure we have abnormal terminations print "\nAbnormal terminations by type\n-----------------------------\n"; foreach (sort {$abnormalTermTypes{$b}<=>$abnormalTermTypes{$a};} (keys(%abnormalTermTypes))) { # report all abnormal terminations printf "%9d %s\n", $abnormalTermTypes{$_}, $_; } } if (each(%abnormalTermUsers)) { # make sure we know abnormal termination users print "\nMost abnormal terminations by a user", "\n------------------------------------\n"; @ranks= sort {$abnormalTermUsers{$a}<=>$abnormalTermUsers{$b};} (keys(%abnormalTermUsers)); for ($index= 0; $index < $top; $index++) { # report all abnormally terminated calls $value= pop(@ranks); last unless $value; # make sure we did not run out of data my($userID, $type)= split("\t", $value); printf "%20s: %3d %s\n", $userID, $abnormalTermUsers{$value}, $type; } } # Top usage digests if (each(%userTransmitted)) { # make sure we know some user statistics print "\nMost data received by a user\n----------------------------\n"; @ranks= sort {$userTransmitted{$a}<=>$userTransmitted{$b};} (keys(%userTransmitted)); for ($index= 0; $index < $top; $index++) { # report top data transfers $value= pop(@ranks); last unless $value; # make sure we did not run out of data printf "%20s: %s\n", $value, DataSize($userTransmitted{$value}, $userDurations{$value}); } } if (each(%userReceived)) { # make sure we know some user statistics print "\nMost data sent by a user\n------------------------\n"; @ranks= sort {$userReceived{$a}<=>$userReceived{$b};} (keys(%userReceived)); for ($index= 0; $index < $top; $index++) { # report top data transfers $value= pop(@ranks); last unless $value; # make sure we did not run out of data printf "%20s: %s\n", $value, DataSize($userReceived{$value}, $userDurations{$value}); } } if (each(%durations)) { # make sure we know some user statistics print "\nLongest calls\n-------------\n"; @ranks= sort { $a <=> $b; } (keys(%durations)); for ($index= 0; $index < $top; $index++) { # report top longest calls $value= pop(@ranks); last unless $value; # make sure we did not run out of data printf "%20s: %s\n", $durations{$value}, VerboseTime($value); } } # All usage digests if (each(%connections)) { # make sure we know some user statistics print "\nSuccessfull callers ranked by frequency", "\n---------------------------------------\n"; foreach (sort {$connections{$b}<=>$connections{$a}} (keys(%connections))) { # report callers ranked by frequency printf "%20s: %5d connection%s lasting %s (%9ld s)\n", $_, $connections{$_}, $connections{$_} == 1 ? " " : "s", ApproximateTime($userDurations{$_}), $userDurations{$_}; } } if (@configured) { # make sure we have configuration entries print "\nConfiguration changes\n---------------------\n"; foreach (@configured) { # report all configuration events print; } } if (each(%unmatched)) { # make sure we know some user statistics print "\nUnexpected records and lines\n----------------------------\n"; foreach (sort {$unmatched{$b}<=>$unmatched{$a}} (keys(%unmatched))) { # report callers ranked by frequency if ($unmatched{$_} == 0) { # unexpected lines print "\n$_" if $debug; } else { # unexpected records printf "%6d %s line%s\n", $unmatched{$_}, $_, $unmatched{$_}==1 ? "" : "s"; } } } } # GetDates # Learn first and last log dates # sub GetDates { my($firstLog)= shift; my($lastLog)= shift; my($firstDate, $lastDate); open(Input, $firstLog) or die "Cannot read from $firstLog: $!"; $firstDate= ; # learn first log's first line's date close(Input); open(Input, $lastLog) or die "Cannot read from $lastLog: $!"; if (seek(Input, -1000, 2)) { # jump to near the end of the file while () { # run through the last few lines $lastDate= $_; # and save the very last line } } else { $lastDate= ""; } close(Input); return (sprintf("%15.15s through %15.15s", $firstDate, $lastDate)); } # DataSize # Return bytes converted to the best scale of bytes, KB, MB, or GB # sub DataSize { my($size)= shift; # data size my($time)= shift; # time connected, optional return "inconceivable! ($size)" if $size < 1; return "nothing ($size)" if $size == 0; return "one byte ($size)" if $size == 1; my($rate); # rate in kilobits per second if ($time) { # calculate rate $rate= sprintf(", %5.2f Kbps)", ($size * 8) / ($time * 1024)); } else { # not enough info for rate calculations $rate= ")"; } return "$size bytes ($size chars$rate" if $size < 1000; return sprintf("%6.2f kilobytes (%9ld chars$rate", $size / 1024, $size) if $size < 1000 * 1024; return sprintf("%6.2f megabytes (%9ld chars$rate", $size / (1024 * 1024), $size) if $size < 1000 * 1024 * 1024; return sprintf("%6.2f gigabytes ($size chars$rate", $size / (1024 * 1024 * 1024)); } # VerboseTime # Returns seconds converted to a verbose time phrase # sub VerboseTime { use integer; # rely on integer arithmetic only my($time)= shift; # get the total time in seconds my($phrase)= ""; # our time phrase my($span); # a given time interval span return "inconceivable! ($time)" if $time < 0; return "no time" unless $time; if ($span= $time % 60) { # insert second count $phrase= sprintf("%2d second%s", $span, $span>1 ? "s" : ""); } return $phrase # check if there is any time left unless $time/= 60; if ($span= $time % 60) { # insert minute count $phrase= sprintf("%2d minute%s%s", $span, $span>1 ? "s" : "", $phrase ? ($span>1 ? ", " : ", ") . $phrase : ""); } return $phrase # check if there is any time left unless $time/= 60; if ($span= $time % 24) { # insert hour count $phrase= sprintf("%2d hour%s%s", $span, $span>1 ? "s" : "", $phrase ? ($span>1 ? ", " : ", ") . $phrase : ""); } return $phrase # check if there is any time left unless $time/= 24; if ($span= $time % 7) { # insert day count $phrase= sprintf("%2d day%s%s", $span, $span>1 ? "s" : "", $phrase ? ($span>1 ? ", " : ", ") . $phrase : ""); } return $phrase # check if there is any time left unless $time/= 7; if ($span= $time % 52) { # insert week count $phrase= sprintf("%2d week%s%s", $span, $span>1 ? "s" : "", $phrase ? ($span>1 ? ", " : ", ") . $phrase : ""); } return $phrase # check if there is any time left unless $time/= 52; # insert year count $phrase= sprintf("%2d year%s%s", $span, $span>1 ? "s" : "", $phrase ? ($span>1 ? ", " : ", ") . $phrase : ""); return $phrase; } # ApproximateTime # Returns time described as an approximation to the largest unit # sub ApproximateTime { my($time)= shift; # get the total time in seconds return "inconceivable! " if $time < 0; return "no time at all " unless $time; return sprintf(" %2d second%s ", $time, $time>1 ? "s" : " ") if ($time < 45); return "about 1 minute " # less than a minute and a half if ($time < 91); return sprintf("about %2.0f minutes ", $time/60) if ($time < 3600); return "about an hour " # less than an hour and a half if ($time < 5401); return sprintf("about %2.0f hours ", $time/3600) if ($time < 86400); return "about 1 day " # less than a day and a half if ($time < 129601); return sprintf("about %2.0f days ", $time/86400) if ($time < 604800); return "about 1 week " # less than a week and a half if ($time < 907201); return sprintf("about %2.0f weeks ", $time/604800) if ($time < 31449600); return "about 1 year " # less than a year and a half if ($time < 47174400); return sprintf("about %2.0f years ", $time/31449600); } # ConnectTime # Compute a connection time stamp from a disconnect stamp and elapsed time # Ignore leap days -- not enough information to treat properly # sub ConnectTime { use integer; my($disconnectTime)= shift; my($span)= shift; my($year, $month, $day, $hour, $minute, $second)= $disconnectTime=~ /^(\d\d)(\d\d)(\d\d)\s+(\d+):(\d+):(\d+)/; my($disconnectSeconds)= ($monthsToDays[$month-1]+$day-1) * 86400 + $hour * 3600 + $minute * 60 + $second; my($connectSeconds)= $disconnectSeconds - $span; while ($connectSeconds < 0) { # correct for year boundaries $connectSeconds+= 365 * 86400; $year--; } foreach (reverse sort { $a <=> $b; } keys(%secondsToMonths)) { if ($connectSeconds >= $_) { # matched a month print "$connectSeconds exceeds $_\n" if $debug; $month= $secondsToMonths{$_}; $connectSeconds-= $_; last; } } $day= $connectSeconds / 86400 + 1; $day= "0" . $day if $day < 10; $connectSeconds%= 86400; $hour= $connectSeconds / 3600; $hour= "0" . $hour if $hour < 10; $connectSeconds%= 3600; $minute= $connectSeconds / 60; $minute= "0" . $minute if $minute < 10; $second= $connectSeconds % 60; $second= "0" . $second if $second < 10; print "$year$month$day $hour:$minute:$second to $disconnectTime ($span)\n" if $debug; return "$year$month$day $hour:$minute:$second"; } # Tabulate # Summarize a time history in a table # sub Tabulate { my($firstDate)= shift; # starting date and time my($firstHour); my($event); # each event of the time history my($current)= 0; # track activity my($max)= 0; my($min)= 99; my($date); my($lastDate); my($hour); my($lastHour); my(@maxima)= (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0); my(@minima)= (99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99); my(@means)= (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0); my(@counts)= (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0); my($table); # formatted table and ingredients my($topRow)= 0; # the top row in our table my($header)= " < hr 0 1 2 3 4 5 6 7 8 9 10 11" . " 12 13 14 15 16 17 18 19 20 21 22 23"; my($separator)= ":-------------------------------------" . "------------------------------------"; ($firstDate, $firstHour)= $firstDate=~ /^(\d{6}) (\d\d)/; $lastDate= $firstDate; $lastHour= $firstHour; $table= $header . "\n date " . $separator; $lastDate=~ /\d\d(\d\d)(\d\d)/; $table.= "\n$1\/$2 : "; # start the first row for ($hour= 0; $hour < $lastHour; $hour++) { # advance to the first data cell $table.= " "; } while($event= shift) { # iterate through the history ($date, $hour)= $event=~ /^(\d{6}) (\d\d)/; if ($date < $firstDate) { # pin to the start of the first log $date= $firstDate; $hour= $firstHour; $track= $false; } elsif ($date == $firstDate and $hour < $firstHour) { $hour= $firstHour; $track= $false; } else { # only track statistics $track= $true; # within the current period } while ($lastDate < $date) { # make a new row while ($lastHour < 24) { # first, fill the rest of the row $table.= $max ? sprintf("%3d", $max) : " "; $minima[$lastHour]= $min if $track and ($min < $minima[$lastHour]); $maxima[$lastHour]= $max if $track and ($max > $maxima[$lastHour]); $means[$lastHour]+= $current if $track; $counts[$lastHour]++ if $track; $lastHour++; $max= $min= $current; # reset extrema } my($year, $month, $day)= $lastDate=~ /^(\d\d)(\d\d)(\d\d)/; $lastHour= 0; # reset hour to beginning of the next day $day++; # advance to the next day if ($day > $daysEachMonth{$month}) { # wrap to the next month $date=~ /^\d\d(\d\d)(\d\d)/; unless ($month == 2 and $day == 29 and $1 == 2 and $2 == 29) { # February is special due to a leap day $month++; if ($month < 10) { # pad single digit months $month= "0$month" if $month < 10; } elsif ($month > 12) { # end of the year $month= "01"; $year++; } $day= "01"; } } $lastDate= "$year$month$day"; $table.= "\n$month\/$day : "; # reset to start of next row } while ($lastHour < $hour) { # catch up to the current hour $table.= $max ? sprintf("%3d", $max) : " "; $minima[$lastHour]= $min if $track and ($min < $minima[$lastHour]); $maxima[$lastHour]= $max if $track and ($max > $maxima[$lastHour]); $means[$lastHour]+= $current if $track; $counts[$lastHour]++ if $track; $lastHour++; $max= $min= $current; # reset extrema } # Track interface counts $event=~ /\+/ ? $current++ : $current--; if ($current < 0) { # should NEVER happen print "Bogus interface count: $event ($track)\n"; $current= 0; } $max= $current if $current > $max; $min= $current if $current < $min; print "Zero count: $event ($track)\n" if $debug and ($min == 0); $means[$hour]+= $current if $track; $counts[$hour]++ if $track; $minima[$hour]= $min if $track and ($min < $minima[$hour]); $maxima[$hour]= $max if $track and ($max > $maxima[$hour]); } $table.= sprintf("%3d", $max); # update for the last hour # Tabulate activity counts per hour for the period $table.= "\n " . $separator . "\n" . $header . "\n \# " . $separator; foreach (@maxima) { # find the overall maximum $topRow= $_ if $_ > $topRow; } do # construct our table { # start a new row $table.= sprintf("\n %2d : ", $topRow); for ($hour= 0; $hour < 24; $hour++) { # construct a cell my($tick)= " "; $tick= " - " if $minima[$hour] == $topRow; $tick= " + " if $maxima[$hour] == $topRow; $tick= " | " if $maxima[$hour] > $topRow and $topRow > $minima[$hour]; $tick= " * " if ($counts[$hour]) and (int($means[$hour]/$counts[$hour]+0.5) == $topRow); $table.= $tick; } } while ($topRow--); return $table . "\n " . $separator; }