feat: add uptime_24h parsing and synthetic heartbeat bars to UptimeKuma client
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user