1#!/usr/local/bin/perl -w 2#%# family=auto 3#%# capabilities=autoconf 4# 5# Copyright (c) 2013 Philip Paeps 6# All rights reserved. 7# 8# Redistribution and use in source and binary forms, with or without 9# modification, are permitted provided that the following conditions 10# are met: 11# 1. Redistributions of source code must retain the above copyright 12# notice, this list of conditions and the following disclaimer 13# in this position and unchanged. 14# 2. Redistributions in binary form must reproduce the above copyright 15# notice, this list of conditions and the following disclaimer in the 16# documentation and/or other materials provided with the distribution. 17# 18# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 19# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 22# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 24# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 25# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 26# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 27# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 28# SUCH DAMAGE. 29# 30 31# 32# Plugin to monitor NSD performance. 33# Contributed by: Philip Paeps <philip@paeps.cx>. 34# Heavily inspired by the unbound_munin_ script by 35# W.C.A. Wijgaards and the nsd3 script by J.T.Sage. 36# 37# Usage: nsd_FUNCTION 38# 39# Available functions: 40# by_type - incoming queries by type 41# by_rcode - answers by rcode 42# hits - base volume 43# 44# Example configuration: 45# 46# [nsd_*] 47# user bind 48# env.logdir /var/log 49# env.logfile nsd.log 50# 51# [nsd_by_type] 52# env.stats = A=A AAAA=AAAA MX=MX PTR=PTR TYPE252=AXFR SOA=SOA 53# 54# The stats line is an optional space-separated string of VALUE=Caption 55# pairs of query types to pick out of the NSTATS returned by NSD. Replace 56# spaces in a caption value by _. 57# 58use strict; 59use Munin::Plugin; 60use POSIX; 61 62my $LOGDIR = $ENV{'logdir'} || '/var/log'; 63my $LOGFILE = $ENV{'logfile'} || 'nsd.log'; 64my $logfile = "$LOGDIR/$LOGFILE"; 65my $pidfile = $ENV{'pidfile'} || '/var/run/nsd/nsd.pid'; 66 67sub signal_nsd { 68 open(my $P, "< $pidfile") || die "Couldn't open pidfile: $!\n"; 69 my $pid = <$P>; 70 close($P); 71 72 chomp $pid; 73 kill USR1 => $pid or die "Couldn't signal NSD: $!\n"; 74 75 # Give the daemon a chance to write statistics. 76 sleep(0.5); 77} 78 79sub read_logfile($) { 80 my $stats = shift; 81 my $pos = undef; 82 83 ($pos) = restore_state(); 84 $pos = 0 unless defined($pos); 85 my ($L, $rotated) = tail_open($logfile, $pos); 86 87 my $last = 0; 88 while (<$L>) { 89 chomp $_; 90 if (/([NX]STATS) (\d+)/) { 91 $last = $2; 92 push @$stats, { 93 'type' => $1, 'when' => $2, 94 'values' => { $_ =~ m/(\S+)=(\d+)/g }, 95 }; 96 } 97 } 98 $pos = tail_close($L); 99 save_state($pos); 100 101 return $last; 102} 103 104sub get_values($) { 105 my $values = shift; 106 107 my @stats = (); 108 my $last = read_logfile(\@stats); 109 110 # If the values found in the logfile are older than three minutes, 111 # signal NSD to print fresh ones, and read them again. 112 my $now = time; 113 if ($now - $last > 3 * 60) { 114 signal_nsd(); 115 read_logfile(\@stats); 116 } 117 118 # @stats is an array of the last lines found in the logfile. Hopefully, 119 # the last two lines will be NSTATS and XSTATS. If not, we need to keep 120 # looking. 121 foreach (keys %$values) { 122 foreach my $stat (reverse(@stats)) { 123 $values->{$_} = $stat->{'values'}{$_} if ($stat->{'values'}{$_}); 124 } 125 } 126 127 return \@stats; 128} 129 130# Base volume 131sub hits($) { 132 my $function = shift; 133 my %captions = (); 134 135 %captions = ( 136 'RR' => 'UDP queries dropped', 137 'SAns' => 'UDP queries answered', 138 'RTCP' => 'TCP connections', 139 'SErr' => 'Transmit errors', 140 ); 141 142 # Print graph configuration. 143 if ($function and $function eq "config") { 144 print "graph_title NSD DNS traffic\n"; 145 print "graph_args --base 1000 -l 0\n"; 146 print "graph_vlabel queries / second\n"; 147 print "graph_category dns\n"; 148 foreach my $caption (keys %captions) { 149 my $label = $captions{$caption}; 150 $label =~ s/_/ /; 151 print lc($caption) . ".label $label\n"; 152 print lc($caption) . ".type DERIVE\n"; 153 print lc($caption) . ".min 0\n"; 154 } 155 print "graph_info Base volume\n"; 156 exit 0; 157 } 158 159 my %values = map { $_ => 0 } keys %captions; 160 get_values(\%values); 161 foreach (keys %values) { 162 print lc($_) . ".value $values{$_}\n"; 163 } 164 165 exit 0; 166} 167 168# DNS queries by type 169sub by_type($$) { 170 my ($function, $stats) = @_; 171 172 # Sensible default set of statistics. 173 if (! $stats) { 174 $stats = "A=A AAAA=AAAA MX=MX PTR=PTR TYPE252=AXFR SOA=SOA " . 175 "TXT=TXT DNSKEY=DNSKEY NS=NS SPF=SPF"; 176 } 177 178 my %captions = map { split /=/ } split / /, $stats; 179 180 # Print graph configuration. 181 if ($function and $function eq "config") { 182 print "graph_title NSD DNS queries by type\n"; 183 print "graph_args --base 1000 -l 0\n"; 184 print "graph_vlabel queries / second\n"; 185 print "graph_category dns\n"; 186 foreach my $caption (keys %captions) { 187 my $label = $captions{$caption}; 188 $label =~ s/_/ /; 189 print lc($caption) . ".label $label\n"; 190 print lc($caption) . ".type DERIVE\n"; 191 print lc($caption) . ".min 0\n"; 192 } 193 print "graph_info Queries by DNS RR type requested\n"; 194 exit 0; 195 } 196 197 my %values = map { $_ => 0 } keys %captions; 198 get_values(\%values); 199 foreach (keys %values) { 200 print lc($_) . ".value $values{$_}\n"; 201 } 202 203 exit 0; 204} 205 206# Answers by rcode 207sub by_rcode($) { 208 my ($function) = shift; 209 my %captions = (); 210 211 %captions = ( 212 'SAns' => 'NOERROR', 213 'SFail' => 'SERVFAIL', 214 'SFErr' => 'FORMERR', 215 'SNXD' => 'NXDOMAIN', 216 ); 217 218 # Print graph configuration. 219 if ($function and $function eq "config") { 220 print "graph_title NSD DNS answers by rcode\n"; 221 print "graph_args --base 1000 -l 0\n"; 222 print "graph_vlabel answers / second\n"; 223 print "graph_category dns\n"; 224 foreach my $caption (keys %captions) { 225 my $label = $captions{$caption}; 226 $label =~ s/_/ /; 227 print lc($caption) . ".label $label\n"; 228 print lc($caption) . ".type DERIVE\n"; 229 print lc($caption) . ".min 0\n"; 230 } 231 print "graph_info Answers by rcode returned\n"; 232 exit 0; 233 } 234 235 my %values = map { $_ => 0 } keys %captions; 236 get_values(\%values); 237 foreach (keys %values) { 238 # SAns is the total number of answers sent, regardless of rcode. 239 # Subtracting SERVFAIL, FORMERR and NXDOMAIN leaves IMPL and 240 # REFUSED. NSD will never send REFUSED, which leaves IMPL - so, 241 # probably correct enough! 242 if ($_ eq "SAns") { 243 $values{$_} -= $values{'SFErr'}; 244 $values{$_} -= $values{'SFail'}; 245 $values{$_} -= $values{'SNXD'}; 246 } 247 print lc($_) . ".value $values{$_}\n"; 248 } 249 250 exit 0; 251} 252 253# Make sure we can access the pidfile and the logfile 254# and that we can send a signal to NSD to print stats. 255# Note that autoconf should exit 0 even if it fails. 256sub autoconf { 257 open(my $L, "< $logfile") || print "no ($logfile: $!)\n"; 258 open(my $P, "< $pidfile") || print "no ($pidfile: $!)\n"; 259 exit 0 unless defined($L) and defined($P); 260 261 eval { signal_nsd() }; 262 if ($@) { 263 chomp $@; 264 warn "no ($@)\n"; 265 exit 0; 266 } 267 268 print "yes\n"; 269 exit 0; 270} 271 272if ($ARGV[0] and $ARGV[0] eq "autoconf") { 273 autoconf(); 274} 275 276if ($Munin::Plugin::me =~ /_by_type$/) { 277 by_type($ARGV[0], $ENV{'stats'}); 278} elsif ($Munin::Plugin::me =~ /_by_rcode$/) { 279 by_rcode($ARGV[0]); 280} elsif ($Munin::Plugin::me =~ /_hits$/) { 281 hits($ARGV[0]); 282} 283 284exit 0; 285