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.