Skip to main content

Tracing Middleware

Distributed tracing with OpenTelemetry for observability across services.

Installation​

npm install @saga-bus/middleware-tracing @opentelemetry/api @opentelemetry/sdk-node

Basic Usage​

import { createTracingMiddleware } from '@saga-bus/middleware-tracing';

const bus = createBus({
transport,
store,
sagas: [{ definition: orderSaga }],
middleware: [
createTracingMiddleware({
serviceName: 'order-service',
}),
],
});

Configuration​

OptionTypeDefaultDescription
serviceNamestringRequiredService name for traces
tracerTracer-Custom OpenTelemetry tracer
propagatorTextMapPropagatorW3CContext propagator
spanAttributesfunction-Custom span attributes
recordExceptionbooleantrueRecord exceptions in spans

Full Configuration Example​

import { createTracingMiddleware } from '@saga-bus/middleware-tracing';
import { trace } from '@opentelemetry/api';
import { W3CTraceContextPropagator } from '@opentelemetry/core';

const tracer = trace.getTracer('saga-bus', '1.0.0');

const tracingMiddleware = createTracingMiddleware({
serviceName: 'order-service',
tracer,
propagator: new W3CTraceContextPropagator(),
spanAttributes: (context) => ({
'saga.name': context.sagaName,
'message.type': context.messageType,
'correlation.id': context.correlationId,
}),
recordException: true,
});

OpenTelemetry Setup​

Node.js SDK​

// tracing.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';

const sdk = new NodeSDK({
serviceName: 'order-service',
traceExporter: new OTLPTraceExporter({
url: 'http://localhost:4318/v1/traces',
}),
instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

Import Before App​

// index.ts
import './tracing'; // Must be first!
import { createBus } from '@saga-bus/core';
// ...

Span Structure​

Span Structure​

Context Propagation​

Traces propagate across services via message headers:

// Automatic context propagation
// When publishing from one saga to another:

// Service A publishes
await bus.publish({
type: 'PaymentRequested',
orderId: '123',
});

// Service B receives - same trace context!
// Headers include: traceparent, tracestate

Exporters​

Jaeger​

import { JaegerExporter } from '@opentelemetry/exporter-jaeger';

const exporter = new JaegerExporter({
endpoint: 'http://localhost:14268/api/traces',
});

Zipkin​

import { ZipkinExporter } from '@opentelemetry/exporter-zipkin';

const exporter = new ZipkinExporter({
url: 'http://localhost:9411/api/v2/spans',
});

OTLP (Grafana, Honeycomb, etc.)​

import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';

const exporter = new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
headers: {
'x-honeycomb-team': process.env.HONEYCOMB_API_KEY,
},
});

Custom Span Attributes​

Add business-specific attributes:

const middleware = createTracingMiddleware({
serviceName: 'order-service',
spanAttributes: (context) => ({
'customer.id': context.message.customerId,
'order.total': context.message.total,
'order.items.count': context.message.items?.length,
}),
});

Error Recording​

Exceptions are recorded as span events:

// Automatic exception recording
const middleware = createTracingMiddleware({
serviceName: 'order-service',
recordException: true, // default
});

// Span will include:
// - status: ERROR
// - exception.type: "PaymentDeclinedError"
// - exception.message: "Card declined"
// - exception.stacktrace: "..."

Docker Setup​

Jaeger All-in-One​

services:
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # UI
- "14268:14268" # Collector
environment:
- COLLECTOR_OTLP_ENABLED=true

Grafana Tempo​

services:
tempo:
image: grafana/tempo:latest
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP

Best Practices​

Always Initialize Early​

// tracing.ts should be imported first
import './tracing';
import { createBus } from '@saga-bus/core';

Use Meaningful Service Names​

// Good
createTracingMiddleware({ serviceName: 'order-service' });

// Avoid
createTracingMiddleware({ serviceName: 'app' });

Add Business Context​

spanAttributes: (ctx) => ({
'customer.tier': ctx.message.customerTier,
'order.priority': ctx.message.priority,
});

See Also​