scripts/chrony_exporter.py (46 lines of code) (raw):

#!/usr/bin/env python3 import subprocess import time import re import os from prometheus_client import start_http_server, Gauge # Prometheus metric for clock error clock_error_gauge = Gauge('chrony_clock_error_ms', 'Clock error in milliseconds') # Get the sleep interval from an environment variable, default to 60 seconds SLEEP_INTERVAL = int(os.environ.get('SLEEP_INTERVAL', 60)) def parse_tracking_output(): """Parses `chronyc tracking` and calculates clock error.""" try: result = subprocess.run(['chronyc', 'tracking'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) output = result.stdout.decode('utf-8') last_offset = root_dispersion = root_delay = None for line in output.splitlines(): if line.startswith('Last offset'): match = re.search(r'([-+]?[0-9]*\.?[0-9]+)', line) if match: last_offset = abs(float(match.group(1))) elif line.startswith('Root dispersion'): match = re.search(r'([-+]?[0-9]*\.?[0-9]+)', line) if match: root_dispersion = float(match.group(1)) elif line.startswith('Root delay'): match = re.search(r'([-+]?[0-9]*\.?[0-9]+)', line) if match: root_delay = float(match.group(1)) if None not in (last_offset, root_dispersion, root_delay): clock_error_sec = last_offset + root_dispersion + (0.5 * root_delay) return clock_error_sec * 1000 # convert to ms except subprocess.CalledProcessError as e: print(f"chronyc error: {e}") except Exception as e: print(f"Unexpected error: {e}") return None def main(): start_http_server(9100) print("Exporter running at http://localhost:9100/metrics") while True: clock_error_ms = parse_tracking_output() if clock_error_ms is not None: clock_error_gauge.set(clock_error_ms) print(f"Updated clock error: {clock_error_ms:.3f} ms") else: print("Clock error computation failed.") time.sleep(SLEEP_INTERVAL) if __name__ == '__main__': main()