Distributed Tracing
When one request flows through many services, a single log file can't tell you where the time went. Distributed tracing stitches the whole journey together so you can follow one request — and find the slow span — across every service it touches.
Learn Distributed Tracing in our free Node.js course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free Node.js course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
In this lesson you'll learn the core ideas of observability — traces, spans, and context propagation — and set up the @opentelemetry/sdk-node SDK with auto-instrumentation, exporting to a backend like Jaeger or Zipkin.
What You'll Learn in This Lesson
1️⃣ Set Up the OpenTelemetry SDK
Tracing starts with the SDK . You configure NodeSDK with a service name, an exporter (where spans go), and a set of instrumentations . The catch: this file must load before the rest of your app, because auto-instrumentation patches libraries at the moment they're required.
getNodeAutoInstrumentations() is the magic: it patches common libraries — http , express , pg , and more — so every request and query produces a span without you writing any tracing code. Load it with node --require ./tracing.js server.js .
2️⃣ Traces and Spans
A span is one timed unit of work; a trace is the tree of spans that share a trace ID. Auto-instrumentation gives you spans for free, but you can also create your own to time a specific piece of business logic. A span started inside another becomes its child , building the tree you see in the UI.
Two rules matter: set attributes so spans are searchable (by user, cart size, status), and always end every span you start. An unended span never records its duration and can leak.
- • Span — a single timed operation with attributes and a parent.
- • Trace — all spans sharing one trace ID, forming a tree.
- • startActiveSpan — start a span and make it the current parent.
- • span.end() — close the span and record its duration.
3️⃣ Context Propagation Across Services
What makes tracing distributed is context propagation . When one service calls another, OpenTelemetry injects the current trace and span IDs into a traceparent header. The receiving service reads it and continues the same trace, so a request that crosses four services shows up as one connected trace, not four disconnected ones.
Because http is auto-instrumented, the traceparent header rides along automatically — you don't set it by hand. This correlation is what lets you open one slow request and see, span by span, exactly which service ate the time.
Your turn. Fill in the ___ blank so the span is properly closed.
Mini-Challenge: Find the Bottleneck
Add a custom span around a deliberately slow function, for example one that await s a 500ms timer, and give it an attribute like slow.reason . Trigger the request, then open the trace in Jaeger or Zipkin and find your span in the waterfall. Confirm its duration stands out against the fast spans around it. That's the everyday workflow: a request feels slow, you open its trace, and the longest span tells you exactly where to look.
Common Errors (and the fix)
- Starting the SDK too late — If your app requires http or pg before the SDK starts, those libraries are never patched and produce no spans. Load tracing first.
- Forgetting to span.end() — An unended span has no duration and can pin memory. Every started span must be ended.
- No exporter configured — Spans are collected but never sent, so the backend stays empty. Configure an exporter pointing at Jaeger, Zipkin, or a collector.
- Breaking propagation with custom HTTP — A bespoke client that strips headers drops the traceparent and splits the trace. Use instrumented clients or inject context manually.
- Over-instrumenting — A span for every tiny function floods the backend and costs money. Trace meaningful units of work and sample in production.
Pro Tips
- 💡 Export through a Collector in production so your app sends telemetry to one OTLP endpoint and the collector fans it out to Jaeger, Zipkin, or your vendor.
- 💡 Add rich attributes (user id, route, status code) to spans so you can search and group traces, not just stare at one at a time.
- 💡 Sample under load: tracing every request at high volume is expensive. Use head- or tail-based sampling to keep the interesting traces while controlling cost.
📋 Quick Reference — OpenTelemetry in Node
Goal
API
Bootstrap the SDK
new NodeSDK(config).start()
Auto-instrument libraries
getNodeAutoInstrumentations()
Get a tracer
trace.getTracer(name)
Start a span
tracer.startActiveSpan(name, fn)
End a span
span.end()
Propagated header
traceparent
Frequently Asked Questions
🎉 Lesson Complete!
- ✅ Tracing follows one request across many services
- ✅ A span is one timed operation; a trace is the tree of spans
- ✅ @opentelemetry/sdk-node bootstraps tracing — load it first
- ✅ Auto-instrumentation patches http, express, and pg for free
- ✅ Context propagation via traceparent connects services
- ✅ Exporters ship spans to Jaeger or Zipkin to find bottlenecks
- ✅ Next lesson: Database Transactions & ACID
Practice quiz
What problem does distributed tracing primarily solve?
- Following a single request as it travels across many services
- Compressing log files
- Replacing the database
- Encrypting network traffic
Answer: Following a single request as it travels across many services. Tracing reconstructs the full path of one request across all the services it touches.
In tracing terminology, what is a span?
- A log line
- A type of database index
- A single named, timed unit of work within a trace
- The whole request from start to finish
Answer: A single named, timed unit of work within a trace. A span represents one operation, with a start time, duration, and attributes; a trace is a tree of spans.
What is a trace?
- A metrics counter
- A collection of spans sharing one trace ID that together describe one request's journey
- A single HTTP header
- A single log message
Answer: A collection of spans sharing one trace ID that together describe one request's journey. A trace is all the spans that share a trace ID, forming the end-to-end view of one request.
How does a trace stay connected as a request crosses a service boundary?
- By guessing based on timestamps
- By writing to a shared file
- It cannot cross service boundaries
- Through context propagation, passing trace and span IDs (commonly via the traceparent header)
Answer: Through context propagation, passing trace and span IDs (commonly via the traceparent header). Context propagation carries the trace/span IDs across calls, usually in the W3C traceparent header.
Which OpenTelemetry package bootstraps tracing for a Node service?
- @opentelemetry/sdk-node
- express
- @grpc/grpc-js
- amqplib
Answer: @opentelemetry/sdk-node. @opentelemetry/sdk-node wires up the SDK; you start it before the rest of your app loads.
What does getNodeAutoInstrumentations() do?
- Deletes old traces
- Automatically instruments common libraries like http, express, and pg without manual code
- Sends emails on errors
- Compiles TypeScript
Answer: Automatically instruments common libraries like http, express, and pg without manual code. Auto-instrumentations patch popular libraries so spans are created without hand-written code.
Where do you typically send (export) the collected spans?
- To the npm registry
- Only to the console, never anywhere else
- Back into the source code
- To a backend like Jaeger or Zipkin via an exporter
Answer: To a backend like Jaeger or Zipkin via an exporter. An exporter ships spans to a tracing backend such as Jaeger or Zipkin for storage and viewing.
Why must the OpenTelemetry SDK be started before your app's other modules load?
- It does not matter when you start it
- To save memory
- So auto-instrumentation can patch libraries like http and pg before they are required
- Because Node requires it by law
Answer: So auto-instrumentation can patch libraries like http and pg before they are required. Instrumentation works by patching modules at require time, so the SDK must initialize first.
What is a parent-child relationship between spans?
- A retry of the same span
- A span created within another span records the outer span as its parent, forming the trace tree
- Two unrelated traces
- A database foreign key
Answer: A span created within another span records the outer span as its parent, forming the trace tree. Nested operations create child spans whose parent is the enclosing span, building the trace tree.
How does tracing help when one request is slow across several services?
- It shows each span's duration so you can pinpoint exactly which service or call is the bottleneck
- It automatically rewrites the code
- It only reports total time with no breakdown
- It hides the slow service
Answer: It shows each span's duration so you can pinpoint exactly which service or call is the bottleneck. Per-span durations reveal where time is actually spent, isolating the bottleneck across services.
Continue this course
- Previous: gRPC Services
- Next: Database Transactions & ACID