import { createServer } from "node:http"; import { arch, cpus, hostname, platform, release } from "node:os"; import { telegrafBatchToHostTelemetry, } from "../../packages/infrastructure-telemetry-contract/src/index.mjs"; const MAX_BODY_BYTES = 512 * 1024; export function createHostTelemetryCollector(options = {}) { const config = normalizeConfig(options); let accepted = 0; let rejected = 0; let lastObservedAt = null; let lastAcceptedAt = null; let lastErrorCode = null; let pending = false; const server = createServer(async (request, response) => { response.setHeader("Content-Type", "application/json; charset=utf-8"); response.setHeader("Cache-Control", "no-store"); response.setHeader("X-Content-Type-Options", "nosniff"); if (request.method !== "POST" || request.url !== "/internal/v1/host-telemetry") { response.statusCode = 404; response.end(JSON.stringify({ ok: false, error: "not_found" })); return; } if (pending) { rejected += 1; response.statusCode = 429; response.end(JSON.stringify({ ok: false, error: "host_telemetry_busy" })); return; } pending = true; try { const body = await readJsonBody(request, MAX_BODY_BYTES); const processors = cpus(); const snapshot = telegrafBatchToHostTelemetry(body, { hostKey: config.hostKey, hostname: hostname(), architecture: arch(), platform: platform(), kernelRelease: release(), cpuModel: processors[0]?.model ?? null, logicalProcessors: processors.length || null, agentVersion: config.agentVersion, }); lastObservedAt = snapshot.observedAt; await config.submit(snapshot); accepted += 1; lastAcceptedAt = new Date().toISOString(); lastErrorCode = null; response.statusCode = 202; response.end(JSON.stringify({ ok: true, accepted: true })); } catch (error) { rejected += 1; lastErrorCode = safeErrorCode(error); response.statusCode = lastErrorCode.includes("too_large") ? 413 : 503; response.end(JSON.stringify({ ok: false, error: lastErrorCode })); } finally { pending = false; } }); return Object.freeze({ async start() { await listen(server, config.port, config.host); return server.address(); }, async stop() { await closeServer(server); }, status() { return Object.freeze({ listening: server.listening, host: config.host, port: config.port, profile: "linux-host-telegraf-v1", accepted, rejected, pending, lastObservedAt, lastAcceptedAt, lastErrorCode, }); }, }); } function normalizeConfig(options) { if (typeof options.submit !== "function") { throw new TypeError("host_telemetry_submit_required"); } return Object.freeze({ submit: options.submit, hostKey: normalizeRef(options.hostKey, "host_telemetry_host_key_invalid"), agentVersion: String(options.agentVersion ?? "1.38.4"), host: normalizeLoopbackHost(options.host ?? "127.0.0.1"), port: normalizePort(options.port ?? 18223), }); } async function readJsonBody(request, maximumBytes) { const chunks = []; let total = 0; for await (const chunk of request) { total += chunk.length; if (total > maximumBytes) throw new Error("host_telemetry_body_too_large"); chunks.push(chunk); } if (total < 2) throw new Error("host_telemetry_body_invalid"); try { return JSON.parse(Buffer.concat(chunks).toString("utf8")); } catch { throw new Error("host_telemetry_body_invalid"); } } function normalizeRef(value, errorCode) { if (typeof value !== "string" || !/^[a-z][a-z0-9-]{1,62}$/.test(value)) { throw new TypeError(errorCode); } return value; } function normalizeLoopbackHost(value) { if (!["127.0.0.1", "::1"].includes(value)) { throw new TypeError("host_telemetry_host_invalid"); } return value; } function normalizePort(value) { const normalized = Number(value); if (!Number.isSafeInteger(normalized) || normalized < 0 || normalized > 65_535) { throw new TypeError("host_telemetry_port_invalid"); } return normalized; } function safeErrorCode(error) { const value = String(error?.message || "host_telemetry_internal_error"); return /^[a-z0-9_.:-]{3,160}$/.test(value) ? value : "host_telemetry_internal_error"; } function listen(server, port, host) { return new Promise((resolve, reject) => { server.once("error", reject); server.listen(port, host, () => { server.off("error", reject); resolve(); }); }); } function closeServer(server) { if (!server.listening) return Promise.resolve(); return new Promise((resolve) => server.close(() => resolve())); }