Using OpenTelemetry with Spring Boot for Distributed Tracing
Integrating Jaeger, Zipkin, or Tempo via Micrometer and OpenTelemetry
Modern microservices are powerful—but they introduce complexity. When dozens of services talk to each other, identifying slow calls, errors, or bottlenecks becomes nearly impossible without observability. That’s where distributed tracing comes in.
OpenTelemetry is the industry-standard framework to collect, process, and export telemetry data—including traces, metrics, and logs. In the Java ecosystem, Spring Boot integrates seamlessly with OpenTelemetry, especially through Micrometer, the observability facade that unifies metrics and tracing APIs.
This article will walk you through:
- Why distributed tracing matters
- How OpenTelemetry fits into Spring Boot
- Integrating tracing backends like Jaeger, Zipkin, or Grafana Tempo
- Best practices for production-grade tracing
1. Why Distributed Tracing?
Imagine a user request flowing through:
- API Gateway
- Order Service
- Payment Service
- Notification Service
If the user experiences a 2-second delay, you need to see where the time was spent—across all services. Distributed tracing lets you:
✅ Visualize end-to-end request flow
✅ Measure latency per hop
✅ Identify failed or slow spans
✅ Optimize performance holistically
This insight is essential for SRE teams, developers, and platform engineers operating microservices.
2. Introducing OpenTelemetry
OpenTelemetry is a CNCF project that merges OpenTracing and OpenCensus into a single, vendor-neutral standard. It defines:
- APIs and SDKs to instrument your code
- Protocols for exporting traces and metrics
- Integration with popular backends (Jaeger, Zipkin, Tempo, New Relic)
Spring Boot doesn’t ship OpenTelemetry by default, but you can integrate it easily via Micrometer Tracing.
3. Spring Boot + Micrometer Tracing + OpenTelemetry
Spring Boot 3+ has embraced Micrometer Tracing, which unifies tracing APIs across OpenTelemetry, Brave (Zipkin), and others.
Core Concept:
- Micrometer provides abstraction.
- You plug in a tracer implementation (OpenTelemetry).
- You configure an exporter (Jaeger, Zipkin, Tempo).
4. Example: Setting Up OpenTelemetry with Jaeger
Let’s walk through a Jaeger integration step by step.
1. Add dependencies
In Gradle:
implementation "io.micrometer:micrometer-tracing-bridge-otel" implementation "io.opentelemetry:opentelemetry-exporter-jaeger"
For Maven:
<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-tracing-bridge-otel</artifactId> </dependency> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-exporter-jaeger</artifactId> </dependency>
2. Configure the exporter
Create a TracingConfig.java class to wire OpenTelemetry and Jaeger:
@Configuration
public class TracingConfig {
@Bean
public OtlpGrpcSpanExporter otlpSpanExporter() {
return OtlpGrpcSpanExporter.builder()
.setEndpoint("http://localhost:14250") // Jaeger collector endpoint
.build();
}
@Bean
public SdkTracerProvider sdkTracerProvider(OtlpGrpcSpanExporter exporter) {
return SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(exporter).build())
.setResource(Resource.getDefault())
.build();
}
@Bean
public OpenTelemetry openTelemetry(SdkTracerProvider sdkTracerProvider) {
return OpenTelemetrySdk.builder()
.setTracerProvider(sdkTracerProvider)
.build();
}
}
Tip: Jaeger must be running. You can start it with Docker:
docker run -d --name jaeger \ -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \ -p 5775:5775/udp \ -p 6831:6831/udp \ -p 6832:6832/udp \ -p 5778:5778 \ -p 16686:16686 \ -p 14268:14268 \ -p 14250:14250 \ -p 9411:9411 \ jaegertracing/all-in-one:1.48
Jaeger UI will be available at http://localhost:16686.
3. Automatic Span Creation
With the dependencies in place, Micrometer automatically creates spans for:
- HTTP server requests (Spring MVC or WebFlux)
- RestTemplate and WebClient calls
- Kafka or RabbitMQ messaging
You don’t need extra annotations for basic tracing.
4. Manual Spans (Custom Operations)
If you want to instrument your own logic:
@Autowired
Tracer tracer;
public void processOrder() {
Span span = this.tracer.nextSpan().name("processOrder").start();
try (Tracer.SpanInScope ws = this.tracer.withSpan(span)) {
// Your business logic
} finally {
span.end();
}
}
This ensures you can track specific business operations.
5. Using Zipkin or Tempo
You can easily switch exporters:
- Zipkin:
implementation "io.opentelemetry:opentelemetry-exporter-zipkin"
- and point the OTLP endpoint to Tempo.
Tempo Example Endpoint:
http://localhost:4317
Tempo is OTLP-native, making it a natural fit for OpenTelemetry.
6. Best Practices for Production Tracing
- Sampling:
Configure sampling to avoid excessive overhead. E.g., 10% of traces:
.setSampler(Sampler.traceIdRatioBased(0.1))
- Batch Exporting:
Always preferBatchSpanProcessoroverSimpleSpanProcessorto reduce CPU and network usage. - Context Propagation:
Ensure context is propagated across threads (e.g.,@Asyncmethods) and messaging systems. - Correlate with Logs:
UsetraceIdandspanIdin your log patterns to correlate logs and traces. - Secure Your Endpoints:
Do not expose tracing exporters to public networks without authentication.
7. Conclusion
OpenTelemetry and Spring Boot make distributed tracing accessible and powerful. By integrating with Jaeger, Zipkin, or Tempo, you gain visibility across your entire service landscape.
With just a few dependencies and configuration classes, you can trace every request, diagnose bottlenecks, and improve system reliability—without vendor lock-in.




