autotel 4.2.1 → 4.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1880,6 +1880,56 @@ try {
1880
1880
  }
1881
1881
  ```
1882
1882
 
1883
+ ## Diagnostics Channels
1884
+
1885
+ `autotel/diagnostics` bridges Node's built-in
1886
+ [`diagnostics_channel`](https://nodejs.org/api/diagnostics_channel.html) into
1887
+ autotel spans and events — no monkey-patching, no `import-in-the-middle`. Every
1888
+ entry point is opt-in and degrades to a no-op on runtimes (edge, old Node) that
1889
+ lack the underlying channels.
1890
+
1891
+ **Capture `console.*` as correlated wide events** (the patch-free way to get
1892
+ `console.log` into your traces):
1893
+
1894
+ ```ts
1895
+ import { captureConsole } from 'autotel/diagnostics';
1896
+
1897
+ const stop = captureConsole(); // every console.* → an OTel log record,
1898
+ // correlated to the active span by trace context
1899
+ ```
1900
+
1901
+ Each call becomes a log record (severity mapped from the method, printf-formatted
1902
+ body, `log.source`/`log.method` attributes). Pass `{ target: 'span-event' }` to
1903
+ add events to the active span instead, or `{ target: 'both' }`. Nothing patches
1904
+ the global `console`, so there's no load-order fragility.
1905
+
1906
+ **Lightweight HTTP spans + W3C propagation** without `import-in-the-middle`:
1907
+
1908
+ ```ts
1909
+ import { instrumentHttp } from 'autotel/diagnostics';
1910
+
1911
+ const stop = instrumentHttp(); // SERVER span per inbound request (parented to the
1912
+ // incoming traceparent) + CLIENT span per outbound
1913
+ // request (injects traceparent downstream)
1914
+ ```
1915
+
1916
+ This is an opt-in alternative to `@opentelemetry/instrumentation-http` for span
1917
+ coverage and propagation. Limitation: a plain channel can't wrap the request
1918
+ handler, so it does **not** set an ambient context for the handler's duration —
1919
+ application spans created inside a handler won't auto-nest under the SERVER span.
1920
+ Use `@opentelemetry/instrumentation-http` if you need that nesting.
1921
+
1922
+ **Bridge any channel** with the shared primitive (also used by
1923
+ `autotel-genai`'s `ai:telemetry` subscriber):
1924
+
1925
+ ```ts
1926
+ import { subscribeChannel, subscribeTracingChannel } from 'autotel/diagnostics';
1927
+
1928
+ const off = subscribeChannel('my-lib:event', (message) => {
1929
+ /* turn message into a span/event */
1930
+ });
1931
+ ```
1932
+
1883
1933
  ## Auto Instrumentation & Advanced Configuration
1884
1934
 
1885
1935
  - `autoInstrumentations` : Enable OpenTelemetry auto-instrumentations (HTTP, Express, Fastify, Prisma, Pino…). Requires `@opentelemetry/auto-instrumentations-node`.
@@ -0,0 +1,279 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ const require_node_require = require('./node-require-CZ_PU448.cjs');
3
+ let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
4
+ let _opentelemetry_api = require("@opentelemetry/api");
5
+ let _opentelemetry_api_logs = require("@opentelemetry/api-logs");
6
+
7
+ //#region src/diagnostics/channel.ts
8
+ /**
9
+ * Edge-safe wrappers over Node's `diagnostics_channel`.
10
+ *
11
+ * The module is loaded lazily through {@link safeRequire} — never a static
12
+ * `node:` import — so merely importing this file is side-effect-free and bundles
13
+ * cleanly for browser/edge targets, where every subscribe call degrades to a
14
+ * no-op (returning an unsubscribe that does nothing). This is the shared
15
+ * primitive behind autotel's diagnostics-channel integrations (console capture,
16
+ * HTTP spans) and any app- or library-specific channel you want to bridge into
17
+ * a span/event.
18
+ *
19
+ * `diagnostics_channel.subscribe` (Node 18.7+) and `tracingChannel` (Node 19+)
20
+ * are used; autotel targets Node 22+, but on any runtime that lacks them the
21
+ * loader returns `undefined` and the helpers no-op.
22
+ */
23
+ let cached;
24
+ function loadDiagnosticsChannel() {
25
+ if (cached !== void 0) return cached ?? void 0;
26
+ cached = require_node_require.safeRequire("node:diagnostics_channel") ?? null;
27
+ return cached ?? void 0;
28
+ }
29
+ /** Whether Node's `diagnostics_channel` is available in this runtime. */
30
+ function diagnosticsChannelAvailable() {
31
+ return loadDiagnosticsChannel() !== void 0;
32
+ }
33
+ /**
34
+ * Subscribe to a named diagnostics channel. Returns an idempotent unsubscribe
35
+ * function; a no-op (that still returns a disposer) on unsupported runtimes.
36
+ */
37
+ function subscribeChannel(name, handler) {
38
+ const dc = loadDiagnosticsChannel();
39
+ if (!dc?.subscribe) return () => {};
40
+ dc.subscribe(name, handler);
41
+ let active = true;
42
+ return () => {
43
+ if (!active) return;
44
+ active = false;
45
+ dc.unsubscribe?.(name, handler);
46
+ };
47
+ }
48
+ /**
49
+ * Subscribe to a `tracingChannel` (the `tracing:${name}:{start,end,…}` set).
50
+ * Returns an idempotent unsubscribe; a no-op on runtimes without
51
+ * `tracingChannel` support.
52
+ */
53
+ function subscribeTracingChannel(name, handlers) {
54
+ const channel = loadDiagnosticsChannel()?.tracingChannel?.(name);
55
+ if (!channel) return () => {};
56
+ channel.subscribe(handlers);
57
+ let active = true;
58
+ return () => {
59
+ if (!active) return;
60
+ active = false;
61
+ channel.unsubscribe(handlers);
62
+ };
63
+ }
64
+
65
+ //#endregion
66
+ //#region src/diagnostics/console.ts
67
+ /**
68
+ * Capture `console.*` calls as wide events — without monkey-patching `console`.
69
+ *
70
+ * Node publishes every `console.log` / `info` / `debug` / `warn` / `error` call
71
+ * on a built-in diagnostics channel. {@link captureConsole} subscribes to those
72
+ * channels and turns each call into an OpenTelemetry **log record** (correlated
73
+ * to the active span via trace context by the logs SDK) and/or a **span event**
74
+ * on the active span. Nothing patches the global `console`, so there is no
75
+ * load-order fragility and no interference with other tooling.
76
+ *
77
+ * Opt-in. Call once after `init()` and keep the returned disposer to stop:
78
+ *
79
+ * ```ts
80
+ * import { captureConsole } from 'autotel/diagnostics';
81
+ *
82
+ * const stop = captureConsole(); // every console.* → correlated log record
83
+ * // …later: stop();
84
+ * ```
85
+ *
86
+ * The built-in `console.*` channels are a Stability-1 (experimental) Node API;
87
+ * this module degrades to a no-op where they are unavailable.
88
+ */
89
+ const ALL_LEVELS = [
90
+ "log",
91
+ "info",
92
+ "debug",
93
+ "warn",
94
+ "error"
95
+ ];
96
+ const SEVERITY = {
97
+ debug: _opentelemetry_api_logs.SeverityNumber.DEBUG,
98
+ log: _opentelemetry_api_logs.SeverityNumber.INFO,
99
+ info: _opentelemetry_api_logs.SeverityNumber.INFO,
100
+ warn: _opentelemetry_api_logs.SeverityNumber.WARN,
101
+ error: _opentelemetry_api_logs.SeverityNumber.ERROR
102
+ };
103
+ const nodeUtil = require_node_require.safeRequire("node:util");
104
+ /** Format console arguments the way `console` itself would (printf + inspect). */
105
+ function formatArgs(args) {
106
+ if (nodeUtil?.format) return nodeUtil.format(...args);
107
+ return args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ");
108
+ }
109
+ function safeStringify(value) {
110
+ try {
111
+ return JSON.stringify(value) ?? String(value);
112
+ } catch {
113
+ return String(value);
114
+ }
115
+ }
116
+ /**
117
+ * Start capturing `console.*` calls as wide events. Returns a disposer that
118
+ * stops capture. Safe to call on runtimes without the console channels (no-op).
119
+ */
120
+ function captureConsole(options = {}) {
121
+ const levels = options.levels ?? ALL_LEVELS;
122
+ const target = options.target ?? "log";
123
+ const toLog = target === "log" || target === "both";
124
+ const toSpan = target === "span-event" || target === "both";
125
+ const logger = _opentelemetry_api_logs.logs.getLogger(options.loggerName ?? "autotel.console");
126
+ let recording = false;
127
+ const disposers = levels.map((level) => subscribeChannel(`console.${level}`, (message) => {
128
+ if (recording) return;
129
+ const body = formatArgs(message?.args ?? []);
130
+ recording = true;
131
+ try {
132
+ const attributes = {
133
+ "log.source": "console",
134
+ "log.method": level,
135
+ ...options.attributes
136
+ };
137
+ if (toLog) logger.emit({
138
+ severityNumber: SEVERITY[level],
139
+ severityText: level.toUpperCase(),
140
+ body,
141
+ attributes
142
+ });
143
+ if (toSpan) _opentelemetry_api.trace.getActiveSpan()?.addEvent("log", {
144
+ "log.message": body,
145
+ ...attributes
146
+ });
147
+ } finally {
148
+ recording = false;
149
+ }
150
+ }));
151
+ let active = true;
152
+ return () => {
153
+ if (!active) return;
154
+ active = false;
155
+ for (const dispose of disposers) dispose();
156
+ };
157
+ }
158
+
159
+ //#endregion
160
+ //#region src/diagnostics/http.ts
161
+ const SERVER_SPANS = /* @__PURE__ */ new WeakMap();
162
+ const CLIENT_SPANS = /* @__PURE__ */ new WeakMap();
163
+ function firstHeader(value) {
164
+ return Array.isArray(value) ? value[0] : value;
165
+ }
166
+ function splitHostPort(host) {
167
+ if (!host) return {};
168
+ const idx = host.lastIndexOf(":");
169
+ if (idx === -1) return { address: host };
170
+ const port = Number(host.slice(idx + 1));
171
+ return {
172
+ address: host.slice(0, idx),
173
+ port: Number.isFinite(port) ? port : void 0
174
+ };
175
+ }
176
+ /**
177
+ * Start emitting HTTP server/client spans from Node's HTTP diagnostics
178
+ * channels. Returns a disposer; a no-op on runtimes without the channels.
179
+ */
180
+ function instrumentHttp(options = {}) {
181
+ const tracer = options.tracer ?? _opentelemetry_api.trace.getTracer("autotel.http-diagnostics");
182
+ const disposers = [];
183
+ if (options.server !== false) disposers.push(subscribeChannel("http.server.request.start", (message) => {
184
+ const request = message?.request;
185
+ if (!request) return;
186
+ const method = request.method ?? "HTTP";
187
+ const { address, port } = splitHostPort(firstHeader(request.headers.host));
188
+ const path = (request.url ?? "/").split("?", 1)[0];
189
+ const attributes = {
190
+ [_opentelemetry_semantic_conventions.ATTR_HTTP_REQUEST_METHOD]: method,
191
+ [_opentelemetry_semantic_conventions.ATTR_URL_PATH]: path,
192
+ [_opentelemetry_semantic_conventions.ATTR_URL_SCHEME]: "http",
193
+ [_opentelemetry_semantic_conventions.ATTR_NETWORK_PROTOCOL_VERSION]: request.httpVersion,
194
+ [_opentelemetry_semantic_conventions.ATTR_USER_AGENT_ORIGINAL]: firstHeader(request.headers["user-agent"]),
195
+ [_opentelemetry_semantic_conventions.ATTR_SERVER_ADDRESS]: address,
196
+ [_opentelemetry_semantic_conventions.ATTR_SERVER_PORT]: port
197
+ };
198
+ const parent = _opentelemetry_api.propagation.extract(_opentelemetry_api.context.active(), request.headers, _opentelemetry_api.defaultTextMapGetter);
199
+ const span = tracer.startSpan(method, {
200
+ kind: _opentelemetry_api.SpanKind.SERVER,
201
+ attributes
202
+ }, parent);
203
+ SERVER_SPANS.set(request, span);
204
+ }), subscribeChannel("http.server.response.finish", (message) => {
205
+ const { request, response } = message ?? {};
206
+ if (!request) return;
207
+ const span = SERVER_SPANS.get(request);
208
+ if (!span) return;
209
+ SERVER_SPANS.delete(request);
210
+ finishHttpSpan(span, response?.statusCode, 500);
211
+ }));
212
+ if (options.client !== false) disposers.push(subscribeChannel("http.client.request.start", (message) => {
213
+ const request = message?.request;
214
+ if (!request) return;
215
+ const method = request.method ?? "HTTP";
216
+ const req = request;
217
+ const { address, port } = splitHostPort(req.host);
218
+ const scheme = (req.protocol ?? "http:").replace(":", "");
219
+ const attributes = {
220
+ [_opentelemetry_semantic_conventions.ATTR_HTTP_REQUEST_METHOD]: method,
221
+ [_opentelemetry_semantic_conventions.ATTR_SERVER_ADDRESS]: address,
222
+ [_opentelemetry_semantic_conventions.ATTR_SERVER_PORT]: port,
223
+ [_opentelemetry_semantic_conventions.ATTR_URL_FULL]: address && req.path ? `${scheme}://${req.host}${req.path}` : void 0
224
+ };
225
+ const span = tracer.startSpan(method, {
226
+ kind: _opentelemetry_api.SpanKind.CLIENT,
227
+ attributes
228
+ });
229
+ CLIENT_SPANS.set(request, span);
230
+ if (!request.headersSent) {
231
+ const carrier = {};
232
+ _opentelemetry_api.propagation.inject(_opentelemetry_api.trace.setSpan(_opentelemetry_api.context.active(), span), carrier, _opentelemetry_api.defaultTextMapSetter);
233
+ for (const [key, value] of Object.entries(carrier)) try {
234
+ request.setHeader(key, value);
235
+ } catch {}
236
+ }
237
+ }), subscribeChannel("http.client.response.finish", (message) => {
238
+ const { request, response } = message ?? {};
239
+ if (!request) return;
240
+ const span = CLIENT_SPANS.get(request);
241
+ if (!span) return;
242
+ CLIENT_SPANS.delete(request);
243
+ finishHttpSpan(span, response?.statusCode, 400);
244
+ }), subscribeChannel("http.client.request.error", (message) => {
245
+ const { request, error } = message ?? {};
246
+ if (!request) return;
247
+ const span = CLIENT_SPANS.get(request);
248
+ if (!span) return;
249
+ CLIENT_SPANS.delete(request);
250
+ if (error instanceof Error) span.recordException(error);
251
+ span.setStatus({
252
+ code: _opentelemetry_api.SpanStatusCode.ERROR,
253
+ message: error instanceof Error ? error.message : void 0
254
+ });
255
+ span.end();
256
+ }));
257
+ let active = true;
258
+ return () => {
259
+ if (!active) return;
260
+ active = false;
261
+ for (const dispose of disposers) dispose();
262
+ };
263
+ }
264
+ /** Set status code + error status (when `>= errorAt`) and end the span. */
265
+ function finishHttpSpan(span, statusCode, errorAt) {
266
+ if (statusCode !== void 0) {
267
+ span.setAttribute(_opentelemetry_semantic_conventions.ATTR_HTTP_RESPONSE_STATUS_CODE, statusCode);
268
+ if (statusCode >= errorAt) span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
269
+ }
270
+ span.end();
271
+ }
272
+
273
+ //#endregion
274
+ exports.captureConsole = captureConsole;
275
+ exports.diagnosticsChannelAvailable = diagnosticsChannelAvailable;
276
+ exports.instrumentHttp = instrumentHttp;
277
+ exports.subscribeChannel = subscribeChannel;
278
+ exports.subscribeTracingChannel = subscribeTracingChannel;
279
+ //# sourceMappingURL=diagnostics.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostics.cjs","names":["safeRequire","SeverityNumber","safeRequire","logs","trace","ATTR_HTTP_REQUEST_METHOD","ATTR_URL_PATH","ATTR_URL_SCHEME","ATTR_NETWORK_PROTOCOL_VERSION","ATTR_USER_AGENT_ORIGINAL","ATTR_SERVER_ADDRESS","ATTR_SERVER_PORT","propagation","otelContext","defaultTextMapGetter","SpanKind","ATTR_URL_FULL","defaultTextMapSetter","SpanStatusCode","ATTR_HTTP_RESPONSE_STATUS_CODE"],"sources":["../src/diagnostics/channel.ts","../src/diagnostics/console.ts","../src/diagnostics/http.ts"],"sourcesContent":["/**\n * Edge-safe wrappers over Node's `diagnostics_channel`.\n *\n * The module is loaded lazily through {@link safeRequire} — never a static\n * `node:` import — so merely importing this file is side-effect-free and bundles\n * cleanly for browser/edge targets, where every subscribe call degrades to a\n * no-op (returning an unsubscribe that does nothing). This is the shared\n * primitive behind autotel's diagnostics-channel integrations (console capture,\n * HTTP spans) and any app- or library-specific channel you want to bridge into\n * a span/event.\n *\n * `diagnostics_channel.subscribe` (Node 18.7+) and `tracingChannel` (Node 19+)\n * are used; autotel targets Node 22+, but on any runtime that lacks them the\n * loader returns `undefined` and the helpers no-op.\n */\n\nimport { safeRequire } from '../node-require.js';\n\ntype DiagnosticsChannelModule = typeof import('node:diagnostics_channel');\n\nlet cached: DiagnosticsChannelModule | null | undefined;\n\nfunction loadDiagnosticsChannel(): DiagnosticsChannelModule | undefined {\n if (cached !== undefined) return cached ?? undefined;\n cached =\n safeRequire<DiagnosticsChannelModule>('node:diagnostics_channel') ?? null;\n return cached ?? undefined;\n}\n\n/** Whether Node's `diagnostics_channel` is available in this runtime. */\nexport function diagnosticsChannelAvailable(): boolean {\n return loadDiagnosticsChannel() !== undefined;\n}\n\n/** Handler for a plain named channel. */\nexport type ChannelMessageHandler = (\n message: unknown,\n name: string | symbol,\n) => void;\n\n/**\n * Subscribe to a named diagnostics channel. Returns an idempotent unsubscribe\n * function; a no-op (that still returns a disposer) on unsupported runtimes.\n */\nexport function subscribeChannel(\n name: string,\n handler: ChannelMessageHandler,\n): () => void {\n const dc = loadDiagnosticsChannel();\n if (!dc?.subscribe) return () => {};\n dc.subscribe(name, handler);\n let active = true;\n return () => {\n if (!active) return;\n active = false;\n dc.unsubscribe?.(name, handler);\n };\n}\n\n/** Subscriber set for a {@link https://nodejs.org/api/diagnostics_channel.html#class-tracingchannel TracingChannel}. */\nexport interface TracingChannelHandlers {\n start?(message: unknown): void;\n end?(message: unknown): void;\n asyncStart?(message: unknown): void;\n asyncEnd?(message: unknown): void;\n error?(message: unknown): void;\n}\n\n/**\n * Subscribe to a `tracingChannel` (the `tracing:${name}:{start,end,…}` set).\n * Returns an idempotent unsubscribe; a no-op on runtimes without\n * `tracingChannel` support.\n */\nexport function subscribeTracingChannel(\n name: string,\n handlers: TracingChannelHandlers,\n): () => void {\n const dc = loadDiagnosticsChannel();\n const channel = dc?.tracingChannel?.(name);\n if (!channel) return () => {};\n // Node's typings want all five handlers; we pass the subset provided.\n channel.subscribe(handlers as Parameters<typeof channel.subscribe>[0]);\n let active = true;\n return () => {\n if (!active) return;\n active = false;\n channel.unsubscribe(handlers as Parameters<typeof channel.unsubscribe>[0]);\n };\n}\n","/**\n * Capture `console.*` calls as wide events — without monkey-patching `console`.\n *\n * Node publishes every `console.log` / `info` / `debug` / `warn` / `error` call\n * on a built-in diagnostics channel. {@link captureConsole} subscribes to those\n * channels and turns each call into an OpenTelemetry **log record** (correlated\n * to the active span via trace context by the logs SDK) and/or a **span event**\n * on the active span. Nothing patches the global `console`, so there is no\n * load-order fragility and no interference with other tooling.\n *\n * Opt-in. Call once after `init()` and keep the returned disposer to stop:\n *\n * ```ts\n * import { captureConsole } from 'autotel/diagnostics';\n *\n * const stop = captureConsole(); // every console.* → correlated log record\n * // …later: stop();\n * ```\n *\n * The built-in `console.*` channels are a Stability-1 (experimental) Node API;\n * this module degrades to a no-op where they are unavailable.\n */\n\nimport { trace, type Attributes } from '@opentelemetry/api';\nimport { logs, SeverityNumber, type Logger } from '@opentelemetry/api-logs';\nimport { safeRequire } from '../node-require.js';\nimport { subscribeChannel } from './channel.js';\n\n/** Console methods that publish a diagnostics channel. */\nexport type ConsoleLevel = 'log' | 'info' | 'debug' | 'warn' | 'error';\n\nconst ALL_LEVELS: readonly ConsoleLevel[] = [\n 'log',\n 'info',\n 'debug',\n 'warn',\n 'error',\n];\n\nconst SEVERITY: Record<ConsoleLevel, SeverityNumber> = {\n debug: SeverityNumber.DEBUG,\n log: SeverityNumber.INFO,\n info: SeverityNumber.INFO,\n warn: SeverityNumber.WARN,\n error: SeverityNumber.ERROR,\n};\n\nexport interface CaptureConsoleOptions {\n /** Which console methods to capture. Defaults to all five. */\n levels?: readonly ConsoleLevel[];\n /**\n * Where to record captured output:\n * - `'log'` (default): emit an OpenTelemetry log record;\n * - `'span-event'`: add an event to the active span (nothing if no active span);\n * - `'both'`.\n */\n target?: 'log' | 'span-event' | 'both';\n /** Logger name for emitted records. Defaults to `'autotel.console'`. */\n loggerName?: string;\n /** Static attributes merged onto every captured record/event. */\n attributes?: Attributes;\n}\n\ntype ConsoleMessage = { args?: unknown[] };\n\nconst nodeUtil = safeRequire<typeof import('node:util')>('node:util');\n\n/** Format console arguments the way `console` itself would (printf + inspect). */\nfunction formatArgs(args: unknown[]): string {\n if (nodeUtil?.format) return nodeUtil.format(...args);\n return args\n .map((a) => (typeof a === 'string' ? a : safeStringify(a)))\n .join(' ');\n}\n\nfunction safeStringify(value: unknown): string {\n try {\n return JSON.stringify(value) ?? String(value);\n } catch {\n return String(value);\n }\n}\n\n/**\n * Start capturing `console.*` calls as wide events. Returns a disposer that\n * stops capture. Safe to call on runtimes without the console channels (no-op).\n */\nexport function captureConsole(\n options: CaptureConsoleOptions = {},\n): () => void {\n const levels = options.levels ?? ALL_LEVELS;\n const target = options.target ?? 'log';\n const toLog = target === 'log' || target === 'both';\n const toSpan = target === 'span-event' || target === 'both';\n const logger: Logger = logs.getLogger(\n options.loggerName ?? 'autotel.console',\n );\n\n // Guard against re-entrancy: if recording a captured call itself triggers a\n // `console.*` (e.g. an exporter logging a warning), don't capture that.\n let recording = false;\n\n const disposers = levels.map((level) =>\n subscribeChannel(`console.${level}`, (message) => {\n if (recording) return;\n const args = (message as ConsoleMessage)?.args ?? [];\n const body = formatArgs(args as unknown[]);\n recording = true;\n try {\n const attributes: Attributes = {\n 'log.source': 'console',\n 'log.method': level,\n ...options.attributes,\n };\n if (toLog) {\n logger.emit({\n severityNumber: SEVERITY[level],\n severityText: level.toUpperCase(),\n body,\n attributes,\n });\n }\n if (toSpan) {\n trace\n .getActiveSpan()\n ?.addEvent('log', { 'log.message': body, ...attributes });\n }\n } finally {\n recording = false;\n }\n }),\n );\n\n let active = true;\n return () => {\n if (!active) return;\n active = false;\n for (const dispose of disposers) dispose();\n };\n}\n","/**\n * Lightweight HTTP spans via Node's built-in `diagnostics_channel` — no\n * monkey-patching, no `import-in-the-middle`.\n *\n * Node publishes `http.server.request.start` / `http.server.response.finish`\n * and `http.client.request.start` / `http.client.response.finish` /\n * `http.client.request.error`. {@link instrumentHttp} subscribes to those and\n * emits a `SERVER` span per inbound request (parented to an incoming W3C\n * `traceparent`) and a `CLIENT` span per outbound request (whose context it\n * injects into the outgoing headers for downstream propagation).\n *\n * ```ts\n * import { instrumentHttp } from 'autotel/diagnostics';\n *\n * const stop = instrumentHttp();\n * ```\n *\n * Scope & limitation. This is an opt-in, low-overhead alternative to\n * `@opentelemetry/instrumentation-http` for HTTP span coverage + W3C\n * propagation. Client-side propagation works (the `traceparent` is injected on\n * the `ClientRequest` object directly). What it does **not** do is establish an\n * *ambient* OpenTelemetry context for the duration of a server request handler,\n * so application spans created inside a handler will not become children of the\n * `SERVER` span.\n *\n * This is structural, not a \"wait for a newer Node\" gap. Node publishes the\n * `http.*` channels with a plain `channel.publish()` — not `runStores` /\n * `tracingChannel` — so a subscriber has no scope to bind a store to. The only\n * ways to get handler nesting both defeat the purpose of using a channel:\n * 1. `AsyncLocalStorage.enterWith()` in the start handler — no scoped exit, so\n * context leaks across requests sharing an event-loop tick / keep-alive\n * connection and misattributes spans. Strictly worse than no nesting.\n * 2. Patching `http.Server.prototype.emit` to wrap the `'request'` listener in\n * `context.with()` — monkey-patching, i.e. reimplementing\n * `@opentelemetry/instrumentation-http`.\n * If you need handler nesting, use `@opentelemetry/instrumentation-http`.\n *\n * The `http.*` channels are a Stability-1 (experimental) Node API; this module\n * degrades to a no-op where they are unavailable.\n */\n\nimport type { ClientRequest, IncomingMessage, ServerResponse } from 'node:http';\nimport {\n context as otelContext,\n defaultTextMapGetter,\n defaultTextMapSetter,\n propagation,\n SpanKind,\n SpanStatusCode,\n trace,\n type Attributes,\n type Span,\n type Tracer,\n} from '@opentelemetry/api';\nimport {\n ATTR_HTTP_REQUEST_METHOD,\n ATTR_HTTP_RESPONSE_STATUS_CODE,\n ATTR_NETWORK_PROTOCOL_VERSION,\n ATTR_SERVER_ADDRESS,\n ATTR_SERVER_PORT,\n ATTR_URL_FULL,\n ATTR_URL_PATH,\n ATTR_URL_SCHEME,\n ATTR_USER_AGENT_ORIGINAL,\n} from '@opentelemetry/semantic-conventions';\nimport { subscribeChannel } from './channel.js';\n\nexport interface InstrumentHttpOptions {\n /** Instrument inbound (server) requests. Default `true`. */\n server?: boolean;\n /** Instrument outbound (client) requests. Default `true`. */\n client?: boolean;\n /** Tracer to use. Defaults to `trace.getTracer('autotel.http-diagnostics')`. */\n tracer?: Tracer;\n}\n\ninterface ServerStartMessage {\n request?: IncomingMessage;\n response?: ServerResponse;\n}\ninterface ServerFinishMessage {\n request?: IncomingMessage;\n response?: ServerResponse;\n}\ninterface ClientStartMessage {\n request?: ClientRequest;\n}\ninterface ClientFinishMessage {\n request?: ClientRequest;\n response?: IncomingMessage;\n}\ninterface ClientErrorMessage {\n request?: ClientRequest;\n error?: unknown;\n}\n\nconst SERVER_SPANS = new WeakMap<object, Span>();\nconst CLIENT_SPANS = new WeakMap<object, Span>();\n\nfunction firstHeader(value: string | string[] | undefined): string | undefined {\n return Array.isArray(value) ? value[0] : value;\n}\n\nfunction splitHostPort(host: string | undefined): {\n address?: string;\n port?: number;\n} {\n if (!host) return {};\n const idx = host.lastIndexOf(':');\n if (idx === -1) return { address: host };\n const port = Number(host.slice(idx + 1));\n return {\n address: host.slice(0, idx),\n port: Number.isFinite(port) ? port : undefined,\n };\n}\n\n/**\n * Start emitting HTTP server/client spans from Node's HTTP diagnostics\n * channels. Returns a disposer; a no-op on runtimes without the channels.\n */\nexport function instrumentHttp(\n options: InstrumentHttpOptions = {},\n): () => void {\n const tracer = options.tracer ?? trace.getTracer('autotel.http-diagnostics');\n const disposers: Array<() => void> = [];\n\n if (options.server !== false) {\n disposers.push(\n subscribeChannel('http.server.request.start', (message) => {\n const request = (message as ServerStartMessage)?.request;\n if (!request) return;\n const method = request.method ?? 'HTTP';\n const host = firstHeader(request.headers.host);\n const { address, port } = splitHostPort(host);\n const path = (request.url ?? '/').split('?', 1)[0];\n const attributes: Attributes = {\n [ATTR_HTTP_REQUEST_METHOD]: method,\n [ATTR_URL_PATH]: path,\n [ATTR_URL_SCHEME]: 'http',\n [ATTR_NETWORK_PROTOCOL_VERSION]: request.httpVersion,\n [ATTR_USER_AGENT_ORIGINAL]: firstHeader(\n request.headers['user-agent'],\n ),\n [ATTR_SERVER_ADDRESS]: address,\n [ATTR_SERVER_PORT]: port,\n };\n const parent = propagation.extract(\n otelContext.active(),\n request.headers,\n defaultTextMapGetter,\n );\n const span = tracer.startSpan(\n method,\n { kind: SpanKind.SERVER, attributes },\n parent,\n );\n SERVER_SPANS.set(request, span);\n }),\n subscribeChannel('http.server.response.finish', (message) => {\n const { request, response } = (message as ServerFinishMessage) ?? {};\n if (!request) return;\n const span = SERVER_SPANS.get(request);\n if (!span) return;\n SERVER_SPANS.delete(request);\n finishHttpSpan(span, response?.statusCode, 500);\n }),\n );\n }\n\n if (options.client !== false) {\n disposers.push(\n subscribeChannel('http.client.request.start', (message) => {\n const request = (message as ClientStartMessage)?.request;\n if (!request) return;\n const method = request.method ?? 'HTTP';\n // `ClientRequest` exposes host/protocol/path on the public surface.\n const req = request as ClientRequest & {\n host?: string;\n protocol?: string;\n path?: string;\n };\n const { address, port } = splitHostPort(req.host);\n const scheme = (req.protocol ?? 'http:').replace(':', '');\n const attributes: Attributes = {\n [ATTR_HTTP_REQUEST_METHOD]: method,\n [ATTR_SERVER_ADDRESS]: address,\n [ATTR_SERVER_PORT]: port,\n [ATTR_URL_FULL]:\n address && req.path\n ? `${scheme}://${req.host}${req.path}`\n : undefined,\n };\n const span = tracer.startSpan(method, {\n kind: SpanKind.CLIENT,\n attributes,\n });\n CLIENT_SPANS.set(request, span);\n\n // Inject this span's context into the outbound headers so the\n // downstream service continues the trace.\n if (!request.headersSent) {\n const carrier: Record<string, string> = {};\n propagation.inject(\n trace.setSpan(otelContext.active(), span),\n carrier,\n defaultTextMapSetter,\n );\n for (const [key, value] of Object.entries(carrier)) {\n try {\n request.setHeader(key, value);\n } catch {\n // Headers already sent / immutable — propagation best-effort.\n }\n }\n }\n }),\n subscribeChannel('http.client.response.finish', (message) => {\n const { request, response } = (message as ClientFinishMessage) ?? {};\n if (!request) return;\n const span = CLIENT_SPANS.get(request);\n if (!span) return;\n CLIENT_SPANS.delete(request);\n finishHttpSpan(span, response?.statusCode, 400);\n }),\n subscribeChannel('http.client.request.error', (message) => {\n const { request, error } = (message as ClientErrorMessage) ?? {};\n if (!request) return;\n const span = CLIENT_SPANS.get(request);\n if (!span) return;\n CLIENT_SPANS.delete(request);\n if (error instanceof Error) span.recordException(error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : undefined,\n });\n span.end();\n }),\n );\n }\n\n let active = true;\n return () => {\n if (!active) return;\n active = false;\n for (const dispose of disposers) dispose();\n };\n}\n\n/** Set status code + error status (when `>= errorAt`) and end the span. */\nfunction finishHttpSpan(\n span: Span,\n statusCode: number | undefined,\n errorAt: number,\n): void {\n if (statusCode !== undefined) {\n span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, statusCode);\n if (statusCode >= errorAt) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n }\n }\n span.end();\n}\n"],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ import { Attributes, Tracer } from "@opentelemetry/api";
2
+
3
+ //#region src/diagnostics/channel.d.ts
4
+ /**
5
+ * Edge-safe wrappers over Node's `diagnostics_channel`.
6
+ *
7
+ * The module is loaded lazily through {@link safeRequire} — never a static
8
+ * `node:` import — so merely importing this file is side-effect-free and bundles
9
+ * cleanly for browser/edge targets, where every subscribe call degrades to a
10
+ * no-op (returning an unsubscribe that does nothing). This is the shared
11
+ * primitive behind autotel's diagnostics-channel integrations (console capture,
12
+ * HTTP spans) and any app- or library-specific channel you want to bridge into
13
+ * a span/event.
14
+ *
15
+ * `diagnostics_channel.subscribe` (Node 18.7+) and `tracingChannel` (Node 19+)
16
+ * are used; autotel targets Node 22+, but on any runtime that lacks them the
17
+ * loader returns `undefined` and the helpers no-op.
18
+ */
19
+ /** Whether Node's `diagnostics_channel` is available in this runtime. */
20
+ declare function diagnosticsChannelAvailable(): boolean;
21
+ /** Handler for a plain named channel. */
22
+ type ChannelMessageHandler = (message: unknown, name: string | symbol) => void;
23
+ /**
24
+ * Subscribe to a named diagnostics channel. Returns an idempotent unsubscribe
25
+ * function; a no-op (that still returns a disposer) on unsupported runtimes.
26
+ */
27
+ declare function subscribeChannel(name: string, handler: ChannelMessageHandler): () => void;
28
+ /** Subscriber set for a {@link https://nodejs.org/api/diagnostics_channel.html#class-tracingchannel TracingChannel}. */
29
+ interface TracingChannelHandlers {
30
+ start?(message: unknown): void;
31
+ end?(message: unknown): void;
32
+ asyncStart?(message: unknown): void;
33
+ asyncEnd?(message: unknown): void;
34
+ error?(message: unknown): void;
35
+ }
36
+ /**
37
+ * Subscribe to a `tracingChannel` (the `tracing:${name}:{start,end,…}` set).
38
+ * Returns an idempotent unsubscribe; a no-op on runtimes without
39
+ * `tracingChannel` support.
40
+ */
41
+ declare function subscribeTracingChannel(name: string, handlers: TracingChannelHandlers): () => void;
42
+ //#endregion
43
+ //#region src/diagnostics/console.d.ts
44
+ /** Console methods that publish a diagnostics channel. */
45
+ type ConsoleLevel = 'log' | 'info' | 'debug' | 'warn' | 'error';
46
+ interface CaptureConsoleOptions {
47
+ /** Which console methods to capture. Defaults to all five. */
48
+ levels?: readonly ConsoleLevel[];
49
+ /**
50
+ * Where to record captured output:
51
+ * - `'log'` (default): emit an OpenTelemetry log record;
52
+ * - `'span-event'`: add an event to the active span (nothing if no active span);
53
+ * - `'both'`.
54
+ */
55
+ target?: 'log' | 'span-event' | 'both';
56
+ /** Logger name for emitted records. Defaults to `'autotel.console'`. */
57
+ loggerName?: string;
58
+ /** Static attributes merged onto every captured record/event. */
59
+ attributes?: Attributes;
60
+ }
61
+ /**
62
+ * Start capturing `console.*` calls as wide events. Returns a disposer that
63
+ * stops capture. Safe to call on runtimes without the console channels (no-op).
64
+ */
65
+ declare function captureConsole(options?: CaptureConsoleOptions): () => void;
66
+ //#endregion
67
+ //#region src/diagnostics/http.d.ts
68
+ interface InstrumentHttpOptions {
69
+ /** Instrument inbound (server) requests. Default `true`. */
70
+ server?: boolean;
71
+ /** Instrument outbound (client) requests. Default `true`. */
72
+ client?: boolean;
73
+ /** Tracer to use. Defaults to `trace.getTracer('autotel.http-diagnostics')`. */
74
+ tracer?: Tracer;
75
+ }
76
+ /**
77
+ * Start emitting HTTP server/client spans from Node's HTTP diagnostics
78
+ * channels. Returns a disposer; a no-op on runtimes without the channels.
79
+ */
80
+ declare function instrumentHttp(options?: InstrumentHttpOptions): () => void;
81
+ //#endregion
82
+ export { type CaptureConsoleOptions, type ChannelMessageHandler, type ConsoleLevel, type InstrumentHttpOptions, type TracingChannelHandlers, captureConsole, diagnosticsChannelAvailable, instrumentHttp, subscribeChannel, subscribeTracingChannel };
83
+ //# sourceMappingURL=diagnostics.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostics.d.cts","names":[],"sources":["../src/diagnostics/channel.ts","../src/diagnostics/console.ts","../src/diagnostics/http.ts"],"mappings":";;;;;;AA8BA;;;;AAA2C;AAK3C;;;;AAEuB;AAOvB;;;iBAdgB,2BAAA;;KAKJ,qBAAA,IACV,OAAA,WACA,IAAqB;;;AASS;AAchC;iBAhBgB,gBAAA,CACd,IAAA,UACA,OAAA,EAAS,qBAAqB;;UAcf,sBAAA;EACf,KAAA,EAAO,OAAA;EACP,GAAA,EAAK,OAAA;EACL,UAAA,EAAY,OAAA;EACZ,QAAA,EAAU,OAAA;EACV,KAAA,EAAO,OAAA;AAAA;;;;;;iBAQO,uBAAA,CACd,IAAA,UACA,QAAA,EAAU,sBAAsB;;;;KC9CtB,YAAA;AAAA,UAkBK,qBAAA;EDcf;ECZA,MAAA,YAAkB,YAAA;EDalB;;;;;;ECNA,MAAA;EDSO;ECPP,UAAA;EDOuB;ECLvB,UAAA,GAAa,UAAU;AAAA;;;;;iBA2BT,cAAA,CACd,OAAmC,GAA1B,qBAA0B;;;UCrBpB,qBAAA;EFOf;EELA,MAAA;EFMA;EEJA,MAAA;EFIgC;EEFhC,MAAA,GAAS,MAAM;AAAA;;AD5CjB;;;iBC4FgB,cAAA,CACd,OAAmC,GAA1B,qBAA0B"}
@@ -0,0 +1,83 @@
1
+ import { Attributes, Tracer } from "@opentelemetry/api";
2
+
3
+ //#region src/diagnostics/channel.d.ts
4
+ /**
5
+ * Edge-safe wrappers over Node's `diagnostics_channel`.
6
+ *
7
+ * The module is loaded lazily through {@link safeRequire} — never a static
8
+ * `node:` import — so merely importing this file is side-effect-free and bundles
9
+ * cleanly for browser/edge targets, where every subscribe call degrades to a
10
+ * no-op (returning an unsubscribe that does nothing). This is the shared
11
+ * primitive behind autotel's diagnostics-channel integrations (console capture,
12
+ * HTTP spans) and any app- or library-specific channel you want to bridge into
13
+ * a span/event.
14
+ *
15
+ * `diagnostics_channel.subscribe` (Node 18.7+) and `tracingChannel` (Node 19+)
16
+ * are used; autotel targets Node 22+, but on any runtime that lacks them the
17
+ * loader returns `undefined` and the helpers no-op.
18
+ */
19
+ /** Whether Node's `diagnostics_channel` is available in this runtime. */
20
+ declare function diagnosticsChannelAvailable(): boolean;
21
+ /** Handler for a plain named channel. */
22
+ type ChannelMessageHandler = (message: unknown, name: string | symbol) => void;
23
+ /**
24
+ * Subscribe to a named diagnostics channel. Returns an idempotent unsubscribe
25
+ * function; a no-op (that still returns a disposer) on unsupported runtimes.
26
+ */
27
+ declare function subscribeChannel(name: string, handler: ChannelMessageHandler): () => void;
28
+ /** Subscriber set for a {@link https://nodejs.org/api/diagnostics_channel.html#class-tracingchannel TracingChannel}. */
29
+ interface TracingChannelHandlers {
30
+ start?(message: unknown): void;
31
+ end?(message: unknown): void;
32
+ asyncStart?(message: unknown): void;
33
+ asyncEnd?(message: unknown): void;
34
+ error?(message: unknown): void;
35
+ }
36
+ /**
37
+ * Subscribe to a `tracingChannel` (the `tracing:${name}:{start,end,…}` set).
38
+ * Returns an idempotent unsubscribe; a no-op on runtimes without
39
+ * `tracingChannel` support.
40
+ */
41
+ declare function subscribeTracingChannel(name: string, handlers: TracingChannelHandlers): () => void;
42
+ //#endregion
43
+ //#region src/diagnostics/console.d.ts
44
+ /** Console methods that publish a diagnostics channel. */
45
+ type ConsoleLevel = 'log' | 'info' | 'debug' | 'warn' | 'error';
46
+ interface CaptureConsoleOptions {
47
+ /** Which console methods to capture. Defaults to all five. */
48
+ levels?: readonly ConsoleLevel[];
49
+ /**
50
+ * Where to record captured output:
51
+ * - `'log'` (default): emit an OpenTelemetry log record;
52
+ * - `'span-event'`: add an event to the active span (nothing if no active span);
53
+ * - `'both'`.
54
+ */
55
+ target?: 'log' | 'span-event' | 'both';
56
+ /** Logger name for emitted records. Defaults to `'autotel.console'`. */
57
+ loggerName?: string;
58
+ /** Static attributes merged onto every captured record/event. */
59
+ attributes?: Attributes;
60
+ }
61
+ /**
62
+ * Start capturing `console.*` calls as wide events. Returns a disposer that
63
+ * stops capture. Safe to call on runtimes without the console channels (no-op).
64
+ */
65
+ declare function captureConsole(options?: CaptureConsoleOptions): () => void;
66
+ //#endregion
67
+ //#region src/diagnostics/http.d.ts
68
+ interface InstrumentHttpOptions {
69
+ /** Instrument inbound (server) requests. Default `true`. */
70
+ server?: boolean;
71
+ /** Instrument outbound (client) requests. Default `true`. */
72
+ client?: boolean;
73
+ /** Tracer to use. Defaults to `trace.getTracer('autotel.http-diagnostics')`. */
74
+ tracer?: Tracer;
75
+ }
76
+ /**
77
+ * Start emitting HTTP server/client spans from Node's HTTP diagnostics
78
+ * channels. Returns a disposer; a no-op on runtimes without the channels.
79
+ */
80
+ declare function instrumentHttp(options?: InstrumentHttpOptions): () => void;
81
+ //#endregion
82
+ export { type CaptureConsoleOptions, type ChannelMessageHandler, type ConsoleLevel, type InstrumentHttpOptions, type TracingChannelHandlers, captureConsole, diagnosticsChannelAvailable, instrumentHttp, subscribeChannel, subscribeTracingChannel };
83
+ //# sourceMappingURL=diagnostics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostics.d.ts","names":[],"sources":["../src/diagnostics/channel.ts","../src/diagnostics/console.ts","../src/diagnostics/http.ts"],"mappings":";;;;;;AA8BA;;;;AAA2C;AAK3C;;;;AAEuB;AAOvB;;;iBAdgB,2BAAA;;KAKJ,qBAAA,IACV,OAAA,WACA,IAAqB;;;AASS;AAchC;iBAhBgB,gBAAA,CACd,IAAA,UACA,OAAA,EAAS,qBAAqB;;UAcf,sBAAA;EACf,KAAA,EAAO,OAAA;EACP,GAAA,EAAK,OAAA;EACL,UAAA,EAAY,OAAA;EACZ,QAAA,EAAU,OAAA;EACV,KAAA,EAAO,OAAA;AAAA;;;;;;iBAQO,uBAAA,CACd,IAAA,UACA,QAAA,EAAU,sBAAsB;;;;KC9CtB,YAAA;AAAA,UAkBK,qBAAA;EDcf;ECZA,MAAA,YAAkB,YAAA;EDalB;;;;;;ECNA,MAAA;EDSO;ECPP,UAAA;EDOuB;ECLvB,UAAA,GAAa,UAAU;AAAA;;;;;iBA2BT,cAAA,CACd,OAAmC,GAA1B,qBAA0B;;;UCrBpB,qBAAA;EFOf;EELA,MAAA;EFMA;EEJA,MAAA;EFIgC;EEFhC,MAAA,GAAS,MAAM;AAAA;;AD5CjB;;;iBC4FgB,cAAA,CACd,OAAmC,GAA1B,qBAA0B"}
@@ -0,0 +1,274 @@
1
+ import { n as safeRequire } from "./node-require-vROmTeJ8.js";
2
+ 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";
3
+ import { SpanKind, SpanStatusCode, context, defaultTextMapGetter, defaultTextMapSetter, propagation, trace } from "@opentelemetry/api";
4
+ import { SeverityNumber, logs } from "@opentelemetry/api-logs";
5
+
6
+ //#region src/diagnostics/channel.ts
7
+ /**
8
+ * Edge-safe wrappers over Node's `diagnostics_channel`.
9
+ *
10
+ * The module is loaded lazily through {@link safeRequire} — never a static
11
+ * `node:` import — so merely importing this file is side-effect-free and bundles
12
+ * cleanly for browser/edge targets, where every subscribe call degrades to a
13
+ * no-op (returning an unsubscribe that does nothing). This is the shared
14
+ * primitive behind autotel's diagnostics-channel integrations (console capture,
15
+ * HTTP spans) and any app- or library-specific channel you want to bridge into
16
+ * a span/event.
17
+ *
18
+ * `diagnostics_channel.subscribe` (Node 18.7+) and `tracingChannel` (Node 19+)
19
+ * are used; autotel targets Node 22+, but on any runtime that lacks them the
20
+ * loader returns `undefined` and the helpers no-op.
21
+ */
22
+ let cached;
23
+ function loadDiagnosticsChannel() {
24
+ if (cached !== void 0) return cached ?? void 0;
25
+ cached = safeRequire("node:diagnostics_channel") ?? null;
26
+ return cached ?? void 0;
27
+ }
28
+ /** Whether Node's `diagnostics_channel` is available in this runtime. */
29
+ function diagnosticsChannelAvailable() {
30
+ return loadDiagnosticsChannel() !== void 0;
31
+ }
32
+ /**
33
+ * Subscribe to a named diagnostics channel. Returns an idempotent unsubscribe
34
+ * function; a no-op (that still returns a disposer) on unsupported runtimes.
35
+ */
36
+ function subscribeChannel(name, handler) {
37
+ const dc = loadDiagnosticsChannel();
38
+ if (!dc?.subscribe) return () => {};
39
+ dc.subscribe(name, handler);
40
+ let active = true;
41
+ return () => {
42
+ if (!active) return;
43
+ active = false;
44
+ dc.unsubscribe?.(name, handler);
45
+ };
46
+ }
47
+ /**
48
+ * Subscribe to a `tracingChannel` (the `tracing:${name}:{start,end,…}` set).
49
+ * Returns an idempotent unsubscribe; a no-op on runtimes without
50
+ * `tracingChannel` support.
51
+ */
52
+ function subscribeTracingChannel(name, handlers) {
53
+ const channel = loadDiagnosticsChannel()?.tracingChannel?.(name);
54
+ if (!channel) return () => {};
55
+ channel.subscribe(handlers);
56
+ let active = true;
57
+ return () => {
58
+ if (!active) return;
59
+ active = false;
60
+ channel.unsubscribe(handlers);
61
+ };
62
+ }
63
+
64
+ //#endregion
65
+ //#region src/diagnostics/console.ts
66
+ /**
67
+ * Capture `console.*` calls as wide events — without monkey-patching `console`.
68
+ *
69
+ * Node publishes every `console.log` / `info` / `debug` / `warn` / `error` call
70
+ * on a built-in diagnostics channel. {@link captureConsole} subscribes to those
71
+ * channels and turns each call into an OpenTelemetry **log record** (correlated
72
+ * to the active span via trace context by the logs SDK) and/or a **span event**
73
+ * on the active span. Nothing patches the global `console`, so there is no
74
+ * load-order fragility and no interference with other tooling.
75
+ *
76
+ * Opt-in. Call once after `init()` and keep the returned disposer to stop:
77
+ *
78
+ * ```ts
79
+ * import { captureConsole } from 'autotel/diagnostics';
80
+ *
81
+ * const stop = captureConsole(); // every console.* → correlated log record
82
+ * // …later: stop();
83
+ * ```
84
+ *
85
+ * The built-in `console.*` channels are a Stability-1 (experimental) Node API;
86
+ * this module degrades to a no-op where they are unavailable.
87
+ */
88
+ const ALL_LEVELS = [
89
+ "log",
90
+ "info",
91
+ "debug",
92
+ "warn",
93
+ "error"
94
+ ];
95
+ const SEVERITY = {
96
+ debug: SeverityNumber.DEBUG,
97
+ log: SeverityNumber.INFO,
98
+ info: SeverityNumber.INFO,
99
+ warn: SeverityNumber.WARN,
100
+ error: SeverityNumber.ERROR
101
+ };
102
+ const nodeUtil = safeRequire("node:util");
103
+ /** Format console arguments the way `console` itself would (printf + inspect). */
104
+ function formatArgs(args) {
105
+ if (nodeUtil?.format) return nodeUtil.format(...args);
106
+ return args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ");
107
+ }
108
+ function safeStringify(value) {
109
+ try {
110
+ return JSON.stringify(value) ?? String(value);
111
+ } catch {
112
+ return String(value);
113
+ }
114
+ }
115
+ /**
116
+ * Start capturing `console.*` calls as wide events. Returns a disposer that
117
+ * stops capture. Safe to call on runtimes without the console channels (no-op).
118
+ */
119
+ function captureConsole(options = {}) {
120
+ const levels = options.levels ?? ALL_LEVELS;
121
+ const target = options.target ?? "log";
122
+ const toLog = target === "log" || target === "both";
123
+ const toSpan = target === "span-event" || target === "both";
124
+ const logger = logs.getLogger(options.loggerName ?? "autotel.console");
125
+ let recording = false;
126
+ const disposers = levels.map((level) => subscribeChannel(`console.${level}`, (message) => {
127
+ if (recording) return;
128
+ const body = formatArgs(message?.args ?? []);
129
+ recording = true;
130
+ try {
131
+ const attributes = {
132
+ "log.source": "console",
133
+ "log.method": level,
134
+ ...options.attributes
135
+ };
136
+ if (toLog) logger.emit({
137
+ severityNumber: SEVERITY[level],
138
+ severityText: level.toUpperCase(),
139
+ body,
140
+ attributes
141
+ });
142
+ if (toSpan) trace.getActiveSpan()?.addEvent("log", {
143
+ "log.message": body,
144
+ ...attributes
145
+ });
146
+ } finally {
147
+ recording = false;
148
+ }
149
+ }));
150
+ let active = true;
151
+ return () => {
152
+ if (!active) return;
153
+ active = false;
154
+ for (const dispose of disposers) dispose();
155
+ };
156
+ }
157
+
158
+ //#endregion
159
+ //#region src/diagnostics/http.ts
160
+ const SERVER_SPANS = /* @__PURE__ */ new WeakMap();
161
+ const CLIENT_SPANS = /* @__PURE__ */ new WeakMap();
162
+ function firstHeader(value) {
163
+ return Array.isArray(value) ? value[0] : value;
164
+ }
165
+ function splitHostPort(host) {
166
+ if (!host) return {};
167
+ const idx = host.lastIndexOf(":");
168
+ if (idx === -1) return { address: host };
169
+ const port = Number(host.slice(idx + 1));
170
+ return {
171
+ address: host.slice(0, idx),
172
+ port: Number.isFinite(port) ? port : void 0
173
+ };
174
+ }
175
+ /**
176
+ * Start emitting HTTP server/client spans from Node's HTTP diagnostics
177
+ * channels. Returns a disposer; a no-op on runtimes without the channels.
178
+ */
179
+ function instrumentHttp(options = {}) {
180
+ const tracer = options.tracer ?? trace.getTracer("autotel.http-diagnostics");
181
+ const disposers = [];
182
+ if (options.server !== false) disposers.push(subscribeChannel("http.server.request.start", (message) => {
183
+ const request = message?.request;
184
+ if (!request) return;
185
+ const method = request.method ?? "HTTP";
186
+ const { address, port } = splitHostPort(firstHeader(request.headers.host));
187
+ const path = (request.url ?? "/").split("?", 1)[0];
188
+ const attributes = {
189
+ [ATTR_HTTP_REQUEST_METHOD]: method,
190
+ [ATTR_URL_PATH]: path,
191
+ [ATTR_URL_SCHEME]: "http",
192
+ [ATTR_NETWORK_PROTOCOL_VERSION]: request.httpVersion,
193
+ [ATTR_USER_AGENT_ORIGINAL]: firstHeader(request.headers["user-agent"]),
194
+ [ATTR_SERVER_ADDRESS]: address,
195
+ [ATTR_SERVER_PORT]: port
196
+ };
197
+ const parent = propagation.extract(context.active(), request.headers, defaultTextMapGetter);
198
+ const span = tracer.startSpan(method, {
199
+ kind: SpanKind.SERVER,
200
+ attributes
201
+ }, parent);
202
+ SERVER_SPANS.set(request, span);
203
+ }), subscribeChannel("http.server.response.finish", (message) => {
204
+ const { request, response } = message ?? {};
205
+ if (!request) return;
206
+ const span = SERVER_SPANS.get(request);
207
+ if (!span) return;
208
+ SERVER_SPANS.delete(request);
209
+ finishHttpSpan(span, response?.statusCode, 500);
210
+ }));
211
+ if (options.client !== false) disposers.push(subscribeChannel("http.client.request.start", (message) => {
212
+ const request = message?.request;
213
+ if (!request) return;
214
+ const method = request.method ?? "HTTP";
215
+ const req = request;
216
+ const { address, port } = splitHostPort(req.host);
217
+ const scheme = (req.protocol ?? "http:").replace(":", "");
218
+ const attributes = {
219
+ [ATTR_HTTP_REQUEST_METHOD]: method,
220
+ [ATTR_SERVER_ADDRESS]: address,
221
+ [ATTR_SERVER_PORT]: port,
222
+ [ATTR_URL_FULL]: address && req.path ? `${scheme}://${req.host}${req.path}` : void 0
223
+ };
224
+ const span = tracer.startSpan(method, {
225
+ kind: SpanKind.CLIENT,
226
+ attributes
227
+ });
228
+ CLIENT_SPANS.set(request, span);
229
+ if (!request.headersSent) {
230
+ const carrier = {};
231
+ propagation.inject(trace.setSpan(context.active(), span), carrier, defaultTextMapSetter);
232
+ for (const [key, value] of Object.entries(carrier)) try {
233
+ request.setHeader(key, value);
234
+ } catch {}
235
+ }
236
+ }), subscribeChannel("http.client.response.finish", (message) => {
237
+ const { request, response } = message ?? {};
238
+ if (!request) return;
239
+ const span = CLIENT_SPANS.get(request);
240
+ if (!span) return;
241
+ CLIENT_SPANS.delete(request);
242
+ finishHttpSpan(span, response?.statusCode, 400);
243
+ }), subscribeChannel("http.client.request.error", (message) => {
244
+ const { request, error } = message ?? {};
245
+ if (!request) return;
246
+ const span = CLIENT_SPANS.get(request);
247
+ if (!span) return;
248
+ CLIENT_SPANS.delete(request);
249
+ if (error instanceof Error) span.recordException(error);
250
+ span.setStatus({
251
+ code: SpanStatusCode.ERROR,
252
+ message: error instanceof Error ? error.message : void 0
253
+ });
254
+ span.end();
255
+ }));
256
+ let active = true;
257
+ return () => {
258
+ if (!active) return;
259
+ active = false;
260
+ for (const dispose of disposers) dispose();
261
+ };
262
+ }
263
+ /** Set status code + error status (when `>= errorAt`) and end the span. */
264
+ function finishHttpSpan(span, statusCode, errorAt) {
265
+ if (statusCode !== void 0) {
266
+ span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, statusCode);
267
+ if (statusCode >= errorAt) span.setStatus({ code: SpanStatusCode.ERROR });
268
+ }
269
+ span.end();
270
+ }
271
+
272
+ //#endregion
273
+ export { captureConsole, diagnosticsChannelAvailable, instrumentHttp, subscribeChannel, subscribeTracingChannel };
274
+ //# sourceMappingURL=diagnostics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostics.js","names":["otelContext"],"sources":["../src/diagnostics/channel.ts","../src/diagnostics/console.ts","../src/diagnostics/http.ts"],"sourcesContent":["/**\n * Edge-safe wrappers over Node's `diagnostics_channel`.\n *\n * The module is loaded lazily through {@link safeRequire} — never a static\n * `node:` import — so merely importing this file is side-effect-free and bundles\n * cleanly for browser/edge targets, where every subscribe call degrades to a\n * no-op (returning an unsubscribe that does nothing). This is the shared\n * primitive behind autotel's diagnostics-channel integrations (console capture,\n * HTTP spans) and any app- or library-specific channel you want to bridge into\n * a span/event.\n *\n * `diagnostics_channel.subscribe` (Node 18.7+) and `tracingChannel` (Node 19+)\n * are used; autotel targets Node 22+, but on any runtime that lacks them the\n * loader returns `undefined` and the helpers no-op.\n */\n\nimport { safeRequire } from '../node-require.js';\n\ntype DiagnosticsChannelModule = typeof import('node:diagnostics_channel');\n\nlet cached: DiagnosticsChannelModule | null | undefined;\n\nfunction loadDiagnosticsChannel(): DiagnosticsChannelModule | undefined {\n if (cached !== undefined) return cached ?? undefined;\n cached =\n safeRequire<DiagnosticsChannelModule>('node:diagnostics_channel') ?? null;\n return cached ?? undefined;\n}\n\n/** Whether Node's `diagnostics_channel` is available in this runtime. */\nexport function diagnosticsChannelAvailable(): boolean {\n return loadDiagnosticsChannel() !== undefined;\n}\n\n/** Handler for a plain named channel. */\nexport type ChannelMessageHandler = (\n message: unknown,\n name: string | symbol,\n) => void;\n\n/**\n * Subscribe to a named diagnostics channel. Returns an idempotent unsubscribe\n * function; a no-op (that still returns a disposer) on unsupported runtimes.\n */\nexport function subscribeChannel(\n name: string,\n handler: ChannelMessageHandler,\n): () => void {\n const dc = loadDiagnosticsChannel();\n if (!dc?.subscribe) return () => {};\n dc.subscribe(name, handler);\n let active = true;\n return () => {\n if (!active) return;\n active = false;\n dc.unsubscribe?.(name, handler);\n };\n}\n\n/** Subscriber set for a {@link https://nodejs.org/api/diagnostics_channel.html#class-tracingchannel TracingChannel}. */\nexport interface TracingChannelHandlers {\n start?(message: unknown): void;\n end?(message: unknown): void;\n asyncStart?(message: unknown): void;\n asyncEnd?(message: unknown): void;\n error?(message: unknown): void;\n}\n\n/**\n * Subscribe to a `tracingChannel` (the `tracing:${name}:{start,end,…}` set).\n * Returns an idempotent unsubscribe; a no-op on runtimes without\n * `tracingChannel` support.\n */\nexport function subscribeTracingChannel(\n name: string,\n handlers: TracingChannelHandlers,\n): () => void {\n const dc = loadDiagnosticsChannel();\n const channel = dc?.tracingChannel?.(name);\n if (!channel) return () => {};\n // Node's typings want all five handlers; we pass the subset provided.\n channel.subscribe(handlers as Parameters<typeof channel.subscribe>[0]);\n let active = true;\n return () => {\n if (!active) return;\n active = false;\n channel.unsubscribe(handlers as Parameters<typeof channel.unsubscribe>[0]);\n };\n}\n","/**\n * Capture `console.*` calls as wide events — without monkey-patching `console`.\n *\n * Node publishes every `console.log` / `info` / `debug` / `warn` / `error` call\n * on a built-in diagnostics channel. {@link captureConsole} subscribes to those\n * channels and turns each call into an OpenTelemetry **log record** (correlated\n * to the active span via trace context by the logs SDK) and/or a **span event**\n * on the active span. Nothing patches the global `console`, so there is no\n * load-order fragility and no interference with other tooling.\n *\n * Opt-in. Call once after `init()` and keep the returned disposer to stop:\n *\n * ```ts\n * import { captureConsole } from 'autotel/diagnostics';\n *\n * const stop = captureConsole(); // every console.* → correlated log record\n * // …later: stop();\n * ```\n *\n * The built-in `console.*` channels are a Stability-1 (experimental) Node API;\n * this module degrades to a no-op where they are unavailable.\n */\n\nimport { trace, type Attributes } from '@opentelemetry/api';\nimport { logs, SeverityNumber, type Logger } from '@opentelemetry/api-logs';\nimport { safeRequire } from '../node-require.js';\nimport { subscribeChannel } from './channel.js';\n\n/** Console methods that publish a diagnostics channel. */\nexport type ConsoleLevel = 'log' | 'info' | 'debug' | 'warn' | 'error';\n\nconst ALL_LEVELS: readonly ConsoleLevel[] = [\n 'log',\n 'info',\n 'debug',\n 'warn',\n 'error',\n];\n\nconst SEVERITY: Record<ConsoleLevel, SeverityNumber> = {\n debug: SeverityNumber.DEBUG,\n log: SeverityNumber.INFO,\n info: SeverityNumber.INFO,\n warn: SeverityNumber.WARN,\n error: SeverityNumber.ERROR,\n};\n\nexport interface CaptureConsoleOptions {\n /** Which console methods to capture. Defaults to all five. */\n levels?: readonly ConsoleLevel[];\n /**\n * Where to record captured output:\n * - `'log'` (default): emit an OpenTelemetry log record;\n * - `'span-event'`: add an event to the active span (nothing if no active span);\n * - `'both'`.\n */\n target?: 'log' | 'span-event' | 'both';\n /** Logger name for emitted records. Defaults to `'autotel.console'`. */\n loggerName?: string;\n /** Static attributes merged onto every captured record/event. */\n attributes?: Attributes;\n}\n\ntype ConsoleMessage = { args?: unknown[] };\n\nconst nodeUtil = safeRequire<typeof import('node:util')>('node:util');\n\n/** Format console arguments the way `console` itself would (printf + inspect). */\nfunction formatArgs(args: unknown[]): string {\n if (nodeUtil?.format) return nodeUtil.format(...args);\n return args\n .map((a) => (typeof a === 'string' ? a : safeStringify(a)))\n .join(' ');\n}\n\nfunction safeStringify(value: unknown): string {\n try {\n return JSON.stringify(value) ?? String(value);\n } catch {\n return String(value);\n }\n}\n\n/**\n * Start capturing `console.*` calls as wide events. Returns a disposer that\n * stops capture. Safe to call on runtimes without the console channels (no-op).\n */\nexport function captureConsole(\n options: CaptureConsoleOptions = {},\n): () => void {\n const levels = options.levels ?? ALL_LEVELS;\n const target = options.target ?? 'log';\n const toLog = target === 'log' || target === 'both';\n const toSpan = target === 'span-event' || target === 'both';\n const logger: Logger = logs.getLogger(\n options.loggerName ?? 'autotel.console',\n );\n\n // Guard against re-entrancy: if recording a captured call itself triggers a\n // `console.*` (e.g. an exporter logging a warning), don't capture that.\n let recording = false;\n\n const disposers = levels.map((level) =>\n subscribeChannel(`console.${level}`, (message) => {\n if (recording) return;\n const args = (message as ConsoleMessage)?.args ?? [];\n const body = formatArgs(args as unknown[]);\n recording = true;\n try {\n const attributes: Attributes = {\n 'log.source': 'console',\n 'log.method': level,\n ...options.attributes,\n };\n if (toLog) {\n logger.emit({\n severityNumber: SEVERITY[level],\n severityText: level.toUpperCase(),\n body,\n attributes,\n });\n }\n if (toSpan) {\n trace\n .getActiveSpan()\n ?.addEvent('log', { 'log.message': body, ...attributes });\n }\n } finally {\n recording = false;\n }\n }),\n );\n\n let active = true;\n return () => {\n if (!active) return;\n active = false;\n for (const dispose of disposers) dispose();\n };\n}\n","/**\n * Lightweight HTTP spans via Node's built-in `diagnostics_channel` — no\n * monkey-patching, no `import-in-the-middle`.\n *\n * Node publishes `http.server.request.start` / `http.server.response.finish`\n * and `http.client.request.start` / `http.client.response.finish` /\n * `http.client.request.error`. {@link instrumentHttp} subscribes to those and\n * emits a `SERVER` span per inbound request (parented to an incoming W3C\n * `traceparent`) and a `CLIENT` span per outbound request (whose context it\n * injects into the outgoing headers for downstream propagation).\n *\n * ```ts\n * import { instrumentHttp } from 'autotel/diagnostics';\n *\n * const stop = instrumentHttp();\n * ```\n *\n * Scope & limitation. This is an opt-in, low-overhead alternative to\n * `@opentelemetry/instrumentation-http` for HTTP span coverage + W3C\n * propagation. Client-side propagation works (the `traceparent` is injected on\n * the `ClientRequest` object directly). What it does **not** do is establish an\n * *ambient* OpenTelemetry context for the duration of a server request handler,\n * so application spans created inside a handler will not become children of the\n * `SERVER` span.\n *\n * This is structural, not a \"wait for a newer Node\" gap. Node publishes the\n * `http.*` channels with a plain `channel.publish()` — not `runStores` /\n * `tracingChannel` — so a subscriber has no scope to bind a store to. The only\n * ways to get handler nesting both defeat the purpose of using a channel:\n * 1. `AsyncLocalStorage.enterWith()` in the start handler — no scoped exit, so\n * context leaks across requests sharing an event-loop tick / keep-alive\n * connection and misattributes spans. Strictly worse than no nesting.\n * 2. Patching `http.Server.prototype.emit` to wrap the `'request'` listener in\n * `context.with()` — monkey-patching, i.e. reimplementing\n * `@opentelemetry/instrumentation-http`.\n * If you need handler nesting, use `@opentelemetry/instrumentation-http`.\n *\n * The `http.*` channels are a Stability-1 (experimental) Node API; this module\n * degrades to a no-op where they are unavailable.\n */\n\nimport type { ClientRequest, IncomingMessage, ServerResponse } from 'node:http';\nimport {\n context as otelContext,\n defaultTextMapGetter,\n defaultTextMapSetter,\n propagation,\n SpanKind,\n SpanStatusCode,\n trace,\n type Attributes,\n type Span,\n type Tracer,\n} from '@opentelemetry/api';\nimport {\n ATTR_HTTP_REQUEST_METHOD,\n ATTR_HTTP_RESPONSE_STATUS_CODE,\n ATTR_NETWORK_PROTOCOL_VERSION,\n ATTR_SERVER_ADDRESS,\n ATTR_SERVER_PORT,\n ATTR_URL_FULL,\n ATTR_URL_PATH,\n ATTR_URL_SCHEME,\n ATTR_USER_AGENT_ORIGINAL,\n} from '@opentelemetry/semantic-conventions';\nimport { subscribeChannel } from './channel.js';\n\nexport interface InstrumentHttpOptions {\n /** Instrument inbound (server) requests. Default `true`. */\n server?: boolean;\n /** Instrument outbound (client) requests. Default `true`. */\n client?: boolean;\n /** Tracer to use. Defaults to `trace.getTracer('autotel.http-diagnostics')`. */\n tracer?: Tracer;\n}\n\ninterface ServerStartMessage {\n request?: IncomingMessage;\n response?: ServerResponse;\n}\ninterface ServerFinishMessage {\n request?: IncomingMessage;\n response?: ServerResponse;\n}\ninterface ClientStartMessage {\n request?: ClientRequest;\n}\ninterface ClientFinishMessage {\n request?: ClientRequest;\n response?: IncomingMessage;\n}\ninterface ClientErrorMessage {\n request?: ClientRequest;\n error?: unknown;\n}\n\nconst SERVER_SPANS = new WeakMap<object, Span>();\nconst CLIENT_SPANS = new WeakMap<object, Span>();\n\nfunction firstHeader(value: string | string[] | undefined): string | undefined {\n return Array.isArray(value) ? value[0] : value;\n}\n\nfunction splitHostPort(host: string | undefined): {\n address?: string;\n port?: number;\n} {\n if (!host) return {};\n const idx = host.lastIndexOf(':');\n if (idx === -1) return { address: host };\n const port = Number(host.slice(idx + 1));\n return {\n address: host.slice(0, idx),\n port: Number.isFinite(port) ? port : undefined,\n };\n}\n\n/**\n * Start emitting HTTP server/client spans from Node's HTTP diagnostics\n * channels. Returns a disposer; a no-op on runtimes without the channels.\n */\nexport function instrumentHttp(\n options: InstrumentHttpOptions = {},\n): () => void {\n const tracer = options.tracer ?? trace.getTracer('autotel.http-diagnostics');\n const disposers: Array<() => void> = [];\n\n if (options.server !== false) {\n disposers.push(\n subscribeChannel('http.server.request.start', (message) => {\n const request = (message as ServerStartMessage)?.request;\n if (!request) return;\n const method = request.method ?? 'HTTP';\n const host = firstHeader(request.headers.host);\n const { address, port } = splitHostPort(host);\n const path = (request.url ?? '/').split('?', 1)[0];\n const attributes: Attributes = {\n [ATTR_HTTP_REQUEST_METHOD]: method,\n [ATTR_URL_PATH]: path,\n [ATTR_URL_SCHEME]: 'http',\n [ATTR_NETWORK_PROTOCOL_VERSION]: request.httpVersion,\n [ATTR_USER_AGENT_ORIGINAL]: firstHeader(\n request.headers['user-agent'],\n ),\n [ATTR_SERVER_ADDRESS]: address,\n [ATTR_SERVER_PORT]: port,\n };\n const parent = propagation.extract(\n otelContext.active(),\n request.headers,\n defaultTextMapGetter,\n );\n const span = tracer.startSpan(\n method,\n { kind: SpanKind.SERVER, attributes },\n parent,\n );\n SERVER_SPANS.set(request, span);\n }),\n subscribeChannel('http.server.response.finish', (message) => {\n const { request, response } = (message as ServerFinishMessage) ?? {};\n if (!request) return;\n const span = SERVER_SPANS.get(request);\n if (!span) return;\n SERVER_SPANS.delete(request);\n finishHttpSpan(span, response?.statusCode, 500);\n }),\n );\n }\n\n if (options.client !== false) {\n disposers.push(\n subscribeChannel('http.client.request.start', (message) => {\n const request = (message as ClientStartMessage)?.request;\n if (!request) return;\n const method = request.method ?? 'HTTP';\n // `ClientRequest` exposes host/protocol/path on the public surface.\n const req = request as ClientRequest & {\n host?: string;\n protocol?: string;\n path?: string;\n };\n const { address, port } = splitHostPort(req.host);\n const scheme = (req.protocol ?? 'http:').replace(':', '');\n const attributes: Attributes = {\n [ATTR_HTTP_REQUEST_METHOD]: method,\n [ATTR_SERVER_ADDRESS]: address,\n [ATTR_SERVER_PORT]: port,\n [ATTR_URL_FULL]:\n address && req.path\n ? `${scheme}://${req.host}${req.path}`\n : undefined,\n };\n const span = tracer.startSpan(method, {\n kind: SpanKind.CLIENT,\n attributes,\n });\n CLIENT_SPANS.set(request, span);\n\n // Inject this span's context into the outbound headers so the\n // downstream service continues the trace.\n if (!request.headersSent) {\n const carrier: Record<string, string> = {};\n propagation.inject(\n trace.setSpan(otelContext.active(), span),\n carrier,\n defaultTextMapSetter,\n );\n for (const [key, value] of Object.entries(carrier)) {\n try {\n request.setHeader(key, value);\n } catch {\n // Headers already sent / immutable — propagation best-effort.\n }\n }\n }\n }),\n subscribeChannel('http.client.response.finish', (message) => {\n const { request, response } = (message as ClientFinishMessage) ?? {};\n if (!request) return;\n const span = CLIENT_SPANS.get(request);\n if (!span) return;\n CLIENT_SPANS.delete(request);\n finishHttpSpan(span, response?.statusCode, 400);\n }),\n subscribeChannel('http.client.request.error', (message) => {\n const { request, error } = (message as ClientErrorMessage) ?? {};\n if (!request) return;\n const span = CLIENT_SPANS.get(request);\n if (!span) return;\n CLIENT_SPANS.delete(request);\n if (error instanceof Error) span.recordException(error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : undefined,\n });\n span.end();\n }),\n );\n }\n\n let active = true;\n return () => {\n if (!active) return;\n active = false;\n for (const dispose of disposers) dispose();\n };\n}\n\n/** Set status code + error status (when `>= errorAt`) and end the span. */\nfunction finishHttpSpan(\n span: Span,\n statusCode: number | undefined,\n errorAt: number,\n): void {\n if (statusCode !== undefined) {\n span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, statusCode);\n if (statusCode >= errorAt) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n }\n }\n span.end();\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autotel",
3
- "version": "4.2.1",
3
+ "version": "4.2.2",
4
4
  "description": "Write Once, Observe Anywhere",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -15,6 +15,11 @@
15
15
  "import": "./dist/index.js",
16
16
  "require": "./dist/index.cjs"
17
17
  },
18
+ "./diagnostics": {
19
+ "types": "./dist/diagnostics.d.ts",
20
+ "import": "./dist/diagnostics.js",
21
+ "require": "./dist/diagnostics.cjs"
22
+ },
18
23
  "./instrumentation": {
19
24
  "types": "./dist/instrumentation.d.ts",
20
25
  "import": "./dist/instrumentation.js",
@@ -393,28 +393,31 @@ autotel implements the **OTel gen-ai semantic conventions** out of the box. Toke
393
393
  ```typescript
394
394
  import { trace } from 'autotel';
395
395
  import { withAiTelemetry } from 'autotel-edge';
396
+ import { registerTelemetry } from 'ai';
396
397
  import { streamText } from 'ai';
398
+ import { autotelTelemetry } from 'autotel-genai/observer';
399
+
400
+ registerTelemetry(autotelTelemetry()); // Node / server runtimes
397
401
 
398
402
  const handler = trace(async (req) => {
399
403
  const result = await streamText({
400
404
  model: withAiTelemetry('anthropic/claude-sonnet-4.6'),
401
405
  messages: req.messages,
402
- experimental_telemetry: { isEnabled: true },
403
406
  });
404
407
  return result.toResponse();
405
408
  });
406
409
  ```
407
410
 
408
- Captured attributes per call: `gen_ai.provider.name`, `gen_ai.request.model`, `gen_ai.usage.input_tokens` / `output_tokens` / `reasoning.output_tokens` / `cache_read.input_tokens`, `gen_ai.response.finish_reasons`, `gen_ai.response.id`, plus per-tool spans with `gen_ai.tool.name`. Cost estimation (`gen_ai.usage.cost.usd`) comes for free if you pass a pricing map to `withAiTelemetry`.
411
+ Captured attributes per call: `gen_ai.provider.name`, `gen_ai.request.model`, `gen_ai.usage.input_tokens` / `output_tokens` / `reasoning.output_tokens` / `cache_read.input_tokens`, `gen_ai.response.finish_reasons`, `gen_ai.response.id`, plus per-tool spans with `gen_ai.tool.name`. Cost estimation (`gen_ai.usage.cost.usd`) comes for free from `autotelTelemetry()` in Node runtimes or if you pass a pricing map to `withAiTelemetry`.
409
412
 
410
413
  Anti-patterns to detect:
411
414
 
412
- | Anti-pattern | Fix |
413
- | ------------------------------------- | --------------------------------------------------------- |
414
- | Manual `result.usage` printing | `withAiTelemetry()` captures via middleware |
415
- | Custom `ai.tokens` attribute names | Use OTel gen-ai conventions (`gen_ai.usage.input_tokens`) |
416
- | Tool calls as plain log lines | Each tool call gets a child span automatically |
417
- | No retry / partial-failure visibility | `experimental_telemetry: { isEnabled: true }` flips it on |
415
+ | Anti-pattern | Fix |
416
+ | ---------------------------------- | --------------------------------------------------------- |
417
+ | Manual `result.usage` printing | `autotelTelemetry()` or `withAiTelemetry()` |
418
+ | Custom `ai.tokens` attribute names | Use OTel gen-ai conventions (`gen_ai.usage.input_tokens`) |
419
+ | Tool calls as plain log lines | Each tool call gets a child span automatically |
420
+ | AI SDK not registered in Node | `registerTelemetry(autotelTelemetry())` |
418
421
 
419
422
  ---
420
423
 
@@ -49,10 +49,11 @@ Run through this list when adding observability to a new service or auditing an
49
49
 
50
50
  ## AI / LLM
51
51
 
52
- - [ ] **`withAiTelemetry()` from `autotel-edge`.** Captures `gen_ai.*` semantic attributes automatically.
52
+ - [ ] **Node/server runtimes register `autotelTelemetry()`.** `registerTelemetry(autotelTelemetry())` captures canonical `gen_ai.*`, tool spans, cost, and streaming timing.
53
+ - [ ] **Edge runtimes use `withAiTelemetry()` from `autotel-edge`.** Captures `gen_ai.*` semantic attributes automatically.
53
54
  - [ ] **No bespoke `ai.tokens` attributes.** Use `gen_ai.usage.input_tokens`, etc.
54
- - [ ] **Cost tracking via the `cost` option** — outputs `gen_ai.cost.usd` on the span.
55
- - [ ] **Tool-call spans enabled.** `experimental_telemetry: { isEnabled: true }`.
55
+ - [ ] **Cost tracking is canonical.** Use `gen_ai.usage.cost.usd`, not custom cost keys.
56
+ - [ ] **Tool-call spans are emitted by the integration.** No plain log-only tool activity.
56
57
 
57
58
  ## Testing
58
59