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);
}

}

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store