]> git.ipfire.org Git - collecty.git/blobdiff - src/collecty/plugins/base.py
plugins: Automatically replace None by NaN
[collecty.git] / src / collecty / plugins / base.py
index 6c31edebf9d5969e02f6e601994192e679f59f7d..cf9c3b402410bf085fd7b0e825644aef42d6fba4 100644 (file)
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/python3
 ###############################################################################
 #                                                                             #
 # collecty - A system statistics collection daemon for IPFire                 #
@@ -19,8 +19,6 @@
 #                                                                             #
 ###############################################################################
 
-from __future__ import division
-
 import datetime
 import logging
 import math
@@ -29,18 +27,11 @@ import rrdtool
 import tempfile
 import threading
 import time
+import unicodedata
 
 from ..constants import *
 from ..i18n import _
 
-_plugins = {}
-
-def get():
-       """
-               Returns a list with all automatically registered plugins.
-       """
-       return _plugins.values()
-
 class Timer(object):
        def __init__(self, timeout, heartbeat=1):
                self.timeout = timeout
@@ -73,7 +64,30 @@ class Timer(object):
                return self.elapsed > self.timeout
 
 
-class Plugin(threading.Thread):
+class PluginRegistration(type):
+       plugins = {}
+
+       def __init__(plugin, name, bases, dict):
+               type.__init__(plugin, name, bases, dict)
+
+               # The main class from which is inherited is not registered
+               # as a plugin.
+               if name == "Plugin":
+                       return
+
+               if not all((plugin.name, plugin.description)):
+                       raise RuntimeError(_("Plugin is not properly configured: %s") % plugin)
+
+               PluginRegistration.plugins[plugin.name] = plugin
+
+
+def get():
+       """
+               Returns a list with all automatically registered plugins.
+       """
+       return PluginRegistration.plugins.values()
+
+class Plugin(object, metaclass=PluginRegistration):
        # The name of this plugin.
        name = None
 
@@ -87,26 +101,7 @@ class Plugin(threading.Thread):
        # The default interval for all plugins
        interval = 60
 
-       # Automatically register all providers.
-       class __metaclass__(type):
-               def __init__(plugin, name, bases, dict):
-                       type.__init__(plugin, name, bases, dict)
-
-                       # The main class from which is inherited is not registered
-                       # as a plugin.
-                       if name == "Plugin":
-                               return
-
-                       if not all((plugin.name, plugin.description)):
-                               raise RuntimeError(_("Plugin is not properly configured: %s") \
-                                       % plugin)
-
-                       _plugins[plugin.name] = plugin
-
        def __init__(self, collecty, **kwargs):
-               threading.Thread.__init__(self, name=self.description)
-               self.daemon = True
-
                self.collecty = collecty
 
                # Check if this plugin was configured correctly.
@@ -122,10 +117,6 @@ class Plugin(threading.Thread):
                # Run some custom initialization.
                self.init(**kwargs)
 
-               # Keepalive options
-               self.running = True
-               self.timer = Timer(self.interval)
-
                self.log.debug(_("Successfully initialized %s") % self.__class__.__name__)
 
        @property
@@ -156,8 +147,7 @@ class Plugin(threading.Thread):
                        try:
                                result = o.collect()
 
-                               if isinstance(result, tuple) or isinstance(result, list):
-                                       result = ":".join(("%s" % e for e in result))
+                               result = self._format_result(result)
                        except:
                                self.log.warning(_("Unhandled exception in %s.collect()") % o, exc_info=True)
                                continue
@@ -173,32 +163,30 @@ class Plugin(threading.Thread):
                        self.collecty.write_queue.add(o, now, result)
 
                # Returns the time this function took to complete.
-               return (time.time() - time_start)
+               delay = time.time() - time_start
 
-       def run(self):
-               self.log.debug(_("%s plugin has started") % self.name)
+               # Log some warning when a collect method takes too long to return some data
+               if delay >= 60:
+                       self.log.warning(_("A worker thread was stalled for %.4fs") % delay)
 
-               # Initially collect everything
-               self.collect()
+       @staticmethod
+       def _format_result(result):
+               if not isinstance(result, tuple) and not isinstance(result, list):
+                       return result
 
-               while self.running:
-                       # Reset the timer.
-                       self.timer.reset()
+               # Replace all Nones by NaN
+               s = []
 
-                       # Wait until the timer has successfully elapsed.
-                       if self.timer.wait():
-                               delay = self.collect()
-                               self.timer.reset(delay)
+               for e in result:
+                       if e is None:
+                               e = "NaN"
 
-               self.log.debug(_("%s plugin has stopped") % self.name)
+                       # Format as string
+                       e = "%s" % e
 
-       def shutdown(self):
-               self.log.debug(_("Received shutdown signal."))
-               self.running = False
+                       s.append(e)
 
-               # Kill any running timers.
-               if self.timer:
-                       self.timer.cancel()
+               return ":".join(s)
 
        def get_object(self, id):
                for object in self.objects:
@@ -235,9 +223,12 @@ class Object(object):
        rrd_schema = None
 
        # RRA properties.
-       rra_types     = ["AVERAGE", "MIN", "MAX"]
-       rra_timespans = [3600, 86400, 604800, 2678400, 31622400]
-       rra_rows      = 2880
+       rra_types     = ("AVERAGE", "MIN", "MAX")
+       rra_timespans = (
+               ("1m", "10d"),
+               ("1h", "18M"),
+               ("1d",  "5y"),
+       )
 
        def __init__(self, plugin, *args, **kwargs):
                self.plugin = plugin
@@ -275,7 +266,19 @@ class Object(object):
                """
                        The absolute path to the RRD file of this plugin.
                """
-               return os.path.join(DATABASE_DIR, self.plugin.path, "%s.rrd" % self.id)
+               filename = self._normalise_filename("%s.rrd" % self.id)
+
+               return os.path.join(DATABASE_DIR, self.plugin.path, filename)
+
+       @staticmethod
+       def _normalise_filename(filename):
+               # Convert the filename into ASCII characters only
+               filename = unicodedata.normalize("NFKC", filename)
+
+               # Replace any spaces by dashes
+               filename = filename.replace(" ", "-")
+
+               return filename
 
        ### Basic methods
 
@@ -341,21 +344,9 @@ class Object(object):
 
                xff = 0.1
 
-               cdp_length = 0
-               for rra_timespan in self.rra_timespans:
-                       if (rra_timespan / self.stepsize) < self.rra_rows:
-                               rra_timespan = self.stepsize * self.rra_rows
-
-                       if cdp_length == 0:
-                               cdp_length = 1
-                       else:
-                               cdp_length = rra_timespan // (self.rra_rows * self.stepsize)
-
-                       cdp_number = math.ceil(rra_timespan / (cdp_length * self.stepsize))
-
-                       for rra_type in self.rra_types:
-                               schema.append("RRA:%s:%.10f:%d:%d" % \
-                                       (rra_type, xff, cdp_length, cdp_number))
+               for steps, rows in self.rra_timespans:
+                       for type in self.rra_types:
+                               schema.append("RRA:%s:%s:%s:%s" % (type, xff, steps, rows))
 
                return schema
 
@@ -401,6 +392,7 @@ class GraphTemplate(object):
                None   : "-3h",
                "hour" : "-1h",
                "day"  : "-25h",
+               "month": "-30d",
                "week" : "-360h",
                "year" : "-365d",
        }
@@ -461,13 +453,13 @@ class GraphTemplate(object):
                        if self.upper_limit is not None:
                                args += ["--upper-limit", self.upper_limit]
 
-               # Add interval
-               args.append("--start")
-
                try:
-                       args.append(self.intervals[interval])
+                       interval = self.intervals[interval]
                except KeyError:
-                       args.append(str(interval))
+                       interval = "end-%s" % interval
+
+               # Add interval
+               args += ["--start", interval]
 
                return args
 
@@ -479,10 +471,17 @@ class GraphTemplate(object):
                        "file" : self.object,
                }
 
+       @property
+       def object_table(self):
+               if not hasattr(self, "_object_table"):
+                       self._object_table = self.get_object_table()
+
+               return self._object_table
+
        def get_object_files(self):
                files = {}
 
-               for id, obj in self.get_object_table().items():
+               for id, obj in self.object_table.items():
                        files[id] = obj.file
 
                return files
@@ -503,17 +502,9 @@ class GraphTemplate(object):
 
                        self.log.debug("  %s" % args[-1])
 
-               return self.write_graph(*args)
-
-       def write_graph(self, *args):
-               # Convert all arguments to string
+               # Convert arguments to string
                args = [str(e) for e in args]
 
-               with tempfile.NamedTemporaryFile() as f:
-                       rrdtool.graph(f.name, *args)
-
-                       # Get back to the beginning of the file
-                       f.seek(0)
+               graph = rrdtool.graphv("-", *args)
 
-                       # Return all the content
-                       return f.read()
+               return graph.get("image")