latency: Rewrite latency module
authorMichael Tremer <michael.tremer@ipfire.org>
Mon, 29 Jun 2015 20:49:02 +0000 (20:49 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Mon, 29 Jun 2015 20:56:04 +0000 (20:56 +0000)
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
configure.ac
po/POTFILES.in
src/_collectymodule.c
src/collecty/ping.py [deleted file]
src/collecty/plugins/latency.py

index fe00da7..0b7e299 100644 (file)
@@ -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)
 
index 59250ca..9b540fd 100644 (file)
@@ -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}])
index f6aebb3..a96f7b2 100644 (file)
@@ -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
index 422c27d..c13ca69 100644 (file)
 #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 (file)
index e2d7970..0000000
+++ /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))
index df67102..a219240 100644 (file)
@@ -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)