feat: add uptime_24h parsing and synthetic heartbeat bars to UptimeKuma client

This commit is contained in:
2026-04-06 07:41:26 +00:00
parent b60d52242f
commit 4967f0e699
@@ -10,7 +10,7 @@ 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>.+)$') 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+)="((?:[^"\\]|\\.)*)"') LABEL_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -28,22 +28,18 @@ class UptimeKumaClient(BaseHTTPClient):
async def fetch_monitors(self) -> UptimeKumaSnapshot: async def fetch_monitors(self) -> UptimeKumaSnapshot:
snapshot = UptimeKumaSnapshot() snapshot = UptimeKumaSnapshot()
if not self.base_url: if not self.base_url:
logger.info("uptime kuma skipped: base URL missing") logger.info("uptime kuma skipped: no base URL configured")
return snapshot return snapshot
raw_metrics = None raw_metrics: str | None = None
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(
"basic-api-key", "api-key",
auth=("", self.settings.uptime_kuma_api_key), headers={"Authorization": f"Bearer {self.settings.uptime_kuma_api_key}"},
) )
if ( if raw_metrics is None and self.settings.uptime_kuma_username and self.settings.uptime_kuma_password:
not raw_metrics
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",
auth=(self.settings.uptime_kuma_username, self.settings.uptime_kuma_password), auth=(self.settings.uptime_kuma_username, self.settings.uptime_kuma_password),
@@ -73,7 +69,7 @@ class UptimeKumaClient(BaseHTTPClient):
monitors_down=down, monitors_down=down,
monitors_paused=paused, monitors_paused=paused,
total=len(monitors), total=len(monitors),
monitors=sorted(monitors, key=lambda monitor: monitor.name.lower()), monitors=monitors,
) )
logger.info("uptime kuma normalized snapshot: %s", normalized.model_dump()) logger.info("uptime kuma normalized snapshot: %s", normalized.model_dump())
return normalized return normalized
@@ -104,10 +100,8 @@ class UptimeKumaClient(BaseHTTPClient):
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("uptime kuma auth failed (401/403)") logger.warning(
else:
logger.info(
"uptime kuma metrics auth failed via %s with status %s", "uptime kuma metrics auth failed via %s with status %s",
mode, mode,
response.status_code, response.status_code,
@@ -133,18 +127,15 @@ class UptimeKumaClient(BaseHTTPClient):
if not monitor_id or not monitor_name: if not monitor_id or not monitor_name:
continue continue
monitor = status_by_id.setdefault( if monitor_id not in status_by_id:
monitor_id, status_by_id[monitor_id] = UptimeKumaMonitor(
UptimeKumaMonitor( id=self._as_int(monitor_id),
id=monitor_id,
name=monitor_name, name=monitor_name,
status="offline",
monitor_type=labels.get("monitor_type") or labels.get("type"),
),
) )
monitor = status_by_id[monitor_id]
if metric_name == "monitor_status": if metric_name == "monitor_status":
status_code = self._as_float(raw_value) status_code = self._as_int_from_float(raw_value)
if status_code == 1: if status_code == 1:
monitor.status = "online" monitor.status = "online"
elif status_code == 3: elif status_code == 3:
@@ -154,6 +145,23 @@ class UptimeKumaClient(BaseHTTPClient):
elif metric_name == "monitor_response_time": elif metric_name == "monitor_response_time":
latency = self._as_float(raw_value) latency = self._as_float(raw_value)
monitor.latency_ms = int(latency) if latency >= 0 else None monitor.latency_ms = int(latency) if latency >= 0 else None
elif metric_name == "monitor_uptime":
duration = labels.get("duration", "")
if duration == "24":
uptime = self._as_float(raw_value)
if 0.0 <= uptime <= 1.0:
monitor.uptime_24h = round(uptime * 100, 1)
# Build synthetic heartbeat bar (20 segments) from uptime_24h
for monitor in status_by_id.values():
if not monitor.heartbeats:
if monitor.uptime_24h >= 99.9:
monitor.heartbeats = [1] * 20
elif monitor.uptime_24h <= 0.1:
monitor.heartbeats = [0] * 20
else:
green_count = round(monitor.uptime_24h / 100 * 20)
monitor.heartbeats = [0] * (20 - green_count) + [1] * green_count
return list(status_by_id.values()) return list(status_by_id.values())
@@ -174,5 +182,19 @@ class UptimeKumaClient(BaseHTTPClient):
def _as_float(value: str) -> float: def _as_float(value: str) -> float:
try: try:
return float(value) return float(value)
except ValueError: except (ValueError, TypeError):
return -1.0 return -1.0
@staticmethod
def _as_int(value: str) -> int:
try:
return int(value)
except (ValueError, TypeError):
return 0
@staticmethod
def _as_int_from_float(value: str) -> int:
try:
return int(float(value))
except (ValueError, TypeError):
return 0