export const HOST_TELEMETRY_SCHEMA = "nodedc.infrastructure.host-telemetry.v1"; export const HOST_TELEMETRY_PROFILE = "linux-host-telegraf-v1"; const REF_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/; const ISO_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/; export function telegrafBatchToHostTelemetry(input, context = {}) { const metrics = telegrafMetrics(input); const observedAt = latestMetricTimestamp(metrics) ?? normalizeTimestamp( context.observedAt ?? new Date().toISOString(), "observed_at", ); const byName = new Map(); for (const metric of metrics) { const values = byName.get(metric.name) ?? []; values.push(metric); byName.set(metric.name, values); } const cpu = metricWithTag(byName.get("cpu"), "cpu", "cpu-total") ?? firstMetric(byName.get("cpu")); const memory = firstMetric(byName.get("mem")); const swap = firstMetric(byName.get("swap")); const system = firstMetric(byName.get("system")); const processes = firstMetric(byName.get("processes")); const systemCpu = firstMetric(byName.get("system_cpu")); return normalizeHostTelemetrySnapshot({ schemaVersion: HOST_TELEMETRY_SCHEMA, profile: HOST_TELEMETRY_PROFILE, hostKey: context.hostKey, observedAt, source: { agent: "telegraf", agentVersion: context.agentVersion ?? "1.38.4", collectorRef: context.collectorRef ?? "service:nodedc-host-telemetry-agent", }, hardware: { hostname: context.hostname ?? metricHost(metrics), architecture: context.architecture ?? null, platform: context.platform ?? "linux", kernelRelease: context.kernelRelease ?? null, cpuModel: context.cpuModel ?? null, logicalProcessors: finiteInteger( context.logicalProcessors ?? systemCpu?.fields.cpu_count, ), }, cpu: { usagePercent: finiteNumber(cpu?.fields.usage_active) ?? percentFromIdle(cpu?.fields.usage_idle), load1: finiteNumber(system?.fields.load1), load5: finiteNumber(system?.fields.load5), load15: finiteNumber(system?.fields.load15), }, memory: { totalBytes: finiteInteger(memory?.fields.total), availableBytes: finiteInteger(memory?.fields.available), usedBytes: finiteInteger(memory?.fields.used), usedPercent: finiteNumber(memory?.fields.used_percent), }, swap: { totalBytes: finiteInteger(swap?.fields.total), freeBytes: finiteInteger(swap?.fields.free), usedBytes: finiteInteger(swap?.fields.used), usedPercent: finiteNumber(swap?.fields.used_percent), }, system: { uptimeSeconds: finiteInteger(system?.fields.uptime), users: finiteInteger(system?.fields.n_users), processes: { total: finiteInteger(processes?.fields.total), running: finiteInteger(processes?.fields.running), sleeping: finiteInteger(processes?.fields.sleeping), blocked: finiteInteger(processes?.fields.blocked), zombies: finiteInteger(processes?.fields.zombies), }, }, disks: (byName.get("disk") ?? []).map((metric) => ({ device: textOrNull(metric.tags.device), mount: textOrNull(metric.tags.path), filesystem: textOrNull(metric.tags.fstype), totalBytes: finiteInteger(metric.fields.total), freeBytes: finiteInteger(metric.fields.free), usedBytes: finiteInteger(metric.fields.used), usedPercent: finiteNumber(metric.fields.used_percent), })), network: (byName.get("net") ?? []).map((metric) => ({ interface: textOrNull(metric.tags.interface), bytesReceived: finiteInteger(metric.fields.bytes_recv), bytesSent: finiteInteger(metric.fields.bytes_sent), packetsReceived: finiteInteger(metric.fields.packets_recv), packetsSent: finiteInteger(metric.fields.packets_sent), errorsReceived: finiteInteger(metric.fields.err_in), errorsSent: finiteInteger(metric.fields.err_out), droppedReceived: finiteInteger(metric.fields.drop_in), droppedSent: finiteInteger(metric.fields.drop_out), })), services: (byName.get("systemd_units") ?? []).map((metric) => ({ name: textOrNull(metric.tags.name), loadState: textOrNull(metric.tags.load), activeState: textOrNull(metric.tags.active), subState: textOrNull(metric.tags.sub), memoryBytes: finiteInteger(metric.fields.mem_current), restarts: finiteInteger(metric.fields.restarts), pid: finiteInteger(metric.fields.pid), })), }); } export function normalizeHostTelemetrySnapshot(input) { assertPlainObject(input, "host_telemetry"); if (input.schemaVersion !== HOST_TELEMETRY_SCHEMA) { throw new TypeError("host_telemetry_schema_invalid"); } if (input.profile !== HOST_TELEMETRY_PROFILE) { throw new TypeError("host_telemetry_profile_invalid"); } const snapshot = { schemaVersion: HOST_TELEMETRY_SCHEMA, profile: HOST_TELEMETRY_PROFILE, hostKey: normalizeRef(input.hostKey, "host_key"), observedAt: normalizeTimestamp(input.observedAt, "observed_at"), source: normalizeSource(input.source), hardware: normalizeHardware(input.hardware), cpu: normalizeCpu(input.cpu), memory: normalizeMemory(input.memory, "memory"), swap: normalizeMemory(input.swap, "swap"), system: normalizeSystem(input.system), disks: normalizeArray(input.disks, normalizeDisk, 32), network: normalizeArray(input.network, normalizeNetwork, 64), services: normalizeArray(input.services, normalizeService, 64), }; return deepFreeze(snapshot); } function telegrafMetrics(input) { const candidate = Array.isArray(input) ? input : input && typeof input === "object" && Array.isArray(input.metrics) ? input.metrics : input && typeof input === "object" ? [input] : null; if (!candidate || candidate.length < 1 || candidate.length > 512) { throw new TypeError("host_telemetry_telegraf_batch_invalid"); } return candidate.map((metric) => { assertPlainObject(metric, "host_telemetry_telegraf_metric"); assertPlainObject(metric.fields, "host_telemetry_telegraf_fields"); const name = text(metric.name, 1, 80, "host_telemetry_telegraf_name_invalid"); const tags = metric.tags == null ? {} : metric.tags; assertPlainObject(tags, "host_telemetry_telegraf_tags"); return { name, fields: { ...metric.fields }, tags: { ...tags }, timestamp: metric.timestamp }; }); } function latestMetricTimestamp(metrics) { let latest = null; for (const metric of metrics) { const raw = Number(metric.timestamp); if (!Number.isFinite(raw) || raw <= 0) continue; const milliseconds = raw > 10_000_000_000 ? raw / 1_000_000 : raw * 1000; if (!Number.isFinite(milliseconds)) continue; const value = new Date(milliseconds).toISOString(); if (!latest || value > latest) latest = value; } return latest; } function metricWithTag(metrics = [], key, value) { return metrics.find((metric) => metric.tags[key] === value) ?? null; } function firstMetric(metrics = []) { return metrics[0] ?? null; } function metricHost(metrics) { for (const metric of metrics) { if (typeof metric.tags.host === "string" && metric.tags.host.trim()) { return metric.tags.host.trim().slice(0, 160); } } return null; } function normalizeSource(input) { assertPlainObject(input, "host_telemetry_source"); return { agent: text(input.agent, 1, 64, "host_telemetry_agent_invalid"), agentVersion: text(input.agentVersion, 1, 64, "host_telemetry_agent_version_invalid"), collectorRef: normalizeRef(input.collectorRef, "collector_ref"), }; } function normalizeHardware(input) { assertPlainObject(input, "host_telemetry_hardware"); return { hostname: optionalText(input.hostname, 160), architecture: optionalText(input.architecture, 64), platform: optionalText(input.platform, 64), kernelRelease: optionalText(input.kernelRelease, 160), cpuModel: optionalText(input.cpuModel, 256), logicalProcessors: optionalInteger(input.logicalProcessors, 1_024), }; } function normalizeCpu(input) { assertPlainObject(input, "host_telemetry_cpu"); return { usagePercent: optionalPercent(input.usagePercent), load1: optionalNumber(input.load1, 0, 100_000), load5: optionalNumber(input.load5, 0, 100_000), load15: optionalNumber(input.load15, 0, 100_000), }; } function normalizeMemory(input, field) { assertPlainObject(input, `host_telemetry_${field}`); return { totalBytes: optionalInteger(input.totalBytes, Number.MAX_SAFE_INTEGER), availableBytes: optionalInteger(input.availableBytes, Number.MAX_SAFE_INTEGER), freeBytes: optionalInteger(input.freeBytes, Number.MAX_SAFE_INTEGER), usedBytes: optionalInteger(input.usedBytes, Number.MAX_SAFE_INTEGER), usedPercent: optionalPercent(input.usedPercent), }; } function normalizeSystem(input) { assertPlainObject(input, "host_telemetry_system"); assertPlainObject(input.processes, "host_telemetry_processes"); return { uptimeSeconds: optionalInteger(input.uptimeSeconds, Number.MAX_SAFE_INTEGER), users: optionalInteger(input.users, 1_000_000), processes: { total: optionalInteger(input.processes.total, 1_000_000), running: optionalInteger(input.processes.running, 1_000_000), sleeping: optionalInteger(input.processes.sleeping, 1_000_000), blocked: optionalInteger(input.processes.blocked, 1_000_000), zombies: optionalInteger(input.processes.zombies, 1_000_000), }, }; } function normalizeDisk(input) { assertPlainObject(input, "host_telemetry_disk"); return { device: optionalText(input.device, 256), mount: optionalText(input.mount, 512), filesystem: optionalText(input.filesystem, 64), totalBytes: optionalInteger(input.totalBytes, Number.MAX_SAFE_INTEGER), freeBytes: optionalInteger(input.freeBytes, Number.MAX_SAFE_INTEGER), usedBytes: optionalInteger(input.usedBytes, Number.MAX_SAFE_INTEGER), usedPercent: optionalPercent(input.usedPercent), }; } function normalizeNetwork(input) { assertPlainObject(input, "host_telemetry_network"); return { interface: optionalText(input.interface, 64), bytesReceived: optionalInteger(input.bytesReceived, Number.MAX_SAFE_INTEGER), bytesSent: optionalInteger(input.bytesSent, Number.MAX_SAFE_INTEGER), packetsReceived: optionalInteger(input.packetsReceived, Number.MAX_SAFE_INTEGER), packetsSent: optionalInteger(input.packetsSent, Number.MAX_SAFE_INTEGER), errorsReceived: optionalInteger(input.errorsReceived, Number.MAX_SAFE_INTEGER), errorsSent: optionalInteger(input.errorsSent, Number.MAX_SAFE_INTEGER), droppedReceived: optionalInteger(input.droppedReceived, Number.MAX_SAFE_INTEGER), droppedSent: optionalInteger(input.droppedSent, Number.MAX_SAFE_INTEGER), }; } function normalizeService(input) { assertPlainObject(input, "host_telemetry_service"); return { name: optionalText(input.name, 160), loadState: optionalText(input.loadState, 64), activeState: optionalText(input.activeState, 64), subState: optionalText(input.subState, 64), memoryBytes: optionalInteger(input.memoryBytes, Number.MAX_SAFE_INTEGER), restarts: optionalInteger(input.restarts, Number.MAX_SAFE_INTEGER), pid: optionalInteger(input.pid, Number.MAX_SAFE_INTEGER), }; } function normalizeArray(value, mapper, maximum) { if (!Array.isArray(value) || value.length > maximum) { throw new TypeError("host_telemetry_collection_invalid"); } return value.map(mapper); } function normalizeRef(value, field) { if (typeof value !== "string" || !REF_RE.test(value)) { throw new TypeError(`host_telemetry_${field}_invalid`); } return value; } function normalizeTimestamp(value, field) { if (typeof value !== "string" || !ISO_TIMESTAMP_RE.test(value)) { throw new TypeError(`host_telemetry_${field}_invalid`); } const timestamp = new Date(value); if (!Number.isFinite(timestamp.valueOf())) { throw new TypeError(`host_telemetry_${field}_invalid`); } return timestamp.toISOString(); } function text(value, minimum, maximum, errorCode) { if (typeof value !== "string") throw new TypeError(errorCode); const normalized = value.trim(); if (normalized.length < minimum || normalized.length > maximum) { throw new TypeError(errorCode); } return normalized; } function optionalText(value, maximum) { if (value == null || value === "") return null; return text(value, 1, maximum, "host_telemetry_text_invalid"); } function textOrNull(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function optionalInteger(value, maximum) { if (value == null) return null; const normalized = Number(value); if (!Number.isSafeInteger(normalized) || normalized < 0 || normalized > maximum) { throw new TypeError("host_telemetry_integer_invalid"); } return normalized; } function optionalNumber(value, minimum, maximum) { if (value == null) return null; const normalized = Number(value); if (!Number.isFinite(normalized) || normalized < minimum || normalized > maximum) { throw new TypeError("host_telemetry_number_invalid"); } return normalized; } function optionalPercent(value) { return optionalNumber(value, 0, 100); } function finiteInteger(value) { const normalized = Number(value); return Number.isSafeInteger(normalized) && normalized >= 0 ? normalized : null; } function finiteNumber(value) { const normalized = Number(value); return Number.isFinite(normalized) && normalized >= 0 ? normalized : null; } function percentFromIdle(value) { const idle = finiteNumber(value); return idle == null ? null : Math.max(0, Math.min(100, 100 - idle)); } function assertPlainObject(value, field) { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new TypeError(`${field}_invalid`); } } function deepFreeze(value) { if (value && typeof value === "object" && !Object.isFrozen(value)) { Object.freeze(value); for (const nested of Object.values(value)) deepFreeze(nested); } return value; }