ljr/livejournal/bin/ljblockwatcher.pl

632 lines
17 KiB
Perl
Executable File

#!/usr/bin/perl
##############################################################################
=head1 NAME
dbreportd - Report database latencies at regular intervals.
=head1 SYNOPSIS
$ dbreportd OPTIONS
=head1 OPTIONS
=over 4
=item -c, --clearscreen
Clear the screen and home the cursor before printing each report, like top. May
not work on all terminals.
=item -d, --debug
Turn on debugging information. May be specified more than once for (potentially)
increased levels of debugging.
=item -h, --help
Output a help message and exit.
=item -i, --interval=SECONDS
Set the number of seconds between reports to SECONDS. Defaults to 3 second
intervals.
=item -p, --port=PORT
Set the port to listen on for reports. This is set in ljconfig.pl, but can be
overridden here.
=item -V, --version
Output version information and exit.
=back
=head1 REQUIRES
I<Token requires line>
=head1 DESCRIPTION
None yet.
=head1 AUTHOR
Michael Granger <ged@FaerieMUD.org>
Copyright (c) 2004 Danga Interactive. All rights reserved.
This program is Open Source software. You may use, modify, and/or redistribute
this software under the terms of the Perl Artistic License. (See
http://language.perl.com/misc/Artistic.html)
THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND
FITNESS FOR A PARTICULAR PURPOSE.
=cut
# :TODO: Change param order in received msgs
##############################################################################
package dbreportd;
use strict;
use warnings qw{all};
###############################################################################
### I N I T I A L I Z A T I O N
###############################################################################
BEGIN {
# Turn STDOUT buffering off
$| = 1;
# Versioning stuff and custom includes
use vars qw{$VERSION $RCSID};
$VERSION = do { my @r = (q$Revision: 1.3 $ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r };
$RCSID = q$Id: ljblockwatcher.pl,v 1.3 2004/03/09 22:25:05 deveiant Exp $;
# Define some constants
use constant TRUE => 1;
use constant FALSE => 0;
# How many time samples to keep around to determine average latency
use constant SAMPLE_DEPTH => 10;
# How many samples to show in the "top <n> slow queries"
use constant TOP_QUERY_SIZE => 5;
# ANSI vt100 escape codes for various things
use constant VT100_CLEARSCREEN => "\e[2J";
use constant VT100_HOME => "\e[0;0H";
# Modules
use Getopt::Long qw{GetOptions};
use Pod::Usage qw{pod2usage};
use IO::Socket::INET qw{};
use IO::Select qw{};
use Time::HiRes qw{usleep};
use Data::Dumper qw{};
# Turn on option bundling (-vid)
Getopt::Long::Configure( "bundling" );
}
our $Debug = FALSE;
### Main body
MAIN: {
my (
$helpFlag, # User requested help?
$versionFlag, # User requested version info?
$interval, # Interval between generated reports
$port, # Port number to listen on
$msg, # The message buffer for reports
$sock, # UDP socket
$selector, # IO::Select object
$lastReport, # time() of last report output
$host, # Message host
$time, # Message time
$notes, # Message notes
$type, # Operation type (currently unused)
%buffers, # SampleBuffers keyed by host
$clearscreenFlag, # Clear the screen before every report?
);
# Print the program header and read in command line options
GetOptions(
'd|debug+' => \$Debug,
'h|help' => \$helpFlag,
'i|interval=i' => \$interval,
'V|version' => \$versionFlag,
'p|port=i' => \$port,
'c|clearscreen' => \$clearscreenFlag,
) or abortWithUsage();
# If the -h flag or -V flag was given, just show the help or version,
# respectively, and exit.
helpMode() and exit if $helpFlag;
versionMode() and exit if $versionFlag;
# Either get the port from the command line or a default
$port ||= 4774;
# Set the interval to a default if it wasn't specified
$interval = 3 if !defined $interval;
# Open a receiving UDP socket
print VT100_CLEARSCREEN, VT100_HOME if $clearscreenFlag;
print "Setting up listener on port $port\n";
$sock = new IO::Socket::INET (
Proto => 'udp',
LocalPort => $port
) or die "Failed to open receiving socket: $!";
$selector = new IO::Select;
$selector->add( $sock );
$lastReport = time();
%buffers = ();
# Print reports every couple of seconds
while ( 1 ) {
if ( $selector->can_read($interval) ) {
# Get the message and split it back into four parts
my $addr = $sock->recv( $msg, 1024, 0 );
print ">>> Message: $msg\n" if $Debug;
( $host, $type, $time, $notes ) = split( /\x3/, $msg, 4 );
# Add the time and notes to the table of hosts
$buffers{ $host } ||= new SampleBuffer ( $host, depth => SAMPLE_DEPTH );
$buffers{ $host }->add( $type, $time, $notes );
} else {
sleep 0.5;
}
} continue {
if ( (time() - $lastReport) > $interval ) {
print VT100_CLEARSCREEN, VT100_HOME if $clearscreenFlag;
print_report( values %buffers );
$lastReport = time();
}
};
}
### FUNCTION: print_report( @buffers )
### Given a list of SampleBuffer objects, print a table with the ones with the
### highest average times.
sub print_report {
my @buffers = @_;
my (
$row, # Row count for display
@top, # Top 5 slowest average buffers
%top, # ^-- Hash of same
@sbuffers, # Buffer objects sorted by hostname
@wbuffers, # Buffer objects sorted by worst op
$fmt, # printf format for report rows
$prefix, # Line prefixes
);
if ( @buffers ) {
# Pick the 5 slowest operations
@top =
map { $_->[0] }
sort { $b->[1] <=> $a->[1] }
map { [$_->host, $_->average_time] }
@buffers;
$row = 0;
%top = ();
foreach my $host ( @top[0 ... TOP_QUERY_SIZE] ) {
last unless defined $host;
$top{$host} = ++$row unless exists $top{$host};
}
#print Data::Dumper->Dumpxs( [\%top], ['top'] ), "\n";
# Make an array of sorted buffer objects by worst average time
@sbuffers =
map { $_->[0] }
sort { $b->[1] <=> $a->[1] }
map { [$_, $_->average_time] }
@buffers;
# Make an array of sorted buffer objects by worst time
@wbuffers =
map { $_->[0] }
sort { $b->[1] <=> $a->[1] }
map { [$_, $_->worst_time] }
@buffers;
# Output all hosts with the average worst operation times
$fmt = "%-2s%25s %0.5fs";
$row = 0;
header( "Average longest blocking operations, by host" );
foreach my $buf (@sbuffers) {
$row++;
if ( exists $top{$buf->host} && $top{$buf->host} <= 3 ) {
$prefix = '+';
} else {
$prefix = ' ';
}
printf "$fmt\n", $prefix, $buf->host, $buf->average_time;
}
print "\n";
# Output the worst operations with their notes
$row = 0;
$fmt = "%0.5fs: '%s' [%s/%s]\n";
header( "%d worst blockers", TOP_QUERY_SIZE );
foreach my $buf (@wbuffers[0 ... TOP_QUERY_SIZE]) {
last unless defined $buf;
$row++;
my $sample = $buf->worst_sample;
printf( $fmt,
$sample->time,
$sample->notes || "(none)",
$sample->type,
$buf->host );
}
print "\n";
# Print the raw buffer objects if debugging
if ( $Debug ) {
header( "Raw buffers" );
foreach my $buf ( @buffers ) {
local $Data::Dumper::Indent = 0;
local $Data::Dumper::Terse = TRUE;
print Data::Dumper->Dumpxs( [$buf], ['buf'] ), "\n";
}
}
print "\n";
}
else {
print "No hosts reporting.\n";
}
}
### FUNCTION: header( $fmt, @args )
### Printf the given message as a header.
sub header {
my ( $fmt, @args ) = @_;
my $msg = sprintf( $fmt, @args );
chomp( $msg );
print "$msg\n", '-' x 75, "\n";
}
### FUNCTION: helpMode()
### Exit normally after printing the usage message
sub helpMode {
pod2usage( -verbose => 1, -exitval => 0 );
}
### FUNCTION: versionMode()
### Exit normally after printing version information
sub versionMode {
print STDERR "dbreportd $VERSION\n";
exit;
}
### FUNCTION: abortWithUsage()
### Abort the program showing usage message.
sub abortWithUsage {
if ( @_ ) {
pod2usage( -verbose => 1, -exitval => 1, -msg => join('', @_) );
} else {
pod2usage( -verbose => 1, -exitval => 1 );
}
}
### FUNCTION: abort( @messages )
### Print the specified messages to the terminal and exit with a non-zero status.
sub abort {
my $msg = @_ ? join '', @_ : "unknown error";
print STDERR $msg, "\n";
exit 1;
}
#####################################################################
### T I M E S A M P L E C L A S S
#####################################################################
package Sample;
use strict;
BEGIN {
use vars qw{$AUTOLOAD};
use Carp qw{croak confess};
use Data::Dumper ();
$Data::Dumper::Terse = 1;
$Data::Dumper::Indent = 1;
}
### METHOD: new( $host )
### Create a new sample buffer for the given host
sub new {
my $proto = shift;
my $class = ref $proto || $proto;
my $self = bless {
type => 'db',
time => 0.0,
notes => '',
}, $class;
if ( @_ && (@_ % 2 == 0) ) {
my %args = @_;
foreach my $meth ( keys %args ) {
$self->$meth( $args{$meth} );
}
}
return $self;
}
### FUNCTION: blessed( $var )
### Returns a true value if the given value is a blessed reference.
sub blessed {
my $arg = shift;
return ref $arg && UNIVERSAL::isa( $arg, 'UNIVERSAL' );
}
### (PROXY) METHOD: AUTOLOAD( @args )
### Proxy method to build (non-translucent) object accessors.
sub AUTOLOAD {
my $self = shift;
confess "Cannot be called as a function" unless $self && blessed $self;
( my $name = $AUTOLOAD ) =~ s{.*::}{};
### Build an accessor for extant attributes
if ( exists $self->{$name} ) {
### Define an accessor for this attribute
my $method = sub {
my $closureSelf = shift or confess "Cannot be called as a function";
$closureSelf->{$name} = shift if @_;
return $closureSelf->{$name};
};
### Install the new method in the symbol table
NO_STRICT_REFS: {
no strict 'refs';
*{$AUTOLOAD} = $method;
}
### Now jump to the new method after sticking the self-ref back onto the
### stack
unshift @_, $self;
goto &$AUTOLOAD;
}
### Try to delegate to our parent's version of the method
my $parentMethod = "SUPER::$name";
return $self->$parentMethod( @_ );
}
sub DESTROY {}
sub END {}
#####################################################################
### S A M P L E B U F F E R C L A S S
#####################################################################
### Class for tracking latencies for a given host
package SampleBuffer;
use strict;
BEGIN {
use Carp qw{croak confess};
use vars qw{$AUTOLOAD};
}
### METHOD: new( $host )
### Create a new sample buffer for the given host
sub new {
my $proto = shift;
my $class = ref $proto || $proto;
my $host = shift or die "No hostname given";
my $self = bless {
host => $host,
samples => {},
depth => 10,
}, $class;
if ( @_ && (@_ % 2 == 0) ) {
my %args = @_;
foreach my $meth ( keys %args ) {
$self->$meth( $args{$meth} );
}
}
return $self;
}
### METHOD: add( $type, $time, $notes )
### Add the specified I<time> to the sample buffer for the given I<type> with
### the given I<notes>.
sub add {
my $self = shift or confess "Cannot be called as a function";
my ( $type, $time, $notes ) = @_;
$self->{samples}{ $type } ||= [];
my $slist = $self->{samples}{ $type };
my $sample = new Sample ( type => $type, time => $time, notes => $notes );
unshift @$slist, $sample;
pop @$slist if @$slist > $self->{depth};
return scalar @$slist;
}
### METHOD: samples( [$type] )
### Fetch a list of the samples of the given I<type> in the buffer, or a list of
### all samples if I<type> is not specified.
sub samples {
my $self = shift or confess "Cannot be used as a function";
my $type = shift;
my @samples = ();
# Gather the samples that are going to be used to make the average, either
# the specific kind requested, or all of 'em.
if ( $type ) {
# Regexp filter
if ( ref $type eq 'Regexp' ) {
foreach my $key ( keys %{$self->{samples}} ) {
push @samples, @{$self->{samples}{ $key }}
if $type =~ $key;
}
}
# Any other filter just gets string-equalled.
else {
@samples = @{$self->{samples}{ $type }};
}
} else {
foreach my $type ( keys %{$self->{samples}} ) {
push @samples, @{$self->{samples}{ $type }};
}
}
return @samples;
}
### METHOD: average_time( [$type] )
### Return the average of all the times currently in the buffer for the given
### I<type>. If I<type> isn't given, returns the overall average.
sub average_time {
my $self = shift or confess "Cannot be used as a function";
my $type = shift;
my ( $time, $count ) = ( 0, 0 );
# Now add and count all the time from each target sample
foreach my $sample ( $self->samples($type) ) {
$time += $sample->time;
$count++;
}
return $time / $count;
}
### METHOD: worst_sample( [$type] )
### Return the worst sample in the buffer for the given I<type>. If no type is
### given, return the worst overall sample.
sub worst_sample {
my $self = shift or confess "Cannot be used as a function";
my $type = shift;
return () unless %{$self->{samples}};
my @samples =
map { $_->[0] }
sort { $a->[1] <=> $b->[1] }
map { [$_, $_->time] } $self->samples( $type );
return $samples[-1];
}
### METHOD: worst_time( [$type] )
### Return the worst time in the buffer for the given I<type>. If no I<type> is
### given, returns the worst overall time.
sub worst_time {
my $self = shift or confess "Cannot be used as a function";
my $type = shift;
my $samp = $self->worst_sample( $type ) or return ();
return $samp->time;
}
### METHOD: worst_notes( [$type] )
### Return the notes from the worst sample in the buffer for the given
### I<type>. If I<type> is not specified, returns the notes for the worst
### overall sample.
sub worst_notes {
my $self = shift or confess "Cannot be used as a function";
my $type = shift;
my $samp = $self->worst_sample( $type ) or return ();
return $samp->notes;
}
### FUNCTION: blessed( $var )
### Returns a true value if the given value is a blessed reference.
sub blessed {
my $arg = shift;
return ref $arg && UNIVERSAL::isa( $arg, 'UNIVERSAL' );
}
### (PROXY) METHOD: AUTOLOAD( @args )
### Proxy method to build (non-translucent) object accessors.
sub AUTOLOAD {
my $self = shift;
confess "Cannot be called as a function" unless $self && blessed $self;
( my $name = $AUTOLOAD ) =~ s{.*::}{};
### Build an accessor for extant attributes
if ( exists $self->{$name} ) {
### Define an accessor for this attribute
my $method = sub {
my $closureSelf = shift or confess "Cannot be called as a function";
$closureSelf->{$name} = shift if @_;
return $closureSelf->{$name};
};
### Install the new method in the symbol table
NO_STRICT_REFS: {
no strict 'refs';
*{$AUTOLOAD} = $method;
}
### Now jump to the new method after sticking the self-ref back onto the
### stack
unshift @_, $self;
goto &$AUTOLOAD;
}
### Try to delegate to our parent's version of the method
my $parentMethod = "SUPER::$name";
return $self->$parentMethod( @_ );
}
sub DESTROY {}
sub END {}