master
py 253 lines 8.55 KB
Raw
1 # -*- coding: utf-8 -*-
2 # Description: go_expvar netdata python.d module
3 # Author: Jan Kral (kralewitz)
4 # SPDX-License-Identifier: GPL-3.0-or-later
5
6 from __future__ import division
7
8 import json
9 from collections import namedtuple
10
11 from bases.FrameworkServices.UrlService import UrlService
12
13 MEMSTATS_ORDER = [
14 'memstats_heap',
15 'memstats_stack',
16 'memstats_mspan',
17 'memstats_mcache',
18 'memstats_sys',
19 'memstats_live_objects',
20 'memstats_gc_pauses',
21 ]
22
23 MEMSTATS_CHARTS = {
24 'memstats_heap': {
25 'options': ['heap', 'memory: size of heap memory structures', 'KiB', 'memstats',
26 'expvar.memstats.heap', 'line'],
27 'lines': [
28 ['memstats_heap_alloc', 'alloc', 'absolute', 1, 1024],
29 ['memstats_heap_inuse', 'inuse', 'absolute', 1, 1024]
30 ]
31 },
32 'memstats_stack': {
33 'options': ['stack', 'memory: size of stack memory structures', 'KiB', 'memstats',
34 'expvar.memstats.stack', 'line'],
35 'lines': [
36 ['memstats_stack_inuse', 'inuse', 'absolute', 1, 1024]
37 ]
38 },
39 'memstats_mspan': {
40 'options': ['mspan', 'memory: size of mspan memory structures', 'KiB', 'memstats',
41 'expvar.memstats.mspan', 'line'],
42 'lines': [
43 ['memstats_mspan_inuse', 'inuse', 'absolute', 1, 1024]
44 ]
45 },
46 'memstats_mcache': {
47 'options': ['mcache', 'memory: size of mcache memory structures', 'KiB', 'memstats',
48 'expvar.memstats.mcache', 'line'],
49 'lines': [
50 ['memstats_mcache_inuse', 'inuse', 'absolute', 1, 1024]
51 ]
52 },
53 'memstats_live_objects': {
54 'options': ['live_objects', 'memory: number of live objects', 'objects', 'memstats',
55 'expvar.memstats.live_objects', 'line'],
56 'lines': [
57 ['memstats_live_objects', 'live']
58 ]
59 },
60 'memstats_sys': {
61 'options': ['sys', 'memory: size of reserved virtual address space', 'KiB', 'memstats',
62 'expvar.memstats.sys', 'line'],
63 'lines': [
64 ['memstats_sys', 'sys', 'absolute', 1, 1024]
65 ]
66 },
67 'memstats_gc_pauses': {
68 'options': ['gc_pauses', 'memory: average duration of GC pauses', 'ns', 'memstats',
69 'expvar.memstats.gc_pauses', 'line'],
70 'lines': [
71 ['memstats_gc_pauses', 'avg']
72 ]
73 }
74 }
75
76 EXPVAR = namedtuple(
77 "EXPVAR",
78 [
79 "key",
80 "type",
81 "id",
82 ]
83 )
84
85
86 def flatten(d, top='', sep='.'):
87 items = []
88 for key, val in d.items():
89 nkey = top + sep + key if top else key
90 if isinstance(val, dict):
91 items.extend(flatten(val, nkey, sep=sep).items())
92 else:
93 items.append((nkey, val))
94 return dict(items)
95
96
97 class Service(UrlService):
98 def __init__(self, configuration=None, name=None):
99 UrlService.__init__(self, configuration=configuration, name=name)
100 # if memstats collection is enabled, add the charts and their order
101 if self.configuration.get('collect_memstats'):
102 self.definitions = dict(MEMSTATS_CHARTS)
103 self.order = list(MEMSTATS_ORDER)
104 else:
105 self.definitions = dict()
106 self.order = list()
107
108 # if extra charts are defined, parse their config
109 extra_charts = self.configuration.get('extra_charts')
110 if extra_charts:
111 self._parse_extra_charts_config(extra_charts)
112
113 def check(self):
114 """
115 Check if the module can collect data:
116 1) At least one JOB configuration has to be specified
117 2) The JOB configuration needs to define the URL and either collect_memstats must be enabled or at least one
118 extra_chart must be defined.
119
120 The configuration and URL check is provided by the UrlService class.
121 """
122
123 if not (self.configuration.get('extra_charts') or self.configuration.get('collect_memstats')):
124 self.error('Memstats collection is disabled and no extra_charts are defined, disabling module.')
125 return False
126
127 return UrlService.check(self)
128
129 def _parse_extra_charts_config(self, extra_charts_config):
130
131 # a place to store the expvar keys and their types
132 self.expvars = list()
133
134 for chart in extra_charts_config:
135
136 chart_dict = dict()
137 chart_id = chart.get('id')
138 chart_lines = chart.get('lines')
139 chart_opts = chart.get('options', dict())
140
141 if not all([chart_id, chart_lines]):
142 self.info('Chart {0} has no ID or no lines defined, skipping'.format(chart))
143 continue
144
145 chart_dict['options'] = [
146 chart_opts.get('name', ''),
147 chart_opts.get('title', ''),
148 chart_opts.get('units', ''),
149 chart_opts.get('family', ''),
150 chart_opts.get('context', ''),
151 chart_opts.get('chart_type', 'line')
152 ]
153 chart_dict['lines'] = list()
154
155 # add the lines to the chart
156 for line in chart_lines:
157
158 ev_key = line.get('expvar_key')
159 ev_type = line.get('expvar_type')
160 line_id = line.get('id')
161
162 if not all([ev_key, ev_type, line_id]):
163 self.info('Line missing expvar_key, expvar_type, or line_id, skipping: {0}'.format(line))
164 continue
165
166 if ev_type not in ['int', 'float']:
167 self.info('Unsupported expvar_type "{0}". Must be "int" or "float"'.format(ev_type))
168 continue
169
170 # self.expvars[ev_key] = (ev_type, line_id)
171 self.expvars.append(EXPVAR(ev_key, ev_type, line_id))
172
173 chart_dict['lines'].append(
174 [
175 line.get('id', ''),
176 line.get('name', ''),
177 line.get('algorithm', ''),
178 line.get('multiplier', 1),
179 line.get('divisor', 100 if ev_type == 'float' else 1),
180 line.get('hidden', False)
181 ]
182 )
183
184 self.order.append(chart_id)
185 self.definitions[chart_id] = chart_dict
186
187 def _get_data(self):
188 """
189 Format data received from http request
190 :return: dict
191 """
192
193 raw_data = self._get_raw_data()
194 if not raw_data:
195 return None
196
197 data = json.loads(raw_data)
198
199 expvars = dict()
200 if self.configuration.get('collect_memstats'):
201 expvars.update(self._parse_memstats(data))
202
203 if self.configuration.get('extra_charts'):
204 # the memstats part of the data has been already parsed, so we remove it before flattening and checking
205 # the rest of the data, thus avoiding needless iterating over the multiply nested memstats dict.
206 del (data['memstats'])
207 flattened = flatten(data)
208
209 for ev in self.expvars:
210 v = flattened.get(ev.key)
211
212 if v is None:
213 continue
214
215 try:
216 if ev.type == 'int':
217 expvars[ev.id] = int(v)
218 elif ev.type == 'float':
219 expvars[ev.id] = float(v) * 100
220 except ValueError:
221 self.info('Failed to parse value for key {0} as {1}, ignoring key.'.format(ev.key, ev.type))
222 return None
223
224 return expvars
225
226 @staticmethod
227 def _parse_memstats(data):
228
229 memstats = data['memstats']
230
231 # calculate the number of live objects in memory
232 live_objs = int(memstats['Mallocs']) - int(memstats['Frees'])
233
234 # calculate GC pause times average
235 # the Go runtime keeps the last 256 GC pause durations in a circular buffer,
236 # so we need to filter out the 0 values before the buffer is filled
237 gc_pauses = memstats['PauseNs']
238 try:
239 gc_pause_avg = sum(gc_pauses) / len([x for x in gc_pauses if x > 0])
240 # no GC cycles have occurred yet
241 except ZeroDivisionError:
242 gc_pause_avg = 0
243
244 return {
245 'memstats_heap_alloc': memstats['HeapAlloc'],
246 'memstats_heap_inuse': memstats['HeapInuse'],
247 'memstats_stack_inuse': memstats['StackInuse'],
248 'memstats_mspan_inuse': memstats['MSpanInuse'],
249 'memstats_mcache_inuse': memstats['MCacheInuse'],
250 'memstats_sys': memstats['Sys'],
251 'memstats_live_objects': live_objs,
252 'memstats_gc_pauses': gc_pause_avg,
253 }