@foam-ai/node 0.1.0-alpha.3 → 0.1.0-alpha.5
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 +538 -75
- package/dist/constants.d.ts +43 -0
- package/dist/constants.js +97 -0
- package/dist/endpoint.d.ts +2 -0
- package/dist/endpoint.js +11 -0
- package/dist/exporters.d.ts +25 -0
- package/dist/exporters.js +103 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +26 -0
- package/dist/ingest.d.ts +6 -0
- package/dist/ingest.js +61 -0
- package/dist/init.d.ts +22 -0
- package/dist/init.js +150 -0
- package/dist/instrumentations.d.ts +3 -0
- package/dist/instrumentations.js +101 -0
- package/dist/logs.d.ts +9 -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 +356 -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 +2 -0
- package/dist/network-capture/index.js +17 -0
- package/dist/network-capture/redact.d.ts +6 -0
- package/dist/network-capture/redact.js +87 -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 +45 -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 -13
- package/dist/node/src/index.js +0 -19
- package/dist/node/src/init.d.ts +0 -27
- package/dist/node/src/init.js +0 -218
- package/dist/node/src/metrics.d.ts +0 -28
- package/dist/node/src/metrics.js +0 -69
- 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,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,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
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; } });
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isSensitiveBodyKey = isSensitiveBodyKey;
|
|
4
|
+
exports.redactBodyText = redactBodyText;
|
|
5
|
+
const constants_js_1 = require("../constants.js");
|
|
6
|
+
const EXACT_KEYS = new Set(constants_js_1.SENSITIVE_BODY_KEYS_EXACT);
|
|
7
|
+
function normalizeKey(key) {
|
|
8
|
+
return key.toLowerCase().replace(/[-_.\s]/g, "");
|
|
9
|
+
}
|
|
10
|
+
function isSensitiveBodyKey(key) {
|
|
11
|
+
const normalized = normalizeKey(key);
|
|
12
|
+
return (EXACT_KEYS.has(normalized) ||
|
|
13
|
+
constants_js_1.SENSITIVE_BODY_KEY_PATTERNS.some((pattern) => normalized.includes(pattern)));
|
|
14
|
+
}
|
|
15
|
+
function mediaType(contentType) {
|
|
16
|
+
const raw = Array.isArray(contentType) ? contentType[0] : contentType;
|
|
17
|
+
if (typeof raw !== "string")
|
|
18
|
+
return "";
|
|
19
|
+
return (raw.split(";")[0] ?? "").trim().toLowerCase();
|
|
20
|
+
}
|
|
21
|
+
function redactNode(node, state) {
|
|
22
|
+
if (Array.isArray(node)) {
|
|
23
|
+
return node.map((item) => redactNode(item, state));
|
|
24
|
+
}
|
|
25
|
+
if (node !== null && typeof node === "object") {
|
|
26
|
+
const result = {};
|
|
27
|
+
for (const [key, value] of Object.entries(node)) {
|
|
28
|
+
if (isSensitiveBodyKey(key) && value !== null && value !== undefined) {
|
|
29
|
+
result[key] = constants_js_1.REDACTED_VALUE;
|
|
30
|
+
state.changed = true;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
result[key] = redactNode(value, state);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
return node;
|
|
39
|
+
}
|
|
40
|
+
// pcga11: Captures a "key": value pair with a string (possibly unterminated
|
|
41
|
+
// at a truncation point), number, or literal value. Used only when the body
|
|
42
|
+
// does not parse as JSON, e.g. because the 1 MiB cap cut it mid-document.
|
|
43
|
+
const JSON_PAIR_PATTERN = /"((?:[^"\\]|\\.)*)"(\s*:\s*)("(?:[^"\\]|\\.)*"?|-?\d[\d.eE+-]*|true|false|null)/g;
|
|
44
|
+
function redactJsonFallback(text) {
|
|
45
|
+
let changed = false;
|
|
46
|
+
const redacted = text.replace(JSON_PAIR_PATTERN, (match, key, separator) => {
|
|
47
|
+
if (!isSensitiveBodyKey(key))
|
|
48
|
+
return match;
|
|
49
|
+
changed = true;
|
|
50
|
+
return `"${key}"${separator}"${constants_js_1.REDACTED_VALUE}"`;
|
|
51
|
+
});
|
|
52
|
+
return changed ? { text: redacted, changed } : { text, changed: false };
|
|
53
|
+
}
|
|
54
|
+
function redactJson(text) {
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(text);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return redactJsonFallback(text);
|
|
61
|
+
}
|
|
62
|
+
const state = { changed: false };
|
|
63
|
+
const redacted = redactNode(parsed, state);
|
|
64
|
+
if (!state.changed)
|
|
65
|
+
return { text, changed: false };
|
|
66
|
+
return { text: JSON.stringify(redacted), changed: true };
|
|
67
|
+
}
|
|
68
|
+
function redactForm(text) {
|
|
69
|
+
const params = new URLSearchParams(text);
|
|
70
|
+
let changed = false;
|
|
71
|
+
for (const key of new Set(params.keys())) {
|
|
72
|
+
if (!isSensitiveBodyKey(key))
|
|
73
|
+
continue;
|
|
74
|
+
params.set(key, constants_js_1.REDACTED_VALUE);
|
|
75
|
+
changed = true;
|
|
76
|
+
}
|
|
77
|
+
return changed ? { text: params.toString(), changed } : { text, changed: false };
|
|
78
|
+
}
|
|
79
|
+
function redactBodyText(text, contentType) {
|
|
80
|
+
const media = mediaType(contentType);
|
|
81
|
+
if (media === "application/x-www-form-urlencoded")
|
|
82
|
+
return redactForm(text);
|
|
83
|
+
if (media.endsWith("/json") || media.endsWith("+json")) {
|
|
84
|
+
return redactJson(text);
|
|
85
|
+
}
|
|
86
|
+
return { text, changed: false };
|
|
87
|
+
}
|
|
@@ -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 const setBaggage: (key: string, value: string) => void;
|
|
5
|
+
export declare function getBaggage(key: string): string | undefined;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setBaggage = void 0;
|
|
4
|
+
exports.injectTraceContext = injectTraceContext;
|
|
5
|
+
exports.extractTraceContext = extractTraceContext;
|
|
6
|
+
exports.getBaggage = getBaggage;
|
|
7
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
8
|
+
const api_1 = require("@opentelemetry/api");
|
|
9
|
+
const core_1 = require("@opentelemetry/core");
|
|
10
|
+
const utils_js_1 = require("./utils.js");
|
|
11
|
+
const state_js_1 = require("./state.js");
|
|
12
|
+
// pcga11: Below are some definitios you may find helpful to understand this file.
|
|
13
|
+
// CompositePropagator is a local propagator used on custom transports (Kafka, SQS, Redis, WebSocket)
|
|
14
|
+
// An "injector/extractor" turns the context into headers and vice versa for distributed tracing
|
|
15
|
+
// Carrier is the headers object can be kafka message, queue payload or any other transport headers
|
|
16
|
+
// Context.active() returns the process' context. Think of it like a local RAM for this request in this Node process. Another service, process or even another Kafka in the same service will have a different context.active().
|
|
17
|
+
const propagator = new core_1.CompositePropagator({
|
|
18
|
+
propagators: [new core_1.W3CTraceContextPropagator(), new core_1.W3CBaggagePropagator()],
|
|
19
|
+
});
|
|
20
|
+
const baggageStorage = new node_async_hooks_1.AsyncLocalStorage();
|
|
21
|
+
function contextWithBaggage() {
|
|
22
|
+
const entries = Object.fromEntries(api_1.propagation.getBaggage(api_1.context.active())?.getAllEntries() ?? []);
|
|
23
|
+
for (const [key, value] of baggageStorage.getStore() ?? []) {
|
|
24
|
+
entries[key] = { value };
|
|
25
|
+
}
|
|
26
|
+
const baggage = api_1.propagation.createBaggage(entries);
|
|
27
|
+
return api_1.propagation.setBaggage(api_1.context.active(), baggage);
|
|
28
|
+
}
|
|
29
|
+
function injectTraceContext(carrier) {
|
|
30
|
+
(0, utils_js_1.safely)(() => {
|
|
31
|
+
propagator.inject(contextWithBaggage(), carrier, api_1.defaultTextMapSetter);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function extractTraceContext(carrier) {
|
|
35
|
+
return (0, utils_js_1.safely)(() => propagator.extract(api_1.context.active(), carrier, api_1.defaultTextMapGetter), api_1.context.active());
|
|
36
|
+
}
|
|
37
|
+
exports.setBaggage = (0, utils_js_1.whenInitialized)(state_js_1.Signals.baggage, (key, value) => {
|
|
38
|
+
const next = new Map(baggageStorage.getStore() ?? []);
|
|
39
|
+
next.set(key, value);
|
|
40
|
+
baggageStorage.enterWith(next);
|
|
41
|
+
});
|
|
42
|
+
function getBaggage(key) {
|
|
43
|
+
return (0, utils_js_1.safely)(() => baggageStorage.getStore()?.get(key) ??
|
|
44
|
+
api_1.propagation.getBaggage(api_1.context.active())?.getEntry(key)?.value, undefined);
|
|
45
|
+
}
|
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
2
|
+
export declare function report({ name, environment, token, severity, message, error, }: {
|
|
3
|
+
name?: string;
|
|
4
|
+
environment?: string;
|
|
5
|
+
token?: string;
|
|
6
|
+
severity: SeverityNumber;
|
|
7
|
+
message: string;
|
|
8
|
+
error?: string;
|
|
9
|
+
}): Promise<void>;
|