Introduction
Distributed tracing is an essential component in the realm of microservices and distributed systems. It allows developers to monitor and observe the flow of requests across different services, providing insights into the performance and behavior of the system. OpenTelemetry is an open-source observability framework that provides tools, APIs, and SDKs for collecting and exporting telemetry data (traces, metrics, and logs) from applications.
In this tutorial, we will delve into how to implement distributed tracing in Java using OpenTelemetry. This guide is designed for developers with intermediate to advanced knowledge of Java and experience in working with microservices or distributed systems.
Prerequisites
Before we begin, ensure you have the following:
- A working knowledge of Java.
- Familiarity with Maven or Gradle build tools.
- Basic understanding of microservices architecture.
- An IDE or text editor set up for Java development.
Understanding Distributed Tracing
Distributed tracing is the method of tracking requests as they flow through various services in a distributed system. Each step of the request is recorded, providing a comprehensive view of the request lifecycle. Key concepts include:
- Trace: Represents the entire journey of a request from start to finish.
- Span: A single unit of work in a trace. It has a start time and a duration.
- Context Propagation: Passing trace information across service boundaries.
Introduction to OpenTelemetry
OpenTelemetry is a collection of tools, APIs, and SDKs used to instrument, generate, collect, and export telemetry data. It supports multiple programming languages and provides interoperability with various observability backends.
Key Components
- API: Defines the interfaces for creating and manipulating telemetry data.
- SDK: Provides default implementations of the API, including exporters.
- Exporters: Send collected data to backend systems like Jaeger, Zipkin, or Prometheus.
Setting Up the Project
We will start by creating a new Java project. You can use either Maven or Gradle as your build tool. For this tutorial, we will use Maven.
Creating a Maven Project
- Create a new directory for your project and navigate to it:
mkdir opentelemetry-tutorial
cd opentelemetry-tutorial
Code language: Shell Session (shell)
- Generate a new Maven project:
mvn archetype:generate -DgroupId=com.example -DartifactId=opentelemetry-tutorial -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
cd opentelemetry-tutorial
Code language: Shell Session (shell)
- Open the
pom.xml
file and add the necessary dependencies for OpenTelemetry:
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>opentelemetry-tutorial</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-jaeger</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-context</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-extension-trace-propagators</artifactId>
<version>1.10.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
Code language: HTML, XML (xml)
Integrating OpenTelemetry with a Java Application
Adding Dependencies
The dependencies for OpenTelemetry include the API, SDK, context propagation, and an exporter for sending traces to Jaeger. We have already added these dependencies in the pom.xml
file.
Initializing OpenTelemetry SDK
Next, we need to initialize the OpenTelemetry SDK in our application. Create a class named OpenTelemetryConfig
to handle this configuration.
package com.example;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.exporter.jaeger.JaegerGrpcSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;
public class OpenTelemetryConfig {
private static final String SERVICE_NAME = "opentelemetry-tutorial";
public static OpenTelemetry initOpenTelemetry() {
// Create a Jaeger exporter
JaegerGrpcSpanExporter jaegerExporter = JaegerGrpcSpanExporter.builder()
.setEndpoint("http://localhost:14250")
.build();
// Create a resource to identify this service
Resource serviceNameResource = Resource.create(
ResourceAttributes.SERVICE_NAME, SERVICE_NAME);
// Create a tracer provider
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(jaegerExporter).build())
.setResource(serviceNameResource)
.build();
// Initialize OpenTelemetry SDK
OpenTelemetrySdk openTelemetrySdk = OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.buildAndRegisterGlobal();
return openTelemetrySdk;
}
public static Tracer getTracer() {
return GlobalOpenTelemetry.getTracer(SERVICE_NAME);
}
}
Code language: Java (java)
Creating and Exporting Spans
Spans represent individual units of work. Let’s create a simple application that uses spans.
- Create a class named
HelloService
:
package com.example;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
public class HelloService {
private final Tracer tracer;
public HelloService(Tracer tracer) {
this.tracer = tracer;
}
public void sayHello(String name) {
Span span = tracer.spanBuilder("sayHello").startSpan();
span.addEvent("Saying hello to " + name);
System.out.println("Hello, " + name);
span.end();
}
}
Code language: Java (java)
- Create a
Main
class to run the application:
package com.example;
public class Main {
public static void main(String[] args) {
OpenTelemetryConfig.initOpenTelemetry();
HelloService helloService = new HelloService(OpenTelemetryConfig.getTracer());
helloService.sayHello("World");
}
}
Code language: Java (java)
Context Propagation
Context propagation ensures that trace information is passed across service boundaries. OpenTelemetry provides several context propagation mechanisms, including W3C Trace Context and B3 Propagation.
To demonstrate context propagation, we’ll simulate a simple HTTP call between two services using the okhttp
library.
- Add the
okhttp
dependency to yourpom.xml
file:
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.3</version>
</dependency>
Code language: HTML, XML (xml)
- Create a
HttpService
class to make HTTP requests:
package com.example;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.extension.trace.propagation.TraceMultiPropagator;
import io.opentelemetry.extension.trace.propagation.W3CTraceContextPropagator
;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
public class HttpService {
private final Tracer tracer;
private final OkHttpClient client;
public HttpService(Tracer tracer) {
this.tracer = tracer;
this.client = new OkHttpClient();
}
public void makeRequest(String url) throws IOException {
Span span = tracer.spanBuilder("makeRequest").startSpan();
try (Scope scope = span.makeCurrent()) {
span.addEvent("Making request to " + url);
Request request = new Request.Builder()
.url(url)
.build();
Response response = client.newCall(request).execute();
span.addEvent("Received response with status code: " + response.code());
} finally {
span.end();
}
}
}
Code language: Java (java)
- Modify the
Main
class to use theHttpService
:
package com.example;
public class Main {
public static void main(String[] args) throws IOException {
OpenTelemetryConfig.initOpenTelemetry();
HelloService helloService = new HelloService(OpenTelemetryConfig.getTracer());
helloService.sayHello("World");
HttpService httpService = new HttpService(OpenTelemetryConfig.getTracer());
httpService.makeRequest("http://example.com");
}
}
Code language: Java (java)
Using OpenTelemetry with Spring Boot
Spring Boot simplifies the process of setting up and running a microservice. Integrating OpenTelemetry with Spring Boot involves adding the necessary dependencies and configuring the tracing components.
- Create a new Spring Boot project using Spring Initializr (https://start.spring.io/) with the following dependencies:
- Spring Web
- Spring Boot Actuator
- Add the OpenTelemetry dependencies to your
pom.xml
:
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-jaeger</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-extension-trace-propagators</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-spring-boot-autoconfigure</artifactId>
<version>1.10.0</version>
</dependency>
Code language: HTML, XML (xml)
- Configure OpenTelemetry in your Spring Boot application. Create a
OpenTelemetryConfig
class:
package com.example;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.exporter.jaeger.JaegerGrpcSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenTelemetryConfig {
private static final String SERVICE_NAME = "spring-boot-opentelemetry";
@Bean
public OpenTelemetry openTelemetry() {
JaegerGrpcSpanExporter jaegerExporter = JaegerGrpcSpanExporter.builder()
.setEndpoint("http://localhost:14250")
.build();
Resource resource = Resource.create(
ResourceAttributes.SERVICE_NAME, SERVICE_NAME);
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(jaegerExporter).build())
.setResource(resource)
.build();
OpenTelemetrySdk openTelemetrySdk = OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.buildAndRegisterGlobal();
return openTelemetrySdk;
}
}
Code language: Java (java)
- Create a simple controller to demonstrate tracing:
package com.example;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
@RestController
public class HelloController {
private final Tracer tracer;
public HelloController(Tracer tracer) {
this.tracer = tracer;
}
@GetMapping("/hello")
public String hello() {
Span span = tracer.spanBuilder("hello").startSpan();
span.addEvent("Handling /hello request");
String message = "Hello, World!";
span.addEvent("Returning response: " + message);
span.end();
return message;
}
}
Code language: Java (java)
Exporting Traces to a Backend
To view traces, we need to export them to a tracing backend like Jaeger. We have already configured Jaeger exporter in our OpenTelemetryConfig
classes.
Running Jaeger
To run Jaeger locally, use the following Docker command:
docker run -d --name jaeger \
-e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
-p 5775:5775/udp \
-p 6831:6831/udp \
-p 6832:6832/udp \
-p 5778:5778 \
-p 16686:16686 \
-p 14268:14268 \
-p 14250:14250 \
-p 9411:9411 \
jaegertracing/all-in-one:1.25
Code language: Shell Session (shell)
Access the Jaeger UI at http://localhost:16686
.
Visualizing Traces with Jaeger
Once Jaeger is running, you can start your Java application and make some requests. Open the Jaeger UI and search for traces. You should see traces from your application with detailed information about each span.
Advanced Topics
Custom Instrumentation
OpenTelemetry allows you to instrument your code to capture specific events or metrics.
Span span = tracer.spanBuilder("custom-operation").startSpan();
try (Scope scope = span.makeCurrent()) {
// Your custom logic here
span.addEvent("Custom event");
} finally {
span.end();
}
Code language: Java (java)
Sampling
Sampling controls the amount of telemetry data collected. By default, OpenTelemetry samples all traces. You can configure a different sampling strategy:
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.setSampler(Sampler.traceIdRatioBased(0.5)) // Sample 50% of traces
.build();
Code language: Java (java)
Resource Attributes
Resource attributes provide additional context about your service. You can add custom resource attributes:
Resource resource = Resource.builder()
.put(ResourceAttributes.SERVICE_NAME, "my-service")
.put(ResourceAttributes.SERVICE_INSTANCE_ID, "instance-1")
.build();
Code language: Java (java)
Best Practices
- Instrument Key Operations: Focus on instrumenting critical paths and key operations.
- Use Context Propagation: Ensure context is propagated across service boundaries for end-to-end tracing.
- Configure Sampling: Adjust sampling rates to balance performance and data collection.
- Leverage Exporters: Use appropriate exporters to send telemetry data to observability backends.
- Monitor Performance: Keep an eye on the performance impact of instrumentation and adjust as necessary.
Conclusion
Implementing distributed tracing in Java with OpenTelemetry provides deep insights into the behavior and performance of your distributed system. By following this tutorial, you have learned how to set up OpenTelemetry, create and export spans, propagate context, and visualize traces using Jaeger. These techniques will help you monitor and debug your applications more effectively, ensuring a more resilient and performant system. Remember, observability is an ongoing process. Continuously refine your instrumentation and tracing strategies to adapt to the evolving needs of your system.