commit d6eb06302cf7b7c53483cd1e75b135643b0a6bf3 Author: le king fu Date: Thu Feb 26 20:48:09 2026 -0500 feat: initial vps-health-api service Zero-dependency Node.js health endpoint exposing CPU, RAM, disk and uptime metrics. Bearer token auth, Docker-ready (node:22-alpine). Co-Authored-By: Claude Opus 4.6 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1ec266f --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +PORT=3001 +HEALTH_TOKEN=change-me-to-a-strong-secret diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7c4136 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.env +.env.local diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ded3be7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM node:22-alpine +WORKDIR /app +COPY package.json index.js ./ +EXPOSE 3001 +USER node +CMD ["node", "index.js"] diff --git a/index.js b/index.js new file mode 100644 index 0000000..17867b8 --- /dev/null +++ b/index.js @@ -0,0 +1,103 @@ +const http = require("node:http"); +const os = require("node:os"); +const { execSync } = require("node:child_process"); + +const PORT = parseInt(process.env.PORT || "3001", 10); +const TOKEN = process.env.HEALTH_TOKEN; + +function readProcStat() { + try { + const line = execSync("head -1 /proc/stat", { encoding: "utf-8" }).trim(); + const parts = line.split(/\s+/).slice(1).map(Number); + const idle = parts[3] + parts[4]; + const total = parts.reduce((a, b) => a + b, 0); + return { idle, total }; + } catch { + return null; + } +} + +function getCpuPercent() { + const t1 = readProcStat(); + if (!t1) return 0; + const { idle: idle1, total: total1 } = t1; + + // Sample over 500ms + execSync("sleep 0.5"); + + const t2 = readProcStat(); + if (!t2) return 0; + const dIdle = t2.idle - idle1; + const dTotal = t2.total - total1; + if (dTotal === 0) return 0; + return Math.round((1 - dIdle / dTotal) * 100); +} + +function getDisk() { + try { + const out = execSync("df / --output=size,used,avail -B1", { + encoding: "utf-8", + }); + const parts = out.trim().split("\n")[1].trim().split(/\s+/).map(Number); + const totalGB = +(parts[0] / 1e9).toFixed(1); + const usedGB = +(parts[1] / 1e9).toFixed(1); + const freeGB = +(parts[2] / 1e9).toFixed(1); + const usagePercent = totalGB > 0 ? Math.round((usedGB / totalGB) * 100) : 0; + return { totalGB, usedGB, freeGB, usagePercent }; + } catch { + return { totalGB: 0, usedGB: 0, freeGB: 0, usagePercent: 0 }; + } +} + +function getHealth() { + const cpus = os.cpus(); + const totalMem = os.totalmem(); + const freeMem = os.freemem(); + const usedMem = totalMem - freeMem; + + return { + timestamp: new Date().toISOString(), + hostname: os.hostname(), + uptime: Math.floor(os.uptime()), + cpu: { + model: cpus[0]?.model?.trim() || "unknown", + cores: cpus.length, + loadAvg: os.loadavg().map((l) => +l.toFixed(2)), + usagePercent: getCpuPercent(), + }, + memory: { + totalGB: +(totalMem / 1e9).toFixed(1), + usedGB: +(usedMem / 1e9).toFixed(1), + freeGB: +(freeMem / 1e9).toFixed(1), + usagePercent: Math.round((usedMem / totalMem) * 100), + }, + disk: getDisk(), + }; +} + +const server = http.createServer((req, res) => { + res.setHeader("Content-Type", "application/json"); + + if (req.url !== "/health" || req.method !== "GET") { + res.writeHead(404); + res.end(JSON.stringify({ error: "Not found" })); + return; + } + + if (TOKEN) { + const auth = req.headers["authorization"]; + if (auth !== `Bearer ${TOKEN}`) { + res.writeHead(401); + res.end(JSON.stringify({ error: "Unauthorized" })); + return; + } + } + + const data = getHealth(); + res.writeHead(200); + res.end(JSON.stringify(data)); +}); + +server.listen(PORT, () => { + console.log(`vps-health-api listening on :${PORT}`); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..6a2a5b8 --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "vps-health-api", + "version": "1.0.0", + "description": "Lightweight VPS health monitoring endpoint", + "main": "index.js", + "scripts": { + "start": "node index.js" + } +}