The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package AnyEvent::Graphite::SNMPAgent;

use warnings;
use strict;

use AnyEvent::Graphite;
use AnyEvent::SNMP;
use AnyEvent::DNS;
use Net::SNMP;

sub new {
    my($class, %args) = @_;

    # the rest of the args will get passed to the object
    # so remove host and port as we'll possibly set them manually here
    my $graphite = AnyEvent::Graphite->new(host => $args{host}, port => $args{port});
    delete $args{host};
    delete $args{port};
   
    # the hosts we'll be storing metrics in
    my $hosts = {};
    my $interval = delete $args{interval} || 60;

    my $timeout = delete $args{timeout} || 3;

    my $snmp_version = delete $args{snmp_version} || "1";

    # need to know about $self before defining the callback just below this
    my $self = bless {
        graphite => $graphite,
        hosts => $hosts,
        snmp_version => $snmp_version,
        %args,
    }, $class;

       
    # start the timer running
    $self->{timer} = AE::timer 0, $interval, sub { $self->gather_metrics; }; 

    return $self;
}

sub add_snmp {
    my ($self, %arg) = @_;
    # toss the arguments into the (per-host) queue
    # look up the host with AnyEvent::DNS so we can be async even here
    AnyEvent::DNS::a $arg{'host'}, sub {
        my (@addrs) = @_;
        push(@{$self->{hosts}{$addrs[0]}}, \%arg); 
    };
}

# this is called by $self->{timer} every $interval seconds
sub gather_metrics {
    my ($self) = @_;
    for my $host (keys %{$self->{hosts}}) {

        # skip any hosts that did not resolve
        next unless $host;

        # steal a community string from the first item in the list. They should all be the same
        my $community = $self->{hosts}{$host}[0]{community} || "public";
        my $session = Net::SNMP->session(
            -hostname => $host,
            -community => $community,
            -nonblocking => 1,
            -version => $self->{snmp_version}
        );

        # in this kind of context it's not clear what would be better to do with errors, here.
        next unless $session;

        # if you don't set a timeout, you can fill up your queues of outstanding processes
        # protects against 'lame' servers
        $session->timeout($self->{timeout});

        for my $metric (@{$self->{hosts}{$host}}) {
            $session->get_request( 
                -varbindlist => [$metric->{oid}], 
                -callback => sub {
                    my ($session) = @_;
                    my $result = $session->var_bind_list();
                    my $value = $result->{$metric->{oid}};
                    if(!defined($value)) {
                        warn "undefined reply from $host" . ":" . "$metric->{oid}: " . $session->error();
                    } else {
                        if($metric->{filter}) {
                            $value = $metric->{filter}->($value);
                        }
                        $self->{graphite}->send($metric->{graphite_key}, $value);
                    }
                });
        }
    }

}

=head1 NAME

AnyEvent::Graphite::SNMPAgent - An SNMP agent which does non-blocking streaming of data from an SNMP server

=head1 SYNOPSIS
    
    my $agent = AnyEvent::Graphite::SNMPAgent->new(
        host => '127.0.0.1',
        port => '2003',
        interval => 60,
        timeout => 5,
    );

    'host' and 'port' are for the graphite server
    'interval' is how many seconds should elapse between each time we try to fire off the queries.
    If you need multiple intervals create one AE::Graphite::SNMPAgent instance per set of metrics


    $agent->add_snmp(host => $host, oid => $oid, community => $community, graphite_key => $key, filter => sub { ... });

    print "Running forever. CTRL-C to interrupt\n";
    AnyEvent->condvar->recv;


=head1 AUTHOR

Joshua Barratt, C<< <josh at mediatemple.net> >>

=head1 COPYRIGHT & LICENSE

This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=cut

1; # End of AnyEvent::Graphite