UNPKG

autotel

Version:
274 lines (270 loc) 9.69 kB
import { n as safeRequire } from "./node-require-vROmTeJ8.js"; import { ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_NETWORK_PROTOCOL_VERSION, ATTR_SERVER_ADDRESS, ATTR_SERVER_PORT, ATTR_URL_FULL, ATTR_URL_PATH, ATTR_URL_SCHEME, ATTR_USER_AGENT_ORIGINAL } from "@opentelemetry/semantic-conventions"; import { SpanKind, SpanStatusCode, context, defaultTextMapGetter, defaultTextMapSetter, propagation, trace } from "@opentelemetry/api"; import { SeverityNumber, logs } from "@opentelemetry/api-logs"; //#region src/diagnostics/channel.ts /** * Edge-safe wrappers over Node's `diagnostics_channel`. * * The module is loaded lazily through {@link safeRequire} — never a static * `node:` import — so merely importing this file is side-effect-free and bundles * cleanly for browser/edge targets, where every subscribe call degrades to a * no-op (returning an unsubscribe that does nothing). This is the shared * primitive behind autotel's diagnostics-channel integrations (console capture, * HTTP spans) and any app- or library-specific channel you want to bridge into * a span/event. * * `diagnostics_channel.subscribe` (Node 18.7+) and `tracingChannel` (Node 19+) * are used; autotel targets Node 22+, but on any runtime that lacks them the * loader returns `undefined` and the helpers no-op. */ let cached; function loadDiagnosticsChannel() { if (cached !== void 0) return cached ?? void 0; cached = safeRequire("node:diagnostics_channel") ?? null; return cached ?? void 0; } /** Whether Node's `diagnostics_channel` is available in this runtime. */ function diagnosticsChannelAvailable() { return loadDiagnosticsChannel() !== void 0; } /** * Subscribe to a named diagnostics channel. Returns an idempotent unsubscribe * function; a no-op (that still returns a disposer) on unsupported runtimes. */ function subscribeChannel(name, handler) { const dc = loadDiagnosticsChannel(); if (!dc?.subscribe) return () => {}; dc.subscribe(name, handler); let active = true; return () => { if (!active) return; active = false; dc.unsubscribe?.(name, handler); }; } /** * Subscribe to a `tracingChannel` (the `tracing:${name}:{start,end,…}` set). * Returns an idempotent unsubscribe; a no-op on runtimes without * `tracingChannel` support. */ function subscribeTracingChannel(name, handlers) { const channel = loadDiagnosticsChannel()?.tracingChannel?.(name); if (!channel) return () => {}; channel.subscribe(handlers); let active = true; return () => { if (!active) return; active = false; channel.unsubscribe(handlers); }; } //#endregion //#region src/diagnostics/console.ts /** * Capture `console.*` calls as wide events — without monkey-patching `console`. * * Node publishes every `console.log` / `info` / `debug` / `warn` / `error` call * on a built-in diagnostics channel. {@link captureConsole} subscribes to those * channels and turns each call into an OpenTelemetry **log record** (correlated * to the active span via trace context by the logs SDK) and/or a **span event** * on the active span. Nothing patches the global `console`, so there is no * load-order fragility and no interference with other tooling. * * Opt-in. Call once after `init()` and keep the returned disposer to stop: * * ```ts * import { captureConsole } from 'autotel/diagnostics'; * * const stop = captureConsole(); // every console.* → correlated log record * // …later: stop(); * ``` * * The built-in `console.*` channels are a Stability-1 (experimental) Node API; * this module degrades to a no-op where they are unavailable. */ const ALL_LEVELS = [ "log", "info", "debug", "warn", "error" ]; const SEVERITY = { debug: SeverityNumber.DEBUG, log: SeverityNumber.INFO, info: SeverityNumber.INFO, warn: SeverityNumber.WARN, error: SeverityNumber.ERROR }; const nodeUtil = safeRequire("node:util"); /** Format console arguments the way `console` itself would (printf + inspect). */ function formatArgs(args) { if (nodeUtil?.format) return nodeUtil.format(...args); return args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" "); } function safeStringify(value) { try { return JSON.stringify(value) ?? String(value); } catch { return String(value); } } /** * Start capturing `console.*` calls as wide events. Returns a disposer that * stops capture. Safe to call on runtimes without the console channels (no-op). */ function captureConsole(options = {}) { const levels = options.levels ?? ALL_LEVELS; const target = options.target ?? "log"; const toLog = target === "log" || target === "both"; const toSpan = target === "span-event" || target === "both"; const logger = logs.getLogger(options.loggerName ?? "autotel.console"); let recording = false; const disposers = levels.map((level) => subscribeChannel(`console.${level}`, (message) => { if (recording) return; const body = formatArgs(message?.args ?? []); recording = true; try { const attributes = { "log.source": "console", "log.method": level, ...options.attributes }; if (toLog) logger.emit({ severityNumber: SEVERITY[level], severityText: level.toUpperCase(), body, attributes }); if (toSpan) trace.getActiveSpan()?.addEvent("log", { "log.message": body, ...attributes }); } finally { recording = false; } })); let active = true; return () => { if (!active) return; active = false; for (const dispose of disposers) dispose(); }; } //#endregion //#region src/diagnostics/http.ts const SERVER_SPANS = /* @__PURE__ */ new WeakMap(); const CLIENT_SPANS = /* @__PURE__ */ new WeakMap(); function firstHeader(value) { return Array.isArray(value) ? value[0] : value; } function splitHostPort(host) { if (!host) return {}; const idx = host.lastIndexOf(":"); if (idx === -1) return { address: host }; const port = Number(host.slice(idx + 1)); return { address: host.slice(0, idx), port: Number.isFinite(port) ? port : void 0 }; } /** * Start emitting HTTP server/client spans from Node's HTTP diagnostics * channels. Returns a disposer; a no-op on runtimes without the channels. */ function instrumentHttp(options = {}) { const tracer = options.tracer ?? trace.getTracer("autotel.http-diagnostics"); const disposers = []; if (options.server !== false) disposers.push(subscribeChannel("http.server.request.start", (message) => { const request = message?.request; if (!request) return; const method = request.method ?? "HTTP"; const { address, port } = splitHostPort(firstHeader(request.headers.host)); const path = (request.url ?? "/").split("?", 1)[0]; const attributes = { [ATTR_HTTP_REQUEST_METHOD]: method, [ATTR_URL_PATH]: path, [ATTR_URL_SCHEME]: "http", [ATTR_NETWORK_PROTOCOL_VERSION]: request.httpVersion, [ATTR_USER_AGENT_ORIGINAL]: firstHeader(request.headers["user-agent"]), [ATTR_SERVER_ADDRESS]: address, [ATTR_SERVER_PORT]: port }; const parent = propagation.extract(context.active(), request.headers, defaultTextMapGetter); const span = tracer.startSpan(method, { kind: SpanKind.SERVER, attributes }, parent); SERVER_SPANS.set(request, span); }), subscribeChannel("http.server.response.finish", (message) => { const { request, response } = message ?? {}; if (!request) return; const span = SERVER_SPANS.get(request); if (!span) return; SERVER_SPANS.delete(request); finishHttpSpan(span, response?.statusCode, 500); })); if (options.client !== false) disposers.push(subscribeChannel("http.client.request.start", (message) => { const request = message?.request; if (!request) return; const method = request.method ?? "HTTP"; const req = request; const { address, port } = splitHostPort(req.host); const scheme = (req.protocol ?? "http:").replace(":", ""); const attributes = { [ATTR_HTTP_REQUEST_METHOD]: method, [ATTR_SERVER_ADDRESS]: address, [ATTR_SERVER_PORT]: port, [ATTR_URL_FULL]: address && req.path ? `${scheme}://${req.host}${req.path}` : void 0 }; const span = tracer.startSpan(method, { kind: SpanKind.CLIENT, attributes }); CLIENT_SPANS.set(request, span); if (!request.headersSent) { const carrier = {}; propagation.inject(trace.setSpan(context.active(), span), carrier, defaultTextMapSetter); for (const [key, value] of Object.entries(carrier)) try { request.setHeader(key, value); } catch {} } }), subscribeChannel("http.client.response.finish", (message) => { const { request, response } = message ?? {}; if (!request) return; const span = CLIENT_SPANS.get(request); if (!span) return; CLIENT_SPANS.delete(request); finishHttpSpan(span, response?.statusCode, 400); }), subscribeChannel("http.client.request.error", (message) => { const { request, error } = message ?? {}; if (!request) return; const span = CLIENT_SPANS.get(request); if (!span) return; CLIENT_SPANS.delete(request); if (error instanceof Error) span.recordException(error); span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : void 0 }); span.end(); })); let active = true; return () => { if (!active) return; active = false; for (const dispose of disposers) dispose(); }; } /** Set status code + error status (when `>= errorAt`) and end the span. */ function finishHttpSpan(span, statusCode, errorAt) { if (statusCode !== void 0) { span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, statusCode); if (statusCode >= errorAt) span.setStatus({ code: SpanStatusCode.ERROR }); } span.end(); } //#endregion export { captureConsole, diagnosticsChannelAvailable, instrumentHttp, subscribeChannel, subscribeTracingChannel }; //# sourceMappingURL=diagnostics.js.map