Why this matters
- A slow checkout might spend 400 ms in inventory, 2 seconds in payment, and 50 ms in your API — logs alone cannot show where the time went across service boundaries.
- Micrometer Tracing replaced Spring Cloud Sleuth in Boot 3.x; it uses OpenTelemetry under the hood and propagates trace context through HTTP headers and message metadata.
- Correlating traces with logs (same trace ID in MDC) turns a Grafana trace view into a jump-off point for the exact log lines on each hop.
Tracing stack in Boot 3.4
- Micrometer Tracing — abstraction layer; bridges to OpenTelemetry or Brave exporters.
- OpenTelemetry SDK — vendor-neutral instrumentation and export to Jaeger, Zipkin, or OTLP collectors.
traceId/spanId— identifiers propagated across services; inject into MDC for log correlation.ObservationRegistry— creates spans around HTTP requests,@Scheduledjobs, and custom code.- OTLP exporter — sends spans to Grafana Tempo, Honeycomb, or any OpenTelemetry-compatible backend.
Logs
Logback + MDCStructured JSON
Metrics
MicrometerCounters, timers
Traces
OpenTelemetryTrace propagation
Dependencies and configuration
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
management:
tracing:
sampling:
probability: 1.0 # reduce in production, e.g. 0.1 for 10%
otlp:
tracing:
endpoint: http://otel-collector:4318/v1/traces
logging:
pattern:
level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"
Automatic instrumentation
Spring Boot auto-instruments:
Auto-instrumented components
- Incoming HTTP requests via
ServerHttpObservationFilter - Outgoing
RestClient/WebClientcalls (trace headers injected) - JDBC queries when the datasource is observed
- Kafka producers and consumers when Spring Kafka tracing is enabled
@Service
public class CheckoutOrchestrator {
private final RestClient inventoryClient;
private final RestClient paymentClient;
private final ObservationRegistry observations;
public CheckoutOrchestrator(RestClient.Builder builder,
ObservationRegistry observations) {
this.inventoryClient = builder.baseUrl("http://inventory-service").build();
this.paymentClient = builder.baseUrl("http://payment-service").build();
this.observations = observations;
}
public CheckoutResult checkout(CheckoutRequest request) {
return Observation.createNotStarted("bookstore.checkout", observations)
.lowCardinalityKeyValue("customer.id", request.customerId())
.observe(() -> {
reserveInventory(request);
chargePayment(request);
return persistOrder(request);
});
}
}
Kafka trace propagation
spring:
kafka:
template:
observation-enabled: true
listener:
observation-enabled: true
@KafkaListener(topics = "bookstore.orders.placed", groupId = "bookstore-analytics")
public void onOrderPlaced(OrderPlacedMessage message) {
log.info("Processing order {} in analytics pipeline", message.orderId());
analyticsService.record(message);
}
The consumer span links to the producer span automatically when observation is enabled on both sides.
Quick recall
Everything you need if you only revisit this box.
- Micrometer Tracing + OpenTelemetry bridge replaces Sleuth in Spring Boot 3.x.
- Auto-instrumentation covers HTTP in/out, JDBC, and Kafka when observation flags are enabled.
- Put
traceIdandspanIdin log patterns via MDC for trace-to-log correlation. - Use
Observation.createNotStarted(...).observe()for custom business spans like full checkout. - Tune
management.tracing.sampling.probability— full sampling is for dev, not production.
Test yourself
Answer these before moving on — recall is what makes it stick.