153 lines
5.7 KiB
Python
153 lines
5.7 KiB
Python
import json
|
|
import os
|
|
import time
|
|
import urllib.parse
|
|
import urllib.request
|
|
from pathlib import Path
|
|
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"))
|
|
SITE_GIT_DIR = Path(os.getenv("SITE_GIT_DIR", "/site/.git"))
|
|
|
|
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!=""}))',
|
|
}
|
|
|
|
HTTP_CHECKS = {
|
|
"gitea": "http://host.docker.internal:3000/api/v1/version",
|
|
"woodpecker": "http://host.docker.internal:8010/",
|
|
"uptime_kuma": "http://host.docker.internal:3001/",
|
|
"prometheus": "http://host.docker.internal:9090/-/healthy",
|
|
}
|
|
|
|
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 get_text(url):
|
|
request = urllib.request.Request(url, headers={"Accept": "*/*"})
|
|
with urllib.request.urlopen(request, timeout=TIMEOUT) as response:
|
|
return response.status, response.read(1024).decode("utf-8", "replace")
|
|
|
|
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 check_services():
|
|
services = {}
|
|
for name, url in HTTP_CHECKS.items():
|
|
try:
|
|
status, body = get_text(url)
|
|
services[name] = {"up": 200 <= status < 400, "status": status}
|
|
if name == "gitea":
|
|
try:
|
|
services[name]["version"] = json.loads(body).get("version")
|
|
except Exception:
|
|
pass
|
|
except Exception:
|
|
services[name] = {"up": False, "status": None}
|
|
return services
|
|
|
|
def deployed_commit():
|
|
try:
|
|
head = (SITE_GIT_DIR / "HEAD").read_text(encoding="utf-8").strip()
|
|
if head.startswith("ref: "):
|
|
ref = head[5:]
|
|
ref_path = SITE_GIT_DIR / ref
|
|
if ref_path.exists():
|
|
value = ref_path.read_text(encoding="utf-8").strip()
|
|
else:
|
|
value = None
|
|
packed = SITE_GIT_DIR / "packed-refs"
|
|
if packed.exists():
|
|
for line in packed.read_text(encoding="utf-8").splitlines():
|
|
if line and not line.startswith(("#", "^")):
|
|
sha, name = line.split(" ", 1)
|
|
if name == ref:
|
|
value = sha
|
|
break
|
|
if not value:
|
|
return None
|
|
else:
|
|
value = head
|
|
return value[:7]
|
|
except Exception:
|
|
return None
|
|
|
|
def build_status():
|
|
values = {name: prometheus_query(query) for name, query in QUERIES.items()}
|
|
targets = target_health()
|
|
services = check_services()
|
|
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,
|
|
"services": services,
|
|
"deployment": {
|
|
"commit": deployed_commit(),
|
|
"site": "bytegeist.casko.dev",
|
|
},
|
|
}
|
|
|
|
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()
|