3 # Description: netdata python modules supervisor
4 # Author: Pawel Krupa (paulfantom)
11 # https://github.com/firehol/netdata/wiki/External-Plugins#environment-variables
12 MODULES_DIR = os.getenv('NETDATA_PLUGINS_DIR',
13 os.path.abspath(__file__).strip("python.d.plugin.py").replace("plugins.d", ""))
14 CONFIG_DIR = os.getenv('NETDATA_CONFIG_DIR', "/etc/netdata/")
15 INTERVAL = os.getenv('NETDATA_UPDATE_EVERY', None)
16 # directories should end with '/'
17 if MODULES_DIR[-1] != "/":
19 MODULES_DIR += "python.d/"
20 if CONFIG_DIR[-1] != "/":
22 sys.path.append(MODULES_DIR + "python_modules")
26 assert sys.version_info >= (3, 1)
27 import importlib.machinery
29 # change this hack below if we want PY_VERSION to be used in modules
31 # builtins.PY_VERSION = 3
33 sys.stderr.write('python.d.plugin: Using python 3\n')
34 except (AssertionError, ImportError):
38 # change this hack below if we want PY_VERSION to be used in modules
40 # __builtin__.PY_VERSION = 2
42 sys.stderr.write('python.d.plugin: Using python 2\n')
44 sys.stderr.write('python.d.plugin: Cannot start. No importlib.machinery on python3 or lack of imp on python2\n')
45 sys.stdout.write('DISABLE\n')
50 sys.stderr.write('python.d.plugin: Cannot find yaml library\n')
51 sys.stdout.write('DISABLE\n')
55 PROGRAM = "python.d.plugin"
56 MODULE_EXTENSION = ".chart.py"
57 BASE_CONFIG = {'update_every': 10,
62 class PythonCharts(object):
64 Main class used to control every python module.
69 modules_path='../python.d/',
70 modules_configs='../conf.d/',
71 modules_disabled=None):
75 :param modules_path: str
76 :param modules_configs: str
77 :param modules_disabled: list
82 if modules_disabled is None:
86 # set configuration directory
87 self.configs = modules_configs
90 loaded_modules = self._load_modules(modules_path, modules, modules_disabled)
92 # load configuration files
93 configured_modules = self._load_configs(loaded_modules)
95 # good economy and prosperity:
96 self.jobs = self._create_jobs(configured_modules) # type: list
97 if DEBUG_FLAG and interval is not None:
99 job.create_timetable(interval)
102 def _import_module(path, name=None):
104 Try to import module using only its path.
111 name = path.split('/')[-1]
112 if name[-len(MODULE_EXTENSION):] != MODULE_EXTENSION:
114 name = name[:-len(MODULE_EXTENSION)]
117 return importlib.machinery.SourceFileLoader(name, path).load_module()
119 return imp.load_source(name, path)
120 except Exception as e:
124 def _load_modules(self, path, modules, disabled):
126 Load modules from 'modules' list or dynamically every file from 'path' (only .chart.py files)
129 :param disabled: list
133 # check if plugin directory exists
134 if not os.path.isdir(path):
135 debug("cannot find charts directory ", path)
136 sys.stdout.write("DISABLE\n")
145 mod = self._import_module(path + m + MODULE_EXTENSION)
148 else: # exit if plugin is not found
149 sys.stdout.write("DISABLE")
153 # scan directory specified in path and load all modules from there
154 names = os.listdir(path)
156 if mod.strip(MODULE_EXTENSION) in disabled:
157 debug("disabling:", mod.strip(MODULE_EXTENSION))
159 m = self._import_module(path + mod)
161 debug("loading module: '" + path + mod + "'")
165 def _load_configs(self, modules):
167 Append configuration in list named `config` to every module.
168 For multi-job modules `config` list is created in _parse_config,
169 otherwise it is created here based on BASE_CONFIG prototype with None as identifier.
174 configfile = self.configs + mod.__name__ + ".conf"
175 if os.path.isfile(configfile):
176 debug("loading module configuration: '" + configfile + "'")
180 self._parse_config(mod, read_config(configfile)))
181 except Exception as e:
182 debug("something went wrong while loading configuration", e)
185 ": configuration file '" +
187 "' not found. Using defaults.")
188 # set config if not found
189 if not hasattr(mod, 'config'):
190 mod.config = {None: {}}
191 for var in BASE_CONFIG:
193 mod.config[None][var] = getattr(mod, var)
194 except AttributeError:
195 mod.config[None][var] = BASE_CONFIG[var]
199 def _parse_config(module, config):
201 Parse configuration file or extract configuration from module file.
202 Example of returned dictionary:
208 :param module: object
214 for key in BASE_CONFIG:
216 # get defaults from module config
217 defaults[key] = int(config.pop(key))
218 except (KeyError, ValueError):
220 # get defaults from module source code
221 defaults[key] = getattr(module, key)
222 except (KeyError, ValueError):
223 # if above failed, get defaults from global dict
224 defaults[key] = BASE_CONFIG[key]
226 # check if there are dict in config dict
229 if type(config[name]) is dict:
233 # assign variables needed by supervisor to every job configuration
237 if key not in config[name]:
238 config[name][key] = defaults[key]
239 # if only one job is needed, values doesn't have to be in dict (in YAML)
241 config = {None: config.copy()}
242 config[None].update(defaults)
244 # return dictionary of jobs where every job has BASE_CONFIG variables
248 def _create_jobs(modules):
250 Create jobs based on module.config dictionary and module.Service class definition.
255 for module in modules:
256 for name in module.config:
258 conf = module.config[name]
260 job = module.Service(configuration=conf, name=name)
261 except Exception as e:
262 debug(module.__name__ +
263 ": Couldn't start job named " +
269 # set chart_name (needed to plot run time graphs)
270 job.chart_name = module.__name__
272 job.chart_name += "_" + name
275 return [j for j in jobs if j is not None]
277 def _stop(self, job, reason=None):
279 Stop specified job and remove it from self.jobs list
280 Also notifies user about job failure if DEBUG_FLAG is set
285 if job.name is not None:
286 prefix = "'" + job.name + "' in "
288 prefix += "'" + job.__module__ + MODULE_EXTENSION + "' "
289 self.jobs.remove(job)
292 elif reason[:3] == "no ":
294 "does not seem to have " +
296 "() function. Disabling it.")
297 elif reason[:7] == "failed ":
300 "() function reports failure.")
301 elif reason[:13] == "configuration":
303 "configuration file '" +
306 ".conf' not found. Using defaults.")
307 elif reason[:11] == "misbehaving":
308 debug(prefix + "is " + reason)
312 Tries to execute check() on every job.
313 This cannot fail thus it is catching every exception
314 If job.check() fails job is stopped
317 while i < len(self.jobs):
321 self._stop(job, "failed check")
324 except AttributeError:
325 self._stop(job, "no check")
326 except (UnboundLocalError, Exception) as e:
327 self._stop(job, "misbehaving. Reason: " + str(e))
331 Tries to execute create() on every job.
332 This cannot fail thus it is catching every exception.
333 If job.create() fails job is stopped.
334 This is also creating job run time chart.
337 while i < len(self.jobs):
341 self._stop(job, "failed create")
343 chart = job.chart_name
345 "CHART netdata.plugin_pythond_" +
347 " '' 'Execution time for " +
349 " plugin' 'milliseconds / run' python.d netdata.plugin_python area 145000 " +
350 str(job.timetable['freq']) +
352 sys.stdout.write("DIMENSION run_time 'run time' absolute 1 1\n\n")
355 except AttributeError:
356 self._stop(job, "no create")
357 except (UnboundLocalError, Exception) as e:
358 self._stop(job, "misbehaving. Reason: " + str(e))
360 def _update_job(self, job):
362 Tries to execute update() on specified job.
363 This cannot fail thus it is catching every exception.
364 If job.update() returns False, number of retries_left is decremented.
365 If there are no more retries, job is stopped.
366 Job is also stopped if it throws an exception.
367 This is also updating job run time chart.
368 Return False if job is stopped
372 t_start = time.time()
373 # check if it is time to execute job update() function
374 if job.timetable['next'] > t_start:
380 since_last = int((t_start - job.timetable['last']) * 1000000)
381 if not job.update(since_last):
382 if job.retries_left <= 0:
383 self._stop(job, "update failed")
385 job.retries_left -= 1
386 job.timetable['next'] += job.timetable['freq']
388 except AttributeError:
389 self._stop(job, "no update")
391 except (UnboundLocalError, Exception) as e:
392 self._stop(job, "misbehaving. Reason: " + str(e))
395 job.timetable['next'] = t_end - (t_end % job.timetable['freq']) + job.timetable['freq']
396 # draw performance graph
397 sys.stdout.write("BEGIN netdata.plugin_pythond_" + job.chart_name + " " + str(since_last) + '\n')
398 sys.stdout.write("SET run_time = " + str(int((t_end - t_start) * 1000)) + '\n')
399 sys.stdout.write("END\n")
401 job.timetable['last'] = t_start
402 job.retries_left = job.retries
403 self.first_run = False
408 Tries to execute update() on every job by using _update_job()
409 This will stay forever and ever and ever forever and ever it'll be the one...
411 self.first_run = True
415 while i < len(self.jobs):
417 if self._update_job(job):
419 next_runs.append(job.timetable['next'])
423 if len(next_runs) == 0:
424 debug("No plugins loaded")
425 sys.stdout.write("DISABLE\n")
427 time.sleep(min(next_runs) - time.time())
430 def read_config(path):
432 Read YAML configuration from specified file
437 with open(path, 'r') as stream:
438 config = yaml.load(stream)
439 except (OSError, IOError):
440 debug(str(path), "is not a valid configuration file")
442 except yaml.YAMLError as e:
443 debug(str(path), "is malformed:", e)
450 Print message on stderr.
454 sys.stderr.write(PROGRAM + ":")
456 sys.stderr.write(" " + str(i))
457 sys.stderr.write("\n")
461 def parse_cmdline(directory, *commands):
463 Parse parameters from command line.
464 :param directory: str
465 :param commands: list of str
472 for cmd in commands[1:]:
475 elif cmd == "debug" or cmd == "all":
477 # redirect stderr to stdout?
478 elif os.path.isfile(directory + cmd + ".chart.py") or os.path.isfile(directory + cmd):
480 mods.append(cmd.replace(".chart.py", ""))
488 debug("started from", commands[0], "with options:", *commands[1:])
489 if len(mods) == 0 and DEBUG_FLAG is False:
492 return {'interval': interval,
496 # if __name__ == '__main__':
501 global PROGRAM, DEBUG_FLAG
502 PROGRAM = sys.argv[0].split('/')[-1].split('.plugin')[0]
504 # read configuration file
506 configfile = CONFIG_DIR + "python.d.conf"
509 conf = read_config(configfile)
512 if str(conf['enable']) is False:
513 debug("disabled in configuration file")
514 sys.stdout.write("DISABLE\n")
516 except (KeyError, TypeError):
519 interval = conf['interval']
520 except (KeyError, TypeError):
521 pass # use default interval from NETDATA_UPDATE_EVERY
523 DEBUG_FLAG = conf['debug']
524 except (KeyError, TypeError):
526 for k, v in conf.items():
527 if k in ("interval", "debug", "enable"):
532 # parse passed command line arguments
533 out = parse_cmdline(MODULES_DIR, *sys.argv)
534 modules = out['modules']
535 if out['interval'] is not None:
536 interval = out['interval']
539 charts = PythonCharts(interval, modules, MODULES_DIR, CONFIG_DIR + "python.d/", disabled)
543 sys.stdout.write("DISABLE")
546 if __name__ == '__main__':