multithreading
paulfantom committed
Jun 23, 2016 at 11:21 UTC
dc36d60a7da55e6c47bf802e3a2df28f7043ad87
5 files changed
+192
-200
plugins.d/python.d.plugin
+59
-172
@@ -7,6 +7,7 @@
7
import os
8
import sys
9
import time
10
+import threading
11
12
# -----------------------------------------------------------------------------
13
# globals & environment setup
@@ -17,7 +18,7 @@ BASE_CONFIG = {'update_every': os.getenv('NETDATA_UPDATE_EVERY', 1),
18
'retries': 10}
19
20
MODULES_DIR = os.path.abspath(os.getenv('NETDATA_PLUGINS_DIR',
20
- os.path.dirname(__file__)) + "/../python.d") + "/"
21
+ os.path.dirname(__file__)) + "/../python.d") + "/"
22
CONFIG_DIR = os.getenv('NETDATA_CONFIG_DIR', "/etc/netdata/")
23
# directories should end with '/'
24
if CONFIG_DIR[-1] != "/":
@@ -28,60 +29,10 @@ PROGRAM = os.path.basename(__file__).replace(".plugin", "")
29
DEBUG_FLAG = False
30
OVERRIDE_UPDATE_EVERY = False
31
31
-
32
# -----------------------------------------------------------------------------
33
-# logging functions
34
-def log_msg(msg_type, *args):
35
- """
36
- Print message on stderr.
37
- :param msg_type: str
38
- """
39
- sys.stderr.write(PROGRAM)
40
- sys.stderr.write(" ")
41
- sys.stderr.write(msg_type)
42
- sys.stderr.write(": ")
43
- for i in args:
44
- sys.stderr.write(" ")
45
- sys.stderr.write(str(i))
46
- sys.stderr.write("\n")
47
- sys.stderr.flush()
48
-
49
-
50
-def debug(*args):
51
- """
52
- Print debug message on stderr.
53
- """
54
- if not DEBUG_FLAG:
55
- return
56
-
57
- log_msg("DEBUG", *args)
58
-
59
-
60
-def error(*args):
61
- """
62
- Print message on stderr.
63
- """
64
- log_msg("ERROR", *args)
65
-
66
-
67
-def info(*args):
68
- """
69
- Print message on stderr.
70
- """
71
- log_msg("INFO", *args)
72
-
73
-
74
-def fatal(*args):
75
- """
76
- Print message on stderr and exit.
77
- """
78
- log_msg("FATAL", *args)
79
- sys.stdout.write('DISABLE\n')
80
- sys.exit(1)
33
+# custom, third party and version specific python modules management
34
+import msg
35
82
-
83
-# -----------------------------------------------------------------------------
84
-# third party and version specific python modules management
36
try:
37
assert sys.version_info >= (3, 1)
38
import importlib.machinery
@@ -90,7 +41,7 @@ try:
41
# import builtins
42
# builtins.PY_VERSION = 3
43
PY_VERSION = 3
93
- info('Using python v3')
44
+ msg.info('Using python v3')
45
except (AssertionError, ImportError):
46
try:
47
import imp
@@ -99,19 +50,20 @@ except (AssertionError, ImportError):
50
# import __builtin__
51
# __builtin__.PY_VERSION = 2
52
PY_VERSION = 2
102
- info('Using python v2')
53
+ msg.info('Using python v2')
54
except ImportError:
104
- fatal('Cannot start. No importlib.machinery on python3 or lack of imp on python2')
55
+ msg.fatal('Cannot start. No importlib.machinery on python3 or lack of imp on python2')
56
try:
57
import yaml
58
except ImportError:
108
- fatal('Cannot find yaml library')
59
+ msg.fatal('Cannot find yaml library')
60
61
62
class PythonCharts(object):
63
"""
64
Main class used to control every python module.
65
"""
66
+
67
def __init__(self,
68
modules=None,
69
modules_path='../python.d/',
@@ -167,7 +119,7 @@ class PythonCharts(object):
119
else:
120
return imp.load_source(name, path)
121
except Exception as e:
170
- error("Problem loading", name, str(e))
122
+ msg.error("Problem loading", name, str(e))
123
return None
124
125
def _load_modules(self, path, modules, disabled):
@@ -181,7 +133,7 @@ class PythonCharts(object):
133
134
# check if plugin directory exists
135
if not os.path.isdir(path):
184
- fatal("cannot find charts directory ", path)
136
+ msg.fatal("cannot find charts directory ", path)
137
138
# load modules
139
loaded = []
@@ -193,17 +145,17 @@ class PythonCharts(object):
145
if mod is not None:
146
loaded.append(mod)
147
else: # exit if plugin is not found
196
- fatal('no modules found.')
148
+ msg.fatal('no modules found.')
149
else:
150
# scan directory specified in path and load all modules from there
151
names = os.listdir(path)
152
for mod in names:
153
if mod.replace(MODULE_EXTENSION, "") in disabled:
202
- error(mod + ": disabled module ", mod.replace(MODULE_EXTENSION, ""))
154
+ msg.error(mod + ": disabled module ", mod.replace(MODULE_EXTENSION, ""))
155
continue
156
m = self._import_module(path + mod)
157
if m is not None:
206
- debug(mod + ": loading module '" + path + mod + "'")
158
+ msg.debug(mod + ": loading module '" + path + mod + "'")
159
loaded.append(m)
160
return loaded
161
@@ -218,7 +170,7 @@ class PythonCharts(object):
170
for mod in modules:
171
configfile = self.configs + mod.__name__ + ".conf"
172
if os.path.isfile(configfile):
221
- debug(mod.__name__ + ": loading module configuration: '" + configfile + "'")
173
+ msg.debug(mod.__name__ + ": loading module configuration: '" + configfile + "'")
174
try:
175
if not hasattr(mod, 'config'):
176
mod.config = {}
@@ -226,12 +178,12 @@ class PythonCharts(object):
178
'config',
179
self._parse_config(mod, read_config(configfile)))
180
except Exception as e:
229
- error(mod.__name__ + ": cannot parse configuration file '" + configfile + "':", str(e))
181
+ msg.error(mod.__name__ + ": cannot parse configuration file '" + configfile + "':", str(e))
182
else:
231
- error(mod.__name__ + ": configuration file '" + configfile + "' not found. Using defaults.")
183
+ msg.error(mod.__name__ + ": configuration file '" + configfile + "' not found. Using defaults.")
184
# set config if not found
185
if not hasattr(mod, 'config'):
234
- debug(mod.__name__ + ": setting configuration for only one job")
186
+ msg.debug(mod.__name__ + ": setting configuration for only one job")
187
mod.config = {None: {}}
188
for var in BASE_CONFIG:
189
try:
@@ -256,7 +208,7 @@ class PythonCharts(object):
208
"""
209
# get default values
210
defaults = {}
259
- debug(module.__name__ + ": reading configuration")
211
+ msg.debug(module.__name__ + ": reading configuration")
212
for key in BASE_CONFIG:
213
try:
214
# get defaults from module config
@@ -305,10 +257,10 @@ class PythonCharts(object):
257
try:
258
job = module.Service(configuration=conf, name=name)
259
except Exception as e:
308
- error(module.__name__ +
309
- ("/" + str(name) if name is not None else "") +
310
- ": cannot start job: '" +
311
- str(e))
260
+ msg.error(module.__name__ +
261
+ ("/" + str(name) if name is not None else "") +
262
+ ": cannot start job: '" +
263
+ str(e))
264
return None
265
else:
266
# set chart_name (needed to plot run time graphs)
@@ -316,7 +268,7 @@ class PythonCharts(object):
268
if name is not None:
269
job.chart_name += "_" + name
270
jobs.append(job)
319
- debug(module.__name__ + ("/" + str(name) if name is not None else "") + ": job added")
271
+ msg.debug(module.__name__ + ("/" + str(name) if name is not None else "") + ": job added")
272
273
return [j for j in jobs if j is not None]
274
@@ -336,22 +288,22 @@ class PythonCharts(object):
288
if reason is None:
289
return
290
elif reason[:3] == "no ":
339
- error(prefix +
340
- "does not seem to have " +
341
- reason[3:] +
342
- "() function. Disabling it.")
291
+ msg.error(prefix +
292
+ "does not seem to have " +
293
+ reason[3:] +
294
+ "() function. Disabling it.")
295
elif reason[:7] == "failed ":
344
- error(prefix +
345
- reason[7:] +
346
- "() function reports failure.")
296
+ msg.error(prefix +
297
+ reason[7:] +
298
+ "() function reports failure.")
299
elif reason[:13] == "configuration":
348
- error(prefix +
349
- "configuration file '" +
350
- self.configs +
351
- job.__module__ +
352
- ".conf' not found. Using defaults.")
300
+ msg.error(prefix +
301
+ "configuration file '" +
302
+ self.configs +
303
+ job.__module__ +
304
+ ".conf' not found. Using defaults.")
305
elif reason[:11] == "misbehaving":
354
- error(prefix + "is " + reason)
306
+ msg.error(prefix + "is " + reason)
307
308
def check(self):
309
"""
@@ -366,7 +318,7 @@ class PythonCharts(object):
318
if not job.check():
319
self._stop(job, "failed check")
320
else:
369
- debug(job.chart_name, ": check succeeded")
321
+ msg.debug(job.chart_name, ": check succeeded")
322
i += 1
323
except AttributeError:
324
self._stop(job, "no check")
@@ -397,7 +349,7 @@ class PythonCharts(object):
349
str(job.timetable['freq']) +
350
'\n')
351
sys.stdout.write("DIMENSION run_time 'run time' absolute 1 1\n\n")
400
- debug("created charts for", job.chart_name)
352
+ msg.debug("created charts for", job.chart_name)
353
# sys.stdout.flush()
354
i += 1
355
except AttributeError:
@@ -405,82 +357,14 @@ class PythonCharts(object):
357
except (UnboundLocalError, Exception) as e:
358
self._stop(job, "misbehaving. Reason: " + str(e))
359
408
- def _update_job(self, job):
409
- """
410
- Tries to execute update() on specified job.
411
- This cannot fail thus it is catching every exception.
412
- If job.update() returns False, number of retries_left is decremented.
413
- If there are no more retries, job is stopped.
414
- Job is also stopped if it throws an exception.
415
- This is also updating job run time chart.
416
- Return False if job is stopped
417
- :param job: object
418
- :return: boolean
419
- """
420
- t_start = time.time()
421
- # check if it is time to execute job update() function
422
- if job.timetable['next'] > t_start:
423
- debug(job.chart_name + " will be run in " + str(int((job.timetable['next'] - t_start) * 1000)) + " ms")
424
- return True
425
- try:
426
- if self.first_run:
427
- since_last = 0
428
- else:
429
- since_last = int((t_start - job.timetable['last']) * 1000000)
430
- debug(job.chart_name +
431
- " ready to run, after " + str(int((t_start - job.timetable['last']) * 1000)) +
432
- " ms (update_every: " + str(job.timetable['freq'] * 1000) +
433
- " ms, latency: " + str(int((t_start - job.timetable['next']) * 1000)) + " ms)")
434
- if not job.update(since_last):
435
- if job.retries_left <= 0:
436
- self._stop(job, "update failed")
437
- return False
438
- job.retries_left -= 1
439
- job.timetable['next'] += job.timetable['freq']
440
- return True
441
- except AttributeError:
442
- self._stop(job, "no update")
443
- return False
444
- except (UnboundLocalError, Exception) as e:
445
- self._stop(job, "misbehaving. Reason: " + str(e))
446
- return False
447
- t_end = time.time()
448
- job.timetable['next'] = t_end - (t_end % job.timetable['freq']) + job.timetable['freq']
449
- # draw performance graph
450
- run_time = str(int((t_end - t_start) * 1000))
451
- debug(job.chart_name, "updated in", run_time, "ms")
452
- sys.stdout.write("BEGIN netdata.plugin_pythond_" + job.chart_name + " " + str(since_last) + '\n')
453
- sys.stdout.write("SET run_time = " + run_time + '\n')
454
- sys.stdout.write("END\n")
455
- # sys.stdout.flush()
456
- job.timetable['last'] = t_start
457
- job.retries_left = job.retries
458
- self.first_run = False
459
- return True
460
-
360
def update(self):
462
- """
463
- Tries to execute update() on every job by using _update_job()
464
- This will stay forever and ever and ever forever and ever it'll be the one...
465
- """
466
- self.first_run = True
361
+ for job in self.jobs:
362
+ job.start()
363
+
364
while True:
468
- next_runs = []
469
- i = 0
470
- while i < len(self.jobs):
471
- job = self.jobs[i]
472
- if self._update_job(job):
473
- try:
474
- next_runs.append(job.timetable['next'])
475
- except KeyError:
476
- pass
477
- i += 1
478
- if len(next_runs) == 0:
479
- fatal('no python.d modules loaded.')
480
- try:
481
- time.sleep(min(next_runs) - time.time())
482
- except IOError:
483
- pass
365
+ if threading.active_count() <= 1:
366
+ msg.fatal("no more jobs")
367
+ time.sleep(1)
368
369
370
def read_config(path):
@@ -493,10 +377,10 @@ def read_config(path):
377
with open(path, 'r') as stream:
378
config = yaml.load(stream)
379
except (OSError, IOError):
496
- error(str(path), "is not a valid configuration file")
380
+ msg.error(str(path), "is not a valid configuration file")
381
return None
382
except yaml.YAMLError as e:
499
- error(str(path), "is malformed:", e)
383
+ msg.error(str(path), "is malformed:", e)
384
return None
385
return config
386
@@ -531,9 +415,9 @@ def parse_cmdline(directory, *commands):
415
pass
416
if changed_update and DEBUG_FLAG:
417
OVERRIDE_UPDATE_EVERY = True
534
- debug(PROGRAM, "overriding update interval to", str(BASE_CONFIG['update_every']))
418
+ msg.debug(PROGRAM, "overriding update interval to", str(BASE_CONFIG['update_every']))
419
536
- debug("started from", commands[0], "with options:", *commands[1:])
420
+ msg.debug("started from", commands[0], "with options:", *commands[1:])
421
422
return mods
423
@@ -548,14 +432,14 @@ def run():
432
# read configuration file
433
disabled = []
434
configfile = CONFIG_DIR + "python.d.conf"
551
- debug(PROGRAM, "reading configuration file:", configfile)
435
+ msg.info(PROGRAM, "reading configuration file:", configfile)
436
437
conf = read_config(configfile)
438
if conf is not None:
439
try:
440
# exit the whole plugin when 'enabled: no' is set in 'python.d.conf'
441
if conf['enabled'] is False:
558
- fatal('disabled in configuration file.\n')
442
+ msg.fatal('disabled in configuration file.\n')
443
except (KeyError, TypeError):
444
pass
445
try:
@@ -573,19 +457,22 @@ def run():
457
if v is False:
458
disabled.append(k)
459
460
+ DEBUG_FLAG = True
461
+ msg.DEBUG_FLAG = DEBUG_FLAG
462
+ msg.PROGRAM = PROGRAM
463
# parse passed command line arguments
464
modules = parse_cmdline(MODULES_DIR, *sys.argv)
578
- info("MODULES_DIR='" + MODULES_DIR +
579
- "', CONFIG_DIR='" + CONFIG_DIR +
580
- "', UPDATE_EVERY=" + str(BASE_CONFIG['update_every']) +
581
- ", ONLY_MODULES=" + str(modules))
465
+ msg.info("MODULES_DIR='" + MODULES_DIR +
466
+ "', CONFIG_DIR='" + CONFIG_DIR +
467
+ "', UPDATE_EVERY=" + str(BASE_CONFIG['update_every']) +
468
+ ", ONLY_MODULES=" + str(modules))
469
470
# run plugins
471
charts = PythonCharts(modules, MODULES_DIR, CONFIG_DIR + "python.d/", disabled)
472
charts.check()
473
charts.create()
474
charts.update()
588
- fatal("finished")
475
+ msg.fatal("finished")
476
477
478
if __name__ == '__main__':
python.d/apache.chart.py
+1
-2
@@ -112,8 +112,7 @@ class Service(UrlService):
112
if str(tmp[0]) in self.assignment:
113
try:
114
data[self.assignment[tmp[0]]] = int(float(tmp[1]))
115
- except (IndexError, ValueError) as a:
116
- print(a)
115
+ except (IndexError, ValueError):
116
pass
117
if len(data) == 0:
118
return None
python.d/example.chart.py
+9
-8
@@ -13,6 +13,7 @@ update_every = 4
13
priority = 90000
14
retries = 7
15
16
+
17
class Service(BaseService):
18
def __init__(self, configuration=None, name=None):
19
super(self.__class__,self).__init__(configuration=configuration, name=name)
@@ -21,15 +22,15 @@ class Service(BaseService):
22
return True
23
24
def create(self):
24
- print("CHART example.python_random '' 'A random number' 'random number' random random line " +
25
- str(self.priority) +
26
- " " +
27
- str(self.update_every))
28
- print("DIMENSION random1 '' absolute 1 1")
25
+ chart = "CHART example.python_random '' 'A random number' 'random number' random random line " + \
26
+ str(self.priority) + " " + \
27
+ str(self.update_every) + "\n" + \
28
+ "DIMENSION random1 '' absolute 1 1"
29
+ print(chart)
30
return True
31
32
def update(self, interval):
32
- print("BEGIN example.python_random "+str(interval))
33
- print("SET random1 = "+str(random.randint(0,100)))
34
- print("END")
33
+ chart = "BEGIN example.python_random "+str(interval)+"\n"
34
+ chart += "SET random1 = "+str(random.randint(0,100))+"\n"
35
+ print(chart + "END")
36
return True
python.d/python_modules/base.py
+67
-18
@@ -2,25 +2,36 @@
2
# Description: prototypes for netdata python.d modules
3
# Author: Pawel Krupa (paulfantom)
4
5
-from time import time
5
+import time
6
import sys
7
try:
8
from urllib.request import urlopen
9
except ImportError:
10
from urllib2 import urlopen
11
12
+import threading
13
+import msg
14
13
-class BaseService(object):
15
+
16
+class BaseService(threading.Thread):
17
"""
18
Prototype of Service class.
19
Implemented basic functionality to run jobs by `python.d.plugin`
20
"""
21
+ debugging = False
22
+
23
def __init__(self, configuration=None, name=None):
24
"""
25
This needs to be initialized in child classes
26
:param configuration: dict
27
:param name: str
28
"""
29
+ threading.Thread.__init__(self)
30
+ self.daemon = True
31
+ self.retries = 0
32
+ self.retries_left = 0
33
+ self.priority = 140000
34
+ self.update_every = 1
35
self.name = name
36
if configuration is None:
37
self.error("BaseService: no configuration parameters supplied. Cannot create Service.")
@@ -58,24 +69,64 @@ class BaseService(object):
69
"""
70
if freq is None:
71
freq = self.update_every
61
- now = time()
72
+ now = time.time()
73
self.timetable = {'last': now,
74
'next': now - (now % freq) + freq,
75
'freq': freq}
76
66
- @staticmethod
67
- def error(msg, exception=""):
68
- if exception != "":
69
- exception = " " + str(exception).replace("\n", " ")
70
- sys.stderr.write(str(msg)+exception+"\n")
71
- sys.stderr.flush()
77
+ def _run_once(self):
78
+ t_start = time.time()
79
+ # check if it is time to execute job update() function
80
+ if self.timetable['next'] > t_start:
81
+ msg.debug(self.chart_name + " will be run in " +
82
+ str(int((self.timetable['next'] - t_start) * 1000)) + " ms")
83
+ return True
84
+
85
+ since_last = int((t_start - self.timetable['last']) * 1000000)
86
+ msg.debug(self.chart_name +
87
+ " ready to run, after " + str(int((t_start - self.timetable['last']) * 1000)) +
88
+ " ms (update_every: " + str(self.timetable['freq'] * 1000) +
89
+ " ms, latency: " + str(int((t_start - self.timetable['next']) * 1000)) + " ms)")
90
+ if not self.update(since_last):
91
+ return False
92
+ t_end = time.time()
93
+ self.timetable['next'] = t_end - (t_end % self.timetable['freq']) + self.timetable['freq']
94
+
95
+ # draw performance graph
96
+ run_time = str(int((t_end - t_start) * 1000))
97
+ run_time_chart = "BEGIN netdata.plugin_pythond_" + self.chart_name + " " + str(since_last) + '\n'
98
+ run_time_chart += "SET run_time = " + run_time + '\n'
99
+ run_time_chart += "END\n"
100
+ sys.stdout.write(run_time_chart)
101
+ msg.debug(self.chart_name + " updated in " + str(run_time) + " ms")
102
+ self.timetable['last'] = t_start
103
+ return True
104
+
105
+ def run(self):
106
+ self.timetable['last'] = time.time()
107
+ while True:
108
+ try:
109
+ status = self._run_once()
110
+ except Exception as e:
111
+ msg.error("Something wrong: " + str(e))
112
+ return
113
+ if status:
114
+ time.sleep(self.timetable['next'] - time.time())
115
+ self.retries_left = self.retries
116
+ else:
117
+ self.retries -= 1
118
+ if self.retries_left <= 0:
119
+ msg.error("no more retries. Exiting")
120
+ return
121
+ else:
122
+ time.sleep(self.timetable['freq'])
123
124
def check(self):
125
"""
126
check() prototype
127
:return: boolean
128
"""
78
- self.error("Service " + str(self.__module__) + "doesn't implement check() function")
129
+ msg.error("Service " + str(self.__module__) + "doesn't implement check() function")
130
return False
131
132
def create(self):
@@ -83,7 +134,7 @@ class BaseService(object):
134
create() prototype
135
:return: boolean
136
"""
86
- self.error("Service " + str(self.__module__) + "doesn't implement create() function?")
137
+ msg.error("Service " + str(self.__module__) + "doesn't implement create() function?")
138
return False
139
140
def update(self, interval):
@@ -92,7 +143,7 @@ class BaseService(object):
143
:param interval: int
144
:return: boolean
145
"""
95
- self.error("Service " + str(self.__module__) + "doesn't implement update() function")
146
+ msg.error("Service " + str(self.__module__) + "doesn't implement update() function")
147
return False
148
149
@@ -128,7 +179,7 @@ class UrlService(BaseService):
179
f = urlopen(self.url, timeout=self.update_every)
180
raw = f.read().decode('utf-8')
181
except Exception as e:
131
- self.error(self.__module__, str(e))
182
+ msg.error(self.__module__, str(e))
183
finally:
184
try:
185
f.close()
@@ -148,7 +199,7 @@ class UrlService(BaseService):
199
Format configuration data and try to connect to server
200
:return: boolean
201
"""
151
- if self.name is None:
202
+ if self.name is None or self.name == str(None):
203
self.name = 'local'
204
else:
205
self.name = str(self.name)
@@ -193,8 +244,7 @@ class UrlService(BaseService):
244
content += "DIMENSION " + line['name'] + " " + line['options'] + "\n"
245
246
if len(content) > 0:
196
- print(header)
197
- print(content)
247
+ print(header + "\n" + content)
248
idx += 1
249
250
if idx == 0:
@@ -220,7 +270,6 @@ class UrlService(BaseService):
270
except KeyError:
271
pass
272
if len(c) != 0:
223
- print(header + c)
224
- print("END")
273
+ print(header + c + "\nEND")
274
275
return True
python.d/python_modules/msg.py
new
+56
@@ -0,0 +1,56 @@
1
+# -*- coding: utf-8 -*-
2
+# Description: logging for netdata python.d modules
3
+
4
+import sys
5
+
6
+DEBUG_FLAG = False
7
+PROGRAM = ""
8
+
9
+
10
+def log_msg(msg_type, *args):
11
+ """
12
+ Print message on stderr.
13
+ :param msg_type: str
14
+ """
15
+ sys.stderr.write(PROGRAM)
16
+ sys.stderr.write(" ")
17
+ sys.stderr.write(msg_type)
18
+ sys.stderr.write(": ")
19
+ for i in args:
20
+ sys.stderr.write(" ")
21
+ sys.stderr.write(str(i))
22
+ sys.stderr.write("\n")
23
+ sys.stderr.flush()
24
+
25
+
26
+def debug(*args):
27
+ """
28
+ Print debug message on stderr.
29
+ """
30
+ if not DEBUG_FLAG:
31
+ return
32
+
33
+ log_msg("DEBUG", *args)
34
+
35
+
36
+def error(*args):
37
+ """
38
+ Print message on stderr.
39
+ """
40
+ log_msg("ERROR", *args)
41
+
42
+
43
+def info(*args):
44
+ """
45
+ Print message on stderr.
46
+ """
47
+ log_msg("INFO", *args)
48
+
49
+
50
+def fatal(*args):
51
+ """
52
+ Print message on stderr and exit.
53
+ """
54
+ log_msg("FATAL", *args)
55
+ sys.stdout.write('DISABLE\n')
56
+ sys.exit(1)
\ No newline at end of file