Distributed Tracing
Propagate W3C TraceContext across HTTP, gRPC, and message queues so a request spanning multiple services appears as one unified trace in the Trasys dashboard.
Distributed Tracing
Distributed tracing connects spans across multiple services into a single trace timeline. When Service A calls Service B, the trace started in A continues in B — both appear as a unified trace in your dashboard with parent–child relationships.
Trasys uses the W3C TraceContext standard (traceparent header). Any OpenTelemetry-compatible service can participate in the same trace.
How it works
Browser → [Service A] → HTTP → [Service B] → gRPC → [Service C]
│ │ │
trace: abc123 trace: abc123 trace: abc123
span: root span: child span: grandchildEach service in the chain:
- Extracts the
traceparentheader from the incoming request - Creates a span as a child of the incoming context
- Injects the
traceparentheader into outgoing requests
The Trasys middleware handles extraction and injection automatically. You just need the SDK running in each service.
HTTP — inbound
The middleware in every framework automatically extracts traceparent from incoming requests. If the header is present, the span created by Trasys is linked as a child of the upstream span. No configuration required.
app.use(sdk.middleware()); // Express
await app.register(sdk.fastifyplugin()); // Fastify
app.use('*', sdk.honoMiddleware()); // HonoHTTP — outbound (fetch)
Global fetch is patched at SDK initialization. Every outbound fetch call automatically gets a traceparent header injected:
// No changes needed — traceparent is injected automatically
const response = await fetch('https://api.internal/payments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});On Bun, outbound fetch spans are not available and traceparent is not injected. This is a platform limitation with no workaround.
HTTP — outbound (Axios)
Axios instances require a one-time manual wrap:
const axios = require('axios');
const { sdk } = require('./trasys');
const http = sdk.wrapHttpClient(axios.create({
baseURL: 'https://api.internal',
}));
// traceparent is injected into every request from this instance
const data = await http.get('/users');Each Axios instance must be wrapped separately. Wrapping the default axios object does not affect instances created with axios.create(), and vice versa.
gRPC
gRPC trace propagation is enabled automatically. The SDK registers GrpcInstrumentation at startup — no configuration or code changes needed.
// Service A — client call
// traceparent is injected into gRPC metadata by the SDK
const response = await grpcClient.getUser({ id: userId });
// Service B — server handler
// The SDK extracts traceparent from incoming metadata and creates a child span
server.addService(UserService, {
getUser: async (call, callback) => {
const user = await db.findUser(call.request.id);
callback(null, user);
},
});Both the client call and the server handler appear in the same trace timeline in your dashboard.
Message queues (BullMQ)
BullMQ workers are instrumented manually. Wrap each worker instance once:
const { Worker } = require('bullmq');
const { sdk } = require('./trasys');
const worker = sdk.wrapQueue(
new Worker('email-queue', async (job) => {
await sendEmail(job.data);
})
);Span attributes captured per job:
| Attribute | Description |
|---|---|
messaging.system | bullmq |
messaging.destination | Queue name |
messaging.message.id | Job ID |
bullmq.job.name | Job name |
bullmq.job.attempt | Attempt number |
Metrics recorded automatically:
bullmq.job.duration_ms— histogrambullmq.jobs.total— counter tagged with{queue, job, result}
Multi-service requirements
For distributed tracing to connect spans across services, every service must have the SDK installed and initialized.
| Setup | Result |
|---|---|
| Service A (SDK) → Service B (SDK) | Full trace — all spans connected |
| Service A (SDK) → Service B (no SDK) | Partial trace — A's spans only |
| Service A (no SDK) → Service B (SDK) | Partial trace — B's spans only |
A service without the SDK still receives the traceparent header but does not create child spans. The trace is visible but incomplete.
Custom spans
For anything the SDK doesn't instrument automatically, @trasys/sdk re-exports the OpenTelemetry primitives you need — you don't need to install @opentelemetry/api separately:
const { trace, context, SpanStatusCode } = require('@trasys/sdk');
async function processInvoice(invoiceId) {
const tracer = trace.getTracer('invoice-processor');
return tracer.startActiveSpan('process-invoice', async (span) => {
span.setAttribute('invoice.id', invoiceId);
try {
const result = await doWork(invoiceId);
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (err) {
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
throw err;
} finally {
span.end();
}
});
}A span created this way is a child of whatever span is active on the current async context — inside an instrumented HTTP request, that's the request's root span, so it appears correctly nested in the trace with no extra wiring.
Troubleshooting: spans not connecting across services
A trace that stops at a service boundary (or has gaps between services) almost always comes down to one of these, in order of likelihood:
The SDK wasn't initialized before the HTTP client library. Just like Express/Fastify instrumentation, outbound trace propagation requires the SDK to patch fetch/Axios before your code imports them. See Installation & Setup.
The downstream service doesn't have the SDK at all. Per the Multi-service requirements table above, a service without the SDK still receives the traceparent header but never creates a child span — the trace looks like it "stops" there, but it's actually just incomplete, not broken.
Axios was wrapped, but a different instance is being used. Each Axios instance must be wrapped separately — wrapping the default axios export does not affect instances created with axios.create(), and vice versa. Check which instance is actually making the call.
You're on Bun. Outbound fetch spans (and the traceparent injection that depends on them) are not available on Bun — this is a platform limitation, not a bug. Inbound spans, AI spans, and database spans are unaffected.
A manual span (above) was created outside the active context, e.g. inside a detached setTimeout/setImmediate callback or a fire-and-forget promise that outlives the request. Use context.with() to explicitly carry the trace context into code that runs outside the normal request flow.
Viewing in the dashboard
When a request flows through multiple services, the dashboard shows:
- Full span tree — root request → child service calls → database queries → AI calls
- Service boundaries — each service's spans are labeled with the service name
- Critical path — which call in the chain caused the overall latency
Click any span to see its attributes, logs, and database or AI sub-spans.
Next steps
- Frameworks — make sure the middleware is mounted correctly in each service
- Logging — logs are automatically correlated to trace spans
Logging
Send structured logs through the Trasys OpenTelemetry pipeline, automatically enriched with the active trace ID, span ID, user ID, tenant ID, and request ID.
Session Tracking
Correlate every request from a single user session across your backend services using an explicit session header or IP/user-agent fingerprinting as a fallback.

