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", "python.d"))
14 CONFIG_DIR = os.getenv('NETDATA_CONFIG_DIR', "/etc/netdata/")
15 INTERVAL = os.getenv('NETDATA_UPDATE_EVERY', 1)
16 # directories should end with '/'
17 if MODULES_DIR[-1] != "/":
19 if CONFIG_DIR[-1] != "/":
21 sys.path.append(MODULES_DIR + "python_modules")
25 assert sys.version_info >= (3, 1)
26 import importlib.machinery
28 # change this hack below if we want PY_VERSION to be used in modules
30 # builtins.PY_VERSION = 3
32 sys.stderr.write('python.d.plugin: Using python 3\n')
33 except (AssertionError, ImportError):
37 # change this hack below if we want PY_VERSION to be used in modules
39 # __builtin__.PY_VERSION = 2
41 sys.stderr.write('python.d.plugin: Using python 2\n')
43 sys.stderr.write('python.d.plugin: Cannot start. No importlib.machinery on python3 or lack of imp on python2\n')
44 sys.stdout.write('DISABLE\n')
49 sys.stderr.write('python.d.plugin: Cannot find yaml library\n')
50 sys.stdout.write('DISABLE\n')
54 PROGRAM = "python.d.plugin"
55 MODULE_EXTENSION = ".chart.py"
56 BASE_CONFIG = {'update_every': 10,
61 class PythonCharts(object):
63 Main class used to control every python module.
68 modules_path='../python.d/',
69 modules_configs='../conf.d/',
70 modules_disabled=None):
74 :param modules_path: str
75 :param modules_configs: str
76 :param modules_disabled: list
81 if modules_disabled is None:
85 # set configuration directory
86 self.configs = modules_configs
89 loaded_modules = self._load_modules(modules_path, modules, modules_disabled)
91 # load configuration files
92 configured_modules = self._load_configs(loaded_modules)
94 # good economy and prosperity:
95 self.jobs = self._create_jobs(configured_modules) # type: list
96 if DEBUG_FLAG and interval is not None:
98 job.create_timetable(interval)
101 def _import_module(path, name=None):
103 Try to import module using only its path.
110 name = path.split('/')[-1]
111 if name[-len(MODULE_EXTENSION):] != MODULE_EXTENSION:
113 name = name[:-len(MODULE_EXTENSION)]
116 return importlib.machinery.SourceFileLoader(name, path).load_module()
118 return imp.load_source(name, path)
119 except Exception as e:
123 def _load_modules(self, path, modules, disabled):
125 Load modules from 'modules' list or dynamically every file from 'path' (only .chart.py files)
128 :param disabled: list
132 # check if plugin directory exists
133 if not os.path.isdir(path):
134 debug("cannot find charts directory ", path)
135 sys.stdout.write("DISABLE\n")
144 mod = self._import_module(path + m + MODULE_EXTENSION)
147 else: # exit if plugin is not found
148 sys.stdout.write("DISABLE")
152 # scan directory specified in path and load all modules from there
153 names = os.listdir(path)
155 if mod.strip(MODULE_EXTENSION) in disabled:
156 debug("disabling:", mod.strip(MODULE_EXTENSION))
158 m = self._import_module(path + mod)
160 debug("loading module: '" + path + mod + "'")
164 def _load_configs(self, modules):
166 Append configuration in list named `config` to every module.
167 For multi-job modules `config` list is created in _parse_config,
168 otherwise it is created here based on BASE_CONFIG prototype with None as identifier.
173 configfile = self.configs + mod.__name__ + ".conf"
174 if os.path.isfile(configfile):
175 debug("loading module configuration: '" + configfile + "'")
179 self._parse_config(mod, read_config(configfile)))
180 except Exception as e:
181 debug("something went wrong while loading configuration", e)
184 ": configuration file '" +
186 "' not found. Using defaults.")
187 # set config if not found
188 if not hasattr(mod, 'config'):
189 mod.config = {None: {}}
190 for var in BASE_CONFIG:
192 mod.config[None][var] = getattr(mod, var)
193 except AttributeError:
194 mod.config[None][var] = BASE_CONFIG[var]
198 def _parse_config(module, config):
200 Parse configuration file or extract configuration from module file.
201 Example of returned dictionary:
207 :param module: object
213 for key in BASE_CONFIG:
215 # get defaults from module config
216 defaults[key] = int(config.pop(key))
217 except (KeyError, ValueError):
219 # get defaults from module source code
220 defaults[key] = getattr(module, key)
221 except (KeyError, ValueError):
222 # if above failed, get defaults from global dict
223 defaults[key] = BASE_CONFIG[key]
225 # check if there are dict in config dict
228 if type(config[name]) is dict:
232 # assign variables needed by supervisor to every job configuration
236 if key not in config[name]:
237 config[name][key] = defaults[key]
238 # if only one job is needed, values doesn't have to be in dict (in YAML)
240 config = {None: config.copy()}
241 config[None].update(defaults)
243 # return dictionary of jobs where every job has BASE_CONFIG variables
247 def _create_jobs(modules):
249 Create jobs based on module.config dictionary and module.Service class definition.
254 for module in modules:
255 for name in module.config:
257 conf = module.config[name]
259 job = module.Service(configuration=conf, name=name)
260 except Exception as e:
261 debug(module.__name__ +
262 ": Couldn't start job named " +
268 # set chart_name (needed to plot run time graphs)
269 job.chart_name = module.__name__
271 job.chart_name += "_" + name
274 return [j for j in jobs if j is not None]
276 def _stop(self, job, reason=None):
278 Stop specified job and remove it from self.jobs list
279 Also notifies user about job failure if DEBUG_FLAG is set
284 if job.name is not None:
285 prefix = "'" + job.name + "' in "
287 prefix += "'" + job.__module__ + MODULE_EXTENSION + "' "
288 self.jobs.remove(job)
291 elif reason[:3] == "no ":
293 "does not seem to have " +
295 "() function. Disabling it.")
296 elif reason[:7] == "failed ":
299 "() function reports failure.")
300 elif reason[:13] == "configuration":
302 "configuration file '" +
305 ".conf' not found. Using defaults.")
306 elif reason[:11] == "misbehaving":
307 debug(prefix + "is " + reason)
311 Tries to execute check() on every job.
312 This cannot fail thus it is catching every exception
313 If job.check() fails job is stopped
316 while i < len(self.jobs):
320 self._stop(job, "failed check")
323 except AttributeError:
324 self._stop(job, "no check")
325 except (UnboundLocalError, Exception) as e:
326 self._stop(job, "misbehaving. Reason: " + str(e))
330 Tries to execute create() on every job.
331 This cannot fail thus it is catching every exception.
332 If job.create() fails job is stopped.
333 This is also creating job run time chart.
336 while i < len(self.jobs):
340 self._stop(job, "failed create")
342 chart = job.chart_name
344 "CHART netdata.plugin_pythond_" +
346 " '' 'Execution time for " +
348 " plugin' 'milliseconds / run' python.d netdata.plugin_python area 145000 " +
349 str(job.timetable['freq']) +
351 sys.stdout.write("DIMENSION run_time 'run time' absolute 1 1\n\n")
354 except AttributeError:
355 self._stop(job, "no create")
356 except (UnboundLocalError, Exception) as e:
357 self._stop(job, "misbehaving. Reason: " + str(e))
359 def _update_job(self, job):
361 Tries to execute update() on specified job.
362 This cannot fail thus it is catching every exception.
363 If job.update() returns False, number of retries_left is decremented.
364 If there are no more retries, job is stopped.
365 Job is also stopped if it throws an exception.
366 This is also updating job run time chart.
367 Return False if job is stopped
371 t_start = time.time()
372 # check if it is time to execute job update() function
373 if job.timetable['next'] > t_start:
379 since_last = int((t_start - job.timetable['last']) * 1000000)
380 if not job.update(since_last):
381 if job.retries_left <= 0:
382 self._stop(job, "update failed")
384 job.retries_left -= 1
385 job.timetable['next'] += job.timetable['freq']
387 except AttributeError:
388 self._stop(job, "no update")
390 except (UnboundLocalError, Exception) as e:
391 self._stop(job, "misbehaving. Reason: " + str(e))
394 job.timetable['next'] = t_end - (t_end % job.timetable['freq']) + job.timetable['freq']
395 # draw performance graph
396 sys.stdout.write("BEGIN netdata.plugin_pythond_" + job.chart_name + " " + str(since_last) + '\n')
397 sys.stdout.write("SET run_time = " + str(int((t_end - t_start) * 1000)) + '\n')
398 sys.stdout.write("END\n")
400 job.timetable['last'] = t_start
401 job.retries_left = job.retries
402 self.first_run = False
407 Tries to execute update() on every job by using _update_job()
408 This will stay forever and ever and ever forever and ever it'll be the one...
410 self.first_run = True
414 while i < len(self.jobs):
416 if self._update_job(job):
418 next_runs.append(job.timetable['next'])
422 if len(next_runs) == 0:
423 debug("No plugins loaded")
424 sys.stdout.write("DISABLE\n")
426 time.sleep(min(next_runs) - time.time())
429 def read_config(path):
431 Read YAML configuration from specified file
436 with open(path, 'r') as stream:
437 config = yaml.load(stream)
438 except (OSError, IOError):
439 debug(str(path), "is not a valid configuration file")
441 except yaml.YAMLError as e:
442 debug(str(path), "is malformed:", e)
449 Print message on stderr.
453 sys.stderr.write(PROGRAM + ":")
455 sys.stderr.write(" " + str(i))
456 sys.stderr.write("\n")
460 def parse_cmdline(directory, *commands):
462 Parse parameters from command line.
463 :param directory: str
464 :param commands: list of str
471 for cmd in commands[1:]:
474 elif cmd == "debug" or cmd == "all":
476 # redirect stderr to stdout?
477 elif os.path.isfile(directory + cmd + ".chart.py") or os.path.isfile(directory + cmd):
479 mods.append(cmd.replace(".chart.py", ""))
487 debug("started from", commands[0], "with options:", *commands[1:])
488 if len(mods) == 0 and DEBUG_FLAG is False:
491 return {'interval': interval,
495 # if __name__ == '__main__':
500 global PROGRAM, DEBUG_FLAG
501 PROGRAM = sys.argv[0].split('/')[-1].split('.plugin')[0]
503 # read configuration file
505 configfile = CONFIG_DIR + "python.d.conf"
508 conf = read_config(configfile)
511 if str(conf['enable']) is False:
512 debug("disabled in configuration file")
513 sys.stdout.write("DISABLE\n")
515 except (KeyError, TypeError):
518 interval = conf['interval']
519 except (KeyError, TypeError):
520 pass # use default interval from NETDATA_UPDATE_EVERY
522 DEBUG_FLAG = conf['debug']
523 except (KeyError, TypeError):
525 for k, v in conf.items():
526 if k in ("interval", "debug", "enable"):
531 modules_conf = CONFIG_DIR + "python.d/"
533 # parse passed command line arguments
534 out = parse_cmdline(MODULES_DIR, *sys.argv)
535 modules = out['modules']
536 if out['interval'] is not None:
537 interval = out['interval']
540 charts = PythonCharts(interval, modules, MODULES_DIR, modules_conf, disabled)
544 sys.stdout.write("DISABLE")
547 if __name__ == '__main__':