@foam-ai/node 0.1.0-alpha.1 → 0.1.0-alpha.10

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 (64) hide show
  1. package/README.md +649 -0
  2. package/dist/before-send.d.ts +16 -0
  3. package/dist/before-send.js +41 -0
  4. package/dist/constants.d.ts +44 -0
  5. package/dist/constants.js +67 -0
  6. package/dist/endpoint.d.ts +2 -0
  7. package/dist/endpoint.js +11 -0
  8. package/dist/exporters.d.ts +33 -0
  9. package/dist/exporters.js +179 -0
  10. package/dist/index.d.ts +10 -0
  11. package/dist/index.js +28 -0
  12. package/dist/ingest.d.ts +12 -0
  13. package/dist/ingest.js +70 -0
  14. package/dist/init.d.ts +26 -0
  15. package/dist/init.js +169 -0
  16. package/dist/instrumentations.d.ts +4 -0
  17. package/dist/instrumentations.js +120 -0
  18. package/dist/library-versions.d.ts +1 -0
  19. package/dist/library-versions.js +59 -0
  20. package/dist/logs.d.ts +10 -0
  21. package/dist/logs.js +61 -0
  22. package/dist/metrics.d.ts +5 -0
  23. package/dist/metrics.js +29 -0
  24. package/dist/network-capture/collector.d.ts +27 -0
  25. package/dist/network-capture/collector.js +411 -0
  26. package/dist/network-capture/http.d.ts +5 -0
  27. package/dist/network-capture/http.js +221 -0
  28. package/dist/network-capture/index.d.ts +3 -0
  29. package/dist/network-capture/index.js +20 -0
  30. package/dist/network-capture/redact.d.ts +9 -0
  31. package/dist/network-capture/redact.js +401 -0
  32. package/dist/network-capture/support.d.ts +2 -0
  33. package/dist/network-capture/support.js +38 -0
  34. package/dist/network-capture/undici.d.ts +5 -0
  35. package/dist/network-capture/undici.js +177 -0
  36. package/dist/otlp.d.ts +8 -0
  37. package/dist/otlp.js +46 -0
  38. package/dist/propagation.d.ts +5 -0
  39. package/dist/propagation.js +47 -0
  40. package/dist/redaction-keys.d.ts +3 -0
  41. package/dist/redaction-keys.js +421 -0
  42. package/dist/redaction.d.ts +24 -0
  43. package/dist/redaction.js +270 -0
  44. package/dist/report.d.ts +9 -0
  45. package/dist/report.js +56 -0
  46. package/dist/resource.d.ts +3 -0
  47. package/dist/resource.js +24 -0
  48. package/dist/state.d.ts +26 -0
  49. package/dist/state.js +61 -0
  50. package/dist/traces.d.ts +1 -0
  51. package/dist/traces.js +21 -0
  52. package/dist/utils.d.ts +4 -0
  53. package/dist/utils.js +20 -0
  54. package/package.json +47 -19
  55. package/dist/node/src/capture-exception.d.ts +0 -9
  56. package/dist/node/src/capture-exception.js +0 -31
  57. package/dist/node/src/index.d.ts +0 -9
  58. package/dist/node/src/index.js +0 -12
  59. package/dist/node/src/init.d.ts +0 -27
  60. package/dist/node/src/init.js +0 -203
  61. package/dist/shared/constants.d.ts +0 -1
  62. package/dist/shared/constants.js +0 -4
  63. package/dist/shared/util.d.ts +0 -9
  64. package/dist/shared/util.js +0 -21
package/README.md ADDED
@@ -0,0 +1,649 @@
1
+ # Foam OpenTelemetry SDK for Node.js
2
+
3
+ `@foam-ai/node` is Foam's OpenTelemetry distribution for Node.js.
4
+
5
+ ## Requirements
6
+
7
+ - Node.js `^18.19.0 || >=20.6.0`, matching the supported engine range of
8
+ `@opentelemetry/auto-instrumentations-node`
9
+ - Foam ingest token, stored as `FOAM_OTEL_TOKEN`
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ npm install @foam-ai/node
15
+ ```
16
+
17
+ ## Initialize
18
+
19
+ Call `init()` before your app loads so OpenTelemetry can patch libraries as
20
+ they import. Put `init()` in its own file and preload it with `--import` or
21
+ `--require` (below).
22
+
23
+ Set `enabled: false` to turn Foam off.
24
+
25
+ Store the ingest token as `FOAM_OTEL_TOKEN` and pass
26
+ `process.env.FOAM_OTEL_TOKEN` to `init()`.
27
+
28
+ ### TypeScript
29
+
30
+ Create `instrumentation.ts`:
31
+
32
+ ```ts
33
+ import { init } from "@foam-ai/node";
34
+
35
+ init({
36
+ name: "checkout-api",
37
+ environment: "production",
38
+ enabled: true,
39
+ token: process.env.FOAM_OTEL_TOKEN,
40
+ version: "1.0.0",
41
+ });
42
+ ```
43
+
44
+ Compile it with the app. If TypeScript emits ESM, preload with `--import`
45
+ (same rule as ESM below):
46
+
47
+ ```sh
48
+ node --import ./dist/instrumentation.js ./dist/app.js
49
+ ```
50
+
51
+ If TypeScript emits CommonJS, preload with `--require`:
52
+
53
+ ```sh
54
+ node --require ./dist/instrumentation.js ./dist/app.js
55
+ ```
56
+
57
+ To run TypeScript without compiling first:
58
+
59
+ ```sh
60
+ node --import tsx --import ./instrumentation.ts ./src/app.ts
61
+ ```
62
+
63
+ ### ESM
64
+
65
+ Create `instrumentation.mjs` with the same `init()` call.
66
+
67
+ `import` lines always run first, even if you write `init()` above them.
68
+ Don't import your app from that file. Use `--import`, or
69
+ `await import("./app.js")` after `init()`:
70
+
71
+ ```sh
72
+ node --import ./instrumentation.mjs ./app.js
73
+ ```
74
+
75
+ ### CommonJS
76
+
77
+ Create `instrumentation.cjs`. `require()` runs in order, so `init()` then
78
+ `require("./app")` in this file is fine. `--require` is still the better
79
+ preload:
80
+
81
+ ```js
82
+ const { init } = require("@foam-ai/node");
83
+
84
+ init({
85
+ name: "checkout-api",
86
+ environment: "production",
87
+ enabled: true,
88
+ token: process.env.FOAM_OTEL_TOKEN,
89
+ version: "1.0.0",
90
+ });
91
+ ```
92
+
93
+ ```sh
94
+ node --require ./instrumentation.cjs ./app.js
95
+ ```
96
+
97
+ ## Public API
98
+
99
+ ### `init(options)`
100
+
101
+ Sets up tracing, metrics, logs, auto-instrumentation, OTLP export to Foam,
102
+ and W3C context propagation.
103
+
104
+ Call this once. Use it when Foam should run OpenTelemetry for the process.
105
+
106
+ ```js
107
+ import { init } from "@foam-ai/node";
108
+
109
+ init({
110
+ name: "checkout-api",
111
+ environment: "production",
112
+ enabled: true,
113
+ token: process.env.FOAM_OTEL_TOKEN,
114
+ version: "1.0.0",
115
+ });
116
+ ```
117
+
118
+ ```ts
119
+ init({
120
+ name,
121
+ environment,
122
+ enabled,
123
+ token,
124
+ endpoint?,
125
+ version?,
126
+ sampleRate?,
127
+ additionalInstrumentations?,
128
+ additionalSpanProcessors?,
129
+ additionalLogRecordProcessors?,
130
+ additionalMetricReaders?,
131
+ additionalResourceAttributes?,
132
+ disableLogSending?,
133
+ networkCapture?,
134
+ ignoredOutboundHosts?,
135
+ redact?,
136
+ beforeSend?,
137
+ }): void
138
+ ```
139
+
140
+ `name`, `environment`, and `enabled` are required.
141
+ `token` is required only when Foam is enabled; store it as `FOAM_OTEL_TOKEN`.
142
+
143
+ `endpoint` defaults to Foam's OTLP endpoint and is where all telemetry is
144
+ delivered. Override it only to route through a Foam-compatible OTLP
145
+ gateway, such as an egress proxy or a local collector. It must be an
146
+ `http(s)` URL; its host is always excluded from outbound tracing so
147
+ export calls do not become client spans.
148
+
149
+ `version` sets `service.version`.
150
+
151
+ `sampleRate` is a number from `0` to `1` and defaults to `1`. Rarely if ever updated. Only change it if you are sending telemetry in the petabytes range.
152
+
153
+ `additionalInstrumentations` adds extra OpenTelemetry instrumentations next
154
+ to Foam's Node auto bundle and `ConsoleInstrumentation`.
155
+
156
+ `additionalSpanProcessors`, `additionalLogRecordProcessors`, and
157
+ `additionalMetricReaders` add extra processors/readers from other vendors next to Foam's
158
+ exporters. Note: Never add Foam's own processors/readers helpers.
159
+
160
+ `additionalResourceAttributes` adds extra resource attributes (for example
161
+ `team` or `cloud.region`) to all telemetry.
162
+
163
+ `disableLogSending` defaults to `false`. Set `true` to stop Foam from
164
+ adding its own sending path to supported loggers; trace correlation in
165
+ log records is unaffected (see Loggers).
166
+
167
+ `networkCapture` defaults to `"basic"`. Upgrade to `"advanced"` unless you
168
+ cannot; it gives much more powerful HTTP and Undici capture. See Network capture.
169
+
170
+ `redact` adds keys to Foam's built-in redaction. `secrets` preserves the last
171
+ four characters of longer values; `pii` fully masks values.
172
+
173
+ ```js
174
+ init({
175
+ // ...
176
+ redact: {
177
+ secrets: ["internalId", "internalReference"],
178
+ pii: ["customerEmail", "homeAddress"],
179
+ },
180
+ });
181
+ ```
182
+
183
+ `beforeSend` can edit or drop spans and logs before Foam exports them. Return
184
+ the event to send it or `null` to drop it.
185
+
186
+ ```js
187
+ init({
188
+ // ...
189
+ beforeSend: (event) => {
190
+ if (event.type === "span" && event.attributes["url.path"] === "/health") {
191
+ return null; // drop health checks
192
+ }
193
+ delete event.attributes["user.email"];
194
+ return event;
195
+ },
196
+ });
197
+ ```
198
+
199
+ Metrics are not passed through `beforeSend`. See Redaction for coverage.
200
+
201
+ `ignoredOutboundHosts` skips outbound HTTP tracing for those hostnames.
202
+ The configured `endpoint` host is always skipped so export calls do not
203
+ become client spans. Add extra hosts for high-volume clients that would drown traces,
204
+ for example a sidecar health-check host (`localhost`) or another vendor's
205
+ OTLP ingest host you already export to.
206
+
207
+ TODO(pcga11): Add other common Gen AI providers.
208
+
209
+ If another SDK already registered traces, metrics, logs, or the propagator,
210
+ `init()` leaves the signal's slot untouched and only takes what is available. When a slot is taken, use its ingest helper instead.
211
+
212
+ ### `createFoamIngestSpanProcessor(name, environment, token, options?)`
213
+
214
+ The helper returns a span processor but does not attach it to the
215
+ application's provider. Include it in the `spanProcessors` option when
216
+ constructing `BasicTracerProvider`. It cannot be added after provider
217
+ construction in `@opentelemetry/sdk-trace-base` 2.10; the old
218
+ `addSpanProcessor()` API is obsolete. If another SDK creates the provider,
219
+ configure the processor through that SDK's startup options.
220
+
221
+ ```js
222
+ import { trace } from "@opentelemetry/api";
223
+ import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base";
224
+ import { createFoamIngestSpanProcessor } from "@foam-ai/node";
225
+
226
+ const spanProcessor = createFoamIngestSpanProcessor(
227
+ "checkout-api",
228
+ "production",
229
+ process.env.FOAM_OTEL_TOKEN,
230
+ );
231
+ const tracerProvider = new BasicTracerProvider({
232
+ spanProcessors: [spanProcessor],
233
+ });
234
+ trace.setGlobalTracerProvider(tracerProvider);
235
+ ```
236
+
237
+ ### `createFoamIngestLogRecordProcessor(name, environment, token, options?)`
238
+
239
+ The helper returns a log-record processor but does not attach it to the
240
+ application's provider. Include it in the `processors` option when constructing
241
+ `LoggerProvider`. It cannot be added after provider construction in
242
+ `@opentelemetry/sdk-logs` 0.221; the old `addLogRecordProcessor()` API is
243
+ obsolete. If another SDK creates the provider, configure the processor through
244
+ that SDK's startup options.
245
+
246
+ ```js
247
+ import { logs } from "@opentelemetry/api-logs";
248
+ import { LoggerProvider } from "@opentelemetry/sdk-logs";
249
+ import { createFoamIngestLogRecordProcessor } from "@foam-ai/node";
250
+
251
+ const logRecordProcessor = createFoamIngestLogRecordProcessor(
252
+ "checkout-api",
253
+ "production",
254
+ process.env.FOAM_OTEL_TOKEN,
255
+ );
256
+ const loggerProvider = new LoggerProvider({
257
+ processors: [logRecordProcessor],
258
+ });
259
+ logs.setGlobalLoggerProvider(loggerProvider);
260
+ ```
261
+
262
+ Both ingest helpers accept the same `redact` and `beforeSend` options as
263
+ `init()`.
264
+
265
+ ### `createFoamIngestMetricReader(name, environment, token)`
266
+
267
+ The helper returns a metric reader but does not attach it to the application's
268
+ provider. Include it in the `readers` option when constructing `MeterProvider`.
269
+ It cannot be added after provider construction in
270
+ `@opentelemetry/sdk-metrics` 2.10; the old `addMetricReader()` API is obsolete.
271
+ If another SDK creates the provider, configure the reader through that SDK's
272
+ startup options.
273
+
274
+ ```js
275
+ import { metrics } from "@opentelemetry/api";
276
+ import { MeterProvider } from "@opentelemetry/sdk-metrics";
277
+ import { createFoamIngestMetricReader } from "@foam-ai/node";
278
+
279
+ const metricReader = createFoamIngestMetricReader(
280
+ "checkout-api",
281
+ "production",
282
+ process.env.FOAM_OTEL_TOKEN,
283
+ );
284
+ const meterProvider = new MeterProvider({
285
+ readers: [metricReader],
286
+ });
287
+ metrics.setGlobalMeterProvider(meterProvider);
288
+ ```
289
+
290
+ If a vendor SDK does not expose a processor or reader startup option, add Foam
291
+ at the collector instead.
292
+
293
+ If the other SDK already set `service.name` or environment,
294
+ Foam's values overwrite them on that copy only. The original spans, logs,
295
+ and metrics are left unchanged. For the Foam SDK, ensure all of the signals (spans, logs and metrics)
296
+ carry identical `service.name`.
297
+
298
+ Don't use `init()` and an ingest helper for the same signal. If another
299
+ SDK already owns traces (or another signal), call `init()` for the rest
300
+ and attach ingest only to the provider you don't own. Don't pass ingest
301
+ helpers as `additionalSpanProcessors` or extra readers: `init()` already
302
+ installs Foam exporters.
303
+
304
+ Ingest helpers do not register instrumentations. If you cannot call
305
+ `init()`, register them with the official OpenTelemetry API
306
+ before the app imports those libraries:
307
+
308
+ ```js
309
+ import { registerInstrumentations } from "@opentelemetry/instrumentation";
310
+ import { RedisInstrumentation } from "@opentelemetry/instrumentation-redis";
311
+ import { createFoamIngestSpanProcessor } from "@foam-ai/node";
312
+
313
+ registerInstrumentations({
314
+ instrumentations: [new RedisInstrumentation()],
315
+ });
316
+ ```
317
+
318
+ Attach the ingest helper to the other SDK's provider as in the examples
319
+ above. If that SDK already instruments the same library, don't register a
320
+ second copy.
321
+
322
+ OpenTelemetry allows only one global propagator per signal. Foam registers W3C Trace
323
+ Context and W3C Baggage (`traceparent`, `tracestate`, `baggage`), not B3,
324
+ Jaeger, or AWS X-Ray. Auto-instrumented HTTP uses whichever propagator
325
+ won. If another SDK already registered a propagator, Foam keeps it and
326
+ `getState().signals.baggage` is `"none"`. A non-W3C propagator will not
327
+ send `traceparent`, so a Foam peer starts a new trace. Foam does not add
328
+ W3C headers on top.
329
+
330
+ A work around is to initialize Foam first, or include W3C in the other
331
+ SDK's composite, if those headers must be on HTTP.
332
+
333
+ TODO(pcga11): Handle traceparent conflict^
334
+
335
+ ### `injectTraceContext(headers)`
336
+
337
+ Writes W3C `traceparent`, `tracestate`, and `baggage` onto a header map.
338
+
339
+ Use this when you send a message yourself (Kafka, a queue, a custom
340
+ socket). Auto-instrumented HTTP, Express, Undici, and Fetch already inject
341
+ these headers; don't call this on ordinary HTTP. This uses Foam's local
342
+ W3C propagator so it works even if Foam did not get the global one.
343
+
344
+ ```js
345
+ const headers = {};
346
+ injectTraceContext(headers);
347
+ await kafka.send({ value: payload, headers });
348
+ ```
349
+
350
+ ### `extractTraceContext(headers)`
351
+
352
+ Reads W3C trace and baggage headers and returns a parent OpenTelemetry
353
+ context.
354
+
355
+ Use this when receiving a message on a custom transport. Without extraction,
356
+ the handler loses the propagated parent context.
357
+
358
+ ```js
359
+ import { context } from "@opentelemetry/api";
360
+
361
+ const parent = extractTraceContext(message.headers);
362
+ await context.with(parent, () => handle(message));
363
+ ```
364
+
365
+ ### `setBaggage(key, value, callback)`
366
+
367
+ Runs a callback with a request-scoped string such as a tenant or user ID.
368
+ The value follows work started by the callback across `await`s without leaking
369
+ to later handlers. Call `injectTraceContext` to send it on custom transports.
370
+
371
+ ```js
372
+ await setBaggage("tenant.id", "acme", () => handleRequest());
373
+ ```
374
+
375
+ ### `getBaggage(key)`
376
+
377
+ Returns a baggage value from the current request, or `undefined`.
378
+
379
+ Use this later in the same request to read a value set with `setBaggage`,
380
+ or a key that arrived on an inbound `baggage` header.
381
+
382
+ ```js
383
+ const tenant = getBaggage("tenant.id"); // "acme"
384
+ ```
385
+
386
+ ### `incrementCounter(name, value?, attributes?, options?)`
387
+
388
+ Creates the counter if it does not exist, then increments it.
389
+
390
+ This counter can only increases. Use this for event counts.
391
+
392
+ ```js
393
+ incrementCounter("orders.created", 1, { "cloud.region": "us-west-1" });
394
+ ```
395
+
396
+ ### `recordHistogram(name, value, attributes?, options?)`
397
+
398
+ Creates the histogram if it does not exist, then records a measurement.
399
+
400
+ Use this for values you want percentiles or averages of: latency, duration, payload size.
401
+
402
+ ```js
403
+ recordHistogram("checkout.duration", 0.142, undefined, { unit: "s" });
404
+ ```
405
+
406
+ ### `addUpDownCounter(name, value, attributes?, options?)`
407
+
408
+ Creates the up-down counter if it does not exist, then adds a signed
409
+ delta. The value can go up or down.
410
+
411
+ Use this for occupancy: active jobs, open connections, items in a pool.
412
+
413
+ In other terms, pass +value when something starts occupying a slot, -value when it leaves. The metric is the current state of occupancy.
414
+
415
+ ```js
416
+ addUpDownCounter("jobs.active", -1);
417
+ ```
418
+
419
+ ### `setMetric(name, value, attributes?, options?)`
420
+
421
+ Creates the gauge if it does not exist, then sets its current value.
422
+
423
+ Use this for a point-in-time level: queue depth, heap used, cache size.
424
+
425
+ ```js
426
+ setMetric("queue.depth", 27);
427
+ ```
428
+
429
+ TODO(pcga11): Investigate if it's worth support observable (pull) instruments API.
430
+
431
+ ### `log(body, severity?, attributes?)`
432
+
433
+ Sends a log record to Foam on Foam's own export path. It does not write to the app's logger, and it does not emit on a LoggerProvider Foam does
434
+ not own.
435
+
436
+ Use this to send logs directly. Typically used for Pino 5–6, Winston 1–2 as they do not support sending logs.
437
+
438
+ ```js
439
+ import { log, SeverityNumber } from "@foam-ai/node";
440
+
441
+ log("checkout failed");
442
+ log("checkout failed", SeverityNumber.ERROR);
443
+ ```
444
+
445
+ ### `recordException(error)`
446
+
447
+ Records an exception on the current active span and sets the span status
448
+ to `ERROR`. Accepts an `Error` or any value (stringified). Never throws;
449
+ does nothing when no span is active or the span is not recording.
450
+
451
+ Use this in `catch` blocks inside auto-instrumented handlers (HTTP
452
+ routes, message consumers) where you did not create the span yourself.
453
+ The exception lands on whichever SDK owns the active span, so it works
454
+ even when another SDK owns tracing.
455
+
456
+ ```js
457
+ import { recordException } from "@foam-ai/node";
458
+
459
+ try {
460
+ await chargeCard(order);
461
+ } catch (err) {
462
+ recordException(err);
463
+ throw err;
464
+ }
465
+ ```
466
+
467
+ ### `getState()`
468
+
469
+ Returns whether Foam initialized, which instrumentations registered, and
470
+ which signals have an export path.
471
+
472
+ Use this in health checks, tests, or diagnostics to confirm Foam is
473
+ running.
474
+
475
+ ```js
476
+ import { getState } from "@foam-ai/node";
477
+
478
+ getState();
479
+ // {
480
+ // initialized: true,
481
+ // instrumentations: ["http", "express", "pg", ...],
482
+ // signals: {
483
+ // traces: "global",
484
+ // metrics: "global",
485
+ // logs: "global",
486
+ // baggage: "global",
487
+ // profile: "none",
488
+ // },
489
+ // }
490
+ ```
491
+
492
+ `instrumentations` lists only the instrumentations registered with a
493
+ successfully started Foam SDK, after upstream defaults and
494
+ `OTEL_NODE_ENABLED_INSTRUMENTATIONS` /
495
+ `OTEL_NODE_DISABLED_INSTRUMENTATIONS` filtering. Registered means the
496
+ instrumentation is ready to patch a supported module when that module
497
+ loads.
498
+
499
+ Each `signals` value names the source of the Foam export path for that
500
+ signal:
501
+
502
+ - `"global"` — `init()` registered Foam's provider in the global
503
+ OpenTelemetry slot.
504
+ - `"ingest"` — a `createFoamIngest*` processor/reader was constructed for
505
+ another SDK's pipeline. Foam cannot tell whether you actually registered
506
+ the returned object.
507
+ - `"local"` — logs only: another SDK owns the global LoggerProvider, but
508
+ Foam keeps its own local provider so `log()` still delivers to Foam.
509
+ - `"none"` — no Foam export path.
510
+
511
+ A value other than `"none"` means telemetry is expected, not that the app
512
+ has already produced or exported it.
513
+
514
+ ## Custom spans and other OpenTelemetry APIs
515
+
516
+ Foam does not wrap span creation. To start your own spans, read the
517
+ current span, or work with context, import `@opentelemetry/api` directly.
518
+ `init()` registers Foam's providers in the global OpenTelemetry slots, so
519
+ the official API routes to Foam automatically.
520
+
521
+ Install the API in your app so it resolves one compatible `1.x` copy:
522
+
523
+ ```sh
524
+ npm install @opentelemetry/api
525
+ ```
526
+
527
+ ```js
528
+ import { trace, SpanStatusCode } from "@opentelemetry/api";
529
+
530
+ const tracer = trace.getTracer("checkout");
531
+
532
+ await tracer.startActiveSpan("charge-card", async (span) => {
533
+ try {
534
+ await chargeCard(order);
535
+ } catch (err) {
536
+ span.recordException(err);
537
+ span.setStatus({ code: SpanStatusCode.ERROR });
538
+ throw err;
539
+ } finally {
540
+ span.end();
541
+ }
542
+ });
543
+ ```
544
+
545
+ Prefer `startActiveSpan` over `startSpan`: it makes the span current, so
546
+ auto-instrumented HTTP/database spans and nested custom spans parent under
547
+ it. Always call `span.end()` (the `finally` above), or the span never
548
+ exports. When you did not create the span yourself, Foam's
549
+ `recordException(err)` does the exception-plus-ERROR-status pair on the
550
+ current active span for you.
551
+
552
+ The same applies to the rest of the API: `trace.getActiveSpan()` to tag
553
+ the current request (`span.setAttribute(...)`), and `context.with(...)`
554
+ to run code under a specific context.
555
+
556
+ This is safe in every state: if another SDK owns the global tracer slot,
557
+ these spans go to that SDK; before `init()` (or when Foam is disabled)
558
+ the API returns a no-op tracer and nothing is recorded. Custom spans
559
+ export wherever `getState().signals.traces` points.
560
+
561
+ ## Network capture
562
+
563
+ `networkCapture` defaults to `"basic"` and controls extra HTTP detail.
564
+
565
+ - `"off"` keeps standard OpenTelemetry HTTP/Undici telemetry, with no extra
566
+ capture.
567
+ - `"basic"` adds allowlisted `content-type`, `content-length`, and
568
+ `content-encoding` headers.
569
+ - `"advanced"` adds those headers plus full Node `http`/`https` and
570
+ Fetch/Undici request and response bodies as raw wire bytes, up to 1 MiB. Always set to `"advanced"` when security and compliance allows.
571
+
572
+ ## Redaction
573
+
574
+ Foam always masks common credentials in captured request and response data,
575
+ URLs, and exported span and log fields. Add application-specific keys with the
576
+ `redact` option shown under `init(options)`.
577
+
578
+ Redaction applies to Foam's exports, including ingest helpers. It does not
579
+ modify telemetry sent through other exporters.
580
+
581
+ ## Loggers
582
+
583
+ Initialize Foam before you import or construct the logger.
584
+
585
+ What Foam does depends on the logger version:
586
+
587
+ - Bunyan 1, Pino 7–10, and Winston 3: Foam adds trace IDs to every
588
+ record and automatically sends a copy to Foam.
589
+ - Pino 5.14–6 and Winston 1–2: Foam adds trace IDs, but these versions
590
+ have no way to attach a sender, so no copy reaches Foam. Upgrade the
591
+ logger, or call `log()` yourself.
592
+ - Everything else: Foam does nothing. Use `console.*` (captured
593
+ below), call `log()`, or upgrade the logger.
594
+
595
+ ### If the app already ships logs somewhere
596
+
597
+ - To a non-OTel destination (files, another vendor's transport): no
598
+ conflict. Keep the defaults; Foam sends its own copy alongside.
599
+ - Through its own OTel stream/transport (a Bunyan OTel stream,
600
+ Winston `OpenTelemetryTransportV3`): those already deliver to Foam
601
+ via the global LoggerProvider. Set `disableLogSending: true` so
602
+ records don't arrive twice.
603
+ - Through another OTel SDK's LoggerProvider: set
604
+ `disableLogSending: true` and attach
605
+ `createFoamIngestLogRecordProcessor` to that provider (see Public
606
+ API) so Foam gets a copy.
607
+ - Through `pino-opentelemetry-transport`: it runs in a worker
608
+ thread and never touches the in-process LoggerProvider, so nothing
609
+ conflicts. Keep `disableLogSending: false`, or Foam gets no Pino
610
+ records at all.
611
+ - Through a collector or agent reading stdout/files: no app changes;
612
+ add a Foam OTLP exporter to the collector config.
613
+
614
+ `log()` sends a single record straight to Foam in any setup. It is a
615
+ manual per-record call, not a mirror of the logger. Use it for
616
+ targeted, high-value records.
617
+
618
+ `ConsoleInstrumentation` turns calls to `console.*` into OTel log records.
619
+ It does not capture arbitrary stdout/stderr writes.
620
+
621
+ ```js
622
+ console.log("hi"); // captured
623
+ process.stdout.write("hi\n"); // not captured
624
+ ```
625
+
626
+ ## OpenTelemetry configuration and compliance
627
+
628
+ Foam uses the official OpenTelemetry JavaScript API, SDK, resource,
629
+ propagation, instrumentation, and OTLP exporter packages.
630
+
631
+ The [OpenTelemetry JavaScript compliance matrix](https://github.com/open-telemetry/opentelemetry-specification/blob/main/spec-compliance-matrix/js.yaml)
632
+
633
+ ## TODO(pcga11): OpenTelemetry profiling
634
+
635
+ The official OpenTelemetry JavaScript SDK has no in-process Profiles provider, processor, or exporter yet. Track upstream progress:
636
+
637
+ - [OpenTelemetry JS profiling implementation issue](https://github.com/open-telemetry/opentelemetry-js/issues/6500)
638
+ - [Profiling SIG language SDK support tracker](https://github.com/open-telemetry/sig-profiling/issues/106)
639
+ - [OpenTelemetry Profiles public alpha announcement](https://opentelemetry.io/blog/2026/profiles-alpha/)
640
+ - [OpenTelemetry eBPF profiler](https://github.com/open-telemetry/opentelemetry-ebpf-profiler)
641
+
642
+ ## TODO(pcga11): Anthropic instrumentation
643
+
644
+ There is no released official OpenTelemetry JavaScript instrumentation for the Anthropic SDK yet, but one is actively in progress in js-contrib, based on the OpenInference donation from Arize. Once `@opentelemetry/instrumentation-anthropic` is released (and picked up by `auto-instrumentations-node`), bundle it here like the OpenAI and aws-sdk ones. Until then, Anthropic coverage is handled case by case with the FDE (see FDE.md). Track upstream progress:
645
+
646
+ - [feat(instrumentation-anthropic): add basic messages instrumentation](https://github.com/open-telemetry/opentelemetry-js-contrib/pull/3664)
647
+ - [refactor(instrumentation-anthropic): use genai-util library](https://github.com/open-telemetry/opentelemetry-js-contrib/pull/3698)
648
+ - [Tracking: Migrate OpenInference JS instrumentations into opentelemetry-js-contrib](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/3668)
649
+ - [feat: Add @opentelemetry/genai-util package for GenAI instrumentations](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/3681)
@@ -0,0 +1,16 @@
1
+ interface BeforeSendSpanEvent {
2
+ readonly type: "span";
3
+ name: string;
4
+ attributes: Record<string, unknown>;
5
+ }
6
+ interface BeforeSendLogEvent {
7
+ readonly type: "log";
8
+ body?: unknown;
9
+ severityText?: string;
10
+ attributes: Record<string, unknown>;
11
+ }
12
+ export type BeforeSendEvent = BeforeSendSpanEvent | BeforeSendLogEvent;
13
+ export type BeforeSendHook = (event: BeforeSendEvent) => BeforeSendEvent | null;
14
+ export declare function resolveBeforeSend(hook: unknown): BeforeSendHook | undefined;
15
+ export declare function invokeBeforeSend(hook: BeforeSendHook, event: BeforeSendEvent): BeforeSendEvent | null | undefined;
16
+ export {};
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveBeforeSend = resolveBeforeSend;
4
+ exports.invokeBeforeSend = invokeBeforeSend;
5
+ function resolveBeforeSend(hook) {
6
+ if (hook === undefined || hook === null)
7
+ return undefined;
8
+ if (typeof hook !== "function") {
9
+ throw new TypeError("[foam] beforeSend must be a function");
10
+ }
11
+ return hook;
12
+ }
13
+ function isRecord(value) {
14
+ return value !== null && typeof value === "object" && !Array.isArray(value);
15
+ }
16
+ function isEvent(value, expectedType) {
17
+ if (!isRecord(value) || value.type !== expectedType || !isRecord(value.attributes)) {
18
+ return false;
19
+ }
20
+ if (expectedType === "span")
21
+ return typeof value.name === "string";
22
+ return value.severityText === undefined || typeof value.severityText === "string";
23
+ }
24
+ function invokeBeforeSend(hook, event) {
25
+ let copy;
26
+ try {
27
+ copy = structuredClone(event);
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ try {
33
+ const result = hook(copy);
34
+ if (result === null)
35
+ return null;
36
+ return isEvent(result, event.type) ? result : undefined;
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ }