]>
git.ipfire.org Git - collecty.git/blob - src/collecty/plugins/base.py
2 ###############################################################################
4 # collecty - A system statistics collection daemon for IPFire #
5 # Copyright (C) 2012 IPFire development team #
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. #
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. #
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/>. #
20 ###############################################################################
22 from __future__
import division
33 from ..constants
import *
40 Returns a list with all automatically registered plugins.
42 return _plugins
.values()
45 def __init__(self
, timeout
, heartbeat
=1):
46 self
.timeout
= timeout
47 self
.heartbeat
= heartbeat
53 def reset(self
, delay
=0):
55 self
.start
= time
.time()
59 # Has this timer been killed?
64 return time
.time() - self
.start
- self
.delay
70 while self
.elapsed
< self
.timeout
and not self
.killed
:
71 time
.sleep(self
.heartbeat
)
73 return self
.elapsed
> self
.timeout
76 class Plugin(threading
.Thread
):
77 # The name of this plugin.
80 # A description for this plugin.
83 # Templates which can be used to generate a graph out of
84 # the data from this data source.
87 # The default interval for all plugins
90 # Automatically register all providers.
91 class __metaclass__(type):
92 def __init__(plugin
, name
, bases
, dict):
93 type.__init
__(plugin
, name
, bases
, dict)
95 # The main class from which is inherited is not registered
100 if not all((plugin
.name
, plugin
.description
)):
101 raise RuntimeError(_("Plugin is not properly configured: %s") \
104 _plugins
[plugin
.name
] = plugin
106 def __init__(self
, collecty
, **kwargs
):
107 threading
.Thread
.__init
__(self
, name
=self
.description
)
110 self
.collecty
= collecty
112 # Check if this plugin was configured correctly.
113 assert self
.name
, "Name of the plugin is not set: %s" % self
.name
114 assert self
.description
, "Description of the plugin is not set: %s" % self
.description
116 # Initialize the logger.
117 self
.log
= logging
.getLogger("collecty.plugins.%s" % self
.name
)
118 self
.log
.propagate
= 1
122 # Run some custom initialization.
127 self
.timer
= Timer(self
.interval
)
129 self
.log
.debug(_("Successfully initialized %s") % self
.__class
__.__name
__)
134 Returns the name of the sub directory in which all RRD files
135 for this plugin should be stored in.
141 def init(self
, **kwargs
):
143 Do some custom initialization stuff here.
149 Gathers the statistical data, this plugin collects.
151 time_start
= time
.time()
153 # Run through all objects of this plugin and call the collect method.
154 for o
in self
.objects
:
155 now
= datetime
.datetime
.utcnow()
159 if isinstance(result
, tuple) or isinstance(result
, list):
160 result
= ":".join(("%s" % e
for e
in result
))
162 self
.log
.warning(_("Unhandled exception in %s.collect()") % o
, exc_info
=True)
166 self
.log
.warning(_("Received empty result: %s") % o
)
169 self
.log
.debug(_("Collected %s: %s") % (o
, result
))
171 # Add the object to the write queue so that the data is written
172 # to the databases later.
173 self
.collecty
.write_queue
.add(o
, now
, result
)
175 # Returns the time this function took to complete.
176 return (time
.time() - time_start
)
179 self
.log
.debug(_("%s plugin has started") % self
.name
)
181 # Initially collect everything
188 # Wait until the timer has successfully elapsed.
189 if self
.timer
.wait():
190 delay
= self
.collect()
191 self
.timer
.reset(delay
)
193 self
.log
.debug(_("%s plugin has stopped") % self
.name
)
196 self
.log
.debug(_("Received shutdown signal."))
199 # Kill any running timers.
203 def get_object(self
, id):
204 for object in self
.objects
:
205 if not object.id == id:
210 def get_template(self
, template_name
, object_id
):
211 for template
in self
.templates
:
212 if not template
.name
== template_name
:
215 return template(self
, object_id
)
217 def generate_graph(self
, template_name
, object_id
="default", **kwargs
):
218 template
= self
.get_template(template_name
, object_id
=object_id
)
220 raise RuntimeError("Could not find template %s" % template_name
)
222 time_start
= time
.time()
224 graph
= template
.generate_graph(**kwargs
)
226 duration
= time
.time() - time_start
227 self
.log
.debug(_("Generated graph %s in %.1fms") \
228 % (template
, duration
* 1000))
233 class Object(object):
234 # The schema of the RRD database.
238 rra_types
= ["AVERAGE", "MIN", "MAX"]
239 rra_timespans
= [3600, 86400, 604800, 2678400, 31622400]
242 def __init__(self
, plugin
, *args
, **kwargs
):
245 # Indicates if this object has collected its data
246 self
.collected
= False
248 # Initialise this object
249 self
.init(*args
, **kwargs
)
251 # Create the database file.
255 return "<%s>" % self
.__class
__.__name
__
259 return self
.plugin
.collecty
263 return self
.plugin
.log
268 Returns a UNIQUE identifier for this object. As this is incorporated
269 into the path of RRD file, it must only contain ASCII characters.
271 raise NotImplementedError
276 The absolute path to the RRD file of this plugin.
278 filename
= self
._normalise
_filename
("%s.rrd" % self
.id)
280 return os
.path
.join(DATABASE_DIR
, self
.plugin
.path
, filename
)
283 def _normalise_filename(filename
):
284 # Convert the filename into ASCII characters only
285 filename
= filename
.encode("ascii", "ignore")
287 # Replace any spaces by dashes
288 filename
= filename
.replace(" ", "-")
294 def init(self
, *args
, **kwargs
):
296 Do some custom initialization stuff here.
302 Creates an empty RRD file with the desired data structures.
304 # Skip if the file does already exist.
305 if os
.path
.exists(self
.file):
308 dirname
= os
.path
.dirname(self
.file)
309 if not os
.path
.exists(dirname
):
312 # Create argument list.
313 args
= self
.get_rrd_schema()
315 rrdtool
.create(self
.file, *args
)
317 self
.log
.debug(_("Created RRD file %s.") % self
.file)
319 self
.log
.debug(" %s" % arg
)
322 return rrdtool
.info(self
.file)
326 return self
.plugin
.interval
330 return self
.stepsize
* 2
332 def get_rrd_schema(self
):
334 "--step", "%s" % self
.stepsize
,
336 for line
in self
.rrd_schema
:
337 if line
.startswith("DS:"):
339 (prefix
, name
, type, lower_limit
, upper_limit
) = line
.split(":")
345 "%s" % self
.heartbeat
,
357 for rra_timespan
in self
.rra_timespans
:
358 if (rra_timespan
/ self
.stepsize
) < self
.rra_rows
:
359 rra_timespan
= self
.stepsize
* self
.rra_rows
364 cdp_length
= rra_timespan
// (self
.rra_rows
* self
.stepsize
)
366 cdp_number
= math
.ceil(rra_timespan
/ (cdp_length
* self
.stepsize
))
368 for rra_type
in self
.rra_types
:
369 schema
.append("RRA:%s:%.10f:%d:%d" % \
370 (rra_type
, xff
, cdp_length
, cdp_number
))
376 raise RuntimeError("This object has already collected its data")
378 self
.collected
= True
379 self
.now
= datetime
.datetime
.utcnow()
382 result
= self
.collect()
386 Will commit the collected data to the database.
388 # Make sure that the RRD database has been created
392 class GraphTemplate(object):
393 # A unique name to identify this graph template.
396 # Headline of the graph image
399 # Vertical label of the graph
400 graph_vertical_label
= None
406 # Instructions how to create the graph.
409 # Extra arguments passed to rrdgraph.
420 # Default dimensions for this graph
421 height
= GRAPH_DEFAULT_HEIGHT
422 width
= GRAPH_DEFAULT_WIDTH
424 def __init__(self
, plugin
, object_id
):
427 # Get all required RRD objects
428 self
.object_id
= object_id
430 # Get the main object
431 self
.object = self
.get_object(self
.object_id
)
434 return "<%s>" % self
.__class
__.__name
__
438 return self
.plugin
.collecty
442 return self
.plugin
.log
444 def _make_command_line(self
, interval
, format
=DEFAULT_IMAGE_FORMAT
,
445 width
=None, height
=None):
448 args
+= GRAPH_DEFAULT_ARGUMENTS
451 "--imgformat", format
,
452 "--height", "%s" % (height
or self
.height
),
453 "--width", "%s" % (width
or self
.width
),
456 args
+= self
.rrd_graph_args
460 args
+= ["--title", self
.graph_title
]
463 if self
.graph_vertical_label
:
464 args
+= ["--vertical-label", self
.graph_vertical_label
]
466 if self
.lower_limit
is not None or self
.upper_limit
is not None:
467 # Force to honour the set limits
468 args
.append("--rigid")
470 if self
.lower_limit
is not None:
471 args
+= ["--lower-limit", self
.lower_limit
]
473 if self
.upper_limit
is not None:
474 args
+= ["--upper-limit", self
.upper_limit
]
477 args
.append("--start")
480 args
.append(self
.intervals
[interval
])
482 args
.append(str(interval
))
486 def get_object(self
, *args
, **kwargs
):
487 return self
.plugin
.get_object(*args
, **kwargs
)
489 def get_object_table(self
):
491 "file" : self
.object,
494 def get_object_files(self
):
497 for id, obj
in self
.get_object_table().items():
502 def generate_graph(self
, interval
=None, **kwargs
):
503 args
= self
._make
_command
_line
(interval
, **kwargs
)
505 self
.log
.info(_("Generating graph %s") % self
)
506 self
.log
.debug(" args: %s" % args
)
508 object_files
= self
.get_object_files()
510 for item
in self
.rrd_graph
:
512 args
.append(item
% object_files
)
516 self
.log
.debug(" %s" % args
[-1])
518 return self
.write_graph(*args
)
520 def write_graph(self
, *args
):
521 # Convert all arguments to string
522 args
= [str(e
) for e
in args
]
524 with tempfile
.NamedTemporaryFile() as f
:
525 rrdtool
.graph(f
.name
, *args
)
527 # Get back to the beginning of the file
530 # Return all the content