# -*- coding: utf-8 -*- # Description: # Author: Ilya Mashchenko (ilyam8) # SPDX-License-Identifier: GPL-3.0-or-later import os from bases.collection import safe_print CHART_PARAMS = ['type', 'id', 'name', 'title', 'units', 'family', 'context', 'chart_type', 'hidden'] DIMENSION_PARAMS = ['id', 'name', 'algorithm', 'multiplier', 'divisor', 'hidden'] VARIABLE_PARAMS = ['id', 'value'] CHART_TYPES = ['line', 'area', 'stacked'] DIMENSION_ALGORITHMS = ['absolute', 'incremental', 'percentage-of-absolute-row', 'percentage-of-incremental-row'] CHART_BEGIN = 'BEGIN {type}.{id} {since_last}\n' CHART_CREATE = "CHART {type}.{id} '{name}' '{title}' '{units}' '{family}' '{context}' " \ "{chart_type} {priority} {update_every} '{hidden}' 'python.d.plugin' '{module_name}'\n" CHART_OBSOLETE = "CHART {type}.{id} '{name}' '{title}' '{units}' '{family}' '{context}' " \ "{chart_type} {priority} {update_every} '{hidden} obsolete'\n" CLABEL_COLLECT_JOB = "CLABEL '_collect_job' '{actual_job_name}' '0'\n" CLABEL_COMMIT = "CLABEL_COMMIT\n" DIMENSION_CREATE = "DIMENSION '{id}' '{name}' {algorithm} {multiplier} {divisor} '{hidden} {obsolete}'\n" DIMENSION_SET = "SET '{id}' = {value}\n" CHART_VARIABLE_SET = "VARIABLE CHART '{id}' = {value}\n" # 1 is label source auto # https://github.com/netdata/netdata/blob/cc2586de697702f86a3c34e60e23652dd4ddcb42/database/rrd.h#L205 RUNTIME_CHART_CREATE = "CHART netdata.runtime_{job_name} '' 'Execution time' 'ms' 'python.d' " \ "netdata.pythond_runtime line 145000 {update_every} '' 'python.d.plugin' '{module_name}'\n" \ "CLABEL '_collect_job' '{actual_job_name}' '1'\n" \ "CLABEL_COMMIT\n" \ "DIMENSION run_time 'run time' absolute 1 1\n" ND_INTERNAL_MONITORING_DISABLED = os.getenv("NETDATA_INTERNALS_MONITORING") == "NO" def create_runtime_chart(func): """ Calls a wrapped function, then prints runtime chart to stdout. Used as a decorator for SimpleService.create() method. The whole point of making 'create runtime chart' functionality as a decorator was to help users who re-implements create() in theirs classes. :param func: class method :return: """ def wrapper(*args, **kwargs): self = args[0] if not ND_INTERNAL_MONITORING_DISABLED: chart = RUNTIME_CHART_CREATE.format( job_name=self.name, actual_job_name=self.actual_job_name, update_every=self._runtime_counters.update_every, module_name=self.module_name, ) safe_print(chart) ok = func(*args, **kwargs) return ok return wrapper class ChartError(Exception): """Base-class for all exceptions raised by this module""" class DuplicateItemError(ChartError): """Occurs when user re-adds a chart or a dimension that has already been added""" class ItemTypeError(ChartError): """Occurs when user passes value of wrong type to Chart, Dimension or ChartVariable class""" class ItemValueError(ChartError): """Occurs when user passes inappropriate value to Chart, Dimension or ChartVariable class""" class Charts: """Represent a collection of charts All charts stored in a dict. Chart is a instance of Chart class. Charts adding must be done using Charts.add_chart() method only""" def __init__(self, job_name, actual_job_name, priority, cleanup, get_update_every, module_name): """ :param job_name: :param priority: :param get_update_every: """ self.job_name = job_name self.actual_job_name = actual_job_name self.priority = priority self.cleanup = cleanup self.get_update_every = get_update_every self.module_name = module_name self.charts = dict() def __len__(self): return len(self.charts) def __iter__(self): return iter(self.charts.values()) def __repr__(self): return 'Charts({0})'.format(self) def __str__(self): return str([chart for chart in self.charts]) def __contains__(self, item): return item in self.charts def __getitem__(self, item): return self.charts[item] def __delitem__(self, key): del self.charts[key] def __bool__(self): return bool(self.charts) def __nonzero__(self): return self.__bool__() def add_chart(self, params): """ Create Chart instance and add it to the dict Manually adds job name, priority and update_every to params. :param params: :return: """ params = [self.job_name()] + params new_chart = Chart(params) new_chart.params['update_every'] = self.get_update_every() new_chart.params['priority'] = self.priority new_chart.params['module_name'] = self.module_name new_chart.params['actual_job_name'] = self.actual_job_name self.priority += 1 self.charts[new_chart.id] = new_chart return new_chart def active_charts(self): return [chart.id for chart in self if not chart.flags.obsoleted] class Chart: """Represent a chart""" def __init__(self, params): """ :param params: """ if not isinstance(params, list): raise ItemTypeError("'chart' must be a list type") if not len(params) >= 8: raise ItemValueError("invalid value for 'chart', must be {0}".format(CHART_PARAMS)) self.params = dict(zip(CHART_PARAMS, (p or str() for p in params))) self.name = '{type}.{id}'.format(type=self.params['type'], id=self.params['id']) if self.params.get('chart_type') not in CHART_TYPES: self.params['chart_type'] = 'absolute' hidden = str(self.params.get('hidden', '')) self.params['hidden'] = 'hidden' if hidden == 'hidden' else '' self.dimensions = list() self.variables = set() self.flags = ChartFlags() self.penalty = 0 def __getattr__(self, item): try: return self.params[item] except KeyError: raise AttributeError("'{instance}' has no attribute '{attr}'".format(instance=repr(self), attr=item)) def __repr__(self): return 'Chart({0})'.format(self.id) def __str__(self): return self.id def __iter__(self): return iter(self.dimensions) def __contains__(self, item): return item in [dimension.id for dimension in self.dimensions] def add_variable(self, variable): """ :param variable: :return: """ self.variables.add(ChartVariable(variable)) def add_dimension(self, dimension): """ :param dimension: :return: """ dim = Dimension(dimension) if dim.id in self: raise DuplicateItemError("'{dimension}' already in '{chart}' dimensions".format(dimension=dim.id, chart=self.name)) self.refresh() self.dimensions.append(dim) return dim def del_dimension(self, dimension_id, hide=True): if dimension_id not in self: return idx = self.dimensions.index(dimension_id) dimension = self.dimensions[idx] if hide: dimension.params['hidden'] = 'hidden' dimension.params['obsolete'] = 'obsolete' self.create() self.dimensions.remove(dimension) def hide_dimension(self, dimension_id, reverse=False): if dimension_id not in self: return idx = self.dimensions.index(dimension_id) dimension = self.dimensions[idx] dimension.params['hidden'] = 'hidden' if not reverse else str() self.refresh() def create(self): """ :return: """ chart = CHART_CREATE.format(**self.params) labels = CLABEL_COLLECT_JOB.format(**self.params) + CLABEL_COMMIT dimensions = ''.join([dimension.create() for dimension in self.dimensions]) variables = ''.join([var.set(var.value) for var in self.variables if var]) self.flags.push = False self.flags.created = True safe_print(chart + labels + dimensions + variables) def can_be_updated(self, data): for dim in self.dimensions: if dim.get_value(data) is not None: return True return False def update(self, data, interval): updated_dimensions, updated_variables = str(), str() for dim in self.dimensions: value = dim.get_value(data) if value is not None: updated_dimensions += dim.set(value) for var in self.variables: value = var.get_value(data) if value is not None: updated_variables += var.set(value) if updated_dimensions: since_last = interval if self.flags.updated else 0 if self.flags.push: self.create() chart_begin = CHART_BEGIN.format(type=self.type, id=self.id, since_last=since_last) safe_print(chart_begin, updated_dimensions, updated_variables, 'END\n') self.flags.updated = True self.penalty = 0 else: self.penalty += 1 self.flags.updated = False return bool(updated_dimensions) def obsolete(self): self.flags.obsoleted = True if self.flags.created: safe_print(CHART_OBSOLETE.format(**self.params)) def refresh(self): self.penalty = 0 self.flags.push = True self.flags.obsoleted = False class Dimension: """Represent a dimension""" def __init__(self, params): """ :param params: """ if not isinstance(params, list): raise ItemTypeError("'dimension' must be a list type") if not params: raise ItemValueError("invalid value for 'dimension', must be {0}".format(DIMENSION_PARAMS)) self.params = dict(zip(DIMENSION_PARAMS, (p or str() for p in params))) self.params['name'] = self.params.get('name') or self.params['id'] if self.params.get('algorithm') not in DIMENSION_ALGORITHMS: self.params['algorithm'] = 'absolute' if not isinstance(self.params.get('multiplier'), int): self.params['multiplier'] = 1 if not isinstance(self.params.get('divisor'), int): self.params['divisor'] = 1 self.params.setdefault('hidden', '') self.params.setdefault('obsolete', '') def __getattr__(self, item): try: return self.params[item] except KeyError: raise AttributeError("'{instance}' has no attribute '{attr}'".format(instance=repr(self), attr=item)) def __repr__(self): return 'Dimension({0})'.format(self.id) def __str__(self): return self.id def __eq__(self, other): if not isinstance(other, Dimension): return self.id == other return self.id == other.id def __ne__(self, other): return not self == other def __hash__(self): return hash(repr(self)) def create(self): return DIMENSION_CREATE.format(**self.params) def set(self, value): """ :param value: : must be a digit :return: """ return DIMENSION_SET.format(id=self.id, value=value) def get_value(self, data): try: return int(data[self.id]) except (KeyError, TypeError): return None class ChartVariable: """Represent a chart variable""" def __init__(self, params): """ :param params: """ if not isinstance(params, list): raise ItemTypeError("'variable' must be a list type") if not params: raise ItemValueError("invalid value for 'variable' must be: {0}".format(VARIABLE_PARAMS)) self.params = dict(zip(VARIABLE_PARAMS, params)) self.params.setdefault('value', None) def __getattr__(self, item): try: return self.params[item] except KeyError: raise AttributeError("'{instance}' has no attribute '{attr}'".format(instance=repr(self), attr=item)) def __bool__(self): return self.value is not None def __nonzero__(self): return self.__bool__() def __repr__(self): return 'ChartVariable({0})'.format(self.id) def __str__(self): return self.id def __eq__(self, other): if isinstance(other, ChartVariable): return self.id == other.id return False def __ne__(self, other): return not self == other def __hash__(self): return hash(repr(self)) def set(self, value): return CHART_VARIABLE_SET.format(id=self.id, value=value) def get_value(self, data): try: return int(data[self.id]) except (KeyError, TypeError): return None class ChartFlags: def __init__(self): self.push = True self.created = False self.updated = False self.obsoleted = False