| 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 | } |