]>
git.ipfire.org Git - collecty.git/blob - src/collecty/plugins/base.py
5b130444324f5c530efdad3b97e3155888d35c4e
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
)
153 self
.log
.propagate
= 1
157 # Run some custom initialization.
160 self
.log
.debug(_("Successfully initialized %s") % self
.__class
__.__name
__)
165 Returns the name of the sub directory in which all RRD files
166 for this plugin should be stored in.
172 def init(self
, **kwargs
):
174 Do some custom initialization stuff here.
180 Gathers the statistical data, this plugin collects.
182 time_start
= time
.time()
184 # Run through all objects of this plugin and call the collect method.
185 for o
in self
.objects
:
186 now
= datetime
.datetime
.utcnow()
190 result
= self
._format
_result
(result
)
192 self
.log
.warning(_("Unhandled exception in %s.collect()") % o
, exc_info
=True)
196 self
.log
.warning(_("Received empty result: %s") % o
)
199 self
.log
.debug(_("Collected %s: %s") % (o
, result
))
201 # Add the object to the write queue so that the data is written
202 # to the databases later.
203 self
.collecty
.write_queue
.add(o
, now
, result
)
205 # Returns the time this function took to complete.
206 delay
= time
.time() - time_start
208 # Log some warning when a collect method takes too long to return some data
210 self
.log
.warning(_("A worker thread was stalled for %.4fs") % delay
)
213 def _format_result(result
):
214 if not isinstance(result
, tuple) and not isinstance(result
, list):
217 # Replace all Nones by NaN
231 def get_object(self
, id):
232 for object in self
.objects
:
233 if not object.id == id:
238 def get_template(self
, template_name
, object_id
, locale
=None, timezone
=None):
239 for template
in self
.templates
:
240 if not template
.name
== template_name
:
243 return template(self
, object_id
, locale
=locale
, timezone
=timezone
)
245 def generate_graph(self
, template_name
, object_id
="default",
246 timezone
=None, locale
=None, **kwargs
):
247 template
= self
.get_template(template_name
, object_id
=object_id
,
248 timezone
=timezone
, locale
=locale
)
250 raise RuntimeError("Could not find template %s" % template_name
)
252 time_start
= time
.time()
254 graph
= template
.generate_graph(**kwargs
)
256 duration
= time
.time() - time_start
257 self
.log
.debug(_("Generated graph %s in %.1fms") \
258 % (template
, duration
* 1000))
262 def graph_info(self
, template_name
, object_id
="default",
263 timezone
=None, locale
=None, **kwargs
):
264 template
= self
.get_template(template_name
, object_id
=object_id
,
265 timezone
=timezone
, locale
=locale
)
267 raise RuntimeError("Could not find template %s" % template_name
)
269 return template
.graph_info()
271 def last_update(self
, object_id
="default"):
272 object = self
.get_object(object_id
)
274 raise RuntimeError("Could not find object %s" % object_id
)
276 return object.last_update()
279 class Object(object):
280 # The schema of the RRD database.
284 rra_types
= ("AVERAGE", "MIN", "MAX")
291 def __init__(self
, plugin
, *args
, **kwargs
):
294 # Indicates if this object has collected its data
295 self
.collected
= False
297 # Initialise this object
298 self
.init(*args
, **kwargs
)
300 # Create the database file.
304 return "<%s>" % self
.__class
__.__name
__
306 def __lt__(self
, other
):
307 return self
.id < other
.id
311 return self
.plugin
.collecty
315 return self
.plugin
.log
320 Returns a UNIQUE identifier for this object. As this is incorporated
321 into the path of RRD file, it must only contain ASCII characters.
323 raise NotImplementedError
328 The absolute path to the RRD file of this plugin.
330 filename
= self
._normalise
_filename
("%s.rrd" % self
.id)
332 return os
.path
.join(DATABASE_DIR
, self
.plugin
.path
, filename
)
335 def _normalise_filename(filename
):
336 # Convert the filename into ASCII characters only
337 filename
= unicodedata
.normalize("NFKC", filename
)
339 # Replace any spaces by dashes
340 filename
= filename
.replace(" ", "-")
346 def init(self
, *args
, **kwargs
):
348 Do some custom initialization stuff here.
354 Creates an empty RRD file with the desired data structures.
356 # Skip if the file does already exist.
357 if os
.path
.exists(self
.file):
360 dirname
= os
.path
.dirname(self
.file)
361 if not os
.path
.exists(dirname
):
364 # Create argument list.
365 args
= self
.get_rrd_schema()
367 rrdtool
.create(self
.file, *args
)
369 self
.log
.debug(_("Created RRD file %s.") % self
.file)
371 self
.log
.debug(" %s" % arg
)
374 return rrdtool
.info(self
.file)
376 def last_update(self
):
378 Returns a dictionary with the timestamp and
379 data set of the last database update.
382 "dataset" : self
.last_dataset
,
383 "timestamp" : self
.last_updated
,
386 def _last_update(self
):
387 return rrdtool
.lastupdate(self
.file)
390 def last_updated(self
):
392 Returns the timestamp when this database was last updated
394 lu
= self
._last
_update
()
397 return lu
.get("date")
400 def last_dataset(self
):
402 Returns the latest dataset in the database
404 lu
= self
._last
_update
()
411 return self
.plugin
.interval
415 return self
.stepsize
* 2
417 def get_rrd_schema(self
):
419 "--step", "%s" % self
.stepsize
,
421 for line
in self
.rrd_schema
:
422 if line
.startswith("DS:"):
424 (prefix
, name
, type, lower_limit
, upper_limit
) = line
.split(":")
430 "%s" % self
.heartbeat
,
441 for steps
, rows
in self
.rra_timespans
:
442 for type in self
.rra_types
:
443 schema
.append("RRA:%s:%s:%s:%s" % (type, xff
, steps
, rows
))
448 def rrd_schema_names(self
):
451 for line
in self
.rrd_schema
:
452 (prefix
, name
, type, lower_limit
, upper_limit
) = line
.split(":")
457 def make_rrd_defs(self
, prefix
=None):
460 for name
in self
.rrd_schema_names
:
462 p
= "%s_%s" % (prefix
, name
)
467 "DEF:%s=%s:%s:AVERAGE" % (p
, self
.file, name
),
472 def get_stddev(self
, interval
=None):
473 args
= self
.make_rrd_defs()
475 # Add the correct interval
476 args
+= ["--start", util
.make_interval(interval
)]
478 for name
in self
.rrd_schema_names
:
480 "VDEF:%s_stddev=%s,STDEV" % (name
, name
),
481 "PRINT:%s_stddev:%%lf" % name
,
484 x
, y
, vals
= rrdtool
.graph("/dev/null", *args
)
485 return dict(zip(self
.rrd_schema_names
, vals
))
489 raise RuntimeError("This object has already collected its data")
491 self
.collected
= True
492 self
.now
= datetime
.datetime
.utcnow()
495 result
= self
.collect()
499 Will commit the collected data to the database.
501 # Make sure that the RRD database has been created
504 # Write everything to disk that is in the write queue
505 self
.collecty
.write_queue
.commit_file(self
.file)
508 class GraphTemplate(object):
509 # A unique name to identify this graph template.
512 # Headline of the graph image
515 # Vertical label of the graph
516 graph_vertical_label
= None
522 # Instructions how to create the graph.
525 # Extra arguments passed to rrdgraph.
528 # Default dimensions for this graph
529 height
= GRAPH_DEFAULT_HEIGHT
530 width
= GRAPH_DEFAULT_WIDTH
532 def __init__(self
, plugin
, object_id
, locale
=None, timezone
=None):
535 # Save localisation parameters
536 self
.locale
= locales
.get(locale
)
537 self
.timezone
= timezone
539 # Get all required RRD objects
540 self
.object_id
= object_id
542 # Get the main object
543 self
.objects
= self
.get_objects(self
.object_id
)
547 return "<%s>" % self
.__class
__.__name
__
551 return self
.plugin
.collecty
555 return self
.plugin
.log
560 Shortcut to the main object
562 if len(self
.objects
) == 1:
563 return self
.objects
[0]
565 def _make_command_line(self
, interval
, format
=DEFAULT_IMAGE_FORMAT
,
566 width
=None, height
=None, with_title
=True, thumbnail
=False):
567 args
= [e
for e
in GRAPH_DEFAULT_ARGUMENTS
]
569 # Set the default dimensions
570 default_height
, default_width
= GRAPH_DEFAULT_HEIGHT
, GRAPH_DEFAULT_WIDTH
572 # A thumbnail doesn't have a legend and other labels
574 args
.append("--only-graph")
576 default_height
= THUMBNAIL_DEFAULT_HEIGHT
577 default_width
= THUMBNAIL_DEFAULT_WIDTH
580 "--imgformat", format
,
581 "--height", "%s" % (height
or default_height
),
582 "--width", "%s" % (width
or default_width
),
585 args
+= self
.rrd_graph_args
588 if with_title
and self
.graph_title
:
589 args
+= ["--title", self
.graph_title
]
592 if self
.graph_vertical_label
:
593 args
+= ["--vertical-label", self
.graph_vertical_label
]
595 if self
.lower_limit
is not None or self
.upper_limit
is not None:
596 # Force to honour the set limits
597 args
.append("--rigid")
599 if self
.lower_limit
is not None:
600 args
+= ["--lower-limit", self
.lower_limit
]
602 if self
.upper_limit
is not None:
603 args
+= ["--upper-limit", self
.upper_limit
]
606 args
+= ["--start", util
.make_interval(interval
)]
611 use_prefix
= len(self
.objects
) >= 2
614 for object in self
.objects
:
616 args
+= object.make_rrd_defs(object.id)
618 args
+= object.make_rrd_defs()
622 def _add_vdefs(self
, args
):
628 # Search for all DEFs and CDEFs
629 m
= re
.match(DEF_MATCH
, "%s" % arg
)
633 # Add the VDEFs for minimum, maximum, etc. values
635 "VDEF:%s_cur=%s,LAST" % (name
, name
),
636 "VDEF:%s_avg=%s,AVERAGE" % (name
, name
),
637 "VDEF:%s_max=%s,MAXIMUM" % (name
, name
),
638 "VDEF:%s_min=%s,MINIMUM" % (name
, name
),
643 def get_objects(self
, *args
, **kwargs
):
644 object = self
.plugin
.get_object(*args
, **kwargs
)
651 def generate_graph(self
, interval
=None, **kwargs
):
652 assert self
.objects
, "Cannot render graph without any objects"
654 # Make sure that all collected data is in the database
655 # to get a recent graph image
656 for object in self
.objects
:
659 args
= self
._make
_command
_line
(interval
, **kwargs
)
661 self
.log
.info(_("Generating graph %s") % self
)
663 rrd_graph
= self
.rrd_graph
665 # Add DEFs for all objects
666 if not any((e
.startswith("DEF:") for e
in rrd_graph
)):
667 args
+= self
._add
_defs
()
670 args
= self
._add
_vdefs
(args
)
672 # Convert arguments to string
673 args
= [str(e
) for e
in args
]
676 self
.log
.debug(" %s" % arg
)
678 with
Environment(self
.timezone
, self
.locale
.lang
):
679 graph
= rrdtool
.graphv("-", *args
)
682 "image" : graph
.get("image"),
683 "image_height" : graph
.get("image_height"),
684 "image_width" : graph
.get("image_width"),
687 def graph_info(self
):
689 Returns a dictionary with useful information
693 "title" : self
.graph_title
,
694 "object_id" : self
.object_id
or "",
695 "template" : self
.name
,