]> arthur.barton.de Git - netdata.git/blob - python.d/python_modules/base.py
eaee62869b5a3a3e93a84d9eac408cd1bf57c160
[netdata.git] / python.d / python_modules / base.py
1 # -*- coding: utf-8 -*-
2 # Description: prototypes for netdata python.d modules
3 # Author: Pawel Krupa (paulfantom)
4
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
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     def __init__(self, configuration=None, name=None):
22         """
23         This needs to be initialized in child classes
24         :param configuration: dict
25         :param name: str
26         """
27         threading.Thread.__init__(self)
28         self._data_stream = ""
29         self.daemon = True
30         self.retries = 0
31         self.retries_left = 0
32         self.priority = 140000
33         self.update_every = 1
34         self.name = name
35         self.override_name = None
36         self.chart_name = ""
37         self._dimensions = []
38         self._charts = []
39         if configuration is None:
40             self.error("BaseService: no configuration parameters supplied. Cannot create Service.")
41             raise RuntimeError
42         else:
43             self._extract_base_config(configuration)
44             self.timetable = {}
45             self.create_timetable()
46
47     def _extract_base_config(self, config):
48         """
49         Get basic parameters to run service
50         Minimum config:
51             config = {'update_every':1,
52                       'priority':100000,
53                       'retries':0}
54         :param config: dict
55         """
56         try:
57             self.override_name = config.pop('override_name')
58         except KeyError:
59             pass
60         self.update_every = int(config.pop('update_every'))
61         self.priority = int(config.pop('priority'))
62         self.retries = int(config.pop('retries'))
63         self.retries_left = self.retries
64         self.configuration = config
65
66     def create_timetable(self, freq=None):
67         """
68         Create service timetable.
69         `freq` is optional
70         Example:
71             timetable = {'last': 1466370091.3767564,
72                          'next': 1466370092,
73                          'freq': 1}
74         :param freq: int
75         """
76         if freq is None:
77             freq = self.update_every
78         now = time.time()
79         self.timetable = {'last': now,
80                           'next': now - (now % freq) + freq,
81                           'freq': freq}
82
83     def _run_once(self):
84         """
85         Executes self.update(interval) and draws run time chart.
86         Return value presents exit status of update()
87         :return: boolean
88         """
89         t_start = time.time()
90         # check if it is time to execute job update() function
91         if self.timetable['next'] > t_start:
92             msg.debug(self.chart_name + " will be run in " +
93                       str(int((self.timetable['next'] - t_start) * 1000)) + " ms")
94             return True
95
96         since_last = int((t_start - self.timetable['last']) * 1000000)
97         msg.debug(self.chart_name +
98                   " ready to run, after " + str(int((t_start - self.timetable['last']) * 1000)) +
99                   " ms (update_every: " + str(self.timetable['freq'] * 1000) +
100                   " ms, latency: " + str(int((t_start - self.timetable['next']) * 1000)) + " ms)")
101         if not self.update(since_last):
102             return False
103         t_end = time.time()
104         self.timetable['next'] = t_end - (t_end % self.timetable['freq']) + self.timetable['freq']
105
106         # draw performance graph
107         run_time = str(int((t_end - t_start) * 1000))
108         run_time_chart = "BEGIN netdata.plugin_pythond_" + self.chart_name + " " + str(since_last) + '\n'
109         run_time_chart += "SET run_time = " + run_time + '\n'
110         run_time_chart += "END\n"
111         sys.stdout.write(run_time_chart)
112         msg.debug(self.chart_name + " updated in " + str(run_time) + " ms")
113         self.timetable['last'] = t_start
114         return True
115
116     def run(self):
117         """
118         Runs job in thread. Handles retries.
119         Exits when job failed or timed out.
120         :return: None
121         """
122         self.timetable['last'] = time.time()
123         while True:
124             try:
125                 status = self._run_once()
126             except Exception as e:
127                 msg.error("Something wrong: " + str(e))
128                 return
129             if status:
130                 time.sleep(self.timetable['next'] - time.time())
131                 self.retries_left = self.retries
132             else:
133                 self.retries -= 1
134                 if self.retries_left <= 0:
135                     msg.error("no more retries. Exiting")
136                     return
137                 else:
138                     time.sleep(self.timetable['freq'])
139
140     def _line(self, instruction, *params):
141         """
142         Converts *params to string and joins them with one space between every one.
143         :param params: str/int/float
144         """
145         self._data_stream += instruction
146         for p in params:
147             if p is None:
148                 p = ""
149             else:
150                 p = str(p)
151             if len(p) == 0:
152                 p = "''"
153             if ' ' in p:
154                 p = "'" + p + "'"
155             self._data_stream += " " + p
156         self._data_stream += "\n"
157
158     def chart(self, type_id, name="", title="", units="", family="",
159               category="", charttype="line", priority="", update_every=""):
160         """
161         Defines a new chart.
162         :param type_id: str
163         :param name: str
164         :param title: str
165         :param units: str
166         :param family: str
167         :param category: str
168         :param charttype: str
169         :param priority: int/str
170         :param update_every: int/str
171         """
172         self._charts.append(type_id)
173         self._line("CHART", type_id, name, title, units, family, category, charttype, priority, update_every)
174
175     def dimension(self, id, name=None, algorithm="absolute", multiplier=1, divisor=1, hidden=False):
176         """
177         Defines a new dimension for the chart
178         :param id: str
179         :param name: str
180         :param algorithm: str
181         :param multiplier: int/str
182         :param divisor: int/str
183         :param hidden: boolean
184         :return:
185         """
186         try:
187             int(multiplier)
188         except TypeError:
189             self.error("malformed dimension: multiplier is not a number:", multiplier)
190             multiplier = 1
191         try:
192             int(divisor)
193         except TypeError:
194             self.error("malformed dimension: divisor is not a number:", divisor)
195             divisor = 1
196         if name is None:
197             name = id
198         if algorithm not in ("absolute", "incremental", "percentage-of-absolute-row", "percentage-of-incremental-row"):
199             algorithm = "absolute"
200
201         self._dimensions.append(id)
202         if hidden:
203             self._line("DIMENSION", id, name, algorithm, multiplier, divisor, "hidden")
204         else:
205             self._line("DIMENSION", id, name, algorithm, multiplier, divisor)
206
207     def begin(self, type_id, microseconds=0):
208         """
209         Begin data set
210         :param type_id: str
211         :param microseconds: int
212         :return: boolean
213         """
214         if type_id not in self._charts:
215             self.error("wrong chart type_id:", type_id)
216             return False
217         try:
218             int(microseconds)
219         except TypeError:
220             self.error("malformed begin statement: microseconds are not a number:", microseconds)
221             microseconds = ""
222
223         self._line("BEGIN", type_id, microseconds)
224         return True
225
226     def set(self, id, value):
227         """
228         Set value to dimension
229         :param id: str
230         :param value: int/float
231         :return: boolean
232         """
233         if id not in self._dimensions:
234             self.error("wrong dimension id:", id)
235             return False
236         try:
237             value = str(int(value))
238         except TypeError:
239             self.error("cannot set non-numeric value:", value)
240             return False
241         self._line("SET", id, "=", value)
242         return True
243
244     def end(self):
245         self._line("END")
246
247     def commit(self):
248         """
249         Upload new data to netdata
250         """
251         print(self._data_stream)
252         self._data_stream = ""
253
254     def error(self, *params):
255         """
256         Show error message on stderr
257         """
258         msg.error(self.chart_name, *params)
259
260     def debug(self, *params):
261         """
262         Show debug message on stderr
263         """
264         msg.debug(self.chart_name, *params)
265
266     def info(self, *params):
267         """
268         Show information message on stderr
269         """
270         msg.info(self.chart_name, *params)
271
272     def check(self):
273         """
274         check() prototype
275         :return: boolean
276         """
277         msg.error("Service " + str(self.__module__) + "doesn't implement check() function")
278         return False
279
280     def create(self):
281         """
282         create() prototype
283         :return: boolean
284         """
285         msg.error("Service " + str(self.__module__) + "doesn't implement create() function?")
286         return False
287
288     def update(self, interval):
289         """
290         update() prototype
291         :param interval: int
292         :return: boolean
293         """
294         msg.error("Service " + str(self.__module__) + "doesn't implement update() function")
295         return False
296
297
298 class SimpleService(BaseService):
299     def __init__(self, configuration=None, name=None):
300         self.order = []
301         self.definitions = {}
302         BaseService.__init__(self, configuration=configuration, name=name)
303
304     def _get_data(self):
305         """
306         Get raw data from http request
307         :return: str
308         """
309         return ""
310
311     def _formatted_data(self):
312         """
313         Format data received from http request
314         :return: dict
315         """
316         return {}
317
318     def check(self):
319         """
320         :return:
321         """
322         return True
323
324     def create(self):
325         """
326         Create charts
327         :return: boolean
328         """
329         data = self._formatted_data()
330         if data is None:
331             return False
332
333         idx = 0
334         for name in self.order:
335             options = self.definitions[name]['options'] + [self.priority + idx, self.update_every]
336             self.chart(self.__module__ + "_" + self.name + "." + name, *options)
337             # check if server has this datapoint
338             for line in self.definitions[name]['lines']:
339                 if line[0] in data:
340                     self.dimension(*line)
341             idx += 1
342
343         self.commit()
344         return True
345
346     def update(self, interval):
347         """
348         Update charts
349         :param interval: int
350         :return: boolean
351         """
352         data = self._formatted_data()
353         if data is None:
354             return False
355
356         updated = False
357         for chart in self.order:
358             if self.begin(self.__module__ + "_" + str(self.name) + "." + chart, interval):
359                 updated = True
360                 for dim in self.definitions[chart]['lines']:
361                     try:
362                         self.set(dim[0], data[dim[0]])
363                     except KeyError:
364                         pass
365                 self.end()
366
367         self.commit()
368
369         return updated
370
371
372 class UrlService(SimpleService):
373     def __init__(self, configuration=None, name=None):
374         # definitions are created dynamically in create() method based on 'charts' dictionary. format:
375         # definitions = {
376         #     'chart_name_in_netdata' : [ charts['chart_name_in_netdata']['lines']['name'] ]
377         # }
378         self.url = ""
379         SimpleService.__init__(self, configuration=configuration, name=name)
380
381     def _get_data(self):
382         """
383         Get raw data from http request
384         :return: str
385         """
386         raw = None
387         try:
388             f = urlopen(self.url, timeout=self.update_every)
389             raw = f.read().decode('utf-8')
390         except Exception as e:
391             msg.error(self.__module__, str(e))
392         finally:
393             try:
394                 f.close()
395             except:
396                 pass
397         return raw
398
399     def check(self):
400         """
401         Format configuration data and try to connect to server
402         :return: boolean
403         """
404         if self.name is None or self.name == str(None):
405             self.name = 'local'
406         else:
407             self.name = str(self.name)
408         try:
409             self.url = str(self.configuration['url'])
410         except (KeyError, TypeError):
411             pass
412
413         if self._formatted_data() is not None:
414             return True
415         else:
416             return False