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