@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,401 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isSensitiveBodyKey = isSensitiveBodyKey;
|
|
4
|
+
exports.isNdjsonMediaType = isNdjsonMediaType;
|
|
5
|
+
exports.isJsonSequenceMediaType = isJsonSequenceMediaType;
|
|
6
|
+
exports.isTextualBodyMediaType = isTextualBodyMediaType;
|
|
7
|
+
exports.redactBodyText = redactBodyText;
|
|
8
|
+
const constants_js_1 = require("../constants.js");
|
|
9
|
+
const redaction_js_1 = require("../redaction.js");
|
|
10
|
+
const NDJSON_MEDIA_TYPES = new Set([
|
|
11
|
+
"application/json-stream",
|
|
12
|
+
"application/jsonl",
|
|
13
|
+
"application/jsonlines",
|
|
14
|
+
"application/ndjson",
|
|
15
|
+
"application/stream+json",
|
|
16
|
+
"application/x-json-stream",
|
|
17
|
+
"application/x-jsonlines",
|
|
18
|
+
"application/x-ldjson",
|
|
19
|
+
"application/x-ndjson",
|
|
20
|
+
]);
|
|
21
|
+
const JSON_SEQUENCE_MEDIA_TYPES = new Set([
|
|
22
|
+
"application/json-seq",
|
|
23
|
+
"application/x-json-seq",
|
|
24
|
+
]);
|
|
25
|
+
const XML_MEDIA_TYPES = new Set([
|
|
26
|
+
"application/xml",
|
|
27
|
+
"text/html",
|
|
28
|
+
"text/xml",
|
|
29
|
+
]);
|
|
30
|
+
const YAML_MEDIA_TYPES = new Set([
|
|
31
|
+
"application/x-yaml",
|
|
32
|
+
"application/yaml",
|
|
33
|
+
"text/x-yaml",
|
|
34
|
+
"text/yaml",
|
|
35
|
+
]);
|
|
36
|
+
function rawHeader(value) {
|
|
37
|
+
const first = Array.isArray(value) ? value[0] : value;
|
|
38
|
+
return typeof first === "string" ? first : "";
|
|
39
|
+
}
|
|
40
|
+
function parseContentType(value) {
|
|
41
|
+
const raw = rawHeader(value);
|
|
42
|
+
const media = (raw.split(";", 1)[0] ?? "").trim().toLowerCase();
|
|
43
|
+
const params = new Map();
|
|
44
|
+
const pattern = /;\s*([^=;\s]+)\s*=\s*(?:"([^"]*)"|([^;]*))/g;
|
|
45
|
+
let match;
|
|
46
|
+
while ((match = pattern.exec(raw)) !== null) {
|
|
47
|
+
params.set(match[1].toLowerCase(), (match[2] ?? match[3] ?? "").trim());
|
|
48
|
+
}
|
|
49
|
+
return { media, params };
|
|
50
|
+
}
|
|
51
|
+
function isSensitiveBodyKey(key) {
|
|
52
|
+
return (0, redaction_js_1.keyMatch)(key) !== undefined;
|
|
53
|
+
}
|
|
54
|
+
function isNdjsonMediaType(media) {
|
|
55
|
+
return (NDJSON_MEDIA_TYPES.has(media) ||
|
|
56
|
+
media.endsWith("+ndjson") ||
|
|
57
|
+
media.endsWith("/ndjson"));
|
|
58
|
+
}
|
|
59
|
+
function isJsonSequenceMediaType(media) {
|
|
60
|
+
return JSON_SEQUENCE_MEDIA_TYPES.has(media) || media.endsWith("+json-seq");
|
|
61
|
+
}
|
|
62
|
+
function isTextualBodyMediaType(media) {
|
|
63
|
+
return (media.startsWith("text/") ||
|
|
64
|
+
media.startsWith("multipart/") ||
|
|
65
|
+
media.endsWith("/json") ||
|
|
66
|
+
media.endsWith("+json") ||
|
|
67
|
+
media.endsWith("/xml") ||
|
|
68
|
+
media.endsWith("+xml") ||
|
|
69
|
+
media.endsWith("/yaml") ||
|
|
70
|
+
media.endsWith("+yaml") ||
|
|
71
|
+
isNdjsonMediaType(media) ||
|
|
72
|
+
isJsonSequenceMediaType(media) ||
|
|
73
|
+
media === "application/x-www-form-urlencoded");
|
|
74
|
+
}
|
|
75
|
+
function redactNode(node, state) {
|
|
76
|
+
if (typeof node === "string") {
|
|
77
|
+
const value = (0, redaction_js_1.redactString)(node);
|
|
78
|
+
if (value !== node)
|
|
79
|
+
state.changed = true;
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(node)) {
|
|
83
|
+
return node.map((item) => redactNode(item, state));
|
|
84
|
+
}
|
|
85
|
+
if (node !== null && typeof node === "object") {
|
|
86
|
+
const result = {};
|
|
87
|
+
for (const [key, value] of Object.entries(node)) {
|
|
88
|
+
const match = (0, redaction_js_1.keyMatch)(key);
|
|
89
|
+
if (match && value !== null && value !== undefined) {
|
|
90
|
+
result[key] = (0, redaction_js_1.maskForMatch)(match, value);
|
|
91
|
+
state.changed = true;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
result[key] = redactNode(value, state);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
return node;
|
|
100
|
+
}
|
|
101
|
+
const JSON_PAIR_PATTERN = /"((?:[^"\\]|\\.)*)"(\s*:\s*)("(?:[^"\\]|\\.)*"?|-?\d[\d.eE+-]*|true|false|null)/g;
|
|
102
|
+
function fallbackMask(kind) {
|
|
103
|
+
return kind === "secret" ? constants_js_1.FULL_MASK : constants_js_1.REDACTED_VALUE;
|
|
104
|
+
}
|
|
105
|
+
function redactJsonFallback(text) {
|
|
106
|
+
let changed = false;
|
|
107
|
+
const redacted = text.replace(JSON_PAIR_PATTERN, (match, key, separator) => {
|
|
108
|
+
const kind = (0, redaction_js_1.keyMatch)(key);
|
|
109
|
+
if (!kind)
|
|
110
|
+
return match;
|
|
111
|
+
changed = true;
|
|
112
|
+
return `"${key}"${separator}"${fallbackMask(kind)}"`;
|
|
113
|
+
});
|
|
114
|
+
return changed ? { text: redacted, changed } : { text, changed: false };
|
|
115
|
+
}
|
|
116
|
+
function redactJson(text) {
|
|
117
|
+
let parsed;
|
|
118
|
+
try {
|
|
119
|
+
parsed = JSON.parse(text);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return redactJsonFallback(text);
|
|
123
|
+
}
|
|
124
|
+
const state = { changed: false };
|
|
125
|
+
const redacted = redactNode(parsed, state);
|
|
126
|
+
return state.changed
|
|
127
|
+
? { text: JSON.stringify(redacted), changed: true }
|
|
128
|
+
: { text, changed: false };
|
|
129
|
+
}
|
|
130
|
+
function redactByLine(text, redactLine) {
|
|
131
|
+
let changed = false;
|
|
132
|
+
const lines = text.split("\n").map((raw) => {
|
|
133
|
+
const hasCr = raw.endsWith("\r");
|
|
134
|
+
const line = hasCr ? raw.slice(0, -1) : raw;
|
|
135
|
+
if (!line)
|
|
136
|
+
return raw;
|
|
137
|
+
const result = redactLine(line);
|
|
138
|
+
if (!result.changed)
|
|
139
|
+
return raw;
|
|
140
|
+
changed = true;
|
|
141
|
+
return hasCr ? `${result.text}\r` : result.text;
|
|
142
|
+
});
|
|
143
|
+
return changed
|
|
144
|
+
? { text: lines.join("\n"), changed: true }
|
|
145
|
+
: { text, changed: false };
|
|
146
|
+
}
|
|
147
|
+
function redactNdjson(text) {
|
|
148
|
+
return redactByLine(text, redactJson);
|
|
149
|
+
}
|
|
150
|
+
function redactJsonSequence(text) {
|
|
151
|
+
if (!text.includes("\u001e"))
|
|
152
|
+
return redactJson(text);
|
|
153
|
+
let changed = false;
|
|
154
|
+
const segments = text.split("\u001e");
|
|
155
|
+
const redacted = segments.map((segment, index) => {
|
|
156
|
+
if (index === 0 || !segment)
|
|
157
|
+
return segment;
|
|
158
|
+
const ending = segment.match(/(?:\r\n|\n|\r)$/)?.[0] ?? "";
|
|
159
|
+
const document = ending ? segment.slice(0, -ending.length) : segment;
|
|
160
|
+
const result = redactJson(document);
|
|
161
|
+
if (!result.changed)
|
|
162
|
+
return segment;
|
|
163
|
+
changed = true;
|
|
164
|
+
return `${result.text}${ending}`;
|
|
165
|
+
});
|
|
166
|
+
return changed
|
|
167
|
+
? { text: redacted.join("\u001e"), changed: true }
|
|
168
|
+
: { text, changed: false };
|
|
169
|
+
}
|
|
170
|
+
function redactSseBlock(block) {
|
|
171
|
+
const lineEnding = block.match(/\r\n|\n|\r/)?.[0] ?? "\n";
|
|
172
|
+
const lines = block.split(/\r\n|\n|\r/);
|
|
173
|
+
const data = [];
|
|
174
|
+
lines.forEach((line, index) => {
|
|
175
|
+
const match = /^(data: ?)(.*)$/.exec(line);
|
|
176
|
+
if (match) {
|
|
177
|
+
data.push({ index, prefix: match[1], value: match[2] });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
if (data.length === 0)
|
|
181
|
+
return { text: block, changed: false };
|
|
182
|
+
const payload = data.map((entry) => entry.value).join("\n");
|
|
183
|
+
const json = redactJson(payload);
|
|
184
|
+
const plain = json.changed ? json : redactPlainText(payload);
|
|
185
|
+
if (!plain.changed)
|
|
186
|
+
return { text: block, changed: false };
|
|
187
|
+
lines[data[0].index] = `${data[0].prefix}${plain.text}`;
|
|
188
|
+
for (let index = data.length - 1; index >= 1; index -= 1) {
|
|
189
|
+
lines.splice(data[index].index, 1);
|
|
190
|
+
}
|
|
191
|
+
return { text: lines.join(lineEnding), changed: true };
|
|
192
|
+
}
|
|
193
|
+
function redactSse(text) {
|
|
194
|
+
const separator = /(\r\n\r\n|\n\n|\r\r)/;
|
|
195
|
+
const parts = text.split(separator);
|
|
196
|
+
let changed = false;
|
|
197
|
+
for (let index = 0; index < parts.length; index += 2) {
|
|
198
|
+
const result = redactSseBlock(parts[index]);
|
|
199
|
+
if (result.changed) {
|
|
200
|
+
parts[index] = result.text;
|
|
201
|
+
changed = true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return changed
|
|
205
|
+
? { text: parts.join(""), changed: true }
|
|
206
|
+
: { text, changed: false };
|
|
207
|
+
}
|
|
208
|
+
function redactForm(text) {
|
|
209
|
+
const params = new URLSearchParams(text);
|
|
210
|
+
let changed = false;
|
|
211
|
+
for (const key of new Set(params.keys())) {
|
|
212
|
+
if (!(0, redaction_js_1.keyMatch)(key) && !(0, redaction_js_1.isSensitiveQueryKey)(key))
|
|
213
|
+
continue;
|
|
214
|
+
params.set(key, constants_js_1.REDACTED_VALUE);
|
|
215
|
+
changed = true;
|
|
216
|
+
}
|
|
217
|
+
return changed
|
|
218
|
+
? { text: params.toString(), changed: true }
|
|
219
|
+
: { text, changed: false };
|
|
220
|
+
}
|
|
221
|
+
function localName(name) {
|
|
222
|
+
const separator = name.lastIndexOf(":");
|
|
223
|
+
return separator < 0 ? name : name.slice(separator + 1);
|
|
224
|
+
}
|
|
225
|
+
function escapePattern(value) {
|
|
226
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
227
|
+
}
|
|
228
|
+
function redactXmlElements(text) {
|
|
229
|
+
let changed = false;
|
|
230
|
+
let cursor = 0;
|
|
231
|
+
let output = "";
|
|
232
|
+
const opening = /<([A-Za-z_][\w:.-]*)(?:\s[^>]*)?>/g;
|
|
233
|
+
let match;
|
|
234
|
+
while ((match = opening.exec(text)) !== null) {
|
|
235
|
+
if (match[0].endsWith("/>"))
|
|
236
|
+
continue;
|
|
237
|
+
const kind = (0, redaction_js_1.keyMatch)(localName(match[1]));
|
|
238
|
+
if (!kind)
|
|
239
|
+
continue;
|
|
240
|
+
const closing = new RegExp(`</${escapePattern(match[1])}\\s*>`, "g");
|
|
241
|
+
closing.lastIndex = opening.lastIndex;
|
|
242
|
+
const close = closing.exec(text);
|
|
243
|
+
output += text.slice(cursor, opening.lastIndex);
|
|
244
|
+
if (!close) {
|
|
245
|
+
output += fallbackMask(kind);
|
|
246
|
+
cursor = text.length;
|
|
247
|
+
changed = true;
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
const value = text.slice(opening.lastIndex, close.index);
|
|
251
|
+
output += `${value.includes("<") ? fallbackMask(kind) : (0, redaction_js_1.maskForMatch)(kind, value)}${close[0]}`;
|
|
252
|
+
cursor = closing.lastIndex;
|
|
253
|
+
opening.lastIndex = cursor;
|
|
254
|
+
changed = true;
|
|
255
|
+
}
|
|
256
|
+
if (!changed)
|
|
257
|
+
return { text, changed: false };
|
|
258
|
+
return { text: output + text.slice(cursor), changed: true };
|
|
259
|
+
}
|
|
260
|
+
function redactXml(text) {
|
|
261
|
+
const elements = redactXmlElements(text);
|
|
262
|
+
let changed = elements.changed;
|
|
263
|
+
let output = elements.text;
|
|
264
|
+
output = output.replace(/(\s)([A-Za-z_:][\w:.-]*)(\s*=\s*)(["'])(.*?)\4/g, (match, spacing, name, separator, quote, value) => {
|
|
265
|
+
const kind = (0, redaction_js_1.keyMatch)(localName(name));
|
|
266
|
+
if (!kind)
|
|
267
|
+
return match;
|
|
268
|
+
changed = true;
|
|
269
|
+
return `${spacing}${name}${separator}${quote}${(0, redaction_js_1.maskForMatch)(kind, value)}${quote}`;
|
|
270
|
+
});
|
|
271
|
+
output = output.replace(/<input\b[^>]*>/gi, (tag) => {
|
|
272
|
+
const name = /\bname\s*=\s*(["'])(.*?)\1/i.exec(tag)?.[2];
|
|
273
|
+
if (!name || (!(0, redaction_js_1.keyMatch)(name) && !(0, redaction_js_1.isSensitiveQueryKey)(name)))
|
|
274
|
+
return tag;
|
|
275
|
+
const replaced = tag.replace(/(\bvalue\s*=\s*)(["'])(.*?)\2/i, `$1$2${constants_js_1.REDACTED_VALUE}$2`);
|
|
276
|
+
if (replaced === tag)
|
|
277
|
+
return tag;
|
|
278
|
+
changed = true;
|
|
279
|
+
return replaced;
|
|
280
|
+
});
|
|
281
|
+
return changed ? { text: output, changed: true } : { text, changed: false };
|
|
282
|
+
}
|
|
283
|
+
const YAML_PAIR = /^(\s*(?:-\s*)?)(["']?)([A-Za-z_][\w.-]*)\2(\s*:\s*)(.*)$/;
|
|
284
|
+
function redactYaml(text) {
|
|
285
|
+
let blockSecret = false;
|
|
286
|
+
const result = redactByLine(text, (line) => {
|
|
287
|
+
const match = YAML_PAIR.exec(line);
|
|
288
|
+
if (!match)
|
|
289
|
+
return { text: line, changed: false };
|
|
290
|
+
const kind = (0, redaction_js_1.keyMatch)(match[3]);
|
|
291
|
+
if (!kind)
|
|
292
|
+
return { text: line, changed: false };
|
|
293
|
+
if (/^[>|]/.test(match[5].trim()))
|
|
294
|
+
blockSecret = true;
|
|
295
|
+
return {
|
|
296
|
+
text: `${match[1]}${match[2]}${match[3]}${match[2]}${match[4]}${(0, redaction_js_1.maskForMatch)(kind, match[5].trim().replace(/^(["'])(.*)\1$/, "$2"))}`,
|
|
297
|
+
changed: true,
|
|
298
|
+
};
|
|
299
|
+
});
|
|
300
|
+
return blockSecret ? { text: constants_js_1.REDACTED_VALUE, changed: true } : result;
|
|
301
|
+
}
|
|
302
|
+
function redactPlainText(text) {
|
|
303
|
+
const trimmed = text.trimStart();
|
|
304
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
305
|
+
const json = redactJson(text);
|
|
306
|
+
if (json.changed)
|
|
307
|
+
return json;
|
|
308
|
+
}
|
|
309
|
+
const yaml = redactYaml(text);
|
|
310
|
+
if (yaml.changed)
|
|
311
|
+
return yaml;
|
|
312
|
+
const redacted = (0, redaction_js_1.redactString)(text);
|
|
313
|
+
return redacted === text
|
|
314
|
+
? { text, changed: false }
|
|
315
|
+
: { text: redacted, changed: true };
|
|
316
|
+
}
|
|
317
|
+
function contentDispositionName(headers) {
|
|
318
|
+
const line = headers
|
|
319
|
+
.split(/\r\n|\n|\r/)
|
|
320
|
+
.find((header) => /^content-disposition\s*:/i.test(header));
|
|
321
|
+
return line ? /\bname\s*=\s*(?:"([^"]*)"|([^;\s]*))/i.exec(line)?.slice(1).find(Boolean) : undefined;
|
|
322
|
+
}
|
|
323
|
+
function partContentType(headers) {
|
|
324
|
+
return headers
|
|
325
|
+
.split(/\r\n|\n|\r/)
|
|
326
|
+
.find((header) => /^content-type\s*:/i.test(header))
|
|
327
|
+
?.replace(/^[^:]+:\s*/, "");
|
|
328
|
+
}
|
|
329
|
+
function redactMultipart(text, boundary) {
|
|
330
|
+
if (!boundary || boundary.length > 70)
|
|
331
|
+
return { text, changed: false };
|
|
332
|
+
const marker = `--${boundary}`;
|
|
333
|
+
if (!text.includes(marker))
|
|
334
|
+
return { text, changed: false };
|
|
335
|
+
let changed = false;
|
|
336
|
+
const parts = text.split(marker);
|
|
337
|
+
for (let index = 1; index < parts.length; index += 1) {
|
|
338
|
+
const part = parts[index];
|
|
339
|
+
if (part.startsWith("--"))
|
|
340
|
+
continue;
|
|
341
|
+
const leading = part.match(/^(?:\r\n|\n|\r)/)?.[0] ?? "";
|
|
342
|
+
const content = leading ? part.slice(leading.length) : part;
|
|
343
|
+
const separator = /\r\n\r\n|\n\n|\r\r/.exec(content);
|
|
344
|
+
if (!separator?.index)
|
|
345
|
+
continue;
|
|
346
|
+
const headers = content.slice(0, separator.index);
|
|
347
|
+
const bodyWithEnding = content.slice(separator.index + separator[0].length);
|
|
348
|
+
const ending = bodyWithEnding.match(/(?:\r\n|\n|\r)$/)?.[0] ?? "";
|
|
349
|
+
const body = ending
|
|
350
|
+
? bodyWithEnding.slice(0, -ending.length)
|
|
351
|
+
: bodyWithEnding;
|
|
352
|
+
const name = contentDispositionName(headers);
|
|
353
|
+
let result;
|
|
354
|
+
if (name && ((0, redaction_js_1.keyMatch)(name) || (0, redaction_js_1.isSensitiveQueryKey)(name))) {
|
|
355
|
+
result = { text: constants_js_1.REDACTED_VALUE, changed: true };
|
|
356
|
+
}
|
|
357
|
+
else if (/\bfilename\s*=/i.test(headers)) {
|
|
358
|
+
result = { text: body, changed: false };
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
result = redactBodyText(body, partContentType(headers) ?? "text/plain");
|
|
362
|
+
}
|
|
363
|
+
if (!result.changed)
|
|
364
|
+
continue;
|
|
365
|
+
parts[index] = `${leading}${headers}${separator[0]}${result.text}${ending}`;
|
|
366
|
+
changed = true;
|
|
367
|
+
}
|
|
368
|
+
return changed
|
|
369
|
+
? { text: parts.join(marker), changed: true }
|
|
370
|
+
: { text, changed: false };
|
|
371
|
+
}
|
|
372
|
+
function redactBodyText(text, contentType) {
|
|
373
|
+
const { media, params } = parseContentType(contentType);
|
|
374
|
+
if (media === "application/x-www-form-urlencoded")
|
|
375
|
+
return redactForm(text);
|
|
376
|
+
if (media === "text/event-stream")
|
|
377
|
+
return redactSse(text);
|
|
378
|
+
if (isJsonSequenceMediaType(media))
|
|
379
|
+
return redactJsonSequence(text);
|
|
380
|
+
if (isNdjsonMediaType(media))
|
|
381
|
+
return redactNdjson(text);
|
|
382
|
+
if (media.endsWith("/json") || media.endsWith("+json"))
|
|
383
|
+
return redactJson(text);
|
|
384
|
+
if (media.startsWith("multipart/")) {
|
|
385
|
+
return redactMultipart(text, params.get("boundary") ?? "");
|
|
386
|
+
}
|
|
387
|
+
if (XML_MEDIA_TYPES.has(media) ||
|
|
388
|
+
media.endsWith("/xml") ||
|
|
389
|
+
media.endsWith("+xml")) {
|
|
390
|
+
return redactXml(text);
|
|
391
|
+
}
|
|
392
|
+
if (YAML_MEDIA_TYPES.has(media) ||
|
|
393
|
+
media.endsWith("/yaml") ||
|
|
394
|
+
media.endsWith("+yaml")) {
|
|
395
|
+
return redactYaml(text);
|
|
396
|
+
}
|
|
397
|
+
if (media.startsWith("text/") || params.has("charset")) {
|
|
398
|
+
return redactPlainText(text);
|
|
399
|
+
}
|
|
400
|
+
return { text, changed: false };
|
|
401
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Foam only wires its network-capture hooks when the runtime is one the OTel
|
|
3
|
+
// instrumentation actually supports; otherwise the instrumentation would never
|
|
4
|
+
// invoke them, so patching objects or subscribing to diagnostics channels
|
|
5
|
+
// would be pointless. Floors come from the instrumentation packages:
|
|
6
|
+
//
|
|
7
|
+
// - @opentelemetry/instrumentation-http: engines "^18.19.0 || >=20.6.0"
|
|
8
|
+
// - @opentelemetry/instrumentation-undici: undici >= 5.12.0, the release that
|
|
9
|
+
// added the undici:request:* diagnostics channels it (and Foam) rely on.
|
|
10
|
+
//
|
|
11
|
+
// All floors end in .0, so comparing major.minor is enough.
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.isHttpCaptureSupported = isHttpCaptureSupported;
|
|
14
|
+
exports.isUndiciCaptureSupported = isUndiciCaptureSupported;
|
|
15
|
+
function parseVersion(raw) {
|
|
16
|
+
const match = raw === undefined ? null : /^v?(\d+)\.(\d+)\./.exec(raw);
|
|
17
|
+
if (!match)
|
|
18
|
+
return undefined;
|
|
19
|
+
return { major: Number(match[1]), minor: Number(match[2]) };
|
|
20
|
+
}
|
|
21
|
+
function isHttpCaptureSupported(nodeVersion = process.versions.node) {
|
|
22
|
+
const version = parseVersion(nodeVersion);
|
|
23
|
+
if (!version)
|
|
24
|
+
return false;
|
|
25
|
+
if (version.major === 18)
|
|
26
|
+
return version.minor >= 19;
|
|
27
|
+
return version.major > 20 || (version.major === 20 && version.minor >= 6);
|
|
28
|
+
}
|
|
29
|
+
// process.versions.undici reports the bundled undici (global fetch) and is
|
|
30
|
+
// present on every Node version Foam supports. It counts even when the app
|
|
31
|
+
// also installed an older undici copy: capture stays useful for fetch, and
|
|
32
|
+
// the old copy simply emits no channel events. Unknown means unsupported.
|
|
33
|
+
function isUndiciCaptureSupported(undiciVersion = process.versions.undici) {
|
|
34
|
+
const version = parseVersion(undiciVersion);
|
|
35
|
+
if (!version)
|
|
36
|
+
return false;
|
|
37
|
+
return version.major > 5 || (version.major === 5 && version.minor >= 12);
|
|
38
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RequestHookFunction, ResponseHookFunction } from "@opentelemetry/instrumentation-undici";
|
|
2
|
+
export declare function createUndiciNetworkCaptureHooks(bodyCaptureEnabled?: boolean): {
|
|
3
|
+
requestHook: RequestHookFunction;
|
|
4
|
+
responseHook: ResponseHookFunction;
|
|
5
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createUndiciNetworkCaptureHooks = createUndiciNetworkCaptureHooks;
|
|
4
|
+
const api_1 = require("@opentelemetry/api");
|
|
5
|
+
const node_diagnostics_channel_1 = require("node:diagnostics_channel");
|
|
6
|
+
const utils_js_1 = require("../utils.js");
|
|
7
|
+
const collector_js_1 = require("./collector.js");
|
|
8
|
+
const undiciSessions = new WeakMap();
|
|
9
|
+
const legacyWrappedRequests = new WeakSet();
|
|
10
|
+
let undiciBodyChannelsBound = false;
|
|
11
|
+
function appendHeader(headers, rawName, value) {
|
|
12
|
+
const name = String(rawName).toLowerCase();
|
|
13
|
+
const existing = headers[name];
|
|
14
|
+
if (existing === undefined)
|
|
15
|
+
headers[name] = value;
|
|
16
|
+
else if (Array.isArray(existing))
|
|
17
|
+
existing.push(value);
|
|
18
|
+
else
|
|
19
|
+
headers[name] = [existing, value];
|
|
20
|
+
}
|
|
21
|
+
function undiciRequestHeaders(value) {
|
|
22
|
+
const headers = {};
|
|
23
|
+
if (typeof value === "string") {
|
|
24
|
+
for (const line of value.split("\r\n")) {
|
|
25
|
+
const separator = line.indexOf(":");
|
|
26
|
+
if (separator <= 0)
|
|
27
|
+
continue;
|
|
28
|
+
appendHeader(headers, line.slice(0, separator).trim(), line.slice(separator + 1).trim());
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else if (Array.isArray(value)) {
|
|
32
|
+
for (let index = 0; index + 1 < value.length; index += 2) {
|
|
33
|
+
appendHeader(headers, value[index], value[index + 1]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return headers;
|
|
37
|
+
}
|
|
38
|
+
function undiciResponseHeaders(value) {
|
|
39
|
+
if (!Array.isArray(value))
|
|
40
|
+
return {};
|
|
41
|
+
const headers = {};
|
|
42
|
+
for (let index = 0; index + 1 < value.length; index += 2) {
|
|
43
|
+
const name = value[index];
|
|
44
|
+
const headerValue = value[index + 1];
|
|
45
|
+
appendHeader(headers, Buffer.isBuffer(name) ? name.toString("latin1") : name, Buffer.isBuffer(headerValue)
|
|
46
|
+
? headerValue.toString("latin1")
|
|
47
|
+
: headerValue);
|
|
48
|
+
}
|
|
49
|
+
return headers;
|
|
50
|
+
}
|
|
51
|
+
function undiciRequest(message) {
|
|
52
|
+
if (!message || typeof message !== "object" || !("request" in message)) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const request = message.request;
|
|
56
|
+
return request && typeof request === "object"
|
|
57
|
+
? request
|
|
58
|
+
: undefined;
|
|
59
|
+
}
|
|
60
|
+
function undiciCollector(session, direction) {
|
|
61
|
+
const field = direction === "request" ? "requestBody" : "responseBody";
|
|
62
|
+
const existing = session[field];
|
|
63
|
+
if (existing)
|
|
64
|
+
return existing;
|
|
65
|
+
const headers = direction === "request" ? session.requestHeaders : session.responseHeaders;
|
|
66
|
+
if (!headers)
|
|
67
|
+
return undefined;
|
|
68
|
+
const created = collector_js_1.BodyCollector.create(session.span, "client", direction, () => headers);
|
|
69
|
+
if (created)
|
|
70
|
+
session[field] = created;
|
|
71
|
+
return created;
|
|
72
|
+
}
|
|
73
|
+
function finishUndici(request, complete) {
|
|
74
|
+
const session = undiciSessions.get(request);
|
|
75
|
+
if (!session)
|
|
76
|
+
return;
|
|
77
|
+
undiciSessions.delete(request);
|
|
78
|
+
session.requestBody?.finalize(complete);
|
|
79
|
+
session.responseBody?.finalize(complete);
|
|
80
|
+
}
|
|
81
|
+
function bindUndiciChannel(name, handle) {
|
|
82
|
+
(0, node_diagnostics_channel_1.subscribe)(name, (message) => {
|
|
83
|
+
(0, utils_js_1.safely)(() => {
|
|
84
|
+
const request = undiciRequest(message);
|
|
85
|
+
if (!request)
|
|
86
|
+
return;
|
|
87
|
+
const session = undiciSessions.get(request);
|
|
88
|
+
if (!session)
|
|
89
|
+
return;
|
|
90
|
+
handle(message, request, session);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function wrapLegacyBodyMethod(request, name, direction) {
|
|
95
|
+
const target = request;
|
|
96
|
+
const original = target[name];
|
|
97
|
+
if (typeof original !== "function")
|
|
98
|
+
return;
|
|
99
|
+
target[name] = function (chunk, ...rest) {
|
|
100
|
+
const result = original.call(this, chunk, ...rest);
|
|
101
|
+
(0, utils_js_1.safely)(() => {
|
|
102
|
+
const session = undiciSessions.get(request);
|
|
103
|
+
if (!session || session.modernBodyChannels)
|
|
104
|
+
return;
|
|
105
|
+
undiciCollector(session, direction)?.observe(chunk);
|
|
106
|
+
});
|
|
107
|
+
return result;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function bindLegacyUndiciBodyMethods(request) {
|
|
111
|
+
if (legacyWrappedRequests.has(request))
|
|
112
|
+
return;
|
|
113
|
+
legacyWrappedRequests.add(request);
|
|
114
|
+
wrapLegacyBodyMethod(request, "onBodySent", "request");
|
|
115
|
+
wrapLegacyBodyMethod(request, "onData", "response");
|
|
116
|
+
wrapLegacyBodyMethod(request, "onResponseData", "response");
|
|
117
|
+
}
|
|
118
|
+
function bindUndiciBodyChannels() {
|
|
119
|
+
if (undiciBodyChannelsBound)
|
|
120
|
+
return;
|
|
121
|
+
undiciBodyChannelsBound = true;
|
|
122
|
+
(0, node_diagnostics_channel_1.subscribe)("undici:request:create", (message) => {
|
|
123
|
+
(0, utils_js_1.safely)(() => {
|
|
124
|
+
const request = undiciRequest(message);
|
|
125
|
+
if (request)
|
|
126
|
+
bindLegacyUndiciBodyMethods(request);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
bindUndiciChannel("undici:request:bodyChunkSent", (message, _request, session) => {
|
|
130
|
+
session.modernBodyChannels = true;
|
|
131
|
+
undiciCollector(session, "request")?.observe(message.chunk);
|
|
132
|
+
});
|
|
133
|
+
bindUndiciChannel("undici:request:bodySent", (_message, _request, session) => {
|
|
134
|
+
session.requestBody?.finalize(true);
|
|
135
|
+
});
|
|
136
|
+
bindUndiciChannel("undici:request:bodyChunkReceived", (message, _request, session) => {
|
|
137
|
+
session.modernBodyChannels = true;
|
|
138
|
+
undiciCollector(session, "response")?.observe(message.chunk);
|
|
139
|
+
});
|
|
140
|
+
bindUndiciChannel("undici:request:trailers", (_message, request) => {
|
|
141
|
+
finishUndici(request, true);
|
|
142
|
+
});
|
|
143
|
+
bindUndiciChannel("undici:request:error", (_message, request) => {
|
|
144
|
+
finishUndici(request, false);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function createUndiciNetworkCaptureHooks(bodyCaptureEnabled = true) {
|
|
148
|
+
if (bodyCaptureEnabled)
|
|
149
|
+
bindUndiciBodyChannels();
|
|
150
|
+
return {
|
|
151
|
+
requestHook(span, request) {
|
|
152
|
+
(0, utils_js_1.safely)(() => {
|
|
153
|
+
const headers = undiciRequestHeaders(request.headers);
|
|
154
|
+
(0, collector_js_1.setHeaderAttributes)(span, "request", headers);
|
|
155
|
+
if (!bodyCaptureEnabled ||
|
|
156
|
+
!span.isRecording() ||
|
|
157
|
+
(span.spanContext().traceFlags & api_1.TraceFlags.SAMPLED) !==
|
|
158
|
+
api_1.TraceFlags.SAMPLED) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
undiciSessions.set(request, { span, requestHeaders: headers });
|
|
162
|
+
bindLegacyUndiciBodyMethods(request);
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
responseHook(span, info) {
|
|
166
|
+
(0, utils_js_1.safely)(() => {
|
|
167
|
+
const headers = undiciResponseHeaders(info.response.headers);
|
|
168
|
+
(0, collector_js_1.setHeaderAttributes)(span, "response", headers);
|
|
169
|
+
if (!bodyCaptureEnabled || info.request === undefined)
|
|
170
|
+
return;
|
|
171
|
+
const session = undiciSessions.get(info.request);
|
|
172
|
+
if (session)
|
|
173
|
+
session.responseHeaders = headers;
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
package/dist/otlp.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SeverityNumber } from "@opentelemetry/api-logs";
|
|
2
|
+
export declare function sendOtlpLog({ token, resourceAttributes, scopeName, severityNumber, body, }: {
|
|
3
|
+
token: string;
|
|
4
|
+
resourceAttributes: Record<string, string>;
|
|
5
|
+
scopeName: string;
|
|
6
|
+
severityNumber: SeverityNumber;
|
|
7
|
+
body: string;
|
|
8
|
+
}): Promise<void>;
|
package/dist/otlp.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sendOtlpLog = sendOtlpLog;
|
|
4
|
+
const constants_js_1 = require("./constants.js");
|
|
5
|
+
const endpoint_js_1 = require("./endpoint.js");
|
|
6
|
+
// pcga11: Pipeline-independent OTLP log delivery.
|
|
7
|
+
// Use when the SDK's LoggerProvider/exporter never got set up or is
|
|
8
|
+
// broken (diagnostics). Not a client surface: no batching, no resource
|
|
9
|
+
// detectors, no trace correlation, one fetch per record. Client logs go
|
|
10
|
+
// through the Foam LoggerProvider in logs.ts.
|
|
11
|
+
async function sendOtlpLog({ token, resourceAttributes, scopeName, severityNumber, body, }) {
|
|
12
|
+
try {
|
|
13
|
+
await fetch(`${endpoint_js_1.endpoint}${constants_js_1.FOAM_OTLP_LOGS_PATH}`, {
|
|
14
|
+
method: "POST",
|
|
15
|
+
headers: {
|
|
16
|
+
Authorization: `Bearer ${token}`,
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
},
|
|
19
|
+
body: JSON.stringify({
|
|
20
|
+
resourceLogs: [
|
|
21
|
+
{
|
|
22
|
+
resource: {
|
|
23
|
+
attributes: Object.entries(resourceAttributes).map(([key, value]) => ({ key, value: { stringValue: value } })),
|
|
24
|
+
},
|
|
25
|
+
scopeLogs: [
|
|
26
|
+
{
|
|
27
|
+
scope: { name: scopeName },
|
|
28
|
+
logRecords: [
|
|
29
|
+
{
|
|
30
|
+
timeUnixNano: String(BigInt(Date.now()) * 1000000n),
|
|
31
|
+
severityNumber,
|
|
32
|
+
severityText: constants_js_1.SEVERITY_TEXT[severityNumber],
|
|
33
|
+
body: { stringValue: body },
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// pcga11: do not remove this catch block. Ingest must never throw into application code.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Context } from "@opentelemetry/api";
|
|
2
|
+
export declare function injectTraceContext(carrier: Record<string, string>): void;
|
|
3
|
+
export declare function extractTraceContext(carrier: Record<string, string>): Context;
|
|
4
|
+
export declare function setBaggage<T>(key: string, value: string, callback: () => T): T | undefined;
|
|
5
|
+
export declare function getBaggage(key: string): string | undefined;
|