A fixed wrapper around Node.js's tracingChannel that properly propagates otel context
npm install otel-tracing-channelA lightweight wrapper around Node.js's tracingChannel that properly propagates OpenTelemetry context.
Node.js's native tracingChannel doesn't automatically propagate OpenTelemetry context between the start event and the callback execution. This breaks distributed tracing when using diagnostic channels.
While creating spans works fine, the parent-child relationship between spans is broken - traces get created as siblings rather than children, which can paint a misleading picture for end users.
This package solves the problem by binding OpenTelemetry's internal AsyncLocalStorage to the tracing channel's start event using bindStore. This ensures that:
- The OpenTelemetry context is automatically propagated throughout the traced operation
- Parent-child span relationships are maintained correctly
``bash`
npm install otel-tracing-channel
`typescript
import { tracingChannel } from 'otel-tracing-channel';
import { trace } from '@opentelemetry/api';
// Create a channel with a transform function that creates your span
const channel = tracingChannel('my-operation', (data) => {
// Create and return a span from the channel data
const span = trace.getTracer('my-app').startSpan('my-operation', {
attributes: {
userId: data.userId,
// ... other attributes from data
},
});
// Return OTEL Span
return span;
});
// Subscribe to events to handle span lifecycle
channel.subscribe({
asyncEnd(data) {
// The span is available on data.span
data.span?.end();
},
error(data) {
data.span?.recordException(data.error);
data.span?.end();
},
});
await channel.tracePromise(
async () => {
// Your async work - OpenTelemetry context is properly propagated
await doSomething();
},
{ userId: '123' },
);
`
`typescript
import { tracingChannel } from 'otel-tracing-channel';
import * as Sentry from '@sentry/node';
const channel = tracingChannel('database:query', (data) => {
return Sentry.startSpanManual(
{
name: 'db.query',
op: 'db',
attributes: {
'db.statement': data.query,
'db.system': 'postgresql',
},
},
(span) => span,
);
});
channel.subscribe({
asyncEnd: (data) => {
data.span?.end();
},
});
// Execute with automatic context propagation
await channel.tracePromise(
async () => {
return await db.query('SELECT * FROM users');
},
{ query: 'SELECT * FROM users' },
);
`
You can also wrap existing TracingChannel instances:
`typescript
import { tracingChannel as nativeTracingChannel } from 'node:diagnostics_channel';
import { tracingChannel } from 'otel-tracing-channel';
const existingChannel = nativeTracingChannel('my-channel');
const wrappedChannel = tracingChannel(existingChannel, (data) =>
createMySpan(data),
);
`
Creates or wraps a tracing channel with OpenTelemetry context propagation.
Parameters:
- channelNameOrInstance: Either a string channel name or an existing TracingChannel instancetransformStart
- : A function that receives the channel data and returns an OpenTelemetry Span
Returns: A TracingChannel instance with OTel context binding
The transformStart function is called during the start event and:
- Receives the channel data as its parameter
- Should create and return an OpenTelemetry Spandata.span
- The returned span is automatically stored on for access in event handlers
- The span's context is automatically propagated throughout the traced operation
Type definition for the transform function:
`typescript`
type TracingChannelTransform
Subscribe to channel events. All handlers are optional:
- start(data) - Called when operation startsasyncStart(data)
- - Called for async operationsasyncEnd(data)
- - Called when async operation ends (good place to end spans)end(data)
- - Called when operation endserror(data)
- - Called on errors (access error via data.error)
The span created in transformStart is available as data.span in all handlers.
Execute an async function with tracing. Context is properly propagated.
Execute a sync function with tracing. Context is properly propagated.
Enable debug logs to see what's happening under the hood:
`typescript
import { setDebugFlag } from 'otel-tracing-channel';
setDebugFlag(true); // Enable debug logs
setDebugFlag(false); // Disable debug logs
`
Debug logs will show:
- Whether OpenTelemetry AsyncLocalStorage was found
- When spans are created in the transform
- When context is stored in AsyncLocalStorage
Under the hood, this package:
1. Accesses OpenTelemetry's internal AsyncLocalStorage instance via context._getContextManager()start
2. Binds it to the channel's event using bindStoretransformStart
3. In the transform function:
- Calls your to create the spandata.span
- Stores the span on for handler accessAsyncLocalStorage
- Wraps the span in an OTel context
- Returns the context to be stored in
This ensures the OpenTelemetry context (and your span) is active throughout the entire traced operation.
If OpenTelemetry context is not available (e.g., no SDK initialized), the library:
- Logs a debug message (if debug logging is enabled)
- Returns the channel without OTel binding
- The channel still works normally, just without automatic context propagation
Full TypeScript support with generics for channel data:
`typescript
interface QueryData {
query: string;
params: any[];
}
const channel = tracingChannel
// data is typed as QueryData
return createSpan(data.query, data.params);
});
`
This package uses npm Trusted Publishers with GitHub Actions. No npm tokens required!
Version Options:
- as-is - Publish current version in package.json (no auto-bump)patch
- - Bug fixes (0.1.0 → 0.1.1)minor
- - New features (0.1.0 → 0.2.0)major
- - Breaking changes (0.1.0 → 1.0.0)
You can manually edit package.json version and use as-is`, or let the workflow bump it automatically.
Apache-2.0