diff --git a/collectors/certificates.py b/collectors/certificates.py new file mode 100644 index 0000000..62fdc90 --- /dev/null +++ b/collectors/certificates.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +import subprocess +from datetime import datetime, timezone +from pathlib import Path + +CERT_ROOT = Path( + "/opt/docker/caddy/data/caddy/certificates/" + "acme-v02.api.letsencrypt.org-directory" +) + + +def collect(): + certificates = [] + + for cert_file in sorted(CERT_ROOT.glob("*/*.crt")): + result = subprocess.run( + ["openssl", "x509", "-in", str(cert_file), "-noout", "-enddate"], + capture_output=True, + text=True, + ) + + if result.returncode != 0: + continue + + expires_text = result.stdout.strip().removeprefix("notAfter=") + expires = datetime.strptime( + expires_text, + "%b %d %H:%M:%S %Y %Z", + ).replace(tzinfo=timezone.utc) + + days_remaining = (expires - datetime.now(timezone.utc)).days + + certificates.append({ + "name": cert_file.parent.name, + "days_remaining": days_remaining, + "expires": expires.isoformat(), + }) + + if not certificates: + return { + "state": "offline", + "summary": "No certificates", + "detail": "Caddy certificate files not found", + } + + lowest = min(item["days_remaining"] for item in certificates) + + state = "online" + if lowest < 30: + state = "warning" + if lowest < 7: + state = "offline" + + return { + "state": state, + "summary": f"{len(certificates)} certificates", + "detail": f"{lowest} days minimum", + "minimum_days": lowest, + "certificates": certificates, + } diff --git a/collectors/system.py b/collectors/system.py new file mode 100644 index 0000000..e1e3aab --- /dev/null +++ b/collectors/system.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 + +import os +import shutil + + +def collect(): + load_1, _, _ = os.getloadavg() + + memory_total = 0 + memory_available = 0 + + with open("/proc/meminfo", "r", encoding="utf-8") as handle: + for line in handle: + key, value = line.split(":", 1) + kb = int(value.strip().split()[0]) + + if key == "MemTotal": + memory_total = kb + elif key == "MemAvailable": + memory_available = kb + + memory_used_pct = round( + (1 - memory_available / memory_total) * 100 + ) if memory_total else 0 + + disk = shutil.disk_usage("/") + disk_used_pct = round(disk.used / disk.total * 100) + + with open("/proc/uptime", "r", encoding="utf-8") as handle: + uptime_seconds = int(float(handle.read().split()[0])) + + uptime_days = uptime_seconds // 86400 + + state = "online" + + if memory_used_pct >= 85 or disk_used_pct >= 85: + state = "warning" + + if memory_used_pct >= 95 or disk_used_pct >= 95: + state = "offline" + + return { + "state": state, + "summary": f"{memory_used_pct}% memory", + "detail": f"{disk_used_pct}% disk ยท {uptime_days} days uptime", + "load_1": round(load_1, 2), + "memory_used_pct": memory_used_pct, + "disk_used_pct": disk_used_pct, + "uptime_days": uptime_days, + } diff --git a/js/voyager.js b/js/voyager.js index 45a48eb..caeead0 100644 --- a/js/voyager.js +++ b/js/voyager.js @@ -88,6 +88,21 @@ function telemetryStatus(systems) { label: systems.docker.state === 'online' ? 'Healthy' : 'Attention', value: systems.docker.summary, detail: systems.docker.detail -} + }, + { + name: 'Certificates', + state: systems.certificates.state, + label: systems.certificates.state === 'online' ? 'Healthy' : 'Attention', + value: systems.certificates.detail, + detail: systems.certificates.summary + }, + { + name: 'System', + state: systems.system.state, + label: systems.system.state === 'online' ? 'Healthy' : 'Attention', + value: systems.system.summary, + detail: systems.system.detail + }, + ]; } diff --git a/telemetry/collector.py b/telemetry/collector.py index fc91114..6824e21 100755 --- a/telemetry/collector.py +++ b/telemetry/collector.py @@ -5,6 +5,8 @@ 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.system import collect as system_collect OUTPUT = Path("/opt/docker/caddy/site/voyager/data/status.json") @@ -15,6 +17,8 @@ def main(): "backup": backup_collect(), "git": git_collect(), "docker": docker_collect(), + "certificates": certificates_collect(), + "system": system_collect(), } }