Telemetry
Overview
Section titled “Overview”@serviceconnect/telemetry adds OpenTelemetry tracing and metrics to a ServiceConnect bus without baking OTel into the core. It declares @opentelemetry/api as its only peer dependency so the caller controls the OTel API/SDK version and brings the SDK (@opentelemetry/sdk-trace-node, a context manager, a propagator, etc.); its only direct runtime dependencies are @serviceconnect/core and @opentelemetry/semantic-conventions. Two exports do all the work — telemetryProducer wraps an ITransportProducer to emit producer spans, and telemetryConsumeWrapper returns a function suitable for BusOptions.consumeWrapper that wraps consumer dispatch in a consumer span. W3C trace context flows automatically through the envelope headers.
Spans follow OTel semantic conventions: producer spans are named <destination> publish (or <endpoint> send) and consumer spans are named <messageType> process. Both carry standard messaging.* attributes — messaging.system, messaging.operation, messaging.destination.name, messaging.message.id, messaging.message.conversation_id, messaging.message.body.size.
Import
Section titled “Import”import { telemetryConsumeWrapper, telemetryProducer,} from '@serviceconnect/telemetry';import type { TelemetryOptions } from '@serviceconnect/telemetry';TelemetryOptions
Section titled “TelemetryOptions”import type { Meter, Tracer } from '@opentelemetry/api';
export interface TelemetryOptions { readonly tracer?: Tracer; readonly meter?: Meter; readonly messagingSystem?: string;}tracer(optional) — aTracerfrom@opentelemetry/api. When omitted the wrappers calltrace.getTracer('@serviceconnect/telemetry')and pick up whichever provider you registered globally.meter(optional) — aMeter. When omitted the wrappers fall back to the global meter and create the counters and histograms lazily.messagingSystem(optional) — the value to use for themessaging.systemattribute. Defaults to'rabbitmq'. Set this to the appropriate identifier for non-RabbitMQ transports.
Both telemetryProducer and telemetryConsumeWrapper accept the same TelemetryOptions; pass the same object to both if you want them to share a tracer / meter.
telemetryProducer
Section titled “telemetryProducer”import type { ITransportProducer } from '@serviceconnect/core';
export function telemetryProducer( underlying: ITransportProducer, options?: TelemetryOptions,): ITransportProducer;Wraps an ITransportProducer and returns a new producer that:
- Starts a
SpanKind.PRODUCERspan around eachpublish,send, andsendBytescall. Span name is<destination> publishforpublishand<endpoint> sendforsend/sendBytes. - Stamps the producer attributes (
messaging.system,messaging.operation,messaging.destination.name,messaging.message.body.size, plusmessaging.message.id/messaging.message.conversation_idwhen present inopts.headers). - Injects the W3C trace context (
traceparent/tracestate) into the outgoingopts.headersso the downstream consumer can extract it. - Records the
messaging.publish.durationhistogram for every call (tagged witherror.typefrom the thrown error’s name on failure), and increments themessaging.client.published.messagescounter on success. - Re-throws after recording the exception and ending the span so calling code still sees the original failure.
The returned producer transparently forwards isHealthy, supportsRoutingKey, maxMessageSize, and Symbol.asyncDispose to the underlying producer.
Wrap the producer at bus construction (the transport’s producer / consumer are readonly, so wrap them in the transport you pass to createBus rather than mutating the transport object):
const transport = createRabbitMQTransport({ url: 'amqp://localhost' });const bus = createBus({ transport: { producer: telemetryProducer(transport.producer), consumer: transport.consumer, }, // ...rest of your bus options});telemetryConsumeWrapper
Section titled “telemetryConsumeWrapper”import type { ConsumeCallback } from '@serviceconnect/core';
export function telemetryConsumeWrapper( options?: TelemetryOptions,): (cb: ConsumeCallback) => ConsumeCallback;Returns a function suitable for BusOptions.consumeWrapper. When the bus dispatches an inbound envelope the wrapped callback:
- Extracts the W3C trace context from
envelope.headersusing the configured propagator. - Starts a
SpanKind.CONSUMERspan named<messageType> processas a child of the extracted context (so it joins the producer’s trace). - Stamps the same
messaging.*attributes the producer side stamps, plusmessaging.message.idandmessaging.message.conversation_idwhen those headers are present. - Runs the inner consume callback under the new span via
context.with(...)so user handlers see the right active span. - Sets the span status to
OKwhen the inner result is{ success: true }, or toERROR(recording the exception) when{ success: false, error }. - Records the
messaging.process.durationhistogram and increments themessaging.client.consumed.messagescounter (tagged withmessaging.outcome—success,error, orretry) before ending the span.
Wiring example
Section titled “Wiring example”The snippet below wires both wrappers into a bus, using the Node SDK and async-hooks context manager.
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks';import { context, propagation } from '@opentelemetry/api';import { W3CTraceContextPropagator } from '@opentelemetry/core';import { telemetryProducer, telemetryConsumeWrapper } from '@serviceconnect/telemetry';import { createBus } from '@serviceconnect/core';import { createRabbitMQTransport } from '@serviceconnect/rabbitmq';
context.setGlobalContextManager(new AsyncHooksContextManager().enable());propagation.setGlobalPropagator(new W3CTraceContextPropagator());const tracer = new NodeTracerProvider();tracer.register();
const transport = createRabbitMQTransport({ url: 'amqp://localhost' });
const bus = createBus({ transport: { producer: telemetryProducer(transport.producer), consumer: transport.consumer, }, queue: { name: 'orders' }, consumeWrapper: telemetryConsumeWrapper(),});