Initial support for python plugins
paulfantom committed
Jun 10, 2016 at 14:27 UTC
bd546d32209e4cc5ccc8d91a3e70a8808ffe73e0
2 files changed
+246
plugins.d/python.d.plugin
new
+231
@@ -0,0 +1,231 @@
1
+#!/usr/bin/python3 -u
2
+
3
+import os
4
+import sys
5
+import time
6
+import importlib.machinery
7
+import configparser
8
+
9
+class PythonCharts(object):
10
+ def __init__(self,interval=1,modules=[],modules_path='../python.d/',modules_configs='../conf.d/'):
11
+ self.interval = interval
12
+ # check if plugin directory exists
13
+ if not os.path.isdir(modules_path):
14
+ debug("cannot find charts directory ",modules_path)
15
+ sys.exit(1)
16
+ self.configs=modules_configs
17
+
18
+ self.modules = []
19
+ if len(modules) > 0:
20
+ for m in modules:
21
+ self.modules.append(self.import_plugin(modules_path+m+".chart.py"))
22
+ else:
23
+ self.load_modules(modules_path)
24
+ if len(self.modules) == 0:
25
+ debug("cannot find charts directory",modules_path)
26
+# sys.stdout.write("DISABLE")
27
+ sys.exit(1)
28
+
29
+ self.load_configs()
30
+
31
+ # set last execution dict
32
+ self.timetable = {}
33
+ t = time.time()
34
+ for m in self.modules:
35
+ self.timetable[m.__name__] = t
36
+
37
+ def import_plugin(self,path,name=None):
38
+ if name is None:
39
+ name = path.split('/')[-1]
40
+ if name[-9:] != ".chart.py":
41
+ return None
42
+ name=name[:-9]
43
+ try:
44
+ return importlib.machinery.SourceFileLoader(name,path).load_module()
45
+ except FileNotFoundError:
46
+ return None
47
+
48
+ def load_modules(self,path):
49
+ names = os.listdir(path)
50
+ for mod in names:
51
+ m = self.import_plugin(path+mod)
52
+ if m is not None:
53
+ debug("loading chart: '"+path+mod+"'")
54
+ self.modules.append(m)
55
+
56
+ def load_configs(self):
57
+ for m in self.modules:
58
+ if os.path.isfile(self.configs+m.__name__+".conf"):
59
+ debug("loading chart options: '"+self.configs+m.__name__+".conf'")
60
+ for k,v in read_config(self.configs+m.__name__+".conf").items():
61
+ setattr(m,k,v)
62
+ else:
63
+ debug(m.__name__+": configuration file '"+self.configs+m.__name__+".conf' not found. Using defaults.")
64
+
65
+ def disable_module(self,mod,reason=None):
66
+ self.modules.remove(mod)
67
+ del self.timetable[mod.__name__]
68
+ if reason is None:
69
+ return
70
+ elif reason[:3] == "no ":
71
+ debug("chart '"+ mod.__name__,"' does not seem to have "+reason[3:]+"() function. Disabling it.")
72
+ elif reason[:7] == "failed ":
73
+ debug("chart '"+mod.__name__+reason[3:]+"() function. reports failure.")
74
+ elif reason[:13] == "configuration":
75
+ debug(mod.__name__,"configuration file '"+self.configs+mod.__name__+".conf' not found. Using defaults.")
76
+ elif reason[:11] == "misbehaving":
77
+ debug(mod.__name__,"is misbeaving. Disabling it")
78
+ #sys.stdout.write('DISABLE')
79
+
80
+
81
+ def check(self):
82
+ for mod in self.modules:
83
+ try:
84
+ if not mod.check():
85
+ self.disable_module(mod,"failed check")
86
+ except AttributeError:
87
+ self.disable_module(mod,"no check")
88
+ except UnboundLocalError:
89
+ self.disable_module(mod,"misbehaving")
90
+
91
+ def create(self):
92
+ for mod in self.modules:
93
+ try:
94
+ if not mod.create():
95
+ self.disable_module(mod,"failed create")
96
+ else:
97
+ chart=mod.__name__
98
+ sys.stdout.write("CHART netdata.plugin_pythond_"+chart+" '' 'Execution time for "+chart+" plugin' 'milliseconds / run' python.d netdata.plugin_python area 145000 "+str(self.interval)+'\n')
99
+ sys.stdout.write("DIMENSION run_time 'run time' absolute 1 1\n\n")
100
+ sys.stdout.flush()
101
+ except AttributeError:
102
+ self.disable_module(mod,"no create")
103
+ except UnboundLocalError:
104
+ self.disable_module(mod,"misbehaving")
105
+
106
+ def update_module(self,mod):
107
+ t1 = time.time()
108
+ try:
109
+# t=int((t1- self.timetable[mod.__name__]) * 1000000)
110
+# if t == 0: t=""
111
+ t = "" #FIXME
112
+ if not mod.update(t):
113
+# if not mod.update(int((t1- self.timetable[mod.__name__]) * 1000)):
114
+ self.disable_module(mod, "update failed")
115
+ return
116
+ except AttributeError:
117
+ self.disable_module(mod, "no update")
118
+ return
119
+ except UnboundLocalError:
120
+ self.disable_module(mod,"misbehaving")
121
+ return
122
+ t2 = time.time()
123
+# dt = int((t2 - self.timetable[mod.__name__]) * 1000)
124
+# if dt == 0: dt=""
125
+ dt="" #FIXME
126
+ sys.stdout.write("BEGIN netdata.plugin_pythond_"+mod.__name__+" "+str(dt)+'\n')
127
+ sys.stdout.write("SET run_time = "+str(int((t2 - t1))*1000)+'\n')
128
+ sys.stdout.write("END\n")
129
+ sys.stdout.flush()
130
+ self.timetable[mod.__name__] = t1
131
+
132
+ def update(self):
133
+ while True:
134
+ t_begin = time.time()
135
+ for mod in self.modules:
136
+ self.update_module(mod)
137
+ time.sleep(self.interval - (time.time()-t_begin))
138
+
139
+
140
+def read_config(path):
141
+ #TODO make it bulletproof
142
+ config = configparser.ConfigParser()
143
+ config_str = ""
144
+ try:
145
+ with open(path, 'r', encoding="utf_8") as f:
146
+ config_str = '[config]\n' + f.read()
147
+ except IsADirectoryError:
148
+ debug(str(path),"is a directory")
149
+ return
150
+ config.read_string(config_str)
151
+ return dict(config.items('config'))
152
+
153
+def debug(*args):
154
+ if not DEBUG_FLAG:
155
+ return
156
+ sys.stderr.write(PROGRAM+":")
157
+ for i in args:
158
+ sys.stderr.write(" "+str(i))
159
+ sys.stderr.write("\n")
160
+ sys.stderr.flush()
161
+
162
+def parse_cmdline(directory,*commands):
163
+ global DEBUG_FLAG, PROGRAM
164
+ DEBUG_FLAG=False
165
+
166
+ mods = []
167
+ for cmd in commands[1:]:
168
+ if cmd == "check":
169
+ pass
170
+ elif cmd == "debug" or cmd == "all":
171
+ DEBUG_FLAG=True
172
+ # redirect stderr to stdout?
173
+ elif os.path.isfile(directory+cmd+".chart.py") or os.path.isfile(directory+cmd):
174
+ DEBUG_FLAG=True
175
+ mods.append(cmd.replace(".chart.py",""))
176
+
177
+ PROGRAM = commands[0].split('/')[-1].split('.plugin')[0]
178
+ debug("started from",commands[0],"with options:",*commands[1:])
179
+
180
+ return mods
181
+
182
+#if __name__ == '__main__':
183
+def run():
184
+ # parse env variables
185
+ # https://github.com/firehol/netdata/wiki/External-Plugins#environment-variables
186
+ main_dir = os.getenv('NETDATA_PLUGINS_DIR',os.path.abspath(__file__).strip("python.d.plugin.py"))
187
+ config_dir = os.getenv('NETDATA_CONFIG_DIR',"/etc/netdata/")
188
+ # logs_dir = os.getenv('NETDATA_LOGS_DIR',None) #TODO logging?
189
+ interval = int(os.getenv('NETDATA_UPDATE_EVERY',1))
190
+
191
+ # read configuration file
192
+ if config_dir[-1] != '/':
193
+ configfile = config_dir + '/' + "python.d.conf"
194
+ else:
195
+ configfile = config_dir + "python.d.conf"
196
+
197
+ try:
198
+ conf = read_config(configfile)
199
+ try:
200
+ if str(conf['enable']) == "no":
201
+ debug("disabled in configuration file")
202
+ sys.exit(1)
203
+ except (KeyError,TypeError): pass
204
+ try: modules_conf = conf['plugins_config_dir']
205
+ except (KeyError,TypeError): modules_conf = config_dir # default configuration directory
206
+ try: modules_dir = conf['plugins_dir']
207
+ except (KeyError,TypeError): modules_dir = main_dir.replace("plugins.d","python.d")
208
+ try: interval = int(conf['interval'])
209
+ except (KeyError,TypeError): pass # default interval
210
+ global DEBUG_FLAG
211
+ try: DEBUG_FLAG = bool(conf['debug'])
212
+ except (KeyError,TypeError): pass
213
+ except FileNotFoundError:
214
+ modules_conf = config_dir
215
+ modules_dir = main_dir.replace("plugins.d","python.d")
216
+
217
+ # directories should end with '/'
218
+ if modules_dir[-1] != '/': modules_dir+="/"
219
+ if modules_conf[-1] != '/': modules_conf+="/"
220
+
221
+ # parse passed command line arguments
222
+ modules = parse_cmdline(modules_dir,*sys.argv)
223
+
224
+ # run plugins
225
+ charts = PythonCharts(interval,modules,modules_dir,modules_conf)
226
+ charts.check()
227
+ charts.create()
228
+ charts.update()
229
+
230
+if __name__ == '__main__':
231
+ run()
python.d/example.chart.py
new
+15
@@ -0,0 +1,15 @@
1
+import random
2
+
3
+def check():
4
+ return True
5
+
6
+def create():
7
+ print("CHART python_example.random '' 'A random number' 'random number' random random line 90000 1")
8
+ print("DIMENSION random1 '' absolute 1 1")
9
+ return True
10
+
11
+def update(interval):
12
+ print("BEGIN python_example.random "+str(interval))
13
+ print("SET random1 = "+str(random.randint(0,100)))
14
+ print("END")
15
+ return True