#!/usr/bin/perl
use strict;
use warnings;
use Date::Parse;
use Getopt::Long;
use IO::Socket::INET;
use List::Util 'max';
use LWP::Simple;
use POSIX 'strftime';
use SpamSvc::DisplayAligned;
GetOptions(
'mc!' => \(my $memcache),
'pretty!' => \(my $pretty = 1),
'html!' => \(my $html = 0),
);
my @hosts = @ARGV;
push @hosts, 'localhost:23459' unless @hosts;
my @order = qw/
host
version revision built age
date time uptime
period events throughput
min_elapsed median_elapsed mean_elapsed max_elapsed
errors fatals success_rate since_fatal
sys_uptime sys_load1 sys_load5 sys_load15 free_physical free_swap
/;
# ident
my %html_sections = (
version => 'Release',
date => 'Ping',
period => 'Recent',
min_elapsed => 'Performance',
errors => 'Errors',
sys_uptime => 'Health',
);
if ($memcache) {
push @order, qw/ mc_uptime mc_hits mc_misses mc_full mc_gets mc_sets /;
$html_sections{mc_uptime} = 'Memcached';
}
my %units = (
age => 'interval',
uptime => 'interval',
errors => 'count',
fatals => 'count',
events => 'count',
period => 'interval',
since_fatal => 'interval',
throughput => '/s',
success_rate => 'percent',
min_elapsed => 'ms',
median_elapsed => 'ms',
mean_elapsed => 'ms',
max_elapsed => 'ms',
sys_uptime => 'interval',
sys_load1 => '%.2f',
sys_load5 => '%.2f',
sys_load15 => '%.2f',
);
my %stats;
for my $host (@hosts) {
my $s = $stats{$host} ||= {};
$s->{host} = $host;
$s->{host} =~ s,\..*,,;
my $url = "http://$host/status";
my $page = get $url;
next unless $page;
my %host_stats = $page =~ /(\S+)=(.+)$/mg;
$s->{$_} = $host_stats{$_} for @order;
$s->{host} = $host;
$s->{host} =~ s,\..*,,;
$s->{date} = strftime '%F', localtime int $host_stats{now};
$s->{time} = strftime '%r', localtime int $host_stats{now};
$s->{time} =~ s/^0/ /;
if (defined $s->{revision} && $s->{revision} =~ /^\$Revision: (\d+)/) {
$s->{revision} = "r$1";
}
if (defined $s->{built} && $s->{built} =~ /\$Date: ([\d-]+ [\d:]+)/) {
my $built = $1;
$s->{built} = $built;
$s->{age} = time - str2time($built);
}
if (defined $host_stats{freemem} && defined $host_stats{totalmem}) {
$s->{free_physical} = memratio($host_stats{freemem}, $host_stats{totalmem});
}
if (defined $host_stats{freeswap} && defined $host_stats{totalswap}) {
$s->{free_swap} = memratio($host_stats{freeswap}, $host_stats{totalswap});
}
for my $stat (keys %units) {
if (!defined $s->{$stat}) {
$s->{$stat} = 'n/a';
}
elsif ($units{$stat} eq 'count') {
$s->{$stat} = commify($s->{$stat})
}
elsif ($units{$stat} eq 'interval') {
$s->{$stat} = printable_interval($s->{$stat})
}
elsif ($units{$stat} eq 'percent') {
$s->{$stat} = $s->{$stat} . '%';
}
elsif ($units{$stat} =~ /%/) {
$s->{$stat} = sprintf $units{$stat}, $s->{$stat};
}
else {
$s->{$stat} .= $units{$stat};
}
}
if ($memcache) {
my $peer = $host =~ /:/ ? $host : "$host:11211";
my $sock = IO::Socket::INET->new(PeerAddr => $peer);
unless ($sock) {
warn "Failed to connect to $peer: $!\n";
next;
}
print $sock "stats\nquit\n";
my %data;
while (<$sock>) {
$data{$host}{$1} = $2 if /STAT \s+ (\S+) \s+ (\d+)/x;
}
my $total = ($data{$host}{get_hits}+$data{$host}{get_misses}) || 1;
$s->{mc_uptime} = printable_interval($data{$host}{uptime});
$s->{mc_hits} = sprintf "%6.2f%% (%s)",
100*$data{$host}{get_hits}/$total,
commify($data{$host}{get_hits});
$s->{mc_misses} = sprintf "%6.2f%% (%s)",
100*$data{$host}{get_misses}/$total,
commify($data{$host}{get_misses});
$s->{mc_full} = memratio(
$data{$host}{bytes},
$data{$host}{limit_maxbytes}
);
$s->{mc_gets} = sprintf "%5.1f/s (%s)",
$data{$host}{cmd_get}/$data{$host}{uptime},
commify($data{$host}{cmd_get});
$s->{mc_sets} = sprintf "%5.1f/s (%s)",
$data{$host}{cmd_set}/$data{$host}{uptime},
commify($data{$host}{cmd_set});
}
}
my @table;
my $not_first = 0;
for my $key (@order) {
my $pkey = ucfirst $key;
$pkey =~ tr/_/ /;
my @row = ($pkey);
if ($html) {
push @table, [' '] if $html && $html_sections{$key} && $not_first++;
unshift @row,
exists $html_sections{$key}
? "$html_sections{$key}"
: '';
}
for my $host (@hosts) {
my $value = $stats{$host}{$key};
$value = 'unknown' unless defined $value;
push @row, $value;
}
push @table, \@row;
}
my $header = shift @table;
my $aligner = SpamSvc::DisplayAligned->new(fancy => $pretty);
if ($html) {
print $aligner->generate_html(\@table, $header);
}
else {
print $aligner->align(\@table, $header);
}
sub printable_interval {
my $t = shift; # seconds
my $secs = int($t % 60);
$t = ($t-$secs) / 60; # minutes
my $mins = int($t % 60);
$t = ($t-$mins) / 60; # hours
my $hours = int($t % 24);
$t = ($t-$hours) / 24; # days
my $days = int($t % 7);
$t = ($t-$days) / 7; # weeks
my $weeks = int $t;
my $buf = '';
$buf = sprintf '%' . ($mins ? '02' : '' ) . 'ds', $secs;
$buf = sprintf '%' . ($hours ? '02' : '' ) . 'dm %s', $mins, $buf if $mins;
$buf = sprintf '%' . ($days ? '02' : '' ) . 'dh %s', $hours, $buf if $hours;
$buf = "
$buf" if $html && ($days || $weeks);
$buf = sprintf '%d day%s, %s', $days, ($days==1 ? '' : 's'), $buf if $days;
$buf = sprintf '%d week%s, %s', $weeks, ($weeks==1 ? '' : 's'), $buf if $weeks;
$buf =~ s/\s+
'',
1 => 'KB',
2 => 'MB',
3 => 'GB',
4 => 'TB',
);
my $value = shift;
my $domain = 0;
while ($value > 1024*.9) {
$value /= 1024;
$domain++;
}
my $digits = max(0, 2 - int(log($value||1)/log(10)));
if ($domain) {
return sprintf "%.${digits}f%s", $value, $domains{$domain};
}
else {
return "$value";
}
}
sub memratio {
my $part = shift;
my $whole = shift;
if ($html) {
#return sprintf "%s/%s (%.1f%%)", byte_measure($part), byte_measure($whole), 100*$part/$whole;
return sprintf "%s/%s", byte_measure($part), byte_measure($whole);
}
else {
return sprintf "%s/%s (%.1f%%)", byte_measure($part), byte_measure($whole), 100*$part/$whole;
}
}