Merge pull request #893 from pikasTech/fix/v02-metrics-sidecar-http-probe

fix: use HTTP request for v02 metrics health probes
This commit is contained in:
Lyon
2026-06-05 09:13:35 +08:00
committed by GitHub
2 changed files with 78 additions and 18 deletions
+41 -17
View File
@@ -1,4 +1,5 @@
import { createServer } from "node:http";
import { createServer, request as httpRequest } from "node:http";
import { request as httpsRequest } from "node:https";
import { realpathSync } from "node:fs";
import { pathToFileURL } from "node:url";
@@ -23,26 +24,49 @@ function metricLabels(env) {
return `service="${escapeLabelValue(service)}",namespace="${escapeLabelValue(namespace)}",gitops_target="${escapeLabelValue(gitopsTarget)}"`;
}
export async function probeTarget({ targetUrl, timeoutMs = DEFAULT_TARGET_TIMEOUT_MS, fetchImpl = globalThis.fetch } = {}) {
export function requestTarget({ targetUrl, timeoutMs = DEFAULT_TARGET_TIMEOUT_MS } = {}) {
const configured = typeof targetUrl === "string" && targetUrl.trim().length > 0;
if (!configured || typeof fetchImpl !== "function") {
if (!configured) return Promise.resolve({ configured: false, success: false, statusCode: 0, durationSeconds: 0 });
let url;
try {
url = new URL(targetUrl);
} catch {
return Promise.resolve({ configured: true, success: false, statusCode: 0, durationSeconds: 0 });
}
const requestImpl = url.protocol === "https:" ? httpsRequest : url.protocol === "http:" ? httpRequest : null;
if (!requestImpl) return Promise.resolve({ configured: true, success: false, statusCode: 0, durationSeconds: 0 });
const started = Date.now();
return new Promise((resolve) => {
const finish = (result) => resolve({
configured: true,
success: false,
statusCode: 0,
durationSeconds: (Date.now() - started) / 1000,
...result
});
const request = requestImpl(url, { method: "GET", timeout: timeoutMs }, (response) => {
response.resume();
response.on("end", () => finish({ success: Boolean(response.statusCode && response.statusCode >= 200 && response.statusCode < 300), statusCode: response.statusCode || 0 }));
});
request.on("timeout", () => request.destroy());
request.on("error", () => finish({}));
request.end();
});
}
export async function probeTarget({ targetUrl, timeoutMs = DEFAULT_TARGET_TIMEOUT_MS, probeImpl = requestTarget } = {}) {
const configured = typeof targetUrl === "string" && targetUrl.trim().length > 0;
if (!configured || typeof probeImpl !== "function") {
return { configured: false, success: false, statusCode: 0, durationSeconds: 0 };
}
const controller = new AbortController();
const started = Date.now();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetchImpl(targetUrl, { signal: controller.signal });
return {
configured: true,
success: response.ok,
statusCode: response.status || 0,
durationSeconds: (Date.now() - started) / 1000
};
return await probeImpl({ targetUrl, timeoutMs });
} catch {
const started = Date.now();
return { configured: true, success: false, statusCode: 0, durationSeconds: (Date.now() - started) / 1000 };
} finally {
clearTimeout(timeout);
}
}
@@ -85,7 +109,7 @@ function sendJson(response, statusCode, body) {
response.end(payload);
}
export function createMetricsSidecarServer({ env = process.env, fetchImpl = globalThis.fetch } = {}) {
export function createMetricsSidecarServer({ env = process.env, probeImpl = requestTarget } = {}) {
return createServer(async (request, response) => {
const url = new URL(request.url || "/", "http://hwlab-metrics.local");
if (request.method === "GET" && (url.pathname === "/health" || url.pathname === "/health/live")) {
@@ -98,7 +122,7 @@ export function createMetricsSidecarServer({ env = process.env, fetchImpl = glob
}
if (request.method === "GET" && url.pathname === "/metrics") {
const timeoutMs = Number.parseInt(env.HWLAB_METRICS_TARGET_TIMEOUT_MS || "", 10) || DEFAULT_TARGET_TIMEOUT_MS;
const probe = await probeTarget({ targetUrl: env.HWLAB_METRICS_TARGET_URL, timeoutMs, fetchImpl });
const probe = await probeTarget({ targetUrl: env.HWLAB_METRICS_TARGET_URL, timeoutMs, probeImpl });
const payload = buildMetricsText({ env, probe });
response.writeHead(200, {
"content-type": "text/plain; version=0.0.4; charset=utf-8",
@@ -1,9 +1,11 @@
import assert from "node:assert/strict";
import test from "node:test";
import { createServer } from "node:http";
import { once } from "node:events";
import { pathToFileURL } from "node:url";
import { buildMetricsText, isMainModule, sanitizeLabelValue } from "./metrics-sidecar.mjs";
import { buildMetricsText, isMainModule, probeTarget, requestTarget, sanitizeLabelValue } from "./metrics-sidecar.mjs";
test("metrics text exposes stable HWLAB labels without sensitive identifiers", () => {
const text = buildMetricsText({
@@ -26,6 +28,40 @@ test("label sanitization keeps metrics labels low-risk", () => {
assert.equal(sanitizeLabelValue(""), "unknown");
});
test("health target probe uses Node HTTP request against cluster-local style targets", async () => {
const server = createServer((request, response) => {
if (request.url === "/health/live") {
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify({ status: "ok" }));
return;
}
response.writeHead(404);
response.end();
});
server.listen(0, "127.0.0.1");
await once(server, "listening");
try {
const address = server.address();
const probe = await requestTarget({ targetUrl: `http://127.0.0.1:${address.port}/health/live`, timeoutMs: 1000 });
assert.equal(probe.configured, true);
assert.equal(probe.success, true);
assert.equal(probe.statusCode, 200);
assert.ok(probe.durationSeconds >= 0);
} finally {
server.close();
await once(server, "close");
}
});
test("health target probe degrades invalid URLs to failed metrics", async () => {
const probe = await probeTarget({ targetUrl: "not a url", timeoutMs: 1000 });
assert.deepEqual({ configured: probe.configured, success: probe.success, statusCode: probe.statusCode }, {
configured: true,
success: false,
statusCode: 0
});
});
test("entrypoint detection follows ConfigMap symlink paths", () => {
const realScriptPath = "/var/run/configmaps/hwlab-v02-metrics-sidecar/..2026_06_05/metrics-sidecar.mjs";
const mountedScriptPath = "/metrics/metrics-sidecar.mjs";