Here is the English translation of the article body.
Even after visualizing metrics with Prometheus and Grafana, it is still hard to pinpoint which service is causing latency. Distributed tracing is what solves that problem.
This article assumes Spring Boot 3.2 or later and walks through using Micrometer Tracing with Zipkin, all the way to confirming in the Zipkin UI that a trace ID is propagated between two services. RestClient is the HTTP client added in Spring Boot 3.2. If you are on 3.0 or 3.1, use WebClient instead (see also How to use RestTemplate and WebClient).
What Is Distributed Tracing?
The three pillars of observability are metrics, logs, and traces.
Tracing follows the “trail” of a request as it is processed across multiple services. Each request is assigned a trace ID, and a child span is created every time the request crosses a service boundary. When visualized in Zipkin as a waterfall, you can see at a glance which service is consuming how much time.
From Spring Cloud Sleuth to Micrometer Tracing
In Spring Boot 2.x, tracing meant Spring Cloud Sleuth. With the move to Spring Boot 3.x (Spring Framework 6), however, Sleuth was discontinued and its successor has been consolidated into Micrometer Tracing.
Micrometer Tracing is structured so that you can choose between two Tracers, Brave and OpenTelemetry, via a bridge. In this article we will use Brave and send traces to Zipkin.
Sample Setup
This article uses the following two services.
- order-service (port 8080) calls inventory-service using RestClient
- inventory-service (port 8081) provides an inventory check API
Zipkin runs locally in Docker.
Adding Dependencies
You need two dependencies: micrometer-tracing-bridge-brave and zipkin-reporter-brave.
<!-- Micrometer Tracing(Braveブリッジ) -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<!-- Zipkinへのレポーター -->
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
For comparison, here is what it looked like in Spring Boot 2.x (Sleuth).
<!-- Spring Boot 2.x(Sleuth)の場合 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
You can leave version management to the Spring Boot BOM.
Configuring application.properties
spring.application.name=order-service
# 開発時は全件サンプリング(本番は0.1〜0.3程度に下げる)
management.tracing.sampling.probability=1.0
# デフォルトはlocalhost:9411。Kubernetes環境などで変更する場合に明示する
management.zipkin.tracing.endpoint=http://localhost:9411/api/v2/spans
spring.application.name becomes the service name shown in the Zipkin UI, so be sure to set it.
Starting Zipkin with Docker
docker run -d -p 9411:9411 openzipkin/zipkin
The UI opens at http://localhost:9411. If you prefer to manage it with docker-compose, use the following (for more on Dockerfiles, see the Docker containerization guide).
services:
zipkin:
image: openzipkin/zipkin
ports:
- "9411:9411"
Implementing order-service
In Spring Boot 3.2+, the RestClient.Builder Bean is automatically instrumented by ObservationRestClientCustomizer, so the trace ID is attached to HTTP headers with no additional configuration.
@Configuration
public class RestClientConfig {
@Bean
public RestClient restClient(RestClient.Builder builder) {
return builder.baseUrl("http://localhost:8081").build();
}
}
@RestController
public class OrderController {
private final RestClient restClient;
public OrderController(RestClient restClient) {
this.restClient = restClient;
}
@GetMapping("/orders/{id}")
public String getOrder(@PathVariable String id) {
String inventory = restClient.get()
.uri("/inventory/{id}", id)
.retrieve()
.body(String.class);
return "Order: " + id + ", Inventory: " + inventory;
}
}
When using Brave, the trace ID is propagated via the B3 header (b3). If you use the OpenTelemetry bridge, the W3C Trace Context format (traceparent) is used instead, which requires separate configuration.
Implementing inventory-service
@RestController
public class InventoryController {
@GetMapping("/inventory/{id}")
public String getInventory(@PathVariable String id) throws InterruptedException {
Thread.sleep(200); // 遅延を入れてトレースを見やすくする
return "in-stock";
}
}
The trace ID is automatically carried over from the incoming B3 header. No special configuration is needed on this side.
Embedding traceId and spanId in Logs
Micrometer Tracing automatically sets traceId and spanId in the MDC. All you need to do is add them to the pattern in logback-spring.xml.
<pattern>%d{HH:mm:ss} [%X{traceId},%X{spanId}] %-5level %logger{36} - %msg%n</pattern>
For details on logging configuration, see the Logback and SLF4J article. This lets you cross-reference logs and Zipkin traces using the traceId.
Verifying It Works
Start both services and send a request with curl.
curl http://localhost:8080/orders/123
Confirm that the traceId matches in the logs of each service.
# order-service
[abc123def,111aaa] INFO OrderController - ...
# inventory-service(同じtraceIdが伝播している)
[abc123def,222bbb] INFO InventoryController - ...
Open http://localhost:9411 and click Run Query to display the traces.
Reading the Zipkin UI
- Filtering is done by specifying a service name (such as
order-service) or a time range and clicking the Find Traces button. When hunting for slow requests, filtering by Duration (minimum latency) is handy - The waterfall view appears when you click a trace, showing the start and end time of each span as horizontal bars. The 200ms delay from
Thread.sleep(200)in inventory-service should be clearly visible - Error checking is easy because spans where an error occurred are highlighted in red. Clicking a span shows details such as the exception message and HTTP status code
About Asynchronous Processing Such as Kafka
For asynchronous processing such as Kafka or Spring @Async, automatic propagation via HTTP headers is not available. If you use spring-kafka, Micrometer Tracing provides automatic instrumentation. See the Spring Boot + Kafka article for details.
Production Configuration
sampling.probability=1.0 traces every request and is intended for development environments. In production, lower it to around 0.1 to 0.3 to keep overhead down.
In a Kubernetes environment, specify the Zipkin endpoint using the Service domain name.
management.zipkin.tracing.endpoint=http://zipkin.monitoring.svc.cluster.local:9411/api/v2/spans
If you need to switch between B3 and W3C Trace Context, you can do so with management.tracing.propagation.type=w3c. This is useful when other services require the W3C format or when forwarding through an OTel collector.
For deploying to Kubernetes, see also the Kubernetes deployment guide.
Summary
Sleuth is gone with the move to Spring Boot 3.x, but switching to Micrometer Tracing is just a matter of swapping two dependencies, with almost no code changes required.
Once you have all three pillars in place, metrics (Prometheus + Grafana), logs (Logback), and traces (Micrometer + Zipkin), investigating problems in microservices becomes dramatically easier. Start by trying it out locally.