]>
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
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
))
443 def make_rrd_defs(self
, prefix
=None):
446 for line
in self
.rrd_schema
:
447 (def_type
, name
, type, lower_limit
, upper_limit
) = line
.split(":")
450 p
= "%s_%s" % (prefix
, name
)
455 "DEF:%s=%s:%s:AVERAGE" % (p
, self
.file, name
),
462 raise RuntimeError("This object has already collected its data")
464 self
.collected
= True
465 self
.now
= datetime
.datetime
.utcnow()
468 result
= self
.collect()
472 Will commit the collected data to the database.
474 # Make sure that the RRD database has been created
477 # Write everything to disk that is in the write queue
478 self
.collecty
.write_queue
.commit_file(self
.file)
481 class GraphTemplate(object):
482 # A unique name to identify this graph template.
485 # Headline of the graph image
488 # Vertical label of the graph
489 graph_vertical_label
= None
495 # Instructions how to create the graph.
498 # Extra arguments passed to rrdgraph.
510 # Default dimensions for this graph
511 height
= GRAPH_DEFAULT_HEIGHT
512 width
= GRAPH_DEFAULT_WIDTH
514 def __init__(self
, plugin
, object_id
, locale
=None, timezone
=None):
517 # Save localisation parameters
518 self
.locale
= locales
.get(locale
)
519 self
.timezone
= timezone
521 # Get all required RRD objects
522 self
.object_id
= object_id
524 # Get the main object
525 self
.object = self
.get_object(self
.object_id
)
528 return "<%s>" % self
.__class
__.__name
__
532 return self
.plugin
.collecty
536 return self
.plugin
.log
538 def _make_command_line(self
, interval
, format
=DEFAULT_IMAGE_FORMAT
,
539 width
=None, height
=None, with_title
=True, thumbnail
=False):
540 args
= [e
for e
in GRAPH_DEFAULT_ARGUMENTS
]
542 # Set the default dimensions
543 default_height
, default_width
= GRAPH_DEFAULT_HEIGHT
, GRAPH_DEFAULT_WIDTH
545 # A thumbnail doesn't have a legend and other labels
547 args
.append("--only-graph")
549 default_height
= THUMBNAIL_DEFAULT_HEIGHT
550 default_width
= THUMBNAIL_DEFAULT_WIDTH
553 "--imgformat", format
,
554 "--height", "%s" % (height
or default_height
),
555 "--width", "%s" % (width
or default_width
),
558 args
+= self
.rrd_graph_args
561 if with_title
and self
.graph_title
:
562 args
+= ["--title", self
.graph_title
]
565 if self
.graph_vertical_label
:
566 args
+= ["--vertical-label", self
.graph_vertical_label
]
568 if self
.lower_limit
is not None or self
.upper_limit
is not None:
569 # Force to honour the set limits
570 args
.append("--rigid")
572 if self
.lower_limit
is not None:
573 args
+= ["--lower-limit", self
.lower_limit
]
575 if self
.upper_limit
is not None:
576 args
+= ["--upper-limit", self
.upper_limit
]
579 interval
= self
.intervals
[interval
]
581 interval
= "end-%s" % interval
584 args
+= ["--start", interval
]
588 def _add_vdefs(self
, args
):
594 # Search for all DEFs and CDEFs
595 m
= re
.match(DEF_MATCH
, "%s" % arg
)
599 # Add the VDEFs for minimum, maximum, etc. values
601 "VDEF:%s_cur=%s,LAST" % (name
, name
),
602 "VDEF:%s_avg=%s,AVERAGE" % (name
, name
),
603 "VDEF:%s_max=%s,MAXIMUM" % (name
, name
),
604 "VDEF:%s_min=%s,MINIMUM" % (name
, name
),
609 def get_object(self
, *args
, **kwargs
):
610 return self
.plugin
.get_object(*args
, **kwargs
)
612 def get_object_table(self
):
614 "file" : self
.object,
618 def object_table(self
):
619 if not hasattr(self
, "_object_table"):
620 self
._object
_table
= self
.get_object_table()
622 return self
._object
_table
624 def get_object_files(self
):
627 for id, obj
in self
.object_table
.items():
632 def generate_graph(self
, interval
=None, **kwargs
):
633 # Make sure that all collected data is in the database
634 # to get a recent graph image
638 args
= self
._make
_command
_line
(interval
, **kwargs
)
640 self
.log
.info(_("Generating graph %s") % self
)
642 #object_files = self.get_object_files()
645 args
+= self
.object.make_rrd_defs()
647 args
+= self
.rrd_graph
648 args
= self
._add
_vdefs
(args
)
650 # Convert arguments to string
651 args
= [str(e
) for e
in args
]
654 self
.log
.debug(" %s" % arg
)
656 with
Environment(self
.timezone
, self
.locale
.lang
):
657 graph
= rrdtool
.graphv("-", *args
)
660 "image" : graph
.get("image"),
661 "image_height" : graph
.get("image_height"),
662 "image_width" : graph
.get("image_width"),
665 def graph_info(self
):
667 Returns a dictionary with useful information
671 "title" : self
.graph_title
,
672 "object_id" : self
.object_id
or "",
673 "template" : self
.name
,