From bde6e5d2905bfaa03c3399a2d11b74c902f3ec66 Mon Sep 17 00:00:00 2001 From: Ben Kochie Date: Fri, 10 Feb 2017 16:38:39 +0100 Subject: [PATCH] Add a textfile helper for NTPd. Parse the output of `ntpq -np` to provide metrics from a local NTP daemon. --- text_collector_examples/ntpd_metrics.py | 113 ++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100755 text_collector_examples/ntpd_metrics.py diff --git a/text_collector_examples/ntpd_metrics.py b/text_collector_examples/ntpd_metrics.py new file mode 100755 index 00000000..1da4edb8 --- /dev/null +++ b/text_collector_examples/ntpd_metrics.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# +# Description: Extract NTPd metrics from ntpq -np. +# Author: Ben Kochie + +import re +import subprocess +import sys + +# NTP peers status, with no DNS lookups. +ntpq_cmd = ['ntpq', '-np'] + +# Regex to match all of the fields in the output of ntpq -np +metrics_fields = [ + '^(?P.)(?P[\w\.]+)', + '(?P[\w\.]+)', + '(?P\d+)', + '(?P\w)', + '(?P\d+)', + '(?P\d+)', + '(?P\d+)', + '(?P\d+\.\d+)', + '(?P-?\d+\.\d+)', + '(?P\d+\.\d+)', +] +metrics_re = '\s+'.join(metrics_fields) + +# Remote types +# http://support.ntp.org/bin/view/Support/TroubleshootingNTP +remote_types = { + 'l': 'local', + 'u': 'unicast', + 'm': 'multicast', + 'b': 'broadcast', + '-': 'netaddr', +} + +# Status codes: +# http://www.eecis.udel.edu/~mills/ntp/html/decode.html#peer +status_types = { + ' ': 0, + 'x': 1, + '.': 2, + '-': 3, + '+': 4, + '#': 5, + '*': 6, + 'o': 7, +} + + +# Run the ntpq command. +def get_ntpq(): + try: + ntpq = subprocess.check_output(ntpq_cmd, stderr=subprocess.DEVNULL) + except subprocess.CalledProcessError as e: + return None + return ntpq.decode() + + +# Print metrics in Prometheus format. +def print_prometheus(metric, values): + print("# HELP ntpd_%s NTPd metric for %s" % (metric, metric)) + print("# TYPE ntpd_%s gauge" % (metric)) + for labels in values: + print("ntpd_%s{%s} %f" % (metric, labels, values[labels])) + + +# Parse raw ntpq lines. +def parse_line(line): + if re.match('\s+remote\s+refid', line): + return None + if re.match('=+', line): + return None + if re.match('.+\.(LOCL|POOL)\.', line): + return None + if re.match('^$', line): + return None + return re.match(metrics_re, line) + + +# Main function +def main(argv): + ntpq = get_ntpq() + peer_status_metrics = {} + delay_metrics = {} + offset_metrics = {} + jitter_metrics = {} + for line in ntpq.split('\n'): + metric_match = parse_line(line) + if metric_match is None: + continue + remote = metric_match.group('remote') + refid = metric_match.group('refid') + stratum = metric_match.group('stratum') + remote_type = remote_types[metric_match.group('type')] + common_labels = "remote=\"%s\",reference=\"%s\"" % (remote, refid) + peer_labels = "%s,stratum=\"%s\",type=\"%s\"" % (common_labels, stratum, remote_type) + + peer_status_metrics[peer_labels] = float(status_types[metric_match.group('status')]) + delay_metrics[common_labels] = float(metric_match.group('delay')) + offset_metrics[common_labels] = float(metric_match.group('offset')) + jitter_metrics[common_labels] = float(metric_match.group('jitter')) + + print_prometheus('peer_status', peer_status_metrics) + print_prometheus('delay_milliseconds', delay_metrics) + print_prometheus('offset_milliseconds', offset_metrics) + print_prometheus('jitter_milliseconds', jitter_metrics) + + +# Go go go! +if __name__ == "__main__": + main(sys.argv[1:])