From 95c07feb0743359bd407b20ee7e142ad15a090bd Mon Sep 17 00:00:00 2001 From: Paul Gear Date: Mon, 1 Jan 2024 12:04:38 +1000 Subject: [PATCH 01/14] Add version argument and supporting files --- Makefile | 4 ++-- src/check_ntpmon.py | 13 +++++++++++++ src/ntpmon.py | 11 +++++++++++ src/version.py | 9 +++++++++ src/version_data.py | 5 +++++ 5 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 src/version.py create mode 100644 src/version_data.py diff --git a/Makefile b/Makefile index 55b22bd..ce8104e 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ PREFIX=/usr/local SHAREDIR=share/$(NAME) SYSTEMD_SERVICE_DIR=/lib/systemd/system USER=$(NAME) -VERSION=3.0.5 +VERSION=$(shell python3 ./src/ntpmon.py --version) RELEASE=1 TESTS=\ @@ -30,7 +30,7 @@ datatest: PYTHONPATH=./src ./testdata/testdata.sh format: - black --line-length=128 --target-version=py39 src/ unit_tests/ + black --line-length=128 --target-version=py39 --exclude version_data.py src/ unit_tests/ push: git push github diff --git a/src/check_ntpmon.py b/src/check_ntpmon.py index 7967bd3..6ce1550 100755 --- a/src/check_ntpmon.py +++ b/src/check_ntpmon.py @@ -6,6 +6,8 @@ import argparse import sys +import version + from alert import NTPAlerter from peers import NTPPeers from process import ntpchecks @@ -35,7 +37,18 @@ def get_args(checks): action="store_true", help="Obtain peer stats on standard input instead of from running daemon.", ) + parser.add_argument( + "--version", + action="store_true", + default=False, + help="Print the check_ntpmon version and exit", + ) args = parser.parse_args() + + if args.version: + print(version.get_version()) + sys.exit(0) + return args diff --git a/src/ntpmon.py b/src/ntpmon.py index f974854..62b2f5e 100755 --- a/src/ntpmon.py +++ b/src/ntpmon.py @@ -14,6 +14,7 @@ import outputs import peer_stats import process +import version from tailer import Tailer @@ -70,8 +71,18 @@ def get_args() -> argparse.Namespace: help="TCP port on which to listen when acting as a prometheus exporter (default: 9648)", default=9648, ) + parser.add_argument( + "--version", + action="store_true", + default=False, + help="Print the ntpmon version and exit", + ) args = parser.parse_args() + if args.version: + print(version.get_version()) + sys.exit(0) + if "COLLECTD_INTERVAL" in os.environ: if args.interval is None: args.interval = float(os.environ["COLLECTD_INTERVAL"]) diff --git a/src/version.py b/src/version.py new file mode 100644 index 0000000..0b20ca1 --- /dev/null +++ b/src/version.py @@ -0,0 +1,9 @@ +# +# Copyright: (c) 2023 Paul D. Gear +# License: AGPLv3 + +import version_data + + +def get_version() -> str: + return ".".join((str(version_data.MAJOR), str(version_data.MINOR), str(version_data.PATCH))) diff --git a/src/version_data.py b/src/version_data.py new file mode 100644 index 0000000..961d287 --- /dev/null +++ b/src/version_data.py @@ -0,0 +1,5 @@ +# This file should be kept as-is to ensure that it can be parsed by both python +# and bash. +MAJOR="3" +MINOR="0" +PATCH="6" From 02fabde1158bbeab4627d3188872279253d8cbf6 Mon Sep 17 00:00:00 2001 From: Paul Gear Date: Mon, 1 Jan 2024 12:08:15 +1000 Subject: [PATCH 02/14] Fix python 3.8 compatibility with debug flag --- src/ntpmon.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/ntpmon.py b/src/ntpmon.py index 62b2f5e..fa017a4 100755 --- a/src/ntpmon.py +++ b/src/ntpmon.py @@ -39,7 +39,7 @@ def get_args() -> argparse.Namespace: ) parser.add_argument( "--debug", - action=argparse.BooleanOptionalAction, + action="store_true", help="Run in debug mode (default: True if standard output is a tty device)", default=sys.stdout.isatty(), ) @@ -65,6 +65,11 @@ def get_args() -> argparse.Namespace: type=str, help="Log file to follow for peer statistics, if different from the default", ) + parser.add_argument( + "--no-debug", + action="store_false", + dest="debug", + ) parser.add_argument( "--port", type=int, From c214d3537f8f600ae23cc20029fa9827538e5141 Mon Sep 17 00:00:00 2001 From: Paul Gear Date: Mon, 1 Jan 2024 12:09:39 +1000 Subject: [PATCH 03/14] Use peertype instead of type for individual peer metrics To ensure InfluxDB tag compatibility between `ntpmon_peer` and `ntpmon_peers` metrics. --- src/ntpmon.py | 4 ++-- src/peer_stats.py | 11 ++++++----- unit_tests/test_peer_stats.py | 6 +++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/ntpmon.py b/src/ntpmon.py index fa017a4..b9a9209 100755 --- a/src/ntpmon.py +++ b/src/ntpmon.py @@ -162,8 +162,8 @@ async def peer_stats_task(args: argparse.Namespace, output: outputs.Output) -> N for line in lines: stats = peer_stats.parse_measurement(line) if stats is not None: - if "type" not in stats: - stats["type"] = find_type(stats["source"], checkobjs["peers"].peers) + if "peertype" not in stats: + stats["peertype"] = find_type(stats["source"], checkobjs["peers"].peers) output.send_measurement(stats, debug=args.debug) diff --git a/src/peer_stats.py b/src/peer_stats.py index 2a94fdf..306fe8d 100755 --- a/src/peer_stats.py +++ b/src/peer_stats.py @@ -222,12 +222,13 @@ def mjd_to_timestamp(day: float, time: float) -> float: def extract_ntpd_status_word(status: str) -> dict: status_word = int(status, 16) >> 8 return { - "broadcast": bool(status_word & 0x08), - "reachable": bool(status_word & 0x10), - "authenticated": bool(status_word & 0x20), - "authentication_enabled": bool(status_word & 0x40), + # ordered from most significant to least significant bits "persistent": bool(status_word & 0x80), - "type": select_field[status_word & 0x07], + "authentication_enabled": bool(status_word & 0x40), + "authenticated": bool(status_word & 0x20), + "reachable": bool(status_word & 0x10), + "broadcast": bool(status_word & 0x08), + "peertype": select_field[status_word & 0x07], } diff --git a/unit_tests/test_peer_stats.py b/unit_tests/test_peer_stats.py index 99bef59..c72e19c 100644 --- a/unit_tests/test_peer_stats.py +++ b/unit_tests/test_peer_stats.py @@ -68,11 +68,11 @@ def test_parse_ntpd_peerstats() -> None: assert len(measurements) == len(lines) assert all([m is not None for m in measurements]) assert measurements[0]["reachable"] == True - assert measurements[0]["type"] == "survivor" + assert measurements[0]["peertype"] == "survivor" assert measurements[1]["datetime"] == datetime.datetime(2023, 12, 25, 8, 41, 56, 612000, tzinfo=datetime.timezone.utc) assert measurements[2]["offset"] > 0 - assert measurements[3]["type"] == "outlier" - assert measurements[5]["type"] == "sync" + assert measurements[3]["peertype"] == "outlier" + assert measurements[5]["peertype"] == "sync" assert measurements[10]["offset"] < 0 assert all([m["authenticated"] == False for m in measurements]) assert all([m["broadcast"] == False for m in measurements]) From cec38d7e494d062b4e4563664e0a5e0af6218218 Mon Sep 17 00:00:00 2001 From: Paul Gear Date: Mon, 1 Jan 2024 12:10:30 +1000 Subject: [PATCH 04/14] Fix data type on stratum metric for ntpd This was an integer under 2.x and should remain so --- src/readvar.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/readvar.py b/src/readvar.py index e0240af..4047972 100644 --- a/src/readvar.py +++ b/src/readvar.py @@ -44,6 +44,8 @@ def parse_ntpd_vars(vars): elif nameval[0] in _aliases: # convert from milliseconds to seconds, alias metrics[_aliases[nameval[0]]] = round(float(nameval[1]) / 1000.0, 9) + elif nameval[0] == "stratum": + metrics[nameval[0]] = int(nameval[1]) else: metrics[nameval[0]] = float(nameval[1]) except ValueError: From 0f3447cbf6ca0389fef9b71babd245b544ff0eb0 Mon Sep 17 00:00:00 2001 From: Paul Gear Date: Tue, 2 Jan 2024 10:43:31 +1000 Subject: [PATCH 05/14] Cache version string for lifetime of app --- src/version.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/version.py b/src/version.py index 0b20ca1..e9aa300 100644 --- a/src/version.py +++ b/src/version.py @@ -4,6 +4,11 @@ import version_data +_version = None + def get_version() -> str: - return ".".join((str(version_data.MAJOR), str(version_data.MINOR), str(version_data.PATCH))) + global _version + if _version is None: + _version = ".".join((str(version_data.MAJOR), str(version_data.MINOR), str(version_data.PATCH))) + return _version From 5f26dca29365d490f0819d66fcab1efe6fa7ffe8 Mon Sep 17 00:00:00 2001 From: Paul Gear Date: Tue, 2 Jan 2024 10:54:15 +1000 Subject: [PATCH 06/14] Add versioning strategy --- CHANGELOG.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 191eaf4..e5b55c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,24 @@ Notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +and this project (mostly) adheres to [Semantic +Versioning](https://semver.org/spec/v2.0.0.html). + +Semantic Versioning dictates that only the 0.y.z version should undergo rapid +changes. This project differs in that I want to be able to undertake rapid +changes at multiple stages after stable versions have been released. Hence, +from version 3.x onwards NTPmon will use a versioning style somewhat like the +Linux kernel original versioning scheme, where odd-numbered major versions are +development releases, which can have backwards incompatible changes introduced +over the lifetime of that major version. Even-numbered major versions will be +stable releases which will only contain new features and bug fixes. + +The current stable release is 2.1.0. It will be receiving no further +development unless critical security or data integrity bugs are found. + +The current development release is 3.0.6. This is the recommended version for +anyone who wants the latest features. It should be suitable for production +deployment very soon. ## [3.0.5] - 2023-12-30 From f3d9c3c522b1e724991167e8903fe20c9f3c3587 Mon Sep 17 00:00:00 2001 From: Paul Gear Date: Tue, 2 Jan 2024 10:59:29 +1000 Subject: [PATCH 07/14] Another peerstats fix --- src/outputs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/outputs.py b/src/outputs.py index e7925e1..87d7d9b 100644 --- a/src/outputs.py +++ b/src/outputs.py @@ -112,11 +112,11 @@ def __init__(self, args: argparse.Namespace) -> None: peerstatslabels: ClassVar[List[str]] = [ "mode", + "peertype", "refid", "rx_timestamp", "source", "tx_timestamp", - "type", ] peerstatstypes: ClassVar[Dict[str, str]] = { From 7d68986a0109c3fa834808f53c820d7c73bc6129 Mon Sep 17 00:00:00 2001 From: Paul Gear Date: Tue, 2 Jan 2024 11:25:51 +1000 Subject: [PATCH 08/14] Add ntpmon_version tag to prometheus and telegraf metrics If someone wants this for collectd and can explain how to do it, please get in touch. --- src/alert.py | 2 ++ src/outputs.py | 12 ++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/alert.py b/src/alert.py index ffeba4b..5e06fe3 100644 --- a/src/alert.py +++ b/src/alert.py @@ -13,6 +13,7 @@ import metrics import outputs +import version from classifier import MetricClassifier @@ -90,6 +91,7 @@ def alert(self, checkobjs: dict, output: outputs.Output, debug: bool = False) -> self.mc.classify_metrics(self.metrics) (m, rc) = self.mc.worst_metric(self.checks) self.metrics["result"] = self.return_code() + self.metrics["ntpmon_version"] = version.get_version() output.send_summary_stats(self.metrics, debug) output.send_peer_counts(self.metrics, debug) diff --git a/src/outputs.py b/src/outputs.py index 87d7d9b..6fc3bda 100644 --- a/src/outputs.py +++ b/src/outputs.py @@ -60,6 +60,7 @@ class Output: summarytypes: ClassVar[Dict[str, str]] = { "frequency": "frequency/frequency_offset", + "ntpmon_version": None, "offset": "offset/time_offset", "reach": "reachability/percent", "rootdelay": "rootdelay/root_delay", @@ -96,7 +97,7 @@ def send_stats(self, metrics: dict, types: dict, debug: bool = False, hostname: if hostname is None: hostname = self.args.hostname for metric in sorted(types.keys()): - if metric in metrics: + if metric in metrics and types[metric] is not None: print(self.formatstr % (hostname, types[metric], self.args.interval, metrics[metric])) def send_summary_stats(self, metrics: dict, debug: bool = False) -> None: @@ -186,7 +187,14 @@ def send_peer_counts(self, metrics: dict, debug: bool = False) -> None: ) def send_summary_stats(self, metrics: dict, debug: bool = False) -> None: - self.send_stats("ntpmon", metrics, self.summarystatstypes, debug=debug) + self.send_stats( + "ntpmon", + metrics, + self.summarystatstypes, + labelnames=["ntpmon_version"], + labels=[metrics["ntpmon_version"]], + debug=debug, + ) def send_stats( self, From ea7976f849b25c1a886efcbe3f7a8481c4c00fdd Mon Sep 17 00:00:00 2001 From: Paul Gear Date: Tue, 2 Jan 2024 11:26:17 +1000 Subject: [PATCH 09/14] Update README with roadmap info --- README.md | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a6a30cc..4f5ce84 100644 --- a/README.md +++ b/README.md @@ -142,8 +142,8 @@ issues with this. `Collectd` doesn't have a really great way to support these individual peer metrics, so each peer is considered to be a `collectd` "host". This feature -should be considered experimental for `collectd`, and subject to change (input -on this is welcome). +should be considered experimental for `collectd`, and subject to change or +deprecation (input on this is welcome). ## Prometheus exporter @@ -162,13 +162,6 @@ input plugin to be enabled. Use the `--connect` command-line option if you configure this to listen on a host and/or port other than the default (127.0.0.1:8094). -Telegraf is the preferred output integration for NTPmon (over collectd and -prometheus), due to its higher resolution timestamps, and measuring the -timestamp at the source which generated it rather than the scraping host. The -other integrations (first collectd, then Nagios, then prometheus) may eventually -go away if they are not widely used. Please let me know if you have strong -feelings about this. - ## Startup delay By default, until the NTP server has been running for 512 seconds (the minimum @@ -176,3 +169,23 @@ time for 8 polls at 64-second intervals), `check_ntpmon` will return OK (zero return code). This is to prevent false positives on startup or for short-lived VMs. To ignore this safety precaution, use `--run-time` with a low number (e.g. 1 sec). + +## Roadmap + +### Python version + +The current minimum python version targeted is 3.8. This version [reaches end +of life in October 2024](https://www.python.org/downloads/) and will be +deprecated in NTPmon sometime between [the release of Ubuntu +24.04](https://discourse.ubuntu.com/t/noble-numbat-release-schedule/35649) +("Noble Numbat") in April 2024 and python 3.8's EOL date. + +### Output integrations + +Telegraf is the preferred output integration for NTPmon (over collectd and +prometheus), due to its higher resolution timestamps, and measuring the +timestamp at the source which generated it rather than the scraping host. The +other integrations (first collectd, then Nagios, then prometheus) may eventually +go away if they are not widely used. Please let me know (via an +[issue](https://github.com/paulgear/ntpmon/issues)) if you have strong feelings +about this. From a90c96ff1aae69ff5cae579e896876e76836f097 Mon Sep 17 00:00:00 2001 From: Paul Gear Date: Tue, 2 Jan 2024 11:39:21 +1000 Subject: [PATCH 10/14] Prepare 3.0.6 release --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5b55c6..6ee1b07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,25 @@ The current development release is 3.0.6. This is the recommended version for anyone who wants the latest features. It should be suitable for production deployment very soon. +## [3.0.6] - 2024-01-02 + +### Added + +- `ntpmon_version` tag now provided with prometheus and telegraf metrics. + - If someone wants this for collectd and can explain how to do it, please get + in touch. +- `--version` command line argument. +- Versioning strategy in CHANGELOG. +- Roadmap in README. + +### Changed + +- Fix data type on `stratum` metric for ntpd. This was an integer under 2.x and + needs to remain so. +- Use `peertype` instead of `type` for individual peer metrics, to provide tag + compatibility between `ntpmon_peer` and `ntpmon_peers` metrics. +- Fix python 3.8 compatibility with debug flag. + ## [3.0.5] - 2023-12-30 ### Changed From 847fe9a6082a7c354276b492c6d40f73a1dce76e Mon Sep 17 00:00:00 2001 From: Paul Gear Date: Tue, 2 Jan 2024 20:33:43 +1000 Subject: [PATCH 11/14] Move version information to its own dedicated measurement --- Makefile | 1 + src/alert.py | 4 ++- src/info.py | 59 +++++++++++++++++++++++++++++++++++++++++ src/ntpmon.py | 2 +- src/outputs.py | 45 ++++++++++++++++++++++++------- src/process.py | 10 ++++++- unit_tests/test_info.py | 33 +++++++++++++++++++++++ 7 files changed, 141 insertions(+), 13 deletions(-) create mode 100644 src/info.py create mode 100644 unit_tests/test_info.py diff --git a/Makefile b/Makefile index ce8104e..40c0c44 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,7 @@ RELEASE=1 TESTS=\ unit_tests/test_classifier.py \ + unit_tests/test_info.py \ unit_tests/test_line_protocol.py \ unit_tests/test_peer_stats.py \ unit_tests/test_peers.py \ diff --git a/src/alert.py b/src/alert.py index 5e06fe3..daab703 100644 --- a/src/alert.py +++ b/src/alert.py @@ -87,11 +87,13 @@ def alert(self, checkobjs: dict, output: outputs.Output, debug: bool = False) -> """ Produce the metrics """ + if "info" in checkobjs: + output.send_info(checkobjs["info"], debug) + del checkobjs["info"] self.collectmetrics(checkobjs=checkobjs) self.mc.classify_metrics(self.metrics) (m, rc) = self.mc.worst_metric(self.checks) self.metrics["result"] = self.return_code() - self.metrics["ntpmon_version"] = version.get_version() output.send_summary_stats(self.metrics, debug) output.send_peer_counts(self.metrics, debug) diff --git a/src/info.py b/src/info.py new file mode 100644 index 0000000..12a6fbe --- /dev/null +++ b/src/info.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# +# Copyright: (c) 2024 Paul D. Gear +# License: AGPLv3 + +import platform +import re +import time + +from typing import Dict + +import psutil + +import process +import version as ntpmon_version + +_static_info = None + + +def get_info(implementation: str, version: str) -> Dict[str, str]: + """Collect the platform and process info metrics.""" + global _static_info + if _static_info is None: + _static_info = { + "ntpmon_version": ntpmon_version.get_version(), + "platform_machine": platform.machine(), + "platform_release": platform.release(), + "platform_system": platform.system(), + "python_version": platform.python_version(), + } + + this_process = psutil.Process() + memory = this_process.memory_info() + uptime = time.time() - this_process.create_time() + + dynamic_info = { + "implementation_name": implementation, + "implementation_version": extract_version(version), + "ntpmon_rss": memory.rss, + "ntpmon_uptime": uptime, + "ntpmon_vms": memory.vms, + } + + dynamic_info.update(_static_info) + return dynamic_info + + +_version_re = re.compile(r"\b\d\S+") + + +def extract_version(rawversion: str) -> str: + """Extract the version string from the line. Raise ValueError if no match.""" + return _version_re.search(rawversion).group() + + +if __name__ == "__main__": + i = process.get_implementation() + v = process.execute("version")[0][0] + print(get_info(implementation=i, version=v)) diff --git a/src/ntpmon.py b/src/ntpmon.py index b9a9209..8eb00d0 100755 --- a/src/ntpmon.py +++ b/src/ntpmon.py @@ -169,7 +169,7 @@ async def peer_stats_task(args: argparse.Namespace, output: outputs.Output) -> N async def summary_stats_task(args: argparse.Namespace, output: outputs.Output) -> None: global checkobjs - checks = ["proc", "offset", "peers", "reach", "sync", "vars"] + checks = ["proc", "info", "offset", "peers", "reach", "sync", "vars"] alerter = alert.NTPAlerter(checks) while True: implementation = process.get_implementation() diff --git a/src/outputs.py b/src/outputs.py index 6fc3bda..f16769d 100644 --- a/src/outputs.py +++ b/src/outputs.py @@ -60,7 +60,6 @@ class Output: summarytypes: ClassVar[Dict[str, str]] = { "frequency": "frequency/frequency_offset", - "ntpmon_version": None, "offset": "offset/time_offset", "reach": "reachability/percent", "rootdelay": "rootdelay/root_delay", @@ -71,6 +70,9 @@ class Output: "sysoffset": "sysoffset/time_offset", } + def send_info(self, metrics: dict, debug: bool = False) -> None: + pass + def send_measurement(self, metrics: dict, debug: bool = False) -> None: pass @@ -111,6 +113,22 @@ def __init__(self, args: argparse.Namespace) -> None: prometheus_client.start_http_server(addr=args.listen_address, port=args.port) + infolabels: ClassVar[List[str]] = [ + "implementation_name", + "implementation_version", + "ntpmon_version", + "platform_machine", + "platform_release", + "platform_system", + "python_version", + ] + + infotypes: ClassVar[Dict[str, Tuple[str, str, str]]] = { + "ntpmon_rss": ("i", "_bytes", "The resident set size of the ntpmon process"), + "ntpmon_uptime": (None, "_seconds", "Time for which the ntpmon process has been running"), + "ntpmon_vms": ("i", "_bytes", "The virtual memory size of the ntpmon process"), + } + peerstatslabels: ClassVar[List[str]] = [ "mode", "peertype", @@ -120,7 +138,7 @@ def __init__(self, args: argparse.Namespace) -> None: "tx_timestamp", ] - peerstatstypes: ClassVar[Dict[str, str]] = { + peerstatstypes: ClassVar[Dict[str, Tuple[str, str, str]]] = { "authenticated": ("i", None, "Whether the peer is authenticated"), "authentication_enabled": ("i", None, "Whether the peer has authentication enabled"), "authentication_fail": ("i", None, "Whether the peer has failed authentication"), @@ -163,6 +181,16 @@ def __init__(self, args: argparse.Namespace) -> None: "sysoffset": (None, "_seconds", "Current clock offset of selected system peer"), } + def send_info(self, metrics: dict, debug: bool = False) -> None: + self.send_stats( + "ntpmon_info", + metrics, + self.infotypes, + [x for x in self.infolabels if x in metrics], + [metrics[x] for x in self.infolabels if x in metrics], + debug=debug, + ) + def send_measurement(self, metrics: dict, debug: bool = False) -> None: self.send_stats( "ntpmon_peer", @@ -187,14 +215,7 @@ def send_peer_counts(self, metrics: dict, debug: bool = False) -> None: ) def send_summary_stats(self, metrics: dict, debug: bool = False) -> None: - self.send_stats( - "ntpmon", - metrics, - self.summarystatstypes, - labelnames=["ntpmon_version"], - labels=[metrics["ntpmon_version"]], - debug=debug, - ) + self.send_stats("ntpmon", metrics, self.summarystatstypes, debug=debug) def send_stats( self, @@ -267,6 +288,10 @@ def get_telegraf_file(connect: str) -> TextIOWrapper: s.connect((host, port)) return s.makefile(mode="w") + def send_info(self, metrics: dict, debug: bool) -> None: + telegraf_line = line_protocol.to_line_protocol(metrics, "ntpmon_info") + print(telegraf_line, file=self.file) + def send_measurement(self, metrics: dict, debug: bool = False) -> None: telegraf_line = line_protocol.to_line_protocol(metrics, "ntpmon_peer") print(telegraf_line, file=self.file) diff --git a/src/process.py b/src/process.py index b50672e..528542e 100644 --- a/src/process.py +++ b/src/process.py @@ -9,6 +9,8 @@ import psutil +import info + from peers import NTPPeers from readvar import NTPVars @@ -22,10 +24,12 @@ "chronyd": { "peers": "chronyc -c sources", "vars": "chronyc -c tracking", + "version": "chronyd --version", }, "ntpd": { "peers": "ntpq -pn", "vars": "ntpq -nc readvar", + "version": "ntpd --version", }, } @@ -87,7 +91,7 @@ def get_logfile(implementation) -> str: def get_progs(implementation): """Return the dict of programs for this implementation, or None, if one cannot be detected.""" - if implementation in _progs: + if implementation is not None and implementation in _progs: return _progs[implementation] implementation = detect_implementation() @@ -159,6 +163,10 @@ def ntpchecks(checks, debug, implementation=None): (output, elapsed) = execute("vars", debug=debug, implementation=implementation) objs["vars"] = NTPVars(output, elapsed) + if "info" in checks: + (output, elapsed) = execute("version", debug=debug, implementation=implementation) + objs["info"] = info.get_info(implementation=implementation, version=output[0]) + return objs diff --git a/unit_tests/test_info.py b/unit_tests/test_info.py new file mode 100644 index 0000000..8cd7c5f --- /dev/null +++ b/unit_tests/test_info.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# +# Copyright: (c) 2024 Paul D. Gear +# License: AGPLv3 + +import info + +version_tests = { + "4.2": "chronyd (chrony) version 4.2 (+CMDMON +NTP +REFCLOCK +RTC +PRIVDROP +SCFILTER +SIGND +ASYNCDNS +NTS +SECHASH +IPV6 -DEBUG)", + "4.2.8p15@1.3728-o": "ntpd 4.2.8p15@1.3728-o Wed Sep 23 11:46:38 UTC 2020 (1)", + "5.10.0-26-amd64": "Linux ntp5 5.10.0-26-amd64 #1 SMP Debian 5.10.197-1 (2023-09-29) x86_64 GNU/Linux", + "1.12.28-0+deb11u1": "ii dbus 1.12.28-0+deb11u1 amd64 simple interprocess messaging system (daemon and utilities)", + "2.4.52-1ubuntu4.7": "ii apache2-utils 2.4.52-1ubuntu4.7 amd64 Apache HTTP Server (utility programs for web servers)", +} + + +def test_get_info() -> None: + i = info.get_info(implementation="myntp", version="myntp1 1.2.3-beta1 test") + for field in ["platform_machine", "platform_release", "platform_system", "ntpmon_version"]: + assert field in i + assert type(i[field]) == str + + assert i["implementation_name"] == "myntp" + assert i["implementation_version"] == "1.2.3-beta1" + assert i["ntpmon_rss"] > 1000 + assert i["ntpmon_uptime"] > 0.000001 + assert i["ntpmon_vms"] > i["ntpmon_rss"] + assert [int(x) for x in i["python_version"].split(".")] >= [3, 8, 0] + + +def test_extract_version() -> None: + for k, v in version_tests.items(): + assert info.extract_version(v) == k From a896f8b0b3f53f367481f367d744439a83737ba4 Mon Sep 17 00:00:00 2001 From: Paul Gear Date: Tue, 2 Jan 2024 20:33:52 +1000 Subject: [PATCH 12/14] Update changelog --- CHANGELOG.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ee1b07..b161c23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,17 +26,18 @@ deployment very soon. ### Added -- `ntpmon_version` tag now provided with prometheus and telegraf metrics. - - If someone wants this for collectd and can explain how to do it, please get - in touch. +- `ntpmon_info` metric in prometheus and telegraf modes, including tags for + various system, python, and ntp components. + - If someone wants this for collectd and can explain how to do it in a way + which makes sense, please get in touch. - `--version` command line argument. -- Versioning strategy in CHANGELOG. - Roadmap in README. +- Versioning strategy in CHANGELOG. ### Changed -- Fix data type on `stratum` metric for ntpd. This was an integer under 2.x and - needs to remain so. +- Fix data type on `stratum` metric for `ntpd`. This was an integer under 2.x and + now is consistently so between `chronyd` and `ntpd`. - Use `peertype` instead of `type` for individual peer metrics, to provide tag compatibility between `ntpmon_peer` and `ntpmon_peers` metrics. - Fix python 3.8 compatibility with debug flag. From 2558965ada2ef4f24f4e86261659e8977e4a8eaa Mon Sep 17 00:00:00 2001 From: Paul Gear Date: Wed, 3 Jan 2024 07:14:27 +1000 Subject: [PATCH 13/14] Update copyrights for recently changed files Happy New Year! --- debian/check_ntpmon-man.rst | 2 +- debian/copyright | 2 +- debian/ntpmon-man.rst | 2 +- src/alert.py | 2 +- src/check_ntpmon.py | 2 +- src/ntpmon.py | 2 +- src/outputs.py | 2 +- src/peer_stats.py | 2 +- src/process.py | 2 +- src/readvar.py | 2 +- src/version.py | 2 +- unit_tests/test_peer_stats.py | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/debian/check_ntpmon-man.rst b/debian/check_ntpmon-man.rst index 21d3ecd..ca1452a 100644 --- a/debian/check_ntpmon-man.rst +++ b/debian/check_ntpmon-man.rst @@ -1,6 +1,6 @@ :Version: 3.0 :Date: 2023-12-28 -:Copyright: 2015-2023 Paul Gear +:Copyright: 2015-2024 Paul D. Gear :Title: check_ntpmon :Subtitle: NTPmon Nagios check :Manual group: NTP metrics monitor diff --git a/debian/copyright b/debian/copyright index bff9951..2e85b08 100644 --- a/debian/copyright +++ b/debian/copyright @@ -4,7 +4,7 @@ Upstream-Author: Paul Gear Source: https://github.com/paulgear/ntpmon Files: * -Copyright: 2015-2023 Paul D. Gear. +Copyright: 2015-2024 Paul D. Gear. License: AGPL-3.0+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by diff --git a/debian/ntpmon-man.rst b/debian/ntpmon-man.rst index 2240abe..3fe5bbd 100644 --- a/debian/ntpmon-man.rst +++ b/debian/ntpmon-man.rst @@ -1,6 +1,6 @@ :Version: 3.0 :Date: 2023-12-28 -:Copyright: 2015-2023 Paul Gear +:Copyright: 2015-2024 Paul D. Gear :Title: ntpmon :Subtitle: NTP metrics monitor :Manual group: NTP metrics monitor diff --git a/src/alert.py b/src/alert.py index daab703..f0bac9d 100644 --- a/src/alert.py +++ b/src/alert.py @@ -1,5 +1,5 @@ # -# Copyright: (c) 2016-2023 Paul D. Gear +# Copyright: (c) 2016-2024 Paul D. Gear # License: AGPLv3 """ diff --git a/src/check_ntpmon.py b/src/check_ntpmon.py index 6ce1550..3ee7d79 100755 --- a/src/check_ntpmon.py +++ b/src/check_ntpmon.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright: (c) 2016-2023 Paul D. Gear +# Copyright: (c) 2016-2024 Paul D. Gear # License: AGPLv3 import argparse diff --git a/src/ntpmon.py b/src/ntpmon.py index 8eb00d0..7bed56e 100755 --- a/src/ntpmon.py +++ b/src/ntpmon.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright: (c) 2016-2023 Paul D. Gear +# Copyright: (c) 2016-2024 Paul D. Gear # License: AGPLv3 import argparse diff --git a/src/outputs.py b/src/outputs.py index f16769d..05fc001 100644 --- a/src/outputs.py +++ b/src/outputs.py @@ -1,5 +1,5 @@ # -# Copyright: (c) 2023 Paul D. Gear +# Copyright: (c) 2023-2024 Paul D. Gear # License: AGPLv3 diff --git a/src/peer_stats.py b/src/peer_stats.py index 306fe8d..2e3a3b6 100755 --- a/src/peer_stats.py +++ b/src/peer_stats.py @@ -1,6 +1,6 @@ # Extract and parse chronyd measurements and ntpd peerstats # -# Copyright: (c) 2016-2023 Paul D. Gear +# Copyright: (c) 2016-2024 Paul D. Gear # License: AGPLv3 import datetime diff --git a/src/process.py b/src/process.py index 528542e..d2287d1 100644 --- a/src/process.py +++ b/src/process.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright: (c) 2016-2023 Paul D. Gear +# Copyright: (c) 2016-2024 Paul D. Gear # License: AGPLv3 import subprocess diff --git a/src/readvar.py b/src/readvar.py index 4047972..fcbd332 100644 --- a/src/readvar.py +++ b/src/readvar.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright: (c) 2016-2023 Paul D. Gear +# Copyright: (c) 2016-2024 Paul D. Gear # License: AGPLv3 """ diff --git a/src/version.py b/src/version.py index e9aa300..74ff014 100644 --- a/src/version.py +++ b/src/version.py @@ -1,5 +1,5 @@ # -# Copyright: (c) 2023 Paul D. Gear +# Copyright: (c) 2023-2024 Paul D. Gear # License: AGPLv3 import version_data diff --git a/unit_tests/test_peer_stats.py b/unit_tests/test_peer_stats.py index c72e19c..c30d1ce 100644 --- a/unit_tests/test_peer_stats.py +++ b/unit_tests/test_peer_stats.py @@ -1,5 +1,5 @@ # -# Copyright: (c) 2015-2023 Paul D. Gear +# Copyright: (c) 2015-2024 Paul D. Gear # License: AGPLv3 import datetime From d3e81dbb67cf4f1df940ef792b16671a4d8a9faf Mon Sep 17 00:00:00 2001 From: Paul Gear Date: Wed, 3 Jan 2024 07:18:24 +1000 Subject: [PATCH 14/14] Update debian changelog --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index 23a66fb..a4ccebd 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +ntpmon (3.0.6-1) focal; urgency=medium + + * New upstream release. + + -- Paul Gear Wed, 03 Jan 2024 07:17:49 +1000 + ntpmon (3.0.5-1) focal; urgency=medium * New upstream release.