diff --git a/graphping b/graphping index 0776056..733d76b 100755 --- a/graphping +++ b/graphping @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # # Copyright 2013 Martijn Grendelman # @@ -23,7 +23,6 @@ Requires: fping, python-daemon import sys import os import signal -import daemon import subprocess import re import logging @@ -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'") @@ -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): """ @@ -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) @@ -115,29 +100,48 @@ 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 [ ...]" + print("Usage: ping-graphite [ ...]") 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 = {} @@ -145,14 +149,14 @@ class App: 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 @@ -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 @@ -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,