]> git.ipfire.org Git - collecty.git/blame - collecty/plugins/base.py
Add latency plugin.
[collecty.git] / collecty / plugins / base.py
CommitLineData
eed405de
MT
1#!/usr/bin/python
2###############################################################################
3# #
4# collecty - A system statistics collection daemon for IPFire #
5# Copyright (C) 2012 IPFire development team #
6# #
7# This program is free software: you can redistribute it and/or modify #
8# it under the terms of the GNU General Public License as published by #
9# the Free Software Foundation, either version 3 of the License, or #
10# (at your option) any later version. #
11# #
12# This program is distributed in the hope that it will be useful, #
13# but WITHOUT ANY WARRANTY; without even the implied warranty of #
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
15# GNU General Public License for more details. #
16# #
17# You should have received a copy of the GNU General Public License #
18# along with this program. If not, see <http://www.gnu.org/licenses/>. #
19# #
20###############################################################################
21
965a9c51
MT
22from __future__ import division
23
4ac0cdf0 24import logging
965a9c51 25import math
4ac0cdf0
MT
26import os
27import rrdtool
49ce926e 28import threading
4ac0cdf0
MT
29import time
30
4ac0cdf0 31from ..constants import *
eed405de
MT
32from ..i18n import _
33
4be39bf9
MT
34class Timer(object):
35 def __init__(self, timeout, heartbeat=1):
36 self.timeout = timeout
37 self.heartbeat = heartbeat
38
39 self.reset()
40
41 def reset(self):
42 # Save start time.
43 self.start = time.time()
44
45 # Has this timer been killed?
46 self.killed = False
47
48 @property
49 def elapsed(self):
50 return time.time() - self.start
51
52 def cancel(self):
53 self.killed = True
54
55 def wait(self):
56 while self.elapsed < self.timeout and not self.killed:
57 time.sleep(self.heartbeat)
58
59 return self.elapsed > self.timeout
60
61
b1ea4956 62class DataSource(threading.Thread):
4ac0cdf0
MT
63 # The name of this plugin.
64 name = None
65
66 # A description for this plugin.
67 description = None
68
b1ea4956
MT
69 # Templates which can be used to generate a graph out of
70 # the data from this data source.
71 templates = []
72
4ac0cdf0
MT
73 # The schema of the RRD database.
74 rrd_schema = None
75
965a9c51
MT
76 # RRA properties.
77 rra_types = ["AVERAGE", "MIN", "MAX"]
78 rra_timespans = [3600, 86400, 604800, 2678400, 31622400]
79 rra_rows = 2880
80
4ac0cdf0
MT
81 # The default interval of this plugin.
82 default_interval = 60
83
eed405de 84 def __init__(self, collecty, **kwargs):
49ce926e
MT
85 threading.Thread.__init__(self, name=self.description)
86 self.daemon = True
87
eed405de
MT
88 self.collecty = collecty
89
4ac0cdf0
MT
90 # Check if this plugin was configured correctly.
91 assert self.name, "Name of the plugin is not set: %s" % self.name
92 assert self.description, "Description of the plugin is not set: %s" % self.description
93 assert self.rrd_schema
94
95 # Initialize the logger.
96 self.log = logging.getLogger("collecty.plugins.%s" % self.name)
97 self.log.propagate = 1
eed405de 98
eed405de
MT
99 self.data = []
100
269f74cd
MT
101 # Run some custom initialization.
102 self.init(**kwargs)
103
4ac0cdf0 104 # Create the database file.
eed405de 105 self.create()
4ac0cdf0 106
4be39bf9
MT
107 # Keepalive options
108 self.running = True
109 self.timer = Timer(self.interval)
110
75b3f22e 111 self.log.info(_("Successfully initialized (%s).") % self.id)
eed405de
MT
112
113 def __repr__(self):
b1ea4956 114 return "<%s %s>" % (self.__class__.__name__, self.id)
4ac0cdf0 115
73db5226
MT
116 @property
117 def id(self):
118 """
119 A unique ID of the plugin instance.
120 """
121 return self.name
122
4ac0cdf0
MT
123 @property
124 def interval(self):
125 """
126 Returns the interval in milliseconds, when the read method
127 should be called again.
128 """
129 # XXX read this from the settings
130
131 # Otherwise return the default.
132 return self.default_interval
133
965a9c51
MT
134 @property
135 def stepsize(self):
136 return self.interval
137
4ac0cdf0
MT
138 @property
139 def file(self):
140 """
141 The absolute path to the RRD file of this plugin.
142 """
75b3f22e 143 return os.path.join(DATABASE_DIR, "%s.rrd" % self.id)
eed405de 144
4ac0cdf0
MT
145 def create(self):
146 """
147 Creates an empty RRD file with the desired data structures.
148 """
149 # Skip if the file does already exist.
150 if os.path.exists(self.file):
151 return
eed405de 152
4ac0cdf0
MT
153 dirname = os.path.dirname(self.file)
154 if not os.path.exists(dirname):
155 os.makedirs(dirname)
eed405de 156
965a9c51
MT
157 # Create argument list.
158 args = [
159 "--step", "%s" % self.default_interval,
160 ] + self.get_rrd_schema()
161
162 rrdtool.create(self.file, *args)
eed405de 163
4ac0cdf0 164 self.log.debug(_("Created RRD file %s.") % self.file)
eed405de 165
965a9c51
MT
166 def get_rrd_schema(self):
167 schema = [
168 "--step", "%s" % self.stepsize,
169 ]
170 for line in self.rrd_schema:
171 if line.startswith("DS:"):
172 try:
173 (prefix, name, type, lower_limit, upper_limit) = line.split(":")
174
175 line = ":".join((
176 prefix,
177 name,
178 type,
179 "%s" % self.stepsize,
180 lower_limit,
181 upper_limit
182 ))
183 except ValueError:
184 pass
185
186 schema.append(line)
187
188 xff = 0.1
189
190 cdp_length = 0
191 for rra_timespan in self.rra_timespans:
192 if (rra_timespan / self.stepsize) < self.rra_rows:
193 rra_timespan = self.stepsize * self.rra_rows
194
195 if cdp_length == 0:
196 cdp_length = 1
197 else:
198 cdp_length = rra_timespan // (self.rra_rows * self.stepsize)
199
200 cdp_number = math.ceil(rra_timespan / (cdp_length * self.stepsize))
201
202 for rra_type in self.rra_types:
203 schema.append("RRA:%s:%.10f:%d:%d" % \
204 (rra_type, xff, cdp_length, cdp_number))
205
206 return schema
207
4ac0cdf0
MT
208 def info(self):
209 return rrdtool.info(self.file)
eed405de 210
4ac0cdf0
MT
211 ### Basic methods
212
269f74cd 213 def init(self, **kwargs):
4ac0cdf0
MT
214 """
215 Do some custom initialization stuff here.
216 """
217 pass
218
219 def read(self):
220 """
221 Gathers the statistical data, this plugin collects.
222 """
223 raise NotImplementedError
224
225 def submit(self):
226 """
227 Flushes the read data to disk.
228 """
229 # Do nothing in case there is no data to submit.
230 if not self.data:
231 return
232
49ce926e 233 self.log.debug(_("Submitting data to database. %d entries.") % len(self.data))
4ac0cdf0
MT
234 rrdtool.update(self.file, *self.data)
235 self.data = []
eed405de 236
4dc6b0c9 237 def _read(self, *args, **kwargs):
4ac0cdf0
MT
238 """
239 This method catches errors from the read() method and logs them.
240 """
241 try:
242 return self.read(*args, **kwargs)
243
244 # Catch any exceptions, so collecty does not crash.
245 except Exception, e:
246 self.log.critical(_("Unhandled exception in read()!"), exc_info=True)
247
4dc6b0c9 248 def _submit(self, *args, **kwargs):
4ac0cdf0
MT
249 """
250 This method catches errors from the submit() method and logs them.
251 """
252 try:
253 return self.submit(*args, **kwargs)
eed405de 254
4ac0cdf0
MT
255 # Catch any exceptions, so collecty does not crash.
256 except Exception, e:
257 self.log.critical(_("Unhandled exception in submit()!"), exc_info=True)
258
259 def run(self):
260 self.log.debug(_("Started."))
eed405de 261
4ac0cdf0 262 while self.running:
4be39bf9
MT
263 # Reset the timer.
264 self.timer.reset()
265
266 # Wait until the timer has successfully elapsed.
267 if self.timer.wait():
4ac0cdf0 268 self.log.debug(_("Collecting..."))
4dc6b0c9 269 self._read()
4ac0cdf0 270
4dc6b0c9 271 self._submit()
4ac0cdf0
MT
272 self.log.debug(_("Stopped."))
273
274 def shutdown(self):
275 self.log.debug(_("Received shutdown signal."))
276 self.running = False
277
4be39bf9
MT
278 # Kill any running timers.
279 if self.timer:
280 self.timer.cancel()
281
4ac0cdf0
MT
282 @property
283 def now(self):
284 """
285 Returns the current timestamp in the UNIX timestamp format (UTC).
286 """
287 return int(time.time())
eed405de 288
b1ea4956
MT
289
290class GraphTemplate(object):
291 # A unique name to identify this graph template.
292 name = None
293
294 # Instructions how to create the graph.
295 rrd_graph = None
296
297 # Extra arguments passed to rrdgraph.
298 rrd_graph_args = []
299
300 def __init__(self, ds):
301 self.ds = ds
302
303 @property
304 def collecty(self):
305 return self.ds.collecty
306
73db5226
MT
307 def graph(self, file, interval=None,
308 width=GRAPH_DEFAULT_WIDTH, height=GRAPH_DEFAULT_HEIGHT):
73db5226
MT
309 args = [
310 "--width", "%d" % width,
311 "--height", "%d" % height,
312 ]
313 args += self.collecty.graph_default_arguments
314 args += self.rrd_graph_args
eed405de 315
73db5226
MT
316 intervals = {
317 None : "-3h",
318 "hour" : "-1h",
319 "day" : "-25h",
320 "week" : "-360h",
321 "year" : "-365d",
322 }
eed405de
MT
323
324 args.append("--start")
b1ea4956 325 try:
eed405de 326 args.append(intervals[interval])
b1ea4956 327 except KeyError:
eed405de
MT
328 args.append(interval)
329
b1ea4956 330 info = { "file" : self.ds.file }
73db5226 331 for item in self.rrd_graph:
eed405de
MT
332 try:
333 args.append(item % info)
334 except TypeError:
335 args.append(item)
336
73db5226 337 rrdtool.graph(file, *args)