Skip to content
Spring Boot sb microservices 3 min read

Distributed Tracing

When one user request fans out across five services, a stack trace in any single service tells you almost nothing. Distributed tracing stitches the whole journey together: each request gets a trace id that flows through every hop, and each unit of work gets a span. In Spring Boot 3 this is handled by Micrometer Tracing — the successor to the now-retired Spring Cloud Sleuth.

Traces, spans, and correlation ids

trace 7f3a...  (one request, end to end)
 ├─ span A  gateway      [██████████████]
 │   ├─ span B  orders     [████████]
 │   │   ├─ span C  inventory  [███]
 │   │   └─ span D  payments   [████]
TermMeaning
Trace idIdentifies one end-to-end request across all services
Span idIdentifies one unit of work (an HTTP call, a DB query)
Parent spanThe span that caused this one — builds the tree
Correlation idThe trace/span ids injected into log lines

Micrometer Tracing propagates these ids automatically across HTTP clients, server requests, and messaging — you write almost no tracing code.

Dependencies

Micrometer Tracing needs a bridge (the tracer implementation) and a reporter (where spans go). The common choice is the Brave bridge exporting to Zipkin:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
    <groupId>io.zipkin.reporter2</groupId>
    <artifactId>zipkin-reporter-brave</artifactId>
</dependency>

Prefer OpenTelemetry? Swap the bridge and reporter:

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-exporter-zipkin</artifactId>
</dependency>
BridgePropagationTypical backend
micrometer-tracing-bridge-braveB3Zipkin
micrometer-tracing-bridge-otelW3C traceparentTempo, Jaeger, any OTLP collector

Configuration

management:
  tracing:
    sampling:
      probability: 1.0          # sample 100% in dev; lower in production
  zipkin:
    tracing:
      endpoint: http://localhost:9411/api/v2/spans

Run Zipkin locally to receive spans:

docker run -d -p 9411:9411 openzipkin/zipkin

Note: Sampling at 1.0 is fine in development but costly at scale. In production sample a fraction (e.g. 0.1) — Micrometer ensures a sampling decision is consistent across the whole trace.

What you get for free

With the dependencies present, Spring auto-instruments:

  • Incoming HTTP requests (a server span per request).
  • RestClient, WebClient, RestTemplate, and @FeignClient calls (propagating ids downstream).
  • Messaging via RabbitMQ/Kafka.
  • Scheduled tasks and @Async methods.

So a request entering the gateway and flowing to orders → inventory shows up in Zipkin as one connected trace, no code changes.

Log correlation

The real day-to-day win is correlated logs. Micrometer puts the trace and span ids into the MDC; configure the log pattern to print them:

logging:
  pattern:
    level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"

Output (orders-service):

INFO  [orders-service,7f3a9b2c1d4e5f60,a1b2c3d4e5f6a7b8] Placing order 42

Output (inventory-service, same request):

INFO  [inventory-service,7f3a9b2c1d4e5f60,b8a7c6d5e4f3a2b1] Reserving sku COFFEE-1

Same traceId (7f3a...) across both services — grep your aggregated logs by trace id and you see the entire request, in order, across the fleet. See Logging Configuration for log setup.

Custom spans

For a meaningful business operation that isn’t an HTTP/DB call, create a span explicitly with the Observation API or the Tracer:

@Service
@RequiredArgsConstructor
public class PricingService {
    private final ObservationRegistry registry;

    public Money price(Cart cart) {
        return Observation.createNotStarted("pricing.calculate", registry)
                .observe(() -> expensiveCalculation(cart));
    }
}

Tip: Prefer the ObservationRegistry API over the low-level Tracer — one observation simultaneously produces a span and a timer metric, so tracing and metrics stay in sync.

Tracing pairs naturally with metrics and health from Actuator and Micrometer Metrics to complete the three pillars of observability.

Last updated June 13, 2026
Was this helpful?