]>
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 .. import locales
32 from ..constants
import *
35 DEF_MATCH
= r
"C?DEF:([A-Za-z0-9_]+)="
37 class Environment(object):
39 Sets the correct environment for rrdtool to create
40 localised graphs and graphs in the correct timezone.
42 def __init__(self
, timezone
, locale
):
43 # Build the new environment
44 self
.new_environment
= {
45 "TZ" : timezone
or DEFAULT_TIMEZONE
,
48 for k
in ("LANG", "LC_ALL"):
49 self
.new_environment
[k
] = locale
or DEFAULT_LOCALE
52 # Save the current environment
53 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 o
in self
.objects
:
151 now
= datetime
.datetime
.utcnow()
155 result
= self
._format
_result
(result
)
157 self
.log
.warning(_("Unhandled exception in %s.collect()") % o
, exc_info
=True)
161 self
.log
.warning(_("Received empty result: %s") % o
)
164 self
.log
.debug(_("Collected %s: %s") % (o
, result
))
166 # Add the object to the write queue so that the data is written
167 # to the databases later.
168 self
.collecty
.write_queue
.add(o
, now
, 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
)
178 def _format_result(result
):
179 if not isinstance(result
, tuple) and not isinstance(result
, list):
182 # Replace all Nones by UNKNOWN
193 def get_object(self
, id):
194 for object in self
.objects
:
195 if not object.id == id:
200 def get_template(self
, template_name
, object_id
, locale
=None, timezone
=None):
201 for template
in self
.templates
:
202 if not template
.name
== template_name
:
205 return template(self
, object_id
, locale
=locale
, timezone
=timezone
)
207 def generate_graph(self
, template_name
, object_id
="default",
208 timezone
=None, locale
=None, **kwargs
):
209 template
= self
.get_template(template_name
, object_id
=object_id
,
210 timezone
=timezone
, locale
=locale
)
212 raise RuntimeError("Could not find template %s" % template_name
)
214 time_start
= time
.time()
216 graph
= template
.generate_graph(**kwargs
)
218 duration
= time
.time() - time_start
219 self
.log
.debug(_("Generated graph %s in %.1fms") \
220 % (template
, duration
* 1000))
224 def graph_info(self
, template_name
, object_id
="default",
225 timezone
=None, locale
=None, **kwargs
):
226 template
= self
.get_template(template_name
, object_id
=object_id
,
227 timezone
=timezone
, locale
=locale
)
229 raise RuntimeError("Could not find template %s" % template_name
)
231 return template
.graph_info()
233 def last_update(self
, object_id
="default"):
234 object = self
.get_object(object_id
)
236 raise RuntimeError("Could not find object %s" % object_id
)
238 return object.last_update()
241 class Object(object):
242 # The schema of the RRD database.
246 rra_types
= ("AVERAGE", "MIN", "MAX")
253 def __init__(self
, plugin
, *args
, **kwargs
):
256 # Indicates if this object has collected its data
257 self
.collected
= False
259 # Initialise this object
260 self
.init(*args
, **kwargs
)
262 # Create the database file.
266 return "<%s %s>" % (self
.__class
__.__name
__, self
.id)
268 def __lt__(self
, other
):
269 return self
.id < other
.id
273 return self
.plugin
.collecty
277 return self
.plugin
.log
282 Returns a UNIQUE identifier for this object. As this is incorporated
283 into the path of RRD file, it must only contain ASCII characters.
285 raise NotImplementedError
290 The absolute path to the RRD file of this plugin.
292 filename
= self
._normalise
_filename
("%s.rrd" % self
.id)
294 return os
.path
.join(DATABASE_DIR
, self
.plugin
.path
, filename
)
297 def _normalise_filename(filename
):
298 # Convert the filename into ASCII characters only
299 filename
= unicodedata
.normalize("NFKC", filename
)
301 # Replace any spaces by dashes
302 filename
= filename
.replace(" ", "-")
308 def init(self
, *args
, **kwargs
):
310 Do some custom initialization stuff here.
316 Creates an empty RRD file with the desired data structures.
318 # Skip if the file does already exist.
319 if os
.path
.exists(self
.file):
322 dirname
= os
.path
.dirname(self
.file)
323 if not os
.path
.exists(dirname
):
326 # Create argument list.
327 args
= self
.get_rrd_schema()
329 rrdtool
.create(self
.file, *args
)
331 self
.log
.debug(_("Created RRD file %s.") % self
.file)
333 self
.log
.debug(" %s" % arg
)
336 return rrdtool
.info(self
.file)
338 def last_update(self
):
340 Returns a dictionary with the timestamp and
341 data set of the last database update.
344 "dataset" : self
.last_dataset
,
345 "timestamp" : self
.last_updated
,
348 def _last_update(self
):
349 return rrdtool
.lastupdate(self
.file)
352 def last_updated(self
):
354 Returns the timestamp when this database was last updated
356 lu
= self
._last
_update
()
359 return lu
.get("date")
362 def last_dataset(self
):
364 Returns the latest dataset in the database
366 lu
= self
._last
_update
()
373 return self
.plugin
.interval
377 return self
.stepsize
* 2
379 def get_rrd_schema(self
):
381 "--step", "%s" % self
.stepsize
,
383 for line
in self
.rrd_schema
:
384 if line
.startswith("DS:"):
386 (prefix
, name
, type, lower_limit
, upper_limit
) = line
.split(":")
392 "%s" % self
.heartbeat
,
403 for steps
, rows
in self
.rra_timespans
:
404 for type in self
.rra_types
:
405 schema
.append("RRA:%s:%s:%s:%s" % (type, xff
, steps
, rows
))
410 def rrd_schema_names(self
):
413 for line
in self
.rrd_schema
:
414 (prefix
, name
, type, lower_limit
, upper_limit
) = line
.split(":")
419 def make_rrd_defs(self
, prefix
=None):
422 for name
in self
.rrd_schema_names
:
424 p
= "%s_%s" % (prefix
, name
)
429 "DEF:%s=%s:%s:AVERAGE" % (p
, self
.file, name
),
434 def get_stddev(self
, interval
=None):
435 args
= self
.make_rrd_defs()
437 # Add the correct interval
438 args
+= ["--start", util
.make_interval(interval
)]
440 for name
in self
.rrd_schema_names
:
442 "VDEF:%s_stddev=%s,STDEV" % (name
, name
),
443 "PRINT:%s_stddev:%%lf" % name
,
446 x
, y
, vals
= rrdtool
.graph("/dev/null", *args
)
447 return dict(zip(self
.rrd_schema_names
, vals
))
451 raise RuntimeError("This object has already collected its data")
453 self
.collected
= True
454 self
.now
= datetime
.datetime
.utcnow()
457 result
= self
.collect()
461 Will commit the collected data to the database.
463 # Make sure that the RRD database has been created
466 # Write everything to disk that is in the write queue
467 self
.collecty
.write_queue
.commit_file(self
.file)
469 # Convenience functions for plugin authors
471 def read_file(self
, *args
, strip
=True):
473 Reads the content of the given file
475 filename
= os
.path
.join(*args
)
477 with
open(filename
) as f
:
480 # Strip any excess whitespace
482 value
= value
.strip()
486 def read_file_integer(self
, filename
):
488 Reads the content from a file and returns it as an integer
490 value
= self
.read_file(filename
)
497 def read_proc_stat(self
):
499 Reads /proc/stat and returns it as a dictionary
503 with
open("/proc/stat") as f
:
505 # Split the key from the rest of the line
506 key
, line
= line
.split(" ", 1)
508 # Remove any line breaks
509 ret
[key
] = line
.rstrip()
514 class GraphTemplate(object):
515 # A unique name to identify this graph template.
518 # Headline of the graph image
521 # Vertical label of the graph
522 graph_vertical_label
= None
528 # Instructions how to create the graph.
531 # Extra arguments passed to rrdgraph.
534 # Default dimensions for this graph
535 height
= GRAPH_DEFAULT_HEIGHT
536 width
= GRAPH_DEFAULT_WIDTH
538 def __init__(self
, plugin
, object_id
, locale
=None, timezone
=None):
541 # Save localisation parameters
542 self
.locale
= locales
.get(locale
)
543 self
.timezone
= timezone
545 # Get all required RRD objects
546 self
.object_id
= object_id
548 # Get the main object
549 self
.objects
= self
.get_objects(self
.object_id
)
553 return "<%s>" % self
.__class
__.__name
__
557 return self
.plugin
.collecty
561 return self
.plugin
.log
566 Shortcut to the main object
568 if len(self
.objects
) == 1:
569 return self
.objects
[0]
571 def _make_command_line(self
, interval
, format
=DEFAULT_IMAGE_FORMAT
,
572 width
=None, height
=None, with_title
=True, thumbnail
=False):
573 args
= [e
for e
in GRAPH_DEFAULT_ARGUMENTS
]
575 # Set the default dimensions
576 default_height
, default_width
= GRAPH_DEFAULT_HEIGHT
, GRAPH_DEFAULT_WIDTH
578 # A thumbnail doesn't have a legend and other labels
580 args
.append("--only-graph")
582 default_height
= THUMBNAIL_DEFAULT_HEIGHT
583 default_width
= THUMBNAIL_DEFAULT_WIDTH
586 "--imgformat", format
,
587 "--height", "%s" % (height
or default_height
),
588 "--width", "%s" % (width
or default_width
),
591 args
+= self
.rrd_graph_args
594 if with_title
and self
.graph_title
:
595 args
+= ["--title", self
.graph_title
]
598 if self
.graph_vertical_label
:
599 args
+= ["--vertical-label", self
.graph_vertical_label
]
601 if self
.lower_limit
is not None or self
.upper_limit
is not None:
602 # Force to honour the set limits
603 args
.append("--rigid")
605 if self
.lower_limit
is not None:
606 args
+= ["--lower-limit", self
.lower_limit
]
608 if self
.upper_limit
is not None:
609 args
+= ["--upper-limit", self
.upper_limit
]
612 args
+= ["--start", util
.make_interval(interval
)]
617 use_prefix
= len(self
.objects
) >= 2
620 for object in self
.objects
:
622 args
+= object.make_rrd_defs(object.id)
624 args
+= object.make_rrd_defs()
628 def _add_vdefs(self
, args
):
634 # Search for all DEFs and CDEFs
635 m
= re
.match(DEF_MATCH
, "%s" % arg
)
639 # Add the VDEFs for minimum, maximum, etc. values
641 "VDEF:%s_cur=%s,LAST" % (name
, name
),
642 "VDEF:%s_avg=%s,AVERAGE" % (name
, name
),
643 "VDEF:%s_max=%s,MAXIMUM" % (name
, name
),
644 "VDEF:%s_min=%s,MINIMUM" % (name
, name
),
649 def get_objects(self
, *args
, **kwargs
):
650 object = self
.plugin
.get_object(*args
, **kwargs
)
657 def generate_graph(self
, interval
=None, **kwargs
):
658 assert self
.objects
, "Cannot render graph without any objects"
660 # Make sure that all collected data is in the database
661 # to get a recent graph image
662 for object in self
.objects
:
665 args
= self
._make
_command
_line
(interval
, **kwargs
)
667 self
.log
.info(_("Generating graph %s") % self
)
669 rrd_graph
= self
.rrd_graph
671 # Add DEFs for all objects
672 if not any((e
.startswith("DEF:") for e
in rrd_graph
)):
673 args
+= self
._add
_defs
()
676 args
= self
._add
_vdefs
(args
)
678 # Convert arguments to string
679 args
= [str(e
) for e
in args
]
682 self
.log
.debug(" %s" % arg
)
684 with
Environment(self
.timezone
, self
.locale
.lang
):
685 graph
= rrdtool
.graphv("-", *args
)
688 "image" : graph
.get("image"),
689 "image_height" : graph
.get("image_height"),
690 "image_width" : graph
.get("image_width"),
693 def graph_info(self
):
695 Returns a dictionary with useful information
699 "title" : self
.graph_title
,
700 "object_id" : self
.object_id
or "",
701 "template" : self
.name
,