OpenTelemetry Auto-Instrumentation in Java: Complete Guide

Introduction to OpenTelemetry Auto-Instrumentation

OpenTelemetry provides automatic instrumentation for Java applications without requiring code changes. This guide covers comprehensive setup, configuration, and customization of OpenTelemetry auto-instrumentation for various Java frameworks and libraries.

Table of Contents

  1. OpenTelemetry Fundamentals
  2. Auto-Instrumentation Setup
  3. Spring Boot Integration
  4. Database Instrumentation
  5. HTTP Client/Server Instrumentation
  6. Messaging System Instrumentation
  7. Custom Instrumentation
  8. Configuration and Exporters
  9. Performance Best Practices
  10. Real-World Examples

1. Project Setup and Dependencies

Maven Configuration

<properties> <opentelemetry.version>1.28.0</opentelemetry.version> <opentelemetry-instrumentation.version>1.28.0</opentelemetry-instrumentation.version> <spring-boot.version>3.1.0</spring-boot.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 Auto-Instrumentation Annotations --> <dependency> <groupId>io.opentelemetry.instrumentation</groupId> <artifactId>opentelemetry-instrumentation-annotations</artifactId> <version>${opentelemetry-instrumentation.version}</version> </dependency> <!-- OpenTelemetry Exporters --> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-exporter-otlp</artifactId> <version>${opentelemetry.version}</version> </dependency> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-exporter-logging</artifactId> <version>${opentelemetry.version}</version> </dependency> <!-- Spring Boot Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Database --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <!-- HTTP Client --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Messaging --> <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> <version>3.0.8</version> </dependency> <!-- Testing --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>${spring-boot.version}</version> <scope>test</scope> </dependency> </dependencies>

Using OpenTelemetry Java Agent (Recommended)

Download the latest Java agent JAR:

wget https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar

Run your application with the agent:

java -javaagent:path/to/opentelemetry-javaagent.jar \ -Dotel.service.name=my-service \ -Dotel.traces.exporter=otlp \ -Dotel.metrics.exporter=otlp \ -Dotel.logs.exporter=otlp \ -Dotel.exporter.otlp.endpoint=http://localhost:4317 \ -jar your-application.jar

2. Configuration Files

application.yml

# OpenTelemetry Configuration otel: service: name: order-service namespace: ecommerce version: 1.0.0 instance: id: ${HOSTNAME:localhost} # Exporters exporter: otlp: endpoint: http://localhost:4317 protocol: grpc headers: authorization: "Bearer ${OTEL_API_KEY:}" timeout: 10000 logging: enabled: true # Sampling traces: sampler: parentbased_traceidratio sampler: arg: 0.1 # 10% sampling rate # Resource attributes resource: attributes: deployment: environment: ${DEPLOYMENT_ENV:development} telemetry: sdk: language: java name: opentelemetry host: name: ${HOSTNAME:unknown} os: type: ${os.name:unknown} version: ${os.version:unknown} # Spring Boot Actuator management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: always metrics: enabled: true tracing: sampling: probability: 1.0 # 100% for development # Logging correlation logging: pattern: level: "%5p [${spring.application.name:},%X{trace_id:-},%X{span_id:-}]"

Environment Variables

# Service Identification export OTEL_SERVICE_NAME=order-service export OTEL_SERVICE_NAMESPACE=ecommerce export OTEL_SERVICE_VERSION=1.0.0 # Exporters export OTEL_TRACES_EXPORTER=otlp export OTEL_METRICS_EXPORTER=otlp export OTEL_LOGS_EXPORTER=otlp export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 export OTEL_EXPORTER_OTLP_HEADERS="authorization=Bearer your-token" # Sampling export OTEL_TRACES_SAMPLER=parentbased_traceidratio export OTEL_TRACES_SAMPLER_ARG=0.1 # Resource Attributes export OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,host.name=order-service-1 # Java Agent Specific export OTEL_JAVAAGENT_ENABLED=true export OTEL_INSTRUMENTATION_COMMON_DEFAULT_ENABLED=true export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=content-type,content-length,user-agent export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=content-type,content-length

3. Spring Boot Auto-Configuration

OpenTelemetry Configuration Class

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.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.sdk.trace.export.SimpleSpanProcessor; import io.opentelemetry.semconv.ResourceAttributes; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.concurrent.TimeUnit; @Configuration public class OpenTelemetryConfig { @Value("${otel.service.name:unknown-service}") private String serviceName; @Value("${otel.service.namespace:unknown-namespace}") private String serviceNamespace; @Value("${otel.exporter.otlp.endpoint:http://localhost:4317}") private String otlpEndpoint; @Bean public OpenTelemetry openTelemetry() { // Create resource Resource resource = Resource.getDefault() .merge(Resource.create(Attributes.builder() .put(ResourceAttributes.SERVICE_NAME, serviceName) .put(ResourceAttributes.SERVICE_NAMESPACE, serviceNamespace) .put(ResourceAttributes.DEPLOYMENT_ENVIRONMENT, getEnvironment()) .put(ResourceAttributes.HOST_NAME, getHostName()) .build())); // Configure span exporter OtlpGrpcSpanExporter spanExporter = OtlpGrpcSpanExporter.builder() .setEndpoint(otlpEndpoint) .setTimeout(30, TimeUnit.SECONDS) .build(); // Configure tracer provider SdkTracerProvider tracerProvider = SdkTracerProvider.builder() .setResource(resource) .addSpanProcessor(BatchSpanProcessor.builder(spanExporter) .setScheduleDelay(100, TimeUnit.MILLISECONDS) .setMaxExportBatchSize(512) .setMaxQueueSize(2048) .build()) // Add logging exporter for development .addSpanProcessor(SimpleSpanProcessor.create(LoggingSpanExporter.create())) .build(); // Build OpenTelemetry instance return OpenTelemetrySdk.builder() .setTracerProvider(tracerProvider) .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) .buildAndRegisterGlobal(); } private String getEnvironment() { return System.getenv().getOrDefault("DEPLOYMENT_ENV", "development"); } private String getHostName() { return System.getenv().getOrDefault("HOSTNAME", "unknown"); } }

Spring Boot Actuator Configuration

package com.example.otel.config; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.config.MeterFilter; import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MetricsConfig { @Bean public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() { return registry -> registry.config() .commonTags("application", "order-service", "environment", System.getenv().getOrDefault("ENV", "dev"), "instance", System.getenv().getOrDefault("HOSTNAME", "unknown")) .meterFilter(MeterFilter.ignoreTags("too.many.tags")) .meterFilter(MeterFilter.denyNameStartsWith("jvm.threads")); } }

4. Auto-Instrumented Components

HTTP Server Instrumentation (Spring MVC)

package com.example.otel.controller; import io.opentelemetry.instrumentation.annotations.WithSpan; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; @RestController @RequestMapping("/api/orders") public class OrderController { private static final Logger logger = LoggerFactory.getLogger(OrderController.class); private final OrderService orderService; private final PaymentService paymentService; public OrderController(OrderService orderService, PaymentService paymentService) { this.orderService = orderService; this.paymentService = paymentService; } // Auto-instrumented HTTP endpoint @PostMapping public ResponseEntity<OrderResponse> createOrder(@RequestBody CreateOrderRequest request) { logger.info("Creating order for customer: {}", request.customerId()); // This will be automatically traced Order order = orderService.createOrder(request); // Payment processing with tracing PaymentResult paymentResult = paymentService.processPayment(order); return ResponseEntity.ok(new OrderResponse( order.getId(), order.getStatus(), paymentResult.transactionId() )); } // Auto-instrumented with query parameters @GetMapping public ResponseEntity<List<Order>> getOrders( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size, @RequestParam(required = false) String status) { logger.debug("Fetching orders - page: {}, size: {}, status: {}", page, size, status); List<Order> orders = orderService.findOrders(page, size, status); return ResponseEntity.ok(orders); } // Path variables are automatically captured @GetMapping("/{orderId}") public ResponseEntity<Order> getOrder(@PathVariable String orderId) { logger.info("Fetching order: {}", orderId); Order order = orderService.findOrderById(orderId); if (order == null) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(order); } // Async endpoint with automatic tracing @GetMapping("/{orderId}/status") public CompletableFuture<ResponseEntity<OrderStatus>> getOrderStatus(@PathVariable String orderId) { return orderService.getOrderStatusAsync(orderId) .thenApply(ResponseEntity::ok) .exceptionally(throwable -> { logger.error("Failed to get order status: {}", orderId, throwable); return ResponseEntity.status(500).build(); }); } // Custom span with attributes @WithSpan("cancel-order") @DeleteMapping("/{orderId}") public ResponseEntity<Void> cancelOrder(@PathVariable String orderId, @RequestParam String reason) { logger.warn("Cancelling order {} with reason: {}", orderId, reason); // Add custom attributes to span io.opentelemetry.api.trace.Span.current() .setAttribute("order.id", orderId) .setAttribute("cancellation.reason", reason); boolean cancelled = orderService.cancelOrder(orderId, reason); if (cancelled) { return ResponseEntity.noContent().build(); } else { return ResponseEntity.notFound().build(); } } // Record DTOs public record CreateOrderRequest(String customerId, List<OrderItem> items, String shippingAddress) {} public record OrderResponse(String orderId, String status, String transactionId) {} public record OrderStatus(String orderId, String status, String lastUpdated) {} }

Database Instrumentation (JPA/Hibernate)

package com.example.otel.repository; import io.opentelemetry.instrumentation.annotations.WithSpan; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; @Repository public interface OrderRepository extends JpaRepository<Order, String> { // Auto-instrumented JPA methods List<Order> findByCustomerId(String customerId); List<Order> findByStatus(String status); // Custom query with automatic instrumentation @Query("SELECT o FROM Order o WHERE o.customerId = :customerId AND o.status = :status") List<Order> findOrdersByCustomerAndStatus(@Param("customerId") String customerId, @Param("status") String status); // Native query instrumentation @Query(value = "SELECT * FROM orders WHERE total_amount > :minAmount", nativeQuery = true) List<Order> findOrdersWithTotalGreaterThan(@Param("minAmount") Double minAmount); } @Service @Transactional public class OrderService { private static final Logger logger = LoggerFactory.getLogger(OrderService.class); private final OrderRepository orderRepository; private final InventoryService inventoryService; public OrderService(OrderRepository orderRepository, InventoryService inventoryService) { this.orderRepository = orderRepository; this.inventoryService = inventoryService; } // Auto-instrumented database operations @WithSpan("create-order") public Order createOrder(CreateOrderRequest request) { logger.info("Creating order for customer: {}", request.customerId()); // Start transaction (automatically traced) Order order = new Order(); order.setCustomerId(request.customerId()); order.setShippingAddress(request.shippingAddress()); order.setStatus("PENDING"); // Process order items for (OrderItem item : request.items()) { // Check inventory with automatic tracing boolean available = inventoryService.checkAvailability(item.productId(), item.quantity()); if (!available) { throw new InventoryException("Product not available: " + item.productId()); } OrderItem orderItem = new OrderItem(); orderItem.setProductId(item.productId()); orderItem.setQuantity(item.quantity()); orderItem.setUnitPrice(item.unitPrice()); order.addItem(orderItem); } // Calculate total order.calculateTotal(); // Save order (automatically traced JPA operation) Order savedOrder = orderRepository.save(order); logger.info("Order created successfully: {}", savedOrder.getId()); return savedOrder; } @WithSpan("find-order-by-id") public Order findOrderById(String orderId) { // Auto-instrumented database call return orderRepository.findById(orderId) .orElseThrow(() -> new OrderNotFoundException("Order not found: " + orderId)); } @WithSpan("find-orders") public List<Order> findOrders(int page, int size, String status) { // Auto-instrumented database call with pagination Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending()); if (status != null) { return orderRepository.findByStatus(status, pageable); } else { return orderRepository.findAll(pageable).getContent(); } } @WithSpan("cancel-order") public boolean cancelOrder(String orderId, String reason) { Optional<Order> orderOpt = orderRepository.findById(orderId); if (orderOpt.isPresent()) { Order order = orderOpt.get(); order.setStatus("CANCELLED"); order.setCancellationReason(reason); orderRepository.save(order); // Restore inventory for (OrderItem item : order.getItems()) { inventoryService.restoreInventory(item.getProductId(), item.getQuantity()); } return true; } return false; } // Async method with tracing public CompletableFuture<OrderStatus> getOrderStatusAsync(String orderId) { return CompletableFuture.supplyAsync(() -> { Order order = findOrderById(orderId); return new OrderStatus(order.getId(), order.getStatus(), order.getUpdatedAt().toString()); }); } }

HTTP Client Instrumentation

package com.example.otel.service; import io.opentelemetry.instrumentation.annotations.WithSpan; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Mono; import java.time.Duration; @Service public class PaymentService { private static final Logger logger = LoggerFactory.getLogger(PaymentService.class); private final WebClient webClient; public PaymentService(WebClient.Builder webClientBuilder) { this.webClient = webClientBuilder .baseUrl("http://payment-service:8080") .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build(); } // Auto-instrumented HTTP client call @WithSpan("process-payment") public PaymentResult processPayment(Order order) { logger.info("Processing payment for order: {}", order.getId()); PaymentRequest request = new PaymentRequest( order.getId(), order.getCustomerId(), order.getTotalAmount(), "USD" ); try { // Auto-instrumented HTTP call with trace context propagation PaymentResponse response = webClient.post() .uri("/api/payments/process") .bodyValue(request) .retrieve() .bodyToMono(PaymentResponse.class) .timeout(Duration.ofSeconds(30)) .block(); // In production, use reactive programming properly logger.info("Payment processed successfully: {}", response.transactionId()); return new PaymentResult(true, response.transactionId(), "Payment successful"); } catch (WebClientResponseException e) { logger.error("Payment failed with status {}: {}", e.getStatusCode(), e.getResponseBodyAsString()); return new PaymentResult(false, null, "Payment failed: " + e.getMessage()); } catch (Exception e) { logger.error("Payment processing error", e); return new PaymentResult(false, null, "Payment error: " + e.getMessage()); } } // Reactive HTTP client with automatic tracing @WithSpan("validate-payment-method") public Mono<ValidationResult> validatePaymentMethod(String customerId, String paymentMethodId) { return webClient.get() .uri("/api/payments/customers/{customerId}/methods/{paymentMethodId}/validate", customerId, paymentMethodId) .retrieve() .bodyToMono(ValidationResult.class) .timeout(Duration.ofSeconds(10)) .doOnSuccess(result -> logger.debug("Payment method validated: {}", result.valid())) .doOnError(error -> logger.error("Payment method validation failed", error)); } // Record DTOs public record PaymentRequest(String orderId, String customerId, Double amount, String currency) {} public record PaymentResponse(String transactionId, String status, String processedAt) {} public record PaymentResult(boolean success, String transactionId, String message) {} public record ValidationResult(boolean valid, String reason) {} }

Messaging System Instrumentation (Kafka)

package com.example.otel.messaging; import io.opentelemetry.instrumentation.annotations.WithSpan; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.kafka.annotation.KafkaListener; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.stereotype.Component; @Component public class OrderEventsConsumer { private static final Logger logger = LoggerFactory.getLogger(OrderEventsConsumer.class); private final OrderService orderService; private final KafkaTemplate<String, Object> kafkaTemplate; public OrderEventsConsumer(OrderService orderService, KafkaTemplate<String, Object> kafkaTemplate) { this.orderService = orderService; this.kafkaTemplate = kafkaTemplate; } // Auto-instrumented Kafka consumer @KafkaListener(topics = "order-created", groupId = "order-service") public void handleOrderCreated(@Payload OrderCreatedEvent event, ConsumerRecord<String, Object> record) { logger.info("Processing order created event: {}", event.orderId()); try { // Process the event (automatically traced) orderService.processOrderCreation(event); // Send to next topic sendOrderProcessedEvent(event.orderId()); } catch (Exception e) { logger.error("Failed to process order created event: {}", event.orderId(), e); // Send to DLQ sendToDlq(event, e.getMessage()); } } @KafkaListener(topics = "order-cancelled", groupId = "order-service") public void handleOrderCancelled(@Payload OrderCancelledEvent event) { logger.info("Processing order cancelled event: {}", event.orderId()); // Auto-instrumented processing orderService.processOrderCancellation(event); } @WithSpan("send-order-processed-event") private void sendOrderProcessedEvent(String orderId) { OrderProcessedEvent event = new OrderProcessedEvent(orderId, "PROCESSED", System.currentTimeMillis()); // Auto-instrumented Kafka producer kafkaTemplate.send("order-processed", orderId, event) .addCallback( result -> logger.debug("Order processed event sent: {}", orderId), failure -> logger.error("Failed to send order processed event: {}", orderId, failure) ); } @WithSpan("send-to-dlq") private void sendToDlq(Object event, String error) { DlqMessage dlqMessage = new DlqMessage("order-created", event, error, System.currentTimeMillis()); kafkaTemplate.send("order-events-dlq", dlqMessage) .addCallback( result -> logger.warn("Message sent to DLQ"), failure -> logger.error("Failed to send message to DLQ", failure) ); } // Event DTOs public record OrderCreatedEvent(String orderId, String customerId, Double totalAmount, Long timestamp) {} public record OrderCancelledEvent(String orderId, String reason, Long timestamp) {} public record OrderProcessedEvent(String orderId, String status, Long processedAt) {} public record DlqMessage(String originalTopic, Object payload, String error, Long timestamp) {} }

5. Custom Instrumentation

Custom Span Attributes and Events

package com.example.otel.instrumentation; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; @Aspect @Component public class BusinessMetricsAspect { private static final Logger logger = LoggerFactory.getLogger(BusinessMetricsAspect.class); @Around("@annotation(TrackBusinessMetric)") public Object trackBusinessMetric(ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().getName(); String className = joinPoint.getTarget().getClass().getSimpleName(); Span span = Span.current(); // Add custom attributes span.setAttribute("business.operation", methodName) .setAttribute("business.component", className) .setAttribute("business.invocation.time", System.currentTimeMillis()); // Add method arguments as attributes (be careful with sensitive data) Object[] args = joinPoint.getArgs(); for (int i = 0; i < args.length; i++) { if (args[i] != null && !isSensitiveArgument(methodName, i)) { span.setAttribute("business.arg." + i, args[i].toString()); } } // Record start event span.addEvent("business.operation.started", System.currentTimeMillis(), Map.of("method", methodName, "class", className)); try { Object result = joinPoint.proceed(); // Record success event span.addEvent("business.operation.completed", System.currentTimeMillis(), Map.of("status", "success")); // Add result information if applicable if (result != null) { span.setAttribute("business.result.type", result.getClass().getSimpleName()); } return result; } catch (Exception e) { // Record error event span.setStatus(StatusCode.ERROR, "Business operation failed") .recordException(e); span.addEvent("business.operation.failed", System.currentTimeMillis(), Map.of("error", e.getMessage(), "exception.type", e.getClass().getSimpleName())); throw e; } } private boolean isSensitiveArgument(String methodName, int argIndex) { // Define which arguments might contain sensitive data Map<String, int[]> sensitiveMethods = new HashMap<>(); sensitiveMethods.put("processPayment", new int[]{0}); // Payment details sensitiveMethods.put("validateUser", new int[]{1}); // Password int[] sensitiveArgs = sensitiveMethods.get(methodName); if (sensitiveArgs != null) { for (int sensitiveArg : sensitiveArgs) { if (sensitiveArg == argIndex) { return true; } } } return false; } } // Custom annotation for business metrics @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface TrackBusinessMetric { String value() default ""; boolean trackArguments() default true; boolean trackResult() default false; }

Database Query Instrumentation

package com.example.otel.instrumentation; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.StatusCode; import org.hibernate.resource.jdbc.spi.StatementInspector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @Component public class HibernateStatementInspector implements StatementInspector { private static final Logger logger = LoggerFactory.getLogger(HibernateStatementInspector.class); @Override public String inspect(String sql) { Span span = Span.current(); if (span.getSpanContext().isValid()) { // Add SQL information to span span.setAttribute("db.statement", sql); span.setAttribute("db.operation", extractOperation(sql)); // Add query complexity metrics int queryComplexity = estimateQueryComplexity(sql); span.setAttribute("db.query.complexity", queryComplexity); logger.debug("Executing SQL: {}", sql); } return sql; // Return original SQL unchanged } private String extractOperation(String sql) { if (sql == null) return "unknown"; String upperSql = sql.trim().toUpperCase(); if (upperSql.startsWith("SELECT")) return "SELECT"; if (upperSql.startsWith("INSERT")) return "INSERT"; if (upperSql.startsWith("UPDATE")) return "UPDATE"; if (upperSql.startsWith("DELETE")) return "DELETE"; return "OTHER"; } private int estimateQueryComplexity(String sql) { if (sql == null) return 0; int complexity = 0; String upperSql = sql.toUpperCase(); // Simple heuristics for query complexity if (upperSql.contains(" JOIN ")) complexity += 2; if (upperSql.contains(" WHERE ")) complexity += 1; if (upperSql.contains(" GROUP BY ")) complexity += 1; if (upperSql.contains(" ORDER BY ")) complexity += 1; if (upperSql.contains(" UNION ")) complexity += 3; if (upperSql.contains(" SUBQUERY")) complexity += 2; return complexity; } }

6. Configuration Management

OpenTelemetry Configuration Service

package com.example.otel.config; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.trace.Tracer; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class OpenTelemetryBeanConfig { @Value("${otel.service.name:unknown-service}") private String serviceName; @Bean public Tracer tracer() { return GlobalOpenTelemetry.getTracer(serviceName); } @Bean public Meter meter() { return GlobalOpenTelemetry.getMeter(serviceName); } } @Service public class OpenTelemetryConfigService { private static final Logger logger = LoggerFactory.getLogger(OpenTelemetryConfigService.class); private final Tracer tracer; private final Meter meter; public OpenTelemetryConfigService(Tracer tracer, Meter meter) { this.tracer = tracer; this.meter = meter; initializeMetrics(); } private void initializeMetrics() { // Define business metrics LongCounter orderCounter = meter.counterBuilder("orders.created") .setDescription("Total number of orders created") .setUnit("1") .build(); Histogram orderAmountHistogram = meter.histogramBuilder("orders.amount") .setDescription("Order amount distribution") .setUnit("USD") .build(); LongCounter errorCounter = meter.counterBuilder("errors.total") .setDescription("Total number of errors") .setUnit("1") .build(); logger.info("OpenTelemetry metrics initialized"); } public void recordOrderCreated(double amount, String customerType) { // Get or create counter with labels LongCounter orderCounter = meter.counterBuilder("orders.created") .build(); orderCounter.add(1, Attributes.of( AttributeKey.stringKey("customer.type"), customerType, AttributeKey.stringKey("amount.range"), getAmountRange(amount) )); // Record amount distribution Histogram amountHistogram = meter.histogramBuilder("orders.amount") .build(); amountHistogram.record(amount, Attributes.of(AttributeKey.stringKey("customer.type"), customerType)); } public void recordError(String errorType, String component) { LongCounter errorCounter = meter.counterBuilder("errors.total") .build(); errorCounter.add(1, Attributes.of( AttributeKey.stringKey("error.type"), errorType, AttributeKey.stringKey("component"), component )); } private String getAmountRange(double amount) { if (amount < 50) return "0-50"; if (amount < 100) return "50-100"; if (amount < 200) return "100-200"; return "200+"; } }

7. Performance Monitoring

Performance Monitoring Aspect

package com.example.otel.monitoring; import io.opentelemetry.api.metrics.LongHistogram; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.StatusCode; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; @Aspect @Component public class PerformanceMonitoringAspect { private static final Logger logger = LoggerFactory.getLogger(PerformanceMonitoringAspect.class); private final ConcurrentHashMap<String, LongHistogram> methodHistograms = new ConcurrentHashMap<>(); @Around("execution(* com.example.otel.service.*.*(..)) || " + "execution(* com.example.otel.repository.*.*(..)) || " + "execution(* com.example.otel.controller.*.*(..))") public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().toShortString(); String className = joinPoint.getTarget().getClass().getSimpleName(); long startTime = System.nanoTime(); boolean success = true; try { Object result = joinPoint.proceed(); return result; } catch (Exception e) { success = false; Span.current().setStatus(StatusCode.ERROR, e.getMessage()); throw e; } finally { long duration = System.nanoTime() - startTime; long durationMs = TimeUnit.NANOSECONDS.toMillis(duration); // Record method execution time recordMethodExecution(methodName, className, durationMs, success); // Log slow method executions if (durationMs > 1000) { // 1 second threshold logger.warn("Slow method execution: {} took {}ms", methodName, durationMs); } } } private void recordMethodExecution(String methodName, String className, long durationMs, boolean success) { String histogramName = "method.execution.time"; LongHistogram histogram = methodHistograms.computeIfAbsent(histogramName, name -> io.opentelemetry.api.GlobalOpenTelemetry.getMeter("performance") .histogramBuilder(name) .setDescription("Method execution time distribution") .setUnit("ms") .ofLongs() .build()); histogram.record(durationMs, io.opentelemetry.api.common.Attributes.of( io.opentelemetry.api.common.AttributeKey.stringKey("method"), methodName, io.opentelemetry.api.common.AttributeKey.stringKey("class"), className, io.opentelemetry.api.common.AttributeKey.booleanKey("success"), success )); } }

8. Log Correlation

MDC Log Correlation

package com.example.otel.logging; import org.slf4j.MDC; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component @Order(1) public class LogCorrelationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // Extract trace context from headers String traceId = extractTraceId(request); String spanId = extractSpanId(request); // Add to MDC for log correlation if (traceId != null) { MDC.put("trace_id", traceId); } if (spanId != null) { MDC.put("span_id", spanId); } try { filterChain.doFilter(request, response); } finally { // Clear MDC MDC.clear(); } } private String extractTraceId(HttpServletRequest request) { // Extract from W3C Trace Context header String traceParent = request.getHeader("traceparent"); if (traceParent != null && traceParent.length() > 55) { return traceParent.substring(3, 35); // Extract trace ID } // Extract from other headers if needed return request.getHeader("X-B3-TraceId"); } private String extractSpanId(HttpServletRequest request) { // Extract from W3C Trace Context header String traceParent = request.getHeader("traceparent"); if (traceParent != null && traceParent.length() > 55) { return traceParent.substring(36, 52); // Extract span ID } // Extract from other headers if needed return request.getHeader("X-B3-SpanId"); } }

9. Testing and Validation

Integration Tests

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.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.http.ResponseEntity; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class OpenTelemetryIntegrationTest { @RegisterExtension static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); private final TestRestTemplate restTemplate = new TestRestTemplate(); @Test void testHttpRequestCreatesSpan() { // Given String baseUrl = "http://localhost:" + port; // When ResponseEntity<String> response = restTemplate.getForEntity( baseUrl + "/api/orders/123", String.class); // Then assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); // Verify spans were created List<SpanData> spans = otelTesting.getSpans(); assertThat(spans).isNotEmpty(); // Verify HTTP span attributes SpanData httpSpan = spans.stream() .filter(span -> "GET /api/orders/{orderId}".equals(span.getName())) .findFirst() .orElseThrow(); assertThat(httpSpan.getAttributes().get(AttributeKey.stringKey("http.method"))) .isEqualTo("GET"); assertThat(httpSpan.getAttributes().get(AttributeKey.stringKey("http.route"))) .isEqualTo("/api/orders/{orderId}"); } @Test void testDatabaseOperationCreatesSpan() { // When restTemplate.getForEntity("/api/orders?page=0&size=10", String.class); // Then List<SpanData> spans = otelTesting.getSpans(); // Verify database span was created boolean hasDbSpan = spans.stream() .anyMatch(span -> "OrderRepository.findByStatus".equals(span.getName()) || span.getName().startsWith("SELECT")); assertThat(hasDbSpan).isTrue(); } }

10. Spring Boot Application

Main Application Class

package com.example.otel; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableAsync; @SpringBootApplication @EnableAsync public class OrderServiceApplication { public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } }

Summary

This comprehensive OpenTelemetry auto-instrumentation implementation provides:

Key Features:

  • Zero-Code Instrumentation: Automatic tracing for HTTP, database, messaging
  • Spring Boot Integration: Seamless Spring framework support
  • Custom Instrumentation: Business-specific metrics and spans
  • Log Correlation: Trace context in logs
  • Performance Monitoring: Method-level performance tracking
  • Testing Support: Integration tests for telemetry

Auto-Instrumented Components:

  • HTTP Servers (Spring MVC, WebFlux)
  • HTTP Clients (WebClient, RestTemplate)
  • Database Operations (JPA, Hibernate, JDBC)
  • Messaging Systems (Kafka, RabbitMQ)
  • Async Operations (CompletableFuture, @Async)
  • Cache Operations (Spring Cache)

Best Practices:

  • Use Java agent for production deployments
  • Configure appropriate sampling rates
  • Add business context to spans
  • Monitor performance impact
  • Secure sensitive data in spans
  • Use structured logging with trace correlation

This implementation provides production-ready observability with minimal code changes, following OpenTelemetry standards and best practices.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper