224 lines
11 KiB
JavaScript
224 lines
11 KiB
JavaScript
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";
|
|
|
|
const DEFAULT_PORT = 9100;
|
|
const DEFAULT_TARGET_TIMEOUT_MS = 2000;
|
|
const DEFAULT_EXTRA_METRICS_MAX_BYTES = 64 * 1024;
|
|
const startedAtMs = Date.now();
|
|
|
|
export function sanitizeLabelValue(value, fallback = "unknown") {
|
|
const text = String(value ?? "").trim();
|
|
const sanitized = text.replace(/[^A-Za-z0-9_.:-]/gu, "_").slice(0, 120);
|
|
return sanitized || fallback;
|
|
}
|
|
|
|
export function escapeLabelValue(value) {
|
|
return String(value ?? "").replace(/\\/gu, "\\\\").replace(/\n/gu, "\\n").replace(/"/gu, "\\\"");
|
|
}
|
|
|
|
function metricLabels(env) {
|
|
const service = sanitizeLabelValue(env.HWLAB_METRICS_SERVICE_ID || env.HWLAB_SERVICE_ID);
|
|
const namespace = sanitizeLabelValue(env.HWLAB_METRICS_NAMESPACE || env.POD_NAMESPACE || "hwlab-v02");
|
|
const gitopsTarget = sanitizeLabelValue(env.HWLAB_METRICS_GITOPS_TARGET || env.HWLAB_GITOPS_TARGET || "v02");
|
|
return `service="${escapeLabelValue(service)}",namespace="${escapeLabelValue(namespace)}",gitops_target="${escapeLabelValue(gitopsTarget)}"`;
|
|
}
|
|
|
|
export function requestTarget({ targetUrl, timeoutMs = DEFAULT_TARGET_TIMEOUT_MS, collectBody = false, maxBodyBytes = DEFAULT_EXTRA_METRICS_MAX_BYTES } = {}) {
|
|
const configured = typeof targetUrl === "string" && targetUrl.trim().length > 0;
|
|
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) => {
|
|
const chunks = [];
|
|
let bytes = 0;
|
|
if (collectBody) {
|
|
response.on("data", (chunk) => {
|
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
bytes += buffer.length;
|
|
if (bytes <= maxBodyBytes) chunks.push(buffer);
|
|
});
|
|
} else {
|
|
response.resume();
|
|
}
|
|
response.on("end", () => finish({
|
|
success: Boolean(response.statusCode && response.statusCode >= 200 && response.statusCode < 300),
|
|
statusCode: response.statusCode || 0,
|
|
body: collectBody ? Buffer.concat(chunks).toString("utf8") : undefined,
|
|
bodyBytes: collectBody ? Math.min(bytes, maxBodyBytes) : 0,
|
|
truncated: collectBody ? bytes > maxBodyBytes : false
|
|
}));
|
|
});
|
|
request.on("timeout", () => request.destroy());
|
|
request.on("error", () => finish({}));
|
|
request.end();
|
|
});
|
|
}
|
|
|
|
export async function probeExtraMetrics({ targetUrl, timeoutMs = DEFAULT_TARGET_TIMEOUT_MS, maxBodyBytes = DEFAULT_EXTRA_METRICS_MAX_BYTES, 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, text: "" };
|
|
}
|
|
try {
|
|
const result = await probeImpl({ targetUrl, timeoutMs, collectBody: true, maxBodyBytes });
|
|
return {
|
|
configured: true,
|
|
success: Boolean(result.success),
|
|
statusCode: result.statusCode || 0,
|
|
durationSeconds: result.durationSeconds || 0,
|
|
text: result.success ? sanitizeExtraMetricsText(result.body || "") : ""
|
|
};
|
|
} catch {
|
|
return { configured: true, success: false, statusCode: 0, durationSeconds: 0, text: "" };
|
|
}
|
|
}
|
|
|
|
export function sanitizeExtraMetricsText(text) {
|
|
const lines = String(text ?? "").split(/\r?\n/gu);
|
|
return lines.filter((line) => {
|
|
if (!line) return true;
|
|
if (/^#\s+(?:HELP|TYPE)\s+hwlab_[A-Za-z_:][A-Za-z0-9_:]*\b/u.test(line)) return true;
|
|
return /^hwlab_[A-Za-z_:][A-Za-z0-9_:]*(?:\{[^\n{}]*\})?\s+[-+0-9.eE]+$/u.test(line);
|
|
}).join("\n");
|
|
}
|
|
|
|
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 };
|
|
}
|
|
try {
|
|
return await probeImpl({ targetUrl, timeoutMs });
|
|
} catch {
|
|
const started = Date.now();
|
|
return { configured: true, success: false, statusCode: 0, durationSeconds: (Date.now() - started) / 1000 };
|
|
}
|
|
}
|
|
|
|
export function buildMetricsText({ env = process.env, probe = null, extraMetrics = null, now = Date.now() } = {}) {
|
|
const labels = metricLabels(env);
|
|
const uptimeSeconds = Math.max(0, (now - startedAtMs) / 1000);
|
|
const healthProbe = probe ?? { configured: false, success: false, statusCode: 0, durationSeconds: 0 };
|
|
const extraProbe = extraMetrics ?? { configured: false, success: false, statusCode: 0, durationSeconds: 0, text: "" };
|
|
const lines = [
|
|
"# HELP hwlab_service_up HWLAB metrics sidecar availability.",
|
|
"# TYPE hwlab_service_up gauge",
|
|
`hwlab_service_up{${labels}} 1`,
|
|
"# HELP hwlab_service_info Static HWLAB service metrics identity without high-cardinality request identifiers.",
|
|
"# TYPE hwlab_service_info gauge",
|
|
`hwlab_service_info{${labels}} 1`,
|
|
"# HELP hwlab_service_process_uptime_seconds Metrics sidecar process uptime in seconds.",
|
|
"# TYPE hwlab_service_process_uptime_seconds gauge",
|
|
`hwlab_service_process_uptime_seconds{${labels}} ${uptimeSeconds.toFixed(3)}`,
|
|
"# HELP hwlab_service_health_probe_configured Whether the sidecar has a cluster-local health probe target.",
|
|
"# TYPE hwlab_service_health_probe_configured gauge",
|
|
`hwlab_service_health_probe_configured{${labels}} ${healthProbe.configured ? 1 : 0}`,
|
|
"# HELP hwlab_service_health_probe_success Whether the most recent cluster-local health probe succeeded.",
|
|
"# TYPE hwlab_service_health_probe_success gauge",
|
|
`hwlab_service_health_probe_success{${labels}} ${healthProbe.success ? 1 : 0}`,
|
|
"# HELP hwlab_service_health_probe_status_code Most recent cluster-local health probe HTTP status code, or 0 when unavailable.",
|
|
"# TYPE hwlab_service_health_probe_status_code gauge",
|
|
`hwlab_service_health_probe_status_code{${labels}} ${Number.isInteger(healthProbe.statusCode) ? healthProbe.statusCode : 0}`,
|
|
"# HELP hwlab_service_health_probe_duration_seconds Most recent cluster-local health probe duration in seconds.",
|
|
"# TYPE hwlab_service_health_probe_duration_seconds gauge",
|
|
`hwlab_service_health_probe_duration_seconds{${labels}} ${Number(healthProbe.durationSeconds || 0).toFixed(3)}`,
|
|
"# HELP hwlab_service_extra_metrics_configured Whether the sidecar has an additional cluster-local metrics target.",
|
|
"# TYPE hwlab_service_extra_metrics_configured gauge",
|
|
`hwlab_service_extra_metrics_configured{${labels}} ${extraProbe.configured ? 1 : 0}`,
|
|
"# HELP hwlab_service_extra_metrics_success Whether the most recent additional metrics fetch succeeded.",
|
|
"# TYPE hwlab_service_extra_metrics_success gauge",
|
|
`hwlab_service_extra_metrics_success{${labels}} ${extraProbe.success ? 1 : 0}`,
|
|
"# HELP hwlab_service_extra_metrics_status_code Most recent additional metrics HTTP status code, or 0 when unavailable.",
|
|
"# TYPE hwlab_service_extra_metrics_status_code gauge",
|
|
`hwlab_service_extra_metrics_status_code{${labels}} ${Number.isInteger(extraProbe.statusCode) ? extraProbe.statusCode : 0}`,
|
|
"# HELP hwlab_service_extra_metrics_duration_seconds Most recent additional metrics fetch duration in seconds.",
|
|
"# TYPE hwlab_service_extra_metrics_duration_seconds gauge",
|
|
`hwlab_service_extra_metrics_duration_seconds{${labels}} ${Number(extraProbe.durationSeconds || 0).toFixed(3)}`,
|
|
""
|
|
];
|
|
if (extraProbe.text) {
|
|
lines.push(extraProbe.text.trimEnd(), "");
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
|
|
function sendJson(response, statusCode, body) {
|
|
const payload = JSON.stringify(body) + "\n";
|
|
response.writeHead(statusCode, {
|
|
"content-type": "application/json; charset=utf-8",
|
|
"content-length": Buffer.byteLength(payload)
|
|
});
|
|
response.end(payload);
|
|
}
|
|
|
|
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")) {
|
|
sendJson(response, 200, {
|
|
serviceId: sanitizeLabelValue(env.HWLAB_METRICS_SERVICE_ID || env.HWLAB_SERVICE_ID),
|
|
status: "ok",
|
|
metricsPath: "/metrics"
|
|
});
|
|
return;
|
|
}
|
|
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, probeImpl });
|
|
const extraTimeoutMs = Number.parseInt(env.HWLAB_METRICS_EXTRA_TIMEOUT_MS || "", 10) || DEFAULT_TARGET_TIMEOUT_MS;
|
|
const extraMetrics = await probeExtraMetrics({ targetUrl: env.HWLAB_METRICS_EXTRA_URL, timeoutMs: extraTimeoutMs, probeImpl });
|
|
const payload = buildMetricsText({ env, probe, extraMetrics });
|
|
response.writeHead(200, {
|
|
"content-type": "text/plain; version=0.0.4; charset=utf-8",
|
|
"content-length": Buffer.byteLength(payload)
|
|
});
|
|
response.end(payload);
|
|
return;
|
|
}
|
|
sendJson(response, 404, { error: "not_found", path: url.pathname });
|
|
});
|
|
}
|
|
|
|
export function startMetricsSidecar({ env = process.env } = {}) {
|
|
const port = Number.parseInt(env.HWLAB_METRICS_PORT || env.PORT || "", 10) || DEFAULT_PORT;
|
|
const server = createMetricsSidecarServer({ env });
|
|
server.listen(port, "0.0.0.0", () => {
|
|
process.stdout.write(JSON.stringify({ serviceId: env.HWLAB_METRICS_SERVICE_ID || "hwlab-unknown", status: "metrics-listening", port }) + "\n");
|
|
});
|
|
return server;
|
|
}
|
|
|
|
export function isMainModule({ moduleUrl = import.meta.url, argvPath = process.argv[1], realpath = realpathSync } = {}) {
|
|
if (!argvPath) return false;
|
|
if (moduleUrl === pathToFileURL(argvPath).href) return true;
|
|
try {
|
|
return moduleUrl === pathToFileURL(realpath(argvPath)).href;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (isMainModule()) {
|
|
startMetricsSidecar();
|
|
}
|