From 9f7cc82554efd82565797e64bb60719052bc638a Mon Sep 17 00:00:00 2001 From: Micha Date: Mon, 6 Apr 2026 12:51:39 +0000 Subject: [PATCH] apps/dashboard/backend/app/clients/uptime_kuma_client.py aktualisiert --- .../backend/app/clients/uptime_kuma_client.py | 48 ++++++++----------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/apps/dashboard/backend/app/clients/uptime_kuma_client.py b/apps/dashboard/backend/app/clients/uptime_kuma_client.py index 41c2104..7bf5389 100644 --- a/apps/dashboard/backend/app/clients/uptime_kuma_client.py +++ b/apps/dashboard/backend/app/clients/uptime_kuma_client.py @@ -9,17 +9,19 @@ from app.clients.base import BaseHTTPClient from app.config import Settings from app.models.sources import UptimeKumaMonitor, UptimeKumaSnapshot +METRIC_LINE_RE = re.compile(r'^(?P[a-zA-Z_:][a-zA-Z0-9_:]*){(?P[^}]*)}s+(?P.+)$') +LABEL_RE = re.compile(r'(w+)="((?:[^"\\]|\\.)*)"') -METRIC_LINE_RE = re.compile(r'^(?P[a-zA-Z_:][a-zA-Z0-9_:]*){(?P[^}]*)}\s+(?P.+)$') -LABEL_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') logger = logging.getLogger(__name__) class UptimeKumaClient(BaseHTTPClient): """ - Reads Uptime Kuma monitor status from the documented /metrics endpoint. - This avoids coupling the backend to Socket.IO login flows, but still relies - on Kuma's internal metrics surface, which may change across releases. + Reads Uptime Kuma monitor status from the /metrics endpoint. + + Auth: once an API key exists in Uptime Kuma, username/password Basic Auth + is permanently disabled. The correct format is HTTP Basic Auth with an + empty username and the API key as the password: auth=("", api_key). """ def __init__(self, settings: Settings) -> None: @@ -33,12 +35,14 @@ class UptimeKumaClient(BaseHTTPClient): raw_metrics: str | None = None + # Primary: API key as Basic Auth password (empty username) if self.settings.uptime_kuma_api_key: raw_metrics = await self._request_metrics_with_mode( "api-key", - headers={"Authorization": f"Bearer {self.settings.uptime_kuma_api_key}"}, + auth=("", self.settings.uptime_kuma_api_key), ) + # Fallback: regular username/password (only works if no API keys exist) if raw_metrics is None and self.settings.uptime_kuma_username and self.settings.uptime_kuma_password: raw_metrics = await self._request_metrics_with_mode( "basic-user", @@ -57,12 +61,10 @@ class UptimeKumaClient(BaseHTTPClient): return snapshot logger.info("uptime kuma raw metrics first 40 lines: %s", raw_metrics.splitlines()[:40]) - monitors = self._parse_metrics(raw_metrics) up = sum(1 for monitor in monitors if monitor.status == "online") down = sum(1 for monitor in monitors if monitor.status == "offline") paused = sum(1 for monitor in monitors if monitor.status == "degraded") - normalized = UptimeKumaSnapshot( source_status="online", monitors_up=up, @@ -78,34 +80,26 @@ class UptimeKumaClient(BaseHTTPClient): self, mode: str, *, - headers: dict[str, str] | None = None, auth: tuple[str, str] | None = None, ) -> str | None: if not self.base_url: return None - url = f"{self.base_url}/metrics" try: async with httpx.AsyncClient( timeout=self.settings.request_timeout_seconds, trust_env=False, ) as client: - response = await client.request( - "GET", - url, - headers=headers, - auth=auth, + response = await client.request("GET", url, auth=auth) + if response.status_code == 200 and response.text: + logger.info("uptime kuma metrics auth succeeded via %s", mode) + return response.text + if response.status_code in (401, 403): + logger.warning( + "uptime kuma metrics auth failed via %s with status %s", + mode, + response.status_code, ) - if response.status_code == 200 and response.text: - logger.info("uptime kuma metrics auth succeeded via %s", mode) - return response.text - - if response.status_code in (401, 403): - logger.warning( - "uptime kuma metrics auth failed via %s with status %s", - mode, - response.status_code, - ) return None except httpx.TimeoutException: logger.warning("uptime kuma metrics request timed out via %s", mode) @@ -115,25 +109,21 @@ class UptimeKumaClient(BaseHTTPClient): def _parse_metrics(self, payload: str) -> list[UptimeKumaMonitor]: status_by_id: dict[str, UptimeKumaMonitor] = {} - for line in payload.splitlines(): parsed = self._parse_metric_line(line) if parsed is None: continue - metric_name, labels, raw_value = parsed monitor_id = labels.get("monitor_id") or labels.get("id") or labels.get("monitor") or labels.get("monitor_name") monitor_name = labels.get("monitor_name") or labels.get("name") if not monitor_id or not monitor_name: continue - if monitor_id not in status_by_id: status_by_id[monitor_id] = UptimeKumaMonitor( id=self._as_int(monitor_id), name=monitor_name, ) monitor = status_by_id[monitor_id] - if metric_name == "monitor_status": status_code = self._as_int_from_float(raw_value) if status_code == 1: