Révision 6f28e159
true autoconf, a few extra configuration options (extensible use)
| plugins/other/nsd3 | ||
|---|---|---|
| 1 | 1 |
#!/usr/bin/python |
| 2 | 2 |
#%# family=auto |
| 3 |
#%# capabilities=suggest autoconf
|
|
| 3 |
#%# capabilities=autoconf |
|
| 4 | 4 |
|
| 5 | 5 |
|
| 6 | 6 |
""" |
| 7 |
Munin plugin to monitor NSD3 statistics |
|
| 8 |
J.T.Sage <jtsage@gmail.com>, 2010/06/10 |
|
| 9 |
|
|
| 10 |
To use: |
|
| 11 |
ln -s path_to_this_script /etc/munin/plugins/nsd |
|
| 12 |
|
|
| 13 |
Configuration |
|
| 14 |
[nsd] |
|
| 15 |
user nsd |
|
| 16 |
env.statsfile /var/log/nsd.log |
|
| 17 |
env.pidfile /var/run/nsd3/nsd.pid |
|
| 18 |
|
|
| 19 |
Notes: |
|
| 20 |
Note that the plugin has a built in half second pause for the lastest NSD3 |
|
| 21 |
statistics to write. This is done via a SIGUSR1 signal to the process. |
|
| 22 |
If your system is sufficiently loaded to the point that half a second |
|
| 23 |
is not enough time for the file to be written to, please update that |
|
| 24 |
number in the source. It should be more than long enough for most |
|
| 25 |
users. |
|
| 7 |
=head1 NAME |
|
| 8 |
|
|
| 9 |
nsd - Munin plugin to monitor the number of queries a running process |
|
| 10 |
of nsd3 has recievied. |
|
| 11 |
|
|
| 12 |
=head1 APPLICABLE SYSTEMS |
|
| 13 |
|
|
| 14 |
Linux or *nix system with a logging installtion of NSD v3 installed. |
|
| 15 |
(http://nlnetlabs.nl/projects/nsd/) |
|
| 16 |
|
|
| 17 |
=head1 CONFIGURATION |
|
| 18 |
|
|
| 19 |
The plugin needs access to the nsd logfile and the nsd pid file to |
|
| 20 |
force the running nsd process to write the current statistics. |
|
| 21 |
|
|
| 22 |
Tip: To see if it's already set up correctly, just run this plugin |
|
| 23 |
with the paramater "autoconf". If you get a "yes", everything should |
|
| 24 |
work like a charm already. |
|
| 25 |
|
|
| 26 |
This configuration section shows the defaults of the plugin: |
|
| 27 |
|
|
| 28 |
The stats line is a set of space-seperated values that you wish to |
|
| 29 |
retrieve from NSD. The format is VALUE=Caption. For spaces in a |
|
| 30 |
caption value, replace them with an underscore (_). |
|
| 31 |
|
|
| 32 |
[nsd] |
|
| 33 |
env.statsfile /var/log/nsd.log |
|
| 34 |
env.pidfile /var/run/nsd3/nsd.pid |
|
| 35 |
env.stats "A=A AAAA=AAAA MX=MX PTR=PTR TYPE252=AXFR SNXD=NXDOMAIN RQ=Total_Successful" |
|
| 36 |
|
|
| 37 |
If you need to set a user for the logfile to be readable, and most |
|
| 38 |
importantly, the process to recieve the signal, you may specify it. |
|
| 39 |
For example: |
|
| 40 |
|
|
| 41 |
[nsd] |
|
| 42 |
user nsd |
|
| 43 |
|
|
| 44 |
=head1 INTERPRETATION |
|
| 45 |
|
|
| 46 |
The plugin shows the number of queries that nsd has recieved, |
|
| 47 |
averaged over a period to gain the number of queries per second. |
|
| 48 |
For most servers, these values will be very low. In the event |
|
| 49 |
of a misconfiguration, the plugin will return undefined values. |
|
| 50 |
|
|
| 51 |
=head1 MAGIC MARKERS |
|
| 52 |
|
|
| 53 |
#%# family=auto |
|
| 54 |
#%# capabilities=autoconf |
|
| 55 |
|
|
| 56 |
=head1 VERSION |
|
| 57 |
|
|
| 58 |
v1.0.1 |
|
| 59 |
|
|
| 60 |
=head1 AUTHOR |
|
| 61 |
|
|
| 62 |
J.T.Sage <jtsage@gmail.com> |
|
| 63 |
|
|
| 64 |
=head1 LICENSE |
|
| 65 |
|
|
| 66 |
GPLv2 |
|
| 67 |
|
|
| 68 |
=cut |
|
| 26 | 69 |
""" |
| 27 | 70 |
|
| 28 | 71 |
import os |
| ... | ... | |
| 30 | 73 |
import subprocess |
| 31 | 74 |
import time |
| 32 | 75 |
import re |
| 76 |
import signal |
|
| 33 | 77 |
|
| 34 | 78 |
STATS_FILE = os.environ.get('statsfile', '/var/log/nsd.log')
|
| 35 | 79 |
PID_FILE = os.environ.get('pidfile', '/var/run/nsd3/nsd.pid')
|
| 80 |
STATS_STRING = os.environ.get('stats', "A=A AAAA=AAAA MX=MX PTR=PTR TYPE252=AXFR SNXD=NXDOMAIN RQ=Total_Succesful")
|
|
| 81 |
|
|
| 82 |
BOTH_LISTS = STATS_STRING.split() |
|
| 36 | 83 |
|
| 37 | 84 |
def print_config(): |
| 38 |
"""Generates and prints a munin config for a given chart.""" |
|
| 39 |
|
|
| 40 |
print "graph_title NSD3 Queries" |
|
| 41 |
print "graph_vlabel qps" |
|
| 42 |
print "graph_category network" |
|
| 43 |
print "graph_info Queries per second" |
|
| 44 |
print "a.label A" |
|
| 45 |
print "a.type DERIVE" |
|
| 46 |
print "a.min 0" |
|
| 47 |
print "aaaa.label AAAA" |
|
| 48 |
print "aaaa.type DERIVE" |
|
| 49 |
print "aaaa.min 0" |
|
| 50 |
print "ptr.label PTR" |
|
| 51 |
print "ptr.type DERIVE" |
|
| 52 |
print "ptr.min 0" |
|
| 53 |
print "mx.label MX" |
|
| 54 |
print "mx.type DERIVE" |
|
| 55 |
print "mx.min 0" |
|
| 56 |
print "type252.label AXFR" |
|
| 57 |
print "type252.type DERIVE" |
|
| 58 |
print "type252.min 0" |
|
| 59 |
print "snxd.label NXDOMAIN" |
|
| 60 |
print "snxd.type DERIVE" |
|
| 61 |
print "snxd.min 0" |
|
| 62 |
print "rq.label Total Successful" |
|
| 63 |
print "rq.type DERIVE" |
|
| 64 |
print "rq.min 0" |
|
| 65 |
|
|
| 85 |
"""Generates and prints a munin config for a given chart.""" |
|
| 86 |
|
|
| 87 |
print "graph_title NSD3 Queries" |
|
| 88 |
print "graph_vlabel qps" |
|
| 89 |
print "graph_category network" |
|
| 90 |
print "graph_info Queries per second" |
|
| 91 |
for x in BOTH_LISTS: |
|
| 92 |
val = x.split('=')
|
|
| 93 |
name = val[0].lower() |
|
| 94 |
label = val[1].replace('_', ' ')
|
|
| 95 |
print name + '.label ' + label |
|
| 96 |
print name + '.type DERIVE' |
|
| 97 |
print name + '.min 0' |
|
| 98 |
|
|
| 99 |
sys.exit(0) |
|
| 66 | 100 |
|
| 67 | 101 |
def print_values(): |
| 68 |
"""Gets NSD's latest stats.""" |
|
| 69 |
|
|
| 70 |
pidf = open(PID_FILE, 'r') |
|
| 71 |
pidn = pidf.read() |
|
| 72 |
pidf.close(); |
|
| 73 |
statscmd = ['kill', '-SIGUSR1'] |
|
| 74 |
statscmd.append(pidn.strip()) |
|
| 75 |
dropstats = subprocess.call(statscmd) |
|
| 76 |
|
|
| 77 |
time.sleep(.5) # Wait for the log to write. |
|
| 78 |
statf = open(STATS_FILE, 'r') |
|
| 79 |
stats = tail(statf, 10) |
|
| 80 |
|
|
| 81 |
nstats = [] |
|
| 82 |
xstats = [] |
|
| 83 |
|
|
| 84 |
for line in stats: |
|
| 85 |
if "XSTATS" in line: |
|
| 86 |
xstats.append(line.strip()) |
|
| 87 |
if "NSTATS" in line: |
|
| 88 |
nstats.append(line.strip()) |
|
| 89 |
|
|
| 90 |
matches = re.compile(' A=(\d+)').findall(nstats[-1])
|
|
| 91 |
if matches == []: |
|
| 92 |
print "a.value 0" |
|
| 93 |
else: |
|
| 94 |
print "a.value " + matches[0] |
|
| 95 |
|
|
| 96 |
matches = re.compile(' AAAA=(\d+)').findall(nstats[-1])
|
|
| 97 |
if matches == []: |
|
| 98 |
print "aaaa.value 0" |
|
| 99 |
else: |
|
| 100 |
print "aaaa.value " + matches[0] |
|
| 101 |
|
|
| 102 |
matches = re.compile(' PTR=(\d+)').findall(nstats[-1])
|
|
| 103 |
if matches == []: |
|
| 104 |
print "ptr.value 0" |
|
| 105 |
else: |
|
| 106 |
print "ptr.value " + matches[0] |
|
| 107 |
|
|
| 108 |
|
|
| 109 |
matches = re.compile(' MX=(\d+)').findall(nstats[-1])
|
|
| 110 |
if matches == []: |
|
| 111 |
print "mx.value 0" |
|
| 112 |
else: |
|
| 113 |
print "mx.value " + matches[0] |
|
| 114 |
|
|
| 115 |
matches = re.compile(' TYPE252=(\d+)').findall(nstats[-1])
|
|
| 116 |
if matches == []: |
|
| 117 |
print "type252.value 0" |
|
| 118 |
else: |
|
| 119 |
print "type252.value " + matches[0] |
|
| 120 |
|
|
| 121 |
matches = re.compile(' SNXD=(\d+) ').findall(xstats[-1])
|
|
| 122 |
if matches == []: |
|
| 123 |
print "snxd.value 0" |
|
| 124 |
else: |
|
| 125 |
print "snxd.value " + matches[0] |
|
| 126 |
|
|
| 127 |
matches = re.compile(' RQ=(\d+) ').findall(xstats[-1])
|
|
| 128 |
if matches == []: |
|
| 129 |
print "rq.value 0" |
|
| 130 |
else: |
|
| 131 |
print "rq.value " + matches[0] |
|
| 102 |
"""Gets NSD's latest stats.""" |
|
| 103 |
|
|
| 104 |
bigfail = False |
|
| 105 |
if ( not os.access(STATS_FILE, os.R_OK) ) : |
|
| 106 |
bigfail = True |
|
| 107 |
if ( not os.access(PID_FILE, os.R_OK) ) : |
|
| 108 |
bigfail = True |
|
| 109 |
|
|
| 110 |
if ( not bigfail ): |
|
| 111 |
pidf = open(PID_FILE, 'r') |
|
| 112 |
pidn = pidf.read() |
|
| 113 |
pidf.close(); |
|
| 114 |
try: |
|
| 115 |
os.kill(int(pidn.strip()), signal.SIGUSR1) |
|
| 116 |
except OSError: |
|
| 117 |
bigfail = True |
|
| 118 |
|
|
| 119 |
|
|
| 120 |
time.sleep(.5) # Wait for the log to write. |
|
| 121 |
|
|
| 122 |
if ( not bigfail ): |
|
| 123 |
statf = open(STATS_FILE, 'r') |
|
| 124 |
stats = tail(statf, 10) |
|
| 125 |
|
|
| 126 |
nstats = [] |
|
| 127 |
xstats = [] |
|
| 128 |
|
|
| 129 |
for line in stats: |
|
| 130 |
if "XSTATS" in line: |
|
| 131 |
xstats.append(line.strip()) |
|
| 132 |
if "NSTATS" in line: |
|
| 133 |
nstats.append(line.strip()) |
|
| 134 |
|
|
| 135 |
statsline = nstats[-1] + xstats[-1] |
|
| 136 |
else: |
|
| 137 |
statsline = " " |
|
| 138 |
|
|
| 139 |
relist = [] |
|
| 140 |
for x in BOTH_LISTS: |
|
| 141 |
val = x.split('=')
|
|
| 142 |
name = val[0].lower() |
|
| 143 |
rxp = val[0] |
|
| 144 |
relist.append([name, rxp]) |
|
| 145 |
|
|
| 146 |
for point in relist: |
|
| 147 |
matches = re.compile(' '+point[1]+'=(\d+)').findall(statsline)
|
|
| 148 |
if bigfail: |
|
| 149 |
print point[0]+'.value U' |
|
| 150 |
elif matches == []: |
|
| 151 |
print point[0]+'.value 0' |
|
| 152 |
else: |
|
| 153 |
print point[0]+'.value '+matches[0] |
|
| 132 | 154 |
|
| 133 | 155 |
def tail( f, window=20 ): |
| 134 |
f.seek( 0, 2 )
|
|
| 135 |
bytes= f.tell()
|
|
| 136 |
size= window
|
|
| 137 |
block= -1
|
|
| 138 |
while size > 0 and bytes+block*1024 > 0:
|
|
| 139 |
f.seek( block*1024, 2 ) # from the end!
|
|
| 140 |
data= f.read( 1024 )
|
|
| 141 |
linesFound= data.count('\n')
|
|
| 142 |
size -= linesFound
|
|
| 143 |
block -= 1
|
|
| 144 |
f.seek( block*1024, 2 )
|
|
| 145 |
f.readline() # find a newline
|
|
| 146 |
lastBlocks= list( f.readlines() )
|
|
| 147 |
return lastBlocks[-window:]
|
|
| 156 |
f.seek( 0, 2 )
|
|
| 157 |
bytes = f.tell()
|
|
| 158 |
size = window
|
|
| 159 |
block = -1
|
|
| 160 |
while size > 0 and bytes+block*1024 > 0:
|
|
| 161 |
f.seek( block*1024, 2 ) # from the end!
|
|
| 162 |
data = f.read( 1024 )
|
|
| 163 |
linesFound = data.count('\n')
|
|
| 164 |
size -= linesFound
|
|
| 165 |
block -= 1
|
|
| 166 |
f.seek( block*1024, 2 )
|
|
| 167 |
f.readline() # find a newline
|
|
| 168 |
lastBlocks = list( f.readlines() )
|
|
| 169 |
return lastBlocks[-window:]
|
|
| 148 | 170 |
|
| 149 | 171 |
if __name__ == '__main__': |
| 150 |
if len(sys.argv) > 1: |
|
| 151 |
if sys.argv[1] == 'autoconf': |
|
| 152 |
print 'yes' |
|
| 153 |
sys.exit(0) |
|
| 154 |
|
|
| 155 |
if len(sys.argv) > 1 and sys.argv[1] == 'config': |
|
| 156 |
print_config() |
|
| 157 |
sys.exit(0) |
|
| 158 |
|
|
| 159 |
print_values() |
|
| 172 |
if len(sys.argv) > 1: |
|
| 173 |
if sys.argv[1] == 'autoconf': |
|
| 174 |
if ( not os.path.isfile(STATS_FILE) ) : |
|
| 175 |
print 'no (Log file not found)' |
|
| 176 |
elif ( not os.path.isfile(PID_FILE) ) : |
|
| 177 |
print 'no (PID file not found)' |
|
| 178 |
elif ( not os.access(STATS_FILE, os.R_OK) ) : |
|
| 179 |
print 'no (Log file exists, access denied for read)' |
|
| 180 |
elif ( not os.access(PID_FILE, os.R_OK) ) : |
|
| 181 |
print 'no (PID file exists, access denied for read)' |
|
| 182 |
else: |
|
| 183 |
pidf = open(PID_FILE, 'r') |
|
| 184 |
pidn = pidf.read() |
|
| 185 |
pidf.close(); |
|
| 186 |
try: |
|
| 187 |
os.kill(int(pidn.strip()), signal.SIGUSR1) |
|
| 188 |
except OSError as (errno, strerror): |
|
| 189 |
print 'no (Unable to signal process :: '+strerror+')' |
|
| 190 |
sys.exit(0) |
|
| 191 |
print 'yes' |
|
| 192 |
sys.exit(0) |
|
| 193 |
|
|
| 194 |
if len(sys.argv) > 1 and sys.argv[1] == 'config': |
|
| 195 |
print_config() |
|
| 196 |
sys.exit(0) |
|
| 197 |
|
|
| 198 |
print_values() |
|
Formats disponibles : Unified diff