master
py 431 lines 13.5 KB
Raw
1 # -*- coding: utf-8 -*-
2 # Description:
3 # Author: Ilya Mashchenko (ilyam8)
4 # SPDX-License-Identifier: GPL-3.0-or-later
5
6 import os
7
8 from bases.collection import safe_print
9
10 CHART_PARAMS = ['type', 'id', 'name', 'title', 'units', 'family', 'context', 'chart_type', 'hidden']
11 DIMENSION_PARAMS = ['id', 'name', 'algorithm', 'multiplier', 'divisor', 'hidden']
12 VARIABLE_PARAMS = ['id', 'value']
13
14 CHART_TYPES = ['line', 'area', 'stacked']
15 DIMENSION_ALGORITHMS = ['absolute', 'incremental', 'percentage-of-absolute-row', 'percentage-of-incremental-row']
16
17 CHART_BEGIN = 'BEGIN {type}.{id} {since_last}\n'
18 CHART_CREATE = "CHART {type}.{id} '{name}' '{title}' '{units}' '{family}' '{context}' " \
19 "{chart_type} {priority} {update_every} '{hidden}' 'python.d.plugin' '{module_name}'\n"
20 CHART_OBSOLETE = "CHART {type}.{id} '{name}' '{title}' '{units}' '{family}' '{context}' " \
21 "{chart_type} {priority} {update_every} '{hidden} obsolete'\n"
22
23 CLABEL_COLLECT_JOB = "CLABEL '_collect_job' '{actual_job_name}' '0'\n"
24 CLABEL_COMMIT = "CLABEL_COMMIT\n"
25
26 DIMENSION_CREATE = "DIMENSION '{id}' '{name}' {algorithm} {multiplier} {divisor} '{hidden} {obsolete}'\n"
27 DIMENSION_SET = "SET '{id}' = {value}\n"
28
29 CHART_VARIABLE_SET = "VARIABLE CHART '{id}' = {value}\n"
30
31 # 1 is label source auto
32 # https://github.com/netdata/netdata/blob/cc2586de697702f86a3c34e60e23652dd4ddcb42/database/rrd.h#L205
33 RUNTIME_CHART_CREATE = "CHART netdata.runtime_{job_name} '' 'Execution time' 'ms' 'python.d' " \
34 "netdata.pythond_runtime line 145000 {update_every} '' 'python.d.plugin' '{module_name}'\n" \
35 "CLABEL '_collect_job' '{actual_job_name}' '1'\n" \
36 "CLABEL_COMMIT\n" \
37 "DIMENSION run_time 'run time' absolute 1 1\n"
38
39 ND_INTERNAL_MONITORING_DISABLED = os.getenv("NETDATA_INTERNALS_MONITORING") == "NO"
40
41
42 def create_runtime_chart(func):
43 """
44 Calls a wrapped function, then prints runtime chart to stdout.
45
46 Used as a decorator for SimpleService.create() method.
47 The whole point of making 'create runtime chart' functionality as a decorator was
48 to help users who re-implements create() in theirs classes.
49
50 :param func: class method
51 :return:
52 """
53
54 def wrapper(*args, **kwargs):
55 self = args[0]
56 if not ND_INTERNAL_MONITORING_DISABLED:
57 chart = RUNTIME_CHART_CREATE.format(
58 job_name=self.name,
59 actual_job_name=self.actual_job_name,
60 update_every=self._runtime_counters.update_every,
61 module_name=self.module_name,
62 )
63 safe_print(chart)
64 ok = func(*args, **kwargs)
65 return ok
66
67 return wrapper
68
69
70 class ChartError(Exception):
71 """Base-class for all exceptions raised by this module"""
72
73
74 class DuplicateItemError(ChartError):
75 """Occurs when user re-adds a chart or a dimension that has already been added"""
76
77
78 class ItemTypeError(ChartError):
79 """Occurs when user passes value of wrong type to Chart, Dimension or ChartVariable class"""
80
81
82 class ItemValueError(ChartError):
83 """Occurs when user passes inappropriate value to Chart, Dimension or ChartVariable class"""
84
85
86 class Charts:
87 """Represent a collection of charts
88
89 All charts stored in a dict.
90 Chart is a instance of Chart class.
91 Charts adding must be done using Charts.add_chart() method only"""
92
93 def __init__(self, job_name, actual_job_name, priority, cleanup, get_update_every, module_name):
94 """
95 :param job_name: <bound method>
96 :param priority: <int>
97 :param get_update_every: <bound method>
98 """
99 self.job_name = job_name
100 self.actual_job_name = actual_job_name
101 self.priority = priority
102 self.cleanup = cleanup
103 self.get_update_every = get_update_every
104 self.module_name = module_name
105 self.charts = dict()
106
107 def __len__(self):
108 return len(self.charts)
109
110 def __iter__(self):
111 return iter(self.charts.values())
112
113 def __repr__(self):
114 return 'Charts({0})'.format(self)
115
116 def __str__(self):
117 return str([chart for chart in self.charts])
118
119 def __contains__(self, item):
120 return item in self.charts
121
122 def __getitem__(self, item):
123 return self.charts[item]
124
125 def __delitem__(self, key):
126 del self.charts[key]
127
128 def __bool__(self):
129 return bool(self.charts)
130
131 def __nonzero__(self):
132 return self.__bool__()
133
134 def add_chart(self, params):
135 """
136 Create Chart instance and add it to the dict
137
138 Manually adds job name, priority and update_every to params.
139 :param params: <list>
140 :return:
141 """
142 params = [self.job_name()] + params
143 new_chart = Chart(params)
144
145 new_chart.params['update_every'] = self.get_update_every()
146 new_chart.params['priority'] = self.priority
147 new_chart.params['module_name'] = self.module_name
148 new_chart.params['actual_job_name'] = self.actual_job_name
149
150 self.priority += 1
151 self.charts[new_chart.id] = new_chart
152
153 return new_chart
154
155 def active_charts(self):
156 return [chart.id for chart in self if not chart.flags.obsoleted]
157
158
159 class Chart:
160 """Represent a chart"""
161
162 def __init__(self, params):
163 """
164 :param params: <list>
165 """
166 if not isinstance(params, list):
167 raise ItemTypeError("'chart' must be a list type")
168 if not len(params) >= 8:
169 raise ItemValueError("invalid value for 'chart', must be {0}".format(CHART_PARAMS))
170
171 self.params = dict(zip(CHART_PARAMS, (p or str() for p in params)))
172 self.name = '{type}.{id}'.format(type=self.params['type'],
173 id=self.params['id'])
174 if self.params.get('chart_type') not in CHART_TYPES:
175 self.params['chart_type'] = 'absolute'
176 hidden = str(self.params.get('hidden', ''))
177 self.params['hidden'] = 'hidden' if hidden == 'hidden' else ''
178
179 self.dimensions = list()
180 self.variables = set()
181 self.flags = ChartFlags()
182 self.penalty = 0
183
184 def __getattr__(self, item):
185 try:
186 return self.params[item]
187 except KeyError:
188 raise AttributeError("'{instance}' has no attribute '{attr}'".format(instance=repr(self),
189 attr=item))
190
191 def __repr__(self):
192 return 'Chart({0})'.format(self.id)
193
194 def __str__(self):
195 return self.id
196
197 def __iter__(self):
198 return iter(self.dimensions)
199
200 def __contains__(self, item):
201 return item in [dimension.id for dimension in self.dimensions]
202
203 def add_variable(self, variable):
204 """
205 :param variable: <list>
206 :return:
207 """
208 self.variables.add(ChartVariable(variable))
209
210 def add_dimension(self, dimension):
211 """
212 :param dimension: <list>
213 :return:
214 """
215 dim = Dimension(dimension)
216
217 if dim.id in self:
218 raise DuplicateItemError("'{dimension}' already in '{chart}' dimensions".format(dimension=dim.id,
219 chart=self.name))
220 self.refresh()
221 self.dimensions.append(dim)
222 return dim
223
224 def del_dimension(self, dimension_id, hide=True):
225 if dimension_id not in self:
226 return
227 idx = self.dimensions.index(dimension_id)
228 dimension = self.dimensions[idx]
229 if hide:
230 dimension.params['hidden'] = 'hidden'
231 dimension.params['obsolete'] = 'obsolete'
232 self.create()
233 self.dimensions.remove(dimension)
234
235 def hide_dimension(self, dimension_id, reverse=False):
236 if dimension_id not in self:
237 return
238 idx = self.dimensions.index(dimension_id)
239 dimension = self.dimensions[idx]
240 dimension.params['hidden'] = 'hidden' if not reverse else str()
241 self.refresh()
242
243 def create(self):
244 """
245 :return:
246 """
247 chart = CHART_CREATE.format(**self.params)
248 labels = CLABEL_COLLECT_JOB.format(**self.params) + CLABEL_COMMIT
249 dimensions = ''.join([dimension.create() for dimension in self.dimensions])
250 variables = ''.join([var.set(var.value) for var in self.variables if var])
251
252 self.flags.push = False
253 self.flags.created = True
254
255 safe_print(chart + labels + dimensions + variables)
256
257 def can_be_updated(self, data):
258 for dim in self.dimensions:
259 if dim.get_value(data) is not None:
260 return True
261 return False
262
263 def update(self, data, interval):
264 updated_dimensions, updated_variables = str(), str()
265
266 for dim in self.dimensions:
267 value = dim.get_value(data)
268 if value is not None:
269 updated_dimensions += dim.set(value)
270
271 for var in self.variables:
272 value = var.get_value(data)
273 if value is not None:
274 updated_variables += var.set(value)
275
276 if updated_dimensions:
277 since_last = interval if self.flags.updated else 0
278
279 if self.flags.push:
280 self.create()
281
282 chart_begin = CHART_BEGIN.format(type=self.type, id=self.id, since_last=since_last)
283 safe_print(chart_begin, updated_dimensions, updated_variables, 'END\n')
284
285 self.flags.updated = True
286 self.penalty = 0
287 else:
288 self.penalty += 1
289 self.flags.updated = False
290
291 return bool(updated_dimensions)
292
293 def obsolete(self):
294 self.flags.obsoleted = True
295 if self.flags.created:
296 safe_print(CHART_OBSOLETE.format(**self.params))
297
298 def refresh(self):
299 self.penalty = 0
300 self.flags.push = True
301 self.flags.obsoleted = False
302
303
304 class Dimension:
305 """Represent a dimension"""
306
307 def __init__(self, params):
308 """
309 :param params: <list>
310 """
311 if not isinstance(params, list):
312 raise ItemTypeError("'dimension' must be a list type")
313 if not params:
314 raise ItemValueError("invalid value for 'dimension', must be {0}".format(DIMENSION_PARAMS))
315
316 self.params = dict(zip(DIMENSION_PARAMS, (p or str() for p in params)))
317 self.params['name'] = self.params.get('name') or self.params['id']
318
319 if self.params.get('algorithm') not in DIMENSION_ALGORITHMS:
320 self.params['algorithm'] = 'absolute'
321 if not isinstance(self.params.get('multiplier'), int):
322 self.params['multiplier'] = 1
323 if not isinstance(self.params.get('divisor'), int):
324 self.params['divisor'] = 1
325 self.params.setdefault('hidden', '')
326 self.params.setdefault('obsolete', '')
327
328 def __getattr__(self, item):
329 try:
330 return self.params[item]
331 except KeyError:
332 raise AttributeError("'{instance}' has no attribute '{attr}'".format(instance=repr(self),
333 attr=item))
334
335 def __repr__(self):
336 return 'Dimension({0})'.format(self.id)
337
338 def __str__(self):
339 return self.id
340
341 def __eq__(self, other):
342 if not isinstance(other, Dimension):
343 return self.id == other
344 return self.id == other.id
345
346 def __ne__(self, other):
347 return not self == other
348
349 def __hash__(self):
350 return hash(repr(self))
351
352 def create(self):
353 return DIMENSION_CREATE.format(**self.params)
354
355 def set(self, value):
356 """
357 :param value: <str>: must be a digit
358 :return:
359 """
360 return DIMENSION_SET.format(id=self.id,
361 value=value)
362
363 def get_value(self, data):
364 try:
365 return int(data[self.id])
366 except (KeyError, TypeError):
367 return None
368
369
370 class ChartVariable:
371 """Represent a chart variable"""
372
373 def __init__(self, params):
374 """
375 :param params: <list>
376 """
377 if not isinstance(params, list):
378 raise ItemTypeError("'variable' must be a list type")
379 if not params:
380 raise ItemValueError("invalid value for 'variable' must be: {0}".format(VARIABLE_PARAMS))
381
382 self.params = dict(zip(VARIABLE_PARAMS, params))
383 self.params.setdefault('value', None)
384
385 def __getattr__(self, item):
386 try:
387 return self.params[item]
388 except KeyError:
389 raise AttributeError("'{instance}' has no attribute '{attr}'".format(instance=repr(self),
390 attr=item))
391
392 def __bool__(self):
393 return self.value is not None
394
395 def __nonzero__(self):
396 return self.__bool__()
397
398 def __repr__(self):
399 return 'ChartVariable({0})'.format(self.id)
400
401 def __str__(self):
402 return self.id
403
404 def __eq__(self, other):
405 if isinstance(other, ChartVariable):
406 return self.id == other.id
407 return False
408
409 def __ne__(self, other):
410 return not self == other
411
412 def __hash__(self):
413 return hash(repr(self))
414
415 def set(self, value):
416 return CHART_VARIABLE_SET.format(id=self.id,
417 value=value)
418
419 def get_value(self, data):
420 try:
421 return int(data[self.id])
422 except (KeyError, TypeError):
423 return None
424
425
426 class ChartFlags:
427 def __init__(self):
428 self.push = True
429 self.created = False
430 self.updated = False
431 self.obsoleted = False