From 25e533e434bfe33708e72b897d630fd7ac64dd90 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Fri, 5 Jun 2026 09:12:42 +0800 Subject: [PATCH] fix: use HTTP request for v02 metrics health probes --- internal/dev-entrypoint/metrics-sidecar.mjs | 58 +++++++++++++------ .../dev-entrypoint/metrics-sidecar.test.mjs | 38 +++++++++++- 2 files changed, 78 insertions(+), 18 deletions(-) diff --git a/internal/dev-entrypoint/metrics-sidecar.mjs b/internal/dev-entrypoint/metrics-sidecar.mjs index 409b562d..a6e80b89 100644 --- a/internal/dev-entrypoint/metrics-sidecar.mjs +++ b/internal/dev-entrypoint/metrics-sidecar.mjs @@ -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", diff --git a/internal/dev-entrypoint/metrics-sidecar.test.mjs b/internal/dev-entrypoint/metrics-sidecar.test.mjs index 7d84a7de..ddaed9f3 100644 --- a/internal/dev-entrypoint/metrics-sidecar.test.mjs +++ b/internal/dev-entrypoint/metrics-sidecar.test.mjs @@ -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";