Spring Boot 3 Observability with Micrometer and OpenTelemetry
title: “Spring Boot 3 Observability with Micrometer and OpenTelemetry” date: 2025-04-10 tags: [“Spring Boot 3”, “Micrometer”, “OpenTelemetry”, “Observability”, “Distributed Tracing”, “Metrics”] categories: [“Java”]
Spring Boot 3 Observability with Micrometer and OpenTelemetry
If you’ve ever tried to debug a production issue in a distributed system without proper observability, you know the pain. Logs alone tell you what happened, but not why or where the latency came from. With Spring Boot 3, observability is no longer an afterthought—it’s a first-class citizen. The combination of Micrometer and OpenTelemetry provides a powerful, vendor-neutral way to capture metrics, traces, and logs in a unified manner.
In this post, I’ll walk you through setting up observability in a Spring Boot 3 application using Micrometer for metrics and OpenTelemetry for distributed tracing. We’ll cover practical configurations, code examples, and how to export data to popular backends like Prometheus, Jaeger, and Grafana.
Why Spring Boot 3 Changes the Game
Spring Boot 3 introduced a new observability API built on top of Micrometer’s Observation API. This API unifies metrics and tracing under a single abstraction. Before Spring Boot 3, you had to manually instrument your code with separate libraries for metrics (Micrometer) and tracing (Spring Cloud Sleuth + OpenTelemetry). Now, you write one observation and get both metrics and traces automatically.
Key benefits:
- Unified API: One annotation or programmatic API for metrics and traces
- Vendor-neutral: Switch backends (Prometheus, Datadog, Jaeger, Zipkin) without code changes
- Automatic instrumentation: Spring Boot auto-configures many common components (HTTP, JDBC, Redis, Kafka)
- Context propagation: Trace context flows seamlessly across threads and services
Setting Up a Spring Boot 3 Project
Let’s start from scratch. Create a new Spring Boot 3 project with the necessary dependencies. I’ll use Maven, but Gradle works similarly.
pom.xml dependencies
1 | <dependencies> |
Key dependencies explained
micrometer-tracing-bridge-otel: Bridges Micrometer’s tracing API to OpenTelemetryopentelemetry-exporter-otlp: Exports traces via OTLP (OpenTelemetry Protocol) to any OTLP-compatible backendmicrometer-registry-prometheus: Exposes metrics in Prometheus format at/actuator/prometheusspring-boot-starter-actuator: Provides health, info, metrics, and tracing endpoints
Configuration
application.yml
1 | spring: |
Important notes:
- Set
spring.tracing.sampling.probabilityto a lower value (e.g., 0.1) in production to control costs - The OTLP endpoint can point to Jaeger, Grafana Tempo, or any OpenTelemetry Collector
- Metrics tags help filter and group data in Prometheus
Automatic Instrumentation: What You Get for Free
Spring Boot 3 automatically instruments many components. Without writing a single line of code, you get:
- HTTP requests: Incoming and outgoing requests are traced with span context
- JDBC queries: Each SQL statement is captured as a span (if using Spring Data JPA or JDBC)
- Redis operations: Lettuce or Jedis operations are traced
- Kafka messaging: Producer and consumer traces are propagated
- Reactive streams: Reactor operators preserve trace context
Let’s test this with a simple REST controller.
Sample Controller
1 |
|
1 |
|
With the default configuration, every HTTP request to /api/orders/{id} generates:
- A trace with spans for the HTTP request, controller method, and JDBC query
- Metrics like
http.server.requestswith tags for status, method, and URI
You can view the trace in Jaeger (or your backend) and see the exact SQL statement executed.
Custom Instrumentation with @Observed
Sometimes you need to instrument custom business logic. Spring Boot 3 provides the @Observed annotation for this purpose.
Using @Observed
1 | import io.micrometer.observation.annotation.Observed; |
What @Observed does:
- Creates a new span in the current trace
- Records timing metrics (duration histogram)
- Automatically captures exceptions as error tags
- Propagates the trace context to downstream calls
Programmatic Observation
If annotations aren’t flexible enough, use the ObservationRegistry directly.
1 | import io.micrometer.observation.Observation; |
Adding Custom Metrics
While the Observation API handles common cases, you may need custom metrics like gauges or counters.
Counter Example
1 | import io.micrometer.core.instrument.MeterRegistry; |
Timer Example
1 | import io.micrometer.core.instrument.Timer; |
Integrating with Grafana, Prometheus, and Jaeger
Let’s set up a complete observability stack using Docker Compose.
docker-compose.yml
1 | version: '3.8' |
prometheus.yml
1 | scrape_configs: |
Advanced: Context Propagation Across Threads
One common challenge is preserving trace context when using async operations. Spring Boot 3 handles this with ThreadPoolTaskExecutor auto-configuration.
Async Example
1 |
|
1 |
|
No extra configuration needed—Spring Boot 3 automatically wraps the executor with trace context propagation.
Best Practices from Production
After running this setup in production for several months, here are some lessons learned:
1. Sampling Strategy
Don’t sample 100% in production unless you have unlimited storage. Use a probabilistic sampler with a rate that balances cost and visibility. For critical services, consider a rate-limiting sampler that captures all traces for high-latency requests.
1 | management: |
2. Tag Cardinality
Avoid high-cardinality tags (e.g., user IDs, session IDs) in metrics. They explode the number of time series in Prometheus. Use them only in traces, not metrics.
3. Custom Spans for External Calls
If your service calls external APIs not instrumented by Spring Boot, wrap them with @Observed or programmatic observations.
1 |
|
4. Use OpenTelemetry Collector
Instead of exporting directly to Jaeger or Prometheus, use the OpenTelemetry Collector as a middleware. It provides buffering, retries, and can fan-out to multiple backends.
5. Log Correlation
Spring Boot 3 automatically adds trace IDs and span IDs to MDC (Mapped Diagnostic Context). Configure your logging pattern to include them.
1 | logging: |
This allows you to correlate logs with traces in Grafana or Kibana.
Troubleshooting Common Issues
Traces not appearing?
- Check that the OTLP exporter is correctly configured
- Verify the backend (Jaeger, Tempo) is running and accessible
- Look for errors in logs like
Failed to export spans - Ensure
spring-boot-starter-actuatoris on the classpath
Metrics not showing in Prometheus?
- Hit
/actuator/prometheusendpoint to verify metrics are exposed - Check Prometheus target status in the UI
- Ensure
micrometer-registry-prometheusis on the classpath
High memory usage?
Reduce sampling probability or increase the export interval. Also, consider using the OpenTelemetry Collector with batching.
Key Takeaways
- Spring Boot 3 unifies metrics and tracing through the Micrometer Observation API, reducing boilerplate and cognitive load
- Automatic instrumentation covers HTTP, JDBC, Redis, Kafka, and more—zero code needed for basic observability
- Use @Observed for custom business logic to get both spans and metrics with a single annotation
- Export traces via OTLP to Jaeger, Grafana Tempo, or any OpenTelemetry-compatible backend
- Export metrics via Prometheus and visualize in Grafana for powerful dashboards
- Always sample strategically in production—100% sampling is rarely necessary
- Correlate logs with traces using MDC to speed up debugging
- Invest in the OpenTelemetry Collector for production deployments to handle backpressure and multi-backend export
Observability in Spring Boot 3 is no longer a headache. With Micrometer and OpenTelemetry, you get a robust, vendor-neutral foundation that grows with your system. Start instrumenting today—your future self (and on-call team) will thank you.