java

Complete Guide to Implementing OpenTelemetry Distributed Tracing in Spring Boot Microservices

Learn to implement distributed tracing in Spring Boot microservices with OpenTelemetry. Step-by-step guide covering setup, custom spans, Jaeger integration & best practices.

Complete Guide to Implementing OpenTelemetry Distributed Tracing in Spring Boot Microservices

Recently, I spent three days debugging a production issue where a simple user request was timing out across our microservices. Without proper visibility into the request flow, pinpointing the bottleneck felt like searching for a needle in a haystack. This experience cemented my belief that distributed tracing isn’t a luxury—it’s a necessity for any modern, distributed system. Today, I’ll show you how to implement it using OpenTelemetry in Spring Boot.

The core idea is simple: when a request enters your system, it gets a unique trace ID. As it travels from one service to another, each operation becomes a “span” linked to that trace. This creates a complete map of the request’s journey. Why is this so powerful? Because it transforms a cascade of isolated logs into a coherent story.

Let’s start with the setup. For a Spring Boot 3.x application, the configuration is surprisingly straightforward. Add the OpenTelemetry starter to your pom.xml:

<dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-spring-boot-starter</artifactId>
</dependency>

With just this dependency, you get auto-instrumentation for web requests, JDBC calls, and more. But what if you want to track specific business logic?

This is where custom spans come in. Imagine you have a service that processes orders. You can wrap complex operations to see exactly how long they take. Here’s a basic example using the @WithSpan annotation:

@Service
public class OrderService {
    
    @WithSpan("process-payment")
    public PaymentResult processPayment(Order order) {
        // Your payment logic here
        return paymentGateway.charge(order);
    }
}

The span “process-payment” will now appear in your trace, showing its duration and any errors. But how do we connect spans when a call goes from one service to another?

Context propagation handles this automatically. When your order-service calls your inventory-service using a RestTemplate or WebClient, the trace context is passed via HTTP headers. The receiving service continues the same trace. Have you ever wondered which service in a chain is the real performance culprit? This answers that question precisely.

For database interactions, the auto-instrumentation is equally helpful. It creates spans for SQL queries, but sometimes you need more detail. You can add custom attributes to a span to provide context:

@WithSpan("update-inventory")
public void updateStock(String productId, int quantity) {
    Span.current().setAttribute("product.id", productId);
    Span.current().setAttribute("stock.quantity", quantity);
    // Update database
}

These attributes make your traces searchable and much more meaningful. You can quickly find all traces related to a specific product, for instance.

To see this data, you need an observability backend. Jaeger is a popular open-source choice. Running it locally with Docker is simple:

# docker-compose.yml
version: '3.8'
services:
  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "16686:16686" # UI
      - "4317:4317"   # OTLP gRPC port

Then, configure your application to send traces to Jaeger by adding this to your application.properties:

otel.exporter.otlp.endpoint=http://localhost:4317
otel.service.name=order-service
spring.application.name=order-service

After making a few requests, open Jaeger UI at http://localhost:16686. You’ll see a list of traces. Clicking on one reveals a waterfall diagram of the entire request flow across all services. It visually highlights slow operations and dependencies.

What about metrics? OpenTelemetry isn’t just for traces. You can easily capture business metrics, like the number of orders processed. Here’s a counter example:

@Autowired
private Meter meter;

private final LongCounter orderCounter;

@PostConstruct
public void createMetrics() {
    orderCounter = meter.counterBuilder("orders.processed")
                        .setDescription("Total number of orders processed")
                        .build();
}

public void createOrder(Order order) {
    // Business logic
    orderCounter.add(1, Attributes.of(stringKey("status"), "success"));
}

This gives you a quantitative view of your system’s behavior alongside the qualitative view from traces.

A common concern is performance overhead. The OpenTelemetry SDK is designed to be efficient. Sampling can reduce the volume of data collected. For example, you might sample only 10% of requests in a high-throughput service. The key is to start simple and adjust based on your needs.

I encourage you to try this in a development environment. The immediate visibility you gain into your application’s behavior is transformative. It shifts debugging from guesswork to precise analysis.

Did this guide help you see the path forward for your own microservices? I’d love to hear about your experiences. If you found this useful, please share it with your team or leave a comment below. Let’s build more observable systems together.

Keywords: distributed tracing OpenTelemetry, Spring Boot microservices tracing, OpenTelemetry Spring Boot tutorial, microservices observability Java, Jaeger distributed tracing, OpenTelemetry auto instrumentation, Spring Boot telemetry integration, distributed systems monitoring, OpenTelemetry custom spans, microservices performance tracing



Similar Posts
Blog Image
Spring WebFlux Complete Guide: Build High-Performance Reactive APIs with R2DBC and Redis Caching

Learn to build high-performance reactive REST APIs with Spring WebFlux, R2DBC PostgreSQL integration, and Redis caching for optimal scalability and performance.

Blog Image
Mastering Apache Kafka and Spring Cloud Stream Integration for Scalable Event-Driven Microservices Architecture

Learn how to integrate Apache Kafka with Spring Cloud Stream to build scalable, event-driven microservices with simplified messaging patterns and enterprise-grade reliability.

Blog Image
Spring Boot Memory Management: Advanced GC Tuning and Monitoring for Production Applications

Master Spring Boot memory management & GC tuning. Learn JVM structure, monitoring, optimization strategies & production troubleshooting for peak performance.

Blog Image
How to Build High-Performance Reactive Microservices with Spring WebFlux, R2DBC and Kafka

Learn to build reactive microservices with Spring WebFlux, R2DBC, and Kafka. Master non-blocking I/O, event-driven communication, and backpressure handling for high-performance systems.

Blog Image
Apache Kafka Spring Cloud Stream Integration Guide: Build Scalable Event-Driven Microservices Architecture

Learn how to integrate Apache Kafka with Spring Cloud Stream to build scalable event-driven microservices with simplified messaging and asynchronous communication.

Blog Image
Master Apache Kafka Streams with Spring Boot: Build Real-Time Event Processing Applications (2024 Guide)

Learn to build scalable event streaming applications with Apache Kafka Streams and Spring Boot. Master real-time processing, state management, and optimization techniques for high-performance systems.