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