From 7cfd97a8b4ae0839576a1bbf15c736ff4eaff379 Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Thu, 13 Nov 2025 10:05:05 +0100 Subject: [PATCH 1/6] Unchanged --- README.md | 1 + libprobe/probe.py | 24 ++++++++++++++++++++---- libprobe/version.py | 2 +- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8e65d21..bc3130a 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Variable | Default | Description `LOG_COLORIZED` | `0` | Log using colors (`0`=disabled, `1`=enabled). `LOG_FTM` | `%y%m%d %H:%M:%S` | Log format prefix. `OUTPUT_TYPE` | `JSON` | Set the output type to `JSON` or `PPRINT` (Only for a dry run). +`DISABLE_UNCHANGED` | `0` | Disable keeping track of unchanged check results. ## Usage diff --git a/libprobe/probe.py b/libprobe/probe.py index b40cb6b..3413606 100644 --- a/libprobe/probe.py +++ b/libprobe/probe.py @@ -137,6 +137,8 @@ def __init__( self._dry_run: Optional[Tuple[Asset, dict]] = \ None if dry_run is None else self._load_dry_run_assst(dry_run) self._on_close: Callable[[], Awaitable[None]] | None = None + self._prev_checks: Dict[tuple, dict] = {} # empty if DISABLE_UNCHANGED + self._no_unchanged = bool(int(os.getenv('DISABLE_UNCHANGED', '0'))) if not os.path.exists(config_path): try: @@ -376,6 +378,15 @@ async def _connect(self): finally: self._connecting = False + def _unchanged(self, path: tuple, result: dict) -> bool: + if self._no_unchanged: + return False + prev = self._prev_checks.get(path) + if prev == result: + return True + self._prev_checks[path] = result + return False + def send( self, path: tuple, @@ -388,14 +399,19 @@ def send( 'duration': time.time() - ts, 'timestamp': int(ts), } - if no_count: - framework['no_count'] = True - check_data = { - 'result': result, 'error': error, 'framework': framework } + + if no_count: + framework['no_count'] = True + + if result and self._unchanged(path, result): + framework['unchanged'] = True + else: + check_data['result'] = result + pkg = Package.make( AgentcoreProtocol.PROTO_FAF_DUMP, partid=asset_id, diff --git a/libprobe/version.py b/libprobe/version.py index a6221b3..496a0a4 100644 --- a/libprobe/version.py +++ b/libprobe/version.py @@ -1 +1 @@ -__version__ = '1.0.2' +__version__ = '1.0.3-alpha0' From 21aad43e9cb8aba5cb8508a5db0ab39bdc66c5f4 Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Thu, 13 Nov 2025 11:06:53 +0100 Subject: [PATCH 2/6] End of life --- README.md | 2 +- libprobe/probe.py | 14 ++++++++------ libprobe/version.py | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index bc3130a..deb7b26 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Variable | Default | Description `LOG_COLORIZED` | `0` | Log using colors (`0`=disabled, `1`=enabled). `LOG_FTM` | `%y%m%d %H:%M:%S` | Log format prefix. `OUTPUT_TYPE` | `JSON` | Set the output type to `JSON` or `PPRINT` (Only for a dry run). -`DISABLE_UNCHANGED` | `0` | Disable keeping track of unchanged check results. +`UNCHANGED_AGE` | `14400` | Prevent sending equal data _(use `unchanged`)_ for `X` seconds. A value of `0` disables `unchanged` _(defaults to 4 hours)_. ## Usage diff --git a/libprobe/probe.py b/libprobe/probe.py index 3413606..2719ca9 100644 --- a/libprobe/probe.py +++ b/libprobe/probe.py @@ -137,8 +137,8 @@ def __init__( self._dry_run: Optional[Tuple[Asset, dict]] = \ None if dry_run is None else self._load_dry_run_assst(dry_run) self._on_close: Callable[[], Awaitable[None]] | None = None - self._prev_checks: Dict[tuple, dict] = {} # empty if DISABLE_UNCHANGED - self._no_unchanged = bool(int(os.getenv('DISABLE_UNCHANGED', '0'))) + self._prev_checks: Dict[tuple, Tuple[float, dict]] = {} + self._unchanged_age = float(os.getenv('UNCHANGED_AGE', '14400')) if not os.path.exists(config_path): try: @@ -379,12 +379,13 @@ async def _connect(self): self._connecting = False def _unchanged(self, path: tuple, result: dict) -> bool: - if self._no_unchanged: + if not self._unchanged_age: return False - prev = self._prev_checks.get(path) - if prev == result: + eol, prev = self._prev_checks.get(path, (0.0, None)) + now = time.time() + if eol > now and prev == result: return True - self._prev_checks[path] = result + self._prev_checks[path] = now + self._unchanged_age, result return False def send( @@ -408,6 +409,7 @@ def send( framework['no_count'] = True if result and self._unchanged(path, result): + logging.debug('using previous result (unchanged)') framework['unchanged'] = True else: check_data['result'] = result diff --git a/libprobe/version.py b/libprobe/version.py index 496a0a4..3f6fab6 100644 --- a/libprobe/version.py +++ b/libprobe/version.py @@ -1 +1 @@ -__version__ = '1.0.3-alpha0' +__version__ = '1.0.3' From a0e5cf9be453bdab5d4876053ab90ba2423f25aa Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Thu, 13 Nov 2025 11:11:02 +0100 Subject: [PATCH 3/6] End of life --- README.md | 2 +- libprobe/probe.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index deb7b26..4dcac65 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Variable | Default | Description `LOG_COLORIZED` | `0` | Log using colors (`0`=disabled, `1`=enabled). `LOG_FTM` | `%y%m%d %H:%M:%S` | Log format prefix. `OUTPUT_TYPE` | `JSON` | Set the output type to `JSON` or `PPRINT` (Only for a dry run). -`UNCHANGED_AGE` | `14400` | Prevent sending equal data _(use `unchanged`)_ for `X` seconds. A value of `0` disables `unchanged` _(defaults to 4 hours)_. +`UNCHANGED_EOL` | `14400` | Unchanged End-Of-Life in X seconds. Prevents sending equal check data, a value of `0` disables `unchanged` _(defaults to 4 hours)_. ## Usage diff --git a/libprobe/probe.py b/libprobe/probe.py index 2719ca9..492732e 100644 --- a/libprobe/probe.py +++ b/libprobe/probe.py @@ -138,7 +138,7 @@ def __init__( None if dry_run is None else self._load_dry_run_assst(dry_run) self._on_close: Callable[[], Awaitable[None]] | None = None self._prev_checks: Dict[tuple, Tuple[float, dict]] = {} - self._unchanged_age = float(os.getenv('UNCHANGED_AGE', '14400')) + self._unchanged_eol = float(os.getenv('UNCHANGED_EOL', '14400')) if not os.path.exists(config_path): try: @@ -379,13 +379,13 @@ async def _connect(self): self._connecting = False def _unchanged(self, path: tuple, result: dict) -> bool: - if not self._unchanged_age: + if not self._unchanged_eol: return False eol, prev = self._prev_checks.get(path, (0.0, None)) now = time.time() if eol > now and prev == result: return True - self._prev_checks[path] = now + self._unchanged_age, result + self._prev_checks[path] = now + self._unchanged_eol, result return False def send( From b694efb5b9d38cde77389c683851c1ced1281f1b Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Thu, 13 Nov 2025 11:39:23 +0100 Subject: [PATCH 4/6] Use typing syntax --- libprobe/config.py | 3 +-- libprobe/exceptions.py | 3 +-- libprobe/net/package.py | 4 ++-- libprobe/net/protocol.py | 17 ++++++++--------- libprobe/probe.py | 32 ++++++++++++++++---------------- 5 files changed, 28 insertions(+), 31 deletions(-) diff --git a/libprobe/config.py b/libprobe/config.py index 6d12948..b3dfdb7 100644 --- a/libprobe/config.py +++ b/libprobe/config.py @@ -17,7 +17,6 @@ username: charlie password: "my other secret" """ -from typing import Optional import logging @@ -50,7 +49,7 @@ def decrypt(layer, fernet): decrypt(v, fernet) -def get_config(conf: dict, probe_name: str, asset_id: int, use: Optional[str]): +def get_config(conf: dict, probe_name: str, asset_id: int, use: str | None): # use might both be None or an empty string, depending if the `_use` option # is available for the probe; both must be ignored probe = conf.get(use or probe_name) diff --git a/libprobe/exceptions.py b/libprobe/exceptions.py index 572e51d..5dbc621 100644 --- a/libprobe/exceptions.py +++ b/libprobe/exceptions.py @@ -1,5 +1,4 @@ from .severity import Severity -from typing import Optional class IgnoreResultException(Exception): @@ -62,7 +61,7 @@ def __init__( self, msg: str, result: dict, - severity: Optional[Severity] = None): + severity: Severity | None = None): assert isinstance(result, dict) self.is_exception = severity is not None super().__init__( diff --git a/libprobe/net/package.py b/libprobe/net/package.py index 47d8e83..03d5151 100644 --- a/libprobe/net/package.py +++ b/libprobe/net/package.py @@ -1,7 +1,7 @@ from __future__ import annotations import msgpack import struct -from typing import Optional, Any +from typing import Any class Package(object): @@ -10,7 +10,7 @@ class Package(object): st_package = struct.Struct(' bool: def request( self, pkg: Package, - timeout: Union[None, float, int] = None + timeout: float | int | None = None ) -> asyncio.Future: self._pid += 1 self._pid %= 0x10000 @@ -86,7 +85,7 @@ def data_received(self, data: bytes): def on_package_received(self, pkg: Package): raise NotImplementedError - async def _timer(self, pid: int, timeout: Union[float, int]): + async def _timer(self, pid: int, timeout: float | int): await asyncio.sleep(timeout) try: future, task = self._requests.pop(pid) @@ -97,7 +96,7 @@ async def _timer(self, pid: int, timeout: Union[float, int]): future.set_exception(TimeoutError( f'request timed out on package id: {pid}')) - def _get_future(self, pkg: Package) -> Optional[asyncio.Future]: + def _get_future(self, pkg: Package) -> asyncio.Future | None: future, task = self._requests.pop(pkg.pid, (None, None)) if future is None: logging.error( diff --git a/libprobe/probe.py b/libprobe/probe.py index 492732e..5033c96 100644 --- a/libprobe/probe.py +++ b/libprobe/probe.py @@ -11,7 +11,7 @@ from cryptography.fernet import Fernet from pathlib import Path from setproctitle import setproctitle -from typing import Optional, Dict, Tuple, Callable, Awaitable, Mapping +from typing import Callable, Awaitable, Mapping from .exceptions import ( CheckException, IgnoreResultException, @@ -117,7 +117,7 @@ def __init__( logger.setup_logger() start_msg = 'starting' if dry_run is None else 'dry-run' logging.warning(f'{start_msg} probe collector: {name} v{version}') - self.loop: Optional[asyncio.AbstractEventLoop] = None + self.loop: asyncio.AbstractEventLoop | None = None self.name: str = name self.version: str = version self._checks_funs: Mapping[ @@ -125,19 +125,19 @@ def __init__( Callable[[Asset, dict, dict], Awaitable[dict]]] = checks self._config_path: Path = Path(config_path) self._connecting: bool = False - self._protocol: Optional[AgentcoreProtocol] = None + self._protocol: AgentcoreProtocol | None = None self._retry_next: int = 0 self._retry_step: int = 1 - self._local_config: Optional[dict] = None - self._local_config_mtime: Optional[float] = None - self._checks_config: Dict[ - Tuple[int, int], - Tuple[Tuple[str, str], dict]] = {} - self._checks: Dict[Tuple[int, int], asyncio.Future] = {} - self._dry_run: Optional[Tuple[Asset, dict]] = \ + self._local_config: dict | None = None + self._local_config_mtime: float | None = None + self._checks_config: dict[ + tuple[int, int], + tuple[tuple[str, str], dict]] = {} + self._checks: dict[tuple[int, int], asyncio.Future] = {} + self._dry_run: tuple[Asset, dict] | None = \ None if dry_run is None else self._load_dry_run_assst(dry_run) self._on_close: Callable[[], Awaitable[None]] | None = None - self._prev_checks: Dict[tuple, Tuple[float, dict]] = {} + self._prev_checks: dict[tuple, tuple[float, dict]] = {} self._unchanged_eol = float(os.getenv('UNCHANGED_EOL', '14400')) if not os.path.exists(config_path): @@ -157,7 +157,7 @@ def __init__( logging.exception(f"configuration file invalid: {config_path}") exit(1) - def _load_dry_run_assst(self, dry_run: dict) -> Tuple[Asset, dict]: + def _load_dry_run_assst(self, dry_run: dict) -> tuple[Asset, dict]: asset = dry_run.get('asset') if not isinstance(asset, dict): @@ -231,7 +231,7 @@ async def _start(self): for _ in range(step): await asyncio.sleep(1) - def start(self, loop: Optional[asyncio.AbstractEventLoop] = None): + def start(self, loop: asyncio.AbstractEventLoop | None = None): """Start a Infrasonar probe Args: @@ -391,8 +391,8 @@ def _unchanged(self, path: tuple, result: dict) -> bool: def send( self, path: tuple, - result: Optional[dict], - error: Optional[dict], + result: dict | None, + error: dict | None, ts: float, no_count: bool = False): asset_id, _ = path @@ -506,7 +506,7 @@ def _read_local_config(self): self._local_config_mtime = mtime self._local_config = config - def _asset_config(self, asset_id: int, use: Optional[str]) -> dict: + def _asset_config(self, asset_id: int, use: str | None) -> dict: try: self._read_local_config() except Exception: From a902f83bc9e5a2064568f7987a08a39443209916 Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Thu, 13 Nov 2025 12:04:56 +0100 Subject: [PATCH 5/6] Fix None --- libprobe/probe.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/libprobe/probe.py b/libprobe/probe.py index 5033c96..b6aa023 100644 --- a/libprobe/probe.py +++ b/libprobe/probe.py @@ -378,14 +378,17 @@ async def _connect(self): finally: self._connecting = False - def _unchanged(self, path: tuple, result: dict) -> bool: + def _unchanged(self, path: tuple, result: dict | None) -> bool: if not self._unchanged_eol: return False eol, prev = self._prev_checks.get(path, (0.0, None)) now = time.time() if eol > now and prev == result: return True - self._prev_checks[path] = now + self._unchanged_eol, result + if result is None: + self._prev_checks.pop(path, None) + else: + self._prev_checks[path] = now + self._unchanged_eol, result return False def send( @@ -408,7 +411,7 @@ def send( if no_count: framework['no_count'] = True - if result and self._unchanged(path, result): + if self._unchanged(path, result): logging.debug('using previous result (unchanged)') framework['unchanged'] = True else: From 173a21633a89af48492c57633082e649957c5461 Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Thu, 13 Nov 2025 12:07:21 +0100 Subject: [PATCH 6/6] first check None --- libprobe/probe.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/libprobe/probe.py b/libprobe/probe.py index b6aa023..feb1c7d 100644 --- a/libprobe/probe.py +++ b/libprobe/probe.py @@ -381,14 +381,16 @@ async def _connect(self): def _unchanged(self, path: tuple, result: dict | None) -> bool: if not self._unchanged_eol: return False + if result is None: + self._prev_checks.pop(path, None) + return False + eol, prev = self._prev_checks.get(path, (0.0, None)) now = time.time() if eol > now and prev == result: return True - if result is None: - self._prev_checks.pop(path, None) - else: - self._prev_checks[path] = now + self._unchanged_eol, result + + self._prev_checks[path] = now + self._unchanged_eol, result return False def send(