2 # -*- coding: utf-8 -*-
4 # Description: netdata python modules supervisor
5 # Author: Pawel Krupa (paulfantom)
12 MODULE_EXTENSION = ".chart.py"
14 # -----------------------------------------------------------------------------
17 PROGRAM = os.path.basename(__file__).replace(".plugin", "")
23 Print message on stderr.
27 sys.stderr.write(PROGRAM + " DEBUG :")
29 sys.stderr.write(" " + str(i))
30 sys.stderr.write("\n")
36 Print message on stderr.
38 sys.stderr.write(PROGRAM + " ERROR :")
40 sys.stderr.write(" " + str(i))
41 sys.stderr.write("\n")
47 Print message on stderr.
49 sys.stderr.write(PROGRAM + " INFO :")
51 sys.stderr.write(" " + str(i))
52 sys.stderr.write("\n")
58 Print message on stderr and exit.
60 sys.stderr.write(PROGRAM + " FATAL :")
62 sys.stderr.write(" " + str(i))
63 sys.stderr.write("\n")
65 sys.stdout.write('DISABLE\n')
69 # -----------------------------------------------------------------------------
70 # globals & python modules management
73 # https://github.com/firehol/netdata/wiki/External-Plugins#environment-variables
74 MODULES_DIR = os.path.abspath(os.getenv('NETDATA_PLUGINS_DIR',
75 os.path.dirname(__file__)) + "/../python.d") + "/"
76 CONFIG_DIR = os.getenv('NETDATA_CONFIG_DIR', "/etc/netdata/")
77 UPDATE_EVERY = os.getenv('NETDATA_UPDATE_EVERY', 1)
78 # directories should end with '/'
79 if CONFIG_DIR[-1] != "/":
81 sys.path.append(MODULES_DIR + "python_modules")
83 BASE_CONFIG = {'update_every': UPDATE_EVERY,
88 assert sys.version_info >= (3, 1)
89 import importlib.machinery
91 # change this hack below if we want PY_VERSION to be used in modules
93 # builtins.PY_VERSION = 3
95 info('Using python v3')
96 except (AssertionError, ImportError):
100 # change this hack below if we want PY_VERSION to be used in modules
102 # __builtin__.PY_VERSION = 2
104 info('Using python v2')
106 fatal('Cannot start. No importlib.machinery on python3 or lack of imp on python2')
110 fatal('Cannot find yaml library')
113 class PythonCharts(object):
115 Main class used to control every python module.
118 update_every=None, # TODO remove this
120 modules_path='../python.d/',
121 modules_configs='../conf.d/',
122 modules_disabled=None):
124 :param update_every: int
126 :param modules_path: str
127 :param modules_configs: str
128 :param modules_disabled: list
133 if modules_disabled is None:
134 modules_disabled = []
136 self.first_run = True
137 # set configuration directory
138 self.configs = modules_configs
141 loaded_modules = self._load_modules(modules_path, modules, modules_disabled)
143 # load configuration files
144 configured_modules = self._load_configs(loaded_modules)
146 # good economy and prosperity:
147 self.jobs = self._create_jobs(configured_modules) # type: list
148 # if DEBUG_FLAG and update_every is not None:
149 # for job in self.jobs:
150 # job.create_timetable(update_every)
153 def _import_module(path, name=None):
155 Try to import module using only its path.
162 name = path.split('/')[-1]
163 if name[-len(MODULE_EXTENSION):] != MODULE_EXTENSION:
165 name = name[:-len(MODULE_EXTENSION)]
168 return importlib.machinery.SourceFileLoader(name, path).load_module()
170 return imp.load_source(name, path)
171 except Exception as e:
175 def _load_modules(self, path, modules, disabled):
177 Load modules from 'modules' list or dynamically every file from 'path' (only .chart.py files)
180 :param disabled: list
184 # check if plugin directory exists
185 if not os.path.isdir(path):
186 fatal("cannot find charts directory ", path)
194 mod = self._import_module(path + m + MODULE_EXTENSION)
197 else: # exit if plugin is not found
198 fatal('no modules found.')
200 # scan directory specified in path and load all modules from there
201 names = os.listdir(path)
203 if mod.strip(MODULE_EXTENSION) in disabled:
204 error(mod + ": disabled module ", mod.strip(MODULE_EXTENSION))
206 m = self._import_module(path + mod)
208 debug(mod + ": loading module '" + path + mod + "'")
212 def _load_configs(self, modules):
214 Append configuration in list named `config` to every module.
215 For multi-job modules `config` list is created in _parse_config,
216 otherwise it is created here based on BASE_CONFIG prototype with None as identifier.
221 configfile = self.configs + mod.__name__ + ".conf"
222 if os.path.isfile(configfile):
223 debug(mod.__name__ + ": loading module configuration: '" + configfile + "'")
227 self._parse_config(mod, read_config(configfile)))
228 except Exception as e:
229 error(mod.__name__ + ": cannot parse configuration file '" + configfile + "':", str(e))
231 error(mod.__name__ + ": configuration file '" + configfile + "' not found. Using defaults.")
232 # set config if not found
233 if not hasattr(mod, 'config'):
234 mod.config = {None: {}}
235 for var in BASE_CONFIG:
237 mod.config[None][var] = getattr(mod, var)
238 except AttributeError:
239 mod.config[None][var] = BASE_CONFIG[var]
243 def _parse_config(module, config):
245 Parse configuration file or extract configuration from module file.
246 Example of returned dictionary:
252 :param module: object
258 for key in BASE_CONFIG:
260 # get defaults from module config
261 defaults[key] = int(config.pop(key))
262 except (KeyError, ValueError):
264 # get defaults from module source code
265 defaults[key] = getattr(module, key)
266 except (KeyError, ValueError):
267 # if above failed, get defaults from global dict
268 defaults[key] = BASE_CONFIG[key]
270 # check if there are dict in config dict
273 if type(config[name]) is dict:
277 # assign variables needed by supervisor to every job configuration
281 if key not in config[name]:
282 config[name][key] = defaults[key]
283 # if only one job is needed, values doesn't have to be in dict (in YAML)
285 config = {None: config.copy()}
286 config[None].update(defaults)
288 # return dictionary of jobs where every job has BASE_CONFIG variables
292 def _create_jobs(modules):
294 Create jobs based on module.config dictionary and module.Service class definition.
299 for module in modules:
300 for name in module.config:
302 conf = module.config[name]
304 job = module.Service(configuration=conf, name=name)
305 except Exception as e:
306 error(module.__name__ +
307 ("/" + str(name) if name is not None else "") +
308 ": cannot start job: '" +
312 # set chart_name (needed to plot run time graphs)
313 job.chart_name = module.__name__
315 job.chart_name += "_" + name
317 debug(module.__name__ + ("/" + str(name) if name is not None else "") + ": job added")
319 return [j for j in jobs if j is not None]
321 def _stop(self, job, reason=None):
323 Stop specified job and remove it from self.jobs list
324 Also notifies user about job failure if DEBUG_FLAG is set
328 prefix = job.__module__
329 if job.name is not None:
330 prefix += "/" + job.name
333 self.jobs.remove(job)
336 elif reason[:3] == "no ":
338 "does not seem to have " +
340 "() function. Disabling it.")
341 elif reason[:7] == "failed ":
344 "() function reports failure.")
345 elif reason[:13] == "configuration":
347 "configuration file '" +
350 ".conf' not found. Using defaults.")
351 elif reason[:11] == "misbehaving":
352 error(prefix + "is " + reason)
356 Tries to execute check() on every job.
357 This cannot fail thus it is catching every exception
358 If job.check() fails job is stopped
361 while i < len(self.jobs):
365 self._stop(job, "failed check")
368 except AttributeError:
369 self._stop(job, "no check")
370 except (UnboundLocalError, Exception) as e:
371 self._stop(job, "misbehaving. Reason: " + str(e))
375 Tries to execute create() on every job.
376 This cannot fail thus it is catching every exception.
377 If job.create() fails job is stopped.
378 This is also creating job run time chart.
381 while i < len(self.jobs):
385 self._stop(job, "failed create")
387 chart = job.chart_name
389 "CHART netdata.plugin_pythond_" +
391 " '' 'Execution time for " +
393 " plugin' 'milliseconds / run' python.d netdata.plugin_python area 145000 " +
394 str(job.timetable['freq']) +
396 sys.stdout.write("DIMENSION run_time 'run time' absolute 1 1\n\n")
399 except AttributeError:
400 self._stop(job, "no create")
401 except (UnboundLocalError, Exception) as e:
402 self._stop(job, "misbehaving. Reason: " + str(e))
404 def _update_job(self, job):
406 Tries to execute update() on specified job.
407 This cannot fail thus it is catching every exception.
408 If job.update() returns False, number of retries_left is decremented.
409 If there are no more retries, job is stopped.
410 Job is also stopped if it throws an exception.
411 This is also updating job run time chart.
412 Return False if job is stopped
416 t_start = time.time()
417 # check if it is time to execute job update() function
418 if job.timetable['next'] > t_start:
424 since_last = int((t_start - job.timetable['last']) * 1000000)
425 if not job.update(since_last):
426 if job.retries_left <= 0:
427 self._stop(job, "update failed")
429 job.retries_left -= 1
430 job.timetable['next'] += job.timetable['freq']
432 except AttributeError:
433 self._stop(job, "no update")
435 except (UnboundLocalError, Exception) as e:
436 self._stop(job, "misbehaving. Reason: " + str(e))
439 job.timetable['next'] = t_end - (t_end % job.timetable['freq']) + job.timetable['freq']
440 # draw performance graph
441 sys.stdout.write("BEGIN netdata.plugin_pythond_" + job.chart_name + " " + str(since_last) + '\n')
442 sys.stdout.write("SET run_time = " + str(int((t_end - t_start) * 1000)) + '\n')
443 sys.stdout.write("END\n")
445 job.timetable['last'] = t_start
446 job.retries_left = job.retries
447 self.first_run = False
452 Tries to execute update() on every job by using _update_job()
453 This will stay forever and ever and ever forever and ever it'll be the one...
455 self.first_run = True
459 while i < len(self.jobs):
461 if self._update_job(job):
463 next_runs.append(job.timetable['next'])
467 if len(next_runs) == 0:
468 fatal('no python.d modules loaded.')
469 time.sleep(min(next_runs) - time.time())
472 def read_config(path):
474 Read YAML configuration from specified file
479 with open(path, 'r') as stream:
480 config = yaml.load(stream)
481 except (OSError, IOError):
482 error(str(path), "is not a valid configuration file")
484 except yaml.YAMLError as e:
485 error(str(path), "is malformed:", e)
490 def parse_cmdline(directory, *commands):
492 Parse parameters from command line.
493 :param directory: str
494 :param commands: list of str
499 update_every = UPDATE_EVERY
502 for cmd in commands[1:]:
505 elif cmd == "debug" or cmd == "all":
507 # redirect stderr to stdout?
508 elif os.path.isfile(directory + cmd + ".chart.py") or os.path.isfile(directory + cmd):
510 mods.append(cmd.replace(".chart.py", ""))
513 update_every = int(cmd)
515 update_every = UPDATE_EVERY
517 debug("started from", commands[0], "with options:", *commands[1:])
519 UPDATE_EVERY = update_every
520 return {'modules': mods}
523 # if __name__ == '__main__':
528 global PROGRAM, DEBUG_FLAG
529 PROGRAM = sys.argv[0].split('/')[-1].split('.plugin')[0]
531 # read configuration file
533 configfile = CONFIG_DIR + "python.d.conf"
535 update_every = UPDATE_EVERY
536 conf = read_config(configfile)
539 # exit the whole plugin when 'enable: no' is set in 'python.d.conf'
540 if str(conf['enable']) is False:
541 fatal('disabled in configuration file.\n')
542 except (KeyError, TypeError):
545 update_every = conf['update_every']
546 except (KeyError, TypeError):
547 pass # use default update_every from NETDATA_UPDATE_EVERY
549 DEBUG_FLAG = conf['debug']
550 except (KeyError, TypeError):
552 for k, v in conf.items():
553 if k in ("update_every", "debug", "enable"):
558 # parse passed command line arguments
559 modules = parse_cmdline(MODULES_DIR, *sys.argv)
560 info("MODULES_DIR='" + MODULES_DIR +
561 "', CONFIG_DIR='" + CONFIG_DIR +
562 "', UPDATE_EVERY=" + str(UPDATE_EVERY) +
563 ", ONLY_MODULES=" + str(modules))
566 charts = PythonCharts(update_every, modules, MODULES_DIR, CONFIG_DIR + "python.d/", disabled)
573 if __name__ == '__main__':