162 lines
4.7 KiB
JavaScript
162 lines
4.7 KiB
JavaScript
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()));
|
|
}
|