Repository
Munin (contrib)
Last change
2020-09-23
Graph Categories
Family
manual
Keywords
Language
Perl
Authors

multiping_async

Name

multiping_async - Like the multiping plugin but runs asynchronously

Synopsis

munin-run multiping_async

Configuration

The following environment variables are used:

host     - Whitespace-separated list of hosts to ping
times    - How many times to ping the hosts, 3 by default
timeout  - The ping timeout (ping -W), 1 by default (ignored on Solaris)
title    - The graph_title to use for the munin RRD graph
category - What category the graph should be in, network by default

Configuration example, ping all the Linode clusters:

# An optional custom category
[multiping_async_*]
env.category ping

[multiping_async_linode]
# From http://www.linode.com/speedtest/
env.title Ping times to all the Linode clusters
env.host  london1.linode.com newark1.linode.com atlanta1.linode.com dallas1.linode.com fremont1.linode.com

Description

Like the munin multiping plugin except that it runs ping(1) asynchronously with POE, and you can add/remove hosts later on without screwing up your RRD files (multiping reports statistics based on the order of hosts in hosts=).

This plugin used to use POE::Component::Client::Ping but I switched away from it due to having odd timing issues with it, and it had to run as root.

This plugin requires the MooseX::POE and POE::Quickie modules from CPAN. It has been tested with the Linux, FreeBSD and Solaris ping(1) implementations.

Author

Ævar Arnfjörð Bjarmason avar@cpan.org

License

This program is in the public domain.

Magic Markers

#%# family=manual
#!/usr/bin/env perl
package Munin::Plugin::Multiping::Async;
use 5.10.0;
use MooseX::POE;
use MooseX::POE::SweetArgs qw(event);
use POE::Quickie;
use Munin::Plugin;
use Storable;
use Digest;

=head1 NAME

multiping_async - Like the multiping plugin but runs asynchronously

=head1 SYNOPSIS

    munin-run multiping_async

=head1 CONFIGURATION

The following environment variables are used:

    host     - Whitespace-separated list of hosts to ping
    times    - How many times to ping the hosts, 3 by default
    timeout  - The ping timeout (ping -W), 1 by default (ignored on Solaris)
    title    - The graph_title to use for the munin RRD graph
    category - What category the graph should be in, network by default

Configuration example, ping all the Linode clusters:

    # An optional custom category
    [multiping_async_*]
    env.category ping

    [multiping_async_linode]
    # From http://www.linode.com/speedtest/
    env.title Ping times to all the Linode clusters
    env.host  london1.linode.com newark1.linode.com atlanta1.linode.com dallas1.linode.com fremont1.linode.com

=head1 DESCRIPTION

Like the L<munin
multiping|http://munin-monitoring.org/browser/people/janl/src/node/node.d/multiping>
plugin except that it runs L<ping(1)> asynchronously with POE, and you
can add/remove hosts later on without screwing up your RRD files
(multiping reports statistics based on the order of hosts in
C<hosts=>).

This plugin used to use L<POE::Component::Client::Ping> but I switched
away from it due to having odd timing issues with it, and it had to
run as root.

This plugin requires the L<MooseX::POE> and L<POE::Quickie> modules
from CPAN. It has been tested with the Linux, FreeBSD and Solaris
L<ping(1)> implementations.

=head1 AUTHOR

Ævar Arnfjörð Bjarmason <avar@cpan.org>

=head1 LICENSE

This program is in the public domain.

=head1 MAGIC MARKERS

 #%# family=manual

=cut

has graph_title => (
    isa => 'Str',
    is  => 'ro',
    default => $ENV{title} // 'Ping times',
    documentation => 'The munin graph_title',
);

has hosts => (
    isa        => 'ArrayRef',
    is         => 'ro',
    auto_deref => 1,
    default    => sub {
        my $host = $ENV{host} // '';
        return [ split /\s+/, $host ]
    },
    documentation => "Hosts we're going to ping",
);

has times => (
    isa           => 'Int',
    is            => 'ro',
    default       => $ENV{times} // 3,
    documentation => "How many times we ping each host (ping -c)",
);

has timeout => (
    isa           => 'Int',
    is            => 'ro',
    default       => $ENV{timeout} // 1,
    documentation => "How long until ping timeouts (ping -W)",
);

has category => (
    isa           => 'Str',
    is            => 'ro',
    default       => $ENV{category} // 'network',
    documentation => "What munin category we should appear in",
);

has should_config => (
    isa => 'Bool',
    is => 'ro',
    default => sub { defined $ARGV[0] and $ARGV[0] eq "config" },
    documentation => 'Spew out config section?',
);

has response => (
    isa        => 'HashRef',
    is         => 'ro',
    auto_deref => 0,
    default    => sub { +{} },
    documentation => 'To store ping responses',
);

has statefile => (
    isa           => 'Str',
    is            => 'ro',
    default       => $ENV{MUNIN_STATEFILE},
    documentation => 'Where we store state between invocations',
);

sub START {
    my ($self) = @_;

    die "You must supply some hosts" unless @{ $self->hosts } > 0;

    if ($self->should_config) {
        $self->print_config;
        return;
    }

    for my $host ($self->hosts) {
        POE::Quickie->new->run(
            Program     => [ $self->ping_arguments($host) ],
            StdoutEvent => 'stdout',
            ExitEvent   => 'exit',
            Context     => $host,
        );
    }
}

sub ping_arguments {
    my ($self, $host) = @_;

    given ($^O) {
        when ('solaris') {
            return ('ping', '-s', $host, '64', $self->times);
        }
        default {
            # Linux and FreeBSD
            return ('ping', '-c', $self->times, '-W', $self->timeout, => $host);
        }
    }
}

event stdout => sub {
    my ($self, $output, undef, $context) = @_;

    given ($output) {
        my $noslash = qr{[^/]+};
        # Linux output: rtt min/avg/max/mdev = 7.218/7.255/7.293/0.030 ms
        # BSD output  : round-trip min/avg/max/stddev = 34.935/35.665/36.684/0.743 ms
        # Solaris     : round-trip (ms)  min/avg/max/stddev = 5.82/5.95/6.01/0.11
        when (m[= (?<min>$noslash)/(?<avg>$noslash)/(?<max>$noslash)/]) {
            $self->response->{ $context } = $+{avg};
        }
    }
};

event exit => sub {
    my ($self, $code, $x, $context) = @_;

    given ($code) {
        when (0) {
            die "Got no response from $context" unless exists $self->response->{ $context };
            $self->yield( print_host => $context => $self->response->{ $context } );
        }
        default {
            # Host down, probably
            $self->yield( print_host => $context => 0 );
        }
    }

    return;
};

sub STOP {
    my ($self) = @_;

    if (not $self->should_config and my $file = $self->statefile) {
        my $res = $self->response;
        my $ret = store($res, $file);
        # use Data::Dumper;
        # say Dumper { gonna_store => $res, ret => $ret, file => $file };
    }
}

sub print_config {
    my ($self) = @_;
    my $title = $self->graph_title;
    my $times = $self->times;
    my $category = $self->category;
    print <<GRAPH;
graph_title $title
graph_args --base 1000 -l 0
graph_vlabel milliseconds
graph_category $category
graph_info Average ping times (over $times pings)
GRAPH
    for my $host ($self->sorted_hosts) {
        my $fieldname = $self->fieldname($host);
        print <<HOST;
$fieldname.label $host
$fieldname.info Average ping time over $times pings for $host
$fieldname.draw LINE2
HOST
    }
};

sub sorted_hosts {
    my ($self) = @_;

    my @hosts = $self->hosts;
    my $state = $self->statefile;

    given ($self->statefile) {
        when (-e and -r) {
            my $last_res = retrieve($_);
            my @sorted = sort { $last_res->{$b} <=> $last_res->{$a} } keys %$last_res;
            if ($last_res and @hosts == @sorted) {
                return @sorted;
            }
        }
    }

    return @hosts;
}

event print_host => sub {
    my ($self, $context, $time) = @_;

    my $fieldname = $self->fieldname($context);
    my $value = sprintf "%6.6f", $time;

    say "$fieldname.value $value";
};

sub fieldname {
    my ($self, $name) = @_;
    my $sha1 = substr Digest->new("SHA-1")->add($name)->hexdigest, 0, 10;
    return clean_fieldname($name) . '_' . $sha1;
}

no MooseX::POE;
Munin::Plugin::Multiping::Async->new;
POE::Kernel->run;