@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/README.md CHANGED
@@ -1,14 +1,12 @@
1
1
  # Foam OpenTelemetry SDK for Node.js
2
2
 
3
- `@foam-ai/node` is Foam's OpenTelemetry distribution for Node.js. It configures
4
- the official OpenTelemetry JavaScript SDK, node automatic instrumentations, and
5
- OTLP/HTTP exporters with one `init()` call.
3
+ `@foam-ai/node` is Foam's OpenTelemetry distribution for Node.js.
6
4
 
7
5
  ## Requirements
8
6
 
9
7
  - Node.js `^18.19.0 || >=20.6.0`, matching the supported engine range of
10
- `@opentelemetry/auto-instrumentations-node`
11
- - A Foam ingest token, stored as `FOAM_OTEL_TOKEN`
8
+ `@opentelemetry/auto-instrumentations-node`
9
+ - Foam ingest token, stored as `FOAM_OTEL_TOKEN`
12
10
 
13
11
  ## Install
14
12
 
@@ -18,11 +16,14 @@ npm install @foam-ai/node
18
16
 
19
17
  ## Initialize
20
18
 
21
- Call `init()` before any application code loads so OpenTelemetry can patch
22
- libraries as they import. Don't import the app from the same file: ESM runs
23
- static imports before `init()`. Set `enabled: false` to disable Foam. Store
24
- the ingest token as `FOAM_OTEL_TOKEN` and pass `process.env.FOAM_OTEL_TOKEN`;
25
- do not hardcode it.
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()`.
26
27
 
27
28
  ### TypeScript
28
29
 
@@ -40,13 +41,14 @@ init({
40
41
  });
41
42
  ```
42
43
 
43
- Compile it with the app. If TypeScript emits ESM:
44
+ Compile it with the app. If TypeScript emits ESM, preload with `--import`
45
+ (same rule as ESM below):
44
46
 
45
47
  ```sh
46
48
  node --import ./dist/instrumentation.js ./dist/app.js
47
49
  ```
48
50
 
49
- If TypeScript emits CommonJS:
51
+ If TypeScript emits CommonJS, preload with `--require`:
50
52
 
51
53
  ```sh
52
54
  node --require ./dist/instrumentation.js ./dist/app.js
@@ -60,7 +62,11 @@ node --import tsx --import ./instrumentation.ts ./src/app.ts
60
62
 
61
63
  ### ESM
62
64
 
63
- Create `instrumentation.mjs` with the same `init()` call, then:
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()`:
64
70
 
65
71
  ```sh
66
72
  node --import ./instrumentation.mjs ./app.js
@@ -68,7 +74,9 @@ node --import ./instrumentation.mjs ./app.js
68
74
 
69
75
  ### CommonJS
70
76
 
71
- Create `instrumentation.cjs`:
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:
72
80
 
73
81
  ```js
74
82
  const { init } = require("@foam-ai/node");
@@ -90,10 +98,10 @@ node --require ./instrumentation.cjs ./app.js
90
98
 
91
99
  ### `init(options)`
92
100
 
93
- Starts Foam: resource, sampler, OTLP exporters, automatic instrumentations,
94
- and the W3C propagator.
101
+ Sets up tracing, metrics, logs, auto-instrumentation, OTLP export to Foam,
102
+ and W3C context propagation.
95
103
 
96
- Call this once when Foam can own all of one of OTEL slots.
104
+ Call this once. Use it when Foam should run OpenTelemetry for the process.
97
105
 
98
106
  ```js
99
107
  import { init } from "@foam-ai/node";
@@ -113,6 +121,7 @@ init({
113
121
  environment,
114
122
  enabled,
115
123
  token,
124
+ endpoint?,
116
125
  version?,
117
126
  sampleRate?,
118
127
  additionalInstrumentations?,
@@ -123,51 +132,95 @@ init({
123
132
  disableLogSending?,
124
133
  networkCapture?,
125
134
  ignoredOutboundHosts?,
126
- diagnostics?,
135
+ redact?,
136
+ beforeSend?,
127
137
  }): void
128
138
  ```
129
139
 
130
140
  `name`, `environment`, and `enabled` are required.
131
- `token` is required only when enabled; store it as `FOAM_OTEL_TOKEN`.
132
- `sampleRate` accepts values from `0` through `1` and defaults to
133
- `1`.
134
-
135
- Foam always installs this sampler rather than reading `OTEL_TRACES_SAMPLER`.
136
- Diagnostics default to enabled.
137
-
138
- `additionalResourceAttributes` adds custom resource attributes (for example
139
- `team` or `cloud.region`) to all telemetry. The Basic API's `name`,
140
- `environment`, and `version` options take precedence over matching keys.
141
- Foam also sets `telemetry.sdk.*` from the OpenTelemetry SDK and
142
- `telemetry.distro.name` / `telemetry.distro.version` for this distribution.
143
-
144
- Foam detects process, host, and OS attributes from this machine. It does not
145
- run cloud or container resource detectors (AWS, GCP, Azure, Alibaba, or cgroup
146
- `container.id`), which call metadata APIs at startup. Set cloud or container
147
- identity with `additionalResourceAttributes`.
148
-
149
- `additionalInstrumentations` registers extra OpenTelemetry instrumentations
150
- alongside Foam's Node auto bundle and `ConsoleInstrumentation`. Foam already
151
- enables [`@opentelemetry/instrumentation-openai`](https://www.npmjs.com/package/@opentelemetry/instrumentation-openai)
152
- from that bundle (`openai` `>=4.19 <7`); message content is captured only when
153
- `networkCapture` is `"advanced"`. Use this option for libraries Foam does not
154
- ship, such as community Anthropic instrumentations
155
- ([`@traceloop/instrumentation-anthropic`](https://www.npmjs.com/package/@traceloop/instrumentation-anthropic)
156
- or
157
- [`@arizeai/openinference-instrumentation-anthropic`](https://www.npmjs.com/package/@arizeai/openinference-instrumentation-anthropic)),
158
- or to replace a bundled instrumentation by name: last entry wins, and Foam
159
- disables its own copy. Replacing HTTP or Undici this way also replaces Foam's
160
- network capture hooks.
161
-
162
- ### `createFoamIngestSpanProcessor(name, environment, token)`
163
-
164
- Returns a span processor that exports traces to Foam without taking over the
165
- global TracerProvider.
166
-
167
- Use this when another OpenTelemetry SDK already owns traces and you only need
168
- Foam as an extra export destination.
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.
169
172
 
170
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";
171
224
  import { createFoamIngestSpanProcessor } from "@foam-ai/node";
172
225
 
173
226
  const spanProcessor = createFoamIngestSpanProcessor(
@@ -175,17 +228,24 @@ const spanProcessor = createFoamIngestSpanProcessor(
175
228
  "production",
176
229
  process.env.FOAM_OTEL_TOKEN,
177
230
  );
178
- tracerProvider.addSpanProcessor(spanProcessor);
231
+ const tracerProvider = new BasicTracerProvider({
232
+ spanProcessors: [spanProcessor],
233
+ });
234
+ trace.setGlobalTracerProvider(tracerProvider);
179
235
  ```
180
236
 
181
- ### `createFoamIngestLogRecordProcessor(name, environment, token)`
182
-
183
- Returns a log-record processor that exports logs to Foam without taking over
184
- the global LoggerProvider.
237
+ ### `createFoamIngestLogRecordProcessor(name, environment, token, options?)`
185
238
 
186
- Use this when another SDK already owns logs and you want those records in Foam.
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.
187
245
 
188
246
  ```js
247
+ import { logs } from "@opentelemetry/api-logs";
248
+ import { LoggerProvider } from "@opentelemetry/sdk-logs";
189
249
  import { createFoamIngestLogRecordProcessor } from "@foam-ai/node";
190
250
 
191
251
  const logRecordProcessor = createFoamIngestLogRecordProcessor(
@@ -193,18 +253,27 @@ const logRecordProcessor = createFoamIngestLogRecordProcessor(
193
253
  "production",
194
254
  process.env.FOAM_OTEL_TOKEN,
195
255
  );
196
- loggerProvider.addLogRecordProcessor(logRecordProcessor);
256
+ const loggerProvider = new LoggerProvider({
257
+ processors: [logRecordProcessor],
258
+ });
259
+ logs.setGlobalLoggerProvider(loggerProvider);
197
260
  ```
198
261
 
199
- ### `createFoamIngestMetricReader(name, environment, token)`
262
+ Both ingest helpers accept the same `redact` and `beforeSend` options as
263
+ `init()`.
200
264
 
201
- Returns a metric reader that exports metrics to Foam without taking over the
202
- global MeterProvider.
265
+ ### `createFoamIngestMetricReader(name, environment, token)`
203
266
 
204
- Use this when another SDK already owns metrics and you want those meters in
205
- Foam.
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.
206
273
 
207
274
  ```js
275
+ import { metrics } from "@opentelemetry/api";
276
+ import { MeterProvider } from "@opentelemetry/sdk-metrics";
208
277
  import { createFoamIngestMetricReader } from "@foam-ai/node";
209
278
 
210
279
  const metricReader = createFoamIngestMetricReader(
@@ -212,42 +281,65 @@ const metricReader = createFoamIngestMetricReader(
212
281
  "production",
213
282
  process.env.FOAM_OTEL_TOKEN,
214
283
  );
215
- meterProvider.addMetricReader(metricReader);
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
+ });
216
316
  ```
217
317
 
218
- The factories do not claim global OTel providers. `name`, `environment`, and
219
- `token` are required; Foam stamps `service.name`,
220
- `deployment.environment.name`, and `foam.ingest.tier=external` onto the
221
- exported resource. If the other SDK already set `service.name` or environment,
222
- Foam's values overwrite them on the copy sent to Foam only. The original
223
- spans, logs, and metrics are not mutated.
224
-
225
- Use `init()` or ingest, not both on the same signal. If another SDK already
226
- owns traces (or another slot), call `init()` for the free signals and attach
227
- ingest only to the provider you do not own. Do not pass ingest factories as
228
- `additionalSpanProcessors` / readers: `init()` already installs Foam exporters.
229
-
230
- The global propagator is also first-wins. OpenTelemetry allows only one, and
231
- HTTP, Express, Undici, and Fetch all call it to copy trace headers onto
232
- requests. Foam registers W3C Trace Context and W3C Baggage
233
- (`traceparent`, `tracestate`, `baggage`) and nothing else: not B3, Jaeger, or
234
- AWS X-Ray. If Foam wins the slot, auto-instrumented HTTP is W3C-only. If
235
- another SDK already registered a propagator, Foam keeps it,
236
- `getState().signals.baggage` is `false`, and HTTP speaks that winner's format.
237
- A B3-only (or Jaeger/X-Ray-only) winner will not send or honor `traceparent`,
238
- so a Foam peer that only understands W3C starts a new trace. Foam does not
239
- wrap inject/extract to dual-read those formats or add W3C headers on top.
240
- Initialize Foam first, or have the other SDK register a composite that
241
- includes W3C, if those headers must be on HTTP.
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^
242
334
 
243
335
  ### `injectTraceContext(headers)`
244
336
 
245
337
  Writes W3C `traceparent`, `tracestate`, and `baggage` onto a header map.
246
338
 
247
- Use this when you send a message yourself (Kafka, a queue, a custom socket).
248
- Auto-instrumented HTTP, Express, Undici, and Fetch already inject these
249
- headers; do not call this on ordinary HTTP. This path uses Foam's local W3C
250
- propagator even if Foam lost the global slot.
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.
251
343
 
252
344
  ```js
253
345
  const headers = {};
@@ -257,10 +349,11 @@ await kafka.send({ value: payload, headers });
257
349
 
258
350
  ### `extractTraceContext(headers)`
259
351
 
260
- Reads W3C trace and baggage headers and returns a parent OpenTelemetry context.
352
+ Reads W3C trace and baggage headers and returns a parent OpenTelemetry
353
+ context.
261
354
 
262
- Use this when you receive a message on a custom transport so the work continues
263
- the same trace. Ignoring those headers starts a new one.
355
+ Use this when receiving a message on a custom transport. Without extraction,
356
+ the handler loses the propagated parent context.
264
357
 
265
358
  ```js
266
359
  import { context } from "@opentelemetry/api";
@@ -269,25 +362,22 @@ const parent = extractTraceContext(message.headers);
269
362
  await context.with(parent, () => handle(message));
270
363
  ```
271
364
 
272
- ### `setBaggage(key, value)`
273
-
274
- Stores a small string on the current request context.
365
+ ### `setBaggage(key, value, callback)`
275
366
 
276
- Use this for request-scoped identity (tenant, user id) that should follow the
277
- request across `await`s without being passed through every function. Values do
278
- not leak to other concurrent requests. Auto-instrumented HTTP does not send
279
- these keys until you call `injectTraceContext`.
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.
280
370
 
281
371
  ```js
282
- setBaggage("tenant.id", "acme");
372
+ await setBaggage("tenant.id", "acme", () => handleRequest());
283
373
  ```
284
374
 
285
375
  ### `getBaggage(key)`
286
376
 
287
377
  Returns a baggage value from the current request, or `undefined`.
288
378
 
289
- Use this later in the same request to read a value set with `setBaggage`, or a
290
- key that arrived on an inbound `baggage` header.
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.
291
381
 
292
382
  ```js
293
383
  const tenant = getBaggage("tenant.id"); // "acme"
@@ -295,9 +385,9 @@ const tenant = getBaggage("tenant.id"); // "acme"
295
385
 
296
386
  ### `incrementCounter(name, value?, attributes?, options?)`
297
387
 
298
- Adds to a counter that only increases.
388
+ Creates the counter if it does not exist, then increments it.
299
389
 
300
- Use this for event counts: orders created, jobs finished, errors.
390
+ This counter can only increases. Use this for event counts.
301
391
 
302
392
  ```js
303
393
  incrementCounter("orders.created", 1, { "cloud.region": "us-west-1" });
@@ -305,10 +395,9 @@ incrementCounter("orders.created", 1, { "cloud.region": "us-west-1" });
305
395
 
306
396
  ### `recordHistogram(name, value, attributes?, options?)`
307
397
 
308
- Records a measurement in a distribution.
398
+ Creates the histogram if it does not exist, then records a measurement.
309
399
 
310
- Use this for values you want percentiles or averages of: latency, duration,
311
- payload size.
400
+ Use this for values you want percentiles or averages of: latency, duration, payload size.
312
401
 
313
402
  ```js
314
403
  recordHistogram("checkout.duration", 0.142, undefined, { unit: "s" });
@@ -316,17 +405,20 @@ recordHistogram("checkout.duration", 0.142, undefined, { unit: "s" });
316
405
 
317
406
  ### `addUpDownCounter(name, value, attributes?, options?)`
318
407
 
319
- Adds a signed delta to a counter that can go up or down.
408
+ Creates the up-down counter if it does not exist, then adds a signed
409
+ delta. The value can go up or down.
320
410
 
321
411
  Use this for occupancy: active jobs, open connections, items in a pool.
322
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
+
323
415
  ```js
324
416
  addUpDownCounter("jobs.active", -1);
325
417
  ```
326
418
 
327
419
  ### `setMetric(name, value, attributes?, options?)`
328
420
 
329
- Sets the current value of a gauge.
421
+ Creates the gauge if it does not exist, then sets its current value.
330
422
 
331
423
  Use this for a point-in-time level: queue depth, heap used, cache size.
332
424
 
@@ -334,17 +426,51 @@ Use this for a point-in-time level: queue depth, heap used, cache size.
334
426
  setMetric("queue.depth", 27);
335
427
  ```
336
428
 
337
- Each metric helper caches instruments by kind and name. `unit` and
338
- `description` therefore apply on first use. Observable (pull) instruments are
339
- not part of this API; use `@opentelemetry/api` or automatic instrumentation
340
- when a value should be sampled on export rather than recorded on an event.
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
+ ```
341
466
 
342
467
  ### `getState()`
343
468
 
344
- Returns whether Foam initialized, which instrumentations registered, and which
345
- signals have an export path.
469
+ Returns whether Foam initialized, which instrumentations registered, and
470
+ which signals have an export path.
346
471
 
347
- Use this in health checks, tests, or diagnostics to confirm Foam is running.
472
+ Use this in health checks, tests, or diagnostics to confirm Foam is
473
+ running.
348
474
 
349
475
  ```js
350
476
  import { getState } from "@foam-ai/node";
@@ -354,117 +480,170 @@ getState();
354
480
  // initialized: true,
355
481
  // instrumentations: ["http", "express", "pg", ...],
356
482
  // signals: {
357
- // traces: true,
358
- // metrics: true,
359
- // logs: true,
360
- // baggage: true,
361
- // profile: false,
483
+ // traces: "global",
484
+ // metrics: "global",
485
+ // logs: "global",
486
+ // baggage: "global",
487
+ // profile: "none",
362
488
  // },
363
489
  // }
364
490
  ```
365
491
 
366
- `instrumentations` contains only the instrumentations registered with a
492
+ `instrumentations` lists only the instrumentations registered with a
367
493
  successfully started Foam SDK, after upstream defaults and
368
494
  `OTEL_NODE_ENABLED_INSTRUMENTATIONS` /
369
- `OTEL_NODE_DISABLED_INSTRUMENTATIONS` filtering. Registration means an
370
- instrumentation is ready to patch a supported module when that module loads; it
371
- does not prove that the module has loaded or emitted spans.
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.
372
520
 
373
- Each `signals` boolean means Foam successfully constructed an export path for
374
- that signal. This can be a Foam-owned provider from `init()` or a
375
- processor/reader returned by an ingest factory. For ingest factories, Foam
376
- cannot prove that the caller subsequently registered the returned object. The
377
- boolean therefore means telemetry is expected, not that the application has
378
- already produced or successfully exported it.
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.
379
560
 
380
561
  ## Network capture
381
562
 
382
- `networkCapture` defaults to `"basic"` and controls HTTP enrichment: `"off"`
383
- keeps standard OpenTelemetry HTTP/Undici telemetry with no extra capture,
384
- `"basic"` adds allowlisted `content-type`, `content-length`, and
385
- `content-encoding` headers, and `"advanced"` adds those headers plus complete
386
- Node `http`/`https` (and Fetch/Undici, when diagnostics exist) request and
387
- response bodies as raw wire bytes up to 1 MiB. Bodies are emitted as
388
- correlated `foam.http.body.chunk` log events; the HTTP span records semantic
389
- `http.*.body.size` and, for textual bodies, `http.*.body.content`, while
390
- `authorization`, `cookie`, HTTP/2, and gRPC are never captured. Advanced
391
- capture requires Foam to own the logger provider, closes incomplete bodies
392
- after 60 seconds of inactivity, and is replaced if you override the bundled
393
- HTTP instrumentation through `additionalInstrumentations`. Foam partitions
394
- serialized trace and log export batches below 19 MiB to stay under the
395
- ClickStack collector's 20 MiB OTLP/HTTP limit.
396
-
397
- ## Logger support and ownership
398
-
399
- Foam follows the compatibility ranges of its OpenTelemetry logger
400
- instrumentations:
401
-
402
- - Bunyan `>=1 <2`
403
- - Pino `>=5.14 <11`
404
- - Winston `>=1 <4`
405
-
406
- The init log line that lists registered instrumentations also names each
407
- detected logger with its installed version. Initialize Foam before importing or
408
- constructing the logger; registration alone does not prove that a logger was
409
- loaded, patched, or produced records.
410
-
411
- Logger trace correlation remains enabled by default. With
412
- `disableLogSending: false` (the default), OpenTelemetry automatically sends:
413
-
414
- - Bunyan records through its added OTel stream
415
- - Pino 7 and newer records through an added main-thread multistream
416
- - Winston 3 records through the bundled `OpenTelemetryTransportV3`
417
-
418
- Pino 5 and 6 and Winston 1 and 2 support correlation only. Set
419
- `disableLogSending: true` when the application manually installs a Bunyan OTel
420
- stream, configures `pino-opentelemetry-transport`, installs a Winston OTel
421
- transport, or otherwise gives another pipeline ownership of logger records.
422
- This disables automatic sending for Bunyan, Pino, and Winston while preserving
423
- trace correlation.
424
-
425
- OpenTelemetry cannot reliably deduplicate manually configured streams and
426
- transports; explicit `disableLogSending` ownership is the correctness
427
- mechanism.
428
-
429
- `ConsoleInstrumentation` separately turns calls to `console.*` into OTel log
430
- records. It does not make arbitrary stdout/stderr writes observable.
563
+ `networkCapture` defaults to `"basic"` and controls extra HTTP detail.
431
564
 
432
- ## OpenTelemetry configuration and compliance
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.
433
571
 
434
- Foam composes the official OpenTelemetry JavaScript API, SDK, resource,
435
- propagation, instrumentation, and OTLP exporter packages. It does not fork
436
- their data model or redefine spans, metrics, logs, resources, or context.
572
+ ## Redaction
437
573
 
438
- Foam does not use any of environment configuration. So they remain available, including:
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)`.
439
577
 
440
- - `OTEL_RESOURCE_ATTRIBUTES` (the Basic API's required service name and
441
- environment take precedence for those resource attributes)
442
- - `OTEL_NODE_ENABLED_INSTRUMENTATIONS` and
443
- `OTEL_NODE_DISABLED_INSTRUMENTATIONS`
444
- - `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT`
445
- - `OTEL_TRACES_SAMPLER`
578
+ Redaction applies to Foam's exports, including ingest helpers. It does not
579
+ modify telemetry sent through other exporters.
446
580
 
447
- The [OpenTelemetry JavaScript compliance matrix](https://github.com/open-telemetry/opentelemetry-specification/blob/main/spec-compliance-matrix/js.yaml)
448
- describes `opentelemetry-js`, not this distro. It uses `+` (implemented),
449
- `-` (not implemented), `?` (unknown), and `N/A`. Foam does not reimplement
450
- those SDK features and does not inherit every `+` row: `init()` only
451
- constructs the packages and options this distro actually wires (W3C
452
- propagation, OTLP/HTTP to Foam, a parent-based ratio sampler). Rows the
453
- matrix marks `+` for JavaScript but Foam never installs stay unavailable
454
- here, including OTLP/gRPC, Zipkin, Prometheus, Jaeger remote sampling,
455
- B3/Jaeger propagators, metric views, and `OTEL_TRACES_SAMPLER`. Foam also
456
- does not implement rows marked `-` or `?`.
581
+ ## Loggers
457
582
 
458
- ## TODO(pcga11): OpenTelemetry profiling
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.
459
617
 
460
- Profiling is not supported: the official OpenTelemetry JavaScript SDK has no
461
- in-process Profiles provider, processor, or exporter yet.
618
+ `ConsoleInstrumentation` turns calls to `console.*` into OTel log records.
619
+ It does not capture arbitrary stdout/stderr writes.
462
620
 
463
- Add profiling and report `getState().signals.profile: true` after that ships.
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
464
634
 
465
- Track upstream progress:
635
+ The official OpenTelemetry JavaScript SDK has no in-process Profiles provider, processor, or exporter yet. Track upstream progress:
466
636
 
467
637
  - [OpenTelemetry JS profiling implementation issue](https://github.com/open-telemetry/opentelemetry-js/issues/6500)
468
638
  - [Profiling SIG language SDK support tracker](https://github.com/open-telemetry/sig-profiling/issues/106)
469
639
  - [OpenTelemetry Profiles public alpha announcement](https://opentelemetry.io/blog/2026/profiles-alpha/)
470
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)