Spring Boot Tutorial
Distributed Tracing with OpenTelemetry
In a system of many services, one slow checkout request might pass through the gateway, the order service, the payment service, a database and Kafka. Logs from each service show fragments; metrics show that something is slow but not where. Distributed tracing follows each request across every service and shows a timeline of where the time went.
OpenTelemetry (OTel) is the industry standard for traces, metrics and logs. Spring Boot 4 provides spring-boot-starter-opentelemetry, which configures the OpenTelemetry SDK and Micrometer Tracing and exports over OTLP to backends such as Jaeger, Grafana Tempo, Zipkin, Honeycomb, Datadog or New Relic. This lesson covers traces and spans, setup, context propagation, trace ids in logs, custom spans, and sampling.
Traces, Spans and Context
A trace represents one request's whole journey and has a trace id. It consists of spans — timed operations such as "HTTP GET /api/orders", "SELECT orders" or "send to Kafka" — each with a span id, a parent, attributes and events. When a service calls another, the trace context travels in the W3C traceparent HTTP header (or message headers for Kafka/RabbitMQ), so the downstream spans join the same trace.
Setup in Spring Boot 4
Add spring-boot-starter-opentelemetry and set the OTLP endpoint with management.opentelemetry.tracing.export.otlp.endpoint. Spring instruments incoming and outgoing HTTP (RestClient, WebClient, HTTP service clients built from Spring's builders), JDBC-level observations, Kafka, RabbitMQ and @Observed methods automatically. By default only 10% of requests are sampled; set management.tracing.sampling.probability.
Trace IDs in Logs
When tracing is active, Spring Boot puts traceId and spanId into the logging MDC and includes them in log lines by default. You can jump from an error log to the full trace, or search all logs of one request across services — the single most useful debugging capability in microservices.
Custom Spans and Attributes
Add detail with @Observed, the Observation API, or Micrometer's Tracer for manual spans. Add low-cardinality attributes (payment method, tenant tier) that help you filter traces. Never put secrets or personal data in span attributes.
Sampling in Production
Recording 100% of traces is useful in development but expensive at high traffic. Common strategies: a fixed probability (e.g. 10%), or tail-based sampling in an OpenTelemetry Collector that keeps all error and slow traces and a fraction of the rest.
Examples
Dependencies, configuration and a local Jaeger
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-opentelemetry</artifactId>
</dependency>
# application.yml
spring:
application:
name: order-service
management:
tracing:
sampling:
probability: 1.0 # 100% in development
opentelemetry:
tracing:
export:
otlp:
endpoint: http://localhost:4318/v1/traces
# compose.yaml — Jaeger with an OTLP receiver and UI on :16686
services:
jaeger:
image: jaegertracing/jaeger:latest
ports:
- "16686:16686"
- "4318:4318"
Open http://localhost:16686 -> Service: order-service -> Find Traces
Trace ids in logs across two services
// order-service
@PostMapping("/api/orders")
public OrderDto place(@RequestBody PlaceOrder cmd) {
log.info("Placing order for {}", cmd.customerEmail());
PaymentResult payment = paymentClient.charge(cmd.total()); // HTTP service client -> payment-service
return orders.save(cmd, payment);
}
// payment-service
@PostMapping("/api/payments")
public PaymentResult charge(@RequestBody ChargeRequest req) {
log.info("Charging {}", req.amount());
return gateway.charge(req);
}
order-service INFO [order-service,6f1c2b9e4d3a8f7e1c2b9e4d3a8f7e1c,a1b2c3d4e5f60718] Placing order for asha@webnest.in
payment-service INFO [payment-service,6f1c2b9e4d3a8f7e1c2b9e4d3a8f7e1c,0918f7e6d5c4b3a2] Charging 2999.00
(same trace id in both services)
A custom span with attributes and events
@Service
public class FraudCheckService {
private final ObservationRegistry registry;
public FraudCheckService(ObservationRegistry registry) {
this.registry = registry;
}
public FraudScore check(Order order) {
return Observation.createNotStarted("fraud.check", registry)
.contextualName("fraud-check")
.lowCardinalityKeyValue("payment.method", order.paymentMethod())
.observe(() -> {
FraudScore score = model.score(order);
if (score.value() > 0.8) {
Span.current().addEvent("high-risk-order"); // OpenTelemetry API
}
return score;
});
}
}
Jaeger timeline for trace 6f1c2b9e...
POST /api/orders order-service 412 ms
├─ fraud-check order-service 38 ms payment.method=card event: high-risk-order
├─ POST /api/payments order-service → 290 ms
│ └─ POST /api/payments payment-service 281 ms
│ └─ HTTP POST gateway.example payment-service 255 ms <- the slow part
└─ INSERT orders order-service 12 ms
Common Mistakes
- Creating RestClient/WebClient instances with static create() methods instead of Spring's builders, so trace context is not propagated.
- Sampling 100% of production traffic and overwhelming the tracing backend and budget.
- Recording personal data, tokens or request bodies as span attributes.
- Losing trace context in @Async or manually created threads — use context-propagating executors.
- Setting up tracing but not including trace ids in logs, missing the easiest way to correlate.
Key Points to Remember
- A trace follows one request across services; spans are timed operations inside it.
- Spring Boot 4: spring-boot-starter-opentelemetry + management.opentelemetry.tracing.export.otlp.endpoint.
- Context propagates via W3C traceparent headers through Spring's HTTP clients and messaging.
- traceId and spanId appear in logs automatically for correlation.
- Tune management.tracing.sampling.probability and add spans with @Observed or the Observation API.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.