2 # -*- coding: utf-8 -*-
4 # Description: netdata python modules supervisor
5 # Author: Pawel Krupa (paulfantom)
12 # https://github.com/firehol/netdata/wiki/External-Plugins#environment-variables
13 MODULES_DIR = os.getenv('NETDATA_PLUGINS_DIR',
14 os.path.abspath(__file__).strip("python.d.plugin.py").replace("plugins.d", ""))
15 CONFIG_DIR = os.getenv('NETDATA_CONFIG_DIR', "/etc/netdata/")
16 INTERVAL = os.getenv('NETDATA_UPDATE_EVERY', None)
17 # directories should end with '/'
18 if MODULES_DIR[-1] != "/":
20 MODULES_DIR += "python.d/"
21 if CONFIG_DIR[-1] != "/":
23 sys.path.append(MODULES_DIR + "python_modules")
27 assert sys.version_info >= (3, 1)
28 import importlib.machinery
30 # change this hack below if we want PY_VERSION to be used in modules
32 # builtins.PY_VERSION = 3
34 sys.stderr.write('python.d.plugin: Using python 3\n')
35 except (AssertionError, ImportError):
39 # change this hack below if we want PY_VERSION to be used in modules
41 # __builtin__.PY_VERSION = 2
43 sys.stderr.write('python.d.plugin: Using python 2\n')
45 sys.stderr.write('python.d.plugin: Cannot start. No importlib.machinery on python3 or lack of imp on python2\n')
46 sys.stdout.write('DISABLE\n')
51 sys.stderr.write('python.d.plugin: Cannot find yaml library\n')
52 sys.stdout.write('DISABLE\n')
56 PROGRAM = "python.d.plugin"
57 MODULE_EXTENSION = ".chart.py"
58 BASE_CONFIG = {'update_every': 10,
63 class PythonCharts(object):
65 Main class used to control every python module.
70 modules_path='../python.d/',
71 modules_configs='../conf.d/',
72 modules_disabled=None):
76 :param modules_path: str
77 :param modules_configs: str
78 :param modules_disabled: list
83 if modules_disabled is None:
87 # set configuration directory
88 self.configs = modules_configs
91 loaded_modules = self._load_modules(modules_path, modules, modules_disabled)
93 # load configuration files
94 configured_modules = self._load_configs(loaded_modules)
96 # good economy and prosperity:
97 self.jobs = self._create_jobs(configured_modules) # type: list
98 if DEBUG_FLAG and interval is not None:
100 job.create_timetable(interval)
103 def _import_module(path, name=None):
105 Try to import module using only its path.
112 name = path.split('/')[-1]
113 if name[-len(MODULE_EXTENSION):] != MODULE_EXTENSION:
115 name = name[:-len(MODULE_EXTENSION)]
118 return importlib.machinery.SourceFileLoader(name, path).load_module()
120 return imp.load_source(name, path)
121 except Exception as e:
125 def _load_modules(self, path, modules, disabled):
127 Load modules from 'modules' list or dynamically every file from 'path' (only .chart.py files)
130 :param disabled: list
134 # check if plugin directory exists
135 if not os.path.isdir(path):
136 debug("cannot find charts directory ", path)
137 sys.stdout.write("DISABLE\n")
146 mod = self._import_module(path + m + MODULE_EXTENSION)
149 else: # exit if plugin is not found
150 sys.stdout.write("DISABLE")
154 # scan directory specified in path and load all modules from there
155 names = os.listdir(path)
157 if mod.strip(MODULE_EXTENSION) in disabled:
158 debug("disabling:", mod.strip(MODULE_EXTENSION))
160 m = self._import_module(path + mod)
162 debug("loading module: '" + path + mod + "'")
166 def _load_configs(self, modules):
168 Append configuration in list named `config` to every module.
169 For multi-job modules `config` list is created in _parse_config,
170 otherwise it is created here based on BASE_CONFIG prototype with None as identifier.
175 configfile = self.configs + mod.__name__ + ".conf"
176 if os.path.isfile(configfile):
177 debug("loading module configuration: '" + configfile + "'")
181 self._parse_config(mod, read_config(configfile)))
182 except Exception as e:
183 debug("something went wrong while loading configuration", e)
186 ": configuration file '" +
188 "' not found. Using defaults.")
189 # set config if not found
190 if not hasattr(mod, 'config'):
191 mod.config = {None: {}}
192 for var in BASE_CONFIG:
194 mod.config[None][var] = getattr(mod, var)
195 except AttributeError:
196 mod.config[None][var] = BASE_CONFIG[var]
200 def _parse_config(module, config):
202 Parse configuration file or extract configuration from module file.
203 Example of returned dictionary:
209 :param module: object
215 for key in BASE_CONFIG:
217 # get defaults from module config
218 defaults[key] = int(config.pop(key))
219 except (KeyError, ValueError):
221 # get defaults from module source code
222 defaults[key] = getattr(module, key)
223 except (KeyError, ValueError):
224 # if above failed, get defaults from global dict
225 defaults[key] = BASE_CONFIG[key]
227 # check if there are dict in config dict
230 if type(config[name]) is dict:
234 # assign variables needed by supervisor to every job configuration
238 if key not in config[name]:
239 config[name][key] = defaults[key]
240 # if only one job is needed, values doesn't have to be in dict (in YAML)
242 config = {None: config.copy()}
243 config[None].update(defaults)
245 # return dictionary of jobs where every job has BASE_CONFIG variables
249 def _create_jobs(modules):
251 Create jobs based on module.config dictionary and module.Service class definition.
256 for module in modules:
257 for name in module.config:
259 conf = module.config[name]
261 job = module.Service(configuration=conf, name=name)
262 except Exception as e:
263 debug(module.__name__ +
264 ": Couldn't start job named " +
270 # set chart_name (needed to plot run time graphs)
271 job.chart_name = module.__name__
273 job.chart_name += "_" + name
276 return [j for j in jobs if j is not None]
278 def _stop(self, job, reason=None):
280 Stop specified job and remove it from self.jobs list
281 Also notifies user about job failure if DEBUG_FLAG is set
286 if job.name is not None:
287 prefix = "'" + job.name + "' in "
289 prefix += "'" + job.__module__ + MODULE_EXTENSION + "' "
290 self.jobs.remove(job)
293 elif reason[:3] == "no ":
295 "does not seem to have " +
297 "() function. Disabling it.")
298 elif reason[:7] == "failed ":
301 "() function reports failure.")
302 elif reason[:13] == "configuration":
304 "configuration file '" +
307 ".conf' not found. Using defaults.")
308 elif reason[:11] == "misbehaving":
309 debug(prefix + "is " + reason)
313 Tries to execute check() on every job.
314 This cannot fail thus it is catching every exception
315 If job.check() fails job is stopped
318 while i < len(self.jobs):
322 self._stop(job, "failed check")
325 except AttributeError:
326 self._stop(job, "no check")
327 except (UnboundLocalError, Exception) as e:
328 self._stop(job, "misbehaving. Reason: " + str(e))
332 Tries to execute create() on every job.
333 This cannot fail thus it is catching every exception.
334 If job.create() fails job is stopped.
335 This is also creating job run time chart.
338 while i < len(self.jobs):
342 self._stop(job, "failed create")
344 chart = job.chart_name
346 "CHART netdata.plugin_pythond_" +
348 " '' 'Execution time for " +
350 " plugin' 'milliseconds / run' python.d netdata.plugin_python area 145000 " +
351 str(job.timetable['freq']) +
353 sys.stdout.write("DIMENSION run_time 'run time' absolute 1 1\n\n")
356 except AttributeError:
357 self._stop(job, "no create")
358 except (UnboundLocalError, Exception) as e:
359 self._stop(job, "misbehaving. Reason: " + str(e))
361 def _update_job(self, job):
363 Tries to execute update() on specified job.
364 This cannot fail thus it is catching every exception.
365 If job.update() returns False, number of retries_left is decremented.
366 If there are no more retries, job is stopped.
367 Job is also stopped if it throws an exception.
368 This is also updating job run time chart.
369 Return False if job is stopped
373 t_start = time.time()
374 # check if it is time to execute job update() function
375 if job.timetable['next'] > t_start:
381 since_last = int((t_start - job.timetable['last']) * 1000000)
382 if not job.update(since_last):
383 if job.retries_left <= 0:
384 self._stop(job, "update failed")
386 job.retries_left -= 1
387 job.timetable['next'] += job.timetable['freq']
389 except AttributeError:
390 self._stop(job, "no update")
392 except (UnboundLocalError, Exception) as e:
393 self._stop(job, "misbehaving. Reason: " + str(e))
396 job.timetable['next'] = t_end - (t_end % job.timetable['freq']) + job.timetable['freq']
397 # draw performance graph
398 sys.stdout.write("BEGIN netdata.plugin_pythond_" + job.chart_name + " " + str(since_last) + '\n')
399 sys.stdout.write("SET run_time = " + str(int((t_end - t_start) * 1000)) + '\n')
400 sys.stdout.write("END\n")
402 job.timetable['last'] = t_start
403 job.retries_left = job.retries
404 self.first_run = False
409 Tries to execute update() on every job by using _update_job()
410 This will stay forever and ever and ever forever and ever it'll be the one...
412 self.first_run = True
416 while i < len(self.jobs):
418 if self._update_job(job):
420 next_runs.append(job.timetable['next'])
424 if len(next_runs) == 0:
425 debug("No plugins loaded")
426 sys.stdout.write("DISABLE\n")
428 time.sleep(min(next_runs) - time.time())
431 def read_config(path):
433 Read YAML configuration from specified file
438 with open(path, 'r') as stream:
439 config = yaml.load(stream)
440 except (OSError, IOError):
441 debug(str(path), "is not a valid configuration file")
443 except yaml.YAMLError as e:
444 debug(str(path), "is malformed:", e)
451 Print message on stderr.
455 sys.stderr.write(PROGRAM + ":")
457 sys.stderr.write(" " + str(i))
458 sys.stderr.write("\n")
462 def parse_cmdline(directory, *commands):
464 Parse parameters from command line.
465 :param directory: str
466 :param commands: list of str
473 for cmd in commands[1:]:
476 elif cmd == "debug" or cmd == "all":
478 # redirect stderr to stdout?
479 elif os.path.isfile(directory + cmd + ".chart.py") or os.path.isfile(directory + cmd):
481 mods.append(cmd.replace(".chart.py", ""))
489 debug("started from", commands[0], "with options:", *commands[1:])
490 if len(mods) == 0 and DEBUG_FLAG is False:
493 return {'interval': interval,
497 # if __name__ == '__main__':
502 global PROGRAM, DEBUG_FLAG
503 PROGRAM = sys.argv[0].split('/')[-1].split('.plugin')[0]
505 # read configuration file
507 configfile = CONFIG_DIR + "python.d.conf"
510 conf = read_config(configfile)
513 if str(conf['enable']) is False:
514 debug("disabled in configuration file")
515 sys.stdout.write("DISABLE\n")
517 except (KeyError, TypeError):
520 interval = conf['interval']
521 except (KeyError, TypeError):
522 pass # use default interval from NETDATA_UPDATE_EVERY
524 DEBUG_FLAG = conf['debug']
525 except (KeyError, TypeError):
527 for k, v in conf.items():
528 if k in ("interval", "debug", "enable"):
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, CONFIG_DIR + "python.d/", disabled)
544 sys.stdout.write("DISABLE")
547 if __name__ == '__main__':