]>
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 ###############################################################################
32 from ..constants
import *
36 def __init__(self
, timeout
, heartbeat
=1):
37 self
.timeout
= timeout
38 self
.heartbeat
= heartbeat
44 def reset(self
, delay
=0):
46 self
.start
= time
.time()
50 # Has this timer been killed?
55 return time
.time() - self
.start
- self
.delay
61 while self
.elapsed
< self
.timeout
and not self
.killed
:
62 time
.sleep(self
.heartbeat
)
64 return self
.elapsed
> self
.timeout
67 class PluginRegistration(type):
70 def __init__(plugin
, name
, bases
, dict):
71 type.__init
__(plugin
, name
, bases
, dict)
73 # The main class from which is inherited is not registered
78 if not all((plugin
.name
, plugin
.description
)):
79 raise RuntimeError(_("Plugin is not properly configured: %s") % plugin
)
81 PluginRegistration
.plugins
[plugin
.name
] = plugin
86 Returns a list with all automatically registered plugins.
88 return PluginRegistration
.plugins
.values()
90 class Plugin(object, metaclass
=PluginRegistration
):
91 # The name of this plugin.
94 # A description for this plugin.
97 # Templates which can be used to generate a graph out of
98 # the data from this data source.
101 # The default interval for all plugins
104 def __init__(self
, collecty
, **kwargs
):
105 self
.collecty
= collecty
107 # Check if this plugin was configured correctly.
108 assert self
.name
, "Name of the plugin is not set: %s" % self
.name
109 assert self
.description
, "Description of the plugin is not set: %s" % self
.description
111 # Initialize the logger.
112 self
.log
= logging
.getLogger("collecty.plugins.%s" % self
.name
)
113 self
.log
.propagate
= 1
117 # Run some custom initialization.
120 self
.log
.debug(_("Successfully initialized %s") % self
.__class
__.__name
__)
125 Returns the name of the sub directory in which all RRD files
126 for this plugin should be stored in.
132 def init(self
, **kwargs
):
134 Do some custom initialization stuff here.
140 Gathers the statistical data, this plugin collects.
142 time_start
= time
.time()
144 # Run through all objects of this plugin and call the collect method.
145 for o
in self
.objects
:
146 now
= datetime
.datetime
.utcnow()
150 result
= self
._format
_result
(result
)
152 self
.log
.warning(_("Unhandled exception in %s.collect()") % o
, exc_info
=True)
156 self
.log
.warning(_("Received empty result: %s") % o
)
159 self
.log
.debug(_("Collected %s: %s") % (o
, result
))
161 # Add the object to the write queue so that the data is written
162 # to the databases later.
163 self
.collecty
.write_queue
.add(o
, now
, result
)
165 # Returns the time this function took to complete.
166 delay
= time
.time() - time_start
168 # Log some warning when a collect method takes too long to return some data
170 self
.log
.warning(_("A worker thread was stalled for %.4fs") % delay
)
173 def _format_result(result
):
174 if not isinstance(result
, tuple) and not isinstance(result
, list):
177 # Replace all Nones by NaN
191 def get_object(self
, id):
192 for object in self
.objects
:
193 if not object.id == id:
198 def get_template(self
, template_name
, object_id
):
199 for template
in self
.templates
:
200 if not template
.name
== template_name
:
203 return template(self
, object_id
)
205 def generate_graph(self
, template_name
, object_id
="default", **kwargs
):
206 template
= self
.get_template(template_name
, object_id
=object_id
)
208 raise RuntimeError("Could not find template %s" % template_name
)
210 time_start
= time
.time()
212 graph
= template
.generate_graph(**kwargs
)
214 duration
= time
.time() - time_start
215 self
.log
.debug(_("Generated graph %s in %.1fms") \
216 % (template
, duration
* 1000))
221 class Object(object):
222 # The schema of the RRD database.
226 rra_types
= ("AVERAGE", "MIN", "MAX")
233 def __init__(self
, plugin
, *args
, **kwargs
):
236 # Indicates if this object has collected its data
237 self
.collected
= False
239 # Initialise this object
240 self
.init(*args
, **kwargs
)
242 # Create the database file.
246 return "<%s>" % self
.__class
__.__name
__
250 return self
.plugin
.collecty
254 return self
.plugin
.log
259 Returns a UNIQUE identifier for this object. As this is incorporated
260 into the path of RRD file, it must only contain ASCII characters.
262 raise NotImplementedError
267 The absolute path to the RRD file of this plugin.
269 filename
= self
._normalise
_filename
("%s.rrd" % self
.id)
271 return os
.path
.join(DATABASE_DIR
, self
.plugin
.path
, filename
)
274 def _normalise_filename(filename
):
275 # Convert the filename into ASCII characters only
276 filename
= unicodedata
.normalize("NFKC", filename
)
278 # Replace any spaces by dashes
279 filename
= filename
.replace(" ", "-")
285 def init(self
, *args
, **kwargs
):
287 Do some custom initialization stuff here.
293 Creates an empty RRD file with the desired data structures.
295 # Skip if the file does already exist.
296 if os
.path
.exists(self
.file):
299 dirname
= os
.path
.dirname(self
.file)
300 if not os
.path
.exists(dirname
):
303 # Create argument list.
304 args
= self
.get_rrd_schema()
306 rrdtool
.create(self
.file, *args
)
308 self
.log
.debug(_("Created RRD file %s.") % self
.file)
310 self
.log
.debug(" %s" % arg
)
313 return rrdtool
.info(self
.file)
317 return self
.plugin
.interval
321 return self
.stepsize
* 2
323 def get_rrd_schema(self
):
325 "--step", "%s" % self
.stepsize
,
327 for line
in self
.rrd_schema
:
328 if line
.startswith("DS:"):
330 (prefix
, name
, type, lower_limit
, upper_limit
) = line
.split(":")
336 "%s" % self
.heartbeat
,
347 for steps
, rows
in self
.rra_timespans
:
348 for type in self
.rra_types
:
349 schema
.append("RRA:%s:%s:%s:%s" % (type, xff
, steps
, rows
))
355 raise RuntimeError("This object has already collected its data")
357 self
.collected
= True
358 self
.now
= datetime
.datetime
.utcnow()
361 result
= self
.collect()
365 Will commit the collected data to the database.
367 # Make sure that the RRD database has been created
371 class GraphTemplate(object):
372 # A unique name to identify this graph template.
375 # Headline of the graph image
378 # Vertical label of the graph
379 graph_vertical_label
= None
385 # Instructions how to create the graph.
388 # Extra arguments passed to rrdgraph.
400 # Default dimensions for this graph
401 height
= GRAPH_DEFAULT_HEIGHT
402 width
= GRAPH_DEFAULT_WIDTH
404 def __init__(self
, plugin
, object_id
):
407 # Get all required RRD objects
408 self
.object_id
= object_id
410 # Get the main object
411 self
.object = self
.get_object(self
.object_id
)
414 return "<%s>" % self
.__class
__.__name
__
418 return self
.plugin
.collecty
422 return self
.plugin
.log
424 def _make_command_line(self
, interval
, format
=DEFAULT_IMAGE_FORMAT
,
425 width
=None, height
=None):
428 args
+= GRAPH_DEFAULT_ARGUMENTS
431 "--imgformat", format
,
432 "--height", "%s" % (height
or self
.height
),
433 "--width", "%s" % (width
or self
.width
),
436 args
+= self
.rrd_graph_args
440 args
+= ["--title", self
.graph_title
]
443 if self
.graph_vertical_label
:
444 args
+= ["--vertical-label", self
.graph_vertical_label
]
446 if self
.lower_limit
is not None or self
.upper_limit
is not None:
447 # Force to honour the set limits
448 args
.append("--rigid")
450 if self
.lower_limit
is not None:
451 args
+= ["--lower-limit", self
.lower_limit
]
453 if self
.upper_limit
is not None:
454 args
+= ["--upper-limit", self
.upper_limit
]
457 interval
= self
.intervals
[interval
]
459 interval
= "end-%s" % interval
462 args
+= ["--start", interval
]
466 def get_object(self
, *args
, **kwargs
):
467 return self
.plugin
.get_object(*args
, **kwargs
)
469 def get_object_table(self
):
471 "file" : self
.object,
475 def object_table(self
):
476 if not hasattr(self
, "_object_table"):
477 self
._object
_table
= self
.get_object_table()
479 return self
._object
_table
481 def get_object_files(self
):
484 for id, obj
in self
.object_table
.items():
489 def generate_graph(self
, interval
=None, **kwargs
):
490 args
= self
._make
_command
_line
(interval
, **kwargs
)
492 self
.log
.info(_("Generating graph %s") % self
)
493 self
.log
.debug(" args: %s" % args
)
495 object_files
= self
.get_object_files()
497 for item
in self
.rrd_graph
:
499 args
.append(item
% object_files
)
503 self
.log
.debug(" %s" % args
[-1])
505 # Convert arguments to string
506 args
= [str(e
) for e
in args
]
508 graph
= rrdtool
.graphv("-", *args
)
510 return graph
.get("image")