2024-07-23 ngIRCd and Munin

Today I tried to make the irc2 plugin work for my IRC server.

irc2

Mostly fiddling with the regular expressions. Adding an optional comma. Making it case-insensitive. The usual, I guess.

I was positively impressed by Debians pre-packaged Perl modules:

apt install libpoe-component-irc-perl libpoe-component-sslify-perl

Here's the modified plugin for `/etc/munin/plugins`:

#!/usr/bin/perl
# -*- perl -*-

=head1 NAME

ircstats  - Plugin to graph data about an IRC network and a single IRC server

=head1 CONFIGURATION

- env.SERVER to point to the server to connect to, defaults to localhost.
- env.PORT port to use, defaults to 6667.
- env.NICK nickname to use, defaults to munin-$HASH.
- env.USESSL 0 or 1 values to enable SSL/TLS, defaults to 0.
- env.USEIPV6 0 or 1 to enable IPv6 use, defaults to 0.

=head1 USAGE

This plugin connects to an IRC server.

It requires POE::Component::IRC and POE::Component::SSLify if you use SSL/TLS.

=head1 AUTHOR

Robin H. Johnson
Alex Schroeder

=head1 LICENSE

3-clause BSD.

=head1 MAGIC MARKERS

  #%# family=manual

=cut
use strict;
use warnings;
use Data::Dumper;
use POE qw(Component::IRC);
use Digest::MD5 qw(md5_hex);

my $nickname = $ENV{NICK} || 'munin-'.substr(md5_hex(rand().time()), 0, 3);
my $ircname = "Munin statistics gathering from $ENV{SERVER}";
my $server = $ENV{SERVER} || 'localhost';
my $port = $ENV{PORT} || 6667;
my $usessl = $ENV{USESSL} || 0;
my $useipv6 = $ENV{USEIPV6} || 0;

if($ARGV[0] and $ARGV[0] eq "config") {
  print "graph_title ircd status - $server\n";
  print "graph_category chat\n";
  print "graph_order clients channels servers localclients clientmax localclientmax localservers opers unknownconns\n";
  print "graph_args -l 0\n";
  print "clients.label clients\n";
  print "clients.draw LINE2\n";
  print "channels.label channels\n";
  print "channels.draw LINE2\n";
  print "servers.label servers\n";
  print "servers.draw LINE2\n";
  print "localclients.label localclients\n";
  print "localclients.draw LINE2\n";
  print "clientmax.label clientmax\n";
  print "clientmax.draw LINE2\n";
  print "localclientmax.label localclientmax\n";
  print "localclientmax.draw LINE2\n";
  print "opers.label opers\n";
  print "opers.draw LINE2\n";
  print "localservers.label localservers\n";
  print "localservers.draw LINE2\n";
  print "unknownconns.label unknownconns\n";
  print "unknownconns.draw LINE2\n";
  exit 0;
}

my %result;

# We create a new PoCo-IRC object
my $irc = POE::Component::IRC->spawn(
  nick => $nickname,
  ircname => $ircname,
  server => $server,
  port => $port,
  raw => 0,
  UseSSL => $usessl,
  useipv6 => $useipv6,
    ) or die "Oh noooo! $!";

POE::Session->create(
  package_states => [
    # debug messages:
    # main => [ qw(_default _start irc_001 irc_376 irc_disconnected irc_public) ],
    # These are the interesting ones:
    # irc_251:  'localhost' 'There are 13 users and 0 services on 2 servers' [There are 13 users and 0 services on 2 servers]
    # irc_254:  'localhost' '26 :channels formed' [26, channels formed]
    # irc_255:  'localhost' 'I have 8 users, 0 services and 1 servers' [I have 8 users, 0 services and 1 servers]
    # irc_265:  'localhost' '8 8 :Current local users: 8, Max: 8' [8, 8, Current local users: 8, Max: 8]
    # irc_266:  'localhost' '13 14 :Current global users: 13, Max: 14' [13, 14, Current global users: 13, Max: 14]
    main => [ qw(_start irc_001 irc_251 irc_252 irc_253 irc_254 irc_255 irc_265 irc_266 irc_372 irc_375 irc_376 irc_public irc_disconnected) ],
  ],
  heap => { irc => $irc },
    );

$poe_kernel->run();

my $RPL_LUSER_CLIENT = 251;
my $RPL_LUSERCHANNELS = 254;
my $RPL_ENDOFMOTD = 376;

sub _start {
  my ($heap,$kernel,$sender) = @_[HEAP,KERNEL,SENDER];

  # retrieve our component's object from the heap where we stashed it
  my $irc = $heap->{irc};

  $irc->yield( register => 'all' );
  $irc->yield( connect => { } );
  return;
}

sub irc_001 {
  my $sender = $_[SENDER];
  my $irc = $sender->get_heap();
  $irc->yield( quit => { });
  return;
}


#irc_251:  'moo.us.p2p-network.net' 'There are 155 users and 3397 invisible on 16 servers' [There are 155 users and 3397 invisible on 16 servers]
# luserclient
sub irc_251 {
  my $sender = $_[SENDER];
  my $irc = $sender->get_heap();
  my $s = $_[ARG1];
  # Do we have something like an UnrealIRCD?
  if($s =~  /There are (\d+) users and (\d+) invisible on (\d+) servers/) {
    $result{'clients'} = $1 + $2 - 1; # don't count this script
    $result{'servers'} = $3;
  }
  # Or maybe some freendode hyperion stuff?
  elsif($s =~  /There are (\d+) listed and (\d+) unlisted users on (\d+) servers/) {
    $result{'clients'} = $1 + $2 - 1; # don't count this script
    $result{'servers'} = $3;
  }
  # Or some recent ircnet ircd?
  elsif($s =~  /There are (\d+) users and \d+ services on (\d+) servers/) {
    $result{'clients'} = $1 - 1; # don't count this script
    $result{'servers'} = $2;
  }
  # Anything else goes here
  elsif($s =~  /There are (\d+) users and (\d+) invisible/) {
    $result{'clients'} = $1 + $2 - 1; # don't count this script
  }
  # And here (if there are no invisible count)
  elsif($s =~  /There are (\d+) users/) {
    $result{'clients'} = $1 - 1; # don't count this script
  }
}

#irc_252:  'moo.us.p2p-network.net' '18 :operator(s) online' [18, operator(s) online]
# opers
sub irc_252 {
  my $sender = $_[SENDER];
  my $irc = $sender->get_heap();
  my $s = $_[ARG1];
  if($s =~  /^(\d+)/) {
    $result{'opers'} = $1;
  }
}

#irc_253:  'moo.us.p2p-network.net' '1 :unknown connection(s)' [1, unknown connection(s)]
sub irc_253 {
  my $sender = $_[SENDER];
  my $irc = $sender->get_heap();
  my $s = $_[ARG1];
  if($s =~  /^(\d+)/) {
    $result{'unknownconns'} = $1;
  }
}

#irc_254:  'moo.us.p2p-network.net' '1325 :channels formed' [1325, channels formed]
# luserchannels
sub irc_254 {
  my $sender = $_[SENDER];
  my $irc = $sender->get_heap();
  my $s = $_[ARG1];
  if($s =~  /^(\d+)/) {
    $result{'channels'} = $1;
  }
}

#irc_255:  'moo.us.p2p-network.net' 'I have 348 clients and 1 servers' [I have 348 clients and 1 servers]
# local clients/servers
sub irc_255 {
  my $sender = $_[SENDER];
  my $irc = $sender->get_heap();
  my $s = $_[ARG1];
  if($s =~  /I have (\d+) clients and (\d+) servers/) {
    $result{'localclients'} = $1-1; # don't count this script
    $result{'localservers'} = $2;
  }
  elsif($s =~  /I have (\d+) users, \d+ services and (\d+) servers/) {
    $result{'localclients'} = $1-1; # don't count this script
    $result{'localservers'} = $2;
  }
}

#irc_265:  'moo.us.p2p-network.net' 'Current Local Users: 348  Max: 1900' [Current Local Users: 348  Max: 1900]
sub irc_265 {
  my $sender = $_[SENDER];
  my $irc = $sender->get_heap();
  my $s = $_[ARG1];
  if($s =~  /Current Local Users: (\d+),?\s+Max: (\d+)/i) {
    $result{'localclients'} = $1-1; # don't count this script
    $result{'localclientmax'} = $2;
  }
}

#irc_266:  'moo.us.p2p-network.net' 'Current Global Users: 3552  Max: 8742' [Current Global Users: 3552  Max: 8742]
sub irc_266 {
  my $sender = $_[SENDER];
  my $irc = $sender->get_heap();
  my $s = $_[ARG1];
  if($s =~  /Current Global Users: (\d+),?\s+Max: (\d+)/i) {
    $result{'clients'} = $1-1; # don't count this script
    $result{'clientmax'} = $2;
  }
}

# 372 motdline
sub irc_372 {
  return;
}
# 375 startofmotd
sub irc_375 {
  return;
}
# 376 endofmotd
sub irc_376 {
  my $sender = $_[SENDER];
  my $irc = $sender->get_heap();
  $irc->yield( quit => {} );
}

sub munin_print {
  my $key = shift;
  my $val = shift;
  print "${key}.value ".($val || 'U')."\n";
}

sub irc_disconnected {
  for my $var (qw(clients channels servers localclients clientmax localclientmax localservers opers unknownconns)) {
    munin_print($var, $result{$var});
  }
  exit 0;
}

sub irc_public {
  my ($sender, $who, $where, $what) = @_[SENDER, ARG0 .. ARG2];
  my $nick = ( split /!/, $who )[0];
  my $channel = $where->[0];

  if ( my ($rot13) = $what =~ /^rot13 (.+)/ ) {
    $rot13 =~ tr[a-zA-Z][n-za-mN-ZA-M];
    $irc->yield( privmsg => $channel => "$nick: $rot13" );
  }
  return;
}

# We registered for all events, this will produce some debug info.
sub _default {
  my ($event, $args) = @_[ARG0 .. $#_];
  my @output = ( "$event: " );

  for my $arg (@$args) {
    if ( ref $arg eq 'ARRAY' ) {
      push( @output, '[' . join(', ', @$arg ) . ']' );
    }
    else {
      push ( @output, "'$arg'" );
    }
  }
  print join ' ', @output, "\n";
  return 0;
}

To configure, I created the file `/etc/munin/plugin-conf.d/irc.conf`:

[irc2]
env.SERVER campaignwiki.org
env.PORT 6697
env.USESSL 1

The result:

A graph showing some ngIRCd data

​#Administration ​#Munin ​#IRC ​#ngircd