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",
|
"label": "Review",
|
||||||
"value": "Pending",
|
"value": "Pending",
|
||||||
"detail": "Restic + B2"
|
"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": [
|
"services": [
|
||||||
|
|||||||
+19
-1
@@ -67,6 +67,9 @@ async function loadTelemetry() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function telemetryStatus(systems) {
|
function telemetryStatus(systems) {
|
||||||
|
const wireguard = systems.wireguard || {};
|
||||||
|
const monitoring = systems.monitoring || {};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'Backups',
|
name: 'Backups',
|
||||||
@@ -103,6 +106,21 @@ function telemetryStatus(systems) {
|
|||||||
value: systems.system.summary,
|
value: systems.system.summary,
|
||||||
detail: systems.system.detail
|
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
|
#!/usr/bin/env python3
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from collectors.docker import collect as docker_collect
|
from collectors.docker import collect as docker_collect
|
||||||
from .common import write_json
|
|
||||||
from collectors.backup import collect as backup_collect
|
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.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.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")
|
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():
|
def main():
|
||||||
@@ -19,9 +34,10 @@ def main():
|
|||||||
"docker": docker_collect(),
|
"docker": docker_collect(),
|
||||||
"certificates": certificates_collect(),
|
"certificates": certificates_collect(),
|
||||||
"system": system_collect(),
|
"system": system_collect(),
|
||||||
|
"wireguard": collect_safely("WireGuard", wireguard_collect),
|
||||||
|
"monitoring": collect_safely("Monitoring", monitoring_collect),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
write_json(OUTPUT, payload)
|
write_json(OUTPUT, payload)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user