charts.py 13 KB

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