#!/usr/local/bin/perl
#
# Markers for munin's automagical configuration:
#%# family=auto
#%# capabilities=autoconf
#
=head1 NAME

aprsc_munin - Munin plugin which monitors an aprsc server instance

=head CONFIGURATION

This plugin needs to be able to request http://localhost:14501/status.json.
The URL can be changed in the configuration to point to another server URL.

This configuration example shows the default settings for the plugin:

  [aprsc_munin]
     env.url   http://127.0.0.1:14501/status.json

=head1 LICENSE

BSD

=cut

use strict;
use warnings;
use File::Basename;
use Data::Dumper;

my $in_autoconf = (defined $ARGV[0] && $ARGV[0] eq "autoconf");
my $in_config = (defined $ARGV[0] && $ARGV[0] eq "config");
my $in_makelinks = (defined $ARGV[0] && $ARGV[0] eq "makelinks");
my $title_add;

$title_add = $ENV{'title'} if (defined $ENV{'title'});

# present fail message in the right format depending on mode
sub fail($)
{
	my($s) = @_;
	if ($in_autoconf) {
		print "no ($s)\n";
		exit(1);
	}
	
	warn "aprsc_munin failed: $s\n";
	exit(1);
}

# print a config set for a single graph
sub print_config($$)
{
	my($graph, $gr) = @_;
	
	my $attrs = $gr->{'config_attrs'};
	my $srcs = $gr->{'sources'};
	my @order;
	if (defined $gr->{'order'}) {
		@order = @{ $gr->{'order'} };
	} else {
		@order = sort keys %{ $srcs };
	}
	
	if (defined $title_add) {
		$attrs->{'graph_title'} = $title_add . " - " . $attrs->{'graph_title'};
		$attrs->{'graph_category'} .= ' ' . $title_add;
	}
	
	foreach my $k (keys %{ $attrs }) {
		printf("%s %s\n", $k, $attrs->{$k});
	}
	
	foreach my $src (@order) {
		foreach my $k (keys %{ $srcs->{$src}->{'attrs'} }) {
			printf("%s.%s %s\n", $src, $k, $srcs->{$src}->{'attrs'}->{$k});
		}
	}
}

# find a value by key from the JSON object
sub find_val($$)
{
	my($needle, $j) = @_;
	
	my $r = $j;
	foreach my $p (split('\.', $needle)) {
		return 'U' if (ref($r) ne 'HASH' || !defined $r->{$p});
		$r = $r->{$p};
	}
	
	return $r;
}

# print data values
sub print_data($$$)
{
	my($graph, $gr, $j) = @_;
	
	my $srcs = $gr->{'sources'};
	
	foreach my $k (keys %{ $srcs }) {
		my $val = find_val($srcs->{$k}->{'k'}, $j);
		print "$k.value $val\n";
	}
}

#
##### main
#

# require some unusual modules
if (!eval "require LWP::UserAgent;")
{
	fail("LWP::UserAgent not found");
}

if (!eval "require JSON::XS;")
{
	fail("JSON::XS not found");
}

# configuration
my $status_url = defined $ENV{'url'} ? $ENV{'url'} : "http://127.0.0.1:14501/status.json";

# definitions for graphs
my $category = 'aprsc';
my %graphs = (
	'0clients' => {
		'config_attrs' => {
			'graph_title' => 'Clients allocated',
			'graph_vlabel' => 'clients + pseudoclients allocated',
			'graph_args' => '--base 1000',
			'graph_category' => $category,
		},
		'sources' => {
			'clients_total' => {
				'k' => 'totals.clients',
				'attrs' => {
					'label' => 'Total',
					'min' => 0,
					'type' => 'GAUGE',
					'colour' => 'dd00ff'
				}
			}
		}
	},
	'1connects' => {
		'config_attrs' => {
			'graph_title' => 'Connections',
			'graph_vlabel' => 'connections/s',
			'graph_args' => '--base 1000',
			'graph_category' => $category,
		},
		'sources' => {
			'conns' => {
				'k' => 'totals.connects',
				'attrs' => {
					'label' => 'Incoming connections',
					'min' => 0,
					'max' => 100000,
					'type' => 'DERIVE',
					'colour' => '25a7fd'
				}
			}
		}
	},
	'dupecheck' => {
		'config_attrs' => {
			'graph_title' => 'Dupechecked packets',
			'graph_vlabel' => 'packets/s',
			'graph_args' => '--base 1000  --lower-limit 0',
			'graph_category' => $category,
		},
		'sources' => {
			'uniq' => {
				'k' => 'dupecheck.uniques_out',
				'attrs' => {
					'label' => 'Unique packets',
					'min' => 0,
					'type' => 'DERIVE',
					'warning' => '30:200',
					'critical' => '10:400'
				}
			},
			'dup' => {
				'k' => 'dupecheck.dupes_dropped',
				'attrs' => {
					'label' => 'Duplicates dropped',
					'min' => 0,
					'type' => 'DERIVE',
					'colour' => 'e47bfd'
				}
			}
		}
	},
	'dpktstcp' => {
		'config_attrs' => {
			'graph_title' => 'APRS packets over TCP',
			'graph_vlabel' => 'packets/s in (-) / out (+)',
			'graph_args' => '--base 1000',
			'graph_order' => 'rx tx',
			'graph_category' => $category,
		},
		'sources' => {
			'rx' => {
				'k' => 'totals.tcp_pkts_rx',
				'attrs' => {
					'label' => 'Packets RX',
					'min' => 0,
					'type' => 'DERIVE',
					'graph' => 'no',
					'draw' => 'AREA',
					'colour' => '16b5ff'
				}
			},
			'tx' => {
				'k' => 'totals.tcp_pkts_tx',
				'attrs' => {
					'label' => 'Packets',
					'min' => 0,
					'type' => 'DERIVE',
					'negative' => 'rx',
					'draw' => 'AREA',
					'colour' => 'fd745c'
				}
			},
		}
	},
	'dpktsudp' => {
		'config_attrs' => {
			'graph_title' => 'APRS packets over UDP',
			'graph_vlabel' => 'packets/s in (-) / out (+)',
			'graph_args' => '--base 1000',
			'graph_order' => 'rx tx',
			'graph_category' => $category,
		},
		'sources' => {
			'rx' => {
				'k' => 'totals.udp_pkts_rx',
				'attrs' => {
					'label' => 'Packets RX',
					'min' => 0,
					'type' => 'DERIVE',
					'graph' => 'no',
					'draw' => 'AREA',
					'colour' => '16b5ff'
				}
			},
			'tx' => {
				'k' => 'totals.udp_pkts_tx',
				'attrs' => {
					'label' => 'Packets',
					'min' => 0,
					'type' => 'DERIVE',
					'negative' => 'rx',
					'draw' => 'AREA',
					'colour' => 'fd745c'
				}
			},
		}
	},
	'ddatatcp' => {
		'config_attrs' => {
			'graph_title' => 'APRS data over TCP',
			'graph_vlabel' => 'bits/s in (-) / out (+)',
			'graph_args' => '--base 1000',
			'graph_order' => 'rx tx',
			'graph_category' => $category,
		},
		'sources' => {
			'rx' => {
				'k' => 'totals.tcp_bytes_rx',
				'attrs' => {
					'label' => 'Bits/s RX',
					'min' => 0, 
					'type' => 'DERIVE',
					'graph' => 'no',
					'cdef' => 'rx,8,*',
					'draw' => 'AREA',
					'colour' => '16b5ff'
				}
			},
			'tx' => {
				'k' => 'totals.tcp_bytes_tx',
				'attrs' => {
					'label' => 'Bits/s',
					'min' => 0,
					'type' => 'DERIVE',
					'negative' => 'rx',
					'cdef' => 'tx,8,*',
					'draw' => 'AREA',
					'colour' => 'fd745c'
				}
			},
		}
	},
	'ddataudp' => {
		'config_attrs' => {
			'graph_title' => 'APRS data over UDP',
			'graph_vlabel' => 'bits/s in (-) / out (+)',
			'graph_args' => '--base 1000',
			'graph_order' => 'rx tx',
			'graph_category' => $category,
		},
		'sources' => {
			'rx' => {
				'k' => 'totals.udp_bytes_rx',
				'attrs' => {
					'label' => 'Bits/s RX',
					'min' => 0,
					'type' => 'DERIVE',
					'graph' => 'no',
					'cdef' => 'rx,8,*',
					'draw' => 'AREA',
					'colour' => '16b5ff'
				}
			},
			'tx' => {
				'k' => 'totals.udp_bytes_tx',
				'attrs' => {
					'label' => 'Bits/s',
					'min' => 0,
					'type' => 'DERIVE',
					'negative' => 'rx',
					'cdef' => 'tx,8,*',
					'draw' => 'AREA',
					'colour' => 'fd745c'
				}
			},
		}
	},
	'memcellu' => {
		'config_attrs' => {
			'graph_title' => 'Memory cells used',
			'graph_vlabel' => 'cells',
			'graph_args' => '--base 1024 --lower-limit 0',
			'graph_category' => $category,
		},
		'keytail' => 'cells_used',
		'sources' => {
		}
	},
	'memcellub' => {
		'config_attrs' => {
			'graph_title' => 'Memory bytes used',
			'graph_vlabel' => 'bytes',
			'graph_args' => '--base 1024 --lower-limit 0',
			'graph_category' => $category,
		},
		'keytail' => 'used_bytes',
		'sources' => {
		}
	},
	'memcelluba' => {
		'config_attrs' => {
			'graph_title' => 'Memory bytes allocated',
			'graph_vlabel' => 'bytes',
			'graph_args' => '--base 1024 --lower-limit 0',
			'graph_category' => $category,
		},
		'keytail' => 'allocated_bytes',
		'sources' => {
		}
	},
	'clientlist' => {
		'config_attrs' => {
			'graph_title' => 'Clients per listener',
			'graph_vlabel' => 'clients connected',
			'graph_args' => '--base 1000',
			'graph_category' => $category,
		},
		'keytail' => 'clients',
		'type' => 'GAUGE',
		'sources' => {
		}
	},
	'clientx0pin' => {
		'config_attrs' => {
			'graph_title' => 'Packets in per listener',
			'graph_vlabel' => 'packets/s',
			'graph_args' => '--base 1000',
			'graph_category' => $category,
		},
		'keytail' => 'pkts_rx',
		'type' => 'DERIVE',
		'sources' => {
		}
	},
	'clientx0dout' => {
		'config_attrs' => {
			'graph_title' => 'Data out per listener',
			'graph_vlabel' => 'bits/s',
			'graph_args' => '--base 1000',
			'graph_category' => $category,
		},
		'keytail' => 'bytes_tx',
		'type' => 'DERIVE',
		'cdef' => ',8,*',
		'sources' => {
		}
	},
	'peerinpkts' => {
		'config_attrs' => {
			'graph_title' => 'Packets in per peer',
			'graph_vlabel' => 'packets/s',
			'graph_args' => '--base 1000 --lower-limit 0',
			'graph_category' => $category,
		},
		'keytail' => 'pkts_rx',
		'type' => 'DERIVE',
		'sources' => {
		}
	},
	'rxerrstcp' => {
		'config_attrs' => {
			'graph_title' => 'Packets dropped from TCP clients',
			'graph_vlabel' => 'packets/min',
			'graph_args' => '--base 1000',
			'graph_category' => $category,
		},
		'key' => 'totals.tcp_rx_errs',
		'type' => 'DERIVE',
		'cdef' => ',60,*',
		'sources' => {
		}
	},
	'rxerrsudp' => {
		'config_attrs' => {
			'graph_title' => 'Packets dropped from UDP clients',
			'graph_vlabel' => 'packets/min',
			'graph_args' => '--base 1000',
			'graph_category' => $category,
		},
		'key' => 'totals.udp_rx_errs',
		'type' => 'DERIVE',
		'cdef' => ',60,*',
		'sources' => {
		}
	},
);

if ($in_makelinks) {
	my $instance = '';
	$instance = '_' . $ARGV[1] if (defined $ARGV[1]);
	foreach my $graph (sort keys %graphs) {
		my $d = "aprsc" . $instance . "_" . $graph;
		next if (-e $d);
		symlink($0, $d) || die "Failed to symlink $0 to $d: $!\n";
	}
	exit(0);
}

# initialize our hammers and fetch JSON from server
my $json = new JSON::XS;
my $ua = LWP::UserAgent->new(timeout => 5);
my $res = $ua->request(HTTP::Request->new('GET', $status_url));

if (!$res->is_success) {
	# todo: print result error messages
	fail("HTTP request to aprsc failed");
}

#print $res->content;

# decode and validate
my $j = $json->decode($res->content);

if (!defined $j) {
	fail("JSON parsing of status object failed");
}

# if we're just checking if this doable, say so
if ($in_autoconf) {
	print "yes\n";
	exit(0);
}

my $graph = basename($0);
$graph =~ s/.*_//;

if (!defined $graphs{$graph}) {
	fail("No such graph available: $graph");
}
my $gr = $graphs{$graph};

# for memcell graphs, get a dynamic list of sources
if ($graph =~ /^memcell/) {
	my @ord;
	foreach my $k (sort grep(/cells_used$/, keys %{ $j->{'memory'} })) {
		$k =~ s/_cells_used$//;
		push @ord, $k;
		$gr->{'sources'}->{$k} = {
			'k' => 'memory.' . $k . '_' . $gr->{'keytail'},
			'attrs' => {
				'label' => $k,
				'min' => 0,
				'type' => 'GAUGE',
			}
		};
	}
	$gr->{'order'} = \@ord;
	
} elsif ($graph =~ /^client(list|x)/) {
	my @ord;
	# convert array to hash
	my $h = {};
	foreach my $l (@{ $j->{'listeners'} }) {
		my $k = sprintf("%s_%s", $l->{'proto'}, $l->{'addr'});
		$k =~ s/[^\w]/_/g;
		$h->{$k} = $l;
	}
	$j->{'listeners'} = $h;
	my $draw = 'AREA';
	foreach my $k (sort { $j->{'listeners'}->{$a}->{'clients'} <=> $j->{'listeners'}->{$b}->{'clients'} } keys %{ $j->{'listeners'} }) {
		next if ($graph eq 'clientlist' && $j->{'listeners'}->{$k}->{'proto'} eq 'udp');
		push @ord, $k;
		$gr->{'sources'}->{$k} = {
			'k' => 'listeners.' . $k . '.' . $gr->{'keytail'},
			'attrs' => {
				'label' => $j->{'listeners'}->{$k}->{'proto'} . '/' . $j->{'listeners'}->{$k}->{'addr'},
				'min' => 0,
				'type' => $gr->{'type'},
				'draw' => $draw
			}
		};
		if ($gr->{'cdef'}) {
			$gr->{'sources'}->{$k}->{'attrs'}->{'cdef'} = $k . $gr->{'cdef'};
		}
		$draw = 'STACK';
	}
	$gr->{'order'} = \@ord;
} elsif ($graph =~ /^peer/) {
	my @ord;
	# convert array to hash
	my $h = {};
	foreach my $l (@{ $j->{'peers'} }) {
		my $k = sprintf("p%s", $l->{'addr_rem'});
		$k =~ s/[^\w]/_/g;
		$h->{$k} = $l;
		
		if ($graph =~ /^peerinpkts/ && !defined $h->{'out'}) {
			$h->{'out'} = $l;
		}
	}
	
	$j->{'peers'} = $h;
	my $draw = 'AREA';
	foreach my $k (sort keys %{ $j->{'peers'} }) {
		push @ord, $k;
		my $keytail = $gr->{'keytail'};
		my $label = ((defined $j->{'peers'}->{$k}->{'username'}) ? $j->{'peers'}->{$k}->{'username'} . ' ' : '')
			. $j->{'peers'}->{$k}->{'addr_rem'};
		if ($k eq 'out') {
			$label = 'Packets out per peer';
			$keytail = 'pkts_tx';	
		}
		$gr->{'sources'}->{$k} = {
			'k' => 'peers.' . $k . '.' . $keytail,
			'attrs' => {
				'label' => $label,
				'min' => 0,
				'type' => $gr->{'type'},
				'draw' => $draw,
				'warning' => '0.2:100',
				'critical' => '0.05:200',
			}
		};
		if ($gr->{'cdef'}) {
			$gr->{'sources'}->{$k}->{'attrs'}->{'cdef'} = $k . $gr->{'cdef'};
		}
		$draw = 'STACK';
	}
	$gr->{'order'} = \@ord;
	if ($#ord == -1) {
		$gr->{'config_attrs'}->{'update'} = 'no';
		$gr->{'config_attrs'}->{'graph'} = 'no';
	}
} elsif ($graph =~ /^rxerrs(tcp|udp)/) {
	my @ord;
	my $keys = $j->{'rx_errs'};
	# convert array to hash
	my $h = {};
	my $vals = find_val($gr->{'key'}, $j);
	my $draw = 'AREA';
	foreach my $k (@$keys) {
		push @ord, $k;
		#$k =~ s/[^\w]/_/g;
		$h->{$k} = shift @$vals;
		$gr->{'sources'}->{$k} = {
			'k' => 'use.' . $k,
			'attrs' => {
				'label' => $k,
				'min' => 0,
				'type' => $gr->{'type'},
				'draw' => $draw
			}
		};
		if ($gr->{'cdef'}) {
			$gr->{'sources'}->{$k}->{'attrs'}->{'cdef'} = $k . $gr->{'cdef'};
		}
		$draw = 'STACK';
	}
	$j->{'use'} = $h;
	$gr->{'order'} = \@ord;
}

if ($in_config) {
	# default title from the server's ServerId
	$title_add = $j->{'server'}->{'server_id'} if (!defined $title_add && defined $j->{'server'});
	print_config($graph, $gr);
	exit(0);
}

print_data($graph, $gr, $j);

exit(0);
