Compare commits
No commits in common. "fc3c3a92685659c979754ecc87c8fec0b1a48ac1" and "9a4c5c7775d4cfc589fd3ae36a33bb4cd8d9e635" have entirely different histories.
fc3c3a9268
...
9a4c5c7775
3 changed files with 9 additions and 73 deletions
|
|
@ -1,3 +1,2 @@
|
||||||
PORT=3001
|
PORT=3001
|
||||||
HEALTH_TOKEN=change-me-to-a-strong-secret
|
HEALTH_TOKEN=change-me-to-a-strong-secret
|
||||||
LOGTO_HEALTH_URL=https://auth.lacompagniemaximus.com/oidc/.well-known/openid-configuration
|
|
||||||
|
|
|
||||||
28
CLAUDE.md
28
CLAUDE.md
|
|
@ -1,28 +0,0 @@
|
||||||
# VPS Health API
|
|
||||||
|
|
||||||
API sante minimaliste pour le VPS. ~127 lignes, Node 22 + HTTP natif.
|
|
||||||
|
|
||||||
## Endpoints
|
|
||||||
|
|
||||||
- `GET /health` — CPU, memoire, disque, uptime, logto (`{status, responseTimeMs, error?}`)
|
|
||||||
- `GET /defenseurs` — contenu de status.json (rapports defenseurs)
|
|
||||||
|
|
||||||
## Auth
|
|
||||||
|
|
||||||
- Bearer token via env `HEALTH_TOKEN`
|
|
||||||
- Fail-closed : si `HEALTH_TOKEN` non configure, toutes les requetes sont refusees
|
|
||||||
|
|
||||||
## Config
|
|
||||||
|
|
||||||
- Port : `3001` (env `PORT`)
|
|
||||||
- `LOGTO_HEALTH_URL` : URL du `.well-known/openid-configuration` (default auth.lacompagniemaximus.com)
|
|
||||||
- Bind-mount : `/data/defenseurs/status.json` read-only
|
|
||||||
|
|
||||||
## Deploy
|
|
||||||
|
|
||||||
Coolify auto-rebuild depuis push Forgejo. Aucune action manuelle requise.
|
|
||||||
|
|
||||||
## Gotchas
|
|
||||||
|
|
||||||
- Pas d'Express — HTTP natif Node.js uniquement
|
|
||||||
- Le `status.json` est ecrit par le Sergent defenseurs, pas par cette API (read-only)
|
|
||||||
53
index.js
53
index.js
|
|
@ -2,14 +2,9 @@ const http = require("node:http");
|
||||||
const os = require("node:os");
|
const os = require("node:os");
|
||||||
const { execSync } = require("node:child_process");
|
const { execSync } = require("node:child_process");
|
||||||
const { readFileSync } = require("node:fs");
|
const { readFileSync } = require("node:fs");
|
||||||
const { setTimeout: delay } = require("node:timers/promises");
|
|
||||||
|
|
||||||
const PORT = parseInt(process.env.PORT || "3001", 10);
|
const PORT = parseInt(process.env.PORT || "3001", 10);
|
||||||
const TOKEN = process.env.HEALTH_TOKEN;
|
const TOKEN = process.env.HEALTH_TOKEN;
|
||||||
const LOGTO_HEALTH_URL =
|
|
||||||
process.env.LOGTO_HEALTH_URL ||
|
|
||||||
"https://auth.lacompagniemaximus.com/oidc/.well-known/openid-configuration";
|
|
||||||
const LOGTO_TIMEOUT_MS = 3000;
|
|
||||||
|
|
||||||
if (!TOKEN) {
|
if (!TOKEN) {
|
||||||
console.warn("WARNING: HEALTH_TOKEN is not set. All requests will be rejected (fail-closed).");
|
console.warn("WARNING: HEALTH_TOKEN is not set. All requests will be rejected (fail-closed).");
|
||||||
|
|
@ -27,14 +22,13 @@ function readProcStat() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getCpuPercent() {
|
function getCpuPercent() {
|
||||||
const t1 = readProcStat();
|
const t1 = readProcStat();
|
||||||
if (!t1) return 0;
|
if (!t1) return 0;
|
||||||
const { idle: idle1, total: total1 } = t1;
|
const { idle: idle1, total: total1 } = t1;
|
||||||
|
|
||||||
// Sample over 500ms without blocking the event loop, so other async work
|
// Sample over 500ms
|
||||||
// (e.g. the Logto healthcheck) can run concurrently.
|
execSync("sleep 0.5");
|
||||||
await delay(500);
|
|
||||||
|
|
||||||
const t2 = readProcStat();
|
const t2 = readProcStat();
|
||||||
if (!t2) return 0;
|
if (!t2) return 0;
|
||||||
|
|
@ -44,24 +38,6 @@ async function getCpuPercent() {
|
||||||
return Math.round((1 - dIdle / dTotal) * 100);
|
return Math.round((1 - dIdle / dTotal) * 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getLogtoHealth() {
|
|
||||||
const ac = new AbortController();
|
|
||||||
const timer = setTimeout(() => ac.abort(), LOGTO_TIMEOUT_MS);
|
|
||||||
const start = performance.now();
|
|
||||||
try {
|
|
||||||
const res = await fetch(LOGTO_HEALTH_URL, { signal: ac.signal });
|
|
||||||
const responseTimeMs = Math.round(performance.now() - start);
|
|
||||||
if (res.ok) return { status: "up", responseTimeMs };
|
|
||||||
return { status: "down", responseTimeMs, error: `HTTP ${res.status}` };
|
|
||||||
} catch (err) {
|
|
||||||
const responseTimeMs = Math.round(performance.now() - start);
|
|
||||||
const error = err.name === "AbortError" ? "timeout" : err.message || "network error";
|
|
||||||
return { status: "down", responseTimeMs, error };
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDisk() {
|
function getDisk() {
|
||||||
try {
|
try {
|
||||||
// Alpine df doesn't support --output, use standard POSIX format
|
// Alpine df doesn't support --output, use standard POSIX format
|
||||||
|
|
@ -78,17 +54,12 @@ function getDisk() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getHealth() {
|
function getHealth() {
|
||||||
const cpus = os.cpus();
|
const cpus = os.cpus();
|
||||||
const totalMem = os.totalmem();
|
const totalMem = os.totalmem();
|
||||||
const freeMem = os.freemem();
|
const freeMem = os.freemem();
|
||||||
const usedMem = totalMem - freeMem;
|
const usedMem = totalMem - freeMem;
|
||||||
|
|
||||||
const [cpuUsagePercent, logto] = await Promise.all([
|
|
||||||
getCpuPercent(),
|
|
||||||
getLogtoHealth(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
hostname: os.hostname(),
|
hostname: os.hostname(),
|
||||||
|
|
@ -97,7 +68,7 @@ async function getHealth() {
|
||||||
model: cpus[0]?.model?.trim() || "unknown",
|
model: cpus[0]?.model?.trim() || "unknown",
|
||||||
cores: cpus.length,
|
cores: cpus.length,
|
||||||
loadAvg: os.loadavg().map((l) => +l.toFixed(2)),
|
loadAvg: os.loadavg().map((l) => +l.toFixed(2)),
|
||||||
usagePercent: cpuUsagePercent,
|
usagePercent: getCpuPercent(),
|
||||||
},
|
},
|
||||||
memory: {
|
memory: {
|
||||||
totalGB: +(totalMem / 1e9).toFixed(1),
|
totalGB: +(totalMem / 1e9).toFixed(1),
|
||||||
|
|
@ -106,11 +77,10 @@ async function getHealth() {
|
||||||
usagePercent: Math.round((usedMem / totalMem) * 100),
|
usagePercent: Math.round((usedMem / totalMem) * 100),
|
||||||
},
|
},
|
||||||
disk: getDisk(),
|
disk: getDisk(),
|
||||||
logto,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = http.createServer(async (req, res) => {
|
const server = http.createServer((req, res) => {
|
||||||
res.setHeader("Content-Type", "application/json");
|
res.setHeader("Content-Type", "application/json");
|
||||||
|
|
||||||
const validRoutes = ["/health", "/defenseurs"];
|
const validRoutes = ["/health", "/defenseurs"];
|
||||||
|
|
@ -146,14 +116,9 @@ const server = http.createServer(async (req, res) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const data = getHealth();
|
||||||
const data = await getHealth();
|
res.writeHead(200);
|
||||||
res.writeHead(200);
|
res.end(JSON.stringify(data));
|
||||||
res.end(JSON.stringify(data));
|
|
||||||
} catch (err) {
|
|
||||||
res.writeHead(500);
|
|
||||||
res.end(JSON.stringify({ error: "Internal error", message: err.message }));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
server.listen(PORT, () => {
|
server.listen(PORT, () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue