You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
359 lines
11 KiB
Python
359 lines
11 KiB
Python
4 years ago
|
#!/usr/bin/env python
|
||
|
|
||
|
"""
|
||
|
Munin monitoring plug-in for BIND9 DNS statistics server. Tested
|
||
|
with BIND 9.10, 9.11, and 9.12, exporting version 3.x of the XML
|
||
|
statistics.
|
||
|
|
||
|
Copyright (c) 2013-2015, Shumon Huque. All rights reserved.
|
||
|
This program is free software; you can redistribute it and/or modify
|
||
|
it under the same terms as Python itself.
|
||
|
"""
|
||
|
|
||
|
import os, sys
|
||
|
import xml.etree.ElementTree as et
|
||
|
try:
|
||
|
from urllib2 import urlopen # for Python 2
|
||
|
except ImportError:
|
||
|
from urllib.request import urlopen # for Python 3
|
||
|
|
||
|
VERSION = "0.31"
|
||
|
|
||
|
HOST = os.environ.get('HOST', "127.0.0.1")
|
||
|
PORT = os.environ.get('PORT', "8053")
|
||
|
INSTANCE = os.environ.get('INSTANCE', "")
|
||
|
SUBTITLE = os.environ.get('SUBTITLE', "")
|
||
|
|
||
|
STATS_TYPE = "xml" # will support json later
|
||
|
BINDSTATS_URL = "http://%s:%s/%s" % (HOST, PORT, STATS_TYPE)
|
||
|
|
||
|
if SUBTITLE != '':
|
||
|
SUBTITLE = ' ' + SUBTITLE
|
||
|
|
||
|
GraphCategoryName = "dns_bind"
|
||
|
|
||
|
# Note: munin displays these graphs ordered alphabetically by graph title
|
||
|
|
||
|
GraphConfig = (
|
||
|
|
||
|
('dns_opcode_in' + INSTANCE,
|
||
|
dict(title='BIND [00] Opcodes In',
|
||
|
enable=True,
|
||
|
stattype='counter',
|
||
|
args='-l 0',
|
||
|
vlabel='Queries/sec',
|
||
|
location="server/counters[@type='opcode']/counter",
|
||
|
config=dict(type='DERIVE', min=0, draw='AREASTACK'))),
|
||
|
|
||
|
('dns_qtypes_in' + INSTANCE,
|
||
|
dict(title='BIND [01] Query Types In',
|
||
|
enable=True,
|
||
|
stattype='counter',
|
||
|
args='-l 0',
|
||
|
vlabel='Queries/sec',
|
||
|
location="server/counters[@type='qtype']/counter",
|
||
|
config=dict(type='DERIVE', min=0, draw='AREASTACK'))),
|
||
|
|
||
|
('dns_server_stats' + INSTANCE,
|
||
|
dict(title='BIND [02] Server Stats',
|
||
|
enable=True,
|
||
|
stattype='counter',
|
||
|
args='-l 0',
|
||
|
vlabel='Queries/sec',
|
||
|
location="server/counters[@type='nsstat']/counter",
|
||
|
fields=("Requestv4", "Requestv6", "ReqEdns0", "ReqTCP", "ReqTSIG",
|
||
|
"Response", "TruncatedResp", "RespEDNS0", "RespTSIG",
|
||
|
"QrySuccess", "QryAuthAns", "QryNoauthAns", "QryReferral",
|
||
|
"QryNxrrset", "QrySERVFAIL", "QryFORMERR", "QryNXDOMAIN",
|
||
|
"QryRecursion", "QryDuplicate", "QryDropped", "QryFailure",
|
||
|
"XfrReqDone", "UpdateDone", "QryUDP", "QryTCP"),
|
||
|
config=dict(type='DERIVE', min=0))),
|
||
|
|
||
|
('dns_cachedb' + INSTANCE,
|
||
|
dict(title='BIND [03] CacheDB RRsets',
|
||
|
enable=True,
|
||
|
stattype='cachedb',
|
||
|
args='-l 0',
|
||
|
vlabel='Count',
|
||
|
location="views/view[@name='_default']/cache[@name='_default']/rrset",
|
||
|
config=dict(type='GAUGE', min=0))),
|
||
|
|
||
|
('dns_resolver_stats' + INSTANCE,
|
||
|
dict(title='BIND [04] Resolver Stats',
|
||
|
enable=False, # appears to be empty
|
||
|
stattype='counter',
|
||
|
args='-l 0',
|
||
|
vlabel='Count/sec',
|
||
|
location="server/counters[@type='resstat']/counter",
|
||
|
config=dict(type='DERIVE', min=0))),
|
||
|
|
||
|
('dns_resolver_stats_qtype' + INSTANCE,
|
||
|
dict(title='BIND [05] Resolver Outgoing Queries',
|
||
|
enable=True,
|
||
|
stattype='counter',
|
||
|
args='-l 0',
|
||
|
vlabel='Count/sec',
|
||
|
location="views/view[@name='_default']/counters[@type='resqtype']/counter",
|
||
|
config=dict(type='DERIVE', min=0))),
|
||
|
|
||
|
('dns_resolver_stats_view' + INSTANCE,
|
||
|
dict(title='BIND [06] Resolver Stats',
|
||
|
enable=True,
|
||
|
stattype='counter',
|
||
|
args='-l 0',
|
||
|
vlabel='Count/sec',
|
||
|
location="views/view[@name='_default']/counters[@type='resstats']/counter",
|
||
|
config=dict(type='DERIVE', min=0))),
|
||
|
|
||
|
('dns_cachestats' + INSTANCE,
|
||
|
dict(title='BIND [07] Resolver Cache Stats',
|
||
|
enable=True,
|
||
|
stattype='counter',
|
||
|
args='-l 0',
|
||
|
vlabel='Count/sec',
|
||
|
location="views/view[@name='_default']/counters[@type='cachestats']/counter",
|
||
|
fields=("CacheHits", "CacheMisses", "QueryHits", "QueryMisses",
|
||
|
"DeleteLRU", "DeleteTTL"),
|
||
|
config=dict(type='DERIVE', min=0))),
|
||
|
|
||
|
('dns_cache_mem' + INSTANCE,
|
||
|
dict(title='BIND [08] Resolver Cache Memory Stats',
|
||
|
enable=True,
|
||
|
stattype='counter',
|
||
|
args='-l 0 --base 1024',
|
||
|
vlabel='Memory In-Use',
|
||
|
location="views/view[@name='_default']/counters[@type='cachestats']/counter",
|
||
|
fields=("TreeMemInUse", "HeapMemInUse"),
|
||
|
config=dict(type='GAUGE', min=0))),
|
||
|
|
||
|
('dns_socket_activity' + INSTANCE,
|
||
|
dict(title='BIND [09] Socket Activity',
|
||
|
enable=True,
|
||
|
stattype='counter',
|
||
|
args='-l 0',
|
||
|
vlabel='Active',
|
||
|
location="server/counters[@type='sockstat']/counter",
|
||
|
fields=("UDP4Active", "UDP6Active",
|
||
|
"TCP4Active", "TCP6Active",
|
||
|
"UnixActive", "RawActive"),
|
||
|
config=dict(type='GAUGE', min=0))),
|
||
|
|
||
|
('dns_socket_stats' + INSTANCE,
|
||
|
dict(title='BIND [10] Socket Rates',
|
||
|
enable=True,
|
||
|
stattype='counter',
|
||
|
args='-l 0',
|
||
|
vlabel='Count/sec',
|
||
|
location="server/counters[@type='sockstat']/counter",
|
||
|
fields=("UDP4Open", "UDP6Open",
|
||
|
"TCP4Open", "TCP6Open",
|
||
|
"UDP4OpenFail", "UDP6OpenFail",
|
||
|
"TCP4OpenFail", "TCP6OpenFail",
|
||
|
"UDP4Close", "UDP6Close",
|
||
|
"TCP4Close", "TCP6Close",
|
||
|
"UDP4BindFail", "UDP6BindFail",
|
||
|
"TCP4BindFail", "TCP6BindFail",
|
||
|
"UDP4ConnFail", "UDP6ConnFail",
|
||
|
"TCP4ConnFail", "TCP6ConnFail",
|
||
|
"UDP4Conn", "UDP6Conn",
|
||
|
"TCP4Conn", "TCP6Conn",
|
||
|
"TCP4AcceptFail", "TCP6AcceptFail",
|
||
|
"TCP4Accept", "TCP6Accept",
|
||
|
"UDP4SendErr", "UDP6SendErr",
|
||
|
"TCP4SendErr", "TCP6SendErr",
|
||
|
"UDP4RecvErr", "UDP6RecvErr",
|
||
|
"TCP4RecvErr", "TCP6RecvErr"),
|
||
|
config=dict(type='DERIVE', min=0))),
|
||
|
|
||
|
('dns_zone_stats' + INSTANCE,
|
||
|
dict(title='BIND [11] Zone Maintenance',
|
||
|
enable=False,
|
||
|
stattype='counter',
|
||
|
args='-l 0',
|
||
|
vlabel='Count/sec',
|
||
|
location="server/counters[@type='zonestat']/counter",
|
||
|
config=dict(type='DERIVE', min=0))),
|
||
|
|
||
|
('dns_memory_usage' + INSTANCE,
|
||
|
dict(title='BIND [12] Memory Usage',
|
||
|
enable=True,
|
||
|
stattype='memory',
|
||
|
args='-l 0 --base 1024',
|
||
|
vlabel='Memory In-Use',
|
||
|
location='memory/summary',
|
||
|
fields=("ContextSize", "BlockSize", "Lost", "InUse"),
|
||
|
config=dict(type='GAUGE', min=0))),
|
||
|
|
||
|
('dns_adbstat' + INSTANCE,
|
||
|
dict(title='BIND [13] adbstat',
|
||
|
enable=True,
|
||
|
stattype='counter',
|
||
|
args='-l 0',
|
||
|
vlabel='Count',
|
||
|
location="views/view[@name='_default']/counters[@type='adbstat']/counter",
|
||
|
config=dict(type='GAUGE', min=0))),
|
||
|
|
||
|
)
|
||
|
|
||
|
|
||
|
def unsetenvproxy():
|
||
|
"""Unset HTTP Proxy environment variables that might interfere"""
|
||
|
for proxyvar in [ 'http_proxy', 'HTTP_PROXY' ]:
|
||
|
os.unsetenv(proxyvar)
|
||
|
return
|
||
|
|
||
|
|
||
|
def getstatsversion(etree):
|
||
|
"""return version of BIND statistics"""
|
||
|
return etree.attrib['version']
|
||
|
|
||
|
|
||
|
def getdata(graph, etree, getvals=False):
|
||
|
|
||
|
stattype = graph[1]['stattype']
|
||
|
location = graph[1]['location']
|
||
|
|
||
|
if stattype == 'memory':
|
||
|
return getdata_memory(graph, etree, getvals)
|
||
|
elif stattype == 'cachedb':
|
||
|
return getdata_cachedb(graph, etree, getvals)
|
||
|
|
||
|
results = []
|
||
|
counters = etree.findall(location)
|
||
|
|
||
|
if counters is None: # empty result
|
||
|
return results
|
||
|
|
||
|
for c in counters:
|
||
|
key = c.attrib['name']
|
||
|
val = c.text
|
||
|
if getvals:
|
||
|
results.append((key, val))
|
||
|
else:
|
||
|
results.append(key)
|
||
|
return results
|
||
|
|
||
|
|
||
|
def getdata_memory(graph, etree, getvals=False):
|
||
|
|
||
|
location = graph[1]['location']
|
||
|
|
||
|
results = []
|
||
|
counters = etree.find(location)
|
||
|
|
||
|
if counters is None: # empty result
|
||
|
return results
|
||
|
|
||
|
for c in counters:
|
||
|
key = c.tag
|
||
|
val = c.text
|
||
|
if getvals:
|
||
|
results.append((key, val))
|
||
|
else:
|
||
|
results.append(key)
|
||
|
return results
|
||
|
|
||
|
|
||
|
def getdata_cachedb(graph, etree, getvals=False):
|
||
|
|
||
|
location = graph[1]['location']
|
||
|
|
||
|
results = []
|
||
|
counters = etree.findall(location)
|
||
|
|
||
|
if counters is None: # empty result
|
||
|
return results
|
||
|
|
||
|
for c in counters:
|
||
|
key = c.find('name').text
|
||
|
val = c.find('counter').text
|
||
|
if getvals:
|
||
|
results.append((key, val))
|
||
|
else:
|
||
|
results.append(key)
|
||
|
return results
|
||
|
|
||
|
|
||
|
def validkey(graph, key):
|
||
|
fieldlist = graph[1].get('fields', None)
|
||
|
if fieldlist and (key not in fieldlist):
|
||
|
return False
|
||
|
else:
|
||
|
return True
|
||
|
|
||
|
|
||
|
def get_etree_root(url):
|
||
|
"""Return the root of an ElementTree structure populated by
|
||
|
parsing BIND9 statistics obtained at the given URL"""
|
||
|
|
||
|
data = urlopen(url)
|
||
|
return et.parse(data).getroot()
|
||
|
|
||
|
|
||
|
def muninconfig(etree):
|
||
|
"""Generate munin config for the BIND stats plugin"""
|
||
|
|
||
|
for g in GraphConfig:
|
||
|
if not g[1]['enable']:
|
||
|
continue
|
||
|
print("multigraph %s" % g[0])
|
||
|
print("graph_title %s" % g[1]['title'] + SUBTITLE)
|
||
|
print("graph_args %s" % g[1]['args'])
|
||
|
print("graph_vlabel %s" % g[1]['vlabel'])
|
||
|
print("graph_category %s" % GraphCategoryName)
|
||
|
|
||
|
data = getdata(g, etree, getvals=False)
|
||
|
if data != None:
|
||
|
for key in data:
|
||
|
if validkey(g, key):
|
||
|
print("%s.label %s" % (key, key))
|
||
|
if 'draw' in g[1]['config']:
|
||
|
print("%s.draw %s" % (key, g[1]['config']['draw']))
|
||
|
print("%s.min %s" % (key, g[1]['config']['min']))
|
||
|
print("%s.type %s" % (key, g[1]['config']['type']))
|
||
|
print('')
|
||
|
|
||
|
|
||
|
def munindata(etree):
|
||
|
"""Generate munin data for the BIND stats plugin"""
|
||
|
|
||
|
for g in GraphConfig:
|
||
|
if not g[1]['enable']:
|
||
|
continue
|
||
|
print("multigraph %s" % g[0])
|
||
|
data = getdata(g, etree, getvals=True)
|
||
|
if data != None:
|
||
|
for (key, value) in data:
|
||
|
if validkey(g, key):
|
||
|
print("%s.value %s" % (key, value))
|
||
|
print('')
|
||
|
|
||
|
|
||
|
def usage():
|
||
|
"""Print plugin usage"""
|
||
|
print("""\
|
||
|
\nUsage: bind9stats.py [config|statsversion]\n""")
|
||
|
sys.exit(1)
|
||
|
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
|
||
|
tree = get_etree_root(BINDSTATS_URL)
|
||
|
|
||
|
args = sys.argv[1:]
|
||
|
argslen = len(args)
|
||
|
unsetenvproxy()
|
||
|
|
||
|
if argslen == 0:
|
||
|
munindata(tree)
|
||
|
elif argslen == 1:
|
||
|
if args[0] == "config":
|
||
|
muninconfig(tree)
|
||
|
elif args[0] == "statsversion":
|
||
|
print("bind9stats %s version %s" % (STATS_TYPE, getstatsversion(tree)))
|
||
|
else:
|
||
|
usage()
|
||
|
else:
|
||
|
usage()
|
||
|
|