]>
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 ###############################################################################
30 from ..constants
import *
33 DEF_MATCH
= r
"C?DEF:([A-Za-z0-9_]+)="
35 class Environment(object):
37 Sets the correct environment for rrdtool to create
38 localised graphs and graphs in the correct timezone.
40 def __init__(self
, timezone
="UTC", locale
="en_US.utf-8"):
41 # Build the new environment
42 self
.new_environment
= {
49 # Save the current environment
50 self
.old_environment
= {}
52 for k
in self
.new_environment
:
53 self
.old_environment
[k
] = os
.environ
.get(k
, None)
56 os
.environ
.update(self
.new_environment
)
58 def __exit__(self
, type, value
, traceback
):
59 # Roll back to the previous environment
60 for k
, v
in self
.old_environment
.items():
70 class PluginRegistration(type):
73 def __init__(plugin
, name
, bases
, dict):
74 type.__init
__(plugin
, name
, bases
, dict)
76 # The main class from which is inherited is not registered
81 if not all((plugin
.name
, plugin
.description
)):
82 raise RuntimeError(_("Plugin is not properly configured: %s") % plugin
)
84 PluginRegistration
.plugins
[plugin
.name
] = plugin
89 Returns a list with all automatically registered plugins.
91 return PluginRegistration
.plugins
.values()
93 class Plugin(object, metaclass
=PluginRegistration
):
94 # The name of this plugin.
97 # A description for this plugin.
100 # Templates which can be used to generate a graph out of
101 # the data from this data source.
104 # The default interval for all plugins
110 def __init__(self
, collecty
, **kwargs
):
111 self
.collecty
= collecty
113 # Check if this plugin was configured correctly.
114 assert self
.name
, "Name of the plugin is not set: %s" % self
.name
115 assert self
.description
, "Description of the plugin is not set: %s" % self
.description
117 # Initialize the logger.
118 self
.log
= logging
.getLogger("collecty.plugins.%s" % self
.name
)
120 # Run some custom initialization.
123 self
.log
.debug(_("Successfully initialized %s") % self
.__class
__.__name
__)
128 Returns the name of the sub directory in which all RRD files
129 for this plugin should be stored in.
135 def init(self
, **kwargs
):
137 Do some custom initialization stuff here.
143 Gathers the statistical data, this plugin collects.
145 time_start
= time
.time()
147 # Run through all objects of this plugin and call the collect method.
148 for object in self
.objects
:
151 result
= object.collect()
153 # Catch any unhandled exceptions
154 except Exception as e
:
155 self
.log
.warning(_("Unhandled exception in %s.collect()") % object, exc_info
=True)
159 self
.log
.warning(_("Received empty result: %s") % object)
162 # Add the object to the write queue so that the data is written
163 # to the databases later.
164 result
= self
.collecty
.write_queue
.submit(object, result
)
166 self
.log
.debug(_("Collected %s: %s") % (object, result
))
168 # Returns the time this function took to complete.
169 delay
= time
.time() - time_start
171 # Log some warning when a collect method takes too long to return some data
173 self
.log
.warning(_("A worker thread was stalled for %.4fs") % delay
)
175 self
.log
.debug(_("Collection finished in %.2fms") % (delay
* 1000))
177 def get_object(self
, id):
178 for object in self
.objects
:
179 if not object.id == id:
184 def get_template(self
, template_name
, object_id
, locale
=None, timezone
=None):
185 for template
in self
.templates
:
186 if not template
.name
== template_name
:
189 return template(self
, object_id
, locale
=locale
, timezone
=timezone
)
191 def generate_graph(self
, template_name
, object_id
="default",
192 timezone
=None, locale
=None, **kwargs
):
193 template
= self
.get_template(template_name
, object_id
=object_id
,
194 timezone
=timezone
, locale
=locale
)
196 raise RuntimeError("Could not find template %s" % template_name
)
198 time_start
= time
.time()
200 with
Environment(timezone
=timezone
, locale
=locale
):
201 graph
= template
.generate_graph(**kwargs
)
203 duration
= time
.time() - time_start
204 self
.log
.debug(_("Generated graph %s in %.1fms") \
205 % (template
, duration
* 1000))
209 def graph_info(self
, template_name
, object_id
="default",
210 timezone
=None, locale
=None, **kwargs
):
211 template
= self
.get_template(template_name
, object_id
=object_id
,
212 timezone
=timezone
, locale
=locale
)
214 raise RuntimeError("Could not find template %s" % template_name
)
216 return template
.graph_info()
218 def last_update(self
, object_id
="default"):
219 object = self
.get_object(object_id
)
221 raise RuntimeError("Could not find object %s" % object_id
)
223 return object.last_update()
226 class Object(object):
227 # The schema of the RRD database.
231 rra_types
= ("AVERAGE", "MIN", "MAX")
238 def __init__(self
, plugin
, *args
, **kwargs
):
241 # Initialise this object
242 self
.init(*args
, **kwargs
)
244 # Create the database file.
248 return "<%s %s>" % (self
.__class
__.__name
__, self
.id)
250 def __lt__(self
, other
):
251 return self
.id < other
.id
255 return self
.plugin
.collecty
259 return self
.plugin
.log
264 Returns a UNIQUE identifier for this object. As this is incorporated
265 into the path of RRD file, it must only contain ASCII characters.
267 raise NotImplementedError
272 The absolute path to the RRD file of this plugin.
274 filename
= self
._normalise
_filename
("%s.rrd" % self
.id)
276 return os
.path
.join(DATABASE_DIR
, self
.plugin
.path
, filename
)
279 def _normalise_filename(filename
):
280 # Convert the filename into ASCII characters only
281 filename
= unicodedata
.normalize("NFKC", filename
)
283 # Replace any spaces by dashes
284 filename
= filename
.replace(" ", "-")
290 def init(self
, *args
, **kwargs
):
292 Do some custom initialization stuff here.
298 Creates an empty RRD file with the desired data structures.
300 # Skip if the file does already exist.
301 if os
.path
.exists(self
.file):
304 dirname
= os
.path
.dirname(self
.file)
305 if not os
.path
.exists(dirname
):
308 # Create argument list.
309 args
= self
.get_rrd_schema()
311 rrdtool
.create(self
.file, *args
)
313 self
.log
.debug(_("Created RRD file %s.") % self
.file)
315 self
.log
.debug(" %s" % arg
)
318 return rrdtool
.info(self
.file)
320 def last_update(self
):
322 Returns a dictionary with the timestamp and
323 data set of the last database update.
326 "dataset" : self
.last_dataset
,
327 "timestamp" : self
.last_updated
,
330 def _last_update(self
):
331 return rrdtool
.lastupdate(self
.file)
334 def last_updated(self
):
336 Returns the timestamp when this database was last updated
338 lu
= self
._last
_update
()
341 return lu
.get("date")
344 def last_dataset(self
):
346 Returns the latest dataset in the database
348 lu
= self
._last
_update
()
355 return self
.plugin
.interval
359 return self
.stepsize
* 2
361 def get_rrd_schema(self
):
363 "--step", "%s" % self
.stepsize
,
365 for line
in self
.rrd_schema
:
366 if line
.startswith("DS:"):
368 (prefix
, name
, type, lower_limit
, upper_limit
) = line
.split(":")
374 "%s" % self
.heartbeat
,
385 for steps
, rows
in self
.rra_timespans
:
386 for type in self
.rra_types
:
387 schema
.append("RRA:%s:%s:%s:%s" % (type, xff
, steps
, rows
))
392 def rrd_schema_names(self
):
395 for line
in self
.rrd_schema
:
396 (prefix
, name
, type, lower_limit
, upper_limit
) = line
.split(":")
401 def make_rrd_defs(self
, prefix
=None):
404 for name
in self
.rrd_schema_names
:
406 p
= "%s_%s" % (prefix
, name
)
411 "DEF:%s=%s:%s:AVERAGE" % (p
, self
.file, name
),
416 def get_stddev(self
, interval
=None):
417 args
= self
.make_rrd_defs()
419 # Add the correct interval
420 args
+= ["--start", util
.make_interval(interval
)]
422 for name
in self
.rrd_schema_names
:
424 "VDEF:%s_stddev=%s,STDEV" % (name
, name
),
425 "PRINT:%s_stddev:%%lf" % name
,
428 x
, y
, vals
= rrdtool
.graph("/dev/null", *args
)
429 return dict(zip(self
.rrd_schema_names
, vals
))
433 Will commit the collected data to the database.
435 # Make sure that the RRD database has been created
438 # Write everything to disk that is in the write queue
439 self
.collecty
.write_queue
.commit_file(self
.file)
441 # Convenience functions for plugin authors
443 def read_file(self
, *args
, strip
=True):
445 Reads the content of the given file
447 filename
= os
.path
.join(*args
)
449 with
open(filename
) as f
:
452 # Strip any excess whitespace
454 value
= value
.strip()
458 def read_file_integer(self
, filename
):
460 Reads the content from a file and returns it as an integer
462 value
= self
.read_file(filename
)
469 def read_proc_stat(self
):
471 Reads /proc/stat and returns it as a dictionary
475 with
open("/proc/stat") as f
:
477 # Split the key from the rest of the line
478 key
, line
= line
.split(" ", 1)
480 # Remove any line breaks
481 ret
[key
] = line
.rstrip()
485 def read_proc_meminfo(self
):
488 with
open("/proc/meminfo") as f
:
490 # Split the key from the rest of the line
491 key
, line
= line
.split(":", 1)
493 # Remove any whitespace
496 # Remove any trailing kB
497 if line
.endswith(" kB"):
500 # Try to convert to integer
503 except (TypeError, ValueError):
511 class GraphTemplate(object):
512 # A unique name to identify this graph template.
515 # Headline of the graph image
518 # Vertical label of the graph
519 graph_vertical_label
= None
525 # Instructions how to create the graph.
528 # Extra arguments passed to rrdgraph.
531 def __init__(self
, plugin
, object_id
, locale
=None, timezone
=None):
534 # Save localisation parameters
536 self
.timezone
= timezone
538 # Get all required RRD objects
539 self
.object_id
= object_id
541 # Get the main object
542 self
.objects
= self
.get_objects(self
.object_id
)
546 return "<%s>" % self
.__class
__.__name
__
550 return self
.plugin
.collecty
554 return self
.plugin
.log
559 Shortcut to the main object
561 if len(self
.objects
) == 1:
562 return self
.objects
[0]
564 def _make_command_line(self
, interval
, format
=DEFAULT_IMAGE_FORMAT
,
565 width
=None, height
=None, with_title
=True, thumbnail
=False):
567 # Change the background colour
568 "--color", "BACK#FFFFFFFF",
570 # Disable the border around the image
573 # Let's width and height define the size of the entire image
576 # Gives the curves a more organic look
582 # Brand all generated graphs
583 "--watermark", _("Created by collecty"),
586 # Set the default dimensions
587 default_width
, default_height
= 960, 480
589 # A thumbnail doesn't have a legend and other labels
591 args
.append("--only-graph")
593 default_width
, default_height
= 80, 20
596 "--imgformat", format
,
597 "--height", "%s" % (height
or default_height
),
598 "--width", "%s" % (width
or default_width
),
601 args
+= self
.rrd_graph_args
604 if with_title
and self
.graph_title
:
605 args
+= ["--title", self
.graph_title
]
608 if self
.graph_vertical_label
:
609 args
+= ["--vertical-label", self
.graph_vertical_label
]
611 if self
.lower_limit
is not None or self
.upper_limit
is not None:
612 # Force to honour the set limits
613 args
.append("--rigid")
615 if self
.lower_limit
is not None:
616 args
+= ["--lower-limit", self
.lower_limit
]
618 if self
.upper_limit
is not None:
619 args
+= ["--upper-limit", self
.upper_limit
]
622 args
+= ["--start", util
.make_interval(interval
)]
627 use_prefix
= len(self
.objects
) >= 2
630 for object in self
.objects
:
632 args
+= object.make_rrd_defs(object.id)
634 args
+= object.make_rrd_defs()
638 def _add_vdefs(self
, args
):
644 # Search for all DEFs and CDEFs
645 m
= re
.match(DEF_MATCH
, "%s" % arg
)
649 # Add the VDEFs for minimum, maximum, etc. values
651 "VDEF:%s_cur=%s,LAST" % (name
, name
),
652 "VDEF:%s_avg=%s,AVERAGE" % (name
, name
),
653 "VDEF:%s_max=%s,MAXIMUM" % (name
, name
),
654 "VDEF:%s_min=%s,MINIMUM" % (name
, name
),
659 def get_objects(self
, *args
, **kwargs
):
660 object = self
.plugin
.get_object(*args
, **kwargs
)
667 def generate_graph(self
, interval
=None, **kwargs
):
668 assert self
.objects
, "Cannot render graph without any objects"
670 # Make sure that all collected data is in the database
671 # to get a recent graph image
672 for object in self
.objects
:
675 args
= self
._make
_command
_line
(interval
, **kwargs
)
677 self
.log
.info(_("Generating graph %s") % self
)
679 rrd_graph
= self
.rrd_graph
681 # Add DEFs for all objects
682 if not any((e
.startswith("DEF:") for e
in rrd_graph
)):
683 args
+= self
._add
_defs
()
686 args
= self
._add
_vdefs
(args
)
688 # Convert arguments to string
689 args
= [str(e
) for e
in args
]
692 self
.log
.debug(" %s" % arg
)
694 graph
= rrdtool
.graphv("-", *args
)
697 "image" : graph
.get("image"),
698 "image_height" : graph
.get("image_height"),
699 "image_width" : graph
.get("image_width"),
702 def graph_info(self
):
704 Returns a dictionary with useful information
708 "title" : self
.graph_title
,
709 "object_id" : self
.object_id
or "",
710 "template" : self
.name
,