feat(health-checks): enhance health check configuration and logging
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 8s
quality / changes (push) Successful in 5s
quality / docker-check (push) Skipped
quality / web (push) Successful in 51s
quality / api (push) Successful in 44s
CD / quality (push) Successful in 1m44s
CD / publish (push) Successful in 1m35s

- Added new health worker URL and token fields to the AppConfig interface, allowing for Cloudflare Worker integration.
- Updated health check routes to utilize the new worker configuration, enabling dynamic health checks via Cloudflare Workers.
- Introduced a health log endpoint for services, providing detailed logs of health probe results.
- Enhanced health check service logic to support both local and Cloudflare Worker providers, improving flexibility in health monitoring.
- Updated UI components to reflect changes in health check provider settings and display relevant health information.

This commit significantly improves the health check management capabilities, allowing for better integration with Cloudflare Workers and enhanced logging features.
This commit is contained in:
Denozordec
2026-08-19 16:27:34 +07:00
parent d63c86065c
commit 2c92e78b24
36 changed files with 2184 additions and 263 deletions
+37
View File
@@ -0,0 +1,37 @@
# CFDM health-probe Worker
Stateless edge probe for CFDM. **Not** Cloudflare Health Checks API (unavailable on Free).
Cron stays in CFDM — this Worker has no Cron Trigger.
## Deploy
```powershell
cd workers/health-probe
npx wrangler login
npx wrangler secret put PROBE_TOKEN
npx wrangler deploy
```
Paste the Worker URL (`https://cfdm-health-probe.<account>.workers.dev`) and the same token into **Настройки → Health-check**.
Free Workers ≈ 100k requests/day. CFDM cron every 2 minutes × number of IPs must fit.
## API
`POST /probe` + `Authorization: Bearer <PROBE_TOKEN>`
```json
{
"type": "tcp",
"ip": "1.2.3.4",
"hostname": "app.example.com",
"port": 443,
"path": "/",
"expected_status": 200,
"timeout_ms": 3000,
"verify_tls": true,
"method": "GET"
}
```
Response: `{ "ok": true, "latencyMs": 42, "error": null, "colo": "AMS" }`.
+11
View File
@@ -0,0 +1,11 @@
{
"name": "cfdm-health-probe",
"private": true,
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy"
},
"devDependencies": {
"wrangler": "^4.20.0"
}
}
+181
View File
@@ -0,0 +1,181 @@
/**
* Stateless CFDM health probe. Cron lives in CFDM API — this Worker only
* answers POST /probe. Deploy: wrangler deploy; paste URL + token into
* Настройки → Health-check.
*/
export interface Env {
PROBE_TOKEN: string;
}
type ProbeType = "tcp" | "http";
interface ProbeRequest {
type?: ProbeType;
ip?: string;
hostname?: string;
port?: number;
path?: string;
expected_status?: number | null;
timeout_ms?: number;
verify_tls?: boolean;
method?: string;
}
interface ProbeResponse {
ok: boolean;
latencyMs: number;
error: string | null;
colo: string | null;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const colo =
(request as Request & { cf?: { colo?: string } }).cf?.colo ?? null;
if (request.method !== "POST") {
return json({ ok: false, latencyMs: 0, error: "method not allowed", colo }, 405);
}
const pathname = new URL(request.url).pathname.replace(/\/$/, "") || "/";
if (pathname !== "/probe") {
return json({ ok: false, latencyMs: 0, error: "not found", colo }, 404);
}
const token = bearer(request);
if (!env.PROBE_TOKEN || token !== env.PROBE_TOKEN) {
return json({ ok: false, latencyMs: 0, error: "unauthorized", colo }, 401);
}
let body: ProbeRequest;
try {
body = (await request.json()) as ProbeRequest;
} catch {
return json({ ok: false, latencyMs: 0, error: "invalid json", colo }, 400);
}
const ip = String(body.ip ?? "").trim();
if (!ip) {
return json({ ok: false, latencyMs: 0, error: "ip required", colo }, 400);
}
const type: ProbeType = body.type === "http" ? "http" : "tcp";
const port = Number(body.port) || (type === "http" ? 80 : 80);
const timeoutMs = Math.min(Math.max(Number(body.timeout_ms) || 3000, 100), 25_000);
const hostname = String(body.hostname ?? "").trim() || ip;
try {
const result =
type === "http"
? await httpProbe({
ip,
hostname,
port,
path: body.path || "/",
expectedStatus: body.expected_status ?? 200,
timeoutMs,
verifyTls: Boolean(body.verify_tls),
method: (body.method || "GET").toUpperCase(),
})
: await tcpProbe(ip, port, timeoutMs);
return json({ ...result, colo });
} catch (err) {
const message = err instanceof Error ? err.message : "probe failed";
return json({ ok: false, latencyMs: 0, error: message, colo });
}
},
};
function bearer(request: Request): string {
const header = request.headers.get("Authorization") ?? "";
return header.startsWith("Bearer ") ? header.slice(7) : "";
}
function json(body: ProbeResponse, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json" },
});
}
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(`${label} timeout`)), timeoutMs);
promise.then(
(value) => {
clearTimeout(timer);
resolve(value);
},
(err) => {
clearTimeout(timer);
reject(err);
},
);
});
}
async function tcpProbe(
ip: string,
port: number,
timeoutMs: number,
): Promise<Omit<ProbeResponse, "colo">> {
const started = Date.now();
const { connect } = await import("cloudflare:sockets");
const socket = connect({ hostname: ip, port });
try {
await withTimeout(socket.opened, timeoutMs, "tcp");
return { ok: true, latencyMs: Date.now() - started, error: null };
} catch (err) {
const message = err instanceof Error ? err.message : "tcp failed";
return { ok: false, latencyMs: Date.now() - started, error: message };
} finally {
try {
socket.close();
} catch {
// ignore
}
}
}
async function httpProbe(opts: {
ip: string;
hostname: string;
port: number;
path: string;
expectedStatus: number;
timeoutMs: number;
verifyTls: boolean;
method: string;
}): Promise<Omit<ProbeResponse, "colo">> {
const started = Date.now();
const useTls = opts.verifyTls || opts.port === 443;
const host = opts.ip.includes(":") ? `[${opts.ip}]` : opts.ip;
const path = opts.path.startsWith("/") ? opts.path : `/${opts.path}`;
const url = `${useTls ? "https" : "http"}://${host}:${opts.port}${path}`;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), opts.timeoutMs);
try {
const res = await fetch(url, {
method: opts.method === "HEAD" ? "HEAD" : "GET",
headers: { Host: opts.hostname },
signal: controller.signal,
redirect: "manual",
});
const latencyMs = Date.now() - started;
if (res.status !== opts.expectedStatus) {
return {
ok: false,
latencyMs,
error: `HTTP ${res.status} (ожидали ${opts.expectedStatus})`,
};
}
return { ok: true, latencyMs, error: null };
} catch (err) {
const message =
err instanceof Error
? err.name === "AbortError"
? "http timeout"
: err.message
: "http failed";
return { ok: false, latencyMs: Date.now() - started, error: message };
} finally {
clearTimeout(timer);
}
}
+6
View File
@@ -0,0 +1,6 @@
name = "cfdm-health-probe"
main = "src/index.ts"
compatibility_date = "2025-04-01"
# Set the shared secret: wrangler secret put PROBE_TOKEN
# Then paste the Worker URL + token into CFDM → Настройки → Health-check.