]>
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 ###############################################################################
33 from .. import locales
35 from ..constants
import *
38 DEF_MATCH
= re
.compile(r
"C?DEF:([A-Za-z0-9_]+)=")
41 def __init__(self
, timeout
, heartbeat
=1):
42 self
.timeout
= timeout
43 self
.heartbeat
= heartbeat
49 def reset(self
, delay
=0):
51 self
.start
= time
.time()
55 # Has this timer been killed?
60 return time
.time() - self
.start
- self
.delay
66 while self
.elapsed
< self
.timeout
and not self
.killed
:
67 time
.sleep(self
.heartbeat
)
69 return self
.elapsed
> self
.timeout
72 class Environment(object):
74 Sets the correct environment for rrdtool to create
75 localised graphs and graphs in the correct timezone.
77 def __init__(self
, timezone
, locale
):
78 # Build the new environment
79 self
.new_environment
= {
80 "TZ" : timezone
or DEFAULT_TIMEZONE
,
83 for k
in ("LANG", "LC_ALL"):
84 self
.new_environment
[k
] = locale
or DEFAULT_LOCALE
87 # Save the current environment
88 self
.old_environment
= {}
89 for k
in self
.new_environment
:
90 self
.old_environment
[k
] = os
.environ
.get(k
, None)
93 os
.environ
.update(self
.new_environment
)
95 def __exit__(self
, type, value
, traceback
):
96 # Roll back to the previous environment
97 for k
, v
in self
.old_environment
.items():
107 class PluginRegistration(type):
110 def __init__(plugin
, name
, bases
, dict):
111 type.__init
__(plugin
, name
, bases
, dict)
113 # The main class from which is inherited is not registered
118 if not all((plugin
.name
, plugin
.description
)):
119 raise RuntimeError(_("Plugin is not properly configured: %s") % plugin
)
121 PluginRegistration
.plugins
[plugin
.name
] = plugin
126 Returns a list with all automatically registered plugins.
128 return PluginRegistration
.plugins
.values()
130 class Plugin(object, metaclass
=PluginRegistration
):
131 # The name of this plugin.
134 # A description for this plugin.
137 # Templates which can be used to generate a graph out of
138 # the data from this data source.
141 # The default interval for all plugins
144 def __init__(self
, collecty
, **kwargs
):
145 self
.collecty
= collecty
147 # Check if this plugin was configured correctly.
148 assert self
.name
, "Name of the plugin is not set: %s" % self
.name
149 assert self
.description
, "Description of the plugin is not set: %s" % self
.description
151 # Initialize the logger.
152 self
.log
= logging
.getLogger("collecty.plugins.%s" % self
.name
)
156 # Run some custom initialization.
159 self
.log
.debug(_("Successfully initialized %s") % self
.__class
__.__name
__)
164 Returns the name of the sub directory in which all RRD files
165 for this plugin should be stored in.
171 def init(self
, **kwargs
):
173 Do some custom initialization stuff here.
179 Gathers the statistical data, this plugin collects.
181 time_start
= time
.time()
183 # Run through all objects of this plugin and call the collect method.
184 for o
in self
.objects
:
185 now
= datetime
.datetime
.utcnow()
189 result
= self
._format
_result
(result
)
191 self
.log
.warning(_("Unhandled exception in %s.collect()") % o
, exc_info
=True)
195 self
.log
.warning(_("Received empty result: %s") % o
)
198 self
.log
.debug(_("Collected %s: %s") % (o
, result
))
200 # Add the object to the write queue so that the data is written
201 # to the databases later.
202 self
.collecty
.write_queue
.add(o
, now
, result
)
204 # Returns the time this function took to complete.
205 delay
= time
.time() - time_start
207 # Log some warning when a collect method takes too long to return some data
209 self
.log
.warning(_("A worker thread was stalled for %.4fs") % delay
)
212 def _format_result(result
):
213 if not isinstance(result
, tuple) and not isinstance(result
, list):
216 # Replace all Nones by UNKNOWN
227 def get_object(self
, id):
228 for object in self
.objects
:
229 if not object.id == id:
234 def get_template(self
, template_name
, object_id
, locale
=None, timezone
=None):
235 for template
in self
.templates
:
236 if not template
.name
== template_name
:
239 return template(self
, object_id
, locale
=locale
, timezone
=timezone
)
241 def generate_graph(self
, template_name
, object_id
="default",
242 timezone
=None, locale
=None, **kwargs
):
243 template
= self
.get_template(template_name
, object_id
=object_id
,
244 timezone
=timezone
, locale
=locale
)
246 raise RuntimeError("Could not find template %s" % template_name
)
248 time_start
= time
.time()
250 graph
= template
.generate_graph(**kwargs
)
252 duration
= time
.time() - time_start
253 self
.log
.debug(_("Generated graph %s in %.1fms") \
254 % (template
, duration
* 1000))
258 def graph_info(self
, template_name
, object_id
="default",
259 timezone
=None, locale
=None, **kwargs
):
260 template
= self
.get_template(template_name
, object_id
=object_id
,
261 timezone
=timezone
, locale
=locale
)
263 raise RuntimeError("Could not find template %s" % template_name
)
265 return template
.graph_info()
267 def last_update(self
, object_id
="default"):
268 object = self
.get_object(object_id
)
270 raise RuntimeError("Could not find object %s" % object_id
)
272 return object.last_update()
275 class Object(object):
276 # The schema of the RRD database.
280 rra_types
= ("AVERAGE", "MIN", "MAX")
287 def __init__(self
, plugin
, *args
, **kwargs
):
290 # Indicates if this object has collected its data
291 self
.collected
= False
293 # Initialise this object
294 self
.init(*args
, **kwargs
)
296 # Create the database file.
300 return "<%s>" % self
.__class
__.__name
__
302 def __lt__(self
, other
):
303 return self
.id < other
.id
307 return self
.plugin
.collecty
311 return self
.plugin
.log
316 Returns a UNIQUE identifier for this object. As this is incorporated
317 into the path of RRD file, it must only contain ASCII characters.
319 raise NotImplementedError
324 The absolute path to the RRD file of this plugin.
326 filename
= self
._normalise
_filename
("%s.rrd" % self
.id)
328 return os
.path
.join(DATABASE_DIR
, self
.plugin
.path
, filename
)
331 def _normalise_filename(filename
):
332 # Convert the filename into ASCII characters only
333 filename
= unicodedata
.normalize("NFKC", filename
)
335 # Replace any spaces by dashes
336 filename
= filename
.replace(" ", "-")
342 def init(self
, *args
, **kwargs
):
344 Do some custom initialization stuff here.
350 Creates an empty RRD file with the desired data structures.
352 # Skip if the file does already exist.
353 if os
.path
.exists(self
.file):
356 dirname
= os
.path
.dirname(self
.file)
357 if not os
.path
.exists(dirname
):
360 # Create argument list.
361 args
= self
.get_rrd_schema()
363 rrdtool
.create(self
.file, *args
)
365 self
.log
.debug(_("Created RRD file %s.") % self
.file)
367 self
.log
.debug(" %s" % arg
)
370 return rrdtool
.info(self
.file)
372 def last_update(self
):
374 Returns a dictionary with the timestamp and
375 data set of the last database update.
378 "dataset" : self
.last_dataset
,
379 "timestamp" : self
.last_updated
,
382 def _last_update(self
):
383 return rrdtool
.lastupdate(self
.file)
386 def last_updated(self
):
388 Returns the timestamp when this database was last updated
390 lu
= self
._last
_update
()
393 return lu
.get("date")
396 def last_dataset(self
):
398 Returns the latest dataset in the database
400 lu
= self
._last
_update
()
407 return self
.plugin
.interval
411 return self
.stepsize
* 2
413 def get_rrd_schema(self
):
415 "--step", "%s" % self
.stepsize
,
417 for line
in self
.rrd_schema
:
418 if line
.startswith("DS:"):
420 (prefix
, name
, type, lower_limit
, upper_limit
) = line
.split(":")
426 "%s" % self
.heartbeat
,
437 for steps
, rows
in self
.rra_timespans
:
438 for type in self
.rra_types
:
439 schema
.append("RRA:%s:%s:%s:%s" % (type, xff
, steps
, rows
))
444 def rrd_schema_names(self
):
447 for line
in self
.rrd_schema
:
448 (prefix
, name
, type, lower_limit
, upper_limit
) = line
.split(":")
453 def make_rrd_defs(self
, prefix
=None):
456 for name
in self
.rrd_schema_names
:
458 p
= "%s_%s" % (prefix
, name
)
463 "DEF:%s=%s:%s:AVERAGE" % (p
, self
.file, name
),
468 def get_stddev(self
, interval
=None):
469 args
= self
.make_rrd_defs()
471 # Add the correct interval
472 args
+= ["--start", util
.make_interval(interval
)]
474 for name
in self
.rrd_schema_names
:
476 "VDEF:%s_stddev=%s,STDEV" % (name
, name
),
477 "PRINT:%s_stddev:%%lf" % name
,
480 x
, y
, vals
= rrdtool
.graph("/dev/null", *args
)
481 return dict(zip(self
.rrd_schema_names
, vals
))
485 raise RuntimeError("This object has already collected its data")
487 self
.collected
= True
488 self
.now
= datetime
.datetime
.utcnow()
491 result
= self
.collect()
495 Will commit the collected data to the database.
497 # Make sure that the RRD database has been created
500 # Write everything to disk that is in the write queue
501 self
.collecty
.write_queue
.commit_file(self
.file)
504 class GraphTemplate(object):
505 # A unique name to identify this graph template.
508 # Headline of the graph image
511 # Vertical label of the graph
512 graph_vertical_label
= None
518 # Instructions how to create the graph.
521 # Extra arguments passed to rrdgraph.
524 # Default dimensions for this graph
525 height
= GRAPH_DEFAULT_HEIGHT
526 width
= GRAPH_DEFAULT_WIDTH
528 def __init__(self
, plugin
, object_id
, locale
=None, timezone
=None):
531 # Save localisation parameters
532 self
.locale
= locales
.get(locale
)
533 self
.timezone
= timezone
535 # Get all required RRD objects
536 self
.object_id
= object_id
538 # Get the main object
539 self
.objects
= self
.get_objects(self
.object_id
)
543 return "<%s>" % self
.__class
__.__name
__
547 return self
.plugin
.collecty
551 return self
.plugin
.log
556 Shortcut to the main object
558 if len(self
.objects
) == 1:
559 return self
.objects
[0]
561 def _make_command_line(self
, interval
, format
=DEFAULT_IMAGE_FORMAT
,
562 width
=None, height
=None, with_title
=True, thumbnail
=False):
563 args
= [e
for e
in GRAPH_DEFAULT_ARGUMENTS
]
565 # Set the default dimensions
566 default_height
, default_width
= GRAPH_DEFAULT_HEIGHT
, GRAPH_DEFAULT_WIDTH
568 # A thumbnail doesn't have a legend and other labels
570 args
.append("--only-graph")
572 default_height
= THUMBNAIL_DEFAULT_HEIGHT
573 default_width
= THUMBNAIL_DEFAULT_WIDTH
576 "--imgformat", format
,
577 "--height", "%s" % (height
or default_height
),
578 "--width", "%s" % (width
or default_width
),
581 args
+= self
.rrd_graph_args
584 if with_title
and self
.graph_title
:
585 args
+= ["--title", self
.graph_title
]
588 if self
.graph_vertical_label
:
589 args
+= ["--vertical-label", self
.graph_vertical_label
]
591 if self
.lower_limit
is not None or self
.upper_limit
is not None:
592 # Force to honour the set limits
593 args
.append("--rigid")
595 if self
.lower_limit
is not None:
596 args
+= ["--lower-limit", self
.lower_limit
]
598 if self
.upper_limit
is not None:
599 args
+= ["--upper-limit", self
.upper_limit
]
602 args
+= ["--start", util
.make_interval(interval
)]
607 use_prefix
= len(self
.objects
) >= 2
610 for object in self
.objects
:
612 args
+= object.make_rrd_defs(object.id)
614 args
+= object.make_rrd_defs()
618 def _add_vdefs(self
, args
):
624 # Search for all DEFs and CDEFs
625 m
= re
.match(DEF_MATCH
, "%s" % arg
)
629 # Add the VDEFs for minimum, maximum, etc. values
631 "VDEF:%s_cur=%s,LAST" % (name
, name
),
632 "VDEF:%s_avg=%s,AVERAGE" % (name
, name
),
633 "VDEF:%s_max=%s,MAXIMUM" % (name
, name
),
634 "VDEF:%s_min=%s,MINIMUM" % (name
, name
),
639 def get_objects(self
, *args
, **kwargs
):
640 object = self
.plugin
.get_object(*args
, **kwargs
)
647 def generate_graph(self
, interval
=None, **kwargs
):
648 assert self
.objects
, "Cannot render graph without any objects"
650 # Make sure that all collected data is in the database
651 # to get a recent graph image
652 for object in self
.objects
:
655 args
= self
._make
_command
_line
(interval
, **kwargs
)
657 self
.log
.info(_("Generating graph %s") % self
)
659 rrd_graph
= self
.rrd_graph
661 # Add DEFs for all objects
662 if not any((e
.startswith("DEF:") for e
in rrd_graph
)):
663 args
+= self
._add
_defs
()
666 args
= self
._add
_vdefs
(args
)
668 # Convert arguments to string
669 args
= [str(e
) for e
in args
]
672 self
.log
.debug(" %s" % arg
)
674 with
Environment(self
.timezone
, self
.locale
.lang
):
675 graph
= rrdtool
.graphv("-", *args
)
678 "image" : graph
.get("image"),
679 "image_height" : graph
.get("image_height"),
680 "image_width" : graph
.get("image_width"),
683 def graph_info(self
):
685 Returns a dictionary with useful information
689 "title" : self
.graph_title
,
690 "object_id" : self
.object_id
or "",
691 "template" : self
.name
,