Distributed Tracing

Observability

Chapter 8 · Distributed Tracing

obs1-7 closed with a promise: traces are the more powerful version of the same correlation idea logs only partially deliver. This chapter is where that becomes concrete — the third pillar, and genuinely new material nothing else on this site has covered before now.

What a Trace Actually Is

A trace represents one request's full journey across every service it touches. It's built from spans — each span represents one unit of work: one service handling the request, or one specific operation inside it, like a database query. A span has a name, a start time, a duration, and a set of key-value attributes, similar in spirit to a metric's own labels.

Spans — The Building Block

Every span has a unique span ID, and — except the very first, root span — a parent span ID, forming a tree that reconstructs the exact call graph a request actually took. Every span belonging to the same request shares one trace ID, the thread tying the whole tree together.

Trace ID: abc-123 gateway (50ms) [root span] auth-service (10ms) checkout-service (35ms) inventory-service (28ms) db-query (25ms)

This is obs1-1's own checkout example, made literal. "3.8 of the 4 seconds spent waiting on inventory" isn't a conclusion someone had to piece together from separate logs anymore — it's a visible span in this exact tree, with its own duration sitting right there.

Trace Context Propagation

For spans created in separate services to link into one trace, the trace ID — and the current span ID, to establish the parent-child relationship — has to travel along with every downstream call, usually as an HTTP header. This is mechanically the same idea as obs1-7's own correlation ID, formalized into a real, standardized format: the W3C Trace Context traceparent header, rather than every team inventing its own ad-hoc header.

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

Four dash-separated parts: version, trace ID, the parent span's own ID, and trace flags. Every hop that receives this header, creates its own span as a child of that parent ID, and forwards an updated version downstream — the exact mechanism that turns separate services' separate spans into one connected tree.

OpenTelemetry — A First Look at Instrumenting Your Code

OpenTelemetry (OTel) is the vendor-neutral standard for generating this telemetry — an SDK application code calls directly to create spans, which are then exported to a tracing backend. obs1-9 covers it properly; here's the shape of it:

with tracer.start_as_current_span("process_payment") as span: # do the actual work span.set_attribute("payment.amount", 49.99)

Many frameworks and libraries support auto-instrumentation — OTel can automatically create spans for common operations (incoming HTTP requests, outgoing calls, database queries) with no manual code changes at all, while manual spans like the one above cover anything genuinely business-specific a generic instrumentation library couldn't know to track on its own.

Jaeger — Storing and Visualizing Traces

Jaeger receives the spans instrumented services export, stores them, and provides a UI to search traces — by service, operation, duration, tags — and to visualize any single trace as a waterfall/timeline view, exactly like the tree above but interactive. It's the tracing pillar's own equivalent of what Grafana is for metrics: a query-and-visualization layer sitting on top of the actual data. And as obs1-5 already noted, Grafana itself can visualize Jaeger (or Tempo, its close relative) traces directly as a data source, right alongside metrics and logs in the same dashboard.

PillarUnique propertyReal cost consideration
MetricsCheap, aggregated trendsStorage grows with cardinality (obs1-2)
LogsRich per-event detailStorage grows with log volume
TracesThe full call graph and timing for one specific requestInstrumentation + export overhead per traced request — sampling is the standard mitigation
Sampling — a real, honest operational tradeoff
Tracing every single request at real production scale can be genuinely expensive, in both instrumentation overhead and storage. Most real systems sample — tracing only a percentage of requests, or deliberately always tracing 100% of errors and slow requests specifically — rather than tracing everything all the time. This is a deliberate tradeoff, not a compromise to feel bad about: the requests most worth having a full trace for are disproportionately the ones already flagged as errors or outliers.
A dropped traceparent header doesn't error — it silently starts a new trace
A service that fails to forward the incoming traceparent header to its own downstream calls doesn't produce any visible failure — it just begins a brand-new, disconnected trace instead of continuing the existing one. The result looks, from Jaeger's own UI, like the request's journey simply stopped at that service, with no indication that work continued elsewhere under a different trace ID entirely. This is the tracing pillar's own exact version of obs1-7's dropped-correlation-ID gotcha.

Hands-On Exercises

Exercise 1

Draw (as text, matching the chapter's own tree format) a trace for a request that hits an api-gateway (root span), which calls both an auth-service and, only after auth succeeds, a search-service that itself queries a cache and then a database.

📄 View solution
Exercise 2

Explain, using the traceparent header's own four parts, what information a downstream service needs from an incoming request in order to correctly create its own span as a child of the correct parent.

📄 View solution
Exercise 3

A team traces 100% of requests and notices real latency overhead from tracing itself during peak traffic. Propose a sampling strategy that still guarantees every error and every slow request gets a full trace, and explain why this is a reasonable tradeoff rather than a loss of visibility.

📄 View solution

Chapter 8 Quick Reference

  • A trace is one request's full journey; a span is one unit of work within it, with a name, duration, and attributes
  • Spans share a trace ID and form a parent-child tree via span IDs, reconstructing the real call graph
  • The traceparent header (W3C Trace Context) propagates trace/span IDs across services — the formalized version of obs1-7's correlation ID
  • OpenTelemetry is the vendor-neutral SDK/standard for generating spans, via auto-instrumentation and manual spans (covered fully in obs1-9)
  • Jaeger stores and visualizes traces — the tracing pillar's own equivalent of Grafana
  • Sampling (not tracing every request) is a real, standard cost tradeoff, often biased toward always tracing errors/slow requests
  • A dropped traceparent header silently starts a disconnected new trace rather than erroring — a real, easy-to-miss gotcha