Spring Boot Microservices — Part8 — Sleuth & Zipkin & TraceableExecutorService
In this tutorial, we’ll implement the Sleuth and Zipkin server and also use TraceableExecutorService to propagate Trace Id within same ThreadPool.
order-service
Docker
The Docker Zipkin project is able to build docker images, provide scripts and a docker-compose.yml
for launching pre-built images. The quickest start is to run the latest image directly:
docker run -d -p 9411:9411 openzipkin/zipkin
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>order-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>order-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
<spring-cloud.version>2021.0.2</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.properties
#MySQL DB
spring.datasource.username=root
spring.datasource.password=password
# Need to look at the ways to
spring.application.name=order-service
server.port=0
eureka.instance.instance-id=${spring.application.name}:${random.uuid}
#spring.config.import=optional:configserver:http://localhost:8888?fail-fast=true&max-attempts=10&max-interval=1500
spring.config.import=optional:configserver:http://localhost:8888?fail-fast=true&max-attempts=10&max-interval=1500&vault://secret/order-service
management.endpoints.web.exposure.include=*
#RabbitMQ
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
#
spring.cloud.stream.bindings.notificationEventSupplier-out-0.destination=notification-events
spring.cloud.stream.source=notificationEventSupplier
# Sleuth & Zipkin
spring.sleuth.circuitbreaker.enabled=false
spring.sleuth.integration.enabled=true
spring.zipkin.base-url=http://localhost:9411/
spring.sleuth.sampler.probability=1
OrderController.java
package com.example.controller;
import com.example.client.InventoryClient;
import com.example.dto.OrderDto;
import com.example.model.Order;
import com.example.repository.OrderRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JCircuitBreaker;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JCircuitBreakerFactory;
import org.springframework.cloud.stream.function.StreamBridge;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.function.Supplier;
@RestController
@RequestMapping("/api/order")
@RequiredArgsConstructor
@Slf4j
public class OrderController {
private final OrderRepository orderRepository;
private final InventoryClient inventoryClient;
private final Resilience4JCircuitBreakerFactory circuitBreakerFactory;
private final StreamBridge streamBridge;
private final ExecutorService traceableExecutorService;
@PostMapping
public String placeOrder(@RequestBody OrderDto orderDto) {
circuitBreakerFactory.configureExecutorService(traceableExecutorService);
Resilience4JCircuitBreaker circuitBreaker = circuitBreakerFactory.create("inventory");
Supplier<Boolean> booleanSupplier = () -> orderDto.getOrderLineItemsList()
.stream()
.allMatch(orderLineItems -> {
log.info("Making call to Inventory Service for SkuCode {}", orderLineItems.getSkuCode());
return inventoryClient.checkStock(orderLineItems.getSkuCode());
});
boolean isProductInStock = circuitBreaker.run(booleanSupplier, throwable -> handleErrorCase());
if (isProductInStock) {
Order order = new Order();
order.setOrderLineItems(orderDto.getOrderLineItemsList());
order.setOrderNumber(UUID.randomUUID().toString());
orderRepository.save(order);
log.info("Sending Order Details with OrderId {} to Notification Service", order.getId());
//Send the Notication via RabbitMQ
streamBridge.send("notificationEventSupplier-out-0",
MessageBuilder.withPayload(order.getId()).build());
return "Order Placed Successfully!";
}else{
return "Order Failed !, One of the products in the order is not in the stock!";
}
}
private Boolean handleErrorCase() {
System.out.println("fallback called !!");
return false;
}
}
MainApp.java
package com.example;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.sleuth.instrument.async.TraceableExecutorService;
import org.springframework.context.annotation.Bean;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
@RequiredArgsConstructor
public class OrderServiceApplication {
private final BeanFactory beanFactory;
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
@Bean
public ExecutorService traceableExecutorService(){
ExecutorService service = Executors.newCachedThreadPool();
return new TraceableExecutorService(beanFactory, service);
}
}
InventoryClient.java, Order.java, OrderLineItems and OrderDto.java will remain unchanged.
Place the Order now
notification-service
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>notification-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>notification-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
<spring-cloud.version>2021.0.2</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
<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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
<scope>test</scope>
<classifier>test-binder</classifier>
<type>test-jar</type>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
EmailSender.java
@Service
@Slf4j
public class EmailSender {
public void sendEmail(String orderNumber) {
log.info("Order Placed Successfully - Order Number is {}", orderNumber);
log.info("Email Sent For Order Id {}", orderNumber);
}
}
MainApp.java
@SpringBootApplication
@EnableEurekaClient
public class NotificationServiceApplication {
public static void main(String[] args) {
SpringApplication.run(NotificationServiceApplication.class, args);
}
@Bean
public Consumer<Message<String>> notificationEventSupplier() {
return stringMessage -> new EmailSender().sendEmail(stringMessage.getPayload());
}
}
application.properties
spring.application.name=notification-service
eureka.instance.instance-id=${spring.application.name}:${random.uuid}
server.port=0
#
spring.cloud.stream.bindings.notificationEventSupplier-in-0.destination=notification-events
# Sleuth & Zipkin
spring.sleuth.integration.enabled=true
spring.zipkin.base-url=http://localhost:9411/
spring.sleuth.sampler.probability=1
Inventory-service
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>inventory-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>inventory-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
<spring-cloud.version>2021.0.2</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.properties
#MySQL DB
spring.datasource.username=root
spring.datasource.password=password
# Need to look at the ways to
spring.application.name=inventory-service
server.port=0
eureka.instance.instance-id=${spring.application.name}:${random.uuid}
#spring.config.import=optional:configserver:http://localhost:8888?fail-fast=true&max-attempts=10&max-interval=1500
spring.config.import=optional:configserver:http://localhost:8888?fail-fast=true&max-attempts=10&max-interval=1500&vault://secret/order-service
management.endpoints.web.exposure.include=*
#RabbitMQ
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
#Sleuth & Zipkin
spring.zipkin.base-url=http://localhost:9411/
spring.sleuth.sampler.probability=1
Inventory.java
@Entity
@Table(name = "inventory")
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Inventory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String skuCode;
private Integer stock;
}
InventoryRepository.java
public interface InventoryRepository extends JpaRepository<Inventory, Long> {
Optional<Inventory> findBySkuCode(String skuCode);
}
InventoryController.java
@RestController
@RequestMapping("/api/inventory")
@RequiredArgsConstructor
@Slf4j
public class InventoryController {
private final InventoryRepository inventoryRepository;
@GetMapping("/{skuCode}")
public Boolean isInStock(@PathVariable("skuCode") String skuCode) {
log.info("Checking stock for product with skucode - " + skuCode);
Optional<Inventory> optionalInventory = inventoryRepository.findBySkuCode(skuCode);
if(!optionalInventory.isPresent()){
new RuntimeException("Cannot Find Product by sku code " + skuCode);
}
return optionalInventory.get().getStock() > 0;
}
}
MainApp.java
@SpringBootApplication
@EnableEurekaClient
public class InventoryServiceApplication {
public static void main(String[] args) {
SpringApplication.run(InventoryServiceApplication.class, args);
}
}