src/collectors/python.d.plugin/python_modules/bases/charts.py
# -*- 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: <bound method>
:param priority: <int>
:param get_update_every: <bound method>
"""
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: <list>
: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: <list>
"""
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: <list>
:return:
"""
self.variables.add(ChartVariable(variable))
def add_dimension(self, dimension):
"""
:param dimension: <list>
: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: <list>
"""
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: <str>: 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: <list>
"""
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