apps/dashboard/backend/app/clients/uptime_kuma_client.py aktualisiert
This commit is contained in:
@@ -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,28 +80,20 @@ 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",
|
|
||||||
url,
|
|
||||||
headers=headers,
|
|
||||||
auth=auth,
|
|
||||||
)
|
|
||||||
if response.status_code == 200 and response.text:
|
if response.status_code == 200 and response.text:
|
||||||
logger.info("uptime kuma metrics auth succeeded via %s", mode)
|
logger.info("uptime kuma metrics auth succeeded via %s", mode)
|
||||||
return response.text
|
return response.text
|
||||||
|
|
||||||
if response.status_code in (401, 403):
|
if response.status_code in (401, 403):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"uptime kuma metrics auth failed via %s with status %s",
|
"uptime kuma metrics auth failed via %s with status %s",
|
||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user