@foam-ai/node 0.1.0-alpha.2 → 0.1.0-alpha.4

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.
Files changed (44) hide show
  1. package/README.md +424 -77
  2. package/dist/constants.d.ts +42 -0
  3. package/dist/constants.js +54 -0
  4. package/dist/diagnostics.d.ts +13 -0
  5. package/dist/diagnostics.js +83 -0
  6. package/dist/exporters.d.ts +25 -0
  7. package/dist/exporters.js +105 -0
  8. package/dist/index.d.ts +5 -0
  9. package/dist/index.js +21 -0
  10. package/dist/ingest.d.ts +6 -0
  11. package/dist/ingest.js +58 -0
  12. package/dist/init.d.ts +22 -0
  13. package/dist/init.js +121 -0
  14. package/dist/instrumentations.d.ts +3 -0
  15. package/dist/instrumentations.js +79 -0
  16. package/dist/metrics.d.ts +5 -0
  17. package/dist/metrics.js +29 -0
  18. package/dist/network-capture/collector.d.ts +27 -0
  19. package/dist/network-capture/collector.js +312 -0
  20. package/dist/network-capture/http.d.ts +6 -0
  21. package/dist/network-capture/http.js +223 -0
  22. package/dist/network-capture/index.d.ts +2 -0
  23. package/dist/network-capture/index.js +7 -0
  24. package/dist/network-capture/undici.d.ts +9 -0
  25. package/dist/network-capture/undici.js +144 -0
  26. package/dist/propagation.d.ts +5 -0
  27. package/dist/propagation.js +45 -0
  28. package/dist/resource.d.ts +3 -0
  29. package/dist/resource.js +24 -0
  30. package/dist/state.d.ts +19 -0
  31. package/dist/state.js +44 -0
  32. package/dist/utils.d.ts +4 -0
  33. package/dist/utils.js +20 -0
  34. package/package.json +47 -19
  35. package/dist/node/src/capture-exception.d.ts +0 -9
  36. package/dist/node/src/capture-exception.js +0 -31
  37. package/dist/node/src/index.d.ts +0 -9
  38. package/dist/node/src/index.js +0 -12
  39. package/dist/node/src/init.d.ts +0 -27
  40. package/dist/node/src/init.js +0 -213
  41. package/dist/shared/constants.d.ts +0 -1
  42. package/dist/shared/constants.js +0 -4
  43. package/dist/shared/util.d.ts +0 -9
  44. package/dist/shared/util.js +0 -21
@@ -0,0 +1,312 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BodyCollector = void 0;
4
+ exports.setHeaderAttributes = setHeaderAttributes;
5
+ const api_1 = require("@opentelemetry/api");
6
+ const api_logs_1 = require("@opentelemetry/api-logs");
7
+ const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
8
+ const node_crypto_1 = require("node:crypto");
9
+ const node_zlib_1 = require("node:zlib");
10
+ const constants_js_1 = require("../constants.js");
11
+ const utils_js_1 = require("../utils.js");
12
+ const ALLOWED_HEADERS = new Set(constants_js_1.SAFE_NETWORK_HEADERS);
13
+ const ATTR = {
14
+ request: {
15
+ header: semantic_conventions_1.ATTR_HTTP_REQUEST_HEADER,
16
+ size: constants_js_1.ATTR_HTTP_REQUEST_BODY_SIZE,
17
+ content: constants_js_1.ATTR_HTTP_REQUEST_BODY_CONTENT,
18
+ captureId: constants_js_1.ATTR_FOAM_HTTP_REQUEST_BODY_CAPTURE_ID,
19
+ complete: constants_js_1.ATTR_FOAM_HTTP_REQUEST_BODY_COMPLETE,
20
+ truncated: constants_js_1.ATTR_FOAM_HTTP_REQUEST_BODY_TRUNCATED,
21
+ },
22
+ response: {
23
+ header: semantic_conventions_1.ATTR_HTTP_RESPONSE_HEADER,
24
+ size: constants_js_1.ATTR_HTTP_RESPONSE_BODY_SIZE,
25
+ content: constants_js_1.ATTR_HTTP_RESPONSE_BODY_CONTENT,
26
+ captureId: constants_js_1.ATTR_FOAM_HTTP_RESPONSE_BODY_CAPTURE_ID,
27
+ complete: constants_js_1.ATTR_FOAM_HTTP_RESPONSE_BODY_COMPLETE,
28
+ truncated: constants_js_1.ATTR_FOAM_HTTP_RESPONSE_BODY_TRUNCATED,
29
+ },
30
+ };
31
+ let activeCaptures = 0;
32
+ function captureLogger() {
33
+ return api_logs_1.logs.getLogger(constants_js_1.FOAM_DISTRO_NAME, constants_js_1.FOAM_DISTRO_VERSION);
34
+ }
35
+ function headerValues(value) {
36
+ return Array.isArray(value) ? value.map(String) : [String(value)];
37
+ }
38
+ function setHeaderAttributes(span, direction, headers) {
39
+ const attributes = {};
40
+ for (const [rawName, rawValue] of Object.entries(headers)) {
41
+ if (rawValue === undefined)
42
+ continue;
43
+ const name = rawName.toLowerCase();
44
+ if (!ALLOWED_HEADERS.has(name))
45
+ continue;
46
+ attributes[ATTR[direction].header(name)] = headerValues(rawValue);
47
+ }
48
+ if (Object.keys(attributes).length > 0)
49
+ span.setAttributes(attributes);
50
+ }
51
+ function copyBody(value, encoding) {
52
+ if (typeof value === "string") {
53
+ const enc = typeof encoding === "string" && Buffer.isEncoding(encoding)
54
+ ? encoding
55
+ : "utf8";
56
+ return Buffer.from(value, enc);
57
+ }
58
+ if (Buffer.isBuffer(value))
59
+ return Buffer.from(value);
60
+ if (ArrayBuffer.isView(value)) {
61
+ return Buffer.from(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
62
+ }
63
+ return undefined;
64
+ }
65
+ function decodeBody(bytes, contentEncoding) {
66
+ if (contentEncoding === undefined)
67
+ return bytes;
68
+ const encoding = headerValues(contentEncoding)[0]?.toLowerCase().trim();
69
+ if (!encoding || encoding === "identity")
70
+ return bytes;
71
+ try {
72
+ if (encoding === "gzip" || encoding === "x-gzip")
73
+ return (0, node_zlib_1.gunzipSync)(bytes);
74
+ if (encoding === "deflate") {
75
+ try {
76
+ return (0, node_zlib_1.inflateSync)(bytes);
77
+ }
78
+ catch {
79
+ return (0, node_zlib_1.unzipSync)(bytes);
80
+ }
81
+ }
82
+ if (encoding === "br")
83
+ return (0, node_zlib_1.brotliDecompressSync)(bytes);
84
+ }
85
+ catch {
86
+ return undefined;
87
+ }
88
+ return undefined;
89
+ }
90
+ function textEncoding(contentType) {
91
+ const raw = headerValues(contentType)[0];
92
+ if (raw === undefined)
93
+ return undefined;
94
+ const parts = raw.split(";").map((part) => part.trim());
95
+ const media = (parts[0] ?? "").toLowerCase();
96
+ const charset = parts
97
+ .find((part) => part.toLowerCase().startsWith("charset="))
98
+ ?.slice("charset=".length)
99
+ .toLowerCase();
100
+ const text = media.startsWith("text/") ||
101
+ media.endsWith("/json") ||
102
+ media.endsWith("+json") ||
103
+ media.endsWith("/xml") ||
104
+ media.endsWith("+xml") ||
105
+ media.endsWith("/yaml") ||
106
+ media.endsWith("+yaml") ||
107
+ media === "application/x-www-form-urlencoded" ||
108
+ Boolean(charset);
109
+ if (!text)
110
+ return undefined;
111
+ return charset && Buffer.isEncoding(charset) ? charset : "utf8";
112
+ }
113
+ /** Last index such that `buf.subarray(0, end)` is complete UTF-8, capped at `max`. */
114
+ function utf8End(buf, max) {
115
+ const end = Math.min(max, buf.byteLength);
116
+ if (end === 0)
117
+ return 0;
118
+ let leadAt = end;
119
+ while (leadAt > 0 && (buf[leadAt - 1] & 0xc0) === 0x80)
120
+ leadAt--;
121
+ if (leadAt === 0)
122
+ return 0;
123
+ const lead = buf[leadAt - 1];
124
+ const needed = (lead & 0x80) === 0
125
+ ? 1
126
+ : (lead & 0xe0) === 0xc0
127
+ ? 2
128
+ : (lead & 0xf0) === 0xe0
129
+ ? 3
130
+ : (lead & 0xf8) === 0xf0
131
+ ? 4
132
+ : Number.POSITIVE_INFINITY;
133
+ return end - (leadAt - 1) >= needed ? end : leadAt - 1;
134
+ }
135
+ // pcga11: Span attributes cannot carry byte arrays (OTel AttributeValue).
136
+ // Text goes on the span; binary uses the same `http.*.body.content` name on
137
+ // the correlated event, where log attributes accept Uint8Array.
138
+ function spanBodyContent(decoded, contentType) {
139
+ const encoding = textEncoding(contentType);
140
+ if (encoding === undefined)
141
+ return undefined;
142
+ const limited = decoded.byteLength > constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES
143
+ ? decoded.subarray(0, constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES)
144
+ : decoded;
145
+ const end = encoding === "utf8" || encoding === "utf-8"
146
+ ? utf8End(limited, limited.byteLength)
147
+ : limited.byteLength;
148
+ return limited.subarray(0, end).toString(encoding);
149
+ }
150
+ class BodyCollector {
151
+ span;
152
+ context;
153
+ side;
154
+ direction;
155
+ headers;
156
+ id = (0, node_crypto_1.randomUUID)();
157
+ attr;
158
+ chunks = [];
159
+ cleanups = [];
160
+ deadline;
161
+ observedBytes = 0;
162
+ capturedBytes = 0;
163
+ truncated = false;
164
+ finalized = false;
165
+ constructor(span, context, side, direction, headers) {
166
+ this.span = span;
167
+ this.context = context;
168
+ this.side = side;
169
+ this.direction = direction;
170
+ this.headers = headers;
171
+ this.attr = ATTR[direction];
172
+ activeCaptures += 1;
173
+ this.deadline = setTimeout(() => this.finalize(false), constants_js_1.NETWORK_CAPTURE_DEADLINE_MS);
174
+ this.deadline.unref();
175
+ this.span.setAttribute(this.attr.captureId, this.id);
176
+ }
177
+ static create(span, side, direction, headers) {
178
+ const context = api_1.trace.setSpanContext(api_1.ROOT_CONTEXT, span.spanContext());
179
+ if (activeCaptures >= constants_js_1.MAX_ACTIVE_CAPTURES ||
180
+ !span.isRecording() ||
181
+ (span.spanContext().traceFlags & api_1.TraceFlags.SAMPLED) !==
182
+ api_1.TraceFlags.SAMPLED ||
183
+ !captureLogger().enabled({
184
+ context,
185
+ eventName: constants_js_1.NETWORK_CAPTURE_EVENT,
186
+ severityNumber: api_logs_1.SeverityNumber.INFO,
187
+ })) {
188
+ return undefined;
189
+ }
190
+ return new BodyCollector(span, context, side, direction, headers);
191
+ }
192
+ addCleanup(cleanup) {
193
+ this.cleanups.push(cleanup);
194
+ }
195
+ observe(value, encoding) {
196
+ if (this.finalized)
197
+ return;
198
+ const bytes = copyBody(value, encoding);
199
+ if (!bytes)
200
+ return;
201
+ this.deadline.refresh();
202
+ this.observedBytes += bytes.byteLength;
203
+ this.span.setAttribute(this.attr.size, this.observedBytes);
204
+ if (this.capturedBytes >= constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES) {
205
+ this.truncated = true;
206
+ this.span.setAttribute(this.attr.truncated, true);
207
+ return;
208
+ }
209
+ const room = constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES - this.capturedBytes;
210
+ const kept = bytes.byteLength > room ? bytes.subarray(0, room) : bytes;
211
+ this.chunks.push(kept);
212
+ this.capturedBytes += kept.byteLength;
213
+ if (kept.byteLength < bytes.byteLength) {
214
+ this.truncated = true;
215
+ this.span.setAttribute(this.attr.truncated, true);
216
+ }
217
+ }
218
+ finalize(complete) {
219
+ if (this.finalized)
220
+ return;
221
+ this.finalized = true;
222
+ clearTimeout(this.deadline);
223
+ activeCaptures -= 1;
224
+ for (const cleanup of this.cleanups.splice(0))
225
+ (0, utils_js_1.safely)(cleanup);
226
+ // Runs inside stream handlers; header snapshots may throw on destroyed messages.
227
+ (0, utils_js_1.safely)(() => {
228
+ const headers = this.headers();
229
+ setHeaderAttributes(this.span, this.direction, headers);
230
+ this.span.setAttributes({
231
+ [this.attr.complete]: complete,
232
+ [this.attr.truncated]: this.truncated,
233
+ [this.attr.size]: this.observedBytes,
234
+ });
235
+ this.emit(this.chunks.splice(0), headers, complete);
236
+ });
237
+ }
238
+ emit(buffers, headers, complete) {
239
+ const outcome = this.observedBytes === 0
240
+ ? "empty"
241
+ : complete
242
+ ? "captured"
243
+ : "incomplete";
244
+ if (outcome === "empty")
245
+ return;
246
+ const wire = buffers.length <= 1 ? buffers[0] : Buffer.concat(buffers);
247
+ let binaryContent;
248
+ if (wire) {
249
+ const decoded = decodeBody(wire, headers["content-encoding"]);
250
+ const source = decoded ?? wire;
251
+ const text = decoded
252
+ ? spanBodyContent(decoded, headers["content-type"])
253
+ : undefined;
254
+ if (text !== undefined) {
255
+ this.span.setAttribute(this.attr.content, text);
256
+ }
257
+ else {
258
+ const limited = source.byteLength > constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES
259
+ ? source.subarray(0, constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES)
260
+ : source;
261
+ binaryContent = Uint8Array.from(limited);
262
+ }
263
+ }
264
+ const chunks = [];
265
+ if (wire) {
266
+ for (let i = 0; i < wire.byteLength; i += constants_js_1.NETWORK_BODY_CHUNK_BYTES) {
267
+ chunks.push(wire.subarray(i, i + constants_js_1.NETWORK_BODY_CHUNK_BYTES));
268
+ }
269
+ }
270
+ const common = {
271
+ [constants_js_1.ATTR_FOAM_HTTP_BODY_ID]: this.id,
272
+ [constants_js_1.ATTR_FOAM_HTTP_BODY_SIDE]: this.side,
273
+ [constants_js_1.ATTR_FOAM_HTTP_BODY_DIRECTION]: this.direction,
274
+ [constants_js_1.ATTR_FOAM_HTTP_BODY_COMPLETE]: complete,
275
+ [constants_js_1.ATTR_FOAM_HTTP_BODY_TRUNCATED]: this.truncated,
276
+ [constants_js_1.ATTR_FOAM_HTTP_BODY_OUTCOME]: outcome,
277
+ [this.attr.size]: this.observedBytes,
278
+ [constants_js_1.ATTR_FOAM_HTTP_BODY_CAPTURED_BYTES]: this.capturedBytes,
279
+ [constants_js_1.ATTR_FOAM_HTTP_BODY_CHUNK_COUNT]: chunks.length,
280
+ };
281
+ for (const name of ["content-type", "content-encoding"]) {
282
+ if (headers[name] !== undefined) {
283
+ common[this.attr.header(name)] = headerValues(headers[name]);
284
+ }
285
+ }
286
+ const logger = captureLogger();
287
+ const records = chunks.length === 0 ? [undefined] : chunks;
288
+ records.forEach((chunk, index) => {
289
+ logger.emit({
290
+ eventName: constants_js_1.NETWORK_CAPTURE_EVENT,
291
+ body: constants_js_1.NETWORK_CAPTURE_EVENT_BODY,
292
+ severityNumber: api_logs_1.SeverityNumber.INFO,
293
+ severityText: constants_js_1.SEVERITY_TEXT[api_logs_1.SeverityNumber.INFO],
294
+ attributes: {
295
+ ...common,
296
+ ...(chunk
297
+ ? {
298
+ [constants_js_1.ATTR_FOAM_HTTP_BODY_CHUNK_INDEX]: index,
299
+ [constants_js_1.ATTR_FOAM_HTTP_BODY_CHUNK_BYTES]: chunk.byteLength,
300
+ [constants_js_1.ATTR_FOAM_HTTP_BODY_CHUNK_CONTENT]: Uint8Array.from(chunk),
301
+ }
302
+ : {}),
303
+ ...(index === 0 && binaryContent
304
+ ? { [this.attr.content]: binaryContent }
305
+ : {}),
306
+ },
307
+ context: this.context,
308
+ });
309
+ });
310
+ }
311
+ }
312
+ exports.BodyCollector = BodyCollector;
@@ -0,0 +1,6 @@
1
+ import type { HttpRequestCustomAttributeFunction, HttpResponseCustomAttributeFunction } from "@opentelemetry/instrumentation-http";
2
+ /** Builds OTel HTTP request/response hooks that record headers as span attributes and stream bodies as log events. */
3
+ export declare function createNetworkCaptureHooks(bodyCaptureEnabled?: boolean): {
4
+ requestHook: HttpRequestCustomAttributeFunction;
5
+ responseHook: HttpResponseCustomAttributeFunction;
6
+ };
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createNetworkCaptureHooks = createNetworkCaptureHooks;
4
+ const node_events_1 = require("node:events");
5
+ const utils_js_1 = require("../utils.js");
6
+ const collector_js_1 = require("./collector.js");
7
+ function incomingHeaders(message) {
8
+ return message.headers;
9
+ }
10
+ function outgoingHeaders(message, direct) {
11
+ return {
12
+ ...Object.fromEntries(direct),
13
+ ...message.getHeaders(),
14
+ };
15
+ }
16
+ function observeDirectHeaders(args, headers) {
17
+ const append = (rawKey, value) => {
18
+ const key = String(rawKey).toLowerCase();
19
+ const existing = headers.get(key);
20
+ if (existing === undefined)
21
+ headers.set(key, value);
22
+ else if (Array.isArray(existing))
23
+ existing.push(value);
24
+ else
25
+ headers.set(key, [existing, value]);
26
+ };
27
+ const candidate = args.length >= 3 && typeof args[1] === "string" ? args[2] : args[1];
28
+ if (!candidate || typeof candidate !== "object")
29
+ return;
30
+ if (Array.isArray(candidate)) {
31
+ if (candidate.every(Array.isArray)) {
32
+ for (const entry of candidate) {
33
+ if (entry.length >= 2) {
34
+ append(entry[0], entry[1]);
35
+ }
36
+ }
37
+ }
38
+ else {
39
+ for (let index = 0; index + 1 < candidate.length; index += 2) {
40
+ append(candidate[index], candidate[index + 1]);
41
+ }
42
+ }
43
+ return;
44
+ }
45
+ for (const [rawKey, value] of Object.entries(candidate)) {
46
+ append(rawKey, value);
47
+ }
48
+ }
49
+ /** Replaces a method with a wrapped version; returns an undo that restores it unless someone else re-patched it. */
50
+ function swapMethod(target, key, wrap) {
51
+ const original = target[key];
52
+ const wrapped = wrap(original);
53
+ target[key] = wrapped;
54
+ return () => {
55
+ if (target[key] === wrapped)
56
+ target[key] = original;
57
+ };
58
+ }
59
+ /** Wraps writeHead (when present) to record inline headers; returns an undo. */
60
+ function swapWriteHead(target, directHeaders, afterWriteHead) {
61
+ const original = target.writeHead;
62
+ if (!original)
63
+ return () => { };
64
+ const wrapped = function (...args) {
65
+ const result = Reflect.apply(original, this, args);
66
+ (0, utils_js_1.safely)(() => {
67
+ observeDirectHeaders(args, directHeaders);
68
+ afterWriteHead?.();
69
+ });
70
+ return result;
71
+ };
72
+ target.writeHead = wrapped;
73
+ return () => {
74
+ if (target.writeHead === wrapped)
75
+ target.writeHead = original;
76
+ };
77
+ }
78
+ /** Attaches one-shot listeners; returns an undo that removes all of them. */
79
+ function listenOnce(target, listeners) {
80
+ for (const [event, listener] of listeners)
81
+ target.once(event, listener);
82
+ return () => {
83
+ for (const [event, listener] of listeners) {
84
+ target.removeListener(event, listener);
85
+ }
86
+ };
87
+ }
88
+ function observeWritableHeaders(message, span, direction, directHeaders) {
89
+ const target = message;
90
+ const applyHeaders = () => {
91
+ (0, collector_js_1.setHeaderAttributes)(span, direction, outgoingHeaders(message, directHeaders));
92
+ };
93
+ const restoreEnd = swapMethod(target, "end", (originalEnd) => function (...args) {
94
+ const result = Reflect.apply(originalEnd, this, args);
95
+ (0, utils_js_1.safely)(applyHeaders);
96
+ return result;
97
+ });
98
+ const restoreWriteHead = swapWriteHead(target, directHeaders, applyHeaders);
99
+ let removeListeners = () => { };
100
+ const cleanup = () => {
101
+ restoreEnd();
102
+ restoreWriteHead();
103
+ removeListeners();
104
+ };
105
+ removeListeners = listenOnce(target, [
106
+ ["finish", cleanup],
107
+ ["close", cleanup],
108
+ ]);
109
+ }
110
+ function observeReadable(message, collector) {
111
+ const target = message;
112
+ const restorePush = swapMethod(target, "push", (originalPush) => function (...args) {
113
+ // The parser's delivery path runs first and its result is returned
114
+ // unconditionally; observation failures must never disturb it.
115
+ const result = Reflect.apply(originalPush, this, args);
116
+ (0, utils_js_1.safely)(() => {
117
+ if (args[0] !== null)
118
+ collector.observe(args[0], args[1]);
119
+ else
120
+ collector.finalize(true);
121
+ });
122
+ return result;
123
+ });
124
+ const onComplete = () => collector.finalize(true);
125
+ const onIncomplete = () => collector.finalize(false);
126
+ const removeListeners = listenOnce(target, [
127
+ ["end", onComplete],
128
+ ["aborted", onIncomplete],
129
+ ["close", onIncomplete],
130
+ [node_events_1.errorMonitor, onIncomplete],
131
+ ]);
132
+ collector.addCleanup(() => {
133
+ restorePush();
134
+ removeListeners();
135
+ });
136
+ }
137
+ function observeWritable(message, collector, directHeaders) {
138
+ const target = message;
139
+ // Prevents double-observing a final chunk that end() forwards to write() internally.
140
+ let ending = false;
141
+ const restoreWrite = swapMethod(target, "write", (originalWrite) => function (...args) {
142
+ const result = Reflect.apply(originalWrite, this, args);
143
+ if (!ending)
144
+ (0, utils_js_1.safely)(() => collector.observe(args[0], args[1]));
145
+ return result;
146
+ });
147
+ const restoreEnd = swapMethod(target, "end", (originalEnd) => function (...args) {
148
+ ending = true;
149
+ try {
150
+ const result = Reflect.apply(originalEnd, this, args);
151
+ (0, utils_js_1.safely)(() => collector.observe(args[0], args[1]));
152
+ return result;
153
+ }
154
+ finally {
155
+ ending = false;
156
+ }
157
+ });
158
+ const restoreWriteHead = swapWriteHead(target, directHeaders);
159
+ const onComplete = () => collector.finalize(true);
160
+ const onIncomplete = () => collector.finalize(false);
161
+ const removeListeners = listenOnce(target, [
162
+ ["finish", onComplete],
163
+ ["close", onIncomplete],
164
+ [node_events_1.errorMonitor, onIncomplete],
165
+ ]);
166
+ collector.addCleanup(() => {
167
+ restoreWrite();
168
+ restoreEnd();
169
+ restoreWriteHead();
170
+ removeListeners();
171
+ });
172
+ }
173
+ /**
174
+ * Instruments one HTTP message (either side, either direction) on a span:
175
+ * records headers as `http.<direction>.header.*` attributes and, when enabled,
176
+ * captures the body via a BodyCollector.
177
+ */
178
+ function hookMessage(span, message, direction, bodyCaptureEnabled) {
179
+ // Duck-type: only outgoing messages (ClientRequest/ServerResponse) have write/end.
180
+ if ("write" in message && "end" in message) {
181
+ // A writable request is an outgoing client call; a writable response is served by us.
182
+ const side = direction === "request" ? "client" : "server";
183
+ // Collects headers passed inline to writeHead(), which don't show up in getHeaders().
184
+ const directHeaders = new Map();
185
+ // Eagerly record whatever headers exist right now; late-set headers are handled below.
186
+ (0, collector_js_1.setHeaderAttributes)(span, direction, outgoingHeaders(message, directHeaders));
187
+ const collector = bodyCaptureEnabled
188
+ ? // Headers are passed as a lazy callback so late-set headers are seen at finalize time.
189
+ collector_js_1.BodyCollector.create(span, side, direction, () => outgoingHeaders(message, directHeaders))
190
+ : undefined;
191
+ // With a collector, patch write/end/writeHead to copy body chunks as the app sends them.
192
+ if (collector)
193
+ observeWritable(message, collector, directHeaders);
194
+ else if (span.isRecording()) {
195
+ // No body capture: still patch end/writeHead so late-set headers land on the span.
196
+ observeWritableHeaders(message, span, direction, directHeaders);
197
+ }
198
+ }
199
+ else {
200
+ // Inverse of the writable case: a request arriving at us means we are the server.
201
+ const side = direction === "request" ? "server" : "client";
202
+ // Incoming message: headers are fully parsed by Node before the hook runs, so snapshot once.
203
+ const headers = incomingHeaders(message);
204
+ (0, collector_js_1.setHeaderAttributes)(span, direction, headers);
205
+ const collector = bodyCaptureEnabled
206
+ ? collector_js_1.BodyCollector.create(span, side, direction, () => headers)
207
+ : undefined;
208
+ // Patch the stream's push() to copy body chunks as they arrive from the socket.
209
+ if (collector)
210
+ observeReadable(message, collector);
211
+ }
212
+ }
213
+ /** Builds OTel HTTP request/response hooks that record headers as span attributes and stream bodies as log events. */
214
+ function createNetworkCaptureHooks(bodyCaptureEnabled = true) {
215
+ return {
216
+ requestHook(span, request) {
217
+ hookMessage(span, request, "request", bodyCaptureEnabled);
218
+ },
219
+ responseHook(span, response) {
220
+ hookMessage(span, response, "response", bodyCaptureEnabled);
221
+ },
222
+ };
223
+ }
@@ -0,0 +1,2 @@
1
+ export { createNetworkCaptureHooks } from "./http.js";
2
+ export { createUndiciNetworkCaptureHooks } from "./undici.js";
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createUndiciNetworkCaptureHooks = exports.createNetworkCaptureHooks = void 0;
4
+ var http_js_1 = require("./http.js");
5
+ Object.defineProperty(exports, "createNetworkCaptureHooks", { enumerable: true, get: function () { return http_js_1.createNetworkCaptureHooks; } });
6
+ var undici_js_1 = require("./undici.js");
7
+ Object.defineProperty(exports, "createUndiciNetworkCaptureHooks", { enumerable: true, get: function () { return undici_js_1.createUndiciNetworkCaptureHooks; } });
@@ -0,0 +1,9 @@
1
+ import type { RequestHookFunction, ResponseHookFunction } from "@opentelemetry/instrumentation-undici";
2
+ /**
3
+ * Builds OTel Undici/fetch hooks. Headers use the same allowlist as Node HTTP.
4
+ * Bodies are copied from diagnostics_channel chunks — never from fetch streams.
5
+ */
6
+ export declare function createUndiciNetworkCaptureHooks(bodyCaptureEnabled?: boolean): {
7
+ requestHook: RequestHookFunction;
8
+ responseHook: ResponseHookFunction;
9
+ };