@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,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.instrumentationName = void 0;
|
|
4
|
+
exports.buildInstrumentations = buildInstrumentations;
|
|
5
|
+
const auto_instrumentations_node_1 = require("@opentelemetry/auto-instrumentations-node");
|
|
6
|
+
const instrumentation_console_1 = require("@opentelemetry/instrumentation-console");
|
|
7
|
+
const index_js_1 = require("./network-capture/index.js");
|
|
8
|
+
const endpoint_js_1 = require("./endpoint.js");
|
|
9
|
+
const state_js_1 = require("./state.js");
|
|
10
|
+
const constants_js_1 = require("./constants.js");
|
|
11
|
+
function buildInstrumentations(networkCapture = "basic", disableLogSending = false, ignoredOutboundHosts, additionalInstrumentations) {
|
|
12
|
+
// pcga11: Captured bodies are emitted through the global logs pipeline. Only enable
|
|
13
|
+
// capture when that pipeline delivers to Foam ("global"/"ingest").
|
|
14
|
+
// If another SDK owns the slot (aka we have "local" for logs), capturing would be done for other vendors instead of Foam which might be non-compliant.
|
|
15
|
+
// If it's "none", capturing does not work at all.
|
|
16
|
+
const logsSource = (0, state_js_1.getSignal)(state_js_1.Signals.logs);
|
|
17
|
+
const bodyCaptureEnabled = logsSource === state_js_1.SignalSources.global || logsSource === state_js_1.SignalSources.ingest;
|
|
18
|
+
const capturedHeaders = networkCapture === "off" ? undefined : [...constants_js_1.SAFE_NETWORK_HEADERS];
|
|
19
|
+
// pcga11: Skip Foam's OTLP export HTTP calls so they do not become client spans
|
|
20
|
+
// TODO(pcga11): This could get real interesting: could we use these calls to verify coverage? IDK maybe tarpid
|
|
21
|
+
const ignoredHosts = new Set([
|
|
22
|
+
new URL(endpoint_js_1.endpoint).hostname,
|
|
23
|
+
...(ignoredOutboundHosts ?? []),
|
|
24
|
+
]);
|
|
25
|
+
const ignoreHttpOutgoingRequest = ((request) => typeof request.hostname === "string" &&
|
|
26
|
+
ignoredHosts.has(request.hostname));
|
|
27
|
+
const ignoreUndiciRequest = ((request) => {
|
|
28
|
+
try {
|
|
29
|
+
return ignoredHosts.has(new URL(request.origin).hostname);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
const bodyHooks = networkCapture === "advanced"
|
|
36
|
+
? (0, index_js_1.createNetworkCaptureHooks)(bodyCaptureEnabled)
|
|
37
|
+
: undefined;
|
|
38
|
+
const advancedUndiciHooks = networkCapture === "advanced"
|
|
39
|
+
? (0, index_js_1.createUndiciNetworkCaptureHooks)(bodyCaptureEnabled)
|
|
40
|
+
: undefined;
|
|
41
|
+
const automatic = (0, auto_instrumentations_node_1.getNodeAutoInstrumentations)({
|
|
42
|
+
"@opentelemetry/instrumentation-bunyan": {
|
|
43
|
+
disableLogSending: disableLogSending === true,
|
|
44
|
+
},
|
|
45
|
+
"@opentelemetry/instrumentation-http": {
|
|
46
|
+
...(capturedHeaders
|
|
47
|
+
? {
|
|
48
|
+
headersToSpanAttributes: {
|
|
49
|
+
client: {
|
|
50
|
+
requestHeaders: capturedHeaders,
|
|
51
|
+
responseHeaders: capturedHeaders,
|
|
52
|
+
},
|
|
53
|
+
server: {
|
|
54
|
+
requestHeaders: capturedHeaders,
|
|
55
|
+
responseHeaders: capturedHeaders,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
: {}),
|
|
60
|
+
...(bodyHooks ?? {}),
|
|
61
|
+
ignoreOutgoingRequestHook: ignoreHttpOutgoingRequest,
|
|
62
|
+
},
|
|
63
|
+
"@opentelemetry/instrumentation-openai": {
|
|
64
|
+
captureMessageContent: networkCapture === "advanced",
|
|
65
|
+
},
|
|
66
|
+
"@opentelemetry/instrumentation-pino": {
|
|
67
|
+
disableLogSending: disableLogSending === true,
|
|
68
|
+
},
|
|
69
|
+
"@opentelemetry/instrumentation-undici": {
|
|
70
|
+
...(capturedHeaders
|
|
71
|
+
? {
|
|
72
|
+
headersToSpanAttributes: {
|
|
73
|
+
requestHeaders: capturedHeaders,
|
|
74
|
+
responseHeaders: capturedHeaders,
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
: {}),
|
|
78
|
+
...(advancedUndiciHooks ?? {}),
|
|
79
|
+
ignoreRequestHook: ignoreUndiciRequest,
|
|
80
|
+
},
|
|
81
|
+
"@opentelemetry/instrumentation-winston": {
|
|
82
|
+
disableLogSending: disableLogSending === true,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
const instrumentations = [
|
|
86
|
+
...automatic,
|
|
87
|
+
// pcga11: Ignores disableLogSending: console has no in-process transports to double-ship through.
|
|
88
|
+
new instrumentation_console_1.ConsoleInstrumentation(),
|
|
89
|
+
...(additionalInstrumentations ?? []),
|
|
90
|
+
];
|
|
91
|
+
// pcga11: Last entry per name wins: user entries win over our defaults, so on a name
|
|
92
|
+
// collision the map holds our instance as they come first in the list and we disable it.
|
|
93
|
+
const unique = new Map();
|
|
94
|
+
for (const entry of instrumentations) {
|
|
95
|
+
unique.get(entry.instrumentationName)?.disable();
|
|
96
|
+
unique.set(entry.instrumentationName, entry);
|
|
97
|
+
}
|
|
98
|
+
return [...unique.values()];
|
|
99
|
+
}
|
|
100
|
+
const instrumentationName = (instrumentation) => instrumentation.instrumentationName.replace(/^@opentelemetry\/instrumentation-/, "");
|
|
101
|
+
exports.instrumentationName = instrumentationName;
|
package/dist/logs.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Attributes } from "@opentelemetry/api";
|
|
2
|
+
import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
3
|
+
import { LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
4
|
+
export declare function setFoamLoggerProvider(provider: LoggerProvider): void;
|
|
5
|
+
export declare function getFoamLoggerProvider(): LoggerProvider | undefined;
|
|
6
|
+
export declare function clearFoamLoggerProvider(): void;
|
|
7
|
+
export declare function ensureFoamLoggerProvider(name: string, environment: string, token: string, stamp?: Attributes): LoggerProvider;
|
|
8
|
+
export declare const log: (body: string, severityNumber?: SeverityNumber, attributes?: Attributes) => void;
|
|
9
|
+
export { SeverityNumber };
|
package/dist/logs.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SeverityNumber = exports.log = void 0;
|
|
4
|
+
exports.setFoamLoggerProvider = setFoamLoggerProvider;
|
|
5
|
+
exports.getFoamLoggerProvider = getFoamLoggerProvider;
|
|
6
|
+
exports.clearFoamLoggerProvider = clearFoamLoggerProvider;
|
|
7
|
+
exports.ensureFoamLoggerProvider = ensureFoamLoggerProvider;
|
|
8
|
+
const api_logs_1 = require("@opentelemetry/api-logs");
|
|
9
|
+
Object.defineProperty(exports, "SeverityNumber", { enumerable: true, get: function () { return api_logs_1.SeverityNumber; } });
|
|
10
|
+
const sdk_logs_1 = require("@opentelemetry/sdk-logs");
|
|
11
|
+
const constants_js_1 = require("./constants.js");
|
|
12
|
+
const exporters_js_1 = require("./exporters.js");
|
|
13
|
+
const resource_js_1 = require("./resource.js");
|
|
14
|
+
const utils_js_1 = require("./utils.js");
|
|
15
|
+
let foamLoggerProvider;
|
|
16
|
+
function setFoamLoggerProvider(provider) {
|
|
17
|
+
if (foamLoggerProvider && foamLoggerProvider !== provider) {
|
|
18
|
+
void foamLoggerProvider.shutdown().catch(() => undefined);
|
|
19
|
+
}
|
|
20
|
+
foamLoggerProvider = provider;
|
|
21
|
+
}
|
|
22
|
+
function getFoamLoggerProvider() {
|
|
23
|
+
return foamLoggerProvider;
|
|
24
|
+
}
|
|
25
|
+
function clearFoamLoggerProvider() {
|
|
26
|
+
foamLoggerProvider = undefined;
|
|
27
|
+
}
|
|
28
|
+
function ensureFoamLoggerProvider(name, environment, token, stamp) {
|
|
29
|
+
const existing = foamLoggerProvider;
|
|
30
|
+
if (existing) {
|
|
31
|
+
return existing;
|
|
32
|
+
}
|
|
33
|
+
const provider = new sdk_logs_1.LoggerProvider({
|
|
34
|
+
resource: (0, resource_js_1.createFoamResource)(name, environment),
|
|
35
|
+
processors: [
|
|
36
|
+
new sdk_logs_1.BatchLogRecordProcessor({
|
|
37
|
+
exporter: new exporters_js_1.FoamLogExporter(token, stamp),
|
|
38
|
+
}),
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
setFoamLoggerProvider(provider);
|
|
42
|
+
process.once("beforeExit", () => {
|
|
43
|
+
void provider.shutdown().catch(() => undefined);
|
|
44
|
+
});
|
|
45
|
+
return provider;
|
|
46
|
+
}
|
|
47
|
+
const log = (body, severityNumber = api_logs_1.SeverityNumber.INFO, attributes) => {
|
|
48
|
+
const provider = foamLoggerProvider;
|
|
49
|
+
if (!provider) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
(0, utils_js_1.safely)(() => {
|
|
53
|
+
provider.getLogger(constants_js_1.FOAM_DISTRO_NAME, constants_js_1.FOAM_DISTRO_VERSION).emit({
|
|
54
|
+
body,
|
|
55
|
+
severityNumber,
|
|
56
|
+
severityText: constants_js_1.SEVERITY_TEXT[severityNumber],
|
|
57
|
+
attributes,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
exports.log = log;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Attributes, type MetricOptions } from "@opentelemetry/api";
|
|
2
|
+
export declare const incrementCounter: (name: string, n?: number | undefined, attributes?: Attributes | undefined, options?: MetricOptions | undefined) => void;
|
|
3
|
+
export declare const recordHistogram: (name: string, value: number, attributes?: Attributes | undefined, options?: MetricOptions | undefined) => void;
|
|
4
|
+
export declare const addUpDownCounter: (name: string, n: number, attributes?: Attributes | undefined, options?: MetricOptions | undefined) => void;
|
|
5
|
+
export declare const setMetric: (name: string, value: number, attributes?: Attributes | undefined, options?: MetricOptions | undefined) => void;
|
package/dist/metrics.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setMetric = exports.addUpDownCounter = exports.recordHistogram = exports.incrementCounter = void 0;
|
|
4
|
+
const api_1 = require("@opentelemetry/api");
|
|
5
|
+
const sdk_metrics_1 = require("@opentelemetry/sdk-metrics");
|
|
6
|
+
const constants_js_1 = require("./constants.js");
|
|
7
|
+
const utils_js_1 = require("./utils.js");
|
|
8
|
+
const state_js_1 = require("./state.js");
|
|
9
|
+
const instruments = new Map();
|
|
10
|
+
const meter = () => api_1.metrics.getMeter(constants_js_1.FOAM_DISTRO_NAME);
|
|
11
|
+
// pcga11: We use cache to avoid wasting time searching for the meter every time we need to create an instrument.
|
|
12
|
+
function cachedInstrument(type, name, create) {
|
|
13
|
+
const key = `${type}:${name}`;
|
|
14
|
+
if (!instruments.has(key))
|
|
15
|
+
instruments.set(key, create());
|
|
16
|
+
return instruments.get(key);
|
|
17
|
+
}
|
|
18
|
+
exports.incrementCounter = (0, utils_js_1.whenInitialized)(state_js_1.Signals.metrics, (name, n = 1, attributes, options = {}) => {
|
|
19
|
+
cachedInstrument(sdk_metrics_1.InstrumentType.COUNTER, name, () => meter().createCounter(name, options)).add(n, attributes);
|
|
20
|
+
});
|
|
21
|
+
exports.recordHistogram = (0, utils_js_1.whenInitialized)(state_js_1.Signals.metrics, (name, value, attributes, options = {}) => {
|
|
22
|
+
cachedInstrument(sdk_metrics_1.InstrumentType.HISTOGRAM, name, () => meter().createHistogram(name, options)).record(value, attributes);
|
|
23
|
+
});
|
|
24
|
+
exports.addUpDownCounter = (0, utils_js_1.whenInitialized)(state_js_1.Signals.metrics, (name, n, attributes, options = {}) => {
|
|
25
|
+
cachedInstrument(sdk_metrics_1.InstrumentType.UP_DOWN_COUNTER, name, () => meter().createUpDownCounter(name, options)).add(n, attributes);
|
|
26
|
+
});
|
|
27
|
+
exports.setMetric = (0, utils_js_1.whenInitialized)(state_js_1.Signals.metrics, (name, value, attributes, options = {}) => {
|
|
28
|
+
cachedInstrument(sdk_metrics_1.InstrumentType.GAUGE, name, () => meter().createGauge(name, options)).record(value, attributes);
|
|
29
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Context, type Span } from "@opentelemetry/api";
|
|
2
|
+
export type CaptureDirection = "request" | "response";
|
|
3
|
+
export type CaptureSide = "client" | "server";
|
|
4
|
+
export type HeaderSnapshot = Readonly<Record<string, unknown>>;
|
|
5
|
+
export declare function setHeaderAttributes(span: Span, direction: CaptureDirection, headers: HeaderSnapshot): void;
|
|
6
|
+
export declare class BodyCollector {
|
|
7
|
+
private readonly span;
|
|
8
|
+
private readonly context;
|
|
9
|
+
private readonly side;
|
|
10
|
+
private readonly direction;
|
|
11
|
+
private readonly headers;
|
|
12
|
+
private readonly id;
|
|
13
|
+
private readonly attr;
|
|
14
|
+
private readonly chunks;
|
|
15
|
+
private readonly cleanups;
|
|
16
|
+
private readonly deadline;
|
|
17
|
+
private observedBytes;
|
|
18
|
+
private capturedBytes;
|
|
19
|
+
private truncated;
|
|
20
|
+
private finalized;
|
|
21
|
+
constructor(span: Span, context: Context, side: CaptureSide, direction: CaptureDirection, headers: () => HeaderSnapshot);
|
|
22
|
+
static create(span: Span, side: CaptureSide, direction: CaptureDirection, headers: () => HeaderSnapshot): BodyCollector | undefined;
|
|
23
|
+
addCleanup(cleanup: () => void): void;
|
|
24
|
+
observe(value: unknown, encoding?: unknown): void;
|
|
25
|
+
finalize(complete: boolean): void;
|
|
26
|
+
private emit;
|
|
27
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BodyCollector = void 0;
|
|
4
|
+
exports.setHeaderAttributes = setHeaderAttributes;
|
|
5
|
+
const api_1 = require("@opentelemetry/api");
|
|
6
|
+
const api_logs_1 = require("@opentelemetry/api-logs");
|
|
7
|
+
const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const node_zlib_1 = require("node:zlib");
|
|
10
|
+
const constants_js_1 = require("../constants.js");
|
|
11
|
+
const utils_js_1 = require("../utils.js");
|
|
12
|
+
const redact_js_1 = require("./redact.js");
|
|
13
|
+
const ALLOWED_HEADERS = new Set(constants_js_1.SAFE_NETWORK_HEADERS);
|
|
14
|
+
const ATTR = {
|
|
15
|
+
request: {
|
|
16
|
+
header: semantic_conventions_1.ATTR_HTTP_REQUEST_HEADER,
|
|
17
|
+
size: constants_js_1.ATTR_HTTP_REQUEST_BODY_SIZE,
|
|
18
|
+
content: constants_js_1.ATTR_HTTP_REQUEST_BODY_CONTENT,
|
|
19
|
+
captureId: constants_js_1.ATTR_FOAM_HTTP_REQUEST_BODY_CAPTURE_ID,
|
|
20
|
+
complete: constants_js_1.ATTR_FOAM_HTTP_REQUEST_BODY_COMPLETE,
|
|
21
|
+
truncated: constants_js_1.ATTR_FOAM_HTTP_REQUEST_BODY_TRUNCATED,
|
|
22
|
+
},
|
|
23
|
+
response: {
|
|
24
|
+
header: semantic_conventions_1.ATTR_HTTP_RESPONSE_HEADER,
|
|
25
|
+
size: constants_js_1.ATTR_HTTP_RESPONSE_BODY_SIZE,
|
|
26
|
+
content: constants_js_1.ATTR_HTTP_RESPONSE_BODY_CONTENT,
|
|
27
|
+
captureId: constants_js_1.ATTR_FOAM_HTTP_RESPONSE_BODY_CAPTURE_ID,
|
|
28
|
+
complete: constants_js_1.ATTR_FOAM_HTTP_RESPONSE_BODY_COMPLETE,
|
|
29
|
+
truncated: constants_js_1.ATTR_FOAM_HTTP_RESPONSE_BODY_TRUNCATED,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
let activeCaptures = 0;
|
|
33
|
+
function captureLogger() {
|
|
34
|
+
return api_logs_1.logs.getLogger(constants_js_1.FOAM_DISTRO_NAME, constants_js_1.FOAM_DISTRO_VERSION);
|
|
35
|
+
}
|
|
36
|
+
function headerValues(value) {
|
|
37
|
+
return Array.isArray(value) ? value.map(String) : [String(value)];
|
|
38
|
+
}
|
|
39
|
+
function setHeaderAttributes(span, direction, headers) {
|
|
40
|
+
const attributes = {};
|
|
41
|
+
for (const [rawName, rawValue] of Object.entries(headers)) {
|
|
42
|
+
if (rawValue === undefined)
|
|
43
|
+
continue;
|
|
44
|
+
const name = rawName.toLowerCase();
|
|
45
|
+
if (!ALLOWED_HEADERS.has(name))
|
|
46
|
+
continue;
|
|
47
|
+
attributes[ATTR[direction].header(name)] = headerValues(rawValue);
|
|
48
|
+
}
|
|
49
|
+
if (Object.keys(attributes).length > 0)
|
|
50
|
+
span.setAttributes(attributes);
|
|
51
|
+
}
|
|
52
|
+
function stringEncoding(encoding) {
|
|
53
|
+
return typeof encoding === "string" && Buffer.isEncoding(encoding)
|
|
54
|
+
? encoding
|
|
55
|
+
: "utf8";
|
|
56
|
+
}
|
|
57
|
+
function copyBody(value, encoding) {
|
|
58
|
+
if (typeof value === "string") {
|
|
59
|
+
return Buffer.from(value, stringEncoding(encoding));
|
|
60
|
+
}
|
|
61
|
+
if (Buffer.isBuffer(value))
|
|
62
|
+
return Buffer.from(value);
|
|
63
|
+
if (ArrayBuffer.isView(value)) {
|
|
64
|
+
return Buffer.from(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
// Byte length of a body chunk without copying it; undefined for non-body values.
|
|
69
|
+
function bodyByteLength(value, encoding) {
|
|
70
|
+
if (typeof value === "string") {
|
|
71
|
+
return Buffer.byteLength(value, stringEncoding(encoding));
|
|
72
|
+
}
|
|
73
|
+
if (Buffer.isBuffer(value) || ArrayBuffer.isView(value)) {
|
|
74
|
+
return value.byteLength;
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
function decodeBody(bytes, contentEncoding) {
|
|
79
|
+
if (contentEncoding === undefined)
|
|
80
|
+
return bytes;
|
|
81
|
+
const encoding = headerValues(contentEncoding)[0]?.toLowerCase().trim();
|
|
82
|
+
if (!encoding || encoding === "identity")
|
|
83
|
+
return bytes;
|
|
84
|
+
// maxOutputLength turns a decompression bomb into a caught throw instead
|
|
85
|
+
// of an unrecoverable synchronous OOM allocation.
|
|
86
|
+
const limits = { maxOutputLength: constants_js_1.NETWORK_BODY_MAX_DECODE_BYTES };
|
|
87
|
+
try {
|
|
88
|
+
if (encoding === "gzip" || encoding === "x-gzip") {
|
|
89
|
+
return (0, node_zlib_1.gunzipSync)(bytes, limits);
|
|
90
|
+
}
|
|
91
|
+
if (encoding === "deflate") {
|
|
92
|
+
try {
|
|
93
|
+
return (0, node_zlib_1.inflateSync)(bytes, limits);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return (0, node_zlib_1.unzipSync)(bytes, limits);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (encoding === "br")
|
|
100
|
+
return (0, node_zlib_1.brotliDecompressSync)(bytes, limits);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
function textEncoding(contentType) {
|
|
108
|
+
const raw = headerValues(contentType)[0];
|
|
109
|
+
if (raw === undefined)
|
|
110
|
+
return undefined;
|
|
111
|
+
const parts = raw.split(";").map((part) => part.trim());
|
|
112
|
+
const media = (parts[0] ?? "").toLowerCase();
|
|
113
|
+
const charset = parts
|
|
114
|
+
.find((part) => part.toLowerCase().startsWith("charset="))
|
|
115
|
+
?.slice("charset=".length)
|
|
116
|
+
.toLowerCase();
|
|
117
|
+
const isText = media.startsWith("text/") ||
|
|
118
|
+
media.endsWith("/json") ||
|
|
119
|
+
media.endsWith("+json") ||
|
|
120
|
+
media.endsWith("/xml") ||
|
|
121
|
+
media.endsWith("+xml") ||
|
|
122
|
+
media.endsWith("/yaml") ||
|
|
123
|
+
media.endsWith("+yaml") ||
|
|
124
|
+
media === "application/x-www-form-urlencoded" ||
|
|
125
|
+
Boolean(charset);
|
|
126
|
+
if (!isText)
|
|
127
|
+
return undefined;
|
|
128
|
+
if (charset && Buffer.isEncoding(charset))
|
|
129
|
+
return charset;
|
|
130
|
+
return "utf8";
|
|
131
|
+
}
|
|
132
|
+
// Last index such that `buf.subarray(0, end)` is complete UTF-8, capped at `max`.
|
|
133
|
+
function utf8End(buf, max) {
|
|
134
|
+
const end = Math.min(max, buf.byteLength);
|
|
135
|
+
if (end === 0)
|
|
136
|
+
return 0;
|
|
137
|
+
let leadAt = end;
|
|
138
|
+
while (leadAt > 0 && (buf[leadAt - 1] & 0xc0) === 0x80)
|
|
139
|
+
leadAt--;
|
|
140
|
+
if (leadAt === 0)
|
|
141
|
+
return 0;
|
|
142
|
+
const lead = buf[leadAt - 1];
|
|
143
|
+
const needed = (lead & 0x80) === 0
|
|
144
|
+
? 1
|
|
145
|
+
: (lead & 0xe0) === 0xc0
|
|
146
|
+
? 2
|
|
147
|
+
: (lead & 0xf0) === 0xe0
|
|
148
|
+
? 3
|
|
149
|
+
: (lead & 0xf8) === 0xf0
|
|
150
|
+
? 4
|
|
151
|
+
: Number.POSITIVE_INFINITY;
|
|
152
|
+
return end - (leadAt - 1) >= needed ? end : leadAt - 1;
|
|
153
|
+
}
|
|
154
|
+
function cappedBody(buf) {
|
|
155
|
+
if (buf.byteLength <= constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES)
|
|
156
|
+
return buf;
|
|
157
|
+
return buf.subarray(0, constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES);
|
|
158
|
+
}
|
|
159
|
+
function decodeText(decoded, encoding) {
|
|
160
|
+
const limited = cappedBody(decoded);
|
|
161
|
+
if (encoding === "utf8" || encoding === "utf-8") {
|
|
162
|
+
return limited.subarray(0, utf8End(limited, limited.byteLength)).toString(encoding);
|
|
163
|
+
}
|
|
164
|
+
return limited.toString(encoding);
|
|
165
|
+
}
|
|
166
|
+
class BodyCollector {
|
|
167
|
+
span;
|
|
168
|
+
context;
|
|
169
|
+
side;
|
|
170
|
+
direction;
|
|
171
|
+
headers;
|
|
172
|
+
id = (0, node_crypto_1.randomUUID)();
|
|
173
|
+
attr;
|
|
174
|
+
chunks = [];
|
|
175
|
+
cleanups = [];
|
|
176
|
+
deadline;
|
|
177
|
+
observedBytes = 0;
|
|
178
|
+
capturedBytes = 0;
|
|
179
|
+
truncated = false;
|
|
180
|
+
finalized = false;
|
|
181
|
+
constructor(span, context, side, direction, headers) {
|
|
182
|
+
this.span = span;
|
|
183
|
+
this.context = context;
|
|
184
|
+
this.side = side;
|
|
185
|
+
this.direction = direction;
|
|
186
|
+
this.headers = headers;
|
|
187
|
+
this.attr = ATTR[direction];
|
|
188
|
+
activeCaptures += 1;
|
|
189
|
+
this.deadline = setTimeout(() => this.finalize(false), constants_js_1.NETWORK_CAPTURE_DEADLINE_MS);
|
|
190
|
+
this.deadline.unref();
|
|
191
|
+
this.span.setAttribute(this.attr.captureId, this.id);
|
|
192
|
+
}
|
|
193
|
+
static create(span, side, direction, headers) {
|
|
194
|
+
const context = api_1.trace.setSpanContext(api_1.ROOT_CONTEXT, span.spanContext());
|
|
195
|
+
if (activeCaptures >= constants_js_1.MAX_ACTIVE_CAPTURES ||
|
|
196
|
+
!span.isRecording() ||
|
|
197
|
+
(span.spanContext().traceFlags & api_1.TraceFlags.SAMPLED) !==
|
|
198
|
+
api_1.TraceFlags.SAMPLED ||
|
|
199
|
+
!captureLogger().enabled({
|
|
200
|
+
context,
|
|
201
|
+
eventName: constants_js_1.NETWORK_CAPTURE_EVENT,
|
|
202
|
+
severityNumber: api_logs_1.SeverityNumber.INFO,
|
|
203
|
+
})) {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
return new BodyCollector(span, context, side, direction, headers);
|
|
207
|
+
}
|
|
208
|
+
addCleanup(cleanup) {
|
|
209
|
+
this.cleanups.push(cleanup);
|
|
210
|
+
}
|
|
211
|
+
observe(value, encoding) {
|
|
212
|
+
if (this.finalized)
|
|
213
|
+
return;
|
|
214
|
+
if (this.capturedBytes >= constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES) {
|
|
215
|
+
// Past the cap every chunk is discarded; only its length is needed
|
|
216
|
+
// (for the observed size), so skip the per-chunk copy that would
|
|
217
|
+
// otherwise double the allocation traffic of large transfers.
|
|
218
|
+
const length = bodyByteLength(value, encoding);
|
|
219
|
+
if (length === undefined)
|
|
220
|
+
return;
|
|
221
|
+
this.deadline.refresh();
|
|
222
|
+
this.observedBytes += length;
|
|
223
|
+
this.truncated = true;
|
|
224
|
+
this.span.setAttributes({
|
|
225
|
+
[this.attr.size]: this.observedBytes,
|
|
226
|
+
[this.attr.truncated]: true,
|
|
227
|
+
});
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const bytes = copyBody(value, encoding);
|
|
231
|
+
if (!bytes)
|
|
232
|
+
return;
|
|
233
|
+
this.deadline.refresh();
|
|
234
|
+
this.observedBytes += bytes.byteLength;
|
|
235
|
+
this.span.setAttribute(this.attr.size, this.observedBytes);
|
|
236
|
+
const room = constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES - this.capturedBytes;
|
|
237
|
+
const kept = bytes.byteLength > room ? bytes.subarray(0, room) : bytes;
|
|
238
|
+
this.chunks.push(kept);
|
|
239
|
+
this.capturedBytes += kept.byteLength;
|
|
240
|
+
if (kept.byteLength < bytes.byteLength) {
|
|
241
|
+
this.truncated = true;
|
|
242
|
+
this.span.setAttribute(this.attr.truncated, true);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
finalize(complete) {
|
|
246
|
+
if (this.finalized)
|
|
247
|
+
return;
|
|
248
|
+
this.finalized = true;
|
|
249
|
+
clearTimeout(this.deadline);
|
|
250
|
+
activeCaptures -= 1;
|
|
251
|
+
for (const cleanup of this.cleanups.splice(0))
|
|
252
|
+
(0, utils_js_1.safely)(cleanup);
|
|
253
|
+
// Runs inside stream handlers; header snapshots may throw on destroyed messages.
|
|
254
|
+
(0, utils_js_1.safely)(() => {
|
|
255
|
+
const headers = this.headers();
|
|
256
|
+
setHeaderAttributes(this.span, this.direction, headers);
|
|
257
|
+
this.span.setAttributes({
|
|
258
|
+
[this.attr.complete]: complete,
|
|
259
|
+
[this.attr.truncated]: this.truncated,
|
|
260
|
+
[this.attr.size]: this.observedBytes,
|
|
261
|
+
});
|
|
262
|
+
this.emit(this.chunks.splice(0), headers, complete);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
emit(buffers, headers, complete) {
|
|
266
|
+
let outcome;
|
|
267
|
+
if (this.observedBytes === 0)
|
|
268
|
+
outcome = "empty";
|
|
269
|
+
else if (complete)
|
|
270
|
+
outcome = "captured";
|
|
271
|
+
else
|
|
272
|
+
outcome = "incomplete";
|
|
273
|
+
if (outcome === "empty")
|
|
274
|
+
return;
|
|
275
|
+
let wire;
|
|
276
|
+
if (buffers.length === 0)
|
|
277
|
+
wire = undefined;
|
|
278
|
+
else if (buffers.length === 1)
|
|
279
|
+
wire = buffers[0];
|
|
280
|
+
else
|
|
281
|
+
wire = Buffer.concat(buffers);
|
|
282
|
+
// Span attributes cannot hold bytes (OTel AttributeValue). Text goes on
|
|
283
|
+
// the span; binary uses the same `http.*.body.content` name on the event.
|
|
284
|
+
let binaryContent;
|
|
285
|
+
let chunkSource = wire;
|
|
286
|
+
if (wire !== undefined) {
|
|
287
|
+
const decoded = decodeBody(wire, headers["content-encoding"]);
|
|
288
|
+
if (decoded === undefined) {
|
|
289
|
+
binaryContent = Uint8Array.from(cappedBody(wire));
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
const encoding = textEncoding(headers["content-type"]);
|
|
293
|
+
if (encoding !== undefined) {
|
|
294
|
+
const redacted = (0, redact_js_1.redactBodyText)(decodeText(decoded, encoding), headers["content-type"]);
|
|
295
|
+
this.span.setAttribute(this.attr.content, redacted.text);
|
|
296
|
+
if (redacted.changed) {
|
|
297
|
+
// pcga11: Chunk events must not re-ship bytes the span content
|
|
298
|
+
// redacted, so the (possibly compressed) wire bytes are replaced
|
|
299
|
+
// with the redacted text.
|
|
300
|
+
chunkSource = Buffer.from(redacted.text, encoding);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
binaryContent = Uint8Array.from(cappedBody(decoded));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const chunks = [];
|
|
309
|
+
if (chunkSource) {
|
|
310
|
+
for (let i = 0; i < chunkSource.byteLength; i += constants_js_1.NETWORK_BODY_CHUNK_BYTES) {
|
|
311
|
+
chunks.push(chunkSource.subarray(i, i + constants_js_1.NETWORK_BODY_CHUNK_BYTES));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const common = {
|
|
315
|
+
[constants_js_1.ATTR_FOAM_HTTP_BODY_ID]: this.id,
|
|
316
|
+
[constants_js_1.ATTR_FOAM_HTTP_BODY_SIDE]: this.side,
|
|
317
|
+
[constants_js_1.ATTR_FOAM_HTTP_BODY_DIRECTION]: this.direction,
|
|
318
|
+
[constants_js_1.ATTR_FOAM_HTTP_BODY_COMPLETE]: complete,
|
|
319
|
+
[constants_js_1.ATTR_FOAM_HTTP_BODY_TRUNCATED]: this.truncated,
|
|
320
|
+
[constants_js_1.ATTR_FOAM_HTTP_BODY_OUTCOME]: outcome,
|
|
321
|
+
[this.attr.size]: this.observedBytes,
|
|
322
|
+
[constants_js_1.ATTR_FOAM_HTTP_BODY_CAPTURED_BYTES]: this.capturedBytes,
|
|
323
|
+
[constants_js_1.ATTR_FOAM_HTTP_BODY_CHUNK_COUNT]: chunks.length,
|
|
324
|
+
};
|
|
325
|
+
for (const name of ["content-type", "content-encoding"]) {
|
|
326
|
+
if (headers[name] !== undefined) {
|
|
327
|
+
common[this.attr.header(name)] = headerValues(headers[name]);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const logger = captureLogger();
|
|
331
|
+
const records = chunks.length === 0 ? [undefined] : chunks;
|
|
332
|
+
records.forEach((chunk, index) => {
|
|
333
|
+
logger.emit({
|
|
334
|
+
eventName: constants_js_1.NETWORK_CAPTURE_EVENT,
|
|
335
|
+
body: constants_js_1.NETWORK_CAPTURE_EVENT_BODY,
|
|
336
|
+
severityNumber: api_logs_1.SeverityNumber.INFO,
|
|
337
|
+
severityText: constants_js_1.SEVERITY_TEXT[api_logs_1.SeverityNumber.INFO],
|
|
338
|
+
attributes: {
|
|
339
|
+
...common,
|
|
340
|
+
...(chunk
|
|
341
|
+
? {
|
|
342
|
+
[constants_js_1.ATTR_FOAM_HTTP_BODY_CHUNK_INDEX]: index,
|
|
343
|
+
[constants_js_1.ATTR_FOAM_HTTP_BODY_CHUNK_BYTES]: chunk.byteLength,
|
|
344
|
+
[constants_js_1.ATTR_FOAM_HTTP_BODY_CHUNK_CONTENT]: Uint8Array.from(chunk),
|
|
345
|
+
}
|
|
346
|
+
: {}),
|
|
347
|
+
...(index === 0 && binaryContent
|
|
348
|
+
? { [this.attr.content]: binaryContent }
|
|
349
|
+
: {}),
|
|
350
|
+
},
|
|
351
|
+
context: this.context,
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
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
|
+
};
|