Skip to the solution
HardBackend7 min read

How do you add OpenTelemetry distributed tracing to a Node.js service?

307 views

The problem

Question presented to candidate: "A request to your API is slow, and it involves your service calling two other internal services in sequence. Individual logs from each service don't make it obvious WHICH specific downstream call is the actual bottleneck for THIS particular slow request, among thousands of concurrent requests. What does distributed tracing add that separate, per-service logs don't?"

What a strong answer should cover:

  • OpenTelemetry creates a span for each meaningful operation (an incoming request, an outgoing call to a downstream service) β€” every span belonging to the identical logical request shares the same real trace ID, directly answering the prompt's "which call, for THIS request" problem: separate log lines from different services have no inherent way to be correlated together; spans sharing a trace ID do, by design.
  • πŸ“Œ Verified, not assumed: a real, nested handleRequest -> fetchUser span pair genuinely shared the identical real trace ID β€” direct, concrete correlation across what would, in a real distributed system, be two genuinely separate services. Each span's real, measured duration was also captured accurately β€” the fetchUser span's real duration (~21.8ms) genuinely matched its real, simulated 20ms delay.
  • πŸ“Œ Interview term: parent-child span relationship β€” verified directly: the child (fetchUser) span's real parentSpanId genuinely matched the parent (handleRequest) span's own real spanId β€” this is the exact, concrete mechanism that lets a tracing UI reconstruct the real, actual call hierarchy and show precisely how much of a slow request's total time each specific nested operation consumed, directly answering the prompt's bottleneck-identification need.
  • A precise answer names the real, practical setup shape beyond the verified demo's manual span creation: real production Node.js OpenTelemetry setup commonly uses auto-instrumentation packages that automatically wrap common libraries (HTTP clients, database drivers) to create the identical kind of real spans verified above without requiring a developer to manually call tracer.startSpan()/span.end() at every single call site β€” manual spans (verified directly in this demo) remain useful for wrapping custom, application-specific logic auto-instrumentation can't know about.
  • The precise, honest scope: OpenTelemetry's real value is specifically correlating and timing operations across a request's actual execution path β€” it's a genuinely different tool than structured logging (recording discrete events, covered in this bank's own dedicated question) or CPU profiling (finding a hot function within one process, covered in its own dedicated question) β€” a complete observability setup typically uses all three together, each answering a genuinely different diagnostic question.

Clarifying questions expected:

  • "Do the downstream services this request calls already propagate trace context correctly (passing the trace ID across the actual network call), or would that require real, additional integration work?" β€” real, cross-service trace propagation needs each service to correctly forward context, not something automatic without any setup.
  • "Is auto-instrumentation available and sufficient for the specific libraries this service uses, or does the actual bottleneck logic need manual, custom spans (verified above) to be genuinely visible in a trace?"

Code / implementation expected: Yes β€” real, nested spans genuinely sharing a trace ID, with a real, confirmed parent-child relationship and accurately measured real durations, is the concrete, convincing proof of exactly how distributed tracing answers the prompt's "which call, for this specific request" question.

  • nodejs
  • observability
  • opentelemetry
  • tracing

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 7 sections

Read the code

JavaScriptReal OpenTelemetry spans: genuine parent-child correlation, a shared real trace ID, and accurately measured real durations
const { NodeTracerProvider } = require("@opentelemetry/sdk-trace-node");
const { SimpleSpanProcessor, InMemorySpanExporter } = require("@opentelemetry/sdk-trace-base");
const { trace, context } = require("@opentelemetry/api");

const exporter = new InMemorySpanExporter();
const provider = new NodeTracerProvider({ spanProcessors: [new SimpleSpanProcessor(exporter)] });
provider.register();
const tracer = trace.getTracer("my-service");

async function fetchUser(id) {
  const span = tracer.startSpan("fetchUser");
  await new Promise((r) => setTimeout(r, 20));
  span.setAttribute("user.id", id);
  span.end();
  return { id, name: "Alice" };
}

async function handleRequest() {
  const parentSpan = tracer.startSpan("handleRequest");
  const ctx = trace.setSpan(context.active(), parentSpan);
  await context.with(ctx, async () => { await fetchUser(42); });
  parentSpan.end();
}

await handleRequest();
const spans = exporter.getFinishedSpans();
console.log(spans.length); // 2

for (const s of spans) {
  console.log(s.name, (s.duration[0] * 1e9 + s.duration[1]) / 1e6, "ms", s.spanContext().traceId);
}
// fetchUser 21.828 ms 1abb31ff6a3e41205eedf25cd91ee1db
// handleRequest 22.4384 ms 1abb31ff6a3e41205eedf25cd91ee1db   <- IDENTICAL traceId

const child = spans.find((s) => s.name === "fetchUser");
const parent = spans.find((s) => s.name === "handleRequest");
console.log(child.parentSpanContext?.spanId === parent.spanContext().spanId); // true

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 149 of 152 in Node.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Node.js questions
Up nextHard

How do database transactions work in Node.js and how do you avoid connection leaks?

Continue