Setting Up OpenTelemetry for Java Applications
Introduction to OpenTelemetry
OpenTelemetry is an open-source observability framework that provides a unified set of APIs, libraries, and instrumentation to capture distributed traces, metrics, and logs from your applications. As a CNCF project, it has become the industry standard for application observability.
Why Use OpenTelemetry?
- Vendor-Agnostic: Export data to multiple backends (Jaeger, Zipkin, Prometheus, etc.)
- Comprehensive: Supports traces, metrics, and logs
- Auto-Instrumentation: Automatic instrumentation for popular Java frameworks
- Standards-Based: Industry-standard semantic conventions
- Active Community: Strong ecosystem and regular updates
Prerequisites
Before getting started, ensure you have:
- Java Development Kit (JDK) 8 or higher
- Maven 3.6+ or Gradle 6.0+
- IDE (IntelliJ IDEA, Eclipse, or VS Code)
- Basic knowledge of Spring Boot or Java web frameworks
Setting Up OpenTelemetry in Java
There are two main approaches to instrumenting Java applications with OpenTelemetry:
- Automatic Instrumentation (Java Agent) - Zero code changes
- Manual Instrumentation - More control and customization
We’ll cover both approaches.
Approach 1: Automatic Instrumentation with Java Agent
This is the easiest way to get started with OpenTelemetry in Java applications.
Step 1: Download the OpenTelemetry Java Agent
Download the latest OpenTelemetry Java Agent JAR:
1
curl -L -O https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar
Or specify a specific version:
1
wget https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v1.32.0/opentelemetry-javaagent.jar
Step 2: Create a Spring Boot Application
Create a new Spring Boot application:
1
2
3
# Using Spring Initializr or create manually
mkdir opentelemetry-java-demo
cd opentelemetry-java-demo
pom.xml (Maven):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?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
http://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>3.2.0</version>
</parent>
<groupId>com.example</groupId>
<artifactId>opentelemetry-demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Step 3: Create a Sample REST Controller
src/main/java/com/example/demo/controller/ProductController.java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package com.example.demo.controller;
import org.springframework.web.bind.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@RestController
@RequestMapping("/api/products")
public class ProductController {
private static final Logger logger = LoggerFactory.getLogger(ProductController.class);
private List<Product> products = new ArrayList<>();
public ProductController() {
// Initialize with sample data
products.add(new Product(1L, "Laptop", 1299.99));
products.add(new Product(2L, "Mouse", 29.99));
products.add(new Product(3L, "Keyboard", 79.99));
}
@GetMapping
public List<Product> getAllProducts() {
logger.info("Fetching all products");
simulateDelay(100);
return products;
}
@GetMapping("/{id}")
public Optional<Product> getProductById(@PathVariable Long id) {
logger.info("Fetching product with id: {}", id);
simulateDelay(50);
return products.stream()
.filter(p -> p.getId().equals(id))
.findFirst();
}
@PostMapping
public Product createProduct(@RequestBody Product product) {
logger.info("Creating new product: {}", product.getName());
product.setId((long) (products.size() + 1));
products.add(product);
simulateDelay(150);
return product;
}
private void simulateDelay(int milliseconds) {
try {
Thread.sleep(milliseconds);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
class Product {
private Long id;
private String name;
private Double price;
public Product() {}
public Product(Long id, String name, Double price) {
this.id = id;
this.name = name;
this.price = price;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
}
Step 4: Run with the Java Agent
Run your application with the OpenTelemetry Java Agent:
1
2
3
4
5
6
7
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=java-product-service \
-Dotel.traces.exporter=otlp \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-Dotel.exporter.otlp.endpoint=http://localhost:4317 \
-jar target/opentelemetry-demo-1.0.0.jar
Or with Maven:
1
mvn spring-boot:run -Dspring-boot.run.jvmArguments="-javaagent:opentelemetry-javaagent.jar -Dotel.service.name=java-product-service -Dotel.traces.exporter=otlp -Dotel.exporter.otlp.endpoint=http://localhost:4317"
Approach 2: Manual Instrumentation
For more control over your instrumentation, use the OpenTelemetry SDK directly.
Step 1: Add Dependencies
Add OpenTelemetry dependencies to your pom.xml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<properties>
<opentelemetry.version>1.32.0</opentelemetry.version>
</properties>
<dependencies>
<!-- OpenTelemetry API -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>${opentelemetry.version}</version>
</dependency>
<!-- OpenTelemetry SDK -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<version>${opentelemetry.version}</version>
</dependency>
<!-- OpenTelemetry OTLP Exporter -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
<version>${opentelemetry.version}</version>
</dependency>
<!-- OpenTelemetry Instrumentation for Spring Boot -->
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-spring-boot-starter</artifactId>
<version>1.32.0-alpha</version>
</dependency>
</dependencies>
Step 2: Configure OpenTelemetry
Create a configuration class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.example.demo.config;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
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.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenTelemetryConfig {
@Value("${otel.service.name:java-product-service}")
private String serviceName;
@Value("${otel.exporter.otlp.endpoint:http://localhost:4317}")
private String otlpEndpoint;
@Bean
public OpenTelemetry openTelemetry() {
Resource resource = Resource.getDefault()
.merge(Resource.create(Attributes.of(
ResourceAttributes.SERVICE_NAME, serviceName,
ResourceAttributes.SERVICE_VERSION, "1.0.0"
)));
OtlpGrpcSpanExporter spanExporter = OtlpGrpcSpanExporter.builder()
.setEndpoint(otlpEndpoint)
.build();
SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(spanExporter).build())
.setResource(resource)
.build();
return OpenTelemetrySdk.builder()
.setTracerProvider(sdkTracerProvider)
.setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
.buildAndRegisterGlobal();
}
}
Step 3: Add Custom Instrumentation
Create a service with custom spans:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package com.example.demo.service;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final Tracer tracer;
public OrderService(OpenTelemetry openTelemetry) {
this.tracer = openTelemetry.getTracer("OrderService", "1.0.0");
}
public String processOrder(Long orderId) {
Span span = tracer.spanBuilder("processOrder").startSpan();
try (Scope scope = span.makeCurrent()) {
span.setAttribute("order.id", orderId);
// Validate order
validateOrder(orderId);
// Process payment
processPayment(orderId);
// Ship order
shipOrder(orderId);
span.setAttribute("order.status", "completed");
return "Order processed successfully";
} catch (Exception e) {
span.recordException(e);
span.setAttribute("error", true);
throw e;
} finally {
span.end();
}
}
private void validateOrder(Long orderId) {
Span span = tracer.spanBuilder("validateOrder").startSpan();
try (Scope scope = span.makeCurrent()) {
span.setAttribute("order.id", orderId);
// Validation logic
Thread.sleep(50);
span.setAttribute("validation.status", "passed");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
span.end();
}
}
private void processPayment(Long orderId) {
Span span = tracer.spanBuilder("processPayment").startSpan();
try (Scope scope = span.makeCurrent()) {
span.setAttribute("order.id", orderId);
// Payment processing logic
Thread.sleep(100);
span.setAttribute("payment.status", "success");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
span.end();
}
}
private void shipOrder(Long orderId) {
Span span = tracer.spanBuilder("shipOrder").startSpan();
try (Scope scope = span.makeCurrent()) {
span.setAttribute("order.id", orderId);
// Shipping logic
Thread.sleep(75);
span.setAttribute("shipping.status", "dispatched");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
span.end();
}
}
}
Step 4: Configure Application Properties
Add to application.properties or application.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# application.yml
otel:
service:
name: java-product-service
exporter:
otlp:
endpoint: http://localhost:4317
traces:
exporter: otlp
metrics:
exporter: otlp
spring:
application:
name: java-product-service
logging:
level:
io.opentelemetry: DEBUG
Setting Up an Observability Backend
Using Jaeger with Docker
1
2
3
4
5
6
docker run -d --name jaeger \
-e COLLECTOR_OTLP_ENABLED=true \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
jaegertracing/all-in-one:latest
Access Jaeger UI at: http://localhost:16686
Using Docker Compose
Create docker-compose.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
version: '3.8'
services:
jaeger:
image: jaegertracing/all-in-one:latest
container_name: jaeger
environment:
- COLLECTOR_OTLP_ENABLED=true
ports:
- "16686:16686" # Jaeger UI
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
app:
build: .
container_name: java-app
environment:
- OTEL_SERVICE_NAME=java-product-service
- OTEL_TRACES_EXPORTER=otlp
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317
ports:
- "8080:8080"
depends_on:
- jaeger
Run with:
1
docker-compose up -d
Testing Your Implementation
Step 1: Build and Run
1
2
mvn clean package
java -jar target/opentelemetry-demo-1.0.0.jar
Step 2: Make API Calls
1
2
3
4
5
6
7
8
9
10
# Get all products
curl http://localhost:8080/api/products
# Get a specific product
curl http://localhost:8080/api/products/1
# Create a new product
curl -X POST http://localhost:8080/api/products \
-H "Content-Type: application/json" \
-d '{"name":"Monitor","price":299.99}'
Step 3: View Traces in Jaeger
- Open
http://localhost:16686in your browser - Select
java-product-servicefrom the service dropdown - Click “Find Traces”
- Explore the traces and spans
Best Practices
1. Use Semantic Conventions
Follow OpenTelemetry semantic conventions:
1
2
3
4
5
span.setAttribute("http.method", "GET");
span.setAttribute("http.url", "/api/products");
span.setAttribute("http.status_code", 200);
span.setAttribute("db.system", "postgresql");
span.setAttribute("db.statement", "SELECT * FROM products");
2. Add Meaningful Attributes
1
2
3
span.setAttribute("user.id", userId);
span.setAttribute("order.total", orderTotal);
span.setAttribute("product.category", category);
3. Handle Errors Properly
1
2
3
4
5
6
7
try {
// Business logic
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
}
4. Use Span Events
1
2
3
4
span.addEvent("Order validated");
span.addEvent("Payment processed", Attributes.of(
AttributeKey.stringKey("payment.method"), "credit_card"
));
5. Configure Sampling
For production, configure sampling in application.yml:
1
2
3
4
otel:
traces:
sampler:
probability: 0.1 # Sample 10% of traces
Or programmatically:
1
2
3
SdkTracerProvider.builder()
.setSampler(Sampler.traceIdRatioBased(0.1))
.build();
Common Issues and Troubleshooting
Issue 1: No Traces Appearing
- Verify OTLP endpoint is accessible
- Check if Jaeger/collector is running
- Ensure service name is configured
- Check application logs for errors
Issue 2: Performance Impact
- Use batch span processors (default)
- Configure appropriate sampling rates
- Limit custom span creation
- Use async exporters
Issue 3: Missing Context Propagation
- Ensure W3C Trace Context propagator is configured
- Check HTTP headers are being propagated
- Verify instrumentation covers all HTTP clients
Additional Resources
- OpenTelemetry Java Documentation
- OpenTelemetry Java GitHub
- OpenTelemetry Java Instrumentation
- OpenTelemetry Semantic Conventions
Conclusion
OpenTelemetry provides powerful observability capabilities for Java applications. Whether you choose automatic instrumentation with the Java agent for quick setup or manual instrumentation for fine-grained control, OpenTelemetry gives you the flexibility to instrument your applications effectively. Start with automatic instrumentation and add custom spans where you need additional visibility into your application’s behavior.
Happy tracing!