@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.
Files changed (44) hide show
  1. package/README.md +393 -214
  2. package/dist/before-send.d.ts +16 -0
  3. package/dist/before-send.js +41 -0
  4. package/dist/constants.d.ts +10 -8
  5. package/dist/constants.js +19 -6
  6. package/dist/endpoint.d.ts +2 -0
  7. package/dist/endpoint.js +11 -0
  8. package/dist/exporters.d.ts +11 -3
  9. package/dist/exporters.js +127 -53
  10. package/dist/index.d.ts +7 -2
  11. package/dist/index.js +8 -1
  12. package/dist/ingest.d.ts +8 -2
  13. package/dist/ingest.js +28 -16
  14. package/dist/init.d.ts +7 -3
  15. package/dist/init.js +85 -37
  16. package/dist/instrumentations.js +25 -3
  17. package/dist/logs.d.ts +10 -0
  18. package/dist/logs.js +61 -0
  19. package/dist/network-capture/collector.js +168 -69
  20. package/dist/network-capture/http.d.ts +0 -1
  21. package/dist/network-capture/http.js +23 -25
  22. package/dist/network-capture/index.js +10 -0
  23. package/dist/network-capture/redact.d.ts +9 -0
  24. package/dist/network-capture/redact.js +401 -0
  25. package/dist/network-capture/undici.d.ts +0 -4
  26. package/dist/network-capture/undici.js +55 -22
  27. package/dist/otlp.d.ts +8 -0
  28. package/dist/otlp.js +46 -0
  29. package/dist/propagation.d.ts +1 -1
  30. package/dist/propagation.js +6 -4
  31. package/dist/redaction-keys.d.ts +3 -0
  32. package/dist/redaction-keys.js +421 -0
  33. package/dist/redaction.d.ts +24 -0
  34. package/dist/redaction.js +270 -0
  35. package/dist/report.d.ts +9 -0
  36. package/dist/report.js +56 -0
  37. package/dist/state.d.ts +11 -4
  38. package/dist/state.js +26 -9
  39. package/dist/traces.d.ts +1 -0
  40. package/dist/traces.js +21 -0
  41. package/dist/utils.js +1 -1
  42. package/package.json +1 -1
  43. package/dist/diagnostics.d.ts +0 -13
  44. 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 constants_js_1 = require("./constants.js");
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): redact and beforeSend as well as conflicts handling not yet implemented.
19
- function init({ sampleRate = 1, disableLogSending = false, networkCapture = "basic", // pcga11: We do not default to advanced because this is something our clients approve to opt in during onboarding.
20
- enabled = true, ...options }) {
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, diagnostics_js_1.info)("Foam SDK for Node.js is disabled");
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, diagnostics_js_1.warn)("Foam SDK for Node.js is already initialized");
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
- (0, diagnostics_js_1.configureDiagnostics)();
30
- if (!options.name?.trim() || !options.environment?.trim() || !options.token) {
31
- (0, diagnostics_js_1.error)(`${constants_js_1.FOAM_IDENTIFIER_NAME}: name, environment, and token are required`);
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: Register our context manager and propagator; if another SDK already
37
- // registered one, keep theirs
38
- // TODO(pcga11): Implement a way to handle piggy backing the context manager and propagator from another SDK.
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
- const tracerProvider = new sdk_trace_base_1.BasicTracerProvider({
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
- const meterProvider = new sdk_metrics_1.MeterProvider({
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
- // 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 mark ownership.
81
- (0, state_js_1.setSignal)(state_js_1.Signals.traces, api_1.trace.setGlobalTracerProvider(tracerProvider));
82
- (0, state_js_1.setSignal)(state_js_1.Signals.metrics, api_1.metrics.setGlobalMeterProvider(meterProvider));
83
- (0, state_js_1.setSignal)(state_js_1.Signals.logs, api_logs_1.logs.setGlobalLoggerProvider(loggerProvider) === loggerProvider);
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.shutdown() : undefined,
91
- (0, state_js_1.getSignal)(state_js_1.Signals.metrics) ? meterProvider.shutdown() : undefined,
92
- (0, state_js_1.getSignal)(state_js_1.Signals.logs) ? loggerProvider.shutdown() : undefined,
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, diagnostics_js_1.info)("Foam SDK for Node.js initialized");
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, false);
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, diagnostics_js_1.error)(`Foam SDK for Node.js initialization failed: ${errorMessage}`);
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
  }
@@ -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
- const bodyCaptureEnabled = (0, state_js_1.getSignal)(state_js_1.Signals.logs);
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: ((request) => typeof request.hostname === "string" &&
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 node_zlib_1 = require("node:zlib");
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
- const enc = typeof encoding === "string" && Buffer.isEncoding(encoding)
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 encoding = headerValues(contentEncoding)[0]?.toLowerCase().trim();
69
- if (!encoding || encoding === "identity")
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
- if (encoding === "gzip" || encoding === "x-gzip")
73
- return (0, node_zlib_1.gunzipSync)(bytes);
74
- if (encoding === "deflate") {
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
- catch {
79
- return (0, node_zlib_1.unzipSync)(bytes);
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
- if (encoding === "br")
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 text = media.startsWith("text/") ||
101
- media.endsWith("/json") ||
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
- return charset && Buffer.isEncoding(charset) ? charset : "utf8";
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
- /** Last index such that `buf.subarray(0, end)` is complete UTF-8, capped at `max`. */
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
- // pcga11: Span attributes cannot carry byte arrays (OTel AttributeValue).
136
- // Text goes on the span; binary uses the same `http.*.body.content` name on
137
- // the correlated event, where log attributes accept Uint8Array.
138
- function spanBodyContent(decoded, contentType) {
139
- const encoding = textEncoding(contentType);
140
- if (encoding === undefined)
141
- return undefined;
142
- const limited = decoded.byteLength > constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES
143
- ? decoded.subarray(0, constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES)
144
- : decoded;
145
- const end = encoding === "utf8" || encoding === "utf-8"
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
- const outcome = this.observedBytes === 0
240
- ? "empty"
241
- : complete
242
- ? "captured"
243
- : "incomplete";
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
- const wire = buffers.length <= 1 ? buffers[0] : Buffer.concat(buffers);
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
- if (wire) {
330
+ let chunkSource = wire;
331
+ let chunkSourceIsDecodedText = false;
332
+ if (wire !== undefined) {
249
333
  const decoded = decodeBody(wire, headers["content-encoding"]);
250
- const source = decoded ?? wire;
251
- const text = decoded
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 limited = source.byteLength > constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES
259
- ? source.subarray(0, constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES)
260
- : source;
261
- binaryContent = Uint8Array.from(limited);
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 (wire) {
266
- for (let i = 0; i < wire.byteLength; i += constants_js_1.NETWORK_BODY_CHUNK_BYTES) {
267
- chunks.push(wire.subarray(i, i + constants_js_1.NETWORK_BODY_CHUNK_BYTES));
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]: this.capturedBytes,
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;