fix: probe Prometheus /metrics at server root, not under /v1 (v2.03.10)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-06-09 23:30:33 +02:00
parent c3cf123afe
commit c292ff2242
3 changed files with 41 additions and 23 deletions

View File

@ -9,6 +9,13 @@ The version is defined in [`blitztext/__init__.py`](blitztext/__init__.py).
## [Unreleased] ## [Unreleased]
## [2.03.10] - 2026-06-09
### Fixed
- **Server RAM probe.** Prometheus `/metrics` is almost always at the server
root (`http://host:port/metrics`), not under `/v1`. Now tries the root URL
first before falling back to the API base path.
## [2.03.09] - 2026-06-09 ## [2.03.09] - 2026-06-09
### Added ### Added

View File

@ -6,4 +6,4 @@ counterpart to the macOS Blitztext menu bar app: it runs natively on the host
(not in a container) so it can type into any application via xdotool. (not in a container) so it can type into any application via xdotool.
""" """
__version__ = "2.03.09" __version__ = "2.03.10"

View File

@ -154,34 +154,45 @@ def probe_server_ram_mb(base_url: str, timeout: float = 3.0) -> float | None:
Returns RSS in MB if the server exposes Prometheus metrics with either Returns RSS in MB if the server exposes Prometheus metrics with either
``process_resident_memory_bytes`` (standard Python/Go exporter) or ``process_resident_memory_bytes`` (standard Python/Go exporter) or
``container_memory_rss`` (cAdvisor). Returns None if not available. ``container_memory_rss`` (cAdvisor). Returns None if not available.
Tries the server root first (http://host:port/metrics) then the api base
path (http://host:port/v1/metrics), because Prometheus endpoints are almost
always mounted at root even when the API lives under /v1.
""" """
if not base_url: if not base_url:
return None return None
base = _api_base(base_url) p = urlparse(base_url.rstrip("/"))
try: root = f"{p.scheme}://{p.netloc}"
req = urllib.request.Request(base + "/metrics") candidates = [root + "/metrics"]
with urllib.request.urlopen(req, timeout=timeout) as resp: api_base_metrics = _api_base(base_url) + "/metrics"
text = resp.read().decode("utf-8", "replace") if api_base_metrics != candidates[0]:
except Exception: candidates.append(api_base_metrics)
def _parse(text: str) -> float | None:
for line in text.splitlines():
line = line.strip()
if line.startswith("#"):
continue
for metric in ("process_resident_memory_bytes", "container_memory_rss"):
if line.startswith(metric):
parts = line.split()
if len(parts) >= 2:
try:
return float(parts[-1]) / (1024 * 1024) # bytes → MB
except ValueError:
pass
return None return None
# Parse Prometheus text format — we only need two specific metric names. for url in candidates:
# Lines look like: process_resident_memory_bytes 1.23456e+08 try:
best: float | None = None req = urllib.request.Request(url)
for line in text.splitlines(): with urllib.request.urlopen(req, timeout=timeout) as resp:
line = line.strip() result = _parse(resp.read().decode("utf-8", "replace"))
if line.startswith("#"): if result is not None:
return result
except Exception:
continue continue
for metric in ("process_resident_memory_bytes", "container_memory_rss"): return None
if line.startswith(metric):
parts = line.split()
if len(parts) >= 2:
try:
best = float(parts[-1]) / (1024 * 1024) # bytes → MB
return best
except ValueError:
pass
return best
def detect_remote_device(base_url: str, timeout: float = 3.0) -> str: def detect_remote_device(base_url: str, timeout: float = 3.0) -> str: