collecty: Update to version 004

Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
This commit is contained in:
Michael Tremer 2015-12-15 14:46:25 +00:00
parent 65c47a0107
commit 5fc517c0e5
3 changed files with 2 additions and 1053 deletions

View File

@ -4,8 +4,8 @@
###############################################################################
name = collecty
version = 003
release = 4
version = 004
release = 1
maintainer = Michael Tremer <michael.tremer@ipfire.org>
groups = System/Monitoring

View File

@ -1,56 +0,0 @@
From a9af411f0703eac939e0df5d5f75b46d35f531bc Mon Sep 17 00:00:00 2001
From: Michael Tremer <michael.tremer@ipfire.org>
Date: Mon, 29 Jun 2015 20:44:18 +0000
Subject: [PATCH 1/2] plugins: Automatically replace None by NaN
rrdtool uses NaN to represent no value. Python uses None.
This patch automatically translates from None to NaN.
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
---
src/collecty/plugins/base.py | 22 ++++++++++++++++++++--
1 file changed, 20 insertions(+), 2 deletions(-)
diff --git a/src/collecty/plugins/base.py b/src/collecty/plugins/base.py
index bed461f..cf9c3b4 100644
--- a/src/collecty/plugins/base.py
+++ b/src/collecty/plugins/base.py
@@ -147,8 +147,7 @@ class Plugin(object, metaclass=PluginRegistration):
try:
result = o.collect()
- if isinstance(result, tuple) or isinstance(result, list):
- result = ":".join(("%s" % e for e in result))
+ result = self._format_result(result)
except:
self.log.warning(_("Unhandled exception in %s.collect()") % o, exc_info=True)
continue
@@ -170,6 +169,25 @@ class Plugin(object, metaclass=PluginRegistration):
if delay >= 60:
self.log.warning(_("A worker thread was stalled for %.4fs") % delay)
+ @staticmethod
+ def _format_result(result):
+ if not isinstance(result, tuple) and not isinstance(result, list):
+ return result
+
+ # Replace all Nones by NaN
+ s = []
+
+ for e in result:
+ if e is None:
+ e = "NaN"
+
+ # Format as string
+ e = "%s" % e
+
+ s.append(e)
+
+ return ":".join(s)
+
def get_object(self, id):
for object in self.objects:
if not object.id == id:
--
1.8.1

View File

@ -1,995 +0,0 @@
From 63f9f8beed445a80dcb492570b105c5b50e65a59 Mon Sep 17 00:00:00 2001
From: Michael Tremer <michael.tremer@ipfire.org>
Date: Mon, 29 Jun 2015 20:49:02 +0000
Subject: [PATCH 2/2] latency: Rewrite latency module
This patch replaces the builtin python implementation
that pinged hosts by a Python C module that uses liboping.
liboping is able to ping IPv6 hosts as well and should
implement the ICMP protocol more appropriately.
The graph has been extended so that hosts will have a
line for latency over IPv6 and IPv4 if available and
the packet loss is merged from both protocols, too.
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
---
Makefile.am | 5 +-
configure.ac | 2 +
po/POTFILES.in | 1 -
src/_collectymodule.c | 338 ++++++++++++++++++++++++++++++++++++++++
src/collecty/ping.py | 324 --------------------------------------
src/collecty/plugins/latency.py | 151 +++++++++++-------
6 files changed, 437 insertions(+), 384 deletions(-)
delete mode 100644 src/collecty/ping.py
diff --git a/Makefile.am b/Makefile.am
index fe00da7..0b7e299 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -79,8 +79,7 @@ collecty_PYTHON = \
src/collecty/daemon.py \
src/collecty/errors.py \
src/collecty/i18n.py \
- src/collecty/logger.py \
- src/collecty/ping.py
+ src/collecty/logger.py
collectydir = $(pythondir)/collecty
@@ -109,6 +108,7 @@ _collecty_la_SOURCES = \
_collecty_la_CFLAGS = \
$(AM_CFLAGS) \
$(LIBATASMART_CFLAGS) \
+ $(OPING_CFLAGS) \
$(PYTHON_CFLAGS)
_collecty_la_LDFLAGS = \
@@ -119,6 +119,7 @@ _collecty_la_LDFLAGS = \
_collecty_la_LIBADD = \
$(LIBATASMART_LIBS) \
+ $(OPING_LIBS) \
$(PYTHON_LIBS) \
$(SENSORS_LIBS)
diff --git a/configure.ac b/configure.ac
index 59250ca..9b540fd 100644
--- a/configure.ac
+++ b/configure.ac
@@ -61,6 +61,8 @@ AC_PROG_GCC_TRADITIONAL
AC_PATH_PROG([XSLTPROC], [xsltproc])
+PKG_CHECK_MODULES([OPING], [liboping])
+
# Python
AM_PATH_PYTHON([3.2])
PKG_CHECK_MODULES([PYTHON], [python-${PYTHON_VERSION}])
diff --git a/po/POTFILES.in b/po/POTFILES.in
index f6aebb3..a96f7b2 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -5,7 +5,6 @@ src/collecty/daemon.py
src/collecty/errors.py
src/collecty/i18n.py
src/collecty/__init__.py
-src/collecty/ping.py
src/collecty/plugins/base.py
src/collecty/plugins/conntrack.py
src/collecty/plugins/cpu.py
diff --git a/src/_collectymodule.c b/src/_collectymodule.c
index 422c27d..c13ca69 100644
--- a/src/_collectymodule.c
+++ b/src/_collectymodule.c
@@ -22,15 +22,21 @@
#include <errno.h>
#include <fcntl.h>
#include <linux/hdreg.h>
+#include <oping.h>
#include <sensors/error.h>
#include <sensors/sensors.h>
#include <stdbool.h>
#include <string.h>
#include <sys/ioctl.h>
+#include <time.h>
#define MODEL_SIZE 40
#define SERIAL_SIZE 20
+#define PING_HISTORY_SIZE 1024
+#define PING_DEFAULT_COUNT 10
+#define PING_DEFAULT_TIMEOUT 8
+
typedef struct {
PyObject_HEAD
char* path;
@@ -313,6 +319,324 @@ static PyTypeObject BlockDeviceType = {
BlockDevice_new, /* tp_new */
};
+static PyObject* PyExc_PingError;
+static PyObject* PyExc_PingAddHostError;
+
+typedef struct {
+ PyObject_HEAD
+ pingobj_t* ping;
+ const char* host;
+ struct {
+ double history[PING_HISTORY_SIZE];
+ size_t history_index;
+ size_t history_size;
+ size_t packets_sent;
+ size_t packets_rcvd;
+ double average;
+ double stddev;
+ double loss;
+ } stats;
+} PingObject;
+
+static void Ping_dealloc(PingObject* self) {
+ if (self->ping)
+ ping_destroy(self->ping);
+
+ Py_TYPE(self)->tp_free((PyObject*)self);
+}
+
+static void Ping_init_stats(PingObject* self) {
+ self->stats.history_index = 0;
+ self->stats.history_size = 0;
+ self->stats.packets_sent = 0;
+ self->stats.packets_rcvd = 0;
+
+ self->stats.average = 0.0;
+ self->stats.stddev = 0.0;
+ self->stats.loss = 0.0;
+}
+
+static PyObject* Ping_new(PyTypeObject* type, PyObject* args, PyObject* kwds) {
+ PingObject* self = (PingObject*)type->tp_alloc(type, 0);
+
+ if (self) {
+ self->ping = NULL;
+ self->host = NULL;
+
+ Ping_init_stats(self);
+ }
+
+ return (PyObject*)self;
+}
+
+static int Ping_init(PingObject* self, PyObject* args, PyObject* kwds) {
+ char* kwlist[] = {"host", "family", "timeout", "ttl", NULL};
+ int family = PING_DEF_AF;
+ double timeout = PING_DEFAULT_TIMEOUT;
+ int ttl = PING_DEF_TTL;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|idi", kwlist, &self->host,
+ &family, &timeout, &ttl))
+ return -1;
+
+ if (family != AF_UNSPEC && family != AF_INET6 && family != AF_INET) {
+ PyErr_Format(PyExc_ValueError, "Family must be AF_UNSPEC, AF_INET6, or AF_INET");
+ return -1;
+ }
+
+ if (timeout < 0) {
+ PyErr_Format(PyExc_ValueError, "Timeout must be greater than zero");
+ return -1;
+ }
+
+ if (ttl < 1 || ttl > 255) {
+ PyErr_Format(PyExc_ValueError, "TTL must be between 1 and 255");
+ return -1;
+ }
+
+ self->ping = ping_construct();
+ if (!self->ping) {
+ return -1;
+ }
+
+ // Set options
+ int r;
+
+ r = ping_setopt(self->ping, PING_OPT_AF, &family);
+ if (r) {
+ PyErr_Format(PyExc_RuntimeError, "Could not set address family: %s",
+ ping_get_error(self->ping));
+ return -1;
+ }
+
+ if (timeout > 0) {
+ r = ping_setopt(self->ping, PING_OPT_TIMEOUT, &timeout);
+
+ if (r) {
+ PyErr_Format(PyExc_RuntimeError, "Could not set timeout: %s",
+ ping_get_error(self->ping));
+ return -1;
+ }
+ }
+
+ r = ping_setopt(self->ping, PING_OPT_TTL, &ttl);
+ if (r) {
+ PyErr_Format(PyExc_RuntimeError, "Could not set TTL: %s",
+ ping_get_error(self->ping));
+ return -1;
+ }
+
+ return 0;
+}
+
+static double Ping_compute_average(PingObject* self) {
+ assert(self->stats.packets_rcvd > 0);
+
+ double total_latency = 0.0;
+
+ for (int i = 0; i < self->stats.history_size; i++) {
+ if (self->stats.history[i] > 0)
+ total_latency += self->stats.history[i];
+ }
+
+ return total_latency / self->stats.packets_rcvd;
+}
+
+static double Ping_compute_stddev(PingObject* self, double mean) {
+ assert(self->stats.packets_rcvd > 0);
+
+ double deviation = 0.0;
+
+ for (int i = 0; i < self->stats.history_size; i++) {
+ if (self->stats.history[i] > 0) {
+ deviation += pow(self->stats.history[i] - mean, 2);
+ }
+ }
+
+ // Normalise
+ deviation /= self->stats.packets_rcvd;
+
+ return sqrt(deviation);
+}
+
+static void Ping_compute_stats(PingObject* self) {
+ // Compute the average latency
+ self->stats.average = Ping_compute_average(self);
+
+ // Compute the standard deviation
+ self->stats.stddev = Ping_compute_stddev(self, self->stats.average);
+
+ // Compute lost packets
+ self->stats.loss = 1.0;
+ self->stats.loss -= (double)self->stats.packets_rcvd \
+ / (double)self->stats.packets_sent;
+}
+
+static double time_elapsed(struct timeval* t0) {
+ struct timeval now;
+ gettimeofday(&now, NULL);
+
+ double r = now.tv_sec - t0->tv_sec;
+ r += ((double)now.tv_usec / 1000000) - ((double)t0->tv_usec / 1000000);
+
+ return r;
+}
+
+static PyObject* Ping_ping(PingObject* self, PyObject* args, PyObject* kwds) {
+ char* kwlist[] = {"count", "deadline", NULL};
+ size_t count = PING_DEFAULT_COUNT;
+ double deadline = 0;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Id", kwlist, &count, &deadline))
+ return NULL;
+
+ int r = ping_host_add(self->ping, self->host);
+ if (r) {
+ PyErr_Format(PyExc_PingAddHostError, "Could not add host %s: %s",
+ self->host, ping_get_error(self->ping));
+ return NULL;
+ }
+
+ // Reset all collected statistics in case ping() is called more than once.
+ Ping_init_stats(self);
+
+ // Save start time
+ struct timeval time_start;
+ r = gettimeofday(&time_start, NULL);
+ if (r) {
+ PyErr_Format(PyExc_RuntimeError, "Could not determine start time");
+ return NULL;
+ }
+
+ // Do the pinging
+ while (count--) {
+ self->stats.packets_sent++;
+
+ Py_BEGIN_ALLOW_THREADS
+ r = ping_send(self->ping);
+ Py_END_ALLOW_THREADS
+
+ // Count recieved packets
+ if (r >= 0) {
+ self->stats.packets_rcvd += r;
+
+ // Raise any errors
+ } else {
+ PyErr_Format(PyExc_RuntimeError, "Error executing ping_send(): %s",
+ ping_get_error(self->ping));
+ return NULL;
+ }
+
+ // Extract all data
+ pingobj_iter_t* iter = ping_iterator_get(self->ping);
+
+ double* latency = &self->stats.history[self->stats.history_index];
+ size_t buffer_size = sizeof(latency);
+ ping_iterator_get_info(iter, PING_INFO_LATENCY, latency, &buffer_size);
+
+ // Increase the history pointer
+ self->stats.history_index++;
+ self->stats.history_index %= sizeof(self->stats.history);
+
+ // Increase the history size
+ if (self->stats.history_size < sizeof(self->stats.history))
+ self->stats.history_size++;
+
+ // Check if the deadline is due
+ if (deadline > 0) {
+ double elapsed_time = time_elapsed(&time_start);
+
+ // If we have run longer than the deadline is, we end the main loop
+ if (elapsed_time >= deadline)
+ break;
+ }
+ }
+
+ if (self->stats.packets_rcvd == 0) {
+ PyErr_Format(PyExc_PingError, "No replies received");
+ return NULL;
+ }
+
+ Ping_compute_stats(self);
+
+ Py_RETURN_NONE;
+}
+
+static PyObject* Ping_get_packets_sent(PingObject* self) {
+ return PyLong_FromUnsignedLong(self->stats.packets_sent);
+}
+
+static PyObject* Ping_get_packets_rcvd(PingObject* self) {
+ return PyLong_FromUnsignedLong(self->stats.packets_rcvd);
+}
+
+static PyObject* Ping_get_average(PingObject* self) {
+ return PyFloat_FromDouble(self->stats.average);
+}
+
+static PyObject* Ping_get_stddev(PingObject* self) {
+ return PyFloat_FromDouble(self->stats.stddev);
+}
+
+static PyObject* Ping_get_loss(PingObject* self) {
+ return PyFloat_FromDouble(self->stats.loss);
+}
+
+static PyGetSetDef Ping_getsetters[] = {
+ {"average", (getter)Ping_get_average, NULL, NULL, NULL},
+ {"loss", (getter)Ping_get_loss, NULL, NULL, NULL},
+ {"stddev", (getter)Ping_get_stddev, NULL, NULL, NULL},
+ {"packets_sent", (getter)Ping_get_packets_sent, NULL, NULL, NULL},
+ {"packets_rcvd", (getter)Ping_get_packets_rcvd, NULL, NULL, NULL},
+ {NULL}
+};
+
+static PyMethodDef Ping_methods[] = {
+ {"ping", (PyCFunction)Ping_ping, METH_VARARGS|METH_KEYWORDS, NULL},
+ {NULL}
+};
+
+static PyTypeObject PingType = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ "_collecty.Ping", /*tp_name*/
+ sizeof(PingObject), /*tp_basicsize*/
+ 0, /*tp_itemsize*/
+ (destructor)Ping_dealloc, /*tp_dealloc*/
+ 0, /*tp_print*/
+ 0, /*tp_getattr*/
+ 0, /*tp_setattr*/
+ 0, /*tp_compare*/
+ 0, /*tp_repr*/
+ 0, /*tp_as_number*/
+ 0, /*tp_as_sequence*/
+ 0, /*tp_as_mapping*/
+ 0, /*tp_hash */
+ 0, /*tp_call*/
+ 0, /*tp_str*/
+ 0, /*tp_getattro*/
+ 0, /*tp_setattro*/
+ 0, /*tp_as_buffer*/
+ Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /*tp_flags*/
+ "Ping object", /* tp_doc */
+ 0, /* tp_traverse */
+ 0, /* tp_clear */
+ 0, /* tp_richcompare */
+ 0, /* tp_weaklistoffset */
+ 0, /* tp_iter */
+ 0, /* tp_iternext */
+ Ping_methods, /* tp_methods */
+ 0, /* tp_members */
+ Ping_getsetters, /* tp_getset */
+ 0, /* tp_base */
+ 0, /* tp_dict */
+ 0, /* tp_descr_get */
+ 0, /* tp_descr_set */
+ 0, /* tp_dictoffset */
+ (initproc)Ping_init, /* tp_init */
+ 0, /* tp_alloc */
+ Ping_new, /* tp_new */
+};
+
typedef struct {
PyObject_HEAD
const sensors_chip_name* chip;
@@ -743,6 +1067,9 @@ PyMODINIT_FUNC PyInit__collecty(void) {
if (PyType_Ready(&BlockDeviceType) < 0)
return NULL;
+ if (PyType_Ready(&PingType) < 0)
+ return NULL;
+
if (PyType_Ready(&SensorType) < 0)
return NULL;
@@ -751,6 +1078,17 @@ PyMODINIT_FUNC PyInit__collecty(void) {
Py_INCREF(&BlockDeviceType);
PyModule_AddObject(m, "BlockDevice", (PyObject*)&BlockDeviceType);
+ Py_INCREF(&PingType);
+ PyModule_AddObject(m, "Ping", (PyObject*)&PingType);
+
+ PyExc_PingError = PyErr_NewException("_collecty.PingError", NULL, NULL);
+ Py_INCREF(PyExc_PingError);
+ PyModule_AddObject(m, "PingError", PyExc_PingError);
+
+ PyExc_PingAddHostError = PyErr_NewException("_collecty.PingAddHostError", NULL, NULL);
+ Py_INCREF(PyExc_PingAddHostError);
+ PyModule_AddObject(m, "PingAddHostError", PyExc_PingAddHostError);
+
Py_INCREF(&SensorType);
PyModule_AddObject(m, "Sensor", (PyObject*)&SensorType);
diff --git a/src/collecty/ping.py b/src/collecty/ping.py
deleted file mode 100644
index e2d7970..0000000
--- a/src/collecty/ping.py
+++ /dev/null
@@ -1,324 +0,0 @@
-#!/usr/bin/python3
-
-import array
-import math
-import os
-import random
-import select
-import socket
-import struct
-import sys
-import time
-
-ICMP_TYPE_ECHO_REPLY = 0
-ICMP_TYPE_ECHO_REQUEST = 8
-ICMP_MAX_RECV = 2048
-
-MAX_SLEEP = 1000
-
-class PingError(Exception):
- msg = None
-
-
-class PingResolveError(PingError):
- msg = "Could not resolve hostname"
-
-
-class Ping(object):
- def __init__(self, destination, timeout=1000, packet_size=56):
- self.destination = self._resolve(destination)
- self.timeout = timeout
- self.packet_size = packet_size
-
- self.id = os.getpid() & 0xffff # XXX ? Is this a good idea?
-
- self.seq_number = 0
-
- # Number of sent packets.
- self.send_count = 0
-
- # Save the delay of all responses.
- self.times = []
-
- def run(self, count=None, deadline=None):
- while True:
- delay = self.do()
-
- self.seq_number += 1
-
- if count and self.seq_number >= count:
- break
-
- if deadline and self.total_time >= deadline:
- break
-
- if delay == None:
- delay = 0
-
- if MAX_SLEEP > delay:
- time.sleep((MAX_SLEEP - delay) / 1000)
-
- def do(self):
- s = None
- try:
- # Open a socket for ICMP communication.
- s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.getprotobyname("icmp"))
-
- # Send one packet.
- send_time = self.send_icmp_echo_request(s)
-
- # Increase number of sent packets (even if it could not be sent).
- self.send_count += 1
-
- # If the packet could not be sent, we may stop here.
- if send_time is None:
- return
-
- # Wait for the reply.
- receive_time, packet_size, ip, ip_header, icmp_header = self.receive_icmp_echo_reply(s)
-
- finally:
- # Close the socket.
- if s:
- s.close()
-
- # If a packet has been received...
- if receive_time:
- delay = (receive_time - send_time) * 1000
- self.times.append(delay)
-
- return delay
-
- def send_icmp_echo_request(self, s):
- # Header is type (8), code (8), checksum (16), id (16), sequence (16)
- checksum = 0
-
- # Create a header with checksum == 0.
- header = struct.pack("!BBHHH", ICMP_TYPE_ECHO_REQUEST, 0,
- checksum, self.id, self.seq_number)
-
- # Get some bytes for padding.
- padding = os.urandom(self.packet_size)
-
- # Calculate the checksum for header + padding data.
- checksum = self._calculate_checksum(header + padding)
-
- # Rebuild the header with the new checksum.
- header = struct.pack("!BBHHH", ICMP_TYPE_ECHO_REQUEST, 0,
- checksum, self.id, self.seq_number)
-
- # Build the packet.
- packet = header + padding
-
- # Save the time when the packet has been sent.
- send_time = time.time()
-
- # Send the packet.
- try:
- s.sendto(packet, (self.destination, 0))
- except socket.error as e:
- if e.errno == 1: # Operation not permitted
- # The packet could not be sent, probably because of
- # wrong firewall settings.
- return
-
- return send_time
-
- def receive_icmp_echo_reply(self, s):
- timeout = self.timeout / 1000.0
-
- # Wait until the reply packet arrived or until we hit timeout.
- while True:
- select_start = time.time()
-
- inputready, outputready, exceptready = select.select([s], [], [], timeout)
- select_duration = (time.time() - select_start)
-
- if inputready == []: # Timeout
- return None, 0, 0, 0, 0
-
- # Save the time when the packet has been received.
- receive_time = time.time()
-
- # Read the packet from the socket.
- packet_data, address = s.recvfrom(ICMP_MAX_RECV)
-
- # Parse the ICMP header.
- icmp_header = self._header2dict(
- ["type", "code", "checksum", "packet_id", "seq_number"],
- "!BBHHH", packet_data[20:28]
- )
-
- # This is the reply to our packet if the ID matches.
- if icmp_header["packet_id"] == self.id:
- # Parse the IP header.
- ip_header = self._header2dict(
- ["version", "type", "length", "id", "flags",
- "ttl", "protocol", "checksum", "src_ip", "dst_ip"],
- "!BBHHHBBHII", packet_data[:20]
- )
-
- packet_size = len(packet_data) - 28
- ip = socket.inet_ntoa(struct.pack("!I", ip_header["src_ip"]))
-
- return receive_time, packet_size, ip, ip_header, icmp_header
-
- # Check if the timeout has already been hit.
- timeout = timeout - select_duration
- if timeout <= 0:
- return None, 0, 0, 0, 0
-
- def _header2dict(self, names, struct_format, data):
- """
- Unpack tghe raw received IP and ICMP header informations to a dict
- """
- unpacked_data = struct.unpack(struct_format, data)
- return dict(list(zip(names, unpacked_data)))
-
- def _calculate_checksum(self, source_string):
- if len(source_string) % 2:
- source_string += "\x00"
-
- converted = array.array("H", source_string)
- if sys.byteorder == "big":
- converted.byteswap()
-
- val = sum(converted)
-
- # Truncate val to 32 bits (a variance from ping.c, which uses signed
- # integers, but overflow is unlinkely in ping).
- val &= 0xffffffff
-
- # Add high 16 bits to low 16 bits.
- val = (val >> 16) + (val & 0xffff)
-
- # Add carry from above (if any).
- val += (val >> 16)
-
- # Invert and truncate to 16 bits.
- answer = ~val & 0xffff
-
- return socket.htons(answer)
-
- def _resolve(self, host):
- """
- Resolve host.
- """
- if self._is_valid_ipv4_address(host):
- return host
-
- try:
- return socket.gethostbyname(host)
- except socket.gaierror as e:
- if e.errno == -3:
- raise PingResolveError
-
- raise
-
- def _is_valid_ipv4_address(self, addr):
- """
- Check addr to be a valid IPv4 address.
- """
- parts = addr.split(".")
-
- if not len(parts) == 4:
- return False
-
- for part in parts:
- try:
- number = int(part)
- except ValueError:
- return False
-
- if number > 255:
- return False
-
- return True
-
- @property
- def receive_count(self):
- """
- The number of received packets.
- """
- return len(self.times)
-
- @property
- def total_time(self):
- """
- The total time of all roundtrips.
- """
- try:
- return sum(self.times)
- except ValueError:
- return
-
- @property
- def min_time(self):
- """
- The smallest roundtrip time.
- """
- try:
- return min(self.times)
- except ValueError:
- return
-
- @property
- def max_time(self):
- """
- The biggest roundtrip time.
- """
- try:
- return max(self.times)
- except ValueError:
- return
-
- @property
- def avg_time(self):
- """
- Calculate the average response time.
- """
- try:
- return self.total_time / self.receive_count
- except ZeroDivisionError:
- return
-
- @property
- def variance(self):
- """
- Calculate the variance of all roundtrips.
- """
- if self.avg_time is None:
- return
-
- var = 0
-
- for t in self.times:
- var += (t - self.avg_time) ** 2
-
- var /= self.receive_count
- return var
-
- @property
- def stddev(self):
- """
- Standard deviation of all roundtrips.
- """
- return math.sqrt(self.variance)
-
- @property
- def loss(self):
- """
- Outputs the percentage of dropped packets.
- """
- dropped = self.send_count - self.receive_count
-
- return dropped / self.send_count
-
-
-if __name__ == "__main__":
- p = Ping("ping.ipfire.org")
- p.run(count=5)
-
- print("Min/Avg/Max/Stddev: %.2f/%.2f/%.2f/%.2f" % \
- (p.min_time, p.avg_time, p.max_time, p.stddev))
- print("Sent/Recv/Loss: %d/%d/%.2f" % (p.send_count, p.receive_count, p.loss))
diff --git a/src/collecty/plugins/latency.py b/src/collecty/plugins/latency.py
index df67102..a219240 100644
--- a/src/collecty/plugins/latency.py
+++ b/src/collecty/plugins/latency.py
@@ -19,8 +19,9 @@
# #
###############################################################################
-import collecty.ping
+import socket
+import collecty._collecty
from . import base
from ..i18n import _
@@ -37,86 +38,124 @@ class GraphTemplateLatency(base.GraphTemplate):
@property
def rrd_graph(self):
return [
- "DEF:latency=%(file)s:latency:AVERAGE",
- "DEF:latency_loss=%(file)s:latency_loss:AVERAGE",
- "DEF:latency_stddev=%(file)s:latency_stddev:AVERAGE",
-
- # Compute loss in percentage.
- "CDEF:latency_ploss=latency_loss,100,*",
-
- # Compute standard deviation.
- "CDEF:stddev1=latency,latency_stddev,+",
- "CDEF:stddev2=latency,latency_stddev,-",
-
- "CDEF:l005=latency_ploss,0,5,LIMIT,UN,UNKN,INF,IF",
- "CDEF:l010=latency_ploss,5,10,LIMIT,UN,UNKN,INF,IF",
- "CDEF:l025=latency_ploss,10,25,LIMIT,UN,UNKN,INF,IF",
- "CDEF:l050=latency_ploss,25,50,LIMIT,UN,UNKN,INF,IF",
- "CDEF:l100=latency_ploss,50,100,LIMIT,UN,UNKN,INF,IF",
-
+ "DEF:latency6=%(file)s:latency6:AVERAGE",
+ "DEF:loss6=%(file)s:loss6:AVERAGE",
+ "DEF:stddev6=%(file)s:stddev6:AVERAGE",
+
+ "DEF:latency4=%(file)s:latency4:AVERAGE",
+ "DEF:loss4=%(file)s:loss4:AVERAGE",
+ "DEF:stddev4=%(file)s:stddev4:AVERAGE",
+
+ # Compute the biggest loss and convert into percentage
+ "CDEF:ploss=loss6,loss4,MAX,100,*",
+
+ # Compute standard deviation
+ "CDEF:stddevarea6=stddev6,2,*",
+ "CDEF:spacer6=latency6,stddev6,-",
+ "CDEF:stddevarea4=stddev4,2,*",
+ "CDEF:spacer4=latency4,stddev4,-",
+
+ "CDEF:l005=ploss,0,5,LIMIT,UN,UNKN,INF,IF",
+ "CDEF:l010=ploss,5,10,LIMIT,UN,UNKN,INF,IF",
+ "CDEF:l025=ploss,10,25,LIMIT,UN,UNKN,INF,IF",
+ "CDEF:l050=ploss,25,50,LIMIT,UN,UNKN,INF,IF",
+ "CDEF:l100=ploss,50,100,LIMIT,UN,UNKN,INF,IF",
+
+ "VDEF:latency6min=latency6,MINIMUM",
+ "VDEF:latency6max=latency6,MAXIMUM",
+ "VDEF:latency6avg=latency6,AVERAGE",
+ "VDEF:latency4min=latency4,MINIMUM",
+ "VDEF:latency4max=latency4,MAXIMUM",
+ "VDEF:latency4avg=latency4,AVERAGE",
+
+ "LINE1:latency6avg#00ff0066:%s" % _("Average latency (IPv6)"),
+ "LINE1:latency4avg#ff000066:%s\\r" % _("Average latency (IPv4)"),
+
+ "COMMENT:%s" % _("Packet Loss"),
"AREA:l005#ffffff:%s" % _("0-5%%"),
- "AREA:l010#000000:%s" % _("5-10%%"),
- "AREA:l025#ff0000:%s" % _("10-25%%"),
- "AREA:l050#00ff00:%s" % _("25-50%%"),
- "AREA:l100#0000ff:%s" % _("50-100%%") + "\\n",
-
- "LINE1:stddev1#00660088",
- "LINE1:stddev2#00660088",
-
- "LINE3:latency#ff0000:%s" % _("Latency"),
- "VDEF:latencymin=latency,MINIMUM",
- "VDEF:latencymax=latency,MAXIMUM",
- "VDEF:latencyavg=latency,AVERAGE",
- "GPRINT:latencymax:%12s\:" % _("Maximum") + " %6.2lf",
- "GPRINT:latencymin:%12s\:" % _("Minimum") + " %6.2lf",
- "GPRINT:latencyavg:%12s\:" % _("Average") + " %6.2lf\\n",
-
- "LINE1:latencyavg#000000:%s" % _("Average latency"),
+ "AREA:l010#cccccc:%s" % _("5-10%%"),
+ "AREA:l025#999999:%s" % _("10-25%%"),
+ "AREA:l050#666666:%s" % _("25-50%%"),
+ "AREA:l100#333333:%s" % _("50-100%%") + "\\r",
+
+ "COMMENT: \\n", # empty line
+
+ "AREA:spacer4",
+ "AREA:stddevarea4#ff000033:STACK",
+ "LINE2:latency4#ff0000:%s" % _("Latency (IPv4)"),
+ "GPRINT:latency4max:%12s\:" % _("Maximum") + " %6.2lf",
+ "GPRINT:latency4min:%12s\:" % _("Minimum") + " %6.2lf",
+ "GPRINT:latency4avg:%12s\:" % _("Average") + " %6.2lf\\n",
+
+ "AREA:spacer6",
+ "AREA:stddevarea6#00ff0033:STACK",
+ "LINE2:latency6#00ff00:%s" % _("Latency (IPv6)"),
+ "GPRINT:latency6max:%12s\:" % _("Maximum") + " %6.2lf",
+ "GPRINT:latency6min:%12s\:" % _("Minimum") + " %6.2lf",
+ "GPRINT:latency6avg:%12s\:" % _("Average") + " %6.2lf\\n",
]
@property
def graph_title(self):
- return _("Latency to %(host)s")
+ return _("Latency to %s") % self.object.hostname
@property
def graph_vertical_label(self):
return _("Milliseconds")
+ @property
+ def rrd_graph_args(self):
+ return [
+ "--legend-direction=bottomup",
+ ]
+
class LatencyObject(base.Object):
rrd_schema = [
- "DS:latency:GAUGE:0:U",
- "DS:latency_loss:GAUGE:0:100",
- "DS:latency_stddev:GAUGE:0:U",
+ "DS:latency6:GAUGE:0:U",
+ "DS:stddev6:GAUGE:0:U",
+ "DS:loss6:GAUGE:0:100",
+ "DS:latency4:GAUGE:0:U",
+ "DS:stddev4:GAUGE:0:U",
+ "DS:loss4:GAUGE:0:100",
]
def __repr__(self):
return "<%s %s>" % (self.__class__.__name__, self.hostname)
- def init(self, hostname, deadline=None):
+ def init(self, hostname):
self.hostname = hostname
- self.deadline = deadline
@property
def id(self):
return self.hostname
def collect(self):
- # Send up to five ICMP echo requests.
- try:
- ping = collecty.ping.Ping(destination=self.hostname, timeout=20000)
- ping.run(count=5, deadline=self.deadline)
+ result = []
+
+ for family in (socket.AF_INET6, socket.AF_INET):
+ try:
+ p = collecty._collecty.Ping(self.hostname, family=family)
+ p.ping(count=5, deadline=10)
+
+ result += (p.average, p.stddev, p.loss)
+
+ except collecty._collecty.PingAddHostError as e:
+ self.log.debug(_("Could not add host %(host)s for family %(family)s") \
+ % { "host" : self.hostname, "family" : family })
- except collecty.ping.PingError as e:
- self.log.warning(_("Could not run latency check for %(host)s: %(msg)s") \
- % { "host" : self.hostname, "msg" : e.msg })
- return
+ # No data available
+ result += (None, None, None)
+ continue
- return (
- "%.10f" % ping.avg_time,
- "%.10f" % ping.loss,
- "%.10f" % ping.stddev,
- )
+ except collecty._collecty.PingError as e:
+ self.log.warning(_("Could not run latency check for %(host)s: %(msg)s") \
+ % { "host" : self.hostname, "msg" : e })
+
+ # A hundred percent loss
+ result += (None, None, 1)
+
+ return result
class LatencyPlugin(base.Plugin):
@@ -127,7 +166,5 @@ class LatencyPlugin(base.Plugin):
@property
def objects(self):
- deadline = self.interval / len(PING_HOSTS)
-
for hostname in PING_HOSTS:
- yield LatencyObject(self, hostname, deadline=deadline)
+ yield LatencyObject(self, hostname)
--
1.8.1