| 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 |