autotel
Version:
Write Once, Observe Anywhere
274 lines (270 loc) • 9.69 kB
JavaScript
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