]>
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 Environment(object):
69 Sets the correct environment for rrdtool to create
70 localised graphs and graphs in the correct timezone.
72 def __init__(self
, timezone
, locale
):
73 # Build the new environment
74 self
.new_environment
= {
75 "TZ" : timezone
or DEFAULT_TIMEZONE
,
78 for k
in ("LANG", "LC_ALL"):
79 self
.new_environment
[k
] = locale
or DEFAULT_LOCALE
82 # Save the current environment
83 self
.old_environment
= {}
84 for k
in self
.new_environment
:
85 self
.old_environment
[k
] = os
.environ
.get(k
, None)
88 os
.environ
.update(self
.new_environment
)
90 def __exit__(self
, type, value
, traceback
):
91 # Roll back to the previous environment
92 for k
, v
in self
.old_environment
.items():
102 class PluginRegistration(type):
105 def __init__(plugin
, name
, bases
, dict):
106 type.__init
__(plugin
, name
, bases
, dict)
108 # The main class from which is inherited is not registered
113 if not all((plugin
.name
, plugin
.description
)):
114 raise RuntimeError(_("Plugin is not properly configured: %s") % plugin
)
116 PluginRegistration
.plugins
[plugin
.name
] = plugin
121 Returns a list with all automatically registered plugins.
123 return PluginRegistration
.plugins
.values()
125 class Plugin(object, metaclass
=PluginRegistration
):
126 # The name of this plugin.
129 # A description for this plugin.
132 # Templates which can be used to generate a graph out of
133 # the data from this data source.
136 # The default interval for all plugins
139 def __init__(self
, collecty
, **kwargs
):
140 self
.collecty
= collecty
142 # Check if this plugin was configured correctly.
143 assert self
.name
, "Name of the plugin is not set: %s" % self
.name
144 assert self
.description
, "Description of the plugin is not set: %s" % self
.description
146 # Initialize the logger.
147 self
.log
= logging
.getLogger("collecty.plugins.%s" % self
.name
)
148 self
.log
.propagate
= 1
152 # Run some custom initialization.
155 self
.log
.debug(_("Successfully initialized %s") % self
.__class
__.__name
__)
160 Returns the name of the sub directory in which all RRD files
161 for this plugin should be stored in.
167 def init(self
, **kwargs
):
169 Do some custom initialization stuff here.
175 Gathers the statistical data, this plugin collects.
177 time_start
= time
.time()
179 # Run through all objects of this plugin and call the collect method.
180 for o
in self
.objects
:
181 now
= datetime
.datetime
.utcnow()
185 result
= self
._format
_result
(result
)
187 self
.log
.warning(_("Unhandled exception in %s.collect()") % o
, exc_info
=True)
191 self
.log
.warning(_("Received empty result: %s") % o
)
194 self
.log
.debug(_("Collected %s: %s") % (o
, result
))
196 # Add the object to the write queue so that the data is written
197 # to the databases later.
198 self
.collecty
.write_queue
.add(o
, now
, result
)
200 # Returns the time this function took to complete.
201 delay
= time
.time() - time_start
203 # Log some warning when a collect method takes too long to return some data
205 self
.log
.warning(_("A worker thread was stalled for %.4fs") % delay
)
208 def _format_result(result
):
209 if not isinstance(result
, tuple) and not isinstance(result
, list):
212 # Replace all Nones by NaN
226 def get_object(self
, id):
227 for object in self
.objects
:
228 if not object.id == id:
233 def get_template(self
, template_name
, object_id
):
234 for template
in self
.templates
:
235 if not template
.name
== template_name
:
238 return template(self
, object_id
)
240 def generate_graph(self
, template_name
, object_id
="default", **kwargs
):
241 template
= self
.get_template(template_name
, object_id
=object_id
)
243 raise RuntimeError("Could not find template %s" % template_name
)
245 time_start
= time
.time()
247 graph
= template
.generate_graph(**kwargs
)
249 duration
= time
.time() - time_start
250 self
.log
.debug(_("Generated graph %s in %.1fms") \
251 % (template
, duration
* 1000))
256 class Object(object):
257 # The schema of the RRD database.
261 rra_types
= ("AVERAGE", "MIN", "MAX")
268 def __init__(self
, plugin
, *args
, **kwargs
):
271 # Indicates if this object has collected its data
272 self
.collected
= False
274 # Initialise this object
275 self
.init(*args
, **kwargs
)
277 # Create the database file.
281 return "<%s>" % self
.__class
__.__name
__
285 return self
.plugin
.collecty
289 return self
.plugin
.log
294 Returns a UNIQUE identifier for this object. As this is incorporated
295 into the path of RRD file, it must only contain ASCII characters.
297 raise NotImplementedError
302 The absolute path to the RRD file of this plugin.
304 filename
= self
._normalise
_filename
("%s.rrd" % self
.id)
306 return os
.path
.join(DATABASE_DIR
, self
.plugin
.path
, filename
)
309 def _normalise_filename(filename
):
310 # Convert the filename into ASCII characters only
311 filename
= unicodedata
.normalize("NFKC", filename
)
313 # Replace any spaces by dashes
314 filename
= filename
.replace(" ", "-")
320 def init(self
, *args
, **kwargs
):
322 Do some custom initialization stuff here.
328 Creates an empty RRD file with the desired data structures.
330 # Skip if the file does already exist.
331 if os
.path
.exists(self
.file):
334 dirname
= os
.path
.dirname(self
.file)
335 if not os
.path
.exists(dirname
):
338 # Create argument list.
339 args
= self
.get_rrd_schema()
341 rrdtool
.create(self
.file, *args
)
343 self
.log
.debug(_("Created RRD file %s.") % self
.file)
345 self
.log
.debug(" %s" % arg
)
348 return rrdtool
.info(self
.file)
352 return self
.plugin
.interval
356 return self
.stepsize
* 2
358 def get_rrd_schema(self
):
360 "--step", "%s" % self
.stepsize
,
362 for line
in self
.rrd_schema
:
363 if line
.startswith("DS:"):
365 (prefix
, name
, type, lower_limit
, upper_limit
) = line
.split(":")
371 "%s" % self
.heartbeat
,
382 for steps
, rows
in self
.rra_timespans
:
383 for type in self
.rra_types
:
384 schema
.append("RRA:%s:%s:%s:%s" % (type, xff
, steps
, rows
))
390 raise RuntimeError("This object has already collected its data")
392 self
.collected
= True
393 self
.now
= datetime
.datetime
.utcnow()
396 result
= self
.collect()
400 Will commit the collected data to the database.
402 # Make sure that the RRD database has been created
406 class GraphTemplate(object):
407 # A unique name to identify this graph template.
410 # Headline of the graph image
413 # Vertical label of the graph
414 graph_vertical_label
= None
420 # Instructions how to create the graph.
423 # Extra arguments passed to rrdgraph.
435 # Default dimensions for this graph
436 height
= GRAPH_DEFAULT_HEIGHT
437 width
= GRAPH_DEFAULT_WIDTH
439 def __init__(self
, plugin
, object_id
):
442 # Get all required RRD objects
443 self
.object_id
= object_id
445 # Get the main object
446 self
.object = self
.get_object(self
.object_id
)
449 return "<%s>" % self
.__class
__.__name
__
453 return self
.plugin
.collecty
457 return self
.plugin
.log
459 def _make_command_line(self
, interval
, format
=DEFAULT_IMAGE_FORMAT
,
460 width
=None, height
=None):
463 args
+= GRAPH_DEFAULT_ARGUMENTS
466 "--imgformat", format
,
467 "--height", "%s" % (height
or self
.height
),
468 "--width", "%s" % (width
or self
.width
),
471 args
+= self
.rrd_graph_args
475 args
+= ["--title", self
.graph_title
]
478 if self
.graph_vertical_label
:
479 args
+= ["--vertical-label", self
.graph_vertical_label
]
481 if self
.lower_limit
is not None or self
.upper_limit
is not None:
482 # Force to honour the set limits
483 args
.append("--rigid")
485 if self
.lower_limit
is not None:
486 args
+= ["--lower-limit", self
.lower_limit
]
488 if self
.upper_limit
is not None:
489 args
+= ["--upper-limit", self
.upper_limit
]
492 interval
= self
.intervals
[interval
]
494 interval
= "end-%s" % interval
497 args
+= ["--start", interval
]
501 def get_object(self
, *args
, **kwargs
):
502 return self
.plugin
.get_object(*args
, **kwargs
)
504 def get_object_table(self
):
506 "file" : self
.object,
510 def object_table(self
):
511 if not hasattr(self
, "_object_table"):
512 self
._object
_table
= self
.get_object_table()
514 return self
._object
_table
516 def get_object_files(self
):
519 for id, obj
in self
.object_table
.items():
524 def generate_graph(self
, interval
=None, timezone
=None, locale
=None, **kwargs
):
525 args
= self
._make
_command
_line
(interval
, **kwargs
)
527 self
.log
.info(_("Generating graph %s") % self
)
528 self
.log
.debug(" args: %s" % args
)
530 object_files
= self
.get_object_files()
532 for item
in self
.rrd_graph
:
534 args
.append(item
% object_files
)
538 self
.log
.debug(" %s" % args
[-1])
540 # Convert arguments to string
541 args
= [str(e
) for e
in args
]
543 with
Environment(timezone
, locale
):
544 graph
= rrdtool
.graphv("-", *args
)
546 return graph
.get("image")