From c3357d578aafd1d89af207649f2942ecc9d533f4 Mon Sep 17 00:00:00 2001 From: Bernhard Schmidt Date: Fri, 3 May 2024 17:03:06 +0200 Subject: [PATCH 01/10] python3 compat --- graphping | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/graphping b/graphping index 0776056..1be20c1 100755 --- a/graphping +++ b/graphping @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # # Copyright 2013 Martijn Grendelman # @@ -89,14 +89,14 @@ class App: 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) @@ -117,7 +117,7 @@ class App: def median(self, alist): srtd = sorted(alist) # returns a sorted copy - mid = len(alist)/2 # remember that integer division truncates + 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: @@ -129,7 +129,7 @@ class App: self.usage() def usage(self): - print "Usage: ping-graphite [ ...]" + print("Usage: ping-graphite [ ...]") sys.exit(2) def main(self): @@ -151,8 +151,8 @@ class App: # 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 @@ -208,12 +208,12 @@ class App: 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: + s.send(("%s %d %d\n" % (m_loss, summary['loss'], ts)).encode('ASCII')) + s.send(("%s %.2f %d\n" % (m_median, self.median(rtt[target]), 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')) + except Exception as e: self.logger.info("Could not send! Summary:") self.logger.info(pformat(summary)) From dfdcebe2b697ff84abc3cb7e3066aadb134822ca Mon Sep 17 00:00:00 2001 From: Bernhard Schmidt Date: Fri, 3 May 2024 21:45:16 +0200 Subject: [PATCH 02/10] Only import daemon when we need it --- graphping | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphping b/graphping index 1be20c1..0f1d0c1 100755 --- a/graphping +++ b/graphping @@ -23,7 +23,6 @@ Requires: fping, python-daemon import sys import os import signal -import daemon import subprocess import re import logging @@ -233,6 +232,7 @@ if __name__ == "__main__": app.process_options() if app.daemonize == True: + import daemon context = daemon.DaemonContext() context.signal_map = { signal.SIGTERM: handle_signal, From 29bdb21a2e4b2e7f6188ebf1b535047ce397cd3f Mon Sep 17 00:00:00 2001 From: Bernhard Schmidt Date: Fri, 3 May 2024 21:46:00 +0200 Subject: [PATCH 03/10] py3: map signal number to name --- graphping | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/graphping b/graphping index 0f1d0c1..4d322bb 100755 --- a/graphping +++ b/graphping @@ -46,8 +46,7 @@ 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'") From e98d28338929bae316adc12d93468f47721360c8 Mon Sep 17 00:00:00 2001 From: Bernhard Schmidt Date: Fri, 3 May 2024 22:42:02 +0200 Subject: [PATCH 04/10] argparse and config file --- graphping | 60 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/graphping b/graphping index 4d322bb..8afc138 100755 --- a/graphping +++ b/graphping @@ -31,20 +31,10 @@ import errno import time import socket import datetime +import argparse +import tomllib 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): app.logger.info ("Received signal (%s)" % signal.Signals(sig).name) @@ -78,13 +68,14 @@ 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.g_host = 'localhost' + self.g_port = 2003 + self.g_prefix = 'ping' + self.logfile = None self.daemonize = True self.stop = False self.level = logging.INFO + self.packets = 30 def setup_logging(self): os.umask(0o022) @@ -122,9 +113,34 @@ class App: 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('--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.logfile = args.logfile + self.targets = args.target def usage(self): print("Usage: ping-graphite [ ...]") @@ -133,7 +149,7 @@ class App: def main(self): if self.setup_logging(): - cmd = [ fping, '-c', str(packets) ] + self.targets + cmd = [ self.fping, '-c', str(self.packets) ] + self.targets while not self.stop: @@ -143,7 +159,7 @@ 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: # A USR1 signal causes an 'Interrupted system call' that we should @@ -193,7 +209,7 @@ class App: # Process and send the results ts = time.time() s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.connect((g_host, g_port)) + s.connect((self.g_host, self.g_port)) for target,summary in sry.items(): safe_target = target.replace('.', '_') From 56453ceefee0a87480dd2152ab9ee7523ad87107 Mon Sep 17 00:00:00 2001 From: Bernhard Schmidt Date: Fri, 3 May 2024 22:45:48 +0200 Subject: [PATCH 05/10] Use statistics module to calculate median, calculate/send stdev as well --- graphping | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/graphping b/graphping index 8afc138..6ade63a 100755 --- a/graphping +++ b/graphping @@ -31,6 +31,7 @@ import errno import time import socket import datetime +import statistics import argparse import tomllib from pprint import pprint, pformat @@ -104,14 +105,6 @@ 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): parser = argparse.ArgumentParser( prog='graphping', @@ -215,6 +208,7 @@ class App: 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' @@ -223,7 +217,8 @@ class App: 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_median, self.median(rtt[target]), 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')) 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')) From 1821675db105f15a0a30d8f5dbf463c7a2d15716 Mon Sep 17 00:00:00 2001 From: Bernhard Schmidt Date: Fri, 3 May 2024 22:47:06 +0200 Subject: [PATCH 06/10] Remove duplicate variable initialization --- graphping | 6 ------ 1 file changed, 6 deletions(-) diff --git a/graphping b/graphping index 6ade63a..bdc8a0c 100755 --- a/graphping +++ b/graphping @@ -69,14 +69,8 @@ class StreamToLogger(object): class App: def __init__(self): - self.g_host = 'localhost' - self.g_port = 2003 - self.g_prefix = 'ping' - self.logfile = None - self.daemonize = True self.stop = False self.level = logging.INFO - self.packets = 30 def setup_logging(self): os.umask(0o022) From bc2617e2bda0eea64875ad92661acc2fe767cf7f Mon Sep 17 00:00:00 2001 From: Bernhard Schmidt Date: Fri, 3 May 2024 22:48:30 +0200 Subject: [PATCH 07/10] Make packet size configurable --- graphping | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/graphping b/graphping index bdc8a0c..b20f139 100755 --- a/graphping +++ b/graphping @@ -110,6 +110,7 @@ class App: 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() @@ -126,6 +127,7 @@ class App: self.daemonize = args.daemon self.fping = args.fping self.packets = args.packets + self.pktsize = args.pktsize self.logfile = args.logfile self.targets = args.target @@ -136,7 +138,7 @@ class App: def main(self): if self.setup_logging(): - cmd = [ self.fping, '-c', str(self.packets) ] + self.targets + cmd = [ self.fping, '-c', str(self.packets), '-b', str(self.pktsize), '-p', '100', ] + self.targets while not self.stop: From 202c0bdd5b3edfef89dd7a5cf1eb6f53a920d80c Mon Sep 17 00:00:00 2001 From: Bernhard Schmidt Date: Fri, 3 May 2024 23:00:37 +0200 Subject: [PATCH 08/10] Use threading.Event() for sleep/shutdown --- graphping | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/graphping b/graphping index b20f139..6554788 100755 --- a/graphping +++ b/graphping @@ -34,6 +34,7 @@ import datetime import statistics import argparse import tomllib +from threading import Event from pprint import pprint, pformat def handle_signal (sig, _frame): @@ -51,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): """ @@ -69,7 +70,7 @@ class StreamToLogger(object): class App: def __init__(self): - self.stop = False + self.stop = Event() self.level = logging.INFO def setup_logging(self): @@ -140,7 +141,7 @@ class App: if self.setup_logging(): 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 = {} @@ -150,7 +151,7 @@ class App: 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: @@ -192,7 +193,7 @@ class App: self.ping = False # Escape - if self.stop: + if self.stop.is_set(): return # Process and send the results @@ -225,10 +226,9 @@ class App: s.close() # 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 From e817ed09ca38d868032ec7ce2beddfe62cbe7e20 Mon Sep 17 00:00:00 2001 From: Bernhard Schmidt Date: Sun, 5 May 2024 11:20:57 +0200 Subject: [PATCH 09/10] Deal with targets not listed in results (i.e. DNS lookup errors) --- graphping | 60 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/graphping b/graphping index 6554788..2f773fc 100755 --- a/graphping +++ b/graphping @@ -198,32 +198,42 @@ class App: # Process and send the results ts = time.time() - 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_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: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect((self.g_host, self.g_port)) - 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_median, statistics.median(rtt[target]), ts)).encode('ASCII')) - s.send(("%s %.2f %d\n" % (m_stdev, statistics.stdev(rtt[target]), 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')) - except Exception as e: - self.logger.info("Could not send! Summary:") - self.logger.info(pformat(summary)) - - s.close() + for target,summary in sry.items(): + if not 'loss' in summary: + self.logger.info("No result for target %s" % target) + continue + + 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_median, statistics.median(rtt[target]), ts)).encode('ASCII')) + s.send(("%s %.2f %d\n" % (m_stdev, statistics.stdev(rtt[target]), 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')) + 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 m0 = 60 - time.gmtime().tm_sec From 12ba76521125bf8543fee4234051a92301e56d52 Mon Sep 17 00:00:00 2001 From: Bernhard Schmidt Date: Sun, 5 May 2024 11:24:27 +0200 Subject: [PATCH 10/10] Send calculated results last --- graphping | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphping b/graphping index 2f773fc..733d76b 100755 --- a/graphping +++ b/graphping @@ -219,11 +219,11 @@ class App: 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_median, statistics.median(rtt[target]), ts)).encode('ASCII')) - s.send(("%s %.2f %d\n" % (m_stdev, statistics.stdev(rtt[target]), 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))