]>
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 ###############################################################################
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 "UTC",
47 for k
in ("LANG", "LC_ALL"):
48 self
.new_environment
[k
] = locale
or "en_US.utf-8"
51 # Save the current environment
52 self
.old_environment
= {}
54 for k
in self
.new_environment
:
55 self
.old_environment
[k
] = os
.environ
.get(k
, None)
58 os
.environ
.update(self
.new_environment
)
60 def __exit__(self
, type, value
, traceback
):
61 # Roll back to the previous environment
62 for k
, v
in self
.old_environment
.items():
72 class PluginRegistration(type):
75 def __init__(plugin
, name
, bases
, dict):
76 type.__init
__(plugin
, name
, bases
, dict)
78 # The main class from which is inherited is not registered
83 if not all((plugin
.name
, plugin
.description
)):
84 raise RuntimeError(_("Plugin is not properly configured: %s") % plugin
)
86 PluginRegistration
.plugins
[plugin
.name
] = plugin
91 Returns a list with all automatically registered plugins.
93 return PluginRegistration
.plugins
.values()
95 class Plugin(object, metaclass
=PluginRegistration
):
96 # The name of this plugin.
99 # A description for this plugin.
102 # Templates which can be used to generate a graph out of
103 # the data from this data source.
106 # The default interval for all plugins
112 def __init__(self
, collecty
, **kwargs
):
113 self
.collecty
= collecty
115 # Check if this plugin was configured correctly.
116 assert self
.name
, "Name of the plugin is not set: %s" % self
.name
117 assert self
.description
, "Description of the plugin is not set: %s" % self
.description
119 # Initialize the logger.
120 self
.log
= logging
.getLogger("collecty.plugins.%s" % self
.name
)
122 # Run some custom initialization.
125 self
.log
.debug(_("Successfully initialized %s") % self
.__class
__.__name
__)
130 Returns the name of the sub directory in which all RRD files
131 for this plugin should be stored in.
137 def init(self
, **kwargs
):
139 Do some custom initialization stuff here.
145 Gathers the statistical data, this plugin collects.
147 time_start
= time
.time()
149 # Run through all objects of this plugin and call the collect method.
150 for object in self
.objects
:
153 result
= object.collect()
155 # Catch any unhandled exceptions
156 except Exception as e
:
157 self
.log
.warning(_("Unhandled exception in %s.collect()") % object, exc_info
=True)
161 self
.log
.warning(_("Received empty result: %s") % object)
164 # Add the object to the write queue so that the data is written
165 # to the databases later.
166 result
= self
.collecty
.write_queue
.submit(object, result
)
168 self
.log
.debug(_("Collected %s: %s") % (object, result
))
170 # Returns the time this function took to complete.
171 delay
= time
.time() - time_start
173 # Log some warning when a collect method takes too long to return some data
175 self
.log
.warning(_("A worker thread was stalled for %.4fs") % delay
)
177 self
.log
.debug(_("Collection finished in %.2fms") % (delay
* 1000))
179 def get_object(self
, id):
180 for object in self
.objects
:
181 if not object.id == id:
186 def get_template(self
, template_name
, object_id
, locale
=None, timezone
=None):
187 for template
in self
.templates
:
188 if not template
.name
== template_name
:
191 return template(self
, object_id
, locale
=locale
, timezone
=timezone
)
193 def generate_graph(self
, template_name
, object_id
="default",
194 timezone
=None, locale
=None, **kwargs
):
195 template
= self
.get_template(template_name
, object_id
=object_id
,
196 timezone
=timezone
, locale
=locale
)
198 raise RuntimeError("Could not find template %s" % template_name
)
200 time_start
= time
.time()
202 graph
= template
.generate_graph(**kwargs
)
204 duration
= time
.time() - time_start
205 self
.log
.debug(_("Generated graph %s in %.1fms") \
206 % (template
, duration
* 1000))
210 def graph_info(self
, template_name
, object_id
="default",
211 timezone
=None, locale
=None, **kwargs
):
212 template
= self
.get_template(template_name
, object_id
=object_id
,
213 timezone
=timezone
, locale
=locale
)
215 raise RuntimeError("Could not find template %s" % template_name
)
217 return template
.graph_info()
219 def last_update(self
, object_id
="default"):
220 object = self
.get_object(object_id
)
222 raise RuntimeError("Could not find object %s" % object_id
)
224 return object.last_update()
227 class Object(object):
228 # The schema of the RRD database.
232 rra_types
= ("AVERAGE", "MIN", "MAX")
239 def __init__(self
, plugin
, *args
, **kwargs
):
242 # Initialise this object
243 self
.init(*args
, **kwargs
)
245 # Create the database file.
249 return "<%s %s>" % (self
.__class
__.__name
__, self
.id)
251 def __lt__(self
, other
):
252 return self
.id < other
.id
256 return self
.plugin
.collecty
260 return self
.plugin
.log
265 Returns a UNIQUE identifier for this object. As this is incorporated
266 into the path of RRD file, it must only contain ASCII characters.
268 raise NotImplementedError
273 The absolute path to the RRD file of this plugin.
275 filename
= self
._normalise
_filename
("%s.rrd" % self
.id)
277 return os
.path
.join(DATABASE_DIR
, self
.plugin
.path
, filename
)
280 def _normalise_filename(filename
):
281 # Convert the filename into ASCII characters only
282 filename
= unicodedata
.normalize("NFKC", filename
)
284 # Replace any spaces by dashes
285 filename
= filename
.replace(" ", "-")
291 def init(self
, *args
, **kwargs
):
293 Do some custom initialization stuff here.
299 Creates an empty RRD file with the desired data structures.
301 # Skip if the file does already exist.
302 if os
.path
.exists(self
.file):
305 dirname
= os
.path
.dirname(self
.file)
306 if not os
.path
.exists(dirname
):
309 # Create argument list.
310 args
= self
.get_rrd_schema()
312 rrdtool
.create(self
.file, *args
)
314 self
.log
.debug(_("Created RRD file %s.") % self
.file)
316 self
.log
.debug(" %s" % arg
)
319 return rrdtool
.info(self
.file)
321 def last_update(self
):
323 Returns a dictionary with the timestamp and
324 data set of the last database update.
327 "dataset" : self
.last_dataset
,
328 "timestamp" : self
.last_updated
,
331 def _last_update(self
):
332 return rrdtool
.lastupdate(self
.file)
335 def last_updated(self
):
337 Returns the timestamp when this database was last updated
339 lu
= self
._last
_update
()
342 return lu
.get("date")
345 def last_dataset(self
):
347 Returns the latest dataset in the database
349 lu
= self
._last
_update
()
356 return self
.plugin
.interval
360 return self
.stepsize
* 2
362 def get_rrd_schema(self
):
364 "--step", "%s" % self
.stepsize
,
366 for line
in self
.rrd_schema
:
367 if line
.startswith("DS:"):
369 (prefix
, name
, type, lower_limit
, upper_limit
) = line
.split(":")
375 "%s" % self
.heartbeat
,
386 for steps
, rows
in self
.rra_timespans
:
387 for type in self
.rra_types
:
388 schema
.append("RRA:%s:%s:%s:%s" % (type, xff
, steps
, rows
))
393 def rrd_schema_names(self
):
396 for line
in self
.rrd_schema
:
397 (prefix
, name
, type, lower_limit
, upper_limit
) = line
.split(":")
402 def make_rrd_defs(self
, prefix
=None):
405 for name
in self
.rrd_schema_names
:
407 p
= "%s_%s" % (prefix
, name
)
412 "DEF:%s=%s:%s:AVERAGE" % (p
, self
.file, name
),
417 def get_stddev(self
, interval
=None):
418 args
= self
.make_rrd_defs()
420 # Add the correct interval
421 args
+= ["--start", util
.make_interval(interval
)]
423 for name
in self
.rrd_schema_names
:
425 "VDEF:%s_stddev=%s,STDEV" % (name
, name
),
426 "PRINT:%s_stddev:%%lf" % name
,
429 x
, y
, vals
= rrdtool
.graph("/dev/null", *args
)
430 return dict(zip(self
.rrd_schema_names
, vals
))
434 Will commit the collected data to the database.
436 # Make sure that the RRD database has been created
439 # Write everything to disk that is in the write queue
440 self
.collecty
.write_queue
.commit_file(self
.file)
442 # Convenience functions for plugin authors
444 def read_file(self
, *args
, strip
=True):
446 Reads the content of the given file
448 filename
= os
.path
.join(*args
)
450 with
open(filename
) as f
:
453 # Strip any excess whitespace
455 value
= value
.strip()
459 def read_file_integer(self
, filename
):
461 Reads the content from a file and returns it as an integer
463 value
= self
.read_file(filename
)
470 def read_proc_stat(self
):
472 Reads /proc/stat and returns it as a dictionary
476 with
open("/proc/stat") as f
:
478 # Split the key from the rest of the line
479 key
, line
= line
.split(" ", 1)
481 # Remove any line breaks
482 ret
[key
] = line
.rstrip()
486 def read_proc_meminfo(self
):
489 with
open("/proc/meminfo") as f
:
491 # Split the key from the rest of the line
492 key
, line
= line
.split(":", 1)
494 # Remove any whitespace
497 # Remove any trailing kB
498 if line
.endswith(" kB"):
501 # Try to convert to integer
504 except (TypeError, ValueError):
512 class GraphTemplate(object):
513 # A unique name to identify this graph template.
516 # Headline of the graph image
519 # Vertical label of the graph
520 graph_vertical_label
= None
526 # Instructions how to create the graph.
529 # Extra arguments passed to rrdgraph.
532 def __init__(self
, plugin
, object_id
, locale
=None, timezone
=None):
535 # Save localisation parameters
536 self
.locale
= locales
.get(locale
)
537 self
.timezone
= timezone
539 # Get all required RRD objects
540 self
.object_id
= object_id
542 # Get the main object
543 self
.objects
= self
.get_objects(self
.object_id
)
547 return "<%s>" % self
.__class
__.__name
__
551 return self
.plugin
.collecty
555 return self
.plugin
.log
560 Shortcut to the main object
562 if len(self
.objects
) == 1:
563 return self
.objects
[0]
565 def _make_command_line(self
, interval
, format
=DEFAULT_IMAGE_FORMAT
,
566 width
=None, height
=None, with_title
=True, thumbnail
=False):
568 # Change the background colour
569 "--color", "BACK#FFFFFFFF",
571 # Disable the border around the image
574 # Let's width and height define the size of the entire image
577 # Gives the curves a more organic look
583 # Brand all generated graphs
584 "--watermark", _("Created by collecty"),
587 # Set the default dimensions
588 default_height
, default_width
= 960, 480
590 # A thumbnail doesn't have a legend and other labels
592 args
.append("--only-graph")
594 default_height
, default_width
= 80, 20
597 "--imgformat", format
,
598 "--height", "%s" % (height
or default_height
),
599 "--width", "%s" % (width
or default_width
),
602 args
+= self
.rrd_graph_args
605 if with_title
and self
.graph_title
:
606 args
+= ["--title", self
.graph_title
]
609 if self
.graph_vertical_label
:
610 args
+= ["--vertical-label", self
.graph_vertical_label
]
612 if self
.lower_limit
is not None or self
.upper_limit
is not None:
613 # Force to honour the set limits
614 args
.append("--rigid")
616 if self
.lower_limit
is not None:
617 args
+= ["--lower-limit", self
.lower_limit
]
619 if self
.upper_limit
is not None:
620 args
+= ["--upper-limit", self
.upper_limit
]
623 args
+= ["--start", util
.make_interval(interval
)]
628 use_prefix
= len(self
.objects
) >= 2
631 for object in self
.objects
:
633 args
+= object.make_rrd_defs(object.id)
635 args
+= object.make_rrd_defs()
639 def _add_vdefs(self
, args
):
645 # Search for all DEFs and CDEFs
646 m
= re
.match(DEF_MATCH
, "%s" % arg
)
650 # Add the VDEFs for minimum, maximum, etc. values
652 "VDEF:%s_cur=%s,LAST" % (name
, name
),
653 "VDEF:%s_avg=%s,AVERAGE" % (name
, name
),
654 "VDEF:%s_max=%s,MAXIMUM" % (name
, name
),
655 "VDEF:%s_min=%s,MINIMUM" % (name
, name
),
660 def get_objects(self
, *args
, **kwargs
):
661 object = self
.plugin
.get_object(*args
, **kwargs
)
668 def generate_graph(self
, interval
=None, **kwargs
):
669 assert self
.objects
, "Cannot render graph without any objects"
671 # Make sure that all collected data is in the database
672 # to get a recent graph image
673 for object in self
.objects
:
676 args
= self
._make
_command
_line
(interval
, **kwargs
)
678 self
.log
.info(_("Generating graph %s") % self
)
680 rrd_graph
= self
.rrd_graph
682 # Add DEFs for all objects
683 if not any((e
.startswith("DEF:") for e
in rrd_graph
)):
684 args
+= self
._add
_defs
()
687 args
= self
._add
_vdefs
(args
)
689 # Convert arguments to string
690 args
= [str(e
) for e
in args
]
693 self
.log
.debug(" %s" % arg
)
695 with
Environment(self
.timezone
, self
.locale
.lang
):
696 graph
= rrdtool
.graphv("-", *args
)
699 "image" : graph
.get("image"),
700 "image_height" : graph
.get("image_height"),
701 "image_width" : graph
.get("image_width"),
704 def graph_info(self
):
706 Returns a dictionary with useful information
710 "title" : self
.graph_title
,
711 "object_id" : self
.object_id
or "",
712 "template" : self
.name
,