#!/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 =head1 DESCRIPTION None yet. =head1 AUTHOR Michael Granger 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 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