How to implement distributed tracing using Spring Cloud Sleuth in Spring Boot?

Microservices comes with many advantages.

But there are disadvantages too.

One of them is tracing the logs of your microservices.

When there are so many microservices , user requests will span many of them and it will be difficult to trace the logs for a particular request when an issue occurs.

To resolve this you can use one of the design patterns in microservices:

Distributed Tracing.

What is distributed tracing?

Distributed tracing appends two ids for every http request made.

One is common across the request. This is called trace id.

One is common to each microservice component in the request. This is called span id.

This way you can trace a request in production easily when an issue occurs.

If you use a log aggregation tool like Splunk you can easily retrieve all the logs specific to that request by searching for the “trace id”

How to implement log tracing in Spring Boot?

Spring provides a library named Spring Cloud Sleuth.

Just including the dependency in your microservice project is enough to activate distributed tracing.

Let’s look at the implementation with an example:

STEP1: Add required dependencies

Add the below two dependencies in all of our microservices:

         <dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-sleuth</artifactId>
		</dependency>
	
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

The starter-web dependency is required to create the REST APIs.

I created three microservices and added the above three dependencies in all of them.

Here is a sample 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.6</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>sleuth-1</artifactId>
	<version>1.0.0</version>
	<name>sleuth-1</name>
	<description>Demo project for Spring Cloud Sleuth</description>
	<properties>
		<java.version>11</java.version>
		<spring-cloud.version>2021.0.1</spring-cloud.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-sleuth</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</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>
			</plugin>
		</plugins>
	</build>

</project>

STEP2: Create a Rest Template bean

To invoke one microservice from another you need to make a http call using RestTemplate or WebClient. This should be created as a Spring Bean and not using new() operator (where it is used) as Spring needs to append the tracing headers in every HTTP call.

Here is an example Rest Template I created:

package com.example.cloud.sleuth;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class Config {

	
	@Bean
	public RestTemplate restTemplate() {
		
		return new RestTemplate();
	}
}

Just creating a new Rest Template bean in a Configuration class.

This needs to be injected where the REST API call is made.

STEP3: Add application name

Spring Sleuth expects the application name configured in application.yml for each microservice for the tracing to work.

Add them for all the projects.

Here is an example :




spring:
 application:
    name: sleuth1

That’s it!

I deployed three microservices at ports 8080,8081 and 8082.

The one at port 8080 calls the microservice at port 8081 which in turn calls the microservice deployed at port 8082.

Let us look into the three REST API s:

Here is the first API which calls the second microservice:

package com.example.cloud.sleuth;

import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import org.slf4j.Logger;

@RestController
public class SleuthTestController {

	Logger logger = LoggerFactory.getLogger("sleuth1");
	
	@Autowired
	private RestTemplate restTemplate;
	
	@GetMapping("/sleuth1")
	public String sleuth1() {
		
		
		logger.info("Flow started here");
		
		return restTemplate.getForObject("http://localhost:8081/sleuth2", String.class);
	}
}

Just a regular REST API call with the log “Flow started here”. As earlier mentioned I have injected the Rest Template created as a Spring Bean here and not created one new using new operator.

Here is the REST API in the second microservice:

package com.example.cloud.sleuth;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class SleuthTestController {

	Logger logger = LoggerFactory.getLogger("sleuth2");
	@Autowired
	private RestTemplate restTemplate;

	@GetMapping("/sleuth2")
	public String sleuth2() {
		
		logger.info("This is the second component in the flow");
		
		return restTemplate.getForObject("http://localhost:8082/sleuth3", String.class);
	}
}

Same code with a call to the third microservice deployed at port 8082.

Here is the last REST API of the third microservice:

package com.example.cloud.sleuth;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SleuthTestController {

	Logger logger = LoggerFactory.getLogger("sleuth3");

	@GetMapping("/sleuth3")
	public String sleuth3() {
		logger.info("Last Component in the flow");
		
		return "I am the last component in the application flow";
	}
}

No further calls from here.

Now let us hit the first API “/sleuth1”:

We are getting the expected response.

The first microservice hit the second microservice which hit the third microservice and returned the response as shown above.

Now let us look into the logs.

Logs from the first microservice:

Check the part of the log highlighted in green.

It has three values separated by comma within square brackets.

The first one is the application name.

The second one is the trace id – this will be common across all the logs

The third one is the span id – this will be common within this microservice

Now let’s look at the second microservice logs:

As you notice the trace id is the same.

Only the span id is different for this microservice.

Here is the logs from the third microservice:

Here too the trace id is the same as of the previous two microservices.

Only the span id is different and it is same for all the logs within the microservice for the specific request.

That’s it!

Comments

Leave a Reply

Discover more from The Full Stack Developer

Subscribe now to keep reading and get access to the full archive.

Continue reading