OpenTelemetry is a vendor-neutral observability framework for instrumenting, generating, collecting, and exporting telemetry data (metrics, logs, and traces). It's the successor to OpenTracing and OpenCensus.
Overview
OpenTelemetry provides:
- Distributed Tracing: Track requests across service boundaries
- Metrics Collection: Gather quantitative data about system performance
- Logs Integration: Correlate logs with traces and metrics
- Vendor Neutral: Export to multiple backends (Jaeger, Zipkin, Prometheus, etc.)
Setup and Dependencies
Maven Dependencies
<!-- OpenTelemetry BOM --> <dependencyManagement> <dependencies> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-bom</artifactId> <version>1.32.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <!-- Core OpenTelemetry API --> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-api</artifactId> </dependency> <!-- OpenTelemetry SDK --> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-sdk</artifactId> </dependency> <!-- OpenTelemetry Exporter for Jaeger --> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-exporter-jaeger</artifactId> </dependency> <!-- OpenTelemetry Exporter for Logging --> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-exporter-logging</artifactId> </dependency> <!-- OpenTelemetry Metrics --> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-sdk-metrics</artifactId> </dependency> <!-- OpenTelemetry Semantic Conventions --> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-semconv</artifactId> <version>1.23.1-alpha</version> </dependency> <!-- OpenTelemetry Instrumentation Annotations --> <dependency> <groupId>io.opentelemetry.instrumentation</groupId> <artifactId>opentelemetry-instrumentation-annotations</artifactId> <version>2.0.0-alpha</version> </dependency>
Gradle Dependencies
dependencies {
implementation platform('io.opentelemetry:opentelemetry-bom:1.32.0')
implementation 'io.opentelemetry:opentelemetry-api'
implementation 'io.opentelemetry:opentelemetry-sdk'
implementation 'io.opentelemetry:opentelemetry-exporter-jaeger'
implementation 'io.opentelemetry:opentelemetry-exporter-logging'
implementation 'io.opentelemetry:opentelemetry-sdk-metrics'
implementation 'io.opentelemetry:opentelemetry-semconv:1.23.1-alpha'
implementation 'io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations:2.0.0-alpha'
}
Basic Configuration
Example 1: Basic OpenTelemetry Setup
package com.example.otel.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.jaeger.JaegerGrpcSpanExporter;
import io.opentelemetry.exporter.logging.LoggingSpanExporter;
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.sdk.trace.export.SimpleSpanProcessor;
import io.opentelemetry.semconv.ResourceAttributes;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenTelemetryConfig {
@Bean
public OpenTelemetry openTelemetry() {
// Define resource identifying the service
Resource resource = Resource.getDefault()
.merge(Resource.create(Attributes.builder()
.put(ResourceAttributes.SERVICE_NAME, "order-service")
.put(ResourceAttributes.SERVICE_VERSION, "1.0.0")
.put(ResourceAttributes.DEPLOYMENT_ENVIRONMENT, "production")
.build()));
// Configure Jaeger exporter
JaegerGrpcSpanExporter jaegerExporter = JaegerGrpcSpanExporter.builder()
.setEndpoint("http://localhost:14250") // Jaeger collector endpoint
.build();
// Configure tracer provider
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.setResource(resource)
.addSpanProcessor(BatchSpanProcessor.builder(jaegerExporter).build())
.addSpanProcessor(SimpleSpanProcessor.create(LoggingSpanExporter.create()))
.build();
// Build OpenTelemetry instance
return OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
.buildAndRegisterGlobal();
}
}
Example 2: Spring Boot Auto-Configuration
package com.example.otel.config;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.semconv.ResourceAttributes;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OtelSpringConfig {
@Value("${spring.application.name:unknown-service}")
private String serviceName;
@Bean
public Tracer tracer(OpenTelemetry openTelemetry) {
return openTelemetry.getTracer(serviceName, "1.0.0");
}
@Bean
public Meter meter(OpenTelemetry openTelemetry) {
return openTelemetry.getMeter(serviceName);
}
// Custom resource with service attributes
@Bean
public io.opentelemetry.sdk.resources.Resource resource() {
return io.opentelemetry.sdk.resources.Resource.getDefault()
.merge(io.opentelemetry.sdk.resources.Resource.create(
Attributes.of(
ResourceAttributes.SERVICE_NAME, serviceName,
ResourceAttributes.SERVICE_VERSION, "1.0.0",
ResourceAttributes.DEPLOYMENT_ENVIRONMENT, "dev"
)
));
}
}
Manual Instrumentation
Example 3: Basic Tracing
package com.example.otel.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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.ThreadLocalRandom;
@Service
public class OrderService {
private final Tracer tracer;
@Autowired
public OrderService(OpenTelemetry openTelemetry) {
this.tracer = openTelemetry.getTracer(OrderService.class.getName(), "1.0.0");
}
public String createOrder(String productId, int quantity) {
// Start a new span
Span span = tracer.spanBuilder("createOrder")
.setAttribute("product.id", productId)
.setAttribute("order.quantity", quantity)
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Business logic
span.addEvent("Order creation started");
// Validate product
validateProduct(productId);
// Check inventory
boolean inStock = checkInventory(productId, quantity);
if (!inStock) {
span.setAttribute("order.status", "out_of_stock");
span.addEvent("Product out of stock");
throw new RuntimeException("Product out of stock");
}
// Process payment
processPayment(productId, quantity);
// Generate order ID
String orderId = generateOrderId();
span.setAttribute("order.id", orderId);
span.setAttribute("order.status", "created");
span.addEvent("Order created successfully");
return orderId;
} catch (Exception e) {
span.recordException(e);
span.setAttribute("order.status", "failed");
throw e;
} finally {
span.end();
}
}
private void validateProduct(String productId) {
Span span = tracer.spanBuilder("validateProduct")
.setAttribute("product.id", productId)
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Simulate validation
Thread.sleep(ThreadLocalRandom.current().nextInt(50, 200));
span.addEvent("Product validated");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
span.recordException(e);
} finally {
span.end();
}
}
private boolean checkInventory(String productId, int quantity) {
Span span = tracer.spanBuilder("checkInventory")
.setAttribute("product.id", productId)
.setAttribute("check.quantity", quantity)
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Simulate inventory check
Thread.sleep(ThreadLocalRandom.current().nextInt(30, 150));
boolean available = ThreadLocalRandom.current().nextBoolean();
span.setAttribute("inventory.available", available);
return available;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
span.recordException(e);
return false;
} finally {
span.end();
}
}
private void processPayment(String productId, int quantity) {
Span span = tracer.spanBuilder("processPayment")
.setAttribute("product.id", productId)
.setAttribute("payment.quantity", quantity)
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Simulate payment processing
Thread.sleep(ThreadLocalRandom.current().nextInt(100, 500));
span.addEvent("Payment processed successfully");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
span.recordException(e);
throw new RuntimeException("Payment processing failed", e);
} finally {
span.end();
}
}
private String generateOrderId() {
return "ORD-" + System.currentTimeMillis() +
"-" + ThreadLocalRandom.current().nextInt(1000, 9999);
}
}
Example 4: Advanced Tracing with Attributes and Events
package com.example.otel.service;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Context;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class PaymentService {
private final Tracer tracer;
// Custom attribute keys
private static final AttributeKey<String> PAYMENT_METHOD_KEY =
AttributeKey.stringKey("payment.method");
private static final AttributeKey<Long> PAYMENT_AMOUNT_KEY =
AttributeKey.longKey("payment.amount");
private static final AttributeKey<String> PAYMENT_CURRENCY_KEY =
AttributeKey.stringKey("payment.currency");
public PaymentService(Tracer tracer) {
this.tracer = tracer;
}
public PaymentResult processPayment(PaymentRequest request) {
// Create a span with specific attributes
Span span = tracer.spanBuilder("processPayment")
.setSpanKind(SpanKind.SERVER)
.setAttribute(PAYMENT_METHOD_KEY, request.getMethod())
.setAttribute(PAYMENT_AMOUNT_KEY, request.getAmount())
.setAttribute(PAYMENT_CURRENCY_KEY, request.getCurrency())
.setAttribute("user.id", request.getUserId())
.startSpan();
try (var scope = span.makeCurrent()) {
span.addEvent("Payment processing started");
// Validate payment request
validatePaymentRequest(request);
// Process based on payment method
String transactionId = switch (request.getMethod()) {
case "credit_card" -> processCreditCardPayment(request);
case "paypal" -> processPayPalPayment(request);
case "bank_transfer" -> processBankTransfer(request);
default -> throw new IllegalArgumentException("Unsupported payment method");
};
span.addEvent("Payment processed successfully");
span.setAttribute("payment.transaction_id", transactionId);
return new PaymentResult(transactionId, "SUCCESS", "Payment processed successfully");
} catch (Exception e) {
span.setStatus(StatusCode.ERROR, "Payment processing failed");
span.recordException(e, Map.of(
AttributeKey.stringKey("error.type"), e.getClass().getSimpleName(),
AttributeKey.stringKey("error.message"), e.getMessage()
));
return new PaymentResult(null, "FAILED", e.getMessage());
} finally {
span.end();
}
}
private String processCreditCardPayment(PaymentRequest request) {
Span span = tracer.spanBuilder("processCreditCardPayment")
.setAttribute("card.last_four", request.getCardLastFour())
.setAttribute("card.type", request.getCardType())
.startSpan();
try (var scope = span.makeCurrent()) {
// Simulate credit card processing
Thread.sleep(200);
if (request.getAmount() > 10000) {
span.addEvent("Large transaction flagged");
}
// Generate transaction ID
return "CC-" + System.currentTimeMillis();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Credit card processing interrupted", e);
} finally {
span.end();
}
}
private String processPayPalPayment(PaymentRequest request) {
Span span = tracer.spanBuilder("processPayPalPayment")
.setAttribute("paypal.email", request.getPaypalEmail())
.startSpan();
try (var scope = span.makeCurrent()) {
// Simulate PayPal processing
Thread.sleep(150);
return "PP-" + System.currentTimeMillis();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("PayPal processing interrupted", e);
} finally {
span.end();
}
}
private void validatePaymentRequest(PaymentRequest request) {
if (request.getAmount() <= 0) {
throw new IllegalArgumentException("Invalid payment amount");
}
if (request.getMethod() == null || request.getMethod().isBlank()) {
throw new IllegalArgumentException("Payment method is required");
}
}
// DTO classes
public static class PaymentRequest {
private String method;
private long amount;
private String currency;
private String userId;
private String cardLastFour;
private String cardType;
private String paypalEmail;
// constructors, getters, setters
public PaymentRequest() {}
public PaymentRequest(String method, long amount, String currency, String userId) {
this.method = method;
this.amount = amount;
this.currency = currency;
this.userId = userId;
}
// getters and setters
public String getMethod() { return method; }
public void setMethod(String method) { this.method = method; }
public long getAmount() { return amount; }
public void setAmount(long amount) { this.amount = amount; }
public String getCurrency() { return currency; }
public void setCurrency(String currency) { this.currency = currency; }
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getCardLastFour() { return cardLastFour; }
public void setCardLastFour(String cardLastFour) { this.cardLastFour = cardLastFour; }
public String getCardType() { return cardType; }
public void setCardType(String cardType) { this.cardType = cardType; }
public String getPaypalEmail() { return paypalEmail; }
public void setPaypalEmail(String paypalEmail) { this.paypalEmail = paypalEmail; }
}
public static class PaymentResult {
private final String transactionId;
private final String status;
private final String message;
public PaymentResult(String transactionId, String status, String message) {
this.transactionId = transactionId;
this.status = status;
this.message = message;
}
// getters
public String getTransactionId() { return transactionId; }
public String getStatus() { return status; }
public String getMessage() { return message; }
}
}
Metrics Collection
Example 5: Custom Metrics
package com.example.otel.metrics;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.metrics.DoubleHistogram;
import io.opentelemetry.api.metrics.LongCounter;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.api.common.Attributes;
import org.springframework.stereotype.Component;
@Component
public class OrderMetrics {
private final LongCounter orderCounter;
private final LongCounter errorCounter;
private final DoubleHistogram orderProcessingTime;
private final LongCounter inventoryCheckCounter;
public OrderMetrics(OpenTelemetry openTelemetry) {
Meter meter = openTelemetry.getMeter("order.service");
// Counter for tracking orders
this.orderCounter = meter.counterBuilder("orders.total")
.setDescription("Total number of orders")
.setUnit("1")
.build();
// Counter for tracking errors
this.errorCounter = meter.counterBuilder("orders.errors")
.setDescription("Total number of order errors")
.setUnit("1")
.build();
// Histogram for tracking processing time
this.orderProcessingTime = meter.histogramBuilder("order.processing.time")
.setDescription("Order processing time in milliseconds")
.setUnit("ms")
.build();
// Counter for inventory checks
this.inventoryCheckCounter = meter.counterBuilder("inventory.checks")
.setDescription("Number of inventory checks")
.setUnit("1")
.build();
}
public void recordOrder(String productCategory, String status) {
Attributes attributes = Attributes.builder()
.put("product.category", productCategory)
.put("order.status", status)
.build();
orderCounter.add(1, attributes);
}
public void recordError(String errorType, String operation) {
Attributes attributes = Attributes.builder()
.put("error.type", errorType)
.put("operation", operation)
.build();
errorCounter.add(1, attributes);
}
public void recordProcessingTime(double processingTimeMs, String orderType) {
Attributes attributes = Attributes.builder()
.put("order.type", orderType)
.build();
orderProcessingTime.record(processingTimeMs, attributes);
}
public void recordInventoryCheck(String productId, boolean available) {
Attributes attributes = Attributes.builder()
.put("product.id", productId)
.put("inventory.available", available)
.build();
inventoryCheckCounter.add(1, attributes);
}
}
Example 6: Using Metrics in Service
package com.example.otel.service;
import com.example.otel.metrics.OrderMetrics;
import org.springframework.stereotype.Service;
@Service
public class InstrumentedOrderService {
private final OrderService orderService;
private final OrderMetrics metrics;
public InstrumentedOrderService(OrderService orderService, OrderMetrics metrics) {
this.orderService = orderService;
this.metrics = metrics;
}
public String createOrderWithMetrics(String productId, int quantity, String productCategory) {
long startTime = System.currentTimeMillis();
try {
String orderId = orderService.createOrder(productId, quantity);
// Record successful order
metrics.recordOrder(productCategory, "success");
metrics.recordProcessingTime(System.currentTimeMillis() - startTime, "standard");
return orderId;
} catch (Exception e) {
// Record error
metrics.recordError(e.getClass().getSimpleName(), "createOrder");
metrics.recordOrder(productCategory, "failed");
throw e;
}
}
}
Distributed Tracing
Example 7: HTTP Client Instrumentation
package com.example.otel.client;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.context.propagation.TextMapSetter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import java.util.Map;
@Component
public class InstrumentedHttpClient {
private final WebClient webClient;
private final Tracer tracer;
private final TextMapPropagator propagator;
// Setter for injecting headers
private final TextMapSetter<HttpHeaders> setter = (headers, key, value) -> {
if (headers != null) {
headers.add(key, value);
}
};
public InstrumentedHttpClient(OpenTelemetry openTelemetry, WebClient.Builder webClientBuilder) {
this.tracer = openTelemetry.getTracer(InstrumentedHttpClient.class.getName());
this.propagator = openTelemetry.getPropagators().getTextMapPropagator();
this.webClient = webClientBuilder.build();
}
public <T> T get(String url, Class<T> responseType, Map<String, String> headers) {
Span span = tracer.spanBuilder("http.get")
.setAttribute("http.url", url)
.setAttribute("http.method", "GET")
.startSpan();
try (var scope = span.makeCurrent()) {
// Inject tracing headers
HttpHeaders httpHeaders = new HttpHeaders();
headers.forEach(httpHeaders::add);
propagator.inject(Context.current(), httpHeaders, setter);
// Make HTTP request
T response = webClient.method(HttpMethod.GET)
.uri(url)
.headers(h -> h.addAll(httpHeaders))
.retrieve()
.bodyToMono(responseType)
.block();
span.setAttribute("http.status_code", 200);
return response;
} catch (Exception e) {
span.recordException(e);
span.setAttribute("http.status_code", 500);
throw e;
} finally {
span.end();
}
}
public <T> T post(String url, Object body, Class<T> responseType, Map<String, String> headers) {
Span span = tracer.spanBuilder("http.post")
.setAttribute("http.url", url)
.setAttribute("http.method", "POST")
.startSpan();
try (var scope = span.makeCurrent()) {
// Inject tracing headers
HttpHeaders httpHeaders = new HttpHeaders();
headers.forEach(httpHeaders::add);
propagator.inject(Context.current(), httpHeaders, setter);
// Make HTTP request
T response = webClient.method(HttpMethod.POST)
.uri(url)
.headers(h -> h.addAll(httpHeaders))
.bodyValue(body)
.retrieve()
.bodyToMono(responseType)
.block();
span.setAttribute("http.status_code", 200);
return response;
} catch (Exception e) {
span.recordException(e);
span.setAttribute("http.status_code", 500);
throw e;
} finally {
span.end();
}
}
}
Spring Boot Integration
Example 8: Spring Boot Controller with OpenTelemetry
package com.example.otel.controller;
import com.example.otel.service.PaymentService;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api")
public class OrderController {
private final Tracer tracer;
private final PaymentService paymentService;
public OrderController(Tracer tracer, PaymentService paymentService) {
this.tracer = tracer;
this.paymentService = paymentService;
}
@PostMapping("/orders")
public ResponseEntity<?> createOrder(@RequestBody CreateOrderRequest request) {
Span span = tracer.spanBuilder("OrderController.createOrder")
.setAttribute("user.id", request.getUserId())
.setAttribute("order.items.count", request.getItems().size())
.startSpan();
try (var scope = span.makeCurrent()) {
span.addEvent("Order creation request received");
// Process payment
var paymentResult = paymentService.processPayment(
new PaymentService.PaymentRequest(
request.getPaymentMethod(),
request.getTotalAmount(),
request.getCurrency(),
request.getUserId()
)
);
if (!"SUCCESS".equals(paymentResult.getStatus())) {
span.addEvent("Payment failed");
return ResponseEntity.badRequest()
.body(Map.of("error", paymentResult.getMessage()));
}
// Create order record
String orderId = generateOrderId();
span.setAttribute("order.id", orderId);
span.addEvent("Order created successfully");
return ResponseEntity.ok(Map.of(
"orderId", orderId,
"transactionId", paymentResult.getTransactionId(),
"status", "created"
));
} catch (Exception e) {
span.recordException(e);
span.setAttribute("error", true);
return ResponseEntity.internalServerError()
.body(Map.of("error", "Order creation failed"));
} finally {
span.end();
}
}
@GetMapping("/orders/{orderId}")
public ResponseEntity<?> getOrder(@PathVariable String orderId) {
Span span = tracer.spanBuilder("OrderController.getOrder")
.setAttribute("order.id", orderId)
.startSpan();
try (var scope = span.makeCurrent()) {
// Simulate database lookup
Thread.sleep(50);
var order = Map.of(
"orderId", orderId,
"status", "completed",
"amount", 99.99
);
return ResponseEntity.ok(order);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
span.recordException(e);
return ResponseEntity.internalServerError()
.body(Map.of("error", "Request interrupted"));
} catch (Exception e) {
span.recordException(e);
return ResponseEntity.internalServerError()
.body(Map.of("error", e.getMessage()));
} finally {
span.end();
}
}
private String generateOrderId() {
return "ORD-" + System.currentTimeMillis();
}
// Request DTO
public static class CreateOrderRequest {
private String userId;
private String paymentMethod;
private long totalAmount;
private String currency;
private java.util.List<OrderItem> items;
// constructors, getters, setters
public CreateOrderRequest() {}
// getters and setters
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getPaymentMethod() { return paymentMethod; }
public void setPaymentMethod(String paymentMethod) { this.paymentMethod = paymentMethod; }
public long getTotalAmount() { return totalAmount; }
public void setTotalAmount(long totalAmount) { this.totalAmount = totalAmount; }
public String getCurrency() { return currency; }
public void setCurrency(String currency) { this.currency = currency; }
public java.util.List<OrderItem> getItems() { return items; }
public void setItems(java.util.List<OrderItem> items) { this.items = items; }
}
public static class OrderItem {
private String productId;
private int quantity;
private long price;
// getters and setters
public String getProductId() { return productId; }
public void setProductId(String productId) { this.productId = productId; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
public long getPrice() { return price; }
public void setPrice(long price) { this.price = price; }
}
}
Testing OpenTelemetry
Example 9: Unit Testing with OpenTelemetry
package com.example.otel.test;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension;
import io.opentelemetry.sdk.trace.data.SpanData;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
class OrderServiceTest {
@RegisterExtension
static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create();
@Autowired
private OrderService orderService;
@Test
void shouldCreateSpanWhenCreatingOrder() {
// When
String orderId = orderService.createOrder("prod-123", 2);
// Then
List<SpanData> spans = otelTesting.getSpans();
assertThat(spans).hasSize(4); // createOrder + validateProduct + checkInventory + processPayment
SpanData createOrderSpan = spans.get(0);
assertThat(createOrderSpan.getName()).isEqualTo("createOrder");
assertThat(createOrderSpan.getAttributes().get(AttributeKey.stringKey("product.id")))
.isEqualTo("prod-123");
assertThat(createOrderSpan.getAttributes().get(AttributeKey.longKey("order.quantity")))
.isEqualTo(2L);
}
}
Configuration with application.yml
Example 10: Spring Boot Configuration
# application.yml management: tracing: sampling: probability: 1.0 # 100% sampling rate for development endpoints: web: exposure: include: health,info,metrics,prometheus metrics: export: prometheus: enabled: true opentelemetry: service: name: order-service exporter: jaeger: endpoint: http://localhost:14250 otlp: endpoint: http://localhost:4317 instrumentation: micrometer: enabled: true spring: application: name: order-service sleuth: otel: config: trace-id-ratio-based: 1.0 logging: level: io.opentelemetry: DEBUG
Best Practices
1. Span Naming Conventions
public class SpanNamingBestPractices {
// Good span names
public void goodSpanNames() {
// Use lowercase and dots for hierarchy
tracer.spanBuilder("http.request");
tracer.spanBuilder("database.query");
tracer.spanBuilder("service.operation");
tracer.spanBuilder("order.create");
tracer.spanBuilder("payment.process");
}
// Bad span names
public void badSpanNames() {
// Avoid these
tracer.spanBuilder("Create_Order"); // uppercase and underscore
tracer.spanBuilder("createOrder"); // camelCase
tracer.spanBuilder("order"); // too generic
}
}
2. Attribute Best Practices
public class AttributeBestPractices {
public void goodAttributes(Span span, Order order) {
// Use semantic conventions when possible
span.setAttribute("http.method", "POST");
span.setAttribute("http.route", "/api/orders");
span.setAttribute("db.system", "postgresql");
span.setAttribute("db.operation", "INSERT");
// Use meaningful business attributes
span.setAttribute("order.id", order.getId());
span.setAttribute("order.total_amount", order.getTotalAmount());
span.setAttribute("user.id", order.getUserId());
}
public void avoidBadAttributes(Span span) {
// Don't include sensitive information
// span.setAttribute("user.password", "secret"); // BAD!
// span.setAttribute("credit_card.number", "1234-5678-9012-3456"); // BAD!
// Don't include high-cardinality data in attributes
// span.setAttribute("user.email", "[email protected]"); // Use user.id instead
}
}
Summary
OpenTelemetry provides comprehensive observability for Java applications:
- Tracing: Track requests across distributed systems
- Metrics: Collect quantitative performance data
- Logs: Correlate logs with traces and metrics
- Vendor Neutral: Export to multiple backends
Key benefits:
- Standardized observability framework
- Rich instrumentation capabilities
- Excellent Spring Boot integration
- Production-ready with minimal overhead
- Future-proof (replacing OpenTracing/OpenCensus)
By implementing OpenTelemetry, you gain deep insights into your application's performance and behavior, enabling faster debugging and better operational awareness.