@jr200-labs/xstate-nats 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -218,6 +218,60 @@ auth: {
218
218
  - `KV.UNSUBSCRIBE`: Unsubscribe from KV changes
219
219
  - `KV.UNSUBSCRIBE_ALL`: Unsubscribe from all KV changes
220
220
 
221
+ ## OpenTelemetry
222
+
223
+ This library emits OpenTelemetry spans for NATS operations and propagates W3C
224
+ trace context across the wire so a single trace can span publisher, subscriber,
225
+ and request/reply replier.
226
+
227
+ ### Emitted spans
228
+
229
+ | Span name | Emitted by | Attributes |
230
+ | ----------------------- | -------------------- | ---------------------------------------- |
231
+ | `xstate.nats.subscribe` | `SUBJECT.SUBSCRIBE` | `subject` |
232
+ | `xstate.nats.message` | per received message | `subject`, `payload.bytes` |
233
+ | `xstate.nats.publish` | `SUBJECT.PUBLISH` | `subject`, `payload.bytes` |
234
+ | `xstate.nats.request` | `SUBJECT.REQUEST` | `subject`, `payload.bytes`, `timeout.ms` |
235
+ | `xstate.nats.reconnect` | NATS status loop | `reconnect.type` |
236
+ | `xstate.nats.kv.watch` | `KV.SUBSCRIBE` | `bucket`, `key` |
237
+ | `xstate.nats.kv.entry` | per KV watch entry | `bucket`, `key`, `operation` |
238
+
239
+ All error paths record exceptions on the active span, set span status to
240
+ `ERROR`, and emit a named event (`xstate.nats.error` / `xstate.nats.kv.error`)
241
+ with a truncated stack.
242
+
243
+ ### Enabling tracing
244
+
245
+ `@opentelemetry/api` is a peer dependency — the consumer controls the installed
246
+ version and registers the SDK. If no provider is registered all telemetry calls
247
+ become no-ops. Minimal setup:
248
+
249
+ ```ts
250
+ import { trace, propagation, context } from '@opentelemetry/api'
251
+ import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'
252
+ import { W3CTraceContextPropagator } from '@opentelemetry/core'
253
+ import { BasicTracerProvider, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
254
+
255
+ const provider = new BasicTracerProvider({
256
+ spanProcessors: [
257
+ /* your exporter */
258
+ ],
259
+ })
260
+ trace.setGlobalTracerProvider(provider)
261
+ propagation.setGlobalPropagator(new W3CTraceContextPropagator())
262
+
263
+ const ctxMgr = new AsyncLocalStorageContextManager()
264
+ ctxMgr.enable()
265
+ context.setGlobalContextManager(ctxMgr)
266
+ ```
267
+
268
+ ### Context propagation
269
+
270
+ Publish and request operations inject `traceparent` into the outgoing NATS
271
+ headers; received messages extract the traceparent and parent their
272
+ `xstate.nats.message` span on it. Downstream services that propagate the header
273
+ appear as children of the originating publisher/requester span.
274
+
221
275
  ## Examples
222
276
 
223
277
  Check out the [React example](./examples/react-test/) for a complete working implementation.
@@ -1 +1 @@
1
- {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/actions/connection.ts"],"names":[],"mappings":"AACA,OAAO,EACL,iBAAiB,EAEjB,GAAG,EACH,cAAc,EACd,MAAM,EAEP,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AACrC,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,SAAS,CAAA;AA6BzC,MAAM,MAAM,oBAAoB,GAC5B;IAAE,IAAI,EAAE,8BAA8B,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACxD;IAAE,IAAI,EAAE,2BAA2B,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACrD;IAAE,IAAI,EAAE,uBAAuB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,uBAAuB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,8BAA8B,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAE5D,eAAO,MAAM,aAAa;UAIP,iBAAiB;WAAS,UAAU;gCAwDtD,CAAA;AAED,eAAO,MAAM,cAAc;gBACgB,cAAc,GAAG,IAAI;gCAM/D,CAAA;AAED,eAAO,MAAM,eAAe,GAAI,KAAK,GAAG,GAAG,OAAO,GAAG,IAAI,GAAG,KAAK,YAiBhE,CAAA"}
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/actions/connection.ts"],"names":[],"mappings":"AACA,OAAO,EACL,iBAAiB,EAEjB,GAAG,EACH,cAAc,EACd,MAAM,EAEP,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AACrC,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,SAAS,CAAA;AA8BzC,MAAM,MAAM,oBAAoB,GAC5B;IAAE,IAAI,EAAE,8BAA8B,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACxD;IAAE,IAAI,EAAE,2BAA2B,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACrD;IAAE,IAAI,EAAE,uBAAuB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,uBAAuB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,8BAA8B,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAE5D,eAAO,MAAM,aAAa;UAIP,iBAAiB;WAAS,UAAU;gCA+EtD,CAAA;AAED,eAAO,MAAM,cAAc;gBACgB,cAAc,GAAG,IAAI;gCAM/D,CAAA;AAED,eAAO,MAAM,eAAe,GAAI,KAAK,GAAG,GAAG,OAAO,GAAG,IAAI,GAAG,KAAK,YAiBhE,CAAA"}
@@ -1,6 +1,7 @@
1
1
  import { fromPromise } from 'xstate';
2
2
  import { credsAuthenticator, wsconnect, } from '@nats-io/nats-core';
3
3
  import { sendParent } from 'xstate';
4
+ import { withSpan } from '../telemetry';
4
5
  const makeAuthConfig = (auth) => {
5
6
  if (!auth) {
6
7
  return {};
@@ -41,7 +42,11 @@ export const connectToNats = fromPromise(async ({ input, }) => {
41
42
  sendParent({ type: 'NATS_CONNECTION.DISCONNECTED', status });
42
43
  break;
43
44
  case 'reconnect':
44
- sendParent({ type: 'NATS_CONNECTION.RECONNECT', status });
45
+ // Per-event span so reconnect attempts become discoverable in
46
+ // the trace backend (searchable, durations, span counts).
47
+ withSpan('xstate.nats.reconnect', 'xstate.nats.error', { 'reconnect.type': type }, () => {
48
+ sendParent({ type: 'NATS_CONNECTION.RECONNECT', status });
49
+ });
45
50
  break;
46
51
  case 'error':
47
52
  sendParent({ type: 'NATS_CONNECTION.ERROR', status });
@@ -56,10 +61,14 @@ export const connectToNats = fromPromise(async ({ input, }) => {
56
61
  // console.debug('Received ping, pong sent automatically')
57
62
  break;
58
63
  case 'forceReconnect':
59
- sendParent({ type: 'NATS_CONNECTION.RECONNECT', status });
64
+ withSpan('xstate.nats.reconnect', 'xstate.nats.error', { 'reconnect.type': type }, () => {
65
+ sendParent({ type: 'NATS_CONNECTION.RECONNECT', status });
66
+ });
60
67
  break;
61
68
  case 'reconnecting':
62
- sendParent({ type: 'NATS_CONNECTION.RECONNECTING', status });
69
+ withSpan('xstate.nats.reconnect', 'xstate.nats.error', { 'reconnect.type': type }, () => {
70
+ sendParent({ type: 'NATS_CONNECTION.RECONNECTING', status });
71
+ });
63
72
  break;
64
73
  case 'slowConsumer':
65
74
  console.debug('SLOW_CONSUMER', status);
@@ -1 +1 @@
1
- {"version":3,"file":"connection.js","sourceRoot":"","sources":["../../src/actions/connection.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"connection.js","sourceRoot":"","sources":["../../src/actions/connection.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"kv.d.ts","sourceRoot":"","sources":["../../src/actions/kv.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACnE,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC/D,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAA;AAG/B,qBAAa,iBAAkB,SAAQ,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;CAAG;AAE9D,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7B,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B,CAAA;AAED,eAAO,MAAM,kBAAkB;mBAWZ,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,YAAY,CAAC,CAAC;;SANjD,GAAG,GAAG,IAAI;gBACH,cAAc,GAAG,IAAI;kBACnB,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,YAAY,CAAC,CAAC;iBAC1C,GAAG,CAAC,MAAM,EAAE,oBAAoB,CAAC;gCAoEnD,CAAA"}
1
+ {"version":3,"file":"kv.d.ts","sourceRoot":"","sources":["../../src/actions/kv.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACnE,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC/D,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAA;AAI/B,qBAAa,iBAAkB,SAAQ,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;CAAG;AAE9D,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7B,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B,CAAA;AAED,eAAO,MAAM,kBAAkB;mBAWZ,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,YAAY,CAAC,CAAC;;SANjD,GAAG,GAAG,IAAI;gBACH,cAAc,GAAG,IAAI;kBACnB,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,YAAY,CAAC,CAAC;iBAC1C,GAAG,CAAC,MAAM,EAAE,oBAAoB,CAAC;gCA+FnD,CAAA"}
@@ -1,5 +1,6 @@
1
1
  import { Pair } from '../utils';
2
2
  import { fromPromise } from 'xstate';
3
+ import { recordError, withSpan } from '../telemetry';
3
4
  export class KvSubscriptionKey extends Pair {
4
5
  }
5
6
  export const kvConsolidateState = fromPromise(async ({ input, }) => {
@@ -28,23 +29,38 @@ export const kvConsolidateState = fromPromise(async ({ input, }) => {
28
29
  try {
29
30
  const kv = await input.kvm.open(config.bucket);
30
31
  const watchOptions = config;
31
- const watcher = await kv.watch(watchOptions);
32
+ // Short span around the synchronous-ish watch() setup — the watcher
33
+ // iterator itself is long-lived, so each received entry gets its
34
+ // own span below rather than one indefinite parent.
35
+ const watcher = (await withSpan('xstate.nats.kv.watch', 'xstate.nats.kv.error', { bucket: config.bucket, key: config.key }, () => kv.watch(watchOptions)));
32
36
  syncedState.set(kvKey, watcher);
33
37
  (async () => {
34
38
  try {
35
39
  for await (const e of watcher) {
36
40
  if (e.operation !== 'DEL') {
37
- let parsedValue;
38
- try {
39
- parsedValue = JSON.parse(e.string());
40
- }
41
- catch {
42
- parsedValue = e.string();
43
- }
44
- config.callback({
41
+ await withSpan('xstate.nats.kv.entry', 'xstate.nats.kv.error', {
45
42
  bucket: config.bucket,
46
43
  key: config.key,
47
- value: parsedValue,
44
+ operation: e.operation,
45
+ }, (span) => {
46
+ let parsedValue;
47
+ try {
48
+ parsedValue = JSON.parse(e.string());
49
+ }
50
+ catch {
51
+ parsedValue = e.string();
52
+ }
53
+ try {
54
+ config.callback({
55
+ bucket: config.bucket,
56
+ key: config.key,
57
+ value: parsedValue,
58
+ });
59
+ }
60
+ catch (callbackError) {
61
+ recordError(span, 'xstate.nats.kv.error', callbackError);
62
+ console.error(`KV_SUBSCRIBE (connected): Callback error for ${kvKey}:`, callbackError);
63
+ }
48
64
  });
49
65
  }
50
66
  }
@@ -1 +1 @@
1
- {"version":3,"file":"kv.js","sourceRoot":"","sources":["../../src/actions/kv.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAA;AAC/B,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAA;AAEpC,MAAM,OAAO,iBAAkB,SAAQ,IAAoB;CAAG;AAU9D,MAAM,CAAC,MAAM,kBAAkB,GAAG,WAAW,CAC3C,KAAK,EAAE,EACL,KAAK,GAQN,EAEE,EAAE;IACH,IAAI,CAAC,KAAK,CAAC,UAAU,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAA;IAC5D,CAAC;IAED,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,GAAG,KAAK,CAAA;IAC3C,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,YAAY,CAAC,CAAA;IAEzC,yEAAyE;IACzE,KAAK,MAAM,CAAC,KAAK,EAAE,YAAY,CAAC,IAAI,YAAY,EAAE,CAAC;QACjD,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAC5B,IAAI,CAAC;gBACH,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;gBACzB,YAAY,CAAC,IAAI,EAAE,CAAA;YACrB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,KAAK,CAAC,qCAAqC,KAAK,GAAG,EAAE,KAAK,CAAC,CAAA;YACrE,CAAC;QACH,CAAC;IACH,CAAC;IAED,4EAA4E;IAC5E,KAAK,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC;QAC1C,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAC7B,6EAA6E;YAC7E,4BAA4B;YAC5B,IAAI,CAAC;gBACH,MAAM,EAAE,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;gBAE9C,MAAM,YAAY,GAAG,MAAwB,CAAA;gBAC7C,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,YAAY,CAAC,CAAA;gBAE5C,WAAW,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAC9B;gBAAA,CAAC,KAAK,IAAI,EAAE;oBACX,IAAI,CAAC;wBACH,IAAI,KAAK,EAAE,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;4BAC9B,IAAI,CAAC,CAAC,SAAS,KAAK,KAAK,EAAE,CAAC;gCAC1B,IAAI,WAAW,CAAA;gCACf,IAAI,CAAC;oCACH,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAA;gCACtC,CAAC;gCAAC,MAAM,CAAC;oCACP,WAAW,GAAG,CAAC,CAAC,MAAM,EAAE,CAAA;gCAC1B,CAAC;gCAED,MAAM,CAAC,QAAQ,CAAC;oCACd,MAAM,EAAE,MAAM,CAAC,MAAM;oCACrB,GAAG,EAAE,MAAM,CAAC,GAAG;oCACf,KAAK,EAAE,WAAW;iCACnB,CAAC,CAAA;4BACJ,CAAC;wBACH,CAAC;oBACH,CAAC;oBAAC,OAAO,KAAK,EAAE,CAAC;wBACf,OAAO,CAAC,KAAK,CAAC,oDAAoD,KAAK,GAAG,EAAE,KAAK,CAAC,CAAA;oBACpF,CAAC;gBACH,CAAC,CAAC,EAAE,CAAA;YACN,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,KAAK,CAAC,iCAAiC,KAAK,GAAG,EAAE,KAAK,CAAC,CAAA;YACjE,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO;QACL,aAAa,EAAE,WAAW;KAC3B,CAAA;AACH,CAAC,CACF,CAAA"}
1
+ {"version":3,"file":"kv.js","sourceRoot":"","sources":["../../src/actions/kv.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"subject.d.ts","sourceRoot":"","sources":["../../src/actions/subject.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,cAAc,EACd,cAAc,EACd,cAAc,EACd,YAAY,EACZ,mBAAmB,EACpB,MAAM,oBAAoB,CAAA;AAG3B,MAAM,MAAM,yBAAyB,GAAG;IACtC,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7B,IAAI,CAAC,EAAE,mBAAmB,CAAA;CAC3B,CAAA;AAED,eAAO,MAAM,uBAAuB,GAAI,YAErC;IACD,KAAK,EAAE;QACL,UAAU,EAAE,cAAc,GAAG,IAAI,CAAA;QACjC,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;QAC/C,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAA;KAC5D,CAAA;CACF;;CAmDA,CAAA;AAED,eAAO,MAAM,cAAc,GAAI,YAE5B;IACD,KAAK,EAAE;QACL,UAAU,EAAE,cAAc,GAAG,IAAI,CAAA;QACjC,OAAO,EAAE,MAAM,CAAA;QACf,OAAO,EAAE,GAAG,CAAA;QACZ,IAAI,CAAC,EAAE,cAAc,CAAA;QACrB,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,CAAA;KAC9B,CAAA;CACF,SAcA,CAAA;AAED,eAAO,MAAM,cAAc,GAAI,YAE5B;IACD,KAAK,EAAE;QACL,UAAU,EAAE,cAAc,GAAG,IAAI,CAAA;QACjC,OAAO,EAAE,MAAM,CAAA;QACf,OAAO,EAAE,GAAG,CAAA;QACZ,OAAO,CAAC,EAAE,cAAc,CAAA;QACxB,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE;YAAE,EAAE,EAAE,IAAI,CAAA;SAAE,GAAG;YAAE,EAAE,EAAE,KAAK,CAAC;YAAC,KAAK,EAAE,KAAK,CAAA;SAAE,KAAK,IAAI,CAAA;KAC/E,CAAA;CACF,SAaA,CAAA"}
1
+ {"version":3,"file":"subject.d.ts","sourceRoot":"","sources":["../../src/actions/subject.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,cAAc,EACd,cAAc,EACd,cAAc,EACd,YAAY,EACZ,mBAAmB,EACpB,MAAM,oBAAoB,CAAA;AAS3B,MAAM,MAAM,yBAAyB,GAAG;IACtC,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7B,IAAI,CAAC,EAAE,mBAAmB,CAAA;CAC3B,CAAA;AAED,eAAO,MAAM,uBAAuB,GAAI,YAErC;IACD,KAAK,EAAE;QACL,UAAU,EAAE,cAAc,GAAG,IAAI,CAAA;QACjC,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;QAC/C,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAA;KAC5D,CAAA;CACF;;CA2EA,CAAA;AAED,eAAO,MAAM,cAAc,GAAI,YAE5B;IACD,KAAK,EAAE;QACL,UAAU,EAAE,cAAc,GAAG,IAAI,CAAA;QACjC,OAAO,EAAE,MAAM,CAAA;QACf,OAAO,EAAE,GAAG,CAAA;QACZ,IAAI,CAAC,EAAE,cAAc,CAAA;QACrB,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,CAAA;KAC9B,CAAA;CACF,SA4CA,CAAA;AAED,eAAO,MAAM,cAAc,GAAI,YAE5B;IACD,KAAK,EAAE;QACL,UAAU,EAAE,cAAc,GAAG,IAAI,CAAA;QACjC,OAAO,EAAE,MAAM,CAAA;QACf,OAAO,EAAE,GAAG,CAAA;QACZ,OAAO,CAAC,EAAE,cAAc,CAAA;QACxB,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE;YAAE,EAAE,EAAE,IAAI,CAAA;SAAE,GAAG;YAAE,EAAE,EAAE,KAAK,CAAC;YAAC,KAAK,EAAE,KAAK,CAAA;SAAE,KAAK,IAAI,CAAA;KAC/E,CAAA;CACF,SA+BA,CAAA"}
@@ -1,4 +1,5 @@
1
1
  import { parseNatsResult } from './connection';
2
+ import { extractContextFromHeaders, injectContextIntoHeaders, recordError, withSpan, } from '../telemetry';
2
3
  export const subjectConsolidateState = ({ input, }) => {
3
4
  const { connection, currentSubscriptions, targetSubscriptions } = input;
4
5
  if (!connection) {
@@ -21,16 +22,28 @@ export const subjectConsolidateState = ({ input, }) => {
21
22
  for (const [subject, subscriptionConfig] of targetSubscriptions) {
22
23
  if (!currentSubscriptions.has(subject)) {
23
24
  try {
24
- const sub = connection.subscribe(subject, subscriptionConfig.opts);
25
+ // Short span around the synchronous subscribe() call. The iterator
26
+ // below is long-lived; we don't span its whole lifetime (indefinite
27
+ // spans are anti-pattern in most tracing backends).
28
+ const sub = withSpan('xstate.nats.subscribe', 'xstate.nats.error', { subject }, () => connection.subscribe(subject, subscriptionConfig.opts));
25
29
  (async () => {
26
30
  try {
27
31
  for await (const msg of sub) {
28
- try {
29
- subscriptionConfig?.callback(parseNatsResult(msg));
30
- }
31
- catch (callbackError) {
32
- console.error(`Callback error for subject "${subject}"`, callbackError);
33
- }
32
+ const parentCtx = extractContextFromHeaders(msg.headers);
33
+ await withSpan('xstate.nats.message', 'xstate.nats.error', {
34
+ subject,
35
+ 'payload.bytes': msg.data?.length,
36
+ }, (span) => {
37
+ try {
38
+ subscriptionConfig?.callback(parseNatsResult(msg));
39
+ }
40
+ catch (callbackError) {
41
+ // Record on span AND preserve the existing console.error
42
+ // so consumers without OTel still see the failure.
43
+ recordError(span, 'xstate.nats.error', callbackError);
44
+ console.error(`Callback error for subject "${subject}"`, callbackError);
45
+ }
46
+ }, parentCtx);
34
47
  }
35
48
  }
36
49
  catch (iteratorError) {
@@ -53,13 +66,35 @@ export const subjectRequest = ({ input, }) => {
53
66
  if (!connection) {
54
67
  throw new Error('NATS connection is not available');
55
68
  }
56
- connection
57
- .request(subject, payload, opts)
58
- .then((msg) => {
59
- callback(parseNatsResult(msg));
60
- })
61
- .catch((err) => {
62
- console.error(`RequestReply error for subject "${subject}"`, err);
69
+ const payloadBytes = payload instanceof Uint8Array
70
+ ? payload.byteLength
71
+ : typeof payload === 'string'
72
+ ? payload.length
73
+ : undefined;
74
+ void withSpan('xstate.nats.request', 'xstate.nats.error', {
75
+ subject,
76
+ 'payload.bytes': payloadBytes,
77
+ 'timeout.ms': opts?.timeout,
78
+ }, (span) => {
79
+ // Inject INSIDE the span so the replying service parents its handler
80
+ // span on this request span (not on an ambient context). RequestOptions
81
+ // declares `timeout` as required but nats-core only enforces it when
82
+ // opts is provided; cast preserves the "no opts = use conn default"
83
+ // contract.
84
+ const headers = injectContextIntoHeaders(opts?.headers);
85
+ const requestOpts = (opts ? { ...opts, headers } : { headers });
86
+ return connection
87
+ .request(subject, payload, requestOpts)
88
+ .then((msg) => {
89
+ callback(parseNatsResult(msg));
90
+ })
91
+ .catch((err) => {
92
+ // Record on span manually so we can swallow the rejection here —
93
+ // the original fire-and-forget API didn't propagate request errors
94
+ // to callers and changing that now would be a breaking behaviour.
95
+ recordError(span, 'xstate.nats.error', err);
96
+ console.error(`RequestReply error for subject "${subject}"`, err);
97
+ });
63
98
  });
64
99
  };
65
100
  export const subjectPublish = ({ input, }) => {
@@ -67,8 +102,19 @@ export const subjectPublish = ({ input, }) => {
67
102
  if (!connection) {
68
103
  throw new Error('NATS connection is not available');
69
104
  }
105
+ const payloadBytes = payload instanceof Uint8Array
106
+ ? payload.byteLength
107
+ : typeof payload === 'string'
108
+ ? payload.length
109
+ : undefined;
70
110
  try {
71
- connection.publish(subject, payload, options);
111
+ withSpan('xstate.nats.publish', 'xstate.nats.error', { subject, 'payload.bytes': payloadBytes }, () => {
112
+ // Inject INSIDE the span so downstream subscribers see THIS span as
113
+ // parent instead of whatever was ambient before publish.
114
+ const headers = injectContextIntoHeaders(options?.headers);
115
+ const publishOpts = { ...(options ?? {}), headers };
116
+ connection.publish(subject, payload, publishOpts);
117
+ });
72
118
  onPublishResult?.({ ok: true });
73
119
  }
74
120
  catch (callbackError) {
@@ -1 +1 @@
1
- {"version":3,"file":"subject.js","sourceRoot":"","sources":["../../src/actions/subject.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"subject.js","sourceRoot":"","sources":["../../src/actions/subject.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import type { Context, Span, Tracer } from '@opentelemetry/api';
2
+ import type { MsgHdrs } from '@nats-io/nats-core';
3
+ export declare function getTracer(): Tracer;
4
+ /**
5
+ * Record an error on a span using the OTel canonical pattern:
6
+ * recordException + ERROR status + a named event with the truncated stack.
7
+ */
8
+ export declare function recordError(span: Span, errorEventName: string, err: unknown): void;
9
+ /**
10
+ * Run `fn` inside an active span. On synchronous throw or async rejection the
11
+ * error is recorded, the span status is set to ERROR, and the error is
12
+ * re-thrown (callers keep their existing error-handling semantics).
13
+ */
14
+ export declare function withSpan<T>(name: string, errorEventName: string, attributes: Record<string, string | number | boolean | undefined>, fn: (span: Span) => T | Promise<T>, parentContext?: Context): T | Promise<T>;
15
+ /**
16
+ * Inject the active OTel context into a NATS headers object, creating one if
17
+ * none was supplied. Returns the headers object so the caller can pass it to
18
+ * `connection.publish` / `connection.request` via the `opts.headers` field.
19
+ */
20
+ export declare function injectContextIntoHeaders(existing?: MsgHdrs): MsgHdrs;
21
+ /**
22
+ * Extract a parent OTel context from an incoming NATS message's headers. When
23
+ * no traceparent is present the returned context is whatever was already
24
+ * active — callers can use it as the parent for a new span without a branch.
25
+ */
26
+ export declare function extractContextFromHeaders(hdrs: MsgHdrs | undefined): Context;
27
+ //# sourceMappingURL=telemetry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAE/D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AASjD,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAcD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,IAAI,CAQlF;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EACxB,IAAI,EAAE,MAAM,EACZ,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,EACjE,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,EAClC,aAAa,CAAC,EAAE,OAAO,GACtB,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CA4BhB;AA8BD;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,CAIpE;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,CAG5E"}
@@ -0,0 +1,125 @@
1
+ // OpenTelemetry helpers scoped to this package.
2
+ //
3
+ // `@opentelemetry/api` is declared as a peer dependency so the consumer
4
+ // controls the installed version. If the consumer never registers a
5
+ // TracerProvider / TextMapPropagator, every call here becomes a no-op.
6
+ //
7
+ // Design choices:
8
+ // - Single module-level tracer, named and versioned from package.json.
9
+ // - `withSpan` wraps the common try/catch/recordException/setStatus pattern
10
+ // so call sites stay readable.
11
+ // - `injectContextIntoHeaders` / `extractContextFromHeaders` bridge the
12
+ // OTel propagation API onto NATS `MsgHdrs`. Outgoing messages carry the
13
+ // active traceparent; incoming messages become the active context for
14
+ // the per-message span so cross-process traces nest automatically.
15
+ import { context as otelContext, propagation, SpanStatusCode, trace } from '@opentelemetry/api';
16
+ import { headers as natsHeaders } from '@nats-io/nats-core';
17
+ import pkg from '../package.json' with { type: 'json' };
18
+ const TRACER_NAME = '@jr200-labs/xstate-nats';
19
+ // Do not cache — `trace.getTracer` already returns a lightweight ProxyTracer,
20
+ // and caching across global-provider swaps (e.g. test teardown / re-register)
21
+ // would pin the delegate to a retired provider and silently lose spans.
22
+ export function getTracer() {
23
+ return trace.getTracer(TRACER_NAME, pkg.version);
24
+ }
25
+ // Truncate the stack trace before attaching it to a span event. Some backends
26
+ // (and the wire format itself) perform poorly with arbitrarily long strings;
27
+ // the first ~1KB is almost always enough to identify the site.
28
+ const MAX_STACK_LEN = 1024;
29
+ function truncateStack(err) {
30
+ if (!(err instanceof Error) || !err.stack)
31
+ return undefined;
32
+ return err.stack.length > MAX_STACK_LEN
33
+ ? err.stack.slice(0, MAX_STACK_LEN) + '...(truncated)'
34
+ : err.stack;
35
+ }
36
+ /**
37
+ * Record an error on a span using the OTel canonical pattern:
38
+ * recordException + ERROR status + a named event with the truncated stack.
39
+ */
40
+ export function recordError(span, errorEventName, err) {
41
+ const message = err instanceof Error ? err.message : String(err);
42
+ if (err instanceof Error) {
43
+ span.recordException(err);
44
+ }
45
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
46
+ const stack = truncateStack(err);
47
+ span.addEvent(errorEventName, stack ? { stack } : undefined);
48
+ }
49
+ /**
50
+ * Run `fn` inside an active span. On synchronous throw or async rejection the
51
+ * error is recorded, the span status is set to ERROR, and the error is
52
+ * re-thrown (callers keep their existing error-handling semantics).
53
+ */
54
+ export function withSpan(name, errorEventName, attributes, fn, parentContext) {
55
+ const tracer = getTracer();
56
+ const sanitized = sanitizeAttributes(attributes);
57
+ const ctx = parentContext ?? otelContext.active();
58
+ return tracer.startActiveSpan(name, { attributes: sanitized }, ctx, (span) => {
59
+ try {
60
+ const result = fn(span);
61
+ if (result && typeof result.then === 'function') {
62
+ return result.then((value) => {
63
+ span.end();
64
+ return value;
65
+ }, (err) => {
66
+ recordError(span, errorEventName, err);
67
+ span.end();
68
+ throw err;
69
+ });
70
+ }
71
+ span.end();
72
+ return result;
73
+ }
74
+ catch (err) {
75
+ recordError(span, errorEventName, err);
76
+ span.end();
77
+ throw err;
78
+ }
79
+ });
80
+ }
81
+ // OTel attributes cannot be `undefined`; strip those so call sites can pass
82
+ // optional values without a pre-filter.
83
+ function sanitizeAttributes(attrs) {
84
+ const out = {};
85
+ for (const [k, v] of Object.entries(attrs)) {
86
+ if (v !== undefined)
87
+ out[k] = v;
88
+ }
89
+ return out;
90
+ }
91
+ // Carrier adapters: NATS `MsgHdrs` exposes `.set(k, v)`, `.get(k)`, `.keys()`
92
+ // which maps cleanly to the OTel TextMapSetter / TextMapGetter interface.
93
+ const natsHeaderSetter = {
94
+ set: (carrier, key, value) => {
95
+ carrier.set(key, value);
96
+ },
97
+ };
98
+ const natsHeaderGetter = {
99
+ get: (carrier, key) => {
100
+ const v = carrier?.get(key);
101
+ return v || undefined;
102
+ },
103
+ keys: (carrier) => carrier?.keys() ?? [],
104
+ };
105
+ /**
106
+ * Inject the active OTel context into a NATS headers object, creating one if
107
+ * none was supplied. Returns the headers object so the caller can pass it to
108
+ * `connection.publish` / `connection.request` via the `opts.headers` field.
109
+ */
110
+ export function injectContextIntoHeaders(existing) {
111
+ const hdrs = existing ?? natsHeaders();
112
+ propagation.inject(otelContext.active(), hdrs, natsHeaderSetter);
113
+ return hdrs;
114
+ }
115
+ /**
116
+ * Extract a parent OTel context from an incoming NATS message's headers. When
117
+ * no traceparent is present the returned context is whatever was already
118
+ * active — callers can use it as the parent for a new span without a branch.
119
+ */
120
+ export function extractContextFromHeaders(hdrs) {
121
+ if (!hdrs)
122
+ return otelContext.active();
123
+ return propagation.extract(otelContext.active(), hdrs, natsHeaderGetter);
124
+ }
125
+ //# sourceMappingURL=telemetry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jr200-labs/xstate-nats",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "XState machine for NATS",
5
5
  "license": "MIT",
6
6
  "author": "Jayshan Raghunandan",
@@ -37,12 +37,24 @@
37
37
  "@nats-io/obj": "^3.3.1",
38
38
  "xstate": "^5.30.0"
39
39
  },
40
+ "peerDependencies": {
41
+ "@opentelemetry/api": "^1.9.0"
42
+ },
40
43
  "devDependencies": {
41
- "@vitest/coverage-v8": "^4.1.3",
44
+ "@eslint/js": "^10.0.1",
45
+ "@opentelemetry/api": "^1.9.0",
46
+ "@opentelemetry/context-async-hooks": "^2.7.0",
47
+ "@opentelemetry/core": "^2.7.0",
48
+ "@opentelemetry/sdk-trace-base": "^2.7.0",
49
+ "@vitest/coverage-v8": "^4.1.4",
50
+ "eslint": "^10.2.1",
51
+ "globals": "^17.5.0",
42
52
  "husky": "^9.1.7",
43
- "prettier": "^3.8.1",
53
+ "prettier": "^3.8.3",
54
+ "syncpack": "^14.3.0",
44
55
  "typescript": "^6.0.2",
45
- "vitest": "^4.1.3"
56
+ "typescript-eslint": "^8.58.2",
57
+ "vitest": "^4.1.4"
46
58
  },
47
59
  "scripts": {
48
60
  "build": "tsc",
@@ -50,6 +62,6 @@
50
62
  "test:run": "vitest run",
51
63
  "test:watch": "vitest",
52
64
  "prettier": "prettier --write \"**/*.{ts,mjs,js,json,md,yml,yaml}\"",
53
- "lint": "tsc --noEmit"
65
+ "lint": "tsc --noEmit && eslint \"src/**/*.{ts,tsx}\""
54
66
  }
55
67
  }
@@ -10,6 +10,7 @@ import {
10
10
  import { KvEntry } from '@nats-io/kv'
11
11
  import { type AuthConfig } from './types'
12
12
  import { sendParent } from 'xstate'
13
+ import { withSpan } from '../telemetry'
13
14
 
14
15
  const makeAuthConfig = (auth?: AuthConfig) => {
15
16
  if (!auth) {
@@ -68,7 +69,16 @@ export const connectToNats = fromPromise(
68
69
  sendParent({ type: 'NATS_CONNECTION.DISCONNECTED', status })
69
70
  break
70
71
  case 'reconnect':
71
- sendParent({ type: 'NATS_CONNECTION.RECONNECT', status })
72
+ // Per-event span so reconnect attempts become discoverable in
73
+ // the trace backend (searchable, durations, span counts).
74
+ withSpan(
75
+ 'xstate.nats.reconnect',
76
+ 'xstate.nats.error',
77
+ { 'reconnect.type': type },
78
+ () => {
79
+ sendParent({ type: 'NATS_CONNECTION.RECONNECT', status })
80
+ },
81
+ )
72
82
  break
73
83
  case 'error':
74
84
  sendParent({ type: 'NATS_CONNECTION.ERROR', status })
@@ -83,10 +93,24 @@ export const connectToNats = fromPromise(
83
93
  // console.debug('Received ping, pong sent automatically')
84
94
  break
85
95
  case 'forceReconnect':
86
- sendParent({ type: 'NATS_CONNECTION.RECONNECT', status })
96
+ withSpan(
97
+ 'xstate.nats.reconnect',
98
+ 'xstate.nats.error',
99
+ { 'reconnect.type': type },
100
+ () => {
101
+ sendParent({ type: 'NATS_CONNECTION.RECONNECT', status })
102
+ },
103
+ )
87
104
  break
88
105
  case 'reconnecting':
89
- sendParent({ type: 'NATS_CONNECTION.RECONNECTING', status })
106
+ withSpan(
107
+ 'xstate.nats.reconnect',
108
+ 'xstate.nats.error',
109
+ { 'reconnect.type': type },
110
+ () => {
111
+ sendParent({ type: 'NATS_CONNECTION.RECONNECTING', status })
112
+ },
113
+ )
90
114
  break
91
115
  case 'slowConsumer':
92
116
  console.debug('SLOW_CONSUMER', status)
package/src/actions/kv.ts CHANGED
@@ -2,6 +2,7 @@ import { NatsConnection, QueuedIterator } from '@nats-io/nats-core'
2
2
  import { Kvm, KvWatchEntry, KvWatchOptions } from '@nats-io/kv'
3
3
  import { Pair } from '../utils'
4
4
  import { fromPromise } from 'xstate'
5
+ import { recordError, withSpan } from '../telemetry'
5
6
 
6
7
  export class KvSubscriptionKey extends Pair<string, string> {}
7
8
 
@@ -54,25 +55,52 @@ export const kvConsolidateState = fromPromise(
54
55
  const kv = await input.kvm.open(config.bucket)
55
56
 
56
57
  const watchOptions = config as KvWatchOptions
57
- const watcher = await kv.watch(watchOptions)
58
+ // Short span around the synchronous-ish watch() setup — the watcher
59
+ // iterator itself is long-lived, so each received entry gets its
60
+ // own span below rather than one indefinite parent.
61
+ const watcher = (await withSpan(
62
+ 'xstate.nats.kv.watch',
63
+ 'xstate.nats.kv.error',
64
+ { bucket: config.bucket, key: config.key },
65
+ () => kv.watch(watchOptions),
66
+ )) as QueuedIterator<KvWatchEntry>
58
67
 
59
68
  syncedState.set(kvKey, watcher)
60
69
  ;(async () => {
61
70
  try {
62
71
  for await (const e of watcher) {
63
72
  if (e.operation !== 'DEL') {
64
- let parsedValue
65
- try {
66
- parsedValue = JSON.parse(e.string())
67
- } catch {
68
- parsedValue = e.string()
69
- }
73
+ await withSpan(
74
+ 'xstate.nats.kv.entry',
75
+ 'xstate.nats.kv.error',
76
+ {
77
+ bucket: config.bucket,
78
+ key: config.key,
79
+ operation: e.operation,
80
+ },
81
+ (span) => {
82
+ let parsedValue
83
+ try {
84
+ parsedValue = JSON.parse(e.string())
85
+ } catch {
86
+ parsedValue = e.string()
87
+ }
70
88
 
71
- config.callback({
72
- bucket: config.bucket,
73
- key: config.key,
74
- value: parsedValue,
75
- })
89
+ try {
90
+ config.callback({
91
+ bucket: config.bucket,
92
+ key: config.key,
93
+ value: parsedValue,
94
+ })
95
+ } catch (callbackError) {
96
+ recordError(span, 'xstate.nats.kv.error', callbackError)
97
+ console.error(
98
+ `KV_SUBSCRIBE (connected): Callback error for ${kvKey}:`,
99
+ callbackError,
100
+ )
101
+ }
102
+ },
103
+ )
76
104
  }
77
105
  }
78
106
  } catch (error) {
@@ -0,0 +1,199 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { SpanStatusCode } from '@opentelemetry/api'
3
+ import { headers as natsHeaders } from '@nats-io/nats-core'
4
+ import { setupInMemoryTracer, type OtelTestHarness } from '../../test/otel-setup'
5
+ import { subjectConsolidateState, subjectPublish, subjectRequest } from './subject'
6
+ import type { SubjectSubscriptionConfig } from './subject'
7
+
8
+ vi.mock('./connection', () => ({
9
+ parseNatsResult: vi.fn((msg: any) => {
10
+ if (!msg) return null
11
+ try {
12
+ return msg.json()
13
+ } catch {
14
+ return msg.string()
15
+ }
16
+ }),
17
+ }))
18
+
19
+ function createMockConnection() {
20
+ return {
21
+ subscribe: vi.fn(),
22
+ request: vi.fn(),
23
+ publish: vi.fn(),
24
+ } as any
25
+ }
26
+
27
+ describe('subject telemetry', () => {
28
+ let harness: OtelTestHarness
29
+
30
+ beforeEach(() => {
31
+ harness = setupInMemoryTracer()
32
+ vi.spyOn(console, 'error').mockImplementation(() => {})
33
+ })
34
+
35
+ afterEach(() => {
36
+ harness.teardown()
37
+ vi.restoreAllMocks()
38
+ })
39
+
40
+ it('emits xstate.nats.subscribe span when adding a subscription', () => {
41
+ const connection = createMockConnection()
42
+ connection.subscribe.mockReturnValue({
43
+ unsubscribe: vi.fn(),
44
+ [Symbol.asyncIterator]: () => ({ next: () => new Promise(() => {}) }),
45
+ })
46
+
47
+ const target = new Map<string, SubjectSubscriptionConfig>([
48
+ ['t.sub', { subject: 't.sub', callback: vi.fn() }],
49
+ ])
50
+
51
+ subjectConsolidateState({
52
+ input: { connection, currentSubscriptions: new Map(), targetSubscriptions: target },
53
+ })
54
+
55
+ const spans = harness.exporter.getFinishedSpans()
56
+ const sub = spans.find((s) => s.name === 'xstate.nats.subscribe')
57
+ expect(sub).toBeDefined()
58
+ expect(sub!.attributes.subject).toBe('t.sub')
59
+ expect(sub!.status.code).not.toBe(SpanStatusCode.ERROR)
60
+ })
61
+
62
+ it('emits xstate.nats.publish span with payload bytes', () => {
63
+ const connection = createMockConnection()
64
+ subjectPublish({
65
+ input: { connection, subject: 't.pub', payload: 'hello' },
66
+ })
67
+
68
+ const spans = harness.exporter.getFinishedSpans()
69
+ const pub = spans.find((s) => s.name === 'xstate.nats.publish')
70
+ expect(pub).toBeDefined()
71
+ expect(pub!.attributes.subject).toBe('t.pub')
72
+ expect(pub!.attributes['payload.bytes']).toBe(5)
73
+ })
74
+
75
+ it('injects traceparent into publish headers', () => {
76
+ const connection = createMockConnection()
77
+ subjectPublish({
78
+ input: { connection, subject: 't.pub', payload: 'x' },
79
+ })
80
+
81
+ const call = connection.publish.mock.calls[0]
82
+ const opts = call[2]
83
+ expect(opts.headers).toBeDefined()
84
+ // traceparent header is set by W3CTraceContextPropagator
85
+ expect(opts.headers.get('traceparent')).toBeTruthy()
86
+ })
87
+
88
+ it('records error on publish span when connection.publish throws', () => {
89
+ const connection = createMockConnection()
90
+ connection.publish.mockImplementation(() => {
91
+ throw new Error('boom')
92
+ })
93
+
94
+ subjectPublish({
95
+ input: { connection, subject: 't.fail', payload: 'x' },
96
+ })
97
+
98
+ const spans = harness.exporter.getFinishedSpans()
99
+ const pub = spans.find((s) => s.name === 'xstate.nats.publish')
100
+ expect(pub).toBeDefined()
101
+ expect(pub!.status.code).toBe(SpanStatusCode.ERROR)
102
+ expect(pub!.events.some((e) => e.name === 'xstate.nats.error')).toBe(true)
103
+ })
104
+
105
+ it('emits xstate.nats.request span with request attrs', async () => {
106
+ const connection = createMockConnection()
107
+ connection.request.mockResolvedValue({
108
+ json: () => ({ ok: true }),
109
+ string: () => '{"ok":true}',
110
+ })
111
+
112
+ subjectRequest({
113
+ input: {
114
+ connection,
115
+ subject: 't.req',
116
+ payload: 'x',
117
+ opts: { timeout: 2500 } as any,
118
+ callback: vi.fn(),
119
+ },
120
+ })
121
+
122
+ await vi.waitFor(() => {
123
+ expect(
124
+ harness.exporter.getFinishedSpans().some((s) => s.name === 'xstate.nats.request'),
125
+ ).toBe(true)
126
+ })
127
+
128
+ const req = harness.exporter.getFinishedSpans().find((s) => s.name === 'xstate.nats.request')!
129
+ expect(req.attributes.subject).toBe('t.req')
130
+ expect(req.attributes['timeout.ms']).toBe(2500)
131
+ })
132
+
133
+ it('records error on request span when connection.request rejects', async () => {
134
+ const connection = createMockConnection()
135
+ connection.request.mockRejectedValue(new Error('nope'))
136
+
137
+ subjectRequest({
138
+ input: { connection, subject: 't.req', payload: 'x', callback: vi.fn() },
139
+ })
140
+
141
+ await vi.waitFor(() => {
142
+ const req = harness.exporter.getFinishedSpans().find((s) => s.name === 'xstate.nats.request')
143
+ expect(req?.status.code).toBe(SpanStatusCode.ERROR)
144
+ })
145
+ })
146
+
147
+ it('parents per-message span on extracted traceparent', async () => {
148
+ const connection = createMockConnection()
149
+ // Craft a headers object containing a known traceparent so the extractor
150
+ // has something to parent on.
151
+ const incomingHdrs = natsHeaders()
152
+ incomingHdrs.set('traceparent', '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01')
153
+
154
+ let resolveIter: () => void
155
+ const iterDone = new Promise<void>((r) => (resolveIter = r))
156
+ let delivered = false
157
+ connection.subscribe.mockReturnValue({
158
+ unsubscribe: vi.fn(),
159
+ [Symbol.asyncIterator]: () => ({
160
+ next: () => {
161
+ if (!delivered) {
162
+ delivered = true
163
+ return Promise.resolve({
164
+ value: {
165
+ headers: incomingHdrs,
166
+ json: () => ({ hi: 1 }),
167
+ string: () => '{"hi":1}',
168
+ data: new Uint8Array([1, 2, 3]),
169
+ },
170
+ done: false,
171
+ })
172
+ }
173
+ resolveIter!()
174
+ return new Promise(() => {})
175
+ },
176
+ }),
177
+ })
178
+
179
+ const target = new Map<string, SubjectSubscriptionConfig>([
180
+ ['t.msg', { subject: 't.msg', callback: vi.fn() }],
181
+ ])
182
+ subjectConsolidateState({
183
+ input: { connection, currentSubscriptions: new Map(), targetSubscriptions: target },
184
+ })
185
+
186
+ await iterDone
187
+ await vi.waitFor(() => {
188
+ expect(
189
+ harness.exporter.getFinishedSpans().some((s) => s.name === 'xstate.nats.message'),
190
+ ).toBe(true)
191
+ })
192
+
193
+ const msgSpan = harness.exporter
194
+ .getFinishedSpans()
195
+ .find((s) => s.name === 'xstate.nats.message')!
196
+ expect(msgSpan.spanContext().traceId).toBe('0af7651916cd43dd8448eb211c80319c')
197
+ expect(msgSpan.attributes.subject).toBe('t.msg')
198
+ })
199
+ })
@@ -332,7 +332,11 @@ describe('subjectRequest', () => {
332
332
  expect(callback).toHaveBeenCalled()
333
333
  })
334
334
 
335
- expect(connection.request).toHaveBeenCalledWith('test.request', { data: 1 }, { timeout: 5000 })
335
+ expect(connection.request).toHaveBeenCalledWith(
336
+ 'test.request',
337
+ { data: 1 },
338
+ expect.objectContaining({ timeout: 5000 }),
339
+ )
336
340
  })
337
341
 
338
342
  it('should handle request errors', async () => {
@@ -387,7 +391,11 @@ describe('subjectPublish', () => {
387
391
  },
388
392
  })
389
393
 
390
- expect(connection.publish).toHaveBeenCalledWith('test.publish', { msg: 'hello' }, undefined)
394
+ expect(connection.publish).toHaveBeenCalledWith(
395
+ 'test.publish',
396
+ { msg: 'hello' },
397
+ expect.objectContaining({ headers: expect.anything() }),
398
+ )
391
399
  })
392
400
 
393
401
  it('should call onPublishResult with ok on success', () => {
@@ -419,7 +427,11 @@ describe('subjectPublish', () => {
419
427
  },
420
428
  })
421
429
 
422
- expect(connection.publish).toHaveBeenCalledWith('test.publish', 'data', options)
430
+ expect(connection.publish).toHaveBeenCalledWith(
431
+ 'test.publish',
432
+ 'data',
433
+ expect.objectContaining({ headers: expect.anything() }),
434
+ )
423
435
  })
424
436
 
425
437
  it('should call onPublishResult with error on failure', () => {
@@ -7,6 +7,12 @@ import {
7
7
  SubscriptionOptions,
8
8
  } from '@nats-io/nats-core'
9
9
  import { parseNatsResult } from './connection'
10
+ import {
11
+ extractContextFromHeaders,
12
+ injectContextIntoHeaders,
13
+ recordError,
14
+ withSpan,
15
+ } from '../telemetry'
10
16
 
11
17
  export type SubjectSubscriptionConfig = {
12
18
  subject: string
@@ -46,17 +52,41 @@ export const subjectConsolidateState = ({
46
52
  for (const [subject, subscriptionConfig] of targetSubscriptions) {
47
53
  if (!currentSubscriptions.has(subject)) {
48
54
  try {
49
- const sub = connection.subscribe(subject, subscriptionConfig.opts)
55
+ // Short span around the synchronous subscribe() call. The iterator
56
+ // below is long-lived; we don't span its whole lifetime (indefinite
57
+ // spans are anti-pattern in most tracing backends).
58
+ const sub = withSpan('xstate.nats.subscribe', 'xstate.nats.error', { subject }, () =>
59
+ connection.subscribe(subject, subscriptionConfig.opts),
60
+ ) as Subscription
50
61
 
51
- // Set up the message handler
62
+ // Message loop: each received message starts its own span, parented
63
+ // on the traceparent extracted from the message headers (OTel
64
+ // messaging semconv). If the publisher did not propagate context the
65
+ // extracted context falls back to the ambient one, so the span
66
+ // simply becomes a root.
52
67
  ;(async () => {
53
68
  try {
54
69
  for await (const msg of sub) {
55
- try {
56
- subscriptionConfig?.callback(parseNatsResult(msg))
57
- } catch (callbackError) {
58
- console.error(`Callback error for subject "${subject}"`, callbackError)
59
- }
70
+ const parentCtx = extractContextFromHeaders((msg as Msg).headers)
71
+ await withSpan(
72
+ 'xstate.nats.message',
73
+ 'xstate.nats.error',
74
+ {
75
+ subject,
76
+ 'payload.bytes': (msg as Msg).data?.length,
77
+ },
78
+ (span) => {
79
+ try {
80
+ subscriptionConfig?.callback(parseNatsResult(msg))
81
+ } catch (callbackError) {
82
+ // Record on span AND preserve the existing console.error
83
+ // so consumers without OTel still see the failure.
84
+ recordError(span, 'xstate.nats.error', callbackError)
85
+ console.error(`Callback error for subject "${subject}"`, callbackError)
86
+ }
87
+ },
88
+ parentCtx,
89
+ )
60
90
  }
61
91
  } catch (iteratorError) {
62
92
  console.error(`Iterator error for subject "${subject}"`, iteratorError)
@@ -91,14 +121,44 @@ export const subjectRequest = ({
91
121
  throw new Error('NATS connection is not available')
92
122
  }
93
123
 
94
- connection
95
- .request(subject, payload, opts)
96
- .then((msg: Msg) => {
97
- callback(parseNatsResult(msg))
98
- })
99
- .catch((err) => {
100
- console.error(`RequestReply error for subject "${subject}"`, err)
101
- })
124
+ const payloadBytes =
125
+ payload instanceof Uint8Array
126
+ ? payload.byteLength
127
+ : typeof payload === 'string'
128
+ ? payload.length
129
+ : undefined
130
+
131
+ void withSpan(
132
+ 'xstate.nats.request',
133
+ 'xstate.nats.error',
134
+ {
135
+ subject,
136
+ 'payload.bytes': payloadBytes,
137
+ 'timeout.ms': opts?.timeout,
138
+ },
139
+ (span) => {
140
+ // Inject INSIDE the span so the replying service parents its handler
141
+ // span on this request span (not on an ambient context). RequestOptions
142
+ // declares `timeout` as required but nats-core only enforces it when
143
+ // opts is provided; cast preserves the "no opts = use conn default"
144
+ // contract.
145
+ const headers = injectContextIntoHeaders(opts?.headers)
146
+ const requestOpts = (opts ? { ...opts, headers } : { headers }) as RequestOptions
147
+
148
+ return connection
149
+ .request(subject, payload, requestOpts)
150
+ .then((msg: Msg) => {
151
+ callback(parseNatsResult(msg))
152
+ })
153
+ .catch((err) => {
154
+ // Record on span manually so we can swallow the rejection here —
155
+ // the original fire-and-forget API didn't propagate request errors
156
+ // to callers and changing that now would be a breaking behaviour.
157
+ recordError(span, 'xstate.nats.error', err)
158
+ console.error(`RequestReply error for subject "${subject}"`, err)
159
+ })
160
+ },
161
+ )
102
162
  }
103
163
 
104
164
  export const subjectPublish = ({
@@ -117,8 +177,26 @@ export const subjectPublish = ({
117
177
  throw new Error('NATS connection is not available')
118
178
  }
119
179
 
180
+ const payloadBytes =
181
+ payload instanceof Uint8Array
182
+ ? payload.byteLength
183
+ : typeof payload === 'string'
184
+ ? payload.length
185
+ : undefined
186
+
120
187
  try {
121
- connection.publish(subject, payload, options)
188
+ withSpan(
189
+ 'xstate.nats.publish',
190
+ 'xstate.nats.error',
191
+ { subject, 'payload.bytes': payloadBytes },
192
+ () => {
193
+ // Inject INSIDE the span so downstream subscribers see THIS span as
194
+ // parent instead of whatever was ambient before publish.
195
+ const headers = injectContextIntoHeaders(options?.headers)
196
+ const publishOpts: PublishOptions = { ...(options ?? {}), headers }
197
+ connection.publish(subject, payload, publishOpts)
198
+ },
199
+ )
122
200
  onPublishResult?.({ ok: true })
123
201
  } catch (callbackError) {
124
202
  console.error(`Publish callback error for subject "${subject}"`, callbackError)
@@ -2,9 +2,13 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
2
  import { createActor, fromPromise, sendTo, setup, assign } from 'xstate'
3
3
  import { kvManagerLogic } from './kv'
4
4
 
5
- vi.mock('@nats-io/nats-core', () => ({
6
- wsconnect: vi.fn(),
7
- }))
5
+ vi.mock('@nats-io/nats-core', async () => {
6
+ const actual = await vi.importActual<typeof import('@nats-io/nats-core')>('@nats-io/nats-core')
7
+ return {
8
+ ...actual,
9
+ wsconnect: vi.fn(),
10
+ }
11
+ })
8
12
 
9
13
  // We'll set up specific Kvm mock behavior per test
10
14
  const mockKvStore = {
@@ -2,10 +2,14 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
2
  import { createActor, fromPromise, createMachine, sendParent } from 'xstate'
3
3
  import { natsMachine } from './root'
4
4
 
5
- vi.mock('@nats-io/nats-core', () => ({
6
- wsconnect: vi.fn(),
7
- credsAuthenticator: vi.fn(),
8
- }))
5
+ vi.mock('@nats-io/nats-core', async () => {
6
+ const actual = await vi.importActual<typeof import('@nats-io/nats-core')>('@nats-io/nats-core')
7
+ return {
8
+ ...actual,
9
+ wsconnect: vi.fn(),
10
+ credsAuthenticator: vi.fn(),
11
+ }
12
+ })
9
13
 
10
14
  vi.mock('@nats-io/kv', () => ({
11
15
  Kvm: vi.fn().mockImplementation(() => ({})),
@@ -2,9 +2,13 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
2
  import { createActor, createMachine, assign, fromPromise, sendTo, setup } from 'xstate'
3
3
  import { subjectManagerLogic } from './subject'
4
4
 
5
- vi.mock('@nats-io/nats-core', () => ({
6
- wsconnect: vi.fn(),
7
- }))
5
+ vi.mock('@nats-io/nats-core', async () => {
6
+ const actual = await vi.importActual<typeof import('@nats-io/nats-core')>('@nats-io/nats-core')
7
+ return {
8
+ ...actual,
9
+ wsconnect: vi.fn(),
10
+ }
11
+ })
8
12
 
9
13
  function createMockConnection() {
10
14
  return {
@@ -195,7 +199,11 @@ describe('subjectManagerLogic', () => {
195
199
  payload: { msg: 'hello' },
196
200
  })
197
201
 
198
- expect(connection.publish).toHaveBeenCalledWith('test.pub', { msg: 'hello' }, undefined)
202
+ expect(connection.publish).toHaveBeenCalledWith(
203
+ 'test.pub',
204
+ { msg: 'hello' },
205
+ expect.objectContaining({ headers: expect.anything() }),
206
+ )
199
207
  parentActor.stop()
200
208
  })
201
209
 
@@ -214,7 +222,11 @@ describe('subjectManagerLogic', () => {
214
222
  callback,
215
223
  })
216
224
 
217
- expect(connection.request).toHaveBeenCalledWith('test.req', { data: 1 }, undefined)
225
+ expect(connection.request).toHaveBeenCalledWith(
226
+ 'test.req',
227
+ { data: 1 },
228
+ expect.objectContaining({ headers: expect.anything() }),
229
+ )
218
230
  parentActor.stop()
219
231
  })
220
232
 
@@ -0,0 +1,145 @@
1
+ // OpenTelemetry helpers scoped to this package.
2
+ //
3
+ // `@opentelemetry/api` is declared as a peer dependency so the consumer
4
+ // controls the installed version. If the consumer never registers a
5
+ // TracerProvider / TextMapPropagator, every call here becomes a no-op.
6
+ //
7
+ // Design choices:
8
+ // - Single module-level tracer, named and versioned from package.json.
9
+ // - `withSpan` wraps the common try/catch/recordException/setStatus pattern
10
+ // so call sites stay readable.
11
+ // - `injectContextIntoHeaders` / `extractContextFromHeaders` bridge the
12
+ // OTel propagation API onto NATS `MsgHdrs`. Outgoing messages carry the
13
+ // active traceparent; incoming messages become the active context for
14
+ // the per-message span so cross-process traces nest automatically.
15
+
16
+ import type { Context, Span, Tracer } from '@opentelemetry/api'
17
+ import { context as otelContext, propagation, SpanStatusCode, trace } from '@opentelemetry/api'
18
+ import type { MsgHdrs } from '@nats-io/nats-core'
19
+ import { headers as natsHeaders } from '@nats-io/nats-core'
20
+ import pkg from '../package.json' with { type: 'json' }
21
+
22
+ const TRACER_NAME = '@jr200-labs/xstate-nats'
23
+
24
+ // Do not cache — `trace.getTracer` already returns a lightweight ProxyTracer,
25
+ // and caching across global-provider swaps (e.g. test teardown / re-register)
26
+ // would pin the delegate to a retired provider and silently lose spans.
27
+ export function getTracer(): Tracer {
28
+ return trace.getTracer(TRACER_NAME, pkg.version)
29
+ }
30
+
31
+ // Truncate the stack trace before attaching it to a span event. Some backends
32
+ // (and the wire format itself) perform poorly with arbitrarily long strings;
33
+ // the first ~1KB is almost always enough to identify the site.
34
+ const MAX_STACK_LEN = 1024
35
+
36
+ function truncateStack(err: unknown): string | undefined {
37
+ if (!(err instanceof Error) || !err.stack) return undefined
38
+ return err.stack.length > MAX_STACK_LEN
39
+ ? err.stack.slice(0, MAX_STACK_LEN) + '...(truncated)'
40
+ : err.stack
41
+ }
42
+
43
+ /**
44
+ * Record an error on a span using the OTel canonical pattern:
45
+ * recordException + ERROR status + a named event with the truncated stack.
46
+ */
47
+ export function recordError(span: Span, errorEventName: string, err: unknown): void {
48
+ const message = err instanceof Error ? err.message : String(err)
49
+ if (err instanceof Error) {
50
+ span.recordException(err)
51
+ }
52
+ span.setStatus({ code: SpanStatusCode.ERROR, message })
53
+ const stack = truncateStack(err)
54
+ span.addEvent(errorEventName, stack ? { stack } : undefined)
55
+ }
56
+
57
+ /**
58
+ * Run `fn` inside an active span. On synchronous throw or async rejection the
59
+ * error is recorded, the span status is set to ERROR, and the error is
60
+ * re-thrown (callers keep their existing error-handling semantics).
61
+ */
62
+ export function withSpan<T>(
63
+ name: string,
64
+ errorEventName: string,
65
+ attributes: Record<string, string | number | boolean | undefined>,
66
+ fn: (span: Span) => T | Promise<T>,
67
+ parentContext?: Context,
68
+ ): T | Promise<T> {
69
+ const tracer = getTracer()
70
+ const sanitized = sanitizeAttributes(attributes)
71
+ const ctx = parentContext ?? otelContext.active()
72
+ return tracer.startActiveSpan(name, { attributes: sanitized }, ctx, (span) => {
73
+ try {
74
+ const result = fn(span)
75
+ if (result && typeof (result as Promise<T>).then === 'function') {
76
+ return (result as Promise<T>).then(
77
+ (value) => {
78
+ span.end()
79
+ return value
80
+ },
81
+ (err: unknown) => {
82
+ recordError(span, errorEventName, err)
83
+ span.end()
84
+ throw err
85
+ },
86
+ ) as unknown as T
87
+ }
88
+ span.end()
89
+ return result
90
+ } catch (err) {
91
+ recordError(span, errorEventName, err)
92
+ span.end()
93
+ throw err
94
+ }
95
+ })
96
+ }
97
+
98
+ // OTel attributes cannot be `undefined`; strip those so call sites can pass
99
+ // optional values without a pre-filter.
100
+ function sanitizeAttributes(
101
+ attrs: Record<string, string | number | boolean | undefined>,
102
+ ): Record<string, string | number | boolean> {
103
+ const out: Record<string, string | number | boolean> = {}
104
+ for (const [k, v] of Object.entries(attrs)) {
105
+ if (v !== undefined) out[k] = v
106
+ }
107
+ return out
108
+ }
109
+
110
+ // Carrier adapters: NATS `MsgHdrs` exposes `.set(k, v)`, `.get(k)`, `.keys()`
111
+ // which maps cleanly to the OTel TextMapSetter / TextMapGetter interface.
112
+ const natsHeaderSetter = {
113
+ set: (carrier: MsgHdrs, key: string, value: string) => {
114
+ carrier.set(key, value)
115
+ },
116
+ }
117
+
118
+ const natsHeaderGetter = {
119
+ get: (carrier: MsgHdrs | undefined, key: string): string | undefined => {
120
+ const v = carrier?.get(key)
121
+ return v || undefined
122
+ },
123
+ keys: (carrier: MsgHdrs | undefined): string[] => carrier?.keys() ?? [],
124
+ }
125
+
126
+ /**
127
+ * Inject the active OTel context into a NATS headers object, creating one if
128
+ * none was supplied. Returns the headers object so the caller can pass it to
129
+ * `connection.publish` / `connection.request` via the `opts.headers` field.
130
+ */
131
+ export function injectContextIntoHeaders(existing?: MsgHdrs): MsgHdrs {
132
+ const hdrs = existing ?? natsHeaders()
133
+ propagation.inject(otelContext.active(), hdrs, natsHeaderSetter)
134
+ return hdrs
135
+ }
136
+
137
+ /**
138
+ * Extract a parent OTel context from an incoming NATS message's headers. When
139
+ * no traceparent is present the returned context is whatever was already
140
+ * active — callers can use it as the parent for a new span without a branch.
141
+ */
142
+ export function extractContextFromHeaders(hdrs: MsgHdrs | undefined): Context {
143
+ if (!hdrs) return otelContext.active()
144
+ return propagation.extract(otelContext.active(), hdrs, natsHeaderGetter)
145
+ }