Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 86 additions & 70 deletions graphping
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python3
#
# Copyright 2013 Martijn Grendelman <m@rtijn.net>
#
Expand All @@ -23,7 +23,6 @@ Requires: fping, python-daemon
import sys
import os
import signal
import daemon
import subprocess
import re
import logging
Expand All @@ -32,23 +31,14 @@ import errno
import time
import socket
import datetime
import statistics
import argparse
import tomllib
from threading import Event
from pprint import pprint, pformat

# number of ping packets to send
packets = 30

# Graphite
g_host = 'localhost'
g_port = 2003
g_prefix = 'ping'

# System
fping = '/usr/sbin/fping'
logfile = '/tmp/graphping.log'

def handle_signal (sig, _frame):
signals = dict((k, v) for v, k in signal.__dict__.iteritems() if v.startswith('SIG'))
app.logger.info ("Received signal (%s)" % signals[sig])
app.logger.info ("Received signal (%s)" % signal.Signals(sig).name)

if sig == signal.SIGUSR1:
app.logger.warning("Setting log level to 'DEBUG'")
Expand All @@ -62,7 +52,7 @@ def handle_signal (sig, _frame):
else:
app.logger.warning("No ping running.")
app.ping = False
app.stop = True
app.stop.set()

class StreamToLogger(object):
"""
Expand All @@ -80,23 +70,18 @@ class StreamToLogger(object):
class App:

def __init__(self):
self.g_host = g_host
self.g_port = g_port
self.g_prefix = g_prefix
self.logfile = logfile
self.daemonize = True
self.stop = False
self.stop = Event()
self.level = logging.INFO

def setup_logging(self):
os.umask(022)
os.umask(0o022)
self.logger = logging.getLogger()
self.logger.setLevel(self.level)
formatter = logging.Formatter('%(asctime)s [%(process)d] %(levelname)s: %(message)s','%Y-%m-%d %H:%M:%S')
try:
loghandler = logging.handlers.WatchedFileHandler(self.logfile)
except IOError:
print "Could not open logfile (%s)" % self.logfile
print("Could not open logfile (%s)" % self.logfile)
return False

loghandler.setFormatter(formatter)
Expand All @@ -115,44 +100,63 @@ class App:

return True

def median(self, alist):
srtd = sorted(alist) # returns a sorted copy
mid = len(alist)/2 # remember that integer division truncates
if len(alist) % 2 == 0: # take the avg of middle two
return (srtd[mid-1] + srtd[mid]) / 2.0
else:
return srtd[mid]

def process_options(self):
self.targets = sys.argv[1:]
if not self.targets:
self.usage()
parser = argparse.ArgumentParser(
prog='graphping',
description='Send ping statistics for multiple hosts to Graphite')
parser.add_argument('--host', default='localhost')
parser.add_argument('--port', type=int, default=2003)
parser.add_argument('--prefix', default='ping')
parser.add_argument('--daemon', action='store_true')
parser.add_argument('--fping', default='/usr/bin/fping')
parser.add_argument('--logfile', default='/tmp/graphping.log')
parser.add_argument('--packets', type=int, default=30)
parser.add_argument('--pktsize', type=int, default=1000)
parser.add_argument('--config', type=str)
parser.add_argument('target', nargs='+')
args = parser.parse_args()

if args.config:
with open(args.config, "rb") as f:
config = tomllib.load(f)
parser.set_defaults(**config)
args = parser.parse_args()

self.g_host = args.host
self.g_port = args.port
self.g_prefix = args.prefix
self.daemonize = args.daemon
self.fping = args.fping
self.packets = args.packets
self.pktsize = args.pktsize
self.logfile = args.logfile
self.targets = args.target

def usage(self):
print "Usage: ping-graphite <target> [<target> ...]"
print("Usage: ping-graphite <target> [<target> ...]")
sys.exit(2)

def main(self):

if self.setup_logging():
cmd = [ fping, '-c', str(packets) ] + self.targets
cmd = [ self.fping, '-c', str(self.packets), '-b', str(self.pktsize), '-p', '100', ] + self.targets

while not self.stop:
while not self.stop.is_set():

rtt = {}
sry = {}
for t in self.targets:
rtt[t] = []
sry[t] = {}

self.logger.info("Fpinging %d targets x %d packets" % (len(self.targets), packets))
self.logger.info("Fpinging %d targets x %d packets" % (len(self.targets), self.packets))
self.ping = subprocess.Popen(cmd, bufsize=256, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
while not self.stop:
while not self.stop.is_set():
# A USR1 signal causes an 'Interrupted system call' that we should
# handle gracefully
try:
raw_line = self.ping.stdout.readline()
except IOError, e:
raw_line = self.ping.stdout.readline().decode('UTF-8')
except IOError as e:
if e.errno == errno.EINTR:
continue

Expand Down Expand Up @@ -189,41 +193,52 @@ class App:
self.ping = False

# Escape
if self.stop:
if self.stop.is_set():
return

# Process and send the results
ts = time.time()
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((g_host, g_port))
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((self.g_host, self.g_port))

for target,summary in sry.items():
safe_target = target.replace('.', '_')
m_loss = self.g_prefix + '.' + safe_target + '.packetloss'
m_median = self.g_prefix + '.' + safe_target + '.medianrtt'
m_min = self.g_prefix + '.' + safe_target + '.minrtt'
m_avg = self.g_prefix + '.' + safe_target + '.avgrtt'
m_max = self.g_prefix + '.' + safe_target + '.maxrtt'
for target,summary in sry.items():
if not 'loss' in summary:
self.logger.info("No result for target %s" % target)
continue

try:
self.logger.info("Sending data for %s. ts=%d => %s" % (safe_target, ts,
datetime.datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S')))
s.send("%s %d %d\n" % (m_loss, summary['loss'], ts))
s.send("%s %.2f %d\n" % (m_median, self.median(rtt[target]), ts))
s.send("%s %.2f %d\n" % (m_min, summary['min'], ts))
s.send("%s %.2f %d\n" % (m_avg, summary['avg'], ts))
s.send("%s %.2f %d\n" % (m_max, summary['max'], ts))
except Exception:
self.logger.info("Could not send! Summary:")
self.logger.info(pformat(summary))

s.close()
safe_target = target.replace('.', '_')
m_loss = self.g_prefix + '.' + safe_target + '.packetloss'
m_median = self.g_prefix + '.' + safe_target + '.medianrtt'
m_stdev = self.g_prefix + '.' + safe_target + '.stdevrtt'
m_min = self.g_prefix + '.' + safe_target + '.minrtt'
m_avg = self.g_prefix + '.' + safe_target + '.avgrtt'
m_max = self.g_prefix + '.' + safe_target + '.maxrtt'

try:
self.logger.info("Sending data for %s. ts=%d => %s" % (safe_target, ts,
datetime.datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S')))
s.send(("%s %d %d\n" % (m_loss, summary['loss'], ts)).encode('ASCII'))
s.send(("%s %.2f %d\n" % (m_min, summary['min'], ts)).encode('ASCII'))
s.send(("%s %.2f %d\n" % (m_avg, summary['avg'], ts)).encode('ASCII'))
s.send(("%s %.2f %d\n" % (m_max, summary['max'], ts)).encode('ASCII'))
s.send(("%s %.2f %d\n" % (m_median, statistics.median(rtt[target]), ts)).encode('ASCII'))
s.send(("%s %.2f %d\n" % (m_stdev, statistics.stdev(rtt[target]), ts)).encode('ASCII'))
except Exception as e:
self.logger.info("Could not send! Summary:")
self.logger.info(pformat(summary))

s.close()
except Exception as e:
self.logger.warning("Could not connect to Graphite host %s:%s: %s" % (
self.g_host,
self.g_port,
e))

# Wait for the next minute
self.logger.info("Sleeping")
m0 = time.strftime('%M')
while time.strftime('%M') == m0 and not self.stop:
time.sleep(3)
m0 = 60 - time.gmtime().tm_sec
self.logger.info("Sleeping for %d seconds" % m0)
self.stop.wait(m0)

return

Expand All @@ -233,6 +248,7 @@ if __name__ == "__main__":
app.process_options()

if app.daemonize == True:
import daemon
context = daemon.DaemonContext()
context.signal_map = {
signal.SIGTERM: handle_signal,
Expand Down