]>
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_]+)=")
40 class Environment(object):
42 Sets the correct environment for rrdtool to create
43 localised graphs and graphs in the correct timezone.
45 def __init__(self
, timezone
, locale
):
46 # Build the new environment
47 self
.new_environment
= {
48 "TZ" : timezone
or DEFAULT_TIMEZONE
,
51 for k
in ("LANG", "LC_ALL"):
52 self
.new_environment
[k
] = locale
or DEFAULT_LOCALE
55 # Save the current environment
56 self
.old_environment
= {}
57 for k
in self
.new_environment
:
58 self
.old_environment
[k
] = os
.environ
.get(k
, None)
61 os
.environ
.update(self
.new_environment
)
63 def __exit__(self
, type, value
, traceback
):
64 # Roll back to the previous environment
65 for k
, v
in self
.old_environment
.items():
75 class PluginRegistration(type):
78 def __init__(plugin
, name
, bases
, dict):
79 type.__init
__(plugin
, name
, bases
, dict)
81 # The main class from which is inherited is not registered
86 if not all((plugin
.name
, plugin
.description
)):
87 raise RuntimeError(_("Plugin is not properly configured: %s") % plugin
)
89 PluginRegistration
.plugins
[plugin
.name
] = plugin
94 Returns a list with all automatically registered plugins.
96 return PluginRegistration
.plugins
.values()
98 class Plugin(object, metaclass
=PluginRegistration
):
99 # The name of this plugin.
102 # A description for this plugin.
105 # Templates which can be used to generate a graph out of
106 # the data from this data source.
109 # The default interval for all plugins
115 def __init__(self
, collecty
, **kwargs
):
116 self
.collecty
= collecty
118 # Check if this plugin was configured correctly.
119 assert self
.name
, "Name of the plugin is not set: %s" % self
.name
120 assert self
.description
, "Description of the plugin is not set: %s" % self
.description
122 # Initialize the logger.
123 self
.log
= logging
.getLogger("collecty.plugins.%s" % self
.name
)
127 # Run some custom initialization.
130 self
.log
.debug(_("Successfully initialized %s") % self
.__class
__.__name
__)
135 Returns the name of the sub directory in which all RRD files
136 for this plugin should be stored in.
142 def init(self
, **kwargs
):
144 Do some custom initialization stuff here.
150 Gathers the statistical data, this plugin collects.
152 time_start
= time
.time()
154 # Run through all objects of this plugin and call the collect method.
155 for o
in self
.objects
:
156 now
= datetime
.datetime
.utcnow()
160 result
= self
._format
_result
(result
)
162 self
.log
.warning(_("Unhandled exception in %s.collect()") % o
, exc_info
=True)
166 self
.log
.warning(_("Received empty result: %s") % o
)
169 self
.log
.debug(_("Collected %s: %s") % (o
, result
))
171 # Add the object to the write queue so that the data is written
172 # to the databases later.
173 self
.collecty
.write_queue
.add(o
, now
, result
)
175 # Returns the time this function took to complete.
176 delay
= time
.time() - time_start
178 # Log some warning when a collect method takes too long to return some data
180 self
.log
.warning(_("A worker thread was stalled for %.4fs") % delay
)
183 def _format_result(result
):
184 if not isinstance(result
, tuple) and not isinstance(result
, list):
187 # Replace all Nones by UNKNOWN
198 def get_object(self
, id):
199 for object in self
.objects
:
200 if not object.id == id:
205 def get_template(self
, template_name
, object_id
, locale
=None, timezone
=None):
206 for template
in self
.templates
:
207 if not template
.name
== template_name
:
210 return template(self
, object_id
, locale
=locale
, timezone
=timezone
)
212 def generate_graph(self
, template_name
, object_id
="default",
213 timezone
=None, locale
=None, **kwargs
):
214 template
= self
.get_template(template_name
, object_id
=object_id
,
215 timezone
=timezone
, locale
=locale
)
217 raise RuntimeError("Could not find template %s" % template_name
)
219 time_start
= time
.time()
221 graph
= template
.generate_graph(**kwargs
)
223 duration
= time
.time() - time_start
224 self
.log
.debug(_("Generated graph %s in %.1fms") \
225 % (template
, duration
* 1000))
229 def graph_info(self
, template_name
, object_id
="default",
230 timezone
=None, locale
=None, **kwargs
):
231 template
= self
.get_template(template_name
, object_id
=object_id
,
232 timezone
=timezone
, locale
=locale
)
234 raise RuntimeError("Could not find template %s" % template_name
)
236 return template
.graph_info()
238 def last_update(self
, object_id
="default"):
239 object = self
.get_object(object_id
)
241 raise RuntimeError("Could not find object %s" % object_id
)
243 return object.last_update()
246 class Object(object):
247 # The schema of the RRD database.
251 rra_types
= ("AVERAGE", "MIN", "MAX")
258 def __init__(self
, plugin
, *args
, **kwargs
):
261 # Indicates if this object has collected its data
262 self
.collected
= False
264 # Initialise this object
265 self
.init(*args
, **kwargs
)
267 # Create the database file.
271 return "<%s>" % self
.__class
__.__name
__
273 def __lt__(self
, other
):
274 return self
.id < other
.id
278 return self
.plugin
.collecty
282 return self
.plugin
.log
287 Returns a UNIQUE identifier for this object. As this is incorporated
288 into the path of RRD file, it must only contain ASCII characters.
290 raise NotImplementedError
295 The absolute path to the RRD file of this plugin.
297 filename
= self
._normalise
_filename
("%s.rrd" % self
.id)
299 return os
.path
.join(DATABASE_DIR
, self
.plugin
.path
, filename
)
302 def _normalise_filename(filename
):
303 # Convert the filename into ASCII characters only
304 filename
= unicodedata
.normalize("NFKC", filename
)
306 # Replace any spaces by dashes
307 filename
= filename
.replace(" ", "-")
313 def init(self
, *args
, **kwargs
):
315 Do some custom initialization stuff here.
321 Creates an empty RRD file with the desired data structures.
323 # Skip if the file does already exist.
324 if os
.path
.exists(self
.file):
327 dirname
= os
.path
.dirname(self
.file)
328 if not os
.path
.exists(dirname
):
331 # Create argument list.
332 args
= self
.get_rrd_schema()
334 rrdtool
.create(self
.file, *args
)
336 self
.log
.debug(_("Created RRD file %s.") % self
.file)
338 self
.log
.debug(" %s" % arg
)
341 return rrdtool
.info(self
.file)
343 def last_update(self
):
345 Returns a dictionary with the timestamp and
346 data set of the last database update.
349 "dataset" : self
.last_dataset
,
350 "timestamp" : self
.last_updated
,
353 def _last_update(self
):
354 return rrdtool
.lastupdate(self
.file)
357 def last_updated(self
):
359 Returns the timestamp when this database was last updated
361 lu
= self
._last
_update
()
364 return lu
.get("date")
367 def last_dataset(self
):
369 Returns the latest dataset in the database
371 lu
= self
._last
_update
()
378 return self
.plugin
.interval
382 return self
.stepsize
* 2
384 def get_rrd_schema(self
):
386 "--step", "%s" % self
.stepsize
,
388 for line
in self
.rrd_schema
:
389 if line
.startswith("DS:"):
391 (prefix
, name
, type, lower_limit
, upper_limit
) = line
.split(":")
397 "%s" % self
.heartbeat
,
408 for steps
, rows
in self
.rra_timespans
:
409 for type in self
.rra_types
:
410 schema
.append("RRA:%s:%s:%s:%s" % (type, xff
, steps
, rows
))
415 def rrd_schema_names(self
):
418 for line
in self
.rrd_schema
:
419 (prefix
, name
, type, lower_limit
, upper_limit
) = line
.split(":")
424 def make_rrd_defs(self
, prefix
=None):
427 for name
in self
.rrd_schema_names
:
429 p
= "%s_%s" % (prefix
, name
)
434 "DEF:%s=%s:%s:AVERAGE" % (p
, self
.file, name
),
439 def get_stddev(self
, interval
=None):
440 args
= self
.make_rrd_defs()
442 # Add the correct interval
443 args
+= ["--start", util
.make_interval(interval
)]
445 for name
in self
.rrd_schema_names
:
447 "VDEF:%s_stddev=%s,STDEV" % (name
, name
),
448 "PRINT:%s_stddev:%%lf" % name
,
451 x
, y
, vals
= rrdtool
.graph("/dev/null", *args
)
452 return dict(zip(self
.rrd_schema_names
, vals
))
456 raise RuntimeError("This object has already collected its data")
458 self
.collected
= True
459 self
.now
= datetime
.datetime
.utcnow()
462 result
= self
.collect()
466 Will commit the collected data to the database.
468 # Make sure that the RRD database has been created
471 # Write everything to disk that is in the write queue
472 self
.collecty
.write_queue
.commit_file(self
.file)
475 class GraphTemplate(object):
476 # A unique name to identify this graph template.
479 # Headline of the graph image
482 # Vertical label of the graph
483 graph_vertical_label
= None
489 # Instructions how to create the graph.
492 # Extra arguments passed to rrdgraph.
495 # Default dimensions for this graph
496 height
= GRAPH_DEFAULT_HEIGHT
497 width
= GRAPH_DEFAULT_WIDTH
499 def __init__(self
, plugin
, object_id
, locale
=None, timezone
=None):
502 # Save localisation parameters
503 self
.locale
= locales
.get(locale
)
504 self
.timezone
= timezone
506 # Get all required RRD objects
507 self
.object_id
= object_id
509 # Get the main object
510 self
.objects
= self
.get_objects(self
.object_id
)
514 return "<%s>" % self
.__class
__.__name
__
518 return self
.plugin
.collecty
522 return self
.plugin
.log
527 Shortcut to the main object
529 if len(self
.objects
) == 1:
530 return self
.objects
[0]
532 def _make_command_line(self
, interval
, format
=DEFAULT_IMAGE_FORMAT
,
533 width
=None, height
=None, with_title
=True, thumbnail
=False):
534 args
= [e
for e
in GRAPH_DEFAULT_ARGUMENTS
]
536 # Set the default dimensions
537 default_height
, default_width
= GRAPH_DEFAULT_HEIGHT
, GRAPH_DEFAULT_WIDTH
539 # A thumbnail doesn't have a legend and other labels
541 args
.append("--only-graph")
543 default_height
= THUMBNAIL_DEFAULT_HEIGHT
544 default_width
= THUMBNAIL_DEFAULT_WIDTH
547 "--imgformat", format
,
548 "--height", "%s" % (height
or default_height
),
549 "--width", "%s" % (width
or default_width
),
552 args
+= self
.rrd_graph_args
555 if with_title
and self
.graph_title
:
556 args
+= ["--title", self
.graph_title
]
559 if self
.graph_vertical_label
:
560 args
+= ["--vertical-label", self
.graph_vertical_label
]
562 if self
.lower_limit
is not None or self
.upper_limit
is not None:
563 # Force to honour the set limits
564 args
.append("--rigid")
566 if self
.lower_limit
is not None:
567 args
+= ["--lower-limit", self
.lower_limit
]
569 if self
.upper_limit
is not None:
570 args
+= ["--upper-limit", self
.upper_limit
]
573 args
+= ["--start", util
.make_interval(interval
)]
578 use_prefix
= len(self
.objects
) >= 2
581 for object in self
.objects
:
583 args
+= object.make_rrd_defs(object.id)
585 args
+= object.make_rrd_defs()
589 def _add_vdefs(self
, args
):
595 # Search for all DEFs and CDEFs
596 m
= re
.match(DEF_MATCH
, "%s" % arg
)
600 # Add the VDEFs for minimum, maximum, etc. values
602 "VDEF:%s_cur=%s,LAST" % (name
, name
),
603 "VDEF:%s_avg=%s,AVERAGE" % (name
, name
),
604 "VDEF:%s_max=%s,MAXIMUM" % (name
, name
),
605 "VDEF:%s_min=%s,MINIMUM" % (name
, name
),
610 def get_objects(self
, *args
, **kwargs
):
611 object = self
.plugin
.get_object(*args
, **kwargs
)
618 def generate_graph(self
, interval
=None, **kwargs
):
619 assert self
.objects
, "Cannot render graph without any objects"
621 # Make sure that all collected data is in the database
622 # to get a recent graph image
623 for object in self
.objects
:
626 args
= self
._make
_command
_line
(interval
, **kwargs
)
628 self
.log
.info(_("Generating graph %s") % self
)
630 rrd_graph
= self
.rrd_graph
632 # Add DEFs for all objects
633 if not any((e
.startswith("DEF:") for e
in rrd_graph
)):
634 args
+= self
._add
_defs
()
637 args
= self
._add
_vdefs
(args
)
639 # Convert arguments to string
640 args
= [str(e
) for e
in args
]
643 self
.log
.debug(" %s" % arg
)
645 with
Environment(self
.timezone
, self
.locale
.lang
):
646 graph
= rrdtool
.graphv("-", *args
)
649 "image" : graph
.get("image"),
650 "image_height" : graph
.get("image_height"),
651 "image_width" : graph
.get("image_width"),
654 def graph_info(self
):
656 Returns a dictionary with useful information
660 "title" : self
.graph_title
,
661 "object_id" : self
.object_id
or "",
662 "template" : self
.name
,