Add Voyager tunnel and monitoring telemetry
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
import re
|
||||
import sqlite3
|
||||
import subprocess
|
||||
|
||||
CONTAINER = "uptime-kuma"
|
||||
DATABASE = "/opt/docker/uptime-kuma/data/kuma.db"
|
||||
|
||||
|
||||
def _failure(detail):
|
||||
return {
|
||||
"state": "offline", "health": "unavailable", "summary": "Unavailable",
|
||||
"detail": detail, "container_available": False, "version": None,
|
||||
"active_monitors": 0, "up_count": 0, "down_count": 0, "unknown_count": 0,
|
||||
}
|
||||
|
||||
|
||||
def _container_info():
|
||||
result = subprocess.run(
|
||||
["docker", "inspect", "--format",
|
||||
"{{.State.Status}}\t{{.State.Health.Status}}\t{{.Config.Image}}",
|
||||
CONTAINER], capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
fields = result.stdout.strip().split("\t")
|
||||
if len(fields) != 3:
|
||||
return None
|
||||
status, health, image = fields
|
||||
match = re.search(r":([^:@]+)$", image)
|
||||
return status, health, match.group(1) if match else None
|
||||
|
||||
|
||||
def _database_counts():
|
||||
connection = sqlite3.connect(f"file:{DATABASE}?mode=ro", uri=True, timeout=5)
|
||||
try:
|
||||
rows = connection.execute(
|
||||
"""
|
||||
SELECT m.id,
|
||||
(SELECT h.status FROM heartbeat h
|
||||
WHERE h.monitor_id = m.id
|
||||
ORDER BY h.id DESC LIMIT 1) AS latest_status
|
||||
FROM monitor m
|
||||
WHERE m.active = 1
|
||||
"""
|
||||
).fetchall()
|
||||
finally:
|
||||
connection.close()
|
||||
counts = {"active": len(rows), "up": 0, "down": 0, "unknown": 0}
|
||||
for _, status in rows:
|
||||
if status == 1:
|
||||
counts["up"] += 1
|
||||
elif status == 0:
|
||||
counts["down"] += 1
|
||||
else:
|
||||
counts["unknown"] += 1
|
||||
return counts
|
||||
|
||||
|
||||
def collect():
|
||||
container = _container_info()
|
||||
if container is None:
|
||||
return _failure("Uptime Kuma container unavailable")
|
||||
container_status, container_health, version = container
|
||||
if container_status != "running":
|
||||
return _failure("Uptime Kuma container is not running")
|
||||
try:
|
||||
counts = _database_counts()
|
||||
except (OSError, sqlite3.Error):
|
||||
return _failure("Uptime Kuma aggregate query unavailable")
|
||||
active, up = counts["active"], counts["up"]
|
||||
down, unknown = counts["down"], counts["unknown"]
|
||||
degraded = container_health == "unhealthy" or down > 0 or unknown > 0
|
||||
version_detail = f"Uptime Kuma {version}" if version else "Uptime Kuma"
|
||||
return {
|
||||
"state": "warning" if degraded else "online",
|
||||
"health": "partial" if degraded else "healthy",
|
||||
"summary": f"{up}/{active} monitors up",
|
||||
"detail": f"{version_detail} · edge01 monitoring operational",
|
||||
"container_available": True, "container_health": container_health,
|
||||
"version": version, "active_monitors": active, "up_count": up,
|
||||
"down_count": down, "unknown_count": unknown,
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
INTERFACE = "wg0"
|
||||
RECENT_HANDSHAKE_SECONDS = 30 * 60
|
||||
|
||||
|
||||
def _failure(detail):
|
||||
return {
|
||||
"state": "offline", "health": "unavailable", "summary": "Unavailable",
|
||||
"detail": detail, "interface": INTERFACE, "interface_active": False,
|
||||
"configured_peers": 0, "recent_handshakes": 0, "stale_handshakes": 0,
|
||||
"rx_bytes": 0, "tx_bytes": 0,
|
||||
"recent_handshake_threshold_seconds": RECENT_HANDSHAKE_SECONDS,
|
||||
}
|
||||
|
||||
|
||||
def _format_bytes(value):
|
||||
units = ("B", "KiB", "MiB", "GiB", "TiB")
|
||||
amount = float(value)
|
||||
for unit in units:
|
||||
if amount < 1024 or unit == units[-1]:
|
||||
return f"{amount:.2f} {unit}" if unit != "B" else f"{int(amount)} B"
|
||||
amount /= 1024
|
||||
|
||||
|
||||
def collect():
|
||||
result = subprocess.run(["wg", "show", INTERFACE, "dump"],
|
||||
capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
return _failure("WireGuard telemetry unavailable")
|
||||
lines = [line.split() for line in result.stdout.splitlines() if line.strip()]
|
||||
if not lines or len(lines[0]) < 4:
|
||||
return _failure("WireGuard returned invalid telemetry")
|
||||
peers = lines[1:]
|
||||
now = int(time.time())
|
||||
recent = rx_bytes = tx_bytes = 0
|
||||
try:
|
||||
for peer in peers:
|
||||
if len(peer) < 8:
|
||||
return _failure("WireGuard returned incomplete telemetry")
|
||||
handshake = int(peer[4])
|
||||
rx_bytes += int(peer[5])
|
||||
tx_bytes += int(peer[6])
|
||||
if handshake and now - handshake <= RECENT_HANDSHAKE_SECONDS:
|
||||
recent += 1
|
||||
except (TypeError, ValueError):
|
||||
return _failure("WireGuard returned invalid telemetry")
|
||||
configured = len(peers)
|
||||
stale = configured - recent
|
||||
state = "online" if configured and recent == configured else "warning"
|
||||
health = "healthy" if state == "online" else "partial"
|
||||
return {
|
||||
"state": state, "health": health,
|
||||
"summary": f"{recent}/{configured} peers recent",
|
||||
"detail": (f"WireGuard active · {_format_bytes(rx_bytes)} RX · "
|
||||
f"{_format_bytes(tx_bytes)} TX"),
|
||||
"interface": INTERFACE, "interface_active": True,
|
||||
"configured_peers": configured, "recent_handshakes": recent,
|
||||
"stale_handshakes": stale, "rx_bytes": rx_bytes, "tx_bytes": tx_bytes,
|
||||
"recent_handshake_threshold_seconds": RECENT_HANDSHAKE_SECONDS,
|
||||
}
|
||||
@@ -41,20 +41,6 @@
|
||||
"label": "Review",
|
||||
"value": "Pending",
|
||||
"detail": "Restic + B2"
|
||||
},
|
||||
{
|
||||
"name": "Tunnel",
|
||||
"state": "warning",
|
||||
"label": "Pending",
|
||||
"value": "Not built",
|
||||
"detail": "Home network access"
|
||||
},
|
||||
{
|
||||
"name": "Monitoring",
|
||||
"state": "warning",
|
||||
"label": "Planned",
|
||||
"value": "Next phase",
|
||||
"detail": "Uptime Kuma"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
|
||||
+19
-1
@@ -67,6 +67,9 @@ async function loadTelemetry() {
|
||||
}
|
||||
|
||||
function telemetryStatus(systems) {
|
||||
const wireguard = systems.wireguard || {};
|
||||
const monitoring = systems.monitoring || {};
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'Backups',
|
||||
@@ -103,6 +106,21 @@ function telemetryStatus(systems) {
|
||||
value: systems.system.summary,
|
||||
detail: systems.system.detail
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Tunnel',
|
||||
state: wireguard.state || 'offline',
|
||||
label: wireguard.health === 'healthy' ? 'Healthy' :
|
||||
wireguard.health === 'partial' ? 'Partial' : 'Unavailable',
|
||||
value: wireguard.summary || 'Unavailable',
|
||||
detail: wireguard.detail || 'WireGuard telemetry unavailable'
|
||||
},
|
||||
{
|
||||
name: 'Monitoring',
|
||||
state: monitoring.state || 'offline',
|
||||
label: monitoring.health === 'healthy' ? 'Healthy' :
|
||||
monitoring.health === 'partial' ? 'Partial' : 'Unavailable',
|
||||
value: monitoring.summary || 'Unavailable',
|
||||
detail: monitoring.detail || 'Monitoring telemetry unavailable'
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
+20
-4
@@ -1,14 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from collectors.docker import collect as docker_collect
|
||||
from .common import write_json
|
||||
from collectors.backup import collect as backup_collect
|
||||
from collectors.git import collect as git_collect
|
||||
from collectors.certificates import collect as certificates_collect
|
||||
from collectors.git import collect as git_collect
|
||||
from collectors.monitoring import collect as monitoring_collect
|
||||
from collectors.system import collect as system_collect
|
||||
from collectors.wireguard import collect as wireguard_collect
|
||||
from .common import write_json
|
||||
|
||||
OUTPUT = Path("/opt/docker/caddy/site/voyager/data/status.json")
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def collect_safely(name, collector):
|
||||
try:
|
||||
return collector()
|
||||
except Exception:
|
||||
LOGGER.exception("%s telemetry collection failed", name)
|
||||
return {
|
||||
"state": "offline", "health": "unavailable",
|
||||
"summary": "Unavailable", "detail": f"{name} telemetry unavailable",
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
@@ -19,9 +34,10 @@ def main():
|
||||
"docker": docker_collect(),
|
||||
"certificates": certificates_collect(),
|
||||
"system": system_collect(),
|
||||
"wireguard": collect_safely("WireGuard", wireguard_collect),
|
||||
"monitoring": collect_safely("Monitoring", monitoring_collect),
|
||||
}
|
||||
}
|
||||
|
||||
write_json(OUTPUT, payload)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user