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 +50 -0
- package/dist/diagnostics.cjs +279 -0
- package/dist/diagnostics.cjs.map +1 -0
- package/dist/diagnostics.d.cts +83 -0
- package/dist/diagnostics.d.cts.map +1 -0
- package/dist/diagnostics.d.ts +83 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +274 -0
- package/dist/diagnostics.js.map +1 -0
- package/package.json +6 -1
- package/skills/review-otel-patterns/SKILL.md +11 -8
- package/skills/review-otel-patterns/references/code-review.md +4 -3
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.
|
|
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
|
|
413
|
-
|
|
|
414
|
-
| Manual `result.usage` printing
|
|
415
|
-
| Custom `ai.tokens` attribute names
|
|
416
|
-
| Tool calls as plain log lines
|
|
417
|
-
|
|
|
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
|
-
- [ ]
|
|
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
|
|
55
|
-
- [ ] **Tool-call spans
|
|
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
|
|