Skip to content

Commit 77490c0

Browse files
committed
Merge pull request ganglia#139 from cburroughs/bindxml
module for BIND metrics
2 parents 34aed59 + 828841f commit 77490c0

3 files changed

Lines changed: 296 additions & 0 deletions

File tree

bind_xml/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
## Overview
2+
3+
Provides statistics (memory use, query counts) using the xml
4+
statistics from BIND versions >= 9.5. Parsing of the xml is done
5+
using [pybindxml](https://github.com/jforman/pybindxml).
6+
7+
BIND configuration to enable looks something like:
8+
9+
statistics-channels {
10+
inet 127.0.0.1 port 8053 allow {127.0.0.1;};
11+
};
12+
13+
## System Dependencies
14+
15+
yum-rhel6: libxml2-devel libxslt-devel
16+
17+
pip: pybindxml beautifulsoup4 lxml

bind_xml/conf.d/bind_xml.pyconf

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
modules {
2+
module {
3+
name = "bind_xml"
4+
language = "python"
5+
param host {
6+
value = "localhost"
7+
}
8+
param port {
9+
value = 8053
10+
}
11+
12+
# This module calculates all metrics (including derived) rates at
13+
# onces and then serves them out of a cache. This determines the
14+
# minimum TTL.
15+
param min_poll_seconds {
16+
value = 20
17+
}
18+
# Where to log information from this module (syslog facility)
19+
param syslog_facility {
20+
value = "user"
21+
}
22+
# log level, WARNING is not expected to produce any output
23+
param log_level {
24+
value = "WARNING"
25+
}
26+
}
27+
}
28+
29+
collection_group {
30+
collect_every = 30
31+
time_threshold = 60
32+
33+
metric {
34+
name_match = "bind_(.+)"
35+
}
36+
}
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
# Redistribution and use in source and binary forms, with or without
2+
# modification, are permitted provided that the following conditions are met:
3+
# * Redistributions of source code must retain the above copyright
4+
# notice, this list of conditions and the following disclaimer.
5+
# * Redistributions in binary form must reproduce the above copyright
6+
# notice, this list of conditions and the following disclaimer in the
7+
# documentation and/or other materials provided with the distribution.
8+
#
9+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
10+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
11+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
12+
# DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE FOR ANY
13+
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
14+
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
15+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
16+
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
17+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
18+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
19+
20+
21+
import abc
22+
import copy
23+
import logging
24+
import logging.handlers
25+
import optparse
26+
import time
27+
import sys
28+
29+
import pybindxml.reader
30+
31+
log = None
32+
33+
QUERY_TYPES = ['A', 'SOA', 'DS' 'UPDATE', 'MX', 'AAAA', 'DNSKEY', 'QUERY', 'TXT', 'PTR']
34+
35+
METRIC_PREFIX = 'bind_'
36+
37+
DESCRIPTION_SKELETON = {
38+
'name' : 'XXX',
39+
'time_max' : 60,
40+
'value_type' : 'uint', # (string, uint, float, double)
41+
'format' : '%d', #String formatting ('%s', '%d','%f')
42+
'units' : 'XXX',
43+
'slope' : 'both',
44+
'description' : 'XXX',
45+
'groups' : 'bind_xml',
46+
'calc' : 'scalar' # scalar
47+
}
48+
49+
50+
METRICS = [
51+
{'name': 'mem_BlockSize',
52+
'description': '',
53+
'value_type': 'double',
54+
'format': '%f',
55+
'units': 'bytes'},
56+
{'name': 'mem_ContextSize',
57+
'description': '',
58+
'value_type': 'double',
59+
'format': '%f',
60+
'units': 'bytes'},
61+
{'name': 'mem_InUse',
62+
'description': '',
63+
'value_type': 'double',
64+
'format': '%f',
65+
'units': 'bytes'},
66+
{'name': 'mem_TotalUse',
67+
'description': '',
68+
'units': 'bytes',
69+
'value_type': 'double',
70+
'format': '%f'},
71+
]
72+
73+
74+
#### Data Acces
75+
76+
class BindStats(object):
77+
78+
bind_reader = None
79+
80+
stats = None
81+
stats_last = None
82+
now_ts = -1
83+
last_ts = -1
84+
85+
def __init__(self, host, port, min_poll_seconds):
86+
self.host = host
87+
self.port = int(port)
88+
self.min_poll_seconds = int(min_poll_seconds)
89+
90+
def short_name(self, name):
91+
return name.split('bind_')[1]
92+
93+
def get_bind_reader(self):
94+
if self.bind_reader is None:
95+
self.bind_reader = pybindxml.reader.BindXmlReader(host=self.host, port=self.port)
96+
return self.bind_reader
97+
98+
def should_update(self):
99+
return (self.now_ts == -1 or time.time() - self.now_ts > self.min_poll_seconds)
100+
101+
def update_stats(self):
102+
self.stats_last = self.stats
103+
self.last_ts = self.now_ts
104+
self.stats = {}
105+
106+
self.get_bind_reader().get_stats()
107+
for element, value in self.get_bind_reader().stats.memory_stats.items():
108+
self.stats['mem_' + element] = value
109+
110+
# Report queries as a rate of zero if none are reported
111+
for qtype in QUERY_TYPES:
112+
self.stats['query_' + qtype] = 0
113+
for element, value in self.get_bind_reader().stats.query_stats.items():
114+
self.stats['query_' + element] = value
115+
116+
self.now_ts = int(time.time())
117+
118+
119+
def get_metric_value(self, name):
120+
if self.should_update() is True:
121+
self.update_stats()
122+
if self.stats is None or self.stats_last is None:
123+
log.debug('Not enough stat data has been collected yet now_ts:%r last_ts:%r' % (self.now_ts, self.last_ts))
124+
return None
125+
descriptor = NAME_2_DESCRIPTOR[name]
126+
if descriptor['calc'] == 'scalar':
127+
val = self.stats[self.short_name(name)]
128+
elif descriptor['calc'] == 'rate':
129+
val = (self.stats[self.short_name(name)] - self.stats_last[self.short_name(name)]) / (self.now_ts - self.last_ts)
130+
else:
131+
log.warn('unknokwn memtric calc type %s' % descriptor['calc'])
132+
return None
133+
log.debug('on call_back got %s = %r' % (self.short_name(name), val))
134+
if descriptor['value_type'] == 'uint':
135+
return long(val)
136+
else:
137+
return float(val)
138+
139+
140+
#### module functions
141+
142+
143+
def metric_init(params):
144+
global BIND_STATS, NAME_2_DESCRIPTOR
145+
if log is None:
146+
setup_logging('syslog', params['syslog_facility'], params['log_level'])
147+
log.debug('metric_init: %r' % params)
148+
BIND_STATS = BindStats(params['host'], params['port'], params['min_poll_seconds'])
149+
descriptors = []
150+
for qtype in QUERY_TYPES:
151+
METRICS.append({'name': 'query_' + qtype,
152+
'description': '%s queries per second',
153+
'value_type': 'double', 'format': '%f',
154+
'units': 'req/s', 'calc': 'rate'})
155+
for metric in METRICS:
156+
d = copy.copy(DESCRIPTION_SKELETON)
157+
d.update(metric)
158+
d['name'] = METRIC_PREFIX + d['name']
159+
d['call_back'] = BIND_STATS.get_metric_value
160+
descriptors.append(d)
161+
log.debug('descriptors: %r' % descriptors)
162+
for d in descriptors:
163+
for key in ['name', 'units', 'description']:
164+
if d[key] == 'XXX':
165+
log.warn('incomplete descriptor definition: %r' % d)
166+
if d['value_type'] == 'uint' and d['format'] != '%d':
167+
log.warn('value/type format mismatch: %r' % d)
168+
NAME_2_DESCRIPTOR = {}
169+
for d in descriptors:
170+
NAME_2_DESCRIPTOR[d['name']] = d
171+
return descriptors
172+
173+
174+
175+
def metric_cleanup():
176+
logging.shutdown()
177+
178+
179+
#### Main and Friends
180+
181+
def setup_logging(handlers, facility, level):
182+
global log
183+
184+
log = logging.getLogger('gmond_python_bind_xml')
185+
formatter = logging.Formatter(' | '.join(['%(asctime)s', '%(name)s', '%(levelname)s', '%(message)s']))
186+
if handlers in ['syslog', 'both']:
187+
sh = logging.handlers.SysLogHandler(address='/dev/log', facility=facility)
188+
sh.setFormatter(formatter)
189+
log.addHandler(sh)
190+
if handlers in ['stdout', 'both']:
191+
ch = logging.StreamHandler()
192+
ch.setFormatter(formatter)
193+
log.addHandler(ch)
194+
lmap = {
195+
'CRITICAL': logging.CRITICAL,
196+
'ERROR': logging.ERROR,
197+
'WARNING': logging.WARNING,
198+
'INFO': logging.INFO,
199+
'DEBUG': logging.DEBUG,
200+
'NOTSET': logging.NOTSET
201+
}
202+
log.setLevel(lmap[level])
203+
204+
205+
def parse_args(argv):
206+
parser = optparse.OptionParser()
207+
parser.add_option('--log',
208+
action='store', dest='log', default='stdout', choices=['stdout', 'syslog', 'both'],
209+
help='log to stdout and/or syslog')
210+
parser.add_option('--log-level',
211+
action='store', dest='log_level', default='WARNING',
212+
choices=['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'NOTSET'],
213+
help='log to stdout and/or syslog')
214+
parser.add_option('--log-facility',
215+
action='store', dest='log_facility', default='user',
216+
help='facility to use when using syslog')
217+
218+
return parser.parse_args(argv)
219+
220+
221+
def main(argv):
222+
""" used for testing """
223+
(opts, args) = parse_args(argv)
224+
setup_logging(opts.log, opts.log_facility, opts.log_level)
225+
params = {'min_poll_seconds': 5, 'host': 'asu101', 'port': 8053}
226+
descriptors = metric_init(params)
227+
try:
228+
while True:
229+
for d in descriptors:
230+
v = d['call_back'](d['name'])
231+
if v is None:
232+
print 'got None for %s' % d['name']
233+
else:
234+
print 'value for %s is %r' % (d['name'], v)
235+
time.sleep(5)
236+
print '----------------------------'
237+
except KeyboardInterrupt:
238+
log.debug('KeyboardInterrupt, shutting down...')
239+
metric_cleanup()
240+
241+
if __name__ == '__main__':
242+
main(sys.argv[1:])
243+

0 commit comments

Comments
 (0)