apps/dashboard/backend/app/clients/uptime_kuma_client.py aktualisiert

This commit is contained in:
2026-04-06 12:51:39 +00:00
parent 043b5697fd
commit 9f7cc82554
@@ -9,17 +9,19 @@ from app.clients.base import BaseHTTPClient
from app.config import Settings from app.config import Settings
from app.models.sources import UptimeKumaMonitor, UptimeKumaSnapshot from app.models.sources import UptimeKumaMonitor, UptimeKumaSnapshot
METRIC_LINE_RE = re.compile(r'^(?P<name>[a-zA-Z_:][a-zA-Z0-9_:]*){(?P<labels>[^}]*)}s+(?P<value>.+)$')
LABEL_RE = re.compile(r'(w+)="((?:[^"\\]|\\.)*)"')
METRIC_LINE_RE = re.compile(r'^(?P<name>[a-zA-Z_:][a-zA-Z0-9_:]*){(?P<labels>[^}]*)}\s+(?P<value>.+)$')
LABEL_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class UptimeKumaClient(BaseHTTPClient): class UptimeKumaClient(BaseHTTPClient):
""" """
Reads Uptime Kuma monitor status from the documented /metrics endpoint. Reads Uptime Kuma monitor status from the /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. 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: def __init__(self, settings: Settings) -> None:
@@ -33,12 +35,14 @@ class UptimeKumaClient(BaseHTTPClient):
raw_metrics: str | None = None raw_metrics: str | None = None
# Primary: API key as Basic Auth password (empty username)
if self.settings.uptime_kuma_api_key: if self.settings.uptime_kuma_api_key:
raw_metrics = await self._request_metrics_with_mode( raw_metrics = await self._request_metrics_with_mode(
"api-key", "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: 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( raw_metrics = await self._request_metrics_with_mode(
"basic-user", "basic-user",
@@ -57,12 +61,10 @@ class UptimeKumaClient(BaseHTTPClient):
return snapshot return snapshot
logger.info("uptime kuma raw metrics first 40 lines: %s", raw_metrics.splitlines()[:40]) logger.info("uptime kuma raw metrics first 40 lines: %s", raw_metrics.splitlines()[:40])
monitors = self._parse_metrics(raw_metrics) monitors = self._parse_metrics(raw_metrics)
up = sum(1 for monitor in monitors if monitor.status == "online") up = sum(1 for monitor in monitors if monitor.status == "online")
down = sum(1 for monitor in monitors if monitor.status == "offline") down = sum(1 for monitor in monitors if monitor.status == "offline")
paused = sum(1 for monitor in monitors if monitor.status == "degraded") paused = sum(1 for monitor in monitors if monitor.status == "degraded")
normalized = UptimeKumaSnapshot( normalized = UptimeKumaSnapshot(
source_status="online", source_status="online",
monitors_up=up, monitors_up=up,
@@ -78,34 +80,26 @@ class UptimeKumaClient(BaseHTTPClient):
self, self,
mode: str, mode: str,
*, *,
headers: dict[str, str] | None = None,
auth: tuple[str, str] | None = None, auth: tuple[str, str] | None = None,
) -> str | None: ) -> str | None:
if not self.base_url: if not self.base_url:
return None return None
url = f"{self.base_url}/metrics" url = f"{self.base_url}/metrics"
try: try:
async with httpx.AsyncClient( async with httpx.AsyncClient(
timeout=self.settings.request_timeout_seconds, timeout=self.settings.request_timeout_seconds,
trust_env=False, trust_env=False,
) as client: ) as client:
response = await client.request( response = await client.request("GET", url, auth=auth)
"GET", if response.status_code == 200 and response.text:
url, logger.info("uptime kuma metrics auth succeeded via %s", mode)
headers=headers, return response.text
auth=auth, 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 return None
except httpx.TimeoutException: except httpx.TimeoutException:
logger.warning("uptime kuma metrics request timed out via %s", mode) 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]: def _parse_metrics(self, payload: str) -> list[UptimeKumaMonitor]:
status_by_id: dict[str, UptimeKumaMonitor] = {} status_by_id: dict[str, UptimeKumaMonitor] = {}
for line in payload.splitlines(): for line in payload.splitlines():
parsed = self._parse_metric_line(line) parsed = self._parse_metric_line(line)
if parsed is None: if parsed is None:
continue continue
metric_name, labels, raw_value = parsed 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_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") monitor_name = labels.get("monitor_name") or labels.get("name")
if not monitor_id or not monitor_name: if not monitor_id or not monitor_name:
continue continue
if monitor_id not in status_by_id: if monitor_id not in status_by_id:
status_by_id[monitor_id] = UptimeKumaMonitor( status_by_id[monitor_id] = UptimeKumaMonitor(
id=self._as_int(monitor_id), id=self._as_int(monitor_id),
name=monitor_name, name=monitor_name,
) )
monitor = status_by_id[monitor_id] monitor = status_by_id[monitor_id]
if metric_name == "monitor_status": if metric_name == "monitor_status":
status_code = self._as_int_from_float(raw_value) status_code = self._as_int_from_float(raw_value)
if status_code == 1: if status_code == 1: