]>
git.ipfire.org Git - oddments/collecty.git/blob - src/collecty/plugins/base.py
04180027520ffc0c512592db672bf2559ea2bf43
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 ###############################################################################
29 from .. import locales
31 from ..constants
import *
34 DEF_MATCH
= r
"C?DEF:([A-Za-z0-9_]+)="
36 class Environment(object):
38 Sets the correct environment for rrdtool to create
39 localised graphs and graphs in the correct timezone.
41 def __init__(self
, timezone
, locale
):
42 # Build the new environment
43 self
.new_environment
= {
44 "TZ" : timezone
or DEFAULT_TIMEZONE
,
47 for k
in ("LANG", "LC_ALL"):
48 self
.new_environment
[k
] = locale
or DEFAULT_LOCALE
51 # Save the current environment
52 self
.old_environment
= {}
53 for k
in self
.new_environment
:
54 self
.old_environment
[k
] = os
.environ
.get(k
, None)
57 os
.environ
.update(self
.new_environment
)
59 def __exit__(self
, type, value
, traceback
):
60 # Roll back to the previous environment
61 for k
, v
in self
.old_environment
.items():
71 class PluginRegistration(type):
74 def __init__(plugin
, name
, bases
, dict):
75 type.__init
__(plugin
, name
, bases
, dict)
77 # The main class from which is inherited is not registered
82 if not all((plugin
.name
, plugin
.description
)):
83 raise RuntimeError(_("Plugin is not properly configured: %s") % plugin
)
85 PluginRegistration
.plugins
[plugin
.name
] = plugin
90 Returns a list with all automatically registered plugins.
92 return PluginRegistration
.plugins
.values()
94 class Plugin(object, metaclass
=PluginRegistration
):
95 # The name of this plugin.
98 # A description for this plugin.
101 # Templates which can be used to generate a graph out of
102 # the data from this data source.
105 # The default interval for all plugins
111 def __init__(self
, collecty
, **kwargs
):
112 self
.collecty
= collecty
114 # Check if this plugin was configured correctly.
115 assert self
.name
, "Name of the plugin is not set: %s" % self
.name
116 assert self
.description
, "Description of the plugin is not set: %s" % self
.description
118 # Initialize the logger.
119 self
.log
= logging
.getLogger("collecty.plugins.%s" % self
.name
)
121 # Run some custom initialization.
124 self
.log
.debug(_("Successfully initialized %s") % self
.__class
__.__name
__)
129 Returns the name of the sub directory in which all RRD files
130 for this plugin should be stored in.
136 def init(self
, **kwargs
):
138 Do some custom initialization stuff here.
144 Gathers the statistical data, this plugin collects.
146 time_start
= time
.time()
148 # Run through all objects of this plugin and call the collect method.
149 for object in self
.objects
:
152 result
= object.collect()
154 # Catch any unhandled exceptions
155 except Exception as e
:
156 self
.log
.warning(_("Unhandled exception in %s.collect()") % object, exc_info
=True)
160 self
.log
.warning(_("Received empty result: %s") % object)
163 # Add the object to the write queue so that the data is written
164 # to the databases later.
165 result
= self
.collecty
.write_queue
.submit(object, result
)
167 self
.log
.debug(_("Collected %s: %s") % (object, result
))
169 # Returns the time this function took to complete.
170 delay
= time
.time() - time_start
172 # Log some warning when a collect method takes too long to return some data
174 self
.log
.warning(_("A worker thread was stalled for %.4fs") % delay
)
176 self
.log
.debug(_("Collection finished in %.2fms") % (delay
* 1000))
178 def get_object(self
, id):
179 for object in self
.objects
:
180 if not object.id == id:
185 def get_template(self
, template_name
, object_id
, locale
=None, timezone
=None):
186 for template
in self
.templates
:
187 if not template
.name
== template_name
:
190 return template(self
, object_id
, locale
=locale
, timezone
=timezone
)
192 def generate_graph(self
, template_name
, object_id
="default",
193 timezone
=None, locale
=None, **kwargs
):
194 template
= self
.get_template(template_name
, object_id
=object_id
,
195 timezone
=timezone
, locale
=locale
)
197 raise RuntimeError("Could not find template %s" % template_name
)
199 time_start
= time
.time()
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 # Default dimensions for this graph
532 height
= GRAPH_DEFAULT_HEIGHT
533 width
= GRAPH_DEFAULT_WIDTH
535 def __init__(self
, plugin
, object_id
, locale
=None, timezone
=None):
538 # Save localisation parameters
539 self
.locale
= locales
.get(locale
)
540 self
.timezone
= timezone
542 # Get all required RRD objects
543 self
.object_id
= object_id
545 # Get the main object
546 self
.objects
= self
.get_objects(self
.object_id
)
550 return "<%s>" % self
.__class
__.__name
__
554 return self
.plugin
.collecty
558 return self
.plugin
.log
563 Shortcut to the main object
565 if len(self
.objects
) == 1:
566 return self
.objects
[0]
568 def _make_command_line(self
, interval
, format
=DEFAULT_IMAGE_FORMAT
,
569 width
=None, height
=None, with_title
=True, thumbnail
=False):
570 args
= [e
for e
in GRAPH_DEFAULT_ARGUMENTS
]
572 # Set the default dimensions
573 default_height
, default_width
= GRAPH_DEFAULT_HEIGHT
, GRAPH_DEFAULT_WIDTH
575 # A thumbnail doesn't have a legend and other labels
577 args
.append("--only-graph")
579 default_height
= THUMBNAIL_DEFAULT_HEIGHT
580 default_width
= THUMBNAIL_DEFAULT_WIDTH
583 "--imgformat", format
,
584 "--height", "%s" % (height
or default_height
),
585 "--width", "%s" % (width
or default_width
),
588 args
+= self
.rrd_graph_args
591 if with_title
and self
.graph_title
:
592 args
+= ["--title", self
.graph_title
]
595 if self
.graph_vertical_label
:
596 args
+= ["--vertical-label", self
.graph_vertical_label
]
598 if self
.lower_limit
is not None or self
.upper_limit
is not None:
599 # Force to honour the set limits
600 args
.append("--rigid")
602 if self
.lower_limit
is not None:
603 args
+= ["--lower-limit", self
.lower_limit
]
605 if self
.upper_limit
is not None:
606 args
+= ["--upper-limit", self
.upper_limit
]
609 args
+= ["--start", util
.make_interval(interval
)]
614 use_prefix
= len(self
.objects
) >= 2
617 for object in self
.objects
:
619 args
+= object.make_rrd_defs(object.id)
621 args
+= object.make_rrd_defs()
625 def _add_vdefs(self
, args
):
631 # Search for all DEFs and CDEFs
632 m
= re
.match(DEF_MATCH
, "%s" % arg
)
636 # Add the VDEFs for minimum, maximum, etc. values
638 "VDEF:%s_cur=%s,LAST" % (name
, name
),
639 "VDEF:%s_avg=%s,AVERAGE" % (name
, name
),
640 "VDEF:%s_max=%s,MAXIMUM" % (name
, name
),
641 "VDEF:%s_min=%s,MINIMUM" % (name
, name
),
646 def get_objects(self
, *args
, **kwargs
):
647 object = self
.plugin
.get_object(*args
, **kwargs
)
654 def generate_graph(self
, interval
=None, **kwargs
):
655 assert self
.objects
, "Cannot render graph without any objects"
657 # Make sure that all collected data is in the database
658 # to get a recent graph image
659 for object in self
.objects
:
662 args
= self
._make
_command
_line
(interval
, **kwargs
)
664 self
.log
.info(_("Generating graph %s") % self
)
666 rrd_graph
= self
.rrd_graph
668 # Add DEFs for all objects
669 if not any((e
.startswith("DEF:") for e
in rrd_graph
)):
670 args
+= self
._add
_defs
()
673 args
= self
._add
_vdefs
(args
)
675 # Convert arguments to string
676 args
= [str(e
) for e
in args
]
679 self
.log
.debug(" %s" % arg
)
681 with
Environment(self
.timezone
, self
.locale
.lang
):
682 graph
= rrdtool
.graphv("-", *args
)
685 "image" : graph
.get("image"),
686 "image_height" : graph
.get("image_height"),
687 "image_width" : graph
.get("image_width"),
690 def graph_info(self
):
692 Returns a dictionary with useful information
696 "title" : self
.graph_title
,
697 "object_id" : self
.object_id
or "",
698 "template" : self
.name
,