]>
git.ipfire.org Git - collecty.git/blob - src/collecty/plugins/base.py
dd7f0d99156695c437c2035e18ab0aefe7b80368
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
34 from ..constants
import *
37 DEF_MATCH
= re
.compile(r
"C?DEF:([A-Za-z0-9_]+)=")
40 def __init__(self
, timeout
, heartbeat
=1):
41 self
.timeout
= timeout
42 self
.heartbeat
= heartbeat
48 def reset(self
, delay
=0):
50 self
.start
= time
.time()
54 # Has this timer been killed?
59 return time
.time() - self
.start
- self
.delay
65 while self
.elapsed
< self
.timeout
and not self
.killed
:
66 time
.sleep(self
.heartbeat
)
68 return self
.elapsed
> self
.timeout
71 class Environment(object):
73 Sets the correct environment for rrdtool to create
74 localised graphs and graphs in the correct timezone.
76 def __init__(self
, timezone
, locale
):
77 # Build the new environment
78 self
.new_environment
= {
79 "TZ" : timezone
or DEFAULT_TIMEZONE
,
82 for k
in ("LANG", "LC_ALL"):
83 self
.new_environment
[k
] = locale
or DEFAULT_LOCALE
86 # Save the current environment
87 self
.old_environment
= {}
88 for k
in self
.new_environment
:
89 self
.old_environment
[k
] = os
.environ
.get(k
, None)
92 os
.environ
.update(self
.new_environment
)
94 def __exit__(self
, type, value
, traceback
):
95 # Roll back to the previous environment
96 for k
, v
in self
.old_environment
.items():
106 class PluginRegistration(type):
109 def __init__(plugin
, name
, bases
, dict):
110 type.__init
__(plugin
, name
, bases
, dict)
112 # The main class from which is inherited is not registered
117 if not all((plugin
.name
, plugin
.description
)):
118 raise RuntimeError(_("Plugin is not properly configured: %s") % plugin
)
120 PluginRegistration
.plugins
[plugin
.name
] = plugin
125 Returns a list with all automatically registered plugins.
127 return PluginRegistration
.plugins
.values()
129 class Plugin(object, metaclass
=PluginRegistration
):
130 # The name of this plugin.
133 # A description for this plugin.
136 # Templates which can be used to generate a graph out of
137 # the data from this data source.
140 # The default interval for all plugins
143 def __init__(self
, collecty
, **kwargs
):
144 self
.collecty
= collecty
146 # Check if this plugin was configured correctly.
147 assert self
.name
, "Name of the plugin is not set: %s" % self
.name
148 assert self
.description
, "Description of the plugin is not set: %s" % self
.description
150 # Initialize the logger.
151 self
.log
= logging
.getLogger("collecty.plugins.%s" % self
.name
)
152 self
.log
.propagate
= 1
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 NaN
230 def get_object(self
, id):
231 for object in self
.objects
:
232 if not object.id == id:
237 def get_template(self
, template_name
, object_id
, locale
=None, timezone
=None):
238 for template
in self
.templates
:
239 if not template
.name
== template_name
:
242 return template(self
, object_id
, locale
=locale
, timezone
=timezone
)
244 def generate_graph(self
, template_name
, object_id
="default",
245 timezone
=None, locale
=None, **kwargs
):
246 template
= self
.get_template(template_name
, object_id
=object_id
,
247 timezone
=timezone
, locale
=locale
)
249 raise RuntimeError("Could not find template %s" % template_name
)
251 time_start
= time
.time()
253 graph
= template
.generate_graph(**kwargs
)
255 duration
= time
.time() - time_start
256 self
.log
.debug(_("Generated graph %s in %.1fms") \
257 % (template
, duration
* 1000))
261 def graph_info(self
, template_name
, object_id
="default",
262 timezone
=None, locale
=None, **kwargs
):
263 template
= self
.get_template(template_name
, object_id
=object_id
,
264 timezone
=timezone
, locale
=locale
)
266 raise RuntimeError("Could not find template %s" % template_name
)
268 return template
.graph_info()
270 def last_update(self
, object_id
="default"):
271 object = self
.get_object(object_id
)
273 raise RuntimeError("Could not find object %s" % object_id
)
275 return object.last_update()
278 class Object(object):
279 # The schema of the RRD database.
283 rra_types
= ("AVERAGE", "MIN", "MAX")
290 def __init__(self
, plugin
, *args
, **kwargs
):
293 # Indicates if this object has collected its data
294 self
.collected
= False
296 # Initialise this object
297 self
.init(*args
, **kwargs
)
299 # Create the database file.
303 return "<%s>" % self
.__class
__.__name
__
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
),
470 raise RuntimeError("This object has already collected its data")
472 self
.collected
= True
473 self
.now
= datetime
.datetime
.utcnow()
476 result
= self
.collect()
480 Will commit the collected data to the database.
482 # Make sure that the RRD database has been created
485 # Write everything to disk that is in the write queue
486 self
.collecty
.write_queue
.commit_file(self
.file)
489 class GraphTemplate(object):
490 # A unique name to identify this graph template.
493 # Headline of the graph image
496 # Vertical label of the graph
497 graph_vertical_label
= None
503 # Instructions how to create the graph.
506 # Extra arguments passed to rrdgraph.
518 # Default dimensions for this graph
519 height
= GRAPH_DEFAULT_HEIGHT
520 width
= GRAPH_DEFAULT_WIDTH
522 def __init__(self
, plugin
, object_id
, locale
=None, timezone
=None):
525 # Save localisation parameters
526 self
.locale
= locales
.get(locale
)
527 self
.timezone
= timezone
529 # Get all required RRD objects
530 self
.object_id
= object_id
532 # Get the main object
533 self
.object = self
.get_object(self
.object_id
)
536 return "<%s>" % self
.__class
__.__name
__
540 return self
.plugin
.collecty
544 return self
.plugin
.log
546 def _make_command_line(self
, interval
, format
=DEFAULT_IMAGE_FORMAT
,
547 width
=None, height
=None, with_title
=True, thumbnail
=False):
548 args
= [e
for e
in GRAPH_DEFAULT_ARGUMENTS
]
550 # Set the default dimensions
551 default_height
, default_width
= GRAPH_DEFAULT_HEIGHT
, GRAPH_DEFAULT_WIDTH
553 # A thumbnail doesn't have a legend and other labels
555 args
.append("--only-graph")
557 default_height
= THUMBNAIL_DEFAULT_HEIGHT
558 default_width
= THUMBNAIL_DEFAULT_WIDTH
561 "--imgformat", format
,
562 "--height", "%s" % (height
or default_height
),
563 "--width", "%s" % (width
or default_width
),
566 args
+= self
.rrd_graph_args
569 if with_title
and self
.graph_title
:
570 args
+= ["--title", self
.graph_title
]
573 if self
.graph_vertical_label
:
574 args
+= ["--vertical-label", self
.graph_vertical_label
]
576 if self
.lower_limit
is not None or self
.upper_limit
is not None:
577 # Force to honour the set limits
578 args
.append("--rigid")
580 if self
.lower_limit
is not None:
581 args
+= ["--lower-limit", self
.lower_limit
]
583 if self
.upper_limit
is not None:
584 args
+= ["--upper-limit", self
.upper_limit
]
587 interval
= self
.intervals
[interval
]
589 interval
= "end-%s" % interval
592 args
+= ["--start", interval
]
596 def _add_vdefs(self
, args
):
602 # Search for all DEFs and CDEFs
603 m
= re
.match(DEF_MATCH
, "%s" % arg
)
607 # Add the VDEFs for minimum, maximum, etc. values
609 "VDEF:%s_cur=%s,LAST" % (name
, name
),
610 "VDEF:%s_avg=%s,AVERAGE" % (name
, name
),
611 "VDEF:%s_max=%s,MAXIMUM" % (name
, name
),
612 "VDEF:%s_min=%s,MINIMUM" % (name
, name
),
617 def get_object(self
, *args
, **kwargs
):
618 return self
.plugin
.get_object(*args
, **kwargs
)
620 def get_object_table(self
):
622 "file" : self
.object,
626 def object_table(self
):
627 if not hasattr(self
, "_object_table"):
628 self
._object
_table
= self
.get_object_table()
630 return self
._object
_table
632 def get_object_files(self
):
635 for id, obj
in self
.object_table
.items():
640 def generate_graph(self
, interval
=None, **kwargs
):
641 # Make sure that all collected data is in the database
642 # to get a recent graph image
646 args
= self
._make
_command
_line
(interval
, **kwargs
)
648 self
.log
.info(_("Generating graph %s") % self
)
650 #object_files = self.get_object_files()
653 args
+= self
.object.make_rrd_defs()
655 args
+= self
.rrd_graph
656 args
= self
._add
_vdefs
(args
)
658 # Convert arguments to string
659 args
= [str(e
) for e
in args
]
662 self
.log
.debug(" %s" % arg
)
664 with
Environment(self
.timezone
, self
.locale
.lang
):
665 graph
= rrdtool
.graphv("-", *args
)
668 "image" : graph
.get("image"),
669 "image_height" : graph
.get("image_height"),
670 "image_width" : graph
.get("image_width"),
673 def graph_info(self
):
675 Returns a dictionary with useful information
679 "title" : self
.graph_title
,
680 "object_id" : self
.object_id
or "",
681 "template" : self
.name
,