Trasys
Node.js SDK

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: grandchild

Each service in the chain:

  1. Extracts the traceparent header from the incoming request
  2. Creates a span as a child of the incoming context
  3. Injects the traceparent header 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());      // Hono

HTTP — 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:

AttributeDescription
messaging.systembullmq
messaging.destinationQueue name
messaging.message.idJob ID
bullmq.job.nameJob name
bullmq.job.attemptAttempt number

Metrics recorded automatically:

  • bullmq.job.duration_ms — histogram
  • bullmq.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.

SetupResult
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

On this page