]>
git.ipfire.org Git - collecty.git/blob - src/collecty/plugins/base.py
7a613161734eeb9df2f88d509b2a9114ddd3bace
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 ###############################################################################
32 from .. import locales
33 from ..constants
import *
37 def __init__(self
, timeout
, heartbeat
=1):
38 self
.timeout
= timeout
39 self
.heartbeat
= heartbeat
45 def reset(self
, delay
=0):
47 self
.start
= time
.time()
51 # Has this timer been killed?
56 return time
.time() - self
.start
- self
.delay
62 while self
.elapsed
< self
.timeout
and not self
.killed
:
63 time
.sleep(self
.heartbeat
)
65 return self
.elapsed
> self
.timeout
68 class Environment(object):
70 Sets the correct environment for rrdtool to create
71 localised graphs and graphs in the correct timezone.
73 def __init__(self
, timezone
, locale
):
74 # Build the new environment
75 self
.new_environment
= {
76 "TZ" : timezone
or DEFAULT_TIMEZONE
,
79 for k
in ("LANG", "LC_ALL"):
80 self
.new_environment
[k
] = locale
or DEFAULT_LOCALE
83 # Save the current environment
84 self
.old_environment
= {}
85 for k
in self
.new_environment
:
86 self
.old_environment
[k
] = os
.environ
.get(k
, None)
89 os
.environ
.update(self
.new_environment
)
91 def __exit__(self
, type, value
, traceback
):
92 # Roll back to the previous environment
93 for k
, v
in self
.old_environment
.items():
103 class PluginRegistration(type):
106 def __init__(plugin
, name
, bases
, dict):
107 type.__init
__(plugin
, name
, bases
, dict)
109 # The main class from which is inherited is not registered
114 if not all((plugin
.name
, plugin
.description
)):
115 raise RuntimeError(_("Plugin is not properly configured: %s") % plugin
)
117 PluginRegistration
.plugins
[plugin
.name
] = plugin
122 Returns a list with all automatically registered plugins.
124 return PluginRegistration
.plugins
.values()
126 class Plugin(object, metaclass
=PluginRegistration
):
127 # The name of this plugin.
130 # A description for this plugin.
133 # Templates which can be used to generate a graph out of
134 # the data from this data source.
137 # The default interval for all plugins
140 def __init__(self
, collecty
, **kwargs
):
141 self
.collecty
= collecty
143 # Check if this plugin was configured correctly.
144 assert self
.name
, "Name of the plugin is not set: %s" % self
.name
145 assert self
.description
, "Description of the plugin is not set: %s" % self
.description
147 # Initialize the logger.
148 self
.log
= logging
.getLogger("collecty.plugins.%s" % self
.name
)
149 self
.log
.propagate
= 1
153 # Run some custom initialization.
156 self
.log
.debug(_("Successfully initialized %s") % self
.__class
__.__name
__)
161 Returns the name of the sub directory in which all RRD files
162 for this plugin should be stored in.
168 def init(self
, **kwargs
):
170 Do some custom initialization stuff here.
176 Gathers the statistical data, this plugin collects.
178 time_start
= time
.time()
180 # Run through all objects of this plugin and call the collect method.
181 for o
in self
.objects
:
182 now
= datetime
.datetime
.utcnow()
186 result
= self
._format
_result
(result
)
188 self
.log
.warning(_("Unhandled exception in %s.collect()") % o
, exc_info
=True)
192 self
.log
.warning(_("Received empty result: %s") % o
)
195 self
.log
.debug(_("Collected %s: %s") % (o
, result
))
197 # Add the object to the write queue so that the data is written
198 # to the databases later.
199 self
.collecty
.write_queue
.add(o
, now
, result
)
201 # Returns the time this function took to complete.
202 delay
= time
.time() - time_start
204 # Log some warning when a collect method takes too long to return some data
206 self
.log
.warning(_("A worker thread was stalled for %.4fs") % delay
)
209 def _format_result(result
):
210 if not isinstance(result
, tuple) and not isinstance(result
, list):
213 # Replace all Nones by NaN
227 def get_object(self
, id):
228 for object in self
.objects
:
229 if not object.id == id:
234 def get_template(self
, template_name
, object_id
, locale
=None, timezone
=None):
235 for template
in self
.templates
:
236 if not template
.name
== template_name
:
239 return template(self
, object_id
, locale
=locale
, timezone
=timezone
)
241 def generate_graph(self
, template_name
, object_id
="default",
242 timezone
=None, locale
=None, **kwargs
):
243 template
= self
.get_template(template_name
, object_id
=object_id
,
244 timezone
=timezone
, locale
=locale
)
246 raise RuntimeError("Could not find template %s" % template_name
)
248 time_start
= time
.time()
250 graph
= template
.generate_graph(**kwargs
)
252 duration
= time
.time() - time_start
253 self
.log
.debug(_("Generated graph %s in %.1fms") \
254 % (template
, duration
* 1000))
258 def graph_info(self
, template_name
, object_id
="default",
259 timezone
=None, locale
=None, **kwargs
):
260 template
= self
.get_template(template_name
, object_id
=object_id
,
261 timezone
=timezone
, locale
=locale
)
263 raise RuntimeError("Could not find template %s" % template_name
)
265 return template
.graph_info()
267 def last_update(self
, object_id
="default"):
268 object = self
.get_object(object_id
)
270 raise RuntimeError("Could not find object %s" % object_id
)
272 return object.last_update()
275 class Object(object):
276 # The schema of the RRD database.
280 rra_types
= ("AVERAGE", "MIN", "MAX")
287 def __init__(self
, plugin
, *args
, **kwargs
):
290 # Indicates if this object has collected its data
291 self
.collected
= False
293 # Initialise this object
294 self
.init(*args
, **kwargs
)
296 # Create the database file.
300 return "<%s>" % self
.__class
__.__name
__
304 return self
.plugin
.collecty
308 return self
.plugin
.log
313 Returns a UNIQUE identifier for this object. As this is incorporated
314 into the path of RRD file, it must only contain ASCII characters.
316 raise NotImplementedError
321 The absolute path to the RRD file of this plugin.
323 filename
= self
._normalise
_filename
("%s.rrd" % self
.id)
325 return os
.path
.join(DATABASE_DIR
, self
.plugin
.path
, filename
)
328 def _normalise_filename(filename
):
329 # Convert the filename into ASCII characters only
330 filename
= unicodedata
.normalize("NFKC", filename
)
332 # Replace any spaces by dashes
333 filename
= filename
.replace(" ", "-")
339 def init(self
, *args
, **kwargs
):
341 Do some custom initialization stuff here.
347 Creates an empty RRD file with the desired data structures.
349 # Skip if the file does already exist.
350 if os
.path
.exists(self
.file):
353 dirname
= os
.path
.dirname(self
.file)
354 if not os
.path
.exists(dirname
):
357 # Create argument list.
358 args
= self
.get_rrd_schema()
360 rrdtool
.create(self
.file, *args
)
362 self
.log
.debug(_("Created RRD file %s.") % self
.file)
364 self
.log
.debug(" %s" % arg
)
367 return rrdtool
.info(self
.file)
369 def last_update(self
):
371 Returns a dictionary with the timestamp and
372 data set of the last database update.
375 "dataset" : self
.last_dataset
,
376 "timestamp" : self
.last_updated
,
379 def _last_update(self
):
380 return rrdtool
.lastupdate(self
.file)
383 def last_updated(self
):
385 Returns the timestamp when this database was last updated
387 lu
= self
._last
_update
()
390 return lu
.get("date")
393 def last_dataset(self
):
395 Returns the latest dataset in the database
397 lu
= self
._last
_update
()
404 return self
.plugin
.interval
408 return self
.stepsize
* 2
410 def get_rrd_schema(self
):
412 "--step", "%s" % self
.stepsize
,
414 for line
in self
.rrd_schema
:
415 if line
.startswith("DS:"):
417 (prefix
, name
, type, lower_limit
, upper_limit
) = line
.split(":")
423 "%s" % self
.heartbeat
,
434 for steps
, rows
in self
.rra_timespans
:
435 for type in self
.rra_types
:
436 schema
.append("RRA:%s:%s:%s:%s" % (type, xff
, steps
, rows
))
442 raise RuntimeError("This object has already collected its data")
444 self
.collected
= True
445 self
.now
= datetime
.datetime
.utcnow()
448 result
= self
.collect()
452 Will commit the collected data to the database.
454 # Make sure that the RRD database has been created
457 # Write everything to disk that is in the write queue
458 self
.collecty
.write_queue
.commit_file(self
.file)
461 class GraphTemplate(object):
462 # A unique name to identify this graph template.
465 # Headline of the graph image
468 # Vertical label of the graph
469 graph_vertical_label
= None
475 # Instructions how to create the graph.
478 # Extra arguments passed to rrdgraph.
490 # Default dimensions for this graph
491 height
= GRAPH_DEFAULT_HEIGHT
492 width
= GRAPH_DEFAULT_WIDTH
494 def __init__(self
, plugin
, object_id
, locale
=None, timezone
=None):
497 # Save localisation parameters
498 self
.locale
= locales
.get(locale
)
499 self
.timezone
= timezone
501 # Get all required RRD objects
502 self
.object_id
= object_id
504 # Get the main object
505 self
.object = self
.get_object(self
.object_id
)
508 return "<%s>" % self
.__class
__.__name
__
512 return self
.plugin
.collecty
516 return self
.plugin
.log
518 def _make_command_line(self
, interval
, format
=DEFAULT_IMAGE_FORMAT
,
519 width
=None, height
=None, with_title
=True):
522 args
+= GRAPH_DEFAULT_ARGUMENTS
525 "--imgformat", format
,
526 "--height", "%s" % (height
or self
.height
),
527 "--width", "%s" % (width
or self
.width
),
530 args
+= self
.rrd_graph_args
533 if with_title
and self
.graph_title
:
534 args
+= ["--title", self
.graph_title
]
537 if self
.graph_vertical_label
:
538 args
+= ["--vertical-label", self
.graph_vertical_label
]
540 if self
.lower_limit
is not None or self
.upper_limit
is not None:
541 # Force to honour the set limits
542 args
.append("--rigid")
544 if self
.lower_limit
is not None:
545 args
+= ["--lower-limit", self
.lower_limit
]
547 if self
.upper_limit
is not None:
548 args
+= ["--upper-limit", self
.upper_limit
]
551 interval
= self
.intervals
[interval
]
553 interval
= "end-%s" % interval
556 args
+= ["--start", interval
]
560 def get_object(self
, *args
, **kwargs
):
561 return self
.plugin
.get_object(*args
, **kwargs
)
563 def get_object_table(self
):
565 "file" : self
.object,
569 def object_table(self
):
570 if not hasattr(self
, "_object_table"):
571 self
._object
_table
= self
.get_object_table()
573 return self
._object
_table
575 def get_object_files(self
):
578 for id, obj
in self
.object_table
.items():
583 def generate_graph(self
, interval
=None, **kwargs
):
584 # Make sure that all collected data is in the database
585 # to get a recent graph image
589 args
= self
._make
_command
_line
(interval
, **kwargs
)
591 self
.log
.info(_("Generating graph %s") % self
)
592 self
.log
.debug(" args: %s" % args
)
594 object_files
= self
.get_object_files()
596 for item
in self
.rrd_graph
:
598 args
.append(item
% object_files
)
602 self
.log
.debug(" %s" % args
[-1])
604 # Convert arguments to string
605 args
= [str(e
) for e
in args
]
607 with
Environment(self
.timezone
, self
.locale
.lang
):
608 graph
= rrdtool
.graphv("-", *args
)
611 "image" : graph
.get("image"),
612 "image_height" : graph
.get("image_height"),
613 "image_width" : graph
.get("image_width"),
616 def graph_info(self
):
618 Returns a dictionary with useful information
622 "title" : self
.graph_title
,
623 "object_id" : self
.object_id
or "",
624 "template" : self
.name
,