From 03ea00767625752d0605de9bd0fa3a8358054e59 Mon Sep 17 00:00:00 2001 From: Keith Casko Date: Fri, 2 Oct 2026 10:22:55 -0400 Subject: [PATCH] Add live Prometheus telemetry endpoint --- index.html | 46 ++++++++++---------- nginx.conf | 34 +++++++++++++++ script.js | 81 +++++++++++++++++++++++++++++++++++ status-api/Dockerfile | 6 +++ status-api/status_api.py | 91 ++++++++++++++++++++++++++++++++++++++++ style.css | 13 ++++++ 6 files changed, 248 insertions(+), 23 deletions(-) create mode 100644 nginx.conf create mode 100644 status-api/Dockerfile create mode 100644 status-api/status_api.py diff --git a/index.html b/index.html index 9aaa821..9c894a2 100644 --- a/index.html +++ b/index.html @@ -70,7 +70,7 @@
INFRASTRUCTURE OVERVIEW - CONFIGURATION SNAPSHOT + CONNECTING TO TELEMETRY
@@ -84,36 +84,36 @@

-
+
- HOST METRICS - PROMETHEUS + CPU LOAD + --% +

PROMETHEUS / NODE EXPORTER

+
+
+ MEMORY + --% +

PROMETHEUS / NODE EXPORTER

+
+
+ HOST UPTIME + --

NODE EXPORTER

- CONTAINER METRICS - CADVISOR -

DOCKER TELEMETRY

+ RUNNING CONTAINERS + -- +

CADVISOR VIA PROMETHEUS

- AVAILABILITY MONITORING - KUMA -

ENDPOINT CHECKS

+ SCRAPE TARGETS + -- +

PROMETHEUS TARGET HEALTH

- LOGS - LOKI -

CENTRALIZED LOGGING

-
-
- DASHBOARDS - GRAFANA -

OPERATIONS / ANALYSIS

-
-
- IDENTITY - AUTHENTIK -

OIDC / SSO / 2FA

+ TELEMETRY + CONNECTING +

PROMETHEUS / CADVISOR

diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..2716f11 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,34 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location = /api/status { + proxy_pass http://status-api:8080/status; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 2s; + proxy_read_timeout 4s; + add_header Cache-Control "no-store, max-age=0" always; + } + + location / { + try_files $uri $uri/ =404; + } + + location ~ /. { + deny all; + return 404; + } + + location ^~ /status-api/ { + return 404; + } + + location = /nginx.conf { + return 404; + } +} diff --git a/script.js b/script.js index 717ef57..4b056d8 100644 --- a/script.js +++ b/script.js @@ -1,4 +1,17 @@ const timeElement = document.querySelector("#local-time"); +const refreshElement = document.querySelector("#refresh-indicator"); + +const fields = { + cpu: document.querySelector("#cpu-value"), + memory: document.querySelector("#memory-value"), + uptime: document.querySelector("#uptime-value"), + containers: document.querySelector("#containers-value"), + targets: document.querySelector("#targets-value"), + telemetry: document.querySelector("#telemetry-value"), +}; + +let lastSuccessfulUpdate = null; +let requestInFlight = false; function updateClock() { const now = new Date(); @@ -11,5 +24,73 @@ function updateClock() { }).format(now); } +function formatUptime(totalSeconds) { + const seconds = Math.max(0, Number(totalSeconds) || 0); + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + + if (days > 0) return days + "d " + hours + "h"; + if (hours > 0) return hours + "h " + minutes + "m"; + return minutes + "m"; +} + +function setUnavailable() { + for (const key of ["cpu", "memory", "uptime", "containers", "targets"]) { + fields[key].textContent = "--"; + } + fields.telemetry.textContent = "UNAVAILABLE"; + fields.telemetry.classList.add("metric-error"); + refreshElement.textContent = "TELEMETRY UNAVAILABLE"; + refreshElement.classList.add("telemetry-error"); +} + +function renderStatus(data) { + fields.cpu.textContent = Number(data.host.cpu_percent).toFixed(1); + fields.memory.textContent = Number(data.host.memory_percent).toFixed(1); + fields.uptime.textContent = formatUptime(data.host.uptime_seconds); + fields.containers.textContent = String(data.containers.running); + fields.targets.textContent = data.prometheus.up + "/" + data.prometheus.total; + fields.telemetry.textContent = "LIVE"; + fields.telemetry.classList.remove("metric-error"); + refreshElement.classList.remove("telemetry-error"); + + lastSuccessfulUpdate = Date.now(); + updateRefreshLabel(); +} + +function updateRefreshLabel() { + if (!lastSuccessfulUpdate) return; + const ageSeconds = Math.max(0, Math.floor((Date.now() - lastSuccessfulUpdate) / 1000)); + refreshElement.textContent = "LIVE TELEMETRY ยท UPDATED " + String(ageSeconds).padStart(2, "0") + "s AGO"; +} + +async function refreshTelemetry() { + if (requestInFlight) return; + requestInFlight = true; + + try { + const response = await fetch("/api/status", { + cache: "no-store", + headers: { "Accept": "application/json" }, + }); + + if (!response.ok) throw new Error("status " + response.status); + const data = await response.json(); + if (!data.ok) throw new Error("telemetry unavailable"); + + renderStatus(data); + } catch (error) { + console.error("ByteGeist telemetry request failed:", error); + setUnavailable(); + } finally { + requestInFlight = false; + } +} + updateClock(); +refreshTelemetry(); + window.setInterval(updateClock, 1000); +window.setInterval(updateRefreshLabel, 1000); +window.setInterval(refreshTelemetry, 15000); diff --git a/status-api/Dockerfile b/status-api/Dockerfile new file mode 100644 index 0000000..a790b78 --- /dev/null +++ b/status-api/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.12-alpine +WORKDIR /app +COPY status_api.py /app/status_api.py +USER nobody +EXPOSE 8080 +CMD ["python", "/app/status_api.py"] diff --git a/status-api/status_api.py b/status-api/status_api.py new file mode 100644 index 0000000..b4fc861 --- /dev/null +++ b/status-api/status_api.py @@ -0,0 +1,91 @@ +import json +import os +import time +import urllib.parse +import urllib.request +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +PROMETHEUS_URL = os.getenv("PROMETHEUS_URL", "http://host.docker.internal:9090").rstrip("/") +TIMEOUT = float(os.getenv("PROMETHEUS_TIMEOUT", "2.5")) + +QUERIES = { + "cpu_percent": '100-(avg(rate(node_cpu_seconds_total{mode="idle"}[2m]))*100)', + "memory_percent": '(1-(node_memory_MemAvailable_bytes/node_memory_MemTotal_bytes))*100', + "host_uptime_seconds": 'time()-node_boot_time_seconds', + "running_containers": 'count(count by (name) (container_last_seen{name!=""}))', +} + +def get_json(url): + request = urllib.request.Request(url, headers={"Accept": "application/json"}) + with urllib.request.urlopen(request, timeout=TIMEOUT) as response: + return json.load(response) + +def prometheus_query(expression): + url = f"{PROMETHEUS_URL}/api/v1/query?{urllib.parse.urlencode({'query': expression})}" + payload = get_json(url) + if payload.get("status") != "success": + raise RuntimeError("Prometheus query failed") + results = payload.get("data", {}).get("result", []) + if not results: + raise RuntimeError("Prometheus query returned no data") + return float(results[0]["value"][1]) + +def target_health(): + payload = get_json(f"{PROMETHEUS_URL}/api/v1/targets") + targets = payload.get("data", {}).get("activeTargets", []) + jobs = {} + for target in targets: + job = target.get("labels", {}).get("job", "unknown") + jobs[job] = target.get("health") == "up" + return { + "up": sum(1 for target in targets if target.get("health") == "up"), + "total": len(targets), + "jobs": jobs, + } + +def build_status(): + values = {name: prometheus_query(query) for name, query in QUERIES.items()} + targets = target_health() + return { + "ok": True, + "source": "Prometheus / node_exporter / cAdvisor", + "sampled_at": int(time.time()), + "host": { + "cpu_percent": round(values["cpu_percent"], 1), + "memory_percent": round(values["memory_percent"], 1), + "uptime_seconds": int(values["host_uptime_seconds"]), + }, + "containers": {"running": int(values["running_containers"])}, + "prometheus": targets, + } + +class Handler(BaseHTTPRequestHandler): + def send_json(self, status_code, payload): + body = json.dumps(payload, separators=(",", ":")).encode("utf-8") + self.send_response(status_code) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Cache-Control", "no-store, max-age=0") + self.send_header("Content-Length", str(len(body))) + self.send_header("X-Content-Type-Options", "nosniff") + self.end_headers() + self.wfile.write(body) + + def do_GET(self): + if self.path == "/healthz": + self.send_json(200, {"ok": True}) + return + if self.path != "/status": + self.send_json(404, {"ok": False, "error": "not_found"}) + return + try: + self.send_json(200, build_status()) + except Exception as exc: + self.send_json(503, {"ok": False, "sampled_at": int(time.time()), "error": type(exc).__name__}) + + def log_message(self, fmt, *args): + print(f"{self.address_string()} - {fmt % args}", flush=True) + +if __name__ == "__main__": + server = ThreadingHTTPServer(("0.0.0.0", 8080), Handler) + print("ByteGeist status API listening on :8080", flush=True) + server.serve_forever() diff --git a/style.css b/style.css index 592afe8..d7833ed 100644 --- a/style.css +++ b/style.css @@ -384,6 +384,19 @@ h1 { color: var(--green); } +.metric strong.metric-value { + display: inline-block; + margin-top: 16px; + font: 500 clamp(1.35rem, 1.8vw, 2.15rem)/1 var(--mono); + letter-spacing: -.045em; + overflow-wrap: normal; +} + +.metric .metric-error, +.telemetry-error { + color: var(--amber) !important; +} + .spark-bars { height: 23px; margin-top: 12px;