In today’s microservices-driven world, observability has become a cornerstone of maintaining robust and resilient software systems. Observability helps developers gain insight into the internal state of an application, making it easier to identify and resolve issues quickly. One of the key pillars of observability is tracing, which allows for the tracking of requests as they traverse through various services, providing a clear picture of how data flows within an application.
Spring Boot, a widely-used framework for building Java-based microservices, offers several ways to implement tracing. Among the most popular methods are OpenTelemetry, a powerful open-source observability framework, and Micrometer Tracing, which is particularly useful in environments like GraalVM native images where traditional Java agents cannot be used.
In this guide, we will delve into three different approaches to implementing tracing in a Spring Boot application: using the OpenTelemetry Java Agent (both versions 1.x and 2.x) and using Micrometer Tracing. We will compare these methods, highlighting their respective strengths and weaknesses, and provide practical examples to help you decide which approach best suits your needs.
The Application Setup
To demonstrate these tracing methods, we’ll use a simple Spring Boot application written in Kotlin. This application consists of a single endpoint that serves as the entry point to the system. Here’s a brief overview of how the application is structured:
- Function
entry()
: This is the main function triggered by the endpoint. It then calls another function named intermediate()
to continue the processing.
- Function
intermediate()
: Utilizes a WebClient
instance (a modern replacement for RestTemplate
) to make an HTTP call back to the endpoint. To prevent infinite loops, the request includes a custom header that stops further processing if detected by entry()
.
Below is the core code for this setup:
kotlin
@SpringBootApplication
class Agent1xApplication
@RestController
class MicrometerController {
private val logger = LoggerFactory.getLogger(MicrometerController::class.java)
@GetMapping("/{message}")
fun entry(@PathVariable message: String, @RequestHeader("X-done") done: String?) {
logger.info("entry: $message")
if (done == null) intermediate()
}
fun intermediate() {
logger.info("intermediate")
RestClient.builder()
.baseUrl("http://localhost:8080/done")
.build()
.get()
.header("X-done", "true")
.retrieve()
.toBodilessEntity()
}
}
Micrometer Tracing
Micrometer Tracing is a part of the broader Micrometer observability toolkit, which provides a vendor-neutral application observability facade. It enables easy integration of tracing capabilities into your Spring Boot application, supporting multiple backend tracing systems like OpenTelemetry.
Dependencies
To integrate Micrometer Tracing, you need to include the following dependencies in your project:
org.springframework.boot:spring-boot-starter-actuator
io.micrometer:micrometer-tracing
io.micrometer:micrometer-tracing-bridge-otel
io.opentelemetry:opentelemetry-exporter-otlp
These dependencies allow your application to send traces to an OpenTelemetry collector, such as Jaeger, without being locked into any specific vendor.
Configuration
You’ll need to set up some environment variables to specify where your traces should be sent and to identify your application within the tracing system:
yaml
services:
jaeger:
image: jaegertracing/all-in-one:1.55
environment:
- COLLECTOR_OTLP_ENABLED=true
ports:
- "16686:16686"
micrometer-tracing:
build:
dockerfile: Dockerfile-micrometer
environment:
MANAGEMENT_OTLP_TRACING_ENDPOINT: http://jaeger:4318/v1/traces
SPRING_APPLICATION_NAME: micrometer-tracing
Adding Custom Spans with Micrometer
Micrometer allows you to create custom spans, which are individual units of trace data, through its Observation API. This API simplifies the process of adding observability to your application by providing a unified interface for both metrics and traces.
Modifying the Application to Include Custom Spans
Here’s how you can modify the application to include custom spans:
kotlin
class MicrometerController(
private val restClient: RestClient,
private val registry: ObservationRegistry
) {
@GetMapping("/{message}")
fun entry(@PathVariable message: String, @RequestHeader("X-done") done: String?) {
logger.info("entry: $message")
val observation = Observation.start("entry", registry)
if (done == null) intermediate(observation)
observation.stop()
}
fun intermediate(parent: Observation) {
logger.info("intermediate")
val observation = Observation.createNotStarted("intermediate", registry)
.parentObservation(parent)
.start()
restClient.get()
.header("X-done", "true")
.retrieve()
.toBodilessEntity()
observation.stop()
}
}
This code will generate custom spans for both the entry()
and intermediate()
functions, which will appear in your tracing tool, such as Jaeger.
OpenTelemetry Java Agent v1
The OpenTelemetry Java Agent is an alternative method that requires no changes to your application code. Instead, the agent attaches to your application at runtime and automatically instruments it for tracing.
Running the Agent
To use the Java Agent, run your application with the following command:
```bash
java -javaagent:opentelemetry-javaagent.jar agent-one-1.0-SNAPSHOT.jar
The agent will automatically track HTTP requests and responses, as well as methods annotated with Spring-specific annotations. It requires no code modification, making it an excellent option for existing applications.
Configuration
Similar to Micrometer, you’ll need to configure the agent using environment variables:
yaml
Copy code
services:
agent-1x:
build:
dockerfile: Dockerfile-agent1
environment:
OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317
OTEL_RESOURCE_ATTRIBUTES: service.name=agent-1x
OTEL_METRICS_EXPORTER: none
OTEL_LOGS_EXPORTER: none
ports:
- "8081:8080"
OpenTelemetry Java Agent v2
In January 2024, OpenTelemetry released a new version of its Java Agent, which introduced some significant changes. One of the most notable differences is that by default, only HTTP requests are traced, and not every Spring-annotated method.
Manual Tracing with @WithSpan
To trace additional methods, you can manually annotate them with @WithSpan. Here’s how you can use it:
kotlin
Copy code
@WithSpan
fun intermediate() {
logger.info("intermediate")
RestClient.builder()
.baseUrl("http://localhost:8080/done")
.build()
.get()
.header("X-done", "true")
.retrieve()
.toBodilessEntity()
}
This annotation will generate a span for the intermediate() function, which will be visible in your tracing tool.
Credit and Original Source:
This article was originally written by Nicolas Fränkel and published on their blog. For more insights from Nicolas, including in-depth discussions on OpenTelemetry and other Java-related topics, you can visit the original post here
Conclusion
Both Micrometer Tracing and the OpenTelemetry Java Agent offer robust solutions for implementing distributed tracing in Spring Boot applications. Micrometer Tracing provides a flexible, code-centric approach that integrates well with Spring Boot’s ecosystem and is ideal for applications that need to be compiled to GraalVM native images. On the other hand, the OpenTelemetry Java Agent offers a hassle-free, zero-code-change solution, which is especially useful for existing applications where modifying the codebase isn’t feasible.
When choosing between these tools, consider your specific needs: whether you prefer code-level control with Micrometer or the simplicity of the Java Agent. Whichever path you choose, you’ll be well-equipped to enhance the observability of your Spring Boot applications.
For those interested in exploring further, the complete source code for this post is available on GitHub.