]> arthur.barton.de Git - netdata.git/blob - python.d/python_modules/base.py
resolves #602
[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     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         self.override_name = None
37         if configuration is None:
38             self.error("BaseService: no configuration parameters supplied. Cannot create Service.")
39             raise RuntimeError
40         else:
41             self._extract_base_config(configuration)
42             self.timetable = {}
43             self.create_timetable()
44             self.chart_name = ""
45
46     def _extract_base_config(self, config):
47         """
48         Get basic parameters to run service
49         Minimum config:
50             config = {'update_every':1,
51                       'priority':100000,
52                       'retries':0}
53         :param config: dict
54         """
55         try:
56             self.override_name = config.pop('override_name')
57         except KeyError:
58             pass
59         self.update_every = int(config.pop('update_every'))
60         self.priority = int(config.pop('priority'))
61         self.retries = int(config.pop('retries'))
62         self.retries_left = self.retries
63         self.configuration = config
64
65     def create_timetable(self, freq=None):
66         """
67         Create service timetable.
68         `freq` is optional
69         Example:
70             timetable = {'last': 1466370091.3767564,
71                          'next': 1466370092,
72                          'freq': 1}
73         :param freq: int
74         """
75         if freq is None:
76             freq = self.update_every
77         now = time.time()
78         self.timetable = {'last': now,
79                           'next': now - (now % freq) + freq,
80                           'freq': freq}
81
82     def _run_once(self):
83         """
84         Executes self.update(interval) and draws run time chart.
85         Return value presents exit status of update()
86         :return: boolean
87         """
88         t_start = time.time()
89         # check if it is time to execute job update() function
90         if self.timetable['next'] > t_start:
91             msg.debug(self.chart_name + " will be run in " +
92                       str(int((self.timetable['next'] - t_start) * 1000)) + " ms")
93             return True
94
95         since_last = int((t_start - self.timetable['last']) * 1000000)
96         msg.debug(self.chart_name +
97                   " ready to run, after " + str(int((t_start - self.timetable['last']) * 1000)) +
98                   " ms (update_every: " + str(self.timetable['freq'] * 1000) +
99                   " ms, latency: " + str(int((t_start - self.timetable['next']) * 1000)) + " ms)")
100         if not self.update(since_last):
101             return False
102         t_end = time.time()
103         self.timetable['next'] = t_end - (t_end % self.timetable['freq']) + self.timetable['freq']
104
105         # draw performance graph
106         run_time = str(int((t_end - t_start) * 1000))
107         run_time_chart = "BEGIN netdata.plugin_pythond_" + self.chart_name + " " + str(since_last) + '\n'
108         run_time_chart += "SET run_time = " + run_time + '\n'
109         run_time_chart += "END\n"
110         sys.stdout.write(run_time_chart)
111         msg.debug(self.chart_name + " updated in " + str(run_time) + " ms")
112         self.timetable['last'] = t_start
113         return True
114
115     def run(self):
116         """
117         Runs job in thread. Handles retries.
118         Exits when job failed or timed out.
119         :return: None
120         """
121         self.timetable['last'] = time.time()
122         while True:
123             try:
124                 status = self._run_once()
125             except Exception as e:
126                 msg.error("Something wrong: " + str(e))
127                 return
128             if status:
129                 time.sleep(self.timetable['next'] - time.time())
130                 self.retries_left = self.retries
131             else:
132                 self.retries -= 1
133                 if self.retries_left <= 0:
134                     msg.error("no more retries. Exiting")
135                     return
136                 else:
137                     time.sleep(self.timetable['freq'])
138
139     def check(self):
140         """
141         check() prototype
142         :return: boolean
143         """
144         msg.error("Service " + str(self.__module__) + "doesn't implement check() function")
145         return False
146
147     def create(self):
148         """
149         create() prototype
150         :return: boolean
151         """
152         msg.error("Service " + str(self.__module__) + "doesn't implement create() function?")
153         return False
154
155     def update(self, interval):
156         """
157         update() prototype
158         :param interval: int
159         :return: boolean
160         """
161         msg.error("Service " + str(self.__module__) + "doesn't implement update() function")
162         return False
163
164
165 class UrlService(BaseService):
166     def __init__(self, configuration=None, name=None):
167         self.charts = {}
168         # charts definitions in format:
169         # charts = {
170         #    'chart_name_in_netdata': {
171         #        'options': "parameters defining chart (passed to CHART statement)",
172         #        'lines': [
173         #           { 'name': 'dimension_name',
174         #             'options': 'dimension parameters (passed to DIMENSION statement)"
175         #           }
176         #        ]}
177         #    }
178         self.order = []
179         self.definitions = {}
180         # definitions are created dynamically in create() method based on 'charts' dictionary. format:
181         # definitions = {
182         #     'chart_name_in_netdata' : [ charts['chart_name_in_netdata']['lines']['name'] ]
183         # }
184         self.url = ""
185         BaseService.__init__(self, configuration=configuration, name=name)
186
187     def _get_data(self):
188         """
189         Get raw data from http request
190         :return: str
191         """
192         raw = None
193         try:
194             f = urlopen(self.url, timeout=self.update_every)
195             raw = f.read().decode('utf-8')
196         except Exception as e:
197             msg.error(self.__module__, str(e))
198         finally:
199             try:
200                 f.close()
201             except:
202                 pass
203         return raw
204
205     def _formatted_data(self):
206         """
207         Format data received from http request
208         :return: dict
209         """
210         return {}
211
212     def check(self):
213         """
214         Format configuration data and try to connect to server
215         :return: boolean
216         """
217         if self.name is None or self.name == str(None):
218             self.name = 'local'
219         else:
220             self.name = str(self.name)
221         try:
222             self.url = str(self.configuration['url'])
223         except (KeyError, TypeError):
224             pass
225
226         if self._formatted_data() is not None:
227             return True
228         else:
229             return False
230
231     def create(self):
232         """
233         Create charts
234         :return: boolean
235         """
236         for name in self.order:
237             if name not in self.charts:
238                 continue
239             self.definitions[name] = []
240             for line in self.charts[name]['lines']:
241                 self.definitions[name].append(line['name'])
242
243         idx = 0
244         data = self._formatted_data()
245         if data is None:
246             return False
247         data_stream = ""
248         for name in self.order:
249             header = "CHART " + \
250                      self.__module__ + "_" + \
251                      self.name + "." + \
252                      name + " " + \
253                      self.charts[name]['options'] + " " + \
254                      str(self.priority + idx) + " " + \
255                      str(self.update_every)
256             content = ""
257             # check if server has this datapoint
258             for line in self.charts[name]['lines']:
259                 if line['name'] in data:
260                     content += "\nDIMENSION " + line['name'] + " " + line['options']
261
262             if len(content) > 0:
263                 data_stream += header + content + "\n"
264                 idx += 1
265
266         print(data_stream)
267
268         if idx == 0:
269             return False
270         return True
271
272     def update(self, interval):
273         """
274         Update charts
275         :param interval: int
276         :return: boolean
277         """
278         data = self._formatted_data()
279         if data is None:
280             return False
281
282         data_stream = ""
283         for chart, dimensions in self.definitions.items():
284             header = "BEGIN " + self.__module__ + "_" + str(self.name) + "." + chart + " " + str(interval)
285             c = ""
286             for dim in dimensions:
287                 try:
288                     c += "\nSET " + dim + " = " + str(data[dim])
289                 except KeyError:
290                     pass
291             if len(c) != 0:
292                 data_stream += header + c + "\nEND\n"
293         print(data_stream)
294
295         return True