]>
git.ipfire.org Git - oddments/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
)
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 UNKNOWN
228 def get_object(self
, id):
229 for object in self
.objects
:
230 if not object.id == id:
235 def get_template(self
, template_name
, object_id
, locale
=None, timezone
=None):
236 for template
in self
.templates
:
237 if not template
.name
== template_name
:
240 return template(self
, object_id
, locale
=locale
, timezone
=timezone
)
242 def generate_graph(self
, template_name
, object_id
="default",
243 timezone
=None, locale
=None, **kwargs
):
244 template
= self
.get_template(template_name
, object_id
=object_id
,
245 timezone
=timezone
, locale
=locale
)
247 raise RuntimeError("Could not find template %s" % template_name
)
249 time_start
= time
.time()
251 graph
= template
.generate_graph(**kwargs
)
253 duration
= time
.time() - time_start
254 self
.log
.debug(_("Generated graph %s in %.1fms") \
255 % (template
, duration
* 1000))
259 def graph_info(self
, template_name
, object_id
="default",
260 timezone
=None, locale
=None, **kwargs
):
261 template
= self
.get_template(template_name
, object_id
=object_id
,
262 timezone
=timezone
, locale
=locale
)
264 raise RuntimeError("Could not find template %s" % template_name
)
266 return template
.graph_info()
268 def last_update(self
, object_id
="default"):
269 object = self
.get_object(object_id
)
271 raise RuntimeError("Could not find object %s" % object_id
)
273 return object.last_update()
276 class Object(object):
277 # The schema of the RRD database.
281 rra_types
= ("AVERAGE", "MIN", "MAX")
288 def __init__(self
, plugin
, *args
, **kwargs
):
291 # Indicates if this object has collected its data
292 self
.collected
= False
294 # Initialise this object
295 self
.init(*args
, **kwargs
)
297 # Create the database file.
301 return "<%s>" % self
.__class
__.__name
__
303 def __lt__(self
, other
):
304 return self
.id < other
.id
308 return self
.plugin
.collecty
312 return self
.plugin
.log
317 Returns a UNIQUE identifier for this object. As this is incorporated
318 into the path of RRD file, it must only contain ASCII characters.
320 raise NotImplementedError
325 The absolute path to the RRD file of this plugin.
327 filename
= self
._normalise
_filename
("%s.rrd" % self
.id)
329 return os
.path
.join(DATABASE_DIR
, self
.plugin
.path
, filename
)
332 def _normalise_filename(filename
):
333 # Convert the filename into ASCII characters only
334 filename
= unicodedata
.normalize("NFKC", filename
)
336 # Replace any spaces by dashes
337 filename
= filename
.replace(" ", "-")
343 def init(self
, *args
, **kwargs
):
345 Do some custom initialization stuff here.
351 Creates an empty RRD file with the desired data structures.
353 # Skip if the file does already exist.
354 if os
.path
.exists(self
.file):
357 dirname
= os
.path
.dirname(self
.file)
358 if not os
.path
.exists(dirname
):
361 # Create argument list.
362 args
= self
.get_rrd_schema()
364 rrdtool
.create(self
.file, *args
)
366 self
.log
.debug(_("Created RRD file %s.") % self
.file)
368 self
.log
.debug(" %s" % arg
)
371 return rrdtool
.info(self
.file)
373 def last_update(self
):
375 Returns a dictionary with the timestamp and
376 data set of the last database update.
379 "dataset" : self
.last_dataset
,
380 "timestamp" : self
.last_updated
,
383 def _last_update(self
):
384 return rrdtool
.lastupdate(self
.file)
387 def last_updated(self
):
389 Returns the timestamp when this database was last updated
391 lu
= self
._last
_update
()
394 return lu
.get("date")
397 def last_dataset(self
):
399 Returns the latest dataset in the database
401 lu
= self
._last
_update
()
408 return self
.plugin
.interval
412 return self
.stepsize
* 2
414 def get_rrd_schema(self
):
416 "--step", "%s" % self
.stepsize
,
418 for line
in self
.rrd_schema
:
419 if line
.startswith("DS:"):
421 (prefix
, name
, type, lower_limit
, upper_limit
) = line
.split(":")
427 "%s" % self
.heartbeat
,
438 for steps
, rows
in self
.rra_timespans
:
439 for type in self
.rra_types
:
440 schema
.append("RRA:%s:%s:%s:%s" % (type, xff
, steps
, rows
))
445 def rrd_schema_names(self
):
448 for line
in self
.rrd_schema
:
449 (prefix
, name
, type, lower_limit
, upper_limit
) = line
.split(":")
454 def make_rrd_defs(self
, prefix
=None):
457 for name
in self
.rrd_schema_names
:
459 p
= "%s_%s" % (prefix
, name
)
464 "DEF:%s=%s:%s:AVERAGE" % (p
, self
.file, name
),
469 def get_stddev(self
, interval
=None):
470 args
= self
.make_rrd_defs()
472 # Add the correct interval
473 args
+= ["--start", util
.make_interval(interval
)]
475 for name
in self
.rrd_schema_names
:
477 "VDEF:%s_stddev=%s,STDEV" % (name
, name
),
478 "PRINT:%s_stddev:%%lf" % name
,
481 x
, y
, vals
= rrdtool
.graph("/dev/null", *args
)
482 return dict(zip(self
.rrd_schema_names
, vals
))
486 raise RuntimeError("This object has already collected its data")
488 self
.collected
= True
489 self
.now
= datetime
.datetime
.utcnow()
492 result
= self
.collect()
496 Will commit the collected data to the database.
498 # Make sure that the RRD database has been created
501 # Write everything to disk that is in the write queue
502 self
.collecty
.write_queue
.commit_file(self
.file)
505 class GraphTemplate(object):
506 # A unique name to identify this graph template.
509 # Headline of the graph image
512 # Vertical label of the graph
513 graph_vertical_label
= None
519 # Instructions how to create the graph.
522 # Extra arguments passed to rrdgraph.
525 # Default dimensions for this graph
526 height
= GRAPH_DEFAULT_HEIGHT
527 width
= GRAPH_DEFAULT_WIDTH
529 def __init__(self
, plugin
, object_id
, locale
=None, timezone
=None):
532 # Save localisation parameters
533 self
.locale
= locales
.get(locale
)
534 self
.timezone
= timezone
536 # Get all required RRD objects
537 self
.object_id
= object_id
539 # Get the main object
540 self
.objects
= self
.get_objects(self
.object_id
)
544 return "<%s>" % self
.__class
__.__name
__
548 return self
.plugin
.collecty
552 return self
.plugin
.log
557 Shortcut to the main object
559 if len(self
.objects
) == 1:
560 return self
.objects
[0]
562 def _make_command_line(self
, interval
, format
=DEFAULT_IMAGE_FORMAT
,
563 width
=None, height
=None, with_title
=True, thumbnail
=False):
564 args
= [e
for e
in GRAPH_DEFAULT_ARGUMENTS
]
566 # Set the default dimensions
567 default_height
, default_width
= GRAPH_DEFAULT_HEIGHT
, GRAPH_DEFAULT_WIDTH
569 # A thumbnail doesn't have a legend and other labels
571 args
.append("--only-graph")
573 default_height
= THUMBNAIL_DEFAULT_HEIGHT
574 default_width
= THUMBNAIL_DEFAULT_WIDTH
577 "--imgformat", format
,
578 "--height", "%s" % (height
or default_height
),
579 "--width", "%s" % (width
or default_width
),
582 args
+= self
.rrd_graph_args
585 if with_title
and self
.graph_title
:
586 args
+= ["--title", self
.graph_title
]
589 if self
.graph_vertical_label
:
590 args
+= ["--vertical-label", self
.graph_vertical_label
]
592 if self
.lower_limit
is not None or self
.upper_limit
is not None:
593 # Force to honour the set limits
594 args
.append("--rigid")
596 if self
.lower_limit
is not None:
597 args
+= ["--lower-limit", self
.lower_limit
]
599 if self
.upper_limit
is not None:
600 args
+= ["--upper-limit", self
.upper_limit
]
603 args
+= ["--start", util
.make_interval(interval
)]
608 use_prefix
= len(self
.objects
) >= 2
611 for object in self
.objects
:
613 args
+= object.make_rrd_defs(object.id)
615 args
+= object.make_rrd_defs()
619 def _add_vdefs(self
, args
):
625 # Search for all DEFs and CDEFs
626 m
= re
.match(DEF_MATCH
, "%s" % arg
)
630 # Add the VDEFs for minimum, maximum, etc. values
632 "VDEF:%s_cur=%s,LAST" % (name
, name
),
633 "VDEF:%s_avg=%s,AVERAGE" % (name
, name
),
634 "VDEF:%s_max=%s,MAXIMUM" % (name
, name
),
635 "VDEF:%s_min=%s,MINIMUM" % (name
, name
),
640 def get_objects(self
, *args
, **kwargs
):
641 object = self
.plugin
.get_object(*args
, **kwargs
)
648 def generate_graph(self
, interval
=None, **kwargs
):
649 assert self
.objects
, "Cannot render graph without any objects"
651 # Make sure that all collected data is in the database
652 # to get a recent graph image
653 for object in self
.objects
:
656 args
= self
._make
_command
_line
(interval
, **kwargs
)
658 self
.log
.info(_("Generating graph %s") % self
)
660 rrd_graph
= self
.rrd_graph
662 # Add DEFs for all objects
663 if not any((e
.startswith("DEF:") for e
in rrd_graph
)):
664 args
+= self
._add
_defs
()
667 args
= self
._add
_vdefs
(args
)
669 # Convert arguments to string
670 args
= [str(e
) for e
in args
]
673 self
.log
.debug(" %s" % arg
)
675 with
Environment(self
.timezone
, self
.locale
.lang
):
676 graph
= rrdtool
.graphv("-", *args
)
679 "image" : graph
.get("image"),
680 "image_height" : graph
.get("image_height"),
681 "image_width" : graph
.get("image_width"),
684 def graph_info(self
):
686 Returns a dictionary with useful information
690 "title" : self
.graph_title
,
691 "object_id" : self
.object_id
or "",
692 "template" : self
.name
,