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