How to Send Traces from Spring Boot to Jaeger

A guide on enhancing Spring Boot applications to enable sending request traces to Jaeger

Mahdi Mallaki
Cloud Native Daily

--

Introduction

If you want to observe the precise flow of interactions between your microservices, you require a tool that captures the traces of requests as they traverse through the various microservices, as well as the time taken at each step.

There are numerous methods for sending traces from an application to Jaeger. In the case of a Java application, you have different options, such as Micrometer, Spring Cloud Sleuth and OpenTelemetry Java Agent. In this article, I will explain the differences between them and provide guidance on how to send traces from a Java application to a Jaeger server.

Jaeger VS Zipkin

Jaeger is currently the most popular trace analytics tool, but there are alternative options available, such as Zipkin. In comparison to Zipkin, Jaeger boasts modularity, better performance, and a larger community. Additionally, while Zipkin is an older project (initiated in 2012 at Twitter) written in Java, Jaeger is a newer project (commenced in 2017 at Uber) developed in Go.

Micrometer VS Sleuth VS OpenTelemetry agent

Spring Cloud Sleuth has been discontinued in Spring Boot 3.x, and it can only be used until the end of Spring Boot 2. To send your traces to Jaeger, you need to migrate to the Micrometer library.

Alternatively, you can use the OpenTelemetry Java Agent Instrumentor, but I do not recommend it due to potential security problems that a Java Agent may introduce to your code. Additionally, it may cause performance issues as it can inject interceptors before and after each method call within your application.

Therefore, I recommend using Micrometer as a superior alternative to Sleuth or the OpenTelemetry Java Agent.

The architecture

In the above image, the client initiates a request to microservice 1, which is a Java Spring Boot application. Microservice 1 then forwards the request to microservice 2, transparently from the user’s perspective. Once microservice 2 responds, microservice 1 relays the response back to the client.

Both of these microservices send their traces, following the OpenTelemetry Standard Protocol, to the OpenTelemetry Collector. This module is part of the Jaeger family and can currently accept OpenTelemetry traces. Consequently, there is no need to set up a separate instance solely for collecting OpenTelemetry traces. It is important to note that the deprecated OpenTracing protocol has been replaced by OpenTelemetry.

1. Add Micrometer to the project

First, you need to add it to the build.gradle as a dependency like this:

dependencies {
implementation "io.micrometer:micrometer-tracing-bridge-otel"
implementation "io.opentelemetry:opentelemetry-exporter-otlp"
}

2. Configure the Span Exporter

for configuring the span exporter, you need to add a bean like this:

@Bean
public OtlpGrpcSpanExporter otlpHttpSpanExporter(@Value("${tracing.url}") String url) {
return OtlpGrpcSpanExporter.builder().setEndpoint(url).build();
}

add these configurations to application.properties file:

management.tracing.sampling.probability=1.0
tracing.url=http://localhost:4317

The port 4317 is for gRPC, which is lighter than the HTTP protocol.

3. add a sample controller

you can add a sample controller to see what will happen:

@RestController
@RequestMapping("/hello")
public class Controller {

private static final Logger logger = LoggerFactory.getLogger(Controller.class);
Random random = new Random();

private RestTemplate restTemplate;
@Value("${spring.application.name}")
private String applicationName;
public Controller(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}

@GetMapping("/path1")
public ResponseEntity<String> path1() {
logger.info("Incoming request at {} for request /path1 ", applicationName);
doNothingButSleepForSomeTime();
String response = restTemplate.getForObject("http://localhost:8090/hello/path2", String.class);
doNothingButSleepForSomeTime();
return ResponseEntity.ok("response from /path1 + " + response);
}

@GetMapping("/path2")
public ResponseEntity<String> path2() {
logger.info("Incoming request at {} at /path2", applicationName);
doNothingButSleepForSomeTime();
return ResponseEntity.ok("response from /path2 ");
}

public void doNothingButSleepForSomeTime() {
try {
int sleepTime = random.nextInt(2, 5);
logger.info("sleeping for " + sleepTime + " seconds");
Thread.sleep(sleepTime * 1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

the directory structure of this project is something like this:

.
├── docker-compose.yaml
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── demo
│ │ │ ├── Controller.java
│ │ │ ├── DemoApplication.java
│ │ └── resources
│ │ └── application.properties
│ └── test
│ └── java
│ └── com
│ └── example
│ └── demo
│ └── DemoApplicationTests.java

4. Run the project

To test our code, you need to run the application twice using different ports, such as 8080 and 8090, in order to send a request from the first instance to the second instance. You can achieve this by executing the following commands:

$ java -jar target/demo-0.0.1-SNAPSHOT.jar \
--spring.application.name=Service-1 --server.port=8080
$ java -jar target/demo-0.0.1-SNAPSHOT.jar \
--spring.application.name=Service-2 --server.port=8090

5. Bring up Jaeger AllInOne

The Jaeger All-In-One can be easily set up with all the necessary components to test your Java application and ensure that it correctly sends traces. It also includes an OpenTelemetry Collector module for collecting OpenTelemetry traces. You can bring it up simply by using a docker-compose.yaml file like the following:

version: '3.7'
services:
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # the jaeger UI
- "4317:4317" # the OpenTelemetry collector grpc
environment:
- COLLECTOR_OTLP_ENABLED=true

run the following command to bring up the docker-compose file:

$ docker-compose up -d 

6. Send sample requests

Now you can send a request to microservice 1, and it will internally send a request to microservice 2 in the background:

$ curl http://localhost:8080/hello/path1

7. See traces on the Jaeger dashboard

now you can browse the Jaeger dashboard at the http://localhost:16686 address:

Github repository

You can find all the code used in this document at the following GitHub repository:

Feedback

If you have any feedback or suggestions for improving my code, please leave a comment on this post or send me an email at mallakimahdi@gmail.com. I would greatly appreciate your contributions to help make this article better. If you enjoyed this post, be sure to follow me to stay updated on my latest articles.

--

--