2 # -*- coding: utf-8 -*-
4 # Description: netdata python modules supervisor
5 # Author: Pawel Krupa (paulfantom)
11 # -----------------------------------------------------------------------------
12 # globals & environment setup
13 # https://github.com/firehol/netdata/wiki/External-Plugins#environment-variables
14 MODULE_EXTENSION = ".chart.py"
15 BASE_CONFIG = {'update_every': os.getenv('NETDATA_UPDATE_EVERY', 1),
19 MODULES_DIR = os.path.abspath(os.getenv('NETDATA_PLUGINS_DIR',
20 os.path.dirname(__file__)) + "/../python.d") + "/"
21 CONFIG_DIR = os.getenv('NETDATA_CONFIG_DIR', "/etc/netdata/")
22 # directories should end with '/'
23 if CONFIG_DIR[-1] != "/":
25 sys.path.append(MODULES_DIR + "python_modules")
27 PROGRAM = os.path.basename(__file__).replace(".plugin", "")
29 OVERRIDE_UPDATE_EVERY = False
32 # -----------------------------------------------------------------------------
36 Print message on stderr.
40 sys.stderr.write(PROGRAM + " DEBUG :")
42 sys.stderr.write(" " + str(i))
43 sys.stderr.write("\n")
49 Print message on stderr.
51 sys.stderr.write(PROGRAM + " ERROR :")
53 sys.stderr.write(" " + str(i))
54 sys.stderr.write("\n")
60 Print message on stderr.
62 sys.stderr.write(PROGRAM + " INFO :")
64 sys.stderr.write(" " + str(i))
65 sys.stderr.write("\n")
71 Print message on stderr and exit.
73 sys.stderr.write(PROGRAM + " FATAL :")
75 sys.stderr.write(" " + str(i))
76 sys.stderr.write("\n")
78 sys.stdout.write('DISABLE\n')
82 # -----------------------------------------------------------------------------
83 # third party and version specific python modules management
85 assert sys.version_info >= (3, 1)
86 import importlib.machinery
88 # change this hack below if we want PY_VERSION to be used in modules
90 # builtins.PY_VERSION = 3
92 info('Using python v3')
93 except (AssertionError, ImportError):
97 # change this hack below if we want PY_VERSION to be used in modules
99 # __builtin__.PY_VERSION = 2
101 info('Using python v2')
103 fatal('Cannot start. No importlib.machinery on python3 or lack of imp on python2')
107 fatal('Cannot find yaml library')
110 class PythonCharts(object):
112 Main class used to control every python module.
116 modules_path='../python.d/',
117 modules_configs='../conf.d/',
118 modules_disabled=None):
120 :param update_every: int
122 :param modules_path: str
123 :param modules_configs: str
124 :param modules_disabled: list
129 if modules_disabled is None:
130 modules_disabled = []
132 self.first_run = True
133 # set configuration directory
134 self.configs = modules_configs
137 loaded_modules = self._load_modules(modules_path, modules, modules_disabled)
139 # load configuration files
140 configured_modules = self._load_configs(loaded_modules)
142 # good economy and prosperity:
143 self.jobs = self._create_jobs(configured_modules) # type: list
145 # enable timetable override like `python.d.plugin mysql debug 1`
146 if DEBUG_FLAG and OVERRIDE_UPDATE_EVERY:
147 for job in self.jobs:
148 job.create_timetable(BASE_CONFIG['update_every'])
151 def _import_module(path, name=None):
153 Try to import module using only its path.
160 name = path.split('/')[-1]
161 if name[-len(MODULE_EXTENSION):] != MODULE_EXTENSION:
163 name = name[:-len(MODULE_EXTENSION)]
166 return importlib.machinery.SourceFileLoader(name, path).load_module()
168 return imp.load_source(name, path)
169 except Exception as e:
173 def _load_modules(self, path, modules, disabled):
175 Load modules from 'modules' list or dynamically every file from 'path' (only .chart.py files)
178 :param disabled: list
182 # check if plugin directory exists
183 if not os.path.isdir(path):
184 fatal("cannot find charts directory ", path)
192 mod = self._import_module(path + m + MODULE_EXTENSION)
195 else: # exit if plugin is not found
196 fatal('no modules found.')
198 # scan directory specified in path and load all modules from there
199 names = os.listdir(path)
201 if mod.strip(MODULE_EXTENSION) in disabled:
202 error(mod + ": disabled module ", mod.strip(MODULE_EXTENSION))
204 m = self._import_module(path + mod)
206 debug(mod + ": loading module '" + path + mod + "'")
210 def _load_configs(self, modules):
212 Append configuration in list named `config` to every module.
213 For multi-job modules `config` list is created in _parse_config,
214 otherwise it is created here based on BASE_CONFIG prototype with None as identifier.
219 configfile = self.configs + mod.__name__ + ".conf"
220 if os.path.isfile(configfile):
221 debug(mod.__name__ + ": loading module configuration: '" + configfile + "'")
225 self._parse_config(mod, read_config(configfile)))
226 except Exception as e:
227 error(mod.__name__ + ": cannot parse configuration file '" + configfile + "':", str(e))
229 error(mod.__name__ + ": configuration file '" + configfile + "' not found. Using defaults.")
230 # set config if not found
231 if not hasattr(mod, 'config'):
232 mod.config = {None: {}}
233 for var in BASE_CONFIG:
235 mod.config[None][var] = getattr(mod, var)
236 except AttributeError:
237 mod.config[None][var] = BASE_CONFIG[var]
241 def _parse_config(module, config):
243 Parse configuration file or extract configuration from module file.
244 Example of returned dictionary:
250 :param module: object
256 for key in BASE_CONFIG:
258 # get defaults from module config
259 defaults[key] = int(config.pop(key))
260 except (KeyError, ValueError):
262 # get defaults from module source code
263 defaults[key] = getattr(module, key)
264 except (KeyError, ValueError):
265 # if above failed, get defaults from global dict
266 defaults[key] = BASE_CONFIG[key]
268 # check if there are dict in config dict
271 if type(config[name]) is dict:
275 # assign variables needed by supervisor to every job configuration
279 if key not in config[name]:
280 config[name][key] = defaults[key]
281 # if only one job is needed, values doesn't have to be in dict (in YAML)
283 config = {None: config.copy()}
284 config[None].update(defaults)
286 # return dictionary of jobs where every job has BASE_CONFIG variables
290 def _create_jobs(modules):
292 Create jobs based on module.config dictionary and module.Service class definition.
297 for module in modules:
298 for name in module.config:
300 conf = module.config[name]
302 job = module.Service(configuration=conf, name=name)
303 except Exception as e:
304 error(module.__name__ +
305 ("/" + str(name) if name is not None else "") +
306 ": cannot start job: '" +
310 # set chart_name (needed to plot run time graphs)
311 job.chart_name = module.__name__
313 job.chart_name += "_" + name
315 debug(module.__name__ + ("/" + str(name) if name is not None else "") + ": job added")
317 return [j for j in jobs if j is not None]
319 def _stop(self, job, reason=None):
321 Stop specified job and remove it from self.jobs list
322 Also notifies user about job failure if DEBUG_FLAG is set
326 prefix = job.__module__
327 if job.name is not None:
328 prefix += "/" + job.name
331 self.jobs.remove(job)
334 elif reason[:3] == "no ":
336 "does not seem to have " +
338 "() function. Disabling it.")
339 elif reason[:7] == "failed ":
342 "() function reports failure.")
343 elif reason[:13] == "configuration":
345 "configuration file '" +
348 ".conf' not found. Using defaults.")
349 elif reason[:11] == "misbehaving":
350 error(prefix + "is " + reason)
354 Tries to execute check() on every job.
355 This cannot fail thus it is catching every exception
356 If job.check() fails job is stopped
359 while i < len(self.jobs):
363 self._stop(job, "failed check")
366 except AttributeError:
367 self._stop(job, "no check")
368 except (UnboundLocalError, Exception) as e:
369 self._stop(job, "misbehaving. Reason: " + str(e))
373 Tries to execute create() on every job.
374 This cannot fail thus it is catching every exception.
375 If job.create() fails job is stopped.
376 This is also creating job run time chart.
379 while i < len(self.jobs):
383 self._stop(job, "failed create")
385 chart = job.chart_name
387 "CHART netdata.plugin_pythond_" +
389 " '' 'Execution time for " +
391 " plugin' 'milliseconds / run' python.d netdata.plugin_python area 145000 " +
392 str(job.timetable['freq']) +
394 sys.stdout.write("DIMENSION run_time 'run time' absolute 1 1\n\n")
397 except AttributeError:
398 self._stop(job, "no create")
399 except (UnboundLocalError, Exception) as e:
400 self._stop(job, "misbehaving. Reason: " + str(e))
402 def _update_job(self, job):
404 Tries to execute update() on specified job.
405 This cannot fail thus it is catching every exception.
406 If job.update() returns False, number of retries_left is decremented.
407 If there are no more retries, job is stopped.
408 Job is also stopped if it throws an exception.
409 This is also updating job run time chart.
410 Return False if job is stopped
414 t_start = time.time()
415 # check if it is time to execute job update() function
416 if job.timetable['next'] > t_start:
422 since_last = int((t_start - job.timetable['last']) * 1000000)
423 if not job.update(since_last):
424 if job.retries_left <= 0:
425 self._stop(job, "update failed")
427 job.retries_left -= 1
428 job.timetable['next'] += job.timetable['freq']
430 except AttributeError:
431 self._stop(job, "no update")
433 except (UnboundLocalError, Exception) as e:
434 self._stop(job, "misbehaving. Reason: " + str(e))
437 job.timetable['next'] = t_end - (t_end % job.timetable['freq']) + job.timetable['freq']
438 # draw performance graph
439 sys.stdout.write("BEGIN netdata.plugin_pythond_" + job.chart_name + " " + str(since_last) + '\n')
440 sys.stdout.write("SET run_time = " + str(int((t_end - t_start) * 1000)) + '\n')
441 sys.stdout.write("END\n")
443 job.timetable['last'] = t_start
444 job.retries_left = job.retries
445 self.first_run = False
450 Tries to execute update() on every job by using _update_job()
451 This will stay forever and ever and ever forever and ever it'll be the one...
453 self.first_run = True
457 while i < len(self.jobs):
459 if self._update_job(job):
461 next_runs.append(job.timetable['next'])
465 if len(next_runs) == 0:
466 fatal('no python.d modules loaded.')
467 time.sleep(min(next_runs) - time.time())
470 def read_config(path):
472 Read YAML configuration from specified file
477 with open(path, 'r') as stream:
478 config = yaml.load(stream)
479 except (OSError, IOError):
480 error(str(path), "is not a valid configuration file")
482 except yaml.YAMLError as e:
483 error(str(path), "is malformed:", e)
488 def parse_cmdline(directory, *commands):
490 Parse parameters from command line.
491 :param directory: str
492 :param commands: list of str
496 global OVERRIDE_UPDATE_EVERY
500 for cmd in commands[1:]:
503 elif cmd == "debug" or cmd == "all":
505 # redirect stderr to stdout?
506 elif os.path.isfile(directory + cmd + ".chart.py") or os.path.isfile(directory + cmd):
508 mods.append(cmd.replace(".chart.py", ""))
511 BASE_CONFIG['update_every'] = int(cmd)
512 OVERRIDE_UPDATE_EVERY = True
516 debug("started from", commands[0], "with options:", *commands[1:])
521 # if __name__ == '__main__':
526 global DEBUG_FLAG, BASE_CONFIG
528 # read configuration file
530 configfile = CONFIG_DIR + "python.d.conf"
532 conf = read_config(configfile)
535 # exit the whole plugin when 'enable: no' is set in 'python.d.conf'
536 if str(conf['enable']) is False:
537 fatal('disabled in configuration file.\n')
538 except (KeyError, TypeError):
541 for param in BASE_CONFIG:
542 BASE_CONFIG[param] = conf[param]
543 except (KeyError, TypeError):
544 pass # use default update_every from NETDATA_UPDATE_EVERY
546 DEBUG_FLAG = conf['debug']
547 except (KeyError, TypeError):
549 for k, v in conf.items():
550 if k in ("update_every", "debug", "enable"):
555 # parse passed command line arguments
556 modules = parse_cmdline(MODULES_DIR, *sys.argv)
557 info("MODULES_DIR='" + MODULES_DIR +
558 "', CONFIG_DIR='" + CONFIG_DIR +
559 "', UPDATE_EVERY=" + str(BASE_CONFIG['update_every']) +
560 ", ONLY_MODULES=" + str(modules))
563 charts = PythonCharts(modules, MODULES_DIR, CONFIG_DIR + "python.d/", disabled)
570 if __name__ == '__main__':