@cryptotaxi247 / netdata-1 / commits / 1aaa9d44e

bind_rndc plugin: rewritten to use _add_new_dimension

Ilya committed Jun 12, 2017 at 00:29 UTC 1aaa9d44e489fb5340156afaceca379aea75a191
1 file changed +193 -126
python.d/bind_rndc.chart.py
+193 -126
@@ -2,100 +2,141 @@
2 # Description: bind rndc netdata python.d module
3 # Author: l2isbad
4
5 -from base import SimpleService
6 -from re import compile, findall
7 -from os.path import getsize, split
8 -from os import access as is_accessible, R_OK
5 +from os.path import getsize
6 +from os import access, R_OK
7 from subprocess import Popen
8 +from collections import defaultdict
9 +from base import SimpleService
10
11 priority = 60000
12 retries = 60
13 update_every = 30
14
15 -NMS = ['requests', 'responses', 'success', 'auth_answer', 'nonauth_answer', 'nxrrset', 'failure',
16 - 'nxdomain', 'recursion', 'duplicate', 'rejections']
17 -QUERIES = ['RESERVED0', 'A', 'NS', 'CNAME', 'SOA', 'PTR', 'MX', 'TXT', 'X25', 'AAAA', 'SRV', 'NAPTR',
18 - 'A6', 'DS', 'RRSIG', 'DNSKEY', 'SPF', 'ANY', 'DLV']
15 +ORDER = ['name_server_statistics', 'incoming_queries', 'outgoing_queries', 'named_stats_size']
16 +
17 +CHARTS = {
18 + 'name_server_statistics': {
19 + 'options': [None, 'Name Server Statistics', 'stats', 'name server statistics',
20 + 'bind_rndc.name_server_statistics', 'line'],
21 + 'lines': [
22 + ['nms_requests', 'requests', 'incremental'],
23 + ['nms_rejected_queries', 'rejected_queries', 'incremental'],
24 + ['nms_success', 'success', 'incremental'],
25 + ['nms_failure', 'failure', 'incremental'],
26 + ['nms_responses', 'responses', 'incremental'],
27 + ['nms_duplicate', 'duplicate', 'incremental'],
28 + ['nms_recursion', 'recursion', 'incremental'],
29 + ['nms_nxrrset', 'nxrrset', 'incremental'],
30 + ['nms_nxdomain', 'nxdomain', 'incremental'],
31 + ['nms_non_auth_answer', 'non_auth_answer', 'incremental'],
32 + ['nms_auth_answer', 'auth_answer', 'incremental'],
33 + ['nms_dropped_queries', 'dropped_queries', 'incremental'],
34 + ]},
35 + 'incoming_queries': {
36 + 'options': [None, 'Incoming Queries', 'queries', 'incoming queries',
37 + 'bind_rndc.incoming_queries', 'line'],
38 + 'lines': [
39 + ]},
40 + 'outgoing_queries': {
41 + 'options': [None, 'Outgoing Queries', 'queries', 'outgoing queries',
42 + 'bind_rndc.outgoing_queries', 'line'],
43 + 'lines': [
44 + ]},
45 + 'named_stats_size': {
46 + 'options': [None, 'Named Stats File Size', 'MB', 'file size',
47 + 'bind_rndc.stats_size', 'line'],
48 + 'lines': [
49 + ['stats_size', None, 'absolute', 1, 1 << 20]
50 + ]}
51 +}
52 +
53 +NMS = {
54 + 'nms_requests':
55 + ['IPv4 requests received',
56 + 'IPv6 requests received',
57 + 'TCP requests received',
58 + 'requests with EDNS(0) receive'],
59 + 'nms_responses':
60 + ['responses sent',
61 + 'truncated responses sent',
62 + 'responses with EDNS(0) sent',
63 + 'requests with unsupported EDNS version received'],
64 + 'nms_failure':
65 + ['other query failures',
66 + 'queries resulted in SERVFAIL'],
67 + 'nms_auth_answer':
68 + ['queries resulted in authoritative answer'],
69 + 'nms_non_auth_answer':
70 + ['queries resulted in non authoritative answer'],
71 + 'nms_nxrrset':
72 + ['queries resulted in nxrrset'],
73 + 'nms_success':
74 + ['queries resulted in successful answer'],
75 + 'nms_nxdomain':
76 + ['queries resulted in NXDOMAIN'],
77 + 'nms_recursion':
78 + ['queries caused recursion'],
79 + 'nms_duplicate':
80 + ['duplicate queries received'],
81 + 'nms_rejected_queries':
82 + ['auth queries rejected',
83 + 'recursive queries rejected'],
84 + 'nms_dropped_queries':
85 + ['queries dropped']
86 +}
87 +
88 +STATS = ['Name Server Statistics', 'Incoming Queries', 'Outgoing Queries']
89
90
91 class Service(SimpleService):
92 def __init__(self, configuration=None, name=None):
93 SimpleService.__init__(self, configuration=configuration, name=name)
94 + self.order = ORDER
95 + self.definitions = CHARTS
96 self.named_stats_path = self.configuration.get('named_stats_path', '/var/log/bind/named.stats')
25 - self.regex_values = compile(r'([0-9]+) ([^\n]+)')
26 - # self.options = ['Incoming Requests', 'Incoming Queries', 'Outgoing Queries',
27 - # 'Name Server Statistics', 'Zone Maintenance Statistics', 'Resolver Statistics',
28 - # 'Cache DB RRsets', 'Socket I/O Statistics']
29 - self.options = ['Name Server Statistics', 'Incoming Queries', 'Outgoing Queries']
30 - self.regex_options = [r'(%s(?= \+\+)) \+\+([^\+]+)' % option for option in self.options]
97 self.rndc = self.find_binary('rndc')
98 + self.data = dict(nms_requests=0, nms_responses=0, nms_failure=0, nms_auth=0,
99 + nms_non_auth=0, nms_nxrrset=0, nms_success=0, nms_nxdomain=0,
100 + nms_recursion=0, nms_duplicate=0, nms_rejected_queries=0,
101 + nms_dropped_queries=0)
102
103 def check(self):
34 - # We cant start without 'rndc' command
104 if not self.rndc:
105 self.error('Can\'t locate \'rndc\' binary or binary is not executable by netdata')
106 return False
107
39 - # We cant start if stats file is not exist or not readable by netdata user
40 - if not is_accessible(self.named_stats_path, R_OK):
108 + if not access(self.named_stats_path, R_OK):
109 self.error('Cannot access file %s' % self.named_stats_path)
110 return False
111
44 - size_before = getsize(self.named_stats_path)
112 run_rndc = Popen([self.rndc, 'stats'], shell=False)
113 run_rndc.wait()
47 - size_after = getsize(self.named_stats_path)
114
49 - # We cant start if netdata user has no permissions to run 'rndc stats'
115 if not run_rndc.returncode:
51 - # 'rndc' was found, stats file is exist and readable and we can run 'rndc stats'. Lets go!
52 - self.create_charts()
53 -
54 - # BIND APPEND dump on every run 'rndc stats'
55 - # that is why stats file size can be VERY large if update_interval too small
56 - dump_size_24hr = round(86400 / self.update_every * (int(size_after) - int(size_before)) / 1048576, 3)
57 -
58 - # If update_every too small we should WARN user
59 - if self.update_every < 30:
60 - self.info('Update_every %s is NOT recommended for use. Increase the value to > 30' % self.update_every)
61 -
62 - self.info('With current update_interval it will be + %s MB every 24hr. '
63 - 'Don\'t forget to create logrotate conf file for %s' % (dump_size_24hr, self.named_stats_path))
64 -
65 - self.info('Plugin was started successfully.')
66 -
116 return True
68 - else:
69 - self.error('Not enough permissions to run "%s stats"' % self.rndc)
70 - return False
117 + self.error('Not enough permissions to run "%s stats"' % self.rndc)
118 + return False
119
120 def _get_raw_data(self):
121 """
122 Run 'rndc stats' and read last dump from named.stats
75 - :return: tuple(
76 - file.read() obj,
77 - named.stats file size
78 - )
123 + :return: dict
124 """
125 + result = dict()
126 try:
127 current_size = getsize(self.named_stats_path)
82 - except OSError:
83 - return None, None
84 -
85 - run_rndc = Popen([self.rndc, 'stats'], shell=False)
86 - run_rndc.wait()
87 -
88 - if run_rndc.returncode:
89 - return None, None
90 -
91 - try:
92 - with open(self.named_stats_path) as bind_rndc:
93 - bind_rndc.seek(current_size)
94 - result = bind_rndc.read()
95 - except OSError:
96 - return None, None
97 - else:
98 - return result, current_size
128 + run_rndc = Popen([self.rndc, 'stats'], shell=False)
129 + run_rndc.wait()
130 +
131 + if run_rndc.returncode:
132 + return None
133 + with open(self.named_stats_path) as named_stats:
134 + named_stats.seek(current_size)
135 + result['stats'] = named_stats.readlines()
136 + result['size'] = current_size
137 + return result
138 + except (OSError, IOError):
139 + return None
140
141 def _get_data(self):
142 """
@@ -103,72 +144,98 @@ class Service(SimpleService):
144 :return: dict
145 """
146
106 - raw_data, size = self._get_raw_data()
147 + raw_data = self._get_raw_data()
148
149 if raw_data is None:
150 return None
110 -
111 - rndc_stats = dict()
112 -
113 - # Result: dict.
114 - # topic = Cache DB RRsets; body = A 178303 NS 86790 ... ; desc = A; value = 178303
115 - # {'Cache DB RRsets': [('A', 178303), ('NS', 286790), ...],
116 - # {Incoming Queries': [('RESERVED0', 8), ('A', 4557317680), ...],
117 - # ......
118 - for regex in self.regex_options:
119 - rndc_stats.update(dict([(topic, [(desc, int(value)) for value, desc in self.regex_values.findall(body)])
120 - for topic, body in findall(regex, raw_data)]))
121 -
122 - nms = dict(rndc_stats.get('Name Server Statistics', []))
123 -
124 - inc_queries = dict([('i' + k, 0) for k in QUERIES])
125 - inc_queries.update(dict([('i' + k, v) for k, v in rndc_stats.get('Incoming Queries', [])]))
126 - out_queries = dict([('o' + k, 0) for k in QUERIES])
127 - out_queries.update(dict([('o' + k, v) for k, v in rndc_stats.get('Outgoing Queries', [])]))
128 -
129 - to_netdata = dict()
130 - to_netdata['requests'] = sum([v for k, v in nms.items() if 'request' in k and 'received' in k])
131 - to_netdata['responses'] = sum([v for k, v in nms.items() if 'responses' in k and 'sent' in k])
132 - to_netdata['success'] = nms.get('queries resulted in successful answer', 0)
133 - to_netdata['auth_answer'] = nms.get('queries resulted in authoritative answer', 0)
134 - to_netdata['nonauth_answer'] = nms.get('queries resulted in non authoritative answer', 0)
135 - to_netdata['nxrrset'] = nms.get('queries resulted in nxrrset', 0)
136 - to_netdata['failure'] = sum([nms.get('queries resulted in SERVFAIL', 0), nms.get('other query failures', 0)])
137 - to_netdata['nxdomain'] = nms.get('queries resulted in NXDOMAIN', 0)
138 - to_netdata['recursion'] = nms.get('queries caused recursion', 0)
139 - to_netdata['duplicate'] = nms.get('duplicate queries received', 0)
140 - to_netdata['rejections'] = nms.get('recursive queries rejected', 0)
141 - to_netdata['stats_size'] = size
142 -
143 - to_netdata.update(inc_queries)
144 - to_netdata.update(out_queries)
145 - return to_netdata
146 -
147 - def create_charts(self):
148 - self.order = ['stats_size', 'bind_stats', 'incoming_q', 'outgoing_q']
149 - self.definitions = {
150 - 'bind_stats': {
151 - 'options': [None, 'Name Server Statistics', 'stats', 'name server statistics', 'bind_rndc.stats', 'line'],
152 - 'lines': [
153 - ]},
154 - 'incoming_q': {
155 - 'options': [None, 'Incoming queries', 'queries','incoming queries', 'bind_rndc.incq', 'line'],
156 - 'lines': [
157 - ]},
158 - 'outgoing_q': {
159 - 'options': [None, 'Outgoing queries', 'queries','outgoing queries', 'bind_rndc.outq', 'line'],
160 - 'lines': [
161 - ]},
162 - 'stats_size': {
163 - 'options': [None, '%s file size' % split(self.named_stats_path)[1].capitalize(), 'megabytes',
164 - '%s size' % split(self.named_stats_path)[1], 'bind_rndc.size', 'line'],
165 - 'lines': [
166 - ["stats_size", None, "absolute", 1, 1048576]
167 - ]}
168 - }
169 - for elem in QUERIES:
170 - self.definitions['incoming_q']['lines'].append(['i' + elem, elem, 'incremental'])
171 - self.definitions['outgoing_q']['lines'].append(['o' + elem, elem, 'incremental'])
172 -
173 - for elem in NMS:
174 - self.definitions['bind_stats']['lines'].append([elem, None, 'incremental'])
151 + parsed = dict()
152 + for stat in STATS:
153 + parsed[stat] = parse_stats(field=stat,
154 + named_stats=raw_data['stats'])
155 +
156 + self.data.update(nms_mapper(data=parsed['Name Server Statistics']))
157 +
158 + for elem in zip(['Incoming Queries', 'Outgoing Queries'], ['incoming_queries', 'outgoing_queries']):
159 + parsed_key, chart_name = elem[0], elem[1]
160 + for dimension_id, value in queries_mapper(data=parsed[parsed_key],
161 + add=chart_name[:9]).items():
162 + if dimension_id not in self.data:
163 + dimension = dimension_id.replace(chart_name[:9], '')
164 + self._add_new_dimension(dimension_id=dimension_id,
165 + dimension=dimension,
166 + chart_name=chart_name,
167 + priority=self.priority + self.order.index(chart_name))
168 + self.data[dimension_id] = value
169 +
170 + self.data['stats_size'] = raw_data['size']
171 + return self.data
172 +
173 +
174 +def parse_stats(field, named_stats):
175 + """
176 + :param field: str:
177 + :param named_stats: list:
178 + :return: dict
179 +
180 + Example:
181 + filed: 'Incoming Queries'
182 + names_stats (list of lines):
183 + ++ Incoming Requests ++
184 + 1405660 QUERY
185 + 3 NOTIFY
186 + ++ Incoming Queries ++
187 + 1214961 A
188 + 75 NS
189 + 2 CNAME
190 + 2897 SOA
191 + 35544 PTR
192 + 14 MX
193 + 5822 TXT
194 + 145974 AAAA
195 + 371 SRV
196 + ++ Outgoing Queries ++
197 + ...
198 +
199 + result:
200 + {'A', 1214961, 'NS': 75, 'CNAME': 2, 'SOA': 2897, ...}
201 + """
202 + data = dict()
203 + ns = iter(named_stats)
204 + for line in ns:
205 + if field not in line:
206 + continue
207 + while True:
208 + try:
209 + line = next(ns)
210 + except StopIteration:
211 + break
212 + if '++' not in line:
213 + if '[' in line:
214 + continue
215 + v, k = line.strip().split(' ', 1)
216 + data[k] = int(v)
217 + continue
218 + break
219 + break
220 + return data
221 +
222 +
223 +def nms_mapper(data):
224 + """
225 + :param data: dict
226 + :return: dict(defaultdict)
227 + """
228 + result = defaultdict(int)
229 + for k, v in NMS.items():
230 + for elem in v:
231 + result[k] += data.get(elem, 0)
232 + return result
233 +
234 +
235 +def queries_mapper(data, add):
236 + """
237 + :param data: dict
238 + :param add: str
239 + :return: dict
240 + """
241 + return dict([(add + k, v) for k, v in data.items()])