charts.py 13 KB

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