@foam-ai/node 0.1.0-alpha.1 → 0.1.0-alpha.11
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 +656 -0
- package/dist/before-send.d.ts +27 -0
- package/dist/before-send.js +46 -0
- package/dist/constants.d.ts +44 -0
- package/dist/constants.js +67 -0
- package/dist/endpoint.d.ts +2 -0
- package/dist/endpoint.js +11 -0
- package/dist/exporters.d.ts +34 -0
- package/dist/exporters.js +208 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +28 -0
- package/dist/ingest.d.ts +12 -0
- package/dist/ingest.js +70 -0
- package/dist/init.d.ts +26 -0
- package/dist/init.js +171 -0
- package/dist/instrumentations.d.ts +4 -0
- package/dist/instrumentations.js +120 -0
- package/dist/library-versions.d.ts +1 -0
- package/dist/library-versions.js +59 -0
- package/dist/logs.d.ts +10 -0
- package/dist/logs.js +61 -0
- package/dist/metrics.d.ts +5 -0
- package/dist/metrics.js +29 -0
- package/dist/network-capture/collector.d.ts +27 -0
- package/dist/network-capture/collector.js +411 -0
- package/dist/network-capture/http.d.ts +5 -0
- package/dist/network-capture/http.js +221 -0
- package/dist/network-capture/index.d.ts +3 -0
- package/dist/network-capture/index.js +20 -0
- package/dist/network-capture/redact.d.ts +9 -0
- package/dist/network-capture/redact.js +401 -0
- package/dist/network-capture/support.d.ts +2 -0
- package/dist/network-capture/support.js +38 -0
- package/dist/network-capture/undici.d.ts +5 -0
- package/dist/network-capture/undici.js +177 -0
- package/dist/otlp.d.ts +8 -0
- package/dist/otlp.js +46 -0
- package/dist/propagation.d.ts +5 -0
- package/dist/propagation.js +47 -0
- package/dist/redaction-keys.d.ts +4 -0
- package/dist/redaction-keys.js +480 -0
- package/dist/redaction.d.ts +24 -0
- package/dist/redaction.js +281 -0
- package/dist/report.d.ts +9 -0
- package/dist/report.js +56 -0
- package/dist/resource.d.ts +3 -0
- package/dist/resource.js +24 -0
- package/dist/state.d.ts +26 -0
- package/dist/state.js +61 -0
- package/dist/traces.d.ts +1 -0
- package/dist/traces.js +21 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +20 -0
- package/package.json +47 -19
- package/dist/node/src/capture-exception.d.ts +0 -9
- package/dist/node/src/capture-exception.js +0 -31
- package/dist/node/src/index.d.ts +0 -9
- package/dist/node/src/index.js +0 -12
- package/dist/node/src/init.d.ts +0 -27
- package/dist/node/src/init.js +0 -203
- package/dist/shared/constants.d.ts +0 -1
- package/dist/shared/constants.js +0 -4
- package/dist/shared/util.d.ts +0 -9
- package/dist/shared/util.js +0 -21
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.BodyCollector = void 0;
|
|
37
|
+
exports.setHeaderAttributes = setHeaderAttributes;
|
|
38
|
+
const api_1 = require("@opentelemetry/api");
|
|
39
|
+
const api_logs_1 = require("@opentelemetry/api-logs");
|
|
40
|
+
const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
|
|
41
|
+
const node_crypto_1 = require("node:crypto");
|
|
42
|
+
const zlib = __importStar(require("node:zlib"));
|
|
43
|
+
const constants_js_1 = require("../constants.js");
|
|
44
|
+
const utils_js_1 = require("../utils.js");
|
|
45
|
+
const redact_js_1 = require("./redact.js");
|
|
46
|
+
const ALLOWED_HEADERS = new Set(constants_js_1.SAFE_NETWORK_HEADERS);
|
|
47
|
+
const ATTR = {
|
|
48
|
+
request: {
|
|
49
|
+
header: semantic_conventions_1.ATTR_HTTP_REQUEST_HEADER,
|
|
50
|
+
size: constants_js_1.ATTR_HTTP_REQUEST_BODY_SIZE,
|
|
51
|
+
content: constants_js_1.ATTR_HTTP_REQUEST_BODY_CONTENT,
|
|
52
|
+
captureId: constants_js_1.ATTR_FOAM_HTTP_REQUEST_BODY_CAPTURE_ID,
|
|
53
|
+
complete: constants_js_1.ATTR_FOAM_HTTP_REQUEST_BODY_COMPLETE,
|
|
54
|
+
truncated: constants_js_1.ATTR_FOAM_HTTP_REQUEST_BODY_TRUNCATED,
|
|
55
|
+
},
|
|
56
|
+
response: {
|
|
57
|
+
header: semantic_conventions_1.ATTR_HTTP_RESPONSE_HEADER,
|
|
58
|
+
size: constants_js_1.ATTR_HTTP_RESPONSE_BODY_SIZE,
|
|
59
|
+
content: constants_js_1.ATTR_HTTP_RESPONSE_BODY_CONTENT,
|
|
60
|
+
captureId: constants_js_1.ATTR_FOAM_HTTP_RESPONSE_BODY_CAPTURE_ID,
|
|
61
|
+
complete: constants_js_1.ATTR_FOAM_HTTP_RESPONSE_BODY_COMPLETE,
|
|
62
|
+
truncated: constants_js_1.ATTR_FOAM_HTTP_RESPONSE_BODY_TRUNCATED,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
let activeCaptures = 0;
|
|
66
|
+
function captureLogger() {
|
|
67
|
+
return api_logs_1.logs.getLogger(constants_js_1.FOAM_DISTRO_NAME, constants_js_1.FOAM_DISTRO_VERSION);
|
|
68
|
+
}
|
|
69
|
+
function headerValues(value) {
|
|
70
|
+
return Array.isArray(value) ? value.map(String) : [String(value)];
|
|
71
|
+
}
|
|
72
|
+
function setHeaderAttributes(span, direction, headers) {
|
|
73
|
+
const attributes = {};
|
|
74
|
+
for (const [rawName, rawValue] of Object.entries(headers)) {
|
|
75
|
+
if (rawValue === undefined)
|
|
76
|
+
continue;
|
|
77
|
+
const name = rawName.toLowerCase();
|
|
78
|
+
if (!ALLOWED_HEADERS.has(name))
|
|
79
|
+
continue;
|
|
80
|
+
attributes[ATTR[direction].header(name)] = headerValues(rawValue);
|
|
81
|
+
}
|
|
82
|
+
if (Object.keys(attributes).length > 0)
|
|
83
|
+
span.setAttributes(attributes);
|
|
84
|
+
}
|
|
85
|
+
function stringEncoding(encoding) {
|
|
86
|
+
return typeof encoding === "string" && Buffer.isEncoding(encoding)
|
|
87
|
+
? encoding
|
|
88
|
+
: "utf8";
|
|
89
|
+
}
|
|
90
|
+
function copyBody(value, encoding) {
|
|
91
|
+
if (typeof value === "string") {
|
|
92
|
+
return Buffer.from(value, stringEncoding(encoding));
|
|
93
|
+
}
|
|
94
|
+
if (Buffer.isBuffer(value))
|
|
95
|
+
return Buffer.from(value);
|
|
96
|
+
if (ArrayBuffer.isView(value)) {
|
|
97
|
+
return Buffer.from(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
// Byte length of a body chunk without copying it; undefined for non-body values.
|
|
102
|
+
function bodyByteLength(value, encoding) {
|
|
103
|
+
if (typeof value === "string") {
|
|
104
|
+
return Buffer.byteLength(value, stringEncoding(encoding));
|
|
105
|
+
}
|
|
106
|
+
if (Buffer.isBuffer(value) || ArrayBuffer.isView(value)) {
|
|
107
|
+
return value.byteLength;
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
function decodeBody(bytes, contentEncoding) {
|
|
112
|
+
if (contentEncoding === undefined)
|
|
113
|
+
return bytes;
|
|
114
|
+
const encodings = headerValues(contentEncoding)
|
|
115
|
+
.flatMap((value) => value.split(","))
|
|
116
|
+
.map((value) => value.trim().toLowerCase())
|
|
117
|
+
.filter((value) => value && value !== "identity");
|
|
118
|
+
if (encodings.length === 0)
|
|
119
|
+
return bytes;
|
|
120
|
+
const limits = { maxOutputLength: constants_js_1.NETWORK_BODY_MAX_DECODE_BYTES };
|
|
121
|
+
let decoded = bytes;
|
|
122
|
+
try {
|
|
123
|
+
for (const encoding of encodings.reverse()) {
|
|
124
|
+
if (encoding === "gzip" || encoding === "x-gzip") {
|
|
125
|
+
decoded = zlib.gunzipSync(decoded, limits);
|
|
126
|
+
}
|
|
127
|
+
else if (encoding === "deflate") {
|
|
128
|
+
try {
|
|
129
|
+
decoded = zlib.inflateSync(decoded, limits);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
decoded = zlib.unzipSync(decoded, limits);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (encoding === "br") {
|
|
136
|
+
decoded = zlib.brotliDecompressSync(decoded, limits);
|
|
137
|
+
}
|
|
138
|
+
else if (encoding === "zstd") {
|
|
139
|
+
const decompress = zlib.zstdDecompressSync;
|
|
140
|
+
if (!decompress)
|
|
141
|
+
return undefined;
|
|
142
|
+
decoded = decompress(decoded, limits);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return decoded;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function textEncoding(contentType) {
|
|
155
|
+
const raw = headerValues(contentType)[0];
|
|
156
|
+
if (raw === undefined)
|
|
157
|
+
return undefined;
|
|
158
|
+
const parts = raw.split(";").map((part) => part.trim());
|
|
159
|
+
const media = (parts[0] ?? "").toLowerCase();
|
|
160
|
+
const charset = parts
|
|
161
|
+
.find((part) => part.toLowerCase().startsWith("charset="))
|
|
162
|
+
?.slice("charset=".length)
|
|
163
|
+
.trim()
|
|
164
|
+
.replace(/^["']|["']$/g, "")
|
|
165
|
+
.toLowerCase();
|
|
166
|
+
const isText = (0, redact_js_1.isTextualBodyMediaType)(media) || Boolean(charset);
|
|
167
|
+
if (!isText)
|
|
168
|
+
return undefined;
|
|
169
|
+
if (charset && Buffer.isEncoding(charset))
|
|
170
|
+
return charset;
|
|
171
|
+
if (charset === "iso-8859-1" || charset === "windows-1252")
|
|
172
|
+
return "latin1";
|
|
173
|
+
if (media.startsWith("multipart/"))
|
|
174
|
+
return "latin1";
|
|
175
|
+
return "utf8";
|
|
176
|
+
}
|
|
177
|
+
// Last index such that `buf.subarray(0, end)` is complete UTF-8, capped at `max`.
|
|
178
|
+
function utf8End(buf, max) {
|
|
179
|
+
const end = Math.min(max, buf.byteLength);
|
|
180
|
+
if (end === 0)
|
|
181
|
+
return 0;
|
|
182
|
+
let leadAt = end;
|
|
183
|
+
while (leadAt > 0 && (buf[leadAt - 1] & 0xc0) === 0x80)
|
|
184
|
+
leadAt--;
|
|
185
|
+
if (leadAt === 0)
|
|
186
|
+
return 0;
|
|
187
|
+
const lead = buf[leadAt - 1];
|
|
188
|
+
const needed = (lead & 0x80) === 0
|
|
189
|
+
? 1
|
|
190
|
+
: (lead & 0xe0) === 0xc0
|
|
191
|
+
? 2
|
|
192
|
+
: (lead & 0xf0) === 0xe0
|
|
193
|
+
? 3
|
|
194
|
+
: (lead & 0xf8) === 0xf0
|
|
195
|
+
? 4
|
|
196
|
+
: Number.POSITIVE_INFINITY;
|
|
197
|
+
return end - (leadAt - 1) >= needed ? end : leadAt - 1;
|
|
198
|
+
}
|
|
199
|
+
function cappedBody(buf) {
|
|
200
|
+
if (buf.byteLength <= constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES)
|
|
201
|
+
return buf;
|
|
202
|
+
return buf.subarray(0, constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES);
|
|
203
|
+
}
|
|
204
|
+
function decodeText(decoded, encoding) {
|
|
205
|
+
const limited = cappedBody(decoded);
|
|
206
|
+
if (encoding === "utf8" || encoding === "utf-8") {
|
|
207
|
+
return limited.subarray(0, utf8End(limited, limited.byteLength)).toString(encoding);
|
|
208
|
+
}
|
|
209
|
+
return limited.toString(encoding);
|
|
210
|
+
}
|
|
211
|
+
class BodyCollector {
|
|
212
|
+
span;
|
|
213
|
+
context;
|
|
214
|
+
side;
|
|
215
|
+
direction;
|
|
216
|
+
headers;
|
|
217
|
+
id = (0, node_crypto_1.randomUUID)();
|
|
218
|
+
attr;
|
|
219
|
+
chunks = [];
|
|
220
|
+
cleanups = [];
|
|
221
|
+
deadline;
|
|
222
|
+
observedBytes = 0;
|
|
223
|
+
capturedBytes = 0;
|
|
224
|
+
truncated = false;
|
|
225
|
+
finalized = false;
|
|
226
|
+
constructor(span, context, side, direction, headers) {
|
|
227
|
+
this.span = span;
|
|
228
|
+
this.context = context;
|
|
229
|
+
this.side = side;
|
|
230
|
+
this.direction = direction;
|
|
231
|
+
this.headers = headers;
|
|
232
|
+
this.attr = ATTR[direction];
|
|
233
|
+
activeCaptures += 1;
|
|
234
|
+
this.deadline = setTimeout(() => this.finalize(false), constants_js_1.NETWORK_CAPTURE_DEADLINE_MS);
|
|
235
|
+
this.deadline.unref();
|
|
236
|
+
this.span.setAttribute(this.attr.captureId, this.id);
|
|
237
|
+
}
|
|
238
|
+
static create(span, side, direction, headers) {
|
|
239
|
+
const context = api_1.trace.setSpanContext(api_1.ROOT_CONTEXT, span.spanContext());
|
|
240
|
+
if (activeCaptures >= constants_js_1.MAX_ACTIVE_CAPTURES ||
|
|
241
|
+
!span.isRecording() ||
|
|
242
|
+
(span.spanContext().traceFlags & api_1.TraceFlags.SAMPLED) !==
|
|
243
|
+
api_1.TraceFlags.SAMPLED ||
|
|
244
|
+
!captureLogger().enabled({
|
|
245
|
+
context,
|
|
246
|
+
eventName: constants_js_1.NETWORK_CAPTURE_EVENT,
|
|
247
|
+
severityNumber: api_logs_1.SeverityNumber.INFO,
|
|
248
|
+
})) {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
return new BodyCollector(span, context, side, direction, headers);
|
|
252
|
+
}
|
|
253
|
+
addCleanup(cleanup) {
|
|
254
|
+
this.cleanups.push(cleanup);
|
|
255
|
+
}
|
|
256
|
+
observe(value, encoding) {
|
|
257
|
+
if (this.finalized)
|
|
258
|
+
return;
|
|
259
|
+
if (this.capturedBytes >= constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES) {
|
|
260
|
+
// Past the cap every chunk is discarded; only its length is needed
|
|
261
|
+
// (for the observed size), so skip the per-chunk copy that would
|
|
262
|
+
// otherwise double the allocation traffic of large transfers.
|
|
263
|
+
const length = bodyByteLength(value, encoding);
|
|
264
|
+
if (length === undefined)
|
|
265
|
+
return;
|
|
266
|
+
this.deadline.refresh();
|
|
267
|
+
this.observedBytes += length;
|
|
268
|
+
this.truncated = true;
|
|
269
|
+
this.span.setAttributes({
|
|
270
|
+
[this.attr.size]: this.observedBytes,
|
|
271
|
+
[this.attr.truncated]: true,
|
|
272
|
+
});
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const bytes = copyBody(value, encoding);
|
|
276
|
+
if (!bytes)
|
|
277
|
+
return;
|
|
278
|
+
this.deadline.refresh();
|
|
279
|
+
this.observedBytes += bytes.byteLength;
|
|
280
|
+
this.span.setAttribute(this.attr.size, this.observedBytes);
|
|
281
|
+
const room = constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES - this.capturedBytes;
|
|
282
|
+
const kept = bytes.byteLength > room ? bytes.subarray(0, room) : bytes;
|
|
283
|
+
this.chunks.push(kept);
|
|
284
|
+
this.capturedBytes += kept.byteLength;
|
|
285
|
+
if (kept.byteLength < bytes.byteLength) {
|
|
286
|
+
this.truncated = true;
|
|
287
|
+
this.span.setAttribute(this.attr.truncated, true);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
finalize(complete) {
|
|
291
|
+
if (this.finalized)
|
|
292
|
+
return;
|
|
293
|
+
this.finalized = true;
|
|
294
|
+
clearTimeout(this.deadline);
|
|
295
|
+
activeCaptures -= 1;
|
|
296
|
+
for (const cleanup of this.cleanups.splice(0))
|
|
297
|
+
(0, utils_js_1.safely)(cleanup);
|
|
298
|
+
// Runs inside stream handlers; header snapshots may throw on destroyed messages.
|
|
299
|
+
(0, utils_js_1.safely)(() => {
|
|
300
|
+
const headers = this.headers();
|
|
301
|
+
setHeaderAttributes(this.span, this.direction, headers);
|
|
302
|
+
this.span.setAttributes({
|
|
303
|
+
[this.attr.complete]: complete,
|
|
304
|
+
[this.attr.truncated]: this.truncated,
|
|
305
|
+
[this.attr.size]: this.observedBytes,
|
|
306
|
+
});
|
|
307
|
+
this.emit(this.chunks.splice(0), headers, complete);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
emit(buffers, headers, complete) {
|
|
311
|
+
let outcome;
|
|
312
|
+
if (this.observedBytes === 0)
|
|
313
|
+
outcome = "empty";
|
|
314
|
+
else if (complete)
|
|
315
|
+
outcome = "captured";
|
|
316
|
+
else
|
|
317
|
+
outcome = "incomplete";
|
|
318
|
+
if (outcome === "empty")
|
|
319
|
+
return;
|
|
320
|
+
let wire;
|
|
321
|
+
if (buffers.length === 0)
|
|
322
|
+
wire = undefined;
|
|
323
|
+
else if (buffers.length === 1)
|
|
324
|
+
wire = buffers[0];
|
|
325
|
+
else
|
|
326
|
+
wire = Buffer.concat(buffers);
|
|
327
|
+
// Span attributes cannot hold bytes (OTel AttributeValue). Text goes on
|
|
328
|
+
// the span; binary uses the same `http.*.body.content` name on the event.
|
|
329
|
+
let binaryContent;
|
|
330
|
+
let chunkSource = wire;
|
|
331
|
+
let chunkSourceIsDecodedText = false;
|
|
332
|
+
if (wire !== undefined) {
|
|
333
|
+
const decoded = decodeBody(wire, headers["content-encoding"]);
|
|
334
|
+
if (decoded === undefined) {
|
|
335
|
+
binaryContent = Uint8Array.from(cappedBody(wire));
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
const encoding = textEncoding(headers["content-type"]);
|
|
339
|
+
if (encoding !== undefined) {
|
|
340
|
+
const redacted = (0, redact_js_1.redactBodyText)(decodeText(decoded, encoding), headers["content-type"]);
|
|
341
|
+
this.span.setAttribute(this.attr.content, redacted.text);
|
|
342
|
+
if (redacted.changed) {
|
|
343
|
+
// pcga11: Chunk events must not re-ship bytes the span content
|
|
344
|
+
// redacted, so the (possibly compressed) wire bytes are replaced
|
|
345
|
+
// with the redacted text.
|
|
346
|
+
chunkSource = Buffer.from(redacted.text, encoding);
|
|
347
|
+
chunkSourceIsDecodedText = true;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
binaryContent = Uint8Array.from(cappedBody(decoded));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const chunks = [];
|
|
356
|
+
if (chunkSource) {
|
|
357
|
+
for (let i = 0; i < chunkSource.byteLength; i += constants_js_1.NETWORK_BODY_CHUNK_BYTES) {
|
|
358
|
+
chunks.push(chunkSource.subarray(i, i + constants_js_1.NETWORK_BODY_CHUNK_BYTES));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const common = {
|
|
362
|
+
[constants_js_1.ATTR_FOAM_HTTP_BODY_ID]: this.id,
|
|
363
|
+
[constants_js_1.ATTR_FOAM_HTTP_BODY_SIDE]: this.side,
|
|
364
|
+
[constants_js_1.ATTR_FOAM_HTTP_BODY_DIRECTION]: this.direction,
|
|
365
|
+
[constants_js_1.ATTR_FOAM_HTTP_BODY_COMPLETE]: complete,
|
|
366
|
+
[constants_js_1.ATTR_FOAM_HTTP_BODY_TRUNCATED]: this.truncated,
|
|
367
|
+
[constants_js_1.ATTR_FOAM_HTTP_BODY_OUTCOME]: outcome,
|
|
368
|
+
[this.attr.size]: this.observedBytes,
|
|
369
|
+
[constants_js_1.ATTR_FOAM_HTTP_BODY_CAPTURED_BYTES]: chunkSourceIsDecodedText
|
|
370
|
+
? (chunkSource?.byteLength ?? 0)
|
|
371
|
+
: this.capturedBytes,
|
|
372
|
+
[constants_js_1.ATTR_FOAM_HTTP_BODY_CHUNK_COUNT]: chunks.length,
|
|
373
|
+
};
|
|
374
|
+
for (const name of ["content-type", "content-encoding"]) {
|
|
375
|
+
if (name === "content-encoding" &&
|
|
376
|
+
chunkSourceIsDecodedText &&
|
|
377
|
+
headers[name] !== undefined) {
|
|
378
|
+
common[this.attr.header(name)] = ["identity"];
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (headers[name] !== undefined) {
|
|
382
|
+
common[this.attr.header(name)] = headerValues(headers[name]);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const logger = captureLogger();
|
|
386
|
+
const records = chunks.length === 0 ? [undefined] : chunks;
|
|
387
|
+
records.forEach((chunk, index) => {
|
|
388
|
+
logger.emit({
|
|
389
|
+
eventName: constants_js_1.NETWORK_CAPTURE_EVENT,
|
|
390
|
+
body: constants_js_1.NETWORK_CAPTURE_EVENT_BODY,
|
|
391
|
+
severityNumber: api_logs_1.SeverityNumber.INFO,
|
|
392
|
+
severityText: constants_js_1.SEVERITY_TEXT[api_logs_1.SeverityNumber.INFO],
|
|
393
|
+
attributes: {
|
|
394
|
+
...common,
|
|
395
|
+
...(chunk
|
|
396
|
+
? {
|
|
397
|
+
[constants_js_1.ATTR_FOAM_HTTP_BODY_CHUNK_INDEX]: index,
|
|
398
|
+
[constants_js_1.ATTR_FOAM_HTTP_BODY_CHUNK_BYTES]: chunk.byteLength,
|
|
399
|
+
[constants_js_1.ATTR_FOAM_HTTP_BODY_CHUNK_CONTENT]: Uint8Array.from(chunk),
|
|
400
|
+
}
|
|
401
|
+
: {}),
|
|
402
|
+
...(index === 0 && binaryContent
|
|
403
|
+
? { [this.attr.content]: binaryContent }
|
|
404
|
+
: {}),
|
|
405
|
+
},
|
|
406
|
+
context: this.context,
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
exports.BodyCollector = BodyCollector;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { HttpRequestCustomAttributeFunction, HttpResponseCustomAttributeFunction } from "@opentelemetry/instrumentation-http";
|
|
2
|
+
export declare function createNetworkCaptureHooks(bodyCaptureEnabled?: boolean): {
|
|
3
|
+
requestHook: HttpRequestCustomAttributeFunction;
|
|
4
|
+
responseHook: HttpResponseCustomAttributeFunction;
|
|
5
|
+
};
|
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
// pcga11: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
|
+
// Runs inside the customer's EventEmitter emit; a throw here would surface
|
|
101
|
+
// as an uncaughtException in their app.
|
|
102
|
+
const cleanup = () => (0, utils_js_1.safely)(() => {
|
|
103
|
+
restoreEnd();
|
|
104
|
+
restoreWriteHead();
|
|
105
|
+
removeListeners();
|
|
106
|
+
});
|
|
107
|
+
removeListeners = listenOnce(target, [
|
|
108
|
+
["finish", cleanup],
|
|
109
|
+
["close", cleanup],
|
|
110
|
+
]);
|
|
111
|
+
}
|
|
112
|
+
function observeReadable(message, collector) {
|
|
113
|
+
const target = message;
|
|
114
|
+
const restorePush = swapMethod(target, "push", (originalPush) => function (...args) {
|
|
115
|
+
// The parser's delivery path runs first and its result is returned
|
|
116
|
+
// unconditionally; observation failures must never disturb it.
|
|
117
|
+
const result = Reflect.apply(originalPush, this, args);
|
|
118
|
+
(0, utils_js_1.safely)(() => {
|
|
119
|
+
if (args[0] !== null)
|
|
120
|
+
collector.observe(args[0], args[1]);
|
|
121
|
+
else
|
|
122
|
+
collector.finalize(true);
|
|
123
|
+
});
|
|
124
|
+
return result;
|
|
125
|
+
});
|
|
126
|
+
const onComplete = () => (0, utils_js_1.safely)(() => collector.finalize(true));
|
|
127
|
+
const onIncomplete = () => (0, utils_js_1.safely)(() => collector.finalize(false));
|
|
128
|
+
const removeListeners = listenOnce(target, [
|
|
129
|
+
["end", onComplete],
|
|
130
|
+
["aborted", onIncomplete],
|
|
131
|
+
["close", onIncomplete],
|
|
132
|
+
[node_events_1.errorMonitor, onIncomplete],
|
|
133
|
+
]);
|
|
134
|
+
collector.addCleanup(() => {
|
|
135
|
+
restorePush();
|
|
136
|
+
removeListeners();
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
function observeWritable(message, collector, directHeaders) {
|
|
140
|
+
const target = message;
|
|
141
|
+
// Prevents double-observing a final chunk that end() forwards to write() internally.
|
|
142
|
+
let ending = false;
|
|
143
|
+
const restoreWrite = swapMethod(target, "write", (originalWrite) => function (...args) {
|
|
144
|
+
const result = Reflect.apply(originalWrite, this, args);
|
|
145
|
+
if (!ending)
|
|
146
|
+
(0, utils_js_1.safely)(() => collector.observe(args[0], args[1]));
|
|
147
|
+
return result;
|
|
148
|
+
});
|
|
149
|
+
const restoreEnd = swapMethod(target, "end", (originalEnd) => function (...args) {
|
|
150
|
+
ending = true;
|
|
151
|
+
try {
|
|
152
|
+
const result = Reflect.apply(originalEnd, this, args);
|
|
153
|
+
(0, utils_js_1.safely)(() => collector.observe(args[0], args[1]));
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
ending = false;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
const restoreWriteHead = swapWriteHead(target, directHeaders);
|
|
161
|
+
const onComplete = () => (0, utils_js_1.safely)(() => collector.finalize(true));
|
|
162
|
+
const onIncomplete = () => (0, utils_js_1.safely)(() => collector.finalize(false));
|
|
163
|
+
const removeListeners = listenOnce(target, [
|
|
164
|
+
["finish", onComplete],
|
|
165
|
+
["close", onIncomplete],
|
|
166
|
+
[node_events_1.errorMonitor, onIncomplete],
|
|
167
|
+
]);
|
|
168
|
+
collector.addCleanup(() => {
|
|
169
|
+
restoreWrite();
|
|
170
|
+
restoreEnd();
|
|
171
|
+
restoreWriteHead();
|
|
172
|
+
removeListeners();
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
// pcga11: Instruments one HTTP message on a span:
|
|
176
|
+
// - records headers as `http.<direction>.header.*` attributes
|
|
177
|
+
// - captures the body via a BodyCollector when enabled.
|
|
178
|
+
function hookMessage(span, message, direction, bodyCaptureEnabled) {
|
|
179
|
+
// pcga11: Duck-type: only outgoing messages (ClientRequest/ServerResponse) have write/end.
|
|
180
|
+
if ("write" in message && "end" in message) {
|
|
181
|
+
const side = direction === "request" ? "client" : "server";
|
|
182
|
+
const directHeaders = new Map();
|
|
183
|
+
(0, collector_js_1.setHeaderAttributes)(span, direction, outgoingHeaders(message, directHeaders));
|
|
184
|
+
const collector = bodyCaptureEnabled
|
|
185
|
+
? // Headers are passed as a lazy callback so late-set headers are seen at finalize time.
|
|
186
|
+
collector_js_1.BodyCollector.create(span, side, direction, () => outgoingHeaders(message, directHeaders))
|
|
187
|
+
: undefined;
|
|
188
|
+
// With a collector, patch write/end/writeHead to copy body chunks as the app sends them.
|
|
189
|
+
if (collector)
|
|
190
|
+
observeWritable(message, collector, directHeaders);
|
|
191
|
+
else if (span.isRecording()) {
|
|
192
|
+
observeWritableHeaders(message, span, direction, directHeaders);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
const side = direction === "request" ? "server" : "client";
|
|
197
|
+
const headers = incomingHeaders(message);
|
|
198
|
+
(0, collector_js_1.setHeaderAttributes)(span, direction, headers);
|
|
199
|
+
const collector = bodyCaptureEnabled
|
|
200
|
+
? collector_js_1.BodyCollector.create(span, side, direction, () => headers)
|
|
201
|
+
: undefined;
|
|
202
|
+
if (collector)
|
|
203
|
+
observeReadable(message, collector);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// pcga11: Creates network capture hooks for node:http / node:https. Wraps instance
|
|
207
|
+
// methods: push(), write(), and end() on IncomingMessage / ClientRequest /
|
|
208
|
+
// ServerResponse. This works for all Node versions Foam supports.
|
|
209
|
+
function createNetworkCaptureHooks(bodyCaptureEnabled = true) {
|
|
210
|
+
// OTel guards hook errors today, but Foam's no-crash guarantee must not
|
|
211
|
+
// depend on that: any failure here is swallowed and the request proceeds
|
|
212
|
+
// as if Foam were not installed.
|
|
213
|
+
return {
|
|
214
|
+
requestHook(span, request) {
|
|
215
|
+
(0, utils_js_1.safely)(() => hookMessage(span, request, "request", bodyCaptureEnabled));
|
|
216
|
+
},
|
|
217
|
+
responseHook(span, response) {
|
|
218
|
+
(0, utils_js_1.safely)(() => hookMessage(span, response, "response", bodyCaptureEnabled));
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isUndiciCaptureSupported = exports.isHttpCaptureSupported = exports.createUndiciNetworkCaptureHooks = exports.createNetworkCaptureHooks = void 0;
|
|
4
|
+
//
|
|
5
|
+
// Advanced body capture uses three adapters across Foam's Node range:
|
|
6
|
+
// 1. node:http / node:https — instance push(), write(), and end() wrapping
|
|
7
|
+
// (every supported Node version).
|
|
8
|
+
// 2. Undici legacy — request:create plus instance onBodySent / onData
|
|
9
|
+
// (bundled fetch on Node 18.19, 20, 22, and 24.0–24.3, whose Undici is
|
|
10
|
+
// older than 7.11).
|
|
11
|
+
// 3. Undici modern — bodyChunkSent / bodyChunkReceived channels (Undici
|
|
12
|
+
// 7.11+, including Node 24.4+ and a separately installed undici). Future
|
|
13
|
+
// Node is chosen by capability: modern channels win when they fire.
|
|
14
|
+
var http_js_1 = require("./http.js");
|
|
15
|
+
Object.defineProperty(exports, "createNetworkCaptureHooks", { enumerable: true, get: function () { return http_js_1.createNetworkCaptureHooks; } });
|
|
16
|
+
var undici_js_1 = require("./undici.js");
|
|
17
|
+
Object.defineProperty(exports, "createUndiciNetworkCaptureHooks", { enumerable: true, get: function () { return undici_js_1.createUndiciNetworkCaptureHooks; } });
|
|
18
|
+
var support_js_1 = require("./support.js");
|
|
19
|
+
Object.defineProperty(exports, "isHttpCaptureSupported", { enumerable: true, get: function () { return support_js_1.isHttpCaptureSupported; } });
|
|
20
|
+
Object.defineProperty(exports, "isUndiciCaptureSupported", { enumerable: true, get: function () { return support_js_1.isUndiciCaptureSupported; } });
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface RedactionResult {
|
|
2
|
+
readonly text: string;
|
|
3
|
+
readonly changed: boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare function isSensitiveBodyKey(key: string): boolean;
|
|
6
|
+
export declare function isNdjsonMediaType(media: string): boolean;
|
|
7
|
+
export declare function isJsonSequenceMediaType(media: string): boolean;
|
|
8
|
+
export declare function isTextualBodyMediaType(media: string): boolean;
|
|
9
|
+
export declare function redactBodyText(text: string, contentType: unknown): RedactionResult;
|