1#!/usr/local/bin/perl
2#
3# Markers for munin's automagical configuration:
4#%# family=auto
5#%# capabilities=autoconf
6#
7=head1 NAME
8
9aprsc_munin - Munin plugin which monitors an aprsc server instance
10
11=head CONFIGURATION
12
13This plugin needs to be able to request http://localhost:14501/status.json.
14The URL can be changed in the configuration to point to another server URL.
15
16This configuration example shows the default settings for the plugin:
17
18  [aprsc_munin]
19     env.url   http://127.0.0.1:14501/status.json
20
21=head1 LICENSE
22
23BSD
24
25=cut
26
27use strict;
28use warnings;
29use File::Basename;
30use Data::Dumper;
31
32my $in_autoconf = (defined $ARGV[0] && $ARGV[0] eq "autoconf");
33my $in_config = (defined $ARGV[0] && $ARGV[0] eq "config");
34my $in_makelinks = (defined $ARGV[0] && $ARGV[0] eq "makelinks");
35my $title_add;
36
37$title_add = $ENV{'title'} if (defined $ENV{'title'});
38
39# present fail message in the right format depending on mode
40sub fail($)
41{
42	my($s) = @_;
43	if ($in_autoconf) {
44		print "no ($s)\n";
45		exit(1);
46	}
47
48	warn "aprsc_munin failed: $s\n";
49	exit(1);
50}
51
52# print a config set for a single graph
53sub print_config($$)
54{
55	my($graph, $gr) = @_;
56
57	my $attrs = $gr->{'config_attrs'};
58	my $srcs = $gr->{'sources'};
59	my @order;
60	if (defined $gr->{'order'}) {
61		@order = @{ $gr->{'order'} };
62	} else {
63		@order = sort keys %{ $srcs };
64	}
65
66	if (defined $title_add) {
67		$attrs->{'graph_title'} = $title_add . " - " . $attrs->{'graph_title'};
68		$attrs->{'graph_category'} .= ' ' . $title_add;
69	}
70
71	foreach my $k (keys %{ $attrs }) {
72		printf("%s %s\n", $k, $attrs->{$k});
73	}
74
75	foreach my $src (@order) {
76		foreach my $k (keys %{ $srcs->{$src}->{'attrs'} }) {
77			printf("%s.%s %s\n", $src, $k, $srcs->{$src}->{'attrs'}->{$k});
78		}
79	}
80}
81
82# find a value by key from the JSON object
83sub find_val($$)
84{
85	my($needle, $j) = @_;
86
87	my $r = $j;
88	foreach my $p (split('\.', $needle)) {
89		return 'U' if (ref($r) ne 'HASH' || !defined $r->{$p});
90		$r = $r->{$p};
91	}
92
93	return $r;
94}
95
96# print data values
97sub print_data($$$)
98{
99	my($graph, $gr, $j) = @_;
100
101	my $srcs = $gr->{'sources'};
102
103	foreach my $k (keys %{ $srcs }) {
104		my $val = find_val($srcs->{$k}->{'k'}, $j);
105		print "$k.value $val\n";
106	}
107}
108
109#
110##### main
111#
112
113# require some unusual modules
114if (!eval "require LWP::UserAgent;")
115{
116	fail("LWP::UserAgent not found");
117}
118
119if (!eval "require JSON::XS;")
120{
121	fail("JSON::XS not found");
122}
123
124# configuration
125my $status_url = defined $ENV{'url'} ? $ENV{'url'} : "http://127.0.0.1:14501/status.json";
126
127# definitions for graphs
128my $category = 'aprsc';
129my %graphs = (
130	'0clients' => {
131		'config_attrs' => {
132			'graph_title' => 'Clients allocated',
133			'graph_vlabel' => 'clients + pseudoclients allocated',
134			'graph_args' => '--base 1000',
135			'graph_category' => $category,
136		},
137		'sources' => {
138			'clients_total' => {
139				'k' => 'totals.clients',
140				'attrs' => {
141					'label' => 'Total',
142					'min' => 0,
143					'type' => 'GAUGE',
144					'colour' => 'dd00ff'
145				}
146			}
147		}
148	},
149	'1connects' => {
150		'config_attrs' => {
151			'graph_title' => 'Connections',
152			'graph_vlabel' => 'connections/s',
153			'graph_args' => '--base 1000',
154			'graph_category' => $category,
155		},
156		'sources' => {
157			'conns' => {
158				'k' => 'totals.connects',
159				'attrs' => {
160					'label' => 'Incoming connections',
161					'min' => 0,
162					'max' => 100000,
163					'type' => 'DERIVE',
164					'colour' => '25a7fd'
165				}
166			}
167		}
168	},
169	'dupecheck' => {
170		'config_attrs' => {
171			'graph_title' => 'Dupechecked packets',
172			'graph_vlabel' => 'packets/s',
173			'graph_args' => '--base 1000  --lower-limit 0',
174			'graph_category' => $category,
175		},
176		'sources' => {
177			'uniq' => {
178				'k' => 'dupecheck.uniques_out',
179				'attrs' => {
180					'label' => 'Unique packets',
181					'min' => 0,
182					'type' => 'DERIVE',
183					'warning' => '30:200',
184					'critical' => '10:400'
185				}
186			},
187			'dup' => {
188				'k' => 'dupecheck.dupes_dropped',
189				'attrs' => {
190					'label' => 'Duplicates dropped',
191					'min' => 0,
192					'type' => 'DERIVE',
193					'colour' => 'e47bfd'
194				}
195			}
196		}
197	},
198	'dpktstcp' => {
199		'config_attrs' => {
200			'graph_title' => 'APRS packets over TCP',
201			'graph_vlabel' => 'packets/s in (-) / out (+)',
202			'graph_args' => '--base 1000',
203			'graph_order' => 'rx tx',
204			'graph_category' => $category,
205		},
206		'sources' => {
207			'rx' => {
208				'k' => 'totals.tcp_pkts_rx',
209				'attrs' => {
210					'label' => 'Packets RX',
211					'min' => 0,
212					'type' => 'DERIVE',
213					'graph' => 'no',
214					'draw' => 'AREA',
215					'colour' => '16b5ff'
216				}
217			},
218			'tx' => {
219				'k' => 'totals.tcp_pkts_tx',
220				'attrs' => {
221					'label' => 'Packets',
222					'min' => 0,
223					'type' => 'DERIVE',
224					'negative' => 'rx',
225					'draw' => 'AREA',
226					'colour' => 'fd745c'
227				}
228			},
229		}
230	},
231	'dpktsudp' => {
232		'config_attrs' => {
233			'graph_title' => 'APRS packets over UDP',
234			'graph_vlabel' => 'packets/s in (-) / out (+)',
235			'graph_args' => '--base 1000',
236			'graph_order' => 'rx tx',
237			'graph_category' => $category,
238		},
239		'sources' => {
240			'rx' => {
241				'k' => 'totals.udp_pkts_rx',
242				'attrs' => {
243					'label' => 'Packets RX',
244					'min' => 0,
245					'type' => 'DERIVE',
246					'graph' => 'no',
247					'draw' => 'AREA',
248					'colour' => '16b5ff'
249				}
250			},
251			'tx' => {
252				'k' => 'totals.udp_pkts_tx',
253				'attrs' => {
254					'label' => 'Packets',
255					'min' => 0,
256					'type' => 'DERIVE',
257					'negative' => 'rx',
258					'draw' => 'AREA',
259					'colour' => 'fd745c'
260				}
261			},
262		}
263	},
264	'ddatatcp' => {
265		'config_attrs' => {
266			'graph_title' => 'APRS data over TCP',
267			'graph_vlabel' => 'bits/s in (-) / out (+)',
268			'graph_args' => '--base 1000',
269			'graph_order' => 'rx tx',
270			'graph_category' => $category,
271		},
272		'sources' => {
273			'rx' => {
274				'k' => 'totals.tcp_bytes_rx',
275				'attrs' => {
276					'label' => 'Bits/s RX',
277					'min' => 0,
278					'type' => 'DERIVE',
279					'graph' => 'no',
280					'cdef' => 'rx,8,*',
281					'draw' => 'AREA',
282					'colour' => '16b5ff'
283				}
284			},
285			'tx' => {
286				'k' => 'totals.tcp_bytes_tx',
287				'attrs' => {
288					'label' => 'Bits/s',
289					'min' => 0,
290					'type' => 'DERIVE',
291					'negative' => 'rx',
292					'cdef' => 'tx,8,*',
293					'draw' => 'AREA',
294					'colour' => 'fd745c'
295				}
296			},
297		}
298	},
299	'ddataudp' => {
300		'config_attrs' => {
301			'graph_title' => 'APRS data over UDP',
302			'graph_vlabel' => 'bits/s in (-) / out (+)',
303			'graph_args' => '--base 1000',
304			'graph_order' => 'rx tx',
305			'graph_category' => $category,
306		},
307		'sources' => {
308			'rx' => {
309				'k' => 'totals.udp_bytes_rx',
310				'attrs' => {
311					'label' => 'Bits/s RX',
312					'min' => 0,
313					'type' => 'DERIVE',
314					'graph' => 'no',
315					'cdef' => 'rx,8,*',
316					'draw' => 'AREA',
317					'colour' => '16b5ff'
318				}
319			},
320			'tx' => {
321				'k' => 'totals.udp_bytes_tx',
322				'attrs' => {
323					'label' => 'Bits/s',
324					'min' => 0,
325					'type' => 'DERIVE',
326					'negative' => 'rx',
327					'cdef' => 'tx,8,*',
328					'draw' => 'AREA',
329					'colour' => 'fd745c'
330				}
331			},
332		}
333	},
334	'memcellu' => {
335		'config_attrs' => {
336			'graph_title' => 'Memory cells used',
337			'graph_vlabel' => 'cells',
338			'graph_args' => '--base 1024 --lower-limit 0',
339			'graph_category' => $category,
340		},
341		'keytail' => 'cells_used',
342		'sources' => {
343		}
344	},
345	'memcellub' => {
346		'config_attrs' => {
347			'graph_title' => 'Memory bytes used',
348			'graph_vlabel' => 'bytes',
349			'graph_args' => '--base 1024 --lower-limit 0',
350			'graph_category' => $category,
351		},
352		'keytail' => 'used_bytes',
353		'sources' => {
354		}
355	},
356	'memcelluba' => {
357		'config_attrs' => {
358			'graph_title' => 'Memory bytes allocated',
359			'graph_vlabel' => 'bytes',
360			'graph_args' => '--base 1024 --lower-limit 0',
361			'graph_category' => $category,
362		},
363		'keytail' => 'allocated_bytes',
364		'sources' => {
365		}
366	},
367	'clientlist' => {
368		'config_attrs' => {
369			'graph_title' => 'Clients per listener',
370			'graph_vlabel' => 'clients connected',
371			'graph_args' => '--base 1000',
372			'graph_category' => $category,
373		},
374		'keytail' => 'clients',
375		'type' => 'GAUGE',
376		'sources' => {
377		}
378	},
379	'clientx0pin' => {
380		'config_attrs' => {
381			'graph_title' => 'Packets in per listener',
382			'graph_vlabel' => 'packets/s',
383			'graph_args' => '--base 1000',
384			'graph_category' => $category,
385		},
386		'keytail' => 'pkts_rx',
387		'type' => 'DERIVE',
388		'sources' => {
389		}
390	},
391	'clientx0dout' => {
392		'config_attrs' => {
393			'graph_title' => 'Data out per listener',
394			'graph_vlabel' => 'bits/s',
395			'graph_args' => '--base 1000',
396			'graph_category' => $category,
397		},
398		'keytail' => 'bytes_tx',
399		'type' => 'DERIVE',
400		'cdef' => ',8,*',
401		'sources' => {
402		}
403	},
404	'peerinpkts' => {
405		'config_attrs' => {
406			'graph_title' => 'Packets in per peer',
407			'graph_vlabel' => 'packets/s',
408			'graph_args' => '--base 1000 --lower-limit 0',
409			'graph_category' => $category,
410		},
411		'keytail' => 'pkts_rx',
412		'type' => 'DERIVE',
413		'sources' => {
414		}
415	},
416	'rxerrstcp' => {
417		'config_attrs' => {
418			'graph_title' => 'Packets dropped from TCP clients',
419			'graph_vlabel' => 'packets/min',
420			'graph_args' => '--base 1000',
421			'graph_category' => $category,
422		},
423		'key' => 'totals.tcp_rx_errs',
424		'type' => 'DERIVE',
425		'cdef' => ',60,*',
426		'sources' => {
427		}
428	},
429	'rxerrsudp' => {
430		'config_attrs' => {
431			'graph_title' => 'Packets dropped from UDP clients',
432			'graph_vlabel' => 'packets/min',
433			'graph_args' => '--base 1000',
434			'graph_category' => $category,
435		},
436		'key' => 'totals.udp_rx_errs',
437		'type' => 'DERIVE',
438		'cdef' => ',60,*',
439		'sources' => {
440		}
441	},
442);
443
444if ($in_makelinks) {
445	my $instance = '';
446	$instance = '_' . $ARGV[1] if (defined $ARGV[1]);
447	foreach my $graph (sort keys %graphs) {
448		my $d = "aprsc" . $instance . "_" . $graph;
449		next if (-e $d);
450		symlink($0, $d) || die "Failed to symlink $0 to $d: $!\n";
451	}
452	exit(0);
453}
454
455# initialize our hammers and fetch JSON from server
456my $json = new JSON::XS;
457my $ua = LWP::UserAgent->new(timeout => 5);
458my $res = $ua->request(HTTP::Request->new('GET', $status_url));
459
460if (!$res->is_success) {
461	# todo: print result error messages
462	fail("HTTP request to aprsc failed");
463}
464
465#print $res->content;
466
467# decode and validate
468my $j = $json->decode($res->content);
469
470if (!defined $j) {
471	fail("JSON parsing of status object failed");
472}
473
474# if we're just checking if this doable, say so
475if ($in_autoconf) {
476	print "yes\n";
477	exit(0);
478}
479
480my $graph = basename($0);
481$graph =~ s/.*_//;
482
483if (!defined $graphs{$graph}) {
484	fail("No such graph available: $graph");
485}
486my $gr = $graphs{$graph};
487
488# for memcell graphs, get a dynamic list of sources
489if ($graph =~ /^memcell/) {
490	my @ord;
491	foreach my $k (sort grep(/cells_used$/, keys %{ $j->{'memory'} })) {
492		$k =~ s/_cells_used$//;
493		push @ord, $k;
494		$gr->{'sources'}->{$k} = {
495			'k' => 'memory.' . $k . '_' . $gr->{'keytail'},
496			'attrs' => {
497				'label' => $k,
498				'min' => 0,
499				'type' => 'GAUGE',
500			}
501		};
502	}
503	$gr->{'order'} = \@ord;
504
505} elsif ($graph =~ /^client(list|x)/) {
506	my @ord;
507	# convert array to hash
508	my $h = {};
509	foreach my $l (@{ $j->{'listeners'} }) {
510		my $k = sprintf("%s_%s", $l->{'proto'}, $l->{'addr'});
511		$k =~ s/[^\w]/_/g;
512		$h->{$k} = $l;
513	}
514	$j->{'listeners'} = $h;
515	my $draw = 'AREA';
516	foreach my $k (sort { $j->{'listeners'}->{$a}->{'clients'} <=> $j->{'listeners'}->{$b}->{'clients'} } keys %{ $j->{'listeners'} }) {
517		next if ($graph eq 'clientlist' && $j->{'listeners'}->{$k}->{'proto'} eq 'udp');
518		push @ord, $k;
519		$gr->{'sources'}->{$k} = {
520			'k' => 'listeners.' . $k . '.' . $gr->{'keytail'},
521			'attrs' => {
522				'label' => $j->{'listeners'}->{$k}->{'proto'} . '/' . $j->{'listeners'}->{$k}->{'addr'},
523				'min' => 0,
524				'type' => $gr->{'type'},
525				'draw' => $draw
526			}
527		};
528		if ($gr->{'cdef'}) {
529			$gr->{'sources'}->{$k}->{'attrs'}->{'cdef'} = $k . $gr->{'cdef'};
530		}
531		$draw = 'STACK';
532	}
533	$gr->{'order'} = \@ord;
534} elsif ($graph =~ /^peer/) {
535	my @ord;
536	# convert array to hash
537	my $h = {};
538	foreach my $l (@{ $j->{'peers'} }) {
539		my $k = sprintf("p%s", $l->{'addr_rem'});
540		$k =~ s/[^\w]/_/g;
541		$h->{$k} = $l;
542
543		if ($graph =~ /^peerinpkts/ && !defined $h->{'out'}) {
544			$h->{'out'} = $l;
545		}
546	}
547
548	$j->{'peers'} = $h;
549	my $draw = 'AREA';
550	foreach my $k (sort keys %{ $j->{'peers'} }) {
551		push @ord, $k;
552		my $keytail = $gr->{'keytail'};
553		my $label = ((defined $j->{'peers'}->{$k}->{'username'}) ? $j->{'peers'}->{$k}->{'username'} . ' ' : '')
554			. $j->{'peers'}->{$k}->{'addr_rem'};
555		if ($k eq 'out') {
556			$label = 'Packets out per peer';
557			$keytail = 'pkts_tx';
558		}
559		$gr->{'sources'}->{$k} = {
560			'k' => 'peers.' . $k . '.' . $keytail,
561			'attrs' => {
562				'label' => $label,
563				'min' => 0,
564				'type' => $gr->{'type'},
565				'draw' => $draw,
566				'warning' => '0.2:100',
567				'critical' => '0.05:200',
568			}
569		};
570		if ($gr->{'cdef'}) {
571			$gr->{'sources'}->{$k}->{'attrs'}->{'cdef'} = $k . $gr->{'cdef'};
572		}
573		$draw = 'STACK';
574	}
575	$gr->{'order'} = \@ord;
576	if ($#ord == -1) {
577		$gr->{'config_attrs'}->{'update'} = 'no';
578		$gr->{'config_attrs'}->{'graph'} = 'no';
579	}
580} elsif ($graph =~ /^rxerrs(tcp|udp)/) {
581	my @ord;
582	my $keys = $j->{'rx_errs'};
583	# convert array to hash
584	my $h = {};
585	my $vals = find_val($gr->{'key'}, $j);
586	my $draw = 'AREA';
587	foreach my $k (@$keys) {
588		push @ord, $k;
589		#$k =~ s/[^\w]/_/g;
590		$h->{$k} = shift @$vals;
591		$gr->{'sources'}->{$k} = {
592			'k' => 'use.' . $k,
593			'attrs' => {
594				'label' => $k,
595				'min' => 0,
596				'type' => $gr->{'type'},
597				'draw' => $draw
598			}
599		};
600		if ($gr->{'cdef'}) {
601			$gr->{'sources'}->{$k}->{'attrs'}->{'cdef'} = $k . $gr->{'cdef'};
602		}
603		$draw = 'STACK';
604	}
605	$j->{'use'} = $h;
606	$gr->{'order'} = \@ord;
607}
608
609if ($in_config) {
610	# default title from the server's ServerId
611	$title_add = $j->{'server'}->{'server_id'} if (!defined $title_add && defined $j->{'server'});
612	print_config($graph, $gr);
613	exit(0);
614}
615
616print_data($graph, $gr, $j);
617
618exit(0);
619