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 <noreply@anthropic.com>
This commit is contained in:
commit
d6eb06302c
5 changed files with 123 additions and 0 deletions
2
.env.example
Normal file
2
.env.example
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
PORT=3001
|
||||||
|
HEALTH_TOKEN=change-me-to-a-strong-secret
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
6
Dockerfile
Normal file
6
Dockerfile
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
FROM node:22-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json index.js ./
|
||||||
|
EXPOSE 3001
|
||||||
|
USER node
|
||||||
|
CMD ["node", "index.js"]
|
||||||
103
index.js
Normal file
103
index.js
Normal file
|
|
@ -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}`);
|
||||||
|
});
|
||||||
9
package.json
Normal file
9
package.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue