@foam-ai/node 0.1.0-alpha.4 → 0.1.0-alpha.6
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 +393 -214
- package/dist/before-send.d.ts +16 -0
- package/dist/before-send.js +41 -0
- package/dist/constants.d.ts +10 -8
- package/dist/constants.js +19 -6
- package/dist/endpoint.d.ts +2 -0
- package/dist/endpoint.js +11 -0
- package/dist/exporters.d.ts +11 -3
- package/dist/exporters.js +127 -53
- package/dist/index.d.ts +7 -2
- package/dist/index.js +8 -1
- package/dist/ingest.d.ts +8 -2
- package/dist/ingest.js +28 -16
- package/dist/init.d.ts +7 -3
- package/dist/init.js +85 -37
- package/dist/instrumentations.js +25 -3
- package/dist/logs.d.ts +10 -0
- package/dist/logs.js +61 -0
- package/dist/network-capture/collector.js +168 -69
- package/dist/network-capture/http.d.ts +0 -1
- package/dist/network-capture/http.js +23 -25
- package/dist/network-capture/index.js +10 -0
- package/dist/network-capture/redact.d.ts +9 -0
- package/dist/network-capture/redact.js +401 -0
- package/dist/network-capture/undici.d.ts +0 -4
- package/dist/network-capture/undici.js +55 -22
- package/dist/otlp.d.ts +8 -0
- package/dist/otlp.js +46 -0
- package/dist/propagation.d.ts +1 -1
- package/dist/propagation.js +6 -4
- package/dist/redaction-keys.d.ts +3 -0
- package/dist/redaction-keys.js +421 -0
- package/dist/redaction.d.ts +24 -0
- package/dist/redaction.js +270 -0
- package/dist/report.d.ts +9 -0
- package/dist/report.js +56 -0
- package/dist/state.d.ts +11 -4
- package/dist/state.js +26 -9
- package/dist/traces.d.ts +1 -0
- package/dist/traces.js +21 -0
- package/dist/utils.js +1 -1
- package/package.json +1 -1
- package/dist/diagnostics.d.ts +0 -13
- package/dist/diagnostics.js +0 -83
package/dist/init.js
CHANGED
|
@@ -10,32 +10,73 @@ const resource_js_1 = require("./resource.js");
|
|
|
10
10
|
const sdk_logs_1 = require("@opentelemetry/sdk-logs");
|
|
11
11
|
const sdk_metrics_1 = require("@opentelemetry/sdk-metrics");
|
|
12
12
|
const sdk_trace_base_1 = require("@opentelemetry/sdk-trace-base");
|
|
13
|
-
const
|
|
14
|
-
const diagnostics_js_1 = require("./diagnostics.js");
|
|
13
|
+
const report_js_1 = require("./report.js");
|
|
15
14
|
const exporters_js_1 = require("./exporters.js");
|
|
16
15
|
const instrumentations_js_1 = require("./instrumentations.js");
|
|
16
|
+
const before_send_js_1 = require("./before-send.js");
|
|
17
|
+
const logs_js_1 = require("./logs.js");
|
|
18
|
+
const redaction_js_1 = require("./redaction.js");
|
|
17
19
|
const state_js_1 = require("./state.js");
|
|
18
|
-
// TODO(pcga11):
|
|
19
|
-
function init({
|
|
20
|
-
enabled = true,
|
|
20
|
+
// TODO(pcga11): conflicts handling pending.
|
|
21
|
+
function init(options) {
|
|
22
|
+
const { sampleRate = 1, disableLogSending = false, networkCapture = "basic", enabled = true, } = options;
|
|
23
|
+
const redactionConfig = (0, redaction_js_1.resolveRedactionConfig)(options.redact);
|
|
24
|
+
const beforeSend = (0, before_send_js_1.resolveBeforeSend)(options.beforeSend);
|
|
25
|
+
// pcga11: Set params before any early return so every state is reported, including failed or disabled attempts.
|
|
26
|
+
(0, state_js_1.setParams)({ name: options.name, environment: options.environment });
|
|
27
|
+
// pcga11: We keep this option because companies often want to turn the integration off in specific envs
|
|
21
28
|
if (!enabled) {
|
|
22
|
-
(0,
|
|
29
|
+
(0, report_js_1.report)({
|
|
30
|
+
token: options.token,
|
|
31
|
+
severity: api_logs_1.SeverityNumber.INFO,
|
|
32
|
+
message: 'SDK disabled',
|
|
33
|
+
});
|
|
23
34
|
return;
|
|
24
35
|
}
|
|
36
|
+
if (!options.token) {
|
|
37
|
+
// pcga11: Nothing can be sent without a token; report() still logs locally so this is not silent.
|
|
38
|
+
(0, report_js_1.report)({
|
|
39
|
+
severity: api_logs_1.SeverityNumber.ERROR,
|
|
40
|
+
message: 'Parameter token not provided',
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// pcga11: Re-initializing could register duplicate providers/instrumentations, so we track the initialization state and bail out on repeat calls
|
|
25
45
|
if ((0, state_js_1.getInitialized)()) {
|
|
26
|
-
(0,
|
|
46
|
+
(0, report_js_1.report)({
|
|
47
|
+
token: options.token,
|
|
48
|
+
severity: api_logs_1.SeverityNumber.WARN,
|
|
49
|
+
message: 'SDK already initialized',
|
|
50
|
+
});
|
|
27
51
|
return;
|
|
28
52
|
}
|
|
29
|
-
(
|
|
30
|
-
|
|
31
|
-
|
|
53
|
+
if (!options.name?.trim() || !options.environment?.trim()) {
|
|
54
|
+
(0, report_js_1.report)({
|
|
55
|
+
token: options.token,
|
|
56
|
+
severity: api_logs_1.SeverityNumber.ERROR,
|
|
57
|
+
message: 'Parameters name and environment not provided',
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (!Number.isFinite(sampleRate) || sampleRate < 0 || sampleRate > 1) {
|
|
62
|
+
(0, report_js_1.report)({
|
|
63
|
+
token: options.token,
|
|
64
|
+
severity: api_logs_1.SeverityNumber.ERROR,
|
|
65
|
+
message: 'Parameter sampleRate must be a finite number between 0 and 1',
|
|
66
|
+
});
|
|
32
67
|
return;
|
|
33
68
|
}
|
|
69
|
+
// pcga11: Hoisted so the catch block can shut down providers whose batch/periodic
|
|
70
|
+
// export timers started in their constructors, even when init fails midway.
|
|
71
|
+
let tracerProvider;
|
|
72
|
+
let meterProvider;
|
|
34
73
|
try {
|
|
74
|
+
(0, redaction_js_1.setActiveRedactionConfig)(redactionConfig);
|
|
35
75
|
const resource = (0, resource_js_1.createFoamResource)(options.name, options.environment, options.version, options.additionalResourceAttributes);
|
|
36
|
-
// pcga11:
|
|
37
|
-
//
|
|
38
|
-
//
|
|
76
|
+
// pcga11: The context manager is what makes the "current" context (active span, baggage)
|
|
77
|
+
// follow execution across await/callback boundaries, via Node's AsyncLocalStorage. Without
|
|
78
|
+
// one registered, spans would not nest and trace/log correlation would break. Registration
|
|
79
|
+
// is first-wins: if another SDK already installed one, we keep theirs and disable ours.
|
|
39
80
|
const contextManager = new context_async_hooks_1.AsyncLocalStorageContextManager();
|
|
40
81
|
contextManager.enable();
|
|
41
82
|
if (!api_1.context.setGlobalContextManager(contextManager)) {
|
|
@@ -46,20 +87,23 @@ enabled = true, ...options }) {
|
|
|
46
87
|
new core_1.W3CTraceContextPropagator(),
|
|
47
88
|
new core_1.W3CBaggagePropagator(),
|
|
48
89
|
],
|
|
49
|
-
})));
|
|
90
|
+
})) ? state_js_1.SignalSources.global : state_js_1.SignalSources.none);
|
|
50
91
|
// pcga11: The @opentelemetry/api setters return false when another SDK already owns a slot (registration is first-wins). So we try to register our own and if it fails or already taken, nothing happens.
|
|
51
92
|
// TODO(pcga11): Implement a way to handle piggy backing the context and propagation from another SDK.
|
|
52
|
-
|
|
93
|
+
tracerProvider = new sdk_trace_base_1.BasicTracerProvider({
|
|
53
94
|
resource,
|
|
54
95
|
sampler: new sdk_trace_base_1.ParentBasedSampler({
|
|
55
96
|
root: new sdk_trace_base_1.TraceIdRatioBasedSampler(sampleRate),
|
|
56
97
|
}),
|
|
57
98
|
spanProcessors: [
|
|
58
|
-
new sdk_trace_base_1.BatchSpanProcessor(new exporters_js_1.FoamTraceExporter(options.token
|
|
99
|
+
new sdk_trace_base_1.BatchSpanProcessor(new exporters_js_1.FoamTraceExporter(options.token, undefined, {
|
|
100
|
+
redaction: redactionConfig,
|
|
101
|
+
beforeSend,
|
|
102
|
+
})),
|
|
59
103
|
...(options.additionalSpanProcessors ?? []),
|
|
60
104
|
],
|
|
61
105
|
});
|
|
62
|
-
|
|
106
|
+
meterProvider = new sdk_metrics_1.MeterProvider({
|
|
63
107
|
resource,
|
|
64
108
|
readers: [
|
|
65
109
|
new sdk_metrics_1.PeriodicExportingMetricReader({
|
|
@@ -72,49 +116,53 @@ enabled = true, ...options }) {
|
|
|
72
116
|
resource,
|
|
73
117
|
processors: [
|
|
74
118
|
new sdk_logs_1.BatchLogRecordProcessor({
|
|
75
|
-
exporter: new exporters_js_1.FoamLogExporter(options.token
|
|
119
|
+
exporter: new exporters_js_1.FoamLogExporter(options.token, undefined, {
|
|
120
|
+
redaction: redactionConfig,
|
|
121
|
+
beforeSend,
|
|
122
|
+
}),
|
|
76
123
|
}),
|
|
77
124
|
...(options.additionalLogRecordProcessors ?? []),
|
|
78
125
|
],
|
|
79
126
|
});
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
(0, state_js_1.setSignal)(state_js_1.Signals.
|
|
83
|
-
(0, state_js_1.setSignal)(state_js_1.Signals.
|
|
127
|
+
(0, logs_js_1.setFoamLoggerProvider)(loggerProvider);
|
|
128
|
+
// pcga11: trace/metrics/propagation setters return false when the slot is taken by another SDK. The logs setter returns whichever provider ended up registered. We use that to record the source: "global" when Foam owns the slot, "none" when another SDK does. Logs fall back to "local" because the Foam-only provider behind log() exists either way.
|
|
129
|
+
(0, state_js_1.setSignal)(state_js_1.Signals.traces, api_1.trace.setGlobalTracerProvider(tracerProvider) ? state_js_1.SignalSources.global : state_js_1.SignalSources.none);
|
|
130
|
+
(0, state_js_1.setSignal)(state_js_1.Signals.metrics, api_1.metrics.setGlobalMeterProvider(meterProvider) ? state_js_1.SignalSources.global : state_js_1.SignalSources.none);
|
|
131
|
+
(0, state_js_1.setSignal)(state_js_1.Signals.logs, api_logs_1.logs.setGlobalLoggerProvider(loggerProvider) === loggerProvider ? state_js_1.SignalSources.global : state_js_1.SignalSources.local);
|
|
84
132
|
const instrumentations = (0, instrumentations_js_1.buildInstrumentations)(networkCapture, disableLogSending, options.ignoredOutboundHosts, options.additionalInstrumentations ?? []);
|
|
133
|
+
// pcga11: registerInstrumentations turns on instrumentation "hooks". OpenTelemetry wraps supported libraries when the app imports them.
|
|
85
134
|
(0, instrumentation_1.registerInstrumentations)({ instrumentations });
|
|
86
135
|
(0, state_js_1.setInstrumentations)(instrumentations.map(instrumentations_js_1.instrumentationName));
|
|
87
|
-
// pcga11: Signals are flushed, remaining batches are sent, and exporters are stopped before the process exits.
|
|
88
136
|
process.once("beforeExit", () => {
|
|
89
137
|
void Promise.all([
|
|
90
|
-
(0, state_js_1.getSignal)(state_js_1.Signals.traces) ? tracerProvider
|
|
91
|
-
(0, state_js_1.getSignal)(state_js_1.Signals.metrics) ? meterProvider
|
|
92
|
-
|
|
138
|
+
(0, state_js_1.getSignal)(state_js_1.Signals.traces) === state_js_1.SignalSources.global ? tracerProvider?.shutdown() : undefined,
|
|
139
|
+
(0, state_js_1.getSignal)(state_js_1.Signals.metrics) === state_js_1.SignalSources.global ? meterProvider?.shutdown() : undefined,
|
|
140
|
+
loggerProvider.shutdown(),
|
|
93
141
|
]).catch(() => undefined);
|
|
94
142
|
});
|
|
95
143
|
(0, state_js_1.setInitialized)(true);
|
|
96
|
-
(0,
|
|
97
|
-
void (0, diagnostics_js_1.sendStateDiagnosticLog)({
|
|
98
|
-
name: options.name,
|
|
99
|
-
environment: options.environment,
|
|
144
|
+
(0, report_js_1.report)({
|
|
100
145
|
token: options.token,
|
|
101
|
-
tier: "internal",
|
|
102
146
|
severity: api_logs_1.SeverityNumber.INFO,
|
|
147
|
+
message: 'SDK initialized',
|
|
103
148
|
});
|
|
104
149
|
}
|
|
105
150
|
catch (error) {
|
|
106
151
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
107
152
|
for (const signal of [state_js_1.Signals.traces, state_js_1.Signals.metrics, state_js_1.Signals.logs, state_js_1.Signals.baggage]) {
|
|
108
|
-
(0, state_js_1.setSignal)(signal,
|
|
153
|
+
(0, state_js_1.setSignal)(signal, state_js_1.SignalSources.none);
|
|
109
154
|
}
|
|
155
|
+
void Promise.all([
|
|
156
|
+
tracerProvider?.shutdown(),
|
|
157
|
+
meterProvider?.shutdown(),
|
|
158
|
+
(0, logs_js_1.getFoamLoggerProvider)()?.shutdown(),
|
|
159
|
+
]).catch(() => undefined);
|
|
160
|
+
(0, logs_js_1.clearFoamLoggerProvider)();
|
|
110
161
|
(0, state_js_1.setInitialized)(false);
|
|
111
|
-
(0,
|
|
112
|
-
void (0, diagnostics_js_1.sendStateDiagnosticLog)({
|
|
113
|
-
name: options.name,
|
|
114
|
-
environment: options.environment,
|
|
162
|
+
(0, report_js_1.report)({
|
|
115
163
|
token: options.token,
|
|
116
|
-
tier: "internal",
|
|
117
164
|
severity: api_logs_1.SeverityNumber.ERROR,
|
|
165
|
+
message: `SDK initialization failed: ${errorMessage}`,
|
|
118
166
|
error: errorMessage,
|
|
119
167
|
});
|
|
120
168
|
}
|
package/dist/instrumentations.js
CHANGED
|
@@ -5,11 +5,33 @@ exports.buildInstrumentations = buildInstrumentations;
|
|
|
5
5
|
const auto_instrumentations_node_1 = require("@opentelemetry/auto-instrumentations-node");
|
|
6
6
|
const instrumentation_console_1 = require("@opentelemetry/instrumentation-console");
|
|
7
7
|
const index_js_1 = require("./network-capture/index.js");
|
|
8
|
+
const endpoint_js_1 = require("./endpoint.js");
|
|
8
9
|
const state_js_1 = require("./state.js");
|
|
9
10
|
const constants_js_1 = require("./constants.js");
|
|
10
11
|
function buildInstrumentations(networkCapture = "basic", disableLogSending = false, ignoredOutboundHosts, additionalInstrumentations) {
|
|
11
|
-
|
|
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;
|
|
12
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
|
+
});
|
|
13
35
|
const bodyHooks = networkCapture === "advanced"
|
|
14
36
|
? (0, index_js_1.createNetworkCaptureHooks)(bodyCaptureEnabled)
|
|
15
37
|
: undefined;
|
|
@@ -36,8 +58,7 @@ function buildInstrumentations(networkCapture = "basic", disableLogSending = fal
|
|
|
36
58
|
}
|
|
37
59
|
: {}),
|
|
38
60
|
...(bodyHooks ?? {}),
|
|
39
|
-
ignoreOutgoingRequestHook:
|
|
40
|
-
(ignoredOutboundHosts ?? []).includes(request.hostname)),
|
|
61
|
+
ignoreOutgoingRequestHook: ignoreHttpOutgoingRequest,
|
|
41
62
|
},
|
|
42
63
|
"@opentelemetry/instrumentation-openai": {
|
|
43
64
|
captureMessageContent: networkCapture === "advanced",
|
|
@@ -55,6 +76,7 @@ function buildInstrumentations(networkCapture = "basic", disableLogSending = fal
|
|
|
55
76
|
}
|
|
56
77
|
: {}),
|
|
57
78
|
...(advancedUndiciHooks ?? {}),
|
|
79
|
+
ignoreRequestHook: ignoreUndiciRequest,
|
|
58
80
|
},
|
|
59
81
|
"@opentelemetry/instrumentation-winston": {
|
|
60
82
|
disableLogSending: disableLogSending === true,
|
package/dist/logs.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Attributes } from "@opentelemetry/api";
|
|
2
|
+
import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
3
|
+
import { LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
4
|
+
import { type FoamExporterOptions } from "./exporters.js";
|
|
5
|
+
export declare function setFoamLoggerProvider(provider: LoggerProvider): void;
|
|
6
|
+
export declare function getFoamLoggerProvider(): LoggerProvider | undefined;
|
|
7
|
+
export declare function clearFoamLoggerProvider(): void;
|
|
8
|
+
export declare function ensureFoamLoggerProvider(name: string, environment: string, token: string, stamp?: Attributes, options?: FoamExporterOptions): LoggerProvider;
|
|
9
|
+
export declare const log: (body: string, severityNumber?: SeverityNumber, attributes?: Attributes) => void;
|
|
10
|
+
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, options) {
|
|
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, options),
|
|
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;
|
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.BodyCollector = void 0;
|
|
4
37
|
exports.setHeaderAttributes = setHeaderAttributes;
|
|
@@ -6,9 +39,10 @@ const api_1 = require("@opentelemetry/api");
|
|
|
6
39
|
const api_logs_1 = require("@opentelemetry/api-logs");
|
|
7
40
|
const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
|
|
8
41
|
const node_crypto_1 = require("node:crypto");
|
|
9
|
-
const
|
|
42
|
+
const zlib = __importStar(require("node:zlib"));
|
|
10
43
|
const constants_js_1 = require("../constants.js");
|
|
11
44
|
const utils_js_1 = require("../utils.js");
|
|
45
|
+
const redact_js_1 = require("./redact.js");
|
|
12
46
|
const ALLOWED_HEADERS = new Set(constants_js_1.SAFE_NETWORK_HEADERS);
|
|
13
47
|
const ATTR = {
|
|
14
48
|
request: {
|
|
@@ -48,12 +82,14 @@ function setHeaderAttributes(span, direction, headers) {
|
|
|
48
82
|
if (Object.keys(attributes).length > 0)
|
|
49
83
|
span.setAttributes(attributes);
|
|
50
84
|
}
|
|
85
|
+
function stringEncoding(encoding) {
|
|
86
|
+
return typeof encoding === "string" && Buffer.isEncoding(encoding)
|
|
87
|
+
? encoding
|
|
88
|
+
: "utf8";
|
|
89
|
+
}
|
|
51
90
|
function copyBody(value, encoding) {
|
|
52
91
|
if (typeof value === "string") {
|
|
53
|
-
|
|
54
|
-
? encoding
|
|
55
|
-
: "utf8";
|
|
56
|
-
return Buffer.from(value, enc);
|
|
92
|
+
return Buffer.from(value, stringEncoding(encoding));
|
|
57
93
|
}
|
|
58
94
|
if (Buffer.isBuffer(value))
|
|
59
95
|
return Buffer.from(value);
|
|
@@ -62,30 +98,58 @@ function copyBody(value, encoding) {
|
|
|
62
98
|
}
|
|
63
99
|
return undefined;
|
|
64
100
|
}
|
|
101
|
+
// Byte length of a body chunk without copying it; undefined for non-body values.
|
|
102
|
+
function bodyByteLength(value, encoding) {
|
|
103
|
+
if (typeof value === "string") {
|
|
104
|
+
return Buffer.byteLength(value, stringEncoding(encoding));
|
|
105
|
+
}
|
|
106
|
+
if (Buffer.isBuffer(value) || ArrayBuffer.isView(value)) {
|
|
107
|
+
return value.byteLength;
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
65
111
|
function decodeBody(bytes, contentEncoding) {
|
|
66
112
|
if (contentEncoding === undefined)
|
|
67
113
|
return bytes;
|
|
68
|
-
const
|
|
69
|
-
|
|
114
|
+
const encodings = headerValues(contentEncoding)
|
|
115
|
+
.flatMap((value) => value.split(","))
|
|
116
|
+
.map((value) => value.trim().toLowerCase())
|
|
117
|
+
.filter((value) => value && value !== "identity");
|
|
118
|
+
if (encodings.length === 0)
|
|
70
119
|
return bytes;
|
|
120
|
+
const limits = { maxOutputLength: constants_js_1.NETWORK_BODY_MAX_DECODE_BYTES };
|
|
121
|
+
let decoded = bytes;
|
|
71
122
|
try {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
return (0, node_zlib_1.inflateSync)(bytes);
|
|
123
|
+
for (const encoding of encodings.reverse()) {
|
|
124
|
+
if (encoding === "gzip" || encoding === "x-gzip") {
|
|
125
|
+
decoded = zlib.gunzipSync(decoded, limits);
|
|
77
126
|
}
|
|
78
|
-
|
|
79
|
-
|
|
127
|
+
else if (encoding === "deflate") {
|
|
128
|
+
try {
|
|
129
|
+
decoded = zlib.inflateSync(decoded, limits);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
decoded = zlib.unzipSync(decoded, limits);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (encoding === "br") {
|
|
136
|
+
decoded = zlib.brotliDecompressSync(decoded, limits);
|
|
137
|
+
}
|
|
138
|
+
else if (encoding === "zstd") {
|
|
139
|
+
const decompress = zlib.zstdDecompressSync;
|
|
140
|
+
if (!decompress)
|
|
141
|
+
return undefined;
|
|
142
|
+
decoded = decompress(decoded, limits);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
return undefined;
|
|
80
146
|
}
|
|
81
147
|
}
|
|
82
|
-
|
|
83
|
-
return (0, node_zlib_1.brotliDecompressSync)(bytes);
|
|
148
|
+
return decoded;
|
|
84
149
|
}
|
|
85
150
|
catch {
|
|
86
151
|
return undefined;
|
|
87
152
|
}
|
|
88
|
-
return undefined;
|
|
89
153
|
}
|
|
90
154
|
function textEncoding(contentType) {
|
|
91
155
|
const raw = headerValues(contentType)[0];
|
|
@@ -96,21 +160,21 @@ function textEncoding(contentType) {
|
|
|
96
160
|
const charset = parts
|
|
97
161
|
.find((part) => part.toLowerCase().startsWith("charset="))
|
|
98
162
|
?.slice("charset=".length)
|
|
163
|
+
.trim()
|
|
164
|
+
.replace(/^["']|["']$/g, "")
|
|
99
165
|
.toLowerCase();
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
media.endsWith("+json") ||
|
|
103
|
-
media.endsWith("/xml") ||
|
|
104
|
-
media.endsWith("+xml") ||
|
|
105
|
-
media.endsWith("/yaml") ||
|
|
106
|
-
media.endsWith("+yaml") ||
|
|
107
|
-
media === "application/x-www-form-urlencoded" ||
|
|
108
|
-
Boolean(charset);
|
|
109
|
-
if (!text)
|
|
166
|
+
const isText = (0, redact_js_1.isTextualBodyMediaType)(media) || Boolean(charset);
|
|
167
|
+
if (!isText)
|
|
110
168
|
return undefined;
|
|
111
|
-
|
|
169
|
+
if (charset && Buffer.isEncoding(charset))
|
|
170
|
+
return charset;
|
|
171
|
+
if (charset === "iso-8859-1" || charset === "windows-1252")
|
|
172
|
+
return "latin1";
|
|
173
|
+
if (media.startsWith("multipart/"))
|
|
174
|
+
return "latin1";
|
|
175
|
+
return "utf8";
|
|
112
176
|
}
|
|
113
|
-
|
|
177
|
+
// Last index such that `buf.subarray(0, end)` is complete UTF-8, capped at `max`.
|
|
114
178
|
function utf8End(buf, max) {
|
|
115
179
|
const end = Math.min(max, buf.byteLength);
|
|
116
180
|
if (end === 0)
|
|
@@ -132,20 +196,17 @@ function utf8End(buf, max) {
|
|
|
132
196
|
: Number.POSITIVE_INFINITY;
|
|
133
197
|
return end - (leadAt - 1) >= needed ? end : leadAt - 1;
|
|
134
198
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
? utf8End(limited, limited.byteLength)
|
|
147
|
-
: limited.byteLength;
|
|
148
|
-
return limited.subarray(0, end).toString(encoding);
|
|
199
|
+
function cappedBody(buf) {
|
|
200
|
+
if (buf.byteLength <= constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES)
|
|
201
|
+
return buf;
|
|
202
|
+
return buf.subarray(0, constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES);
|
|
203
|
+
}
|
|
204
|
+
function decodeText(decoded, encoding) {
|
|
205
|
+
const limited = cappedBody(decoded);
|
|
206
|
+
if (encoding === "utf8" || encoding === "utf-8") {
|
|
207
|
+
return limited.subarray(0, utf8End(limited, limited.byteLength)).toString(encoding);
|
|
208
|
+
}
|
|
209
|
+
return limited.toString(encoding);
|
|
149
210
|
}
|
|
150
211
|
class BodyCollector {
|
|
151
212
|
span;
|
|
@@ -195,17 +256,28 @@ class BodyCollector {
|
|
|
195
256
|
observe(value, encoding) {
|
|
196
257
|
if (this.finalized)
|
|
197
258
|
return;
|
|
259
|
+
if (this.capturedBytes >= constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES) {
|
|
260
|
+
// Past the cap every chunk is discarded; only its length is needed
|
|
261
|
+
// (for the observed size), so skip the per-chunk copy that would
|
|
262
|
+
// otherwise double the allocation traffic of large transfers.
|
|
263
|
+
const length = bodyByteLength(value, encoding);
|
|
264
|
+
if (length === undefined)
|
|
265
|
+
return;
|
|
266
|
+
this.deadline.refresh();
|
|
267
|
+
this.observedBytes += length;
|
|
268
|
+
this.truncated = true;
|
|
269
|
+
this.span.setAttributes({
|
|
270
|
+
[this.attr.size]: this.observedBytes,
|
|
271
|
+
[this.attr.truncated]: true,
|
|
272
|
+
});
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
198
275
|
const bytes = copyBody(value, encoding);
|
|
199
276
|
if (!bytes)
|
|
200
277
|
return;
|
|
201
278
|
this.deadline.refresh();
|
|
202
279
|
this.observedBytes += bytes.byteLength;
|
|
203
280
|
this.span.setAttribute(this.attr.size, this.observedBytes);
|
|
204
|
-
if (this.capturedBytes >= constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES) {
|
|
205
|
-
this.truncated = true;
|
|
206
|
-
this.span.setAttribute(this.attr.truncated, true);
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
281
|
const room = constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES - this.capturedBytes;
|
|
210
282
|
const kept = bytes.byteLength > room ? bytes.subarray(0, room) : bytes;
|
|
211
283
|
this.chunks.push(kept);
|
|
@@ -236,35 +308,54 @@ class BodyCollector {
|
|
|
236
308
|
});
|
|
237
309
|
}
|
|
238
310
|
emit(buffers, headers, complete) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
311
|
+
let outcome;
|
|
312
|
+
if (this.observedBytes === 0)
|
|
313
|
+
outcome = "empty";
|
|
314
|
+
else if (complete)
|
|
315
|
+
outcome = "captured";
|
|
316
|
+
else
|
|
317
|
+
outcome = "incomplete";
|
|
244
318
|
if (outcome === "empty")
|
|
245
319
|
return;
|
|
246
|
-
|
|
320
|
+
let wire;
|
|
321
|
+
if (buffers.length === 0)
|
|
322
|
+
wire = undefined;
|
|
323
|
+
else if (buffers.length === 1)
|
|
324
|
+
wire = buffers[0];
|
|
325
|
+
else
|
|
326
|
+
wire = Buffer.concat(buffers);
|
|
327
|
+
// Span attributes cannot hold bytes (OTel AttributeValue). Text goes on
|
|
328
|
+
// the span; binary uses the same `http.*.body.content` name on the event.
|
|
247
329
|
let binaryContent;
|
|
248
|
-
|
|
330
|
+
let chunkSource = wire;
|
|
331
|
+
let chunkSourceIsDecodedText = false;
|
|
332
|
+
if (wire !== undefined) {
|
|
249
333
|
const decoded = decodeBody(wire, headers["content-encoding"]);
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
? spanBodyContent(decoded, headers["content-type"])
|
|
253
|
-
: undefined;
|
|
254
|
-
if (text !== undefined) {
|
|
255
|
-
this.span.setAttribute(this.attr.content, text);
|
|
334
|
+
if (decoded === undefined) {
|
|
335
|
+
binaryContent = Uint8Array.from(cappedBody(wire));
|
|
256
336
|
}
|
|
257
337
|
else {
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
338
|
+
const encoding = textEncoding(headers["content-type"]);
|
|
339
|
+
if (encoding !== undefined) {
|
|
340
|
+
const redacted = (0, redact_js_1.redactBodyText)(decodeText(decoded, encoding), headers["content-type"]);
|
|
341
|
+
this.span.setAttribute(this.attr.content, redacted.text);
|
|
342
|
+
if (redacted.changed) {
|
|
343
|
+
// pcga11: Chunk events must not re-ship bytes the span content
|
|
344
|
+
// redacted, so the (possibly compressed) wire bytes are replaced
|
|
345
|
+
// with the redacted text.
|
|
346
|
+
chunkSource = Buffer.from(redacted.text, encoding);
|
|
347
|
+
chunkSourceIsDecodedText = true;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
binaryContent = Uint8Array.from(cappedBody(decoded));
|
|
352
|
+
}
|
|
262
353
|
}
|
|
263
354
|
}
|
|
264
355
|
const chunks = [];
|
|
265
|
-
if (
|
|
266
|
-
for (let i = 0; i <
|
|
267
|
-
chunks.push(
|
|
356
|
+
if (chunkSource) {
|
|
357
|
+
for (let i = 0; i < chunkSource.byteLength; i += constants_js_1.NETWORK_BODY_CHUNK_BYTES) {
|
|
358
|
+
chunks.push(chunkSource.subarray(i, i + constants_js_1.NETWORK_BODY_CHUNK_BYTES));
|
|
268
359
|
}
|
|
269
360
|
}
|
|
270
361
|
const common = {
|
|
@@ -275,10 +366,18 @@ class BodyCollector {
|
|
|
275
366
|
[constants_js_1.ATTR_FOAM_HTTP_BODY_TRUNCATED]: this.truncated,
|
|
276
367
|
[constants_js_1.ATTR_FOAM_HTTP_BODY_OUTCOME]: outcome,
|
|
277
368
|
[this.attr.size]: this.observedBytes,
|
|
278
|
-
[constants_js_1.ATTR_FOAM_HTTP_BODY_CAPTURED_BYTES]:
|
|
369
|
+
[constants_js_1.ATTR_FOAM_HTTP_BODY_CAPTURED_BYTES]: chunkSourceIsDecodedText
|
|
370
|
+
? (chunkSource?.byteLength ?? 0)
|
|
371
|
+
: this.capturedBytes,
|
|
279
372
|
[constants_js_1.ATTR_FOAM_HTTP_BODY_CHUNK_COUNT]: chunks.length,
|
|
280
373
|
};
|
|
281
374
|
for (const name of ["content-type", "content-encoding"]) {
|
|
375
|
+
if (name === "content-encoding" &&
|
|
376
|
+
chunkSourceIsDecodedText &&
|
|
377
|
+
headers[name] !== undefined) {
|
|
378
|
+
common[this.attr.header(name)] = ["identity"];
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
282
381
|
if (headers[name] !== undefined) {
|
|
283
382
|
common[this.attr.header(name)] = headerValues(headers[name]);
|
|
284
383
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { HttpRequestCustomAttributeFunction, HttpResponseCustomAttributeFunction } from "@opentelemetry/instrumentation-http";
|
|
2
|
-
/** Builds OTel HTTP request/response hooks that record headers as span attributes and stream bodies as log events. */
|
|
3
2
|
export declare function createNetworkCaptureHooks(bodyCaptureEnabled?: boolean): {
|
|
4
3
|
requestHook: HttpRequestCustomAttributeFunction;
|
|
5
4
|
responseHook: HttpResponseCustomAttributeFunction;
|