UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

373 lines (372 loc) • 15.2 kB
/** * OTLP/HTTP JSON transport with response-aware acknowledgement. The SDK's HTTP * exporter reports SUCCESS even for partialSuccess or malformed responses, so * its callback cannot establish that every record in our batch was accepted. * Serialization stays with the official OpenTelemetry implementation. */ import { readFileSync } from "node:fs"; import { Agent as HttpAgent, request as httpRequest, validateHeaderName, validateHeaderValue, } from "node:http"; import { Agent as HttpsAgent, request as httpsRequest } from "node:https"; import { setTimeout as delay } from "node:timers/promises"; import { promisify } from "node:util"; import { gzip } from "node:zlib"; import { createRequire } from "node:module"; /** * @opentelemetry/core and @opentelemetry/otlp-transformer are loaded via * require() on first actual use (this file itself stays a normal static * import, since it is genuinely CommonJS-safe to require() these two — * unlike a local ESM file, which would need Node's newer require(esm) * support and break on Node <22.12). Both packages ship plain CJS builds. */ const require = createRequire(import.meta.url); let ExportResultCode; let parseKeyPairsIntoRecord; let JsonLogsSerializer; let transportDepsLoaded = false; function loadTransportDeps() { if (transportDepsLoaded) { return; } ({ ExportResultCode, parseKeyPairsIntoRecord, } = require("@opentelemetry/core")); ({ JsonLogsSerializer } = require("@opentelemetry/otlp-transformer")); transportDepsLoaded = true; } const gzipAsync = promisify(gzip); const MAX_RESPONSE_BYTES = 64 * 1024; const MAX_EXPORT_ATTEMPTS = 5; const RETRYABLE_STATUSES = new Set([429, 502, 503, 504]); const RETRYABLE_NETWORK_CODES = new Set([ "ECONNRESET", "ECONNREFUSED", "EPIPE", "ETIMEDOUT", "EAI_AGAIN", "ENOTFOUND", "ENETUNREACH", "EHOSTUNREACH", ]); class ProxyOtlpResponseError extends Error { retryable; retryAfterMs; constructor(message, retryable = false, retryAfterMs) { super(message); this.retryable = retryable; this.retryAfterMs = retryAfterMs; this.name = "ProxyOtlpResponseError"; } } /** Only explicit transient transport failures may be retained and retried. */ export function getProxyOtlpRetryAfter(error) { return error instanceof ProxyOtlpResponseError && error.retryable ? (error.retryAfterMs ?? 0) : undefined; } function signalSetting(suffix) { return (process.env[`OTEL_EXPORTER_OTLP_LOGS_${suffix}`]?.trim() || process.env[`OTEL_EXPORTER_OTLP_${suffix}`]?.trim() || undefined); } function certificate(suffix) { const path = signalSetting(suffix); if (!path) { return undefined; } try { return readFileSync(path); } catch { // File paths, headers, backend bodies and native TLS errors can contain // credentials. Export diagnostics intentionally expose only fixed reasons. throw new Error(`Proxy OTLP ${suffix} file could not be read`); } } function retryAfter(value) { if (!value) { return undefined; } if (/^\d+$/.test(value.trim())) { return Number(value) * 1000; } const at = Date.parse(value); return Number.isFinite(at) ? Math.max(0, at - Date.now()) : undefined; } function validateAcknowledgement(bytes) { let payload; try { payload = JSON.parse(bytes.toString("utf8")); } catch { // Empty JSON is not a serialized ExportLogsServiceResponse. Its valid // empty message is {}, unlike the zero-byte protobuf representation. throw new ProxyOtlpResponseError("OTLP response JSON is empty or malformed"); } if (!payload || typeof payload !== "object" || Array.isArray(payload)) { throw new ProxyOtlpResponseError("OTLP response must be a JSON object"); } if (Object.keys(payload).some((key) => key !== "partialSuccess" && key !== "partial_success")) { throw new ProxyOtlpResponseError("OTLP response has unexpected fields"); } if ("partialSuccess" in payload && "partial_success" in payload) { throw new ProxyOtlpResponseError("OTLP response has ambiguous field aliases"); } const partial = "partialSuccess" in payload ? payload.partialSuccess : "partial_success" in payload ? payload.partial_success : undefined; if (partial === null || partial === undefined) { return; } if (typeof partial !== "object" || Array.isArray(partial)) { throw new ProxyOtlpResponseError("OTLP partial success response is malformed"); } const allowedFields = new Set([ "rejectedLogRecords", "rejected_log_records", "errorMessage", "error_message", ]); if (Object.keys(partial).some((key) => !allowedFields.has(key))) { throw new ProxyOtlpResponseError("OTLP partial success response has unexpected fields"); } if (("rejectedLogRecords" in partial && "rejected_log_records" in partial) || ("errorMessage" in partial && "error_message" in partial)) { throw new ProxyOtlpResponseError("OTLP partial success has ambiguous field aliases"); } const message = "errorMessage" in partial ? partial.errorMessage : "error_message" in partial ? partial.error_message : undefined; if (message !== null && message !== undefined && typeof message !== "string") { throw new ProxyOtlpResponseError("OTLP partial success response is malformed"); } const rejected = "rejectedLogRecords" in partial ? partial.rejectedLogRecords : "rejected_log_records" in partial ? partial.rejected_log_records : 0; // ProtoJSON accepts integer strings or numbers; null is the absent default. if (rejected === null || rejected === 0 || rejected === "0") { return; } const integer = typeof rejected === "string" && /^\d+$/.test(rejected) ? BigInt(rejected) : typeof rejected === "number" && Number.isSafeInteger(rejected) && rejected >= 0 ? BigInt(rejected) : undefined; if (integer === undefined || integer > 9223372036854775807n) { throw new ProxyOtlpResponseError("OTLP rejected record count is malformed"); } if (integer > 0n) { // We cannot identify the rejected subset. Mark the entire batch uncertain; // replaying a known partial response would duplicate its accepted subset. throw new ProxyOtlpResponseError("OTLP partial success rejected log records"); } } /** Both proxy queues use this transport; success is not backend persistence. */ export function createProxyOtlpLogTransport(endpoint, timeoutMillis) { loadTransportDeps(); let url; try { url = new URL(endpoint); } catch { throw new Error("Proxy OTLP endpoint is invalid"); } if (!new Set(["http:", "https:"]).has(url.protocol)) { throw new Error("Proxy OTLP endpoint must use HTTP or HTTPS"); } if (url.username || url.password) { throw new Error("Proxy OTLP endpoint credentials must use OTLP headers"); } const compression = signalSetting("COMPRESSION") ?? "none"; if (compression !== "none" && compression !== "gzip") { throw new Error("Proxy OTLP compression must be none or gzip"); } const headers = {}; try { for (const raw of [ process.env.OTEL_EXPORTER_OTLP_HEADERS, process.env.OTEL_EXPORTER_OTLP_LOGS_HEADERS, ]) { for (const [key, value] of Object.entries(parseKeyPairsIntoRecord(raw))) { validateHeaderName(key); validateHeaderValue(key, value); headers[key.toLowerCase()] = value; } } } catch { throw new Error("Proxy OTLP headers are invalid"); } // These describe the serialized wire payload, irrespective of user headers. headers["content-type"] = "application/json"; headers["accept"] = "application/json"; headers["accept-encoding"] = "identity"; delete headers["content-encoding"]; if (compression === "gzip") { headers["content-encoding"] = "gzip"; } const tls = { ca: certificate("CERTIFICATE"), cert: certificate("CLIENT_CERTIFICATE"), key: certificate("CLIENT_KEY"), }; if (Boolean(tls.cert) !== Boolean(tls.key)) { throw new Error("Proxy OTLP client certificate and key must be set together"); } const agent = url.protocol === "https:" ? new HttpsAgent({ keepAlive: true, maxSockets: 30, ...tls }) : new HttpAgent({ keepAlive: true, maxSockets: 30 }); const request = url.protocol === "https:" ? httpsRequest : httpRequest; let closed = false; const active = new Set(); const controllers = new Set(); const sendOnce = (data, signal) => new Promise((resolve, reject) => { let receivedResponse = false; const req = request(url, { method: "POST", agent, signal, headers: { ...headers, "content-length": String(data.byteLength) }, }, (response) => { receivedResponse = true; const status = response.statusCode ?? 0; const fail = (error) => { reject(error); response.destroy(); }; // An HTTP status, a partial response or a broken success response must // never be mistaken for an acknowledgement of individual log records. if (status !== 200) { fail(new ProxyOtlpResponseError(`OTLP HTTP ${status}`, RETRYABLE_STATUSES.has(status), retryAfter(response.headers["retry-after"]))); return; } const encoding = response.headers["content-encoding"]; if (encoding && encoding !== "identity") { fail(new ProxyOtlpResponseError("OTLP response encoding is unsupported")); return; } const contentType = response.headers["content-type"]; if (contentType && contentType.split(";", 1)[0].trim().toLowerCase() !== "application/json") { fail(new ProxyOtlpResponseError("OTLP response content type is not JSON")); return; } const chunks = []; let size = 0; response.on("data", (chunk) => { size += chunk.length; if (size > MAX_RESPONSE_BYTES) { fail(new ProxyOtlpResponseError("OTLP response exceeds 64 KiB limit")); return; } chunks.push(chunk); }); response.on("aborted", () => reject(new ProxyOtlpResponseError("OTLP success response was interrupted"))); response.on("error", () => reject(new ProxyOtlpResponseError("OTLP success response could not be read"))); response.on("end", () => { try { validateAcknowledgement(Buffer.concat(chunks)); resolve(); } catch (error) { reject(error); } }); }); req.on("error", (error) => reject(new ProxyOtlpResponseError(signal.aborted ? "OTLP export deadline exceeded" : "OTLP transport failed", !receivedResponse && (signal.aborted || RETRYABLE_NETWORK_CODES.has(error.code ?? ""))))); req.end(data); }); const send = async (records) => { const controller = new AbortController(); controllers.add(controller); const deadlineAt = Date.now() + timeoutMillis; const deadline = setTimeout(() => controller.abort(), timeoutMillis); deadline.unref(); try { const serialized = JsonLogsSerializer.serializeRequest(records); if (!serialized) { throw new ProxyOtlpResponseError("OTLP serialization produced no payload"); } // Serialize and compress once, so ambiguous retries retain byte-for-byte // event identities and payloads for query-side deduplication. const bytes = compression === "gzip" ? await gzipAsync(serialized) : serialized; let attempt = 0; while (!controller.signal.aborted && Date.now() < deadlineAt) { attempt++; try { await sendOnce(bytes, controller.signal); return; } catch (error) { if (!(error instanceof ProxyOtlpResponseError) || !error.retryable) { throw error; } if (attempt >= MAX_EXPORT_ATTEMPTS) { throw new ProxyOtlpResponseError("OTLP retry attempt limit reached", true, error.retryAfterMs); } const exponential = Math.min(5000, 1000 * 2 ** Math.min(attempt - 1, 3)); // Retry-After is a lower bound, never permission to hammer a busy // collector. Even Retry-After: 0 retains exponential backoff+jitter. const wait = Math.max(error.retryAfterMs ?? 0, exponential * (0.8 + Math.random() * 0.4)); if (wait >= deadlineAt - Date.now()) { throw new ProxyOtlpResponseError("OTLP retry exceeds export deadline", true, wait); } await delay(wait, undefined, { signal: controller.signal }); } } throw new ProxyOtlpResponseError("OTLP export deadline exceeded", true); } catch (error) { if (error instanceof ProxyOtlpResponseError) { throw error; } throw new ProxyOtlpResponseError(controller.signal.aborted ? "OTLP export deadline exceeded" : "OTLP export failed", controller.signal.aborted); } finally { clearTimeout(deadline); controllers.delete(controller); } }; return { abortPending() { closed = true; for (const controller of controllers) { controller.abort(); } agent.destroy(); }, export(records, callback) { if (closed) { callback({ code: ExportResultCode.FAILED, error: new Error("OTLP transport is shut down"), }); return; } const operation = send(records).then(() => callback({ code: ExportResultCode.SUCCESS }), (error) => callback({ code: ExportResultCode.FAILED, error })); active.add(operation); void operation.finally(() => active.delete(operation)).catch(() => { }); }, async shutdown() { closed = true; await Promise.allSettled(active); agent.destroy(); }, }; }