Spring Boot Observability in 2026: OpenTelemetry, Distributed Tracing and Interview Questions
Master Spring Boot observability with OpenTelemetry and Micrometer Tracing. Learn distributed tracing setup, the Observation API, OTLP export configuration, and prepare for technical interviews.

Spring Boot observability combines logging, metrics, and distributed tracing into a unified system that reveals how requests flow through microservices. Starting with Spring Boot 3, the framework adopted Micrometer Tracing (replacing Spring Cloud Sleuth) and introduced the Observation API, which provides a single instrumentation point that emits both metrics and traces.
Spring Boot recommends using Observation.observe() rather than calling OpenTelemetry directly. One instrumentation call produces metrics via Micrometer and traces via the OpenTelemetry bridge, reducing code duplication and ensuring consistent tag naming across signals.
How the Observation API Bridges Micrometer and OpenTelemetry
The Observation API acts as a facade over both metrics and tracing. When code calls Observation.createNotStarted(), Spring Boot routes the observation to registered handlers: MeterObservationHandler for Micrometer metrics and TracingObservationHandler for distributed traces. This architecture means instrumenting once exports everywhere.
The bridge dependency micrometer-tracing-bridge-otel connects Micrometer Tracing to OpenTelemetry's SDK. Traces flow through OpenTelemetry's SdkTracerProvider and export via OTLP to backends like Jaeger, Tempo, or any OpenTelemetry-compatible collector.
@Configuration
public class ObservabilityConfig {
@Bean
public ObservationRegistryCustomizer<ObservationRegistry> addLowCardinalityTags() {
return registry -> registry.observationConfig()
.observationHandler(new ObservationTextPublisher()); // Logs observations to console
}
}The configuration above registers an ObservationHandler that logs every observation. In production, the auto-configured handlers push data to Micrometer registries and OpenTelemetry exporters without additional code.
Required Dependencies for Spring Boot 3.4 Tracing
Spring Boot 3.4 requires explicit dependencies for distributed tracing. The spring-boot-starter-actuator provides the Observation API, but tracing needs the OpenTelemetry bridge and an exporter.
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
</dependencies>The micrometer-tracing-bridge-otel artifact bridges Micrometer Tracing to OpenTelemetry's API. The opentelemetry-exporter-otlp sends spans to collectors using the OTLP protocol over HTTP or gRPC. Spring Boot manages version alignment through its dependency management.
Configuring OTLP Export to Jaeger or Tempo
Spring Boot auto-configures an OtlpHttpSpanExporter when the OTLP dependency is present. The exporter sends traces to the endpoint specified in application.yml.
# application.yml
management:
tracing:
sampling:
probability: 1.0 # 100% sampling for dev, reduce in production
otlp:
tracing:
endpoint: http://localhost:4318/v1/traces
opentelemetry:
resource-attributes:
service.name: order-service
deployment.environment: stagingThe resource-attributes attach metadata to every span. Backends like Grafana Tempo and Jaeger use these attributes to group traces by service and environment. Setting sampling.probability to 1.0 captures all requests during development, but production systems typically sample between 1% and 10% to control storage costs.
Creating Custom Observations with Low and High Cardinality Tags
Observations support two tag types: low cardinality for metrics (bounded values like HTTP methods or status codes) and high cardinality for traces (unbounded values like user IDs or request IDs).
@Service
public class PaymentService {
private final ObservationRegistry observationRegistry;
public PaymentService(ObservationRegistry observationRegistry) {
this.observationRegistry = observationRegistry;
}
public PaymentResult processPayment(PaymentRequest request) {
return Observation.createNotStarted("payment.process", observationRegistry)
.lowCardinalityKeyValue("payment.method", request.getMethod().name()) // enum: CARD, BANK_TRANSFER
.lowCardinalityKeyValue("currency", request.getCurrency()) // bounded: USD, EUR, GBP
.highCardinalityKeyValue("payment.id", request.getPaymentId()) // unique per request
.highCardinalityKeyValue("customer.id", request.getCustomerId()) // high cardinality
.observe(() -> executePayment(request));
}
private PaymentResult executePayment(PaymentRequest request) {
// Payment gateway call
return new PaymentResult(true, "TXN-" + UUID.randomUUID());
}
}Low cardinality tags appear in both metrics and traces. High cardinality tags appear only in traces because metrics with unbounded dimensions explode storage. This distinction prevents Prometheus cardinality bombs while keeping trace details rich.
Interviewers often ask why some tags should be excluded from metrics. The answer involves cardinality: a user ID tag on a metric creates one time series per user, potentially millions of series. Monitoring backends struggle with high cardinality, leading to memory exhaustion and slow queries. Traces handle high cardinality through sampling, making them the correct place for request-specific identifiers.
Ready to ace your Spring Boot interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Propagating Trace Context Across Async Boundaries
Distributed tracing requires context propagation. HTTP headers carry trace IDs between services, but async operations like @Async methods or CompletableFuture chains can lose context if not configured.
Spring Boot 3.4 provides automatic propagation for reactive streams and @Async methods when configured:
# application.yml
spring:
reactor:
context-propagation: auto
task:
execution:
propagate-context: trueFor custom TaskExecutor beans, wrap them with ContextPropagatingTaskDecorator:
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setTaskDecorator(new ContextPropagatingTaskDecorator());
executor.initialize();
return executor;
}
}The decorator copies the current Observation and trace context into the async thread. Without it, spans started in async tasks have no parent, breaking the trace tree.
Using @Observed and @NewSpan Annotations
Spring Boot 3.4 supports declarative observability through annotations. Enable annotation processing by adding the AspectJ weaver:
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency># application.yml
management:
observations:
annotations:
enabled: true@Service
public class InventoryService {
@Observed(name = "inventory.check", contextualName = "checkStock")
public StockLevel checkStock(String productId) {
// Database query
return new StockLevel(productId, 42);
}
@NewSpan("inventory.reserve") // Creates a child span under the current trace
public ReservationResult reserveStock(String productId, int quantity) {
// Update inventory
return new ReservationResult(true, "RES-" + System.currentTimeMillis());
}
}@Observed creates both a metric (timer) and a span. @NewSpan creates only a span, useful when the operation already has metrics elsewhere. Both annotations automatically handle exception recording and span status.
Auto-Instrumented Components in Spring Boot 3.4
Spring Boot 3.4 auto-instruments several components without code changes:
| Component | Observation Name | What It Captures |
|---|---|---|
| Spring MVC | http.server.requests | Request path, method, status, exception |
| WebClient | http.client.requests | Outbound HTTP calls |
| RestClient | http.client.requests | Outbound HTTP calls (Spring 6.1+) |
| Spring Kafka | spring.kafka.listener | Consumer group, topic, partition |
| Spring Data JPA | spring.data.repository | Repository method, query time |
| Scheduled Tasks | spring.scheduling | Task name, execution time |
The Spring Boot Actuator documentation lists all auto-instrumented components. Third-party libraries like Datasource Micrometer add JDBC query tracing.
Filtering Observations to Reduce Noise
Health checks, readiness probes, and static resources generate noise in traces. Filter them out using predicates:
@Configuration
public class ObservationFilterConfig {
@Bean
public ObservationPredicate noHealthChecks() {
return (name, context) -> !name.equals("http.server.requests")
|| !isHealthEndpoint(context);
}
private boolean isHealthEndpoint(Observation.Context context) {
if (context instanceof ServerRequestObservationContext http) {
String path = http.getCarrier().getRequestURI();
return path.startsWith("/actuator/health") || path.startsWith("/actuator/ready");
}
return false;
}
}Alternatively, disable observations by name in configuration:
# application.yml
management:
observations:
enable:
spring.security: false # Disable Spring Security observations
http.server.requests.actuator: false # Custom predicate nameSampling reduces the percentage of traces collected across all endpoints. Filtering removes specific endpoints entirely. Use filtering for endpoints that never provide diagnostic value (health checks, metrics scraping). Use sampling to control costs while preserving representative data.
Correlating Logs with Trace IDs
Spring Boot 3.4 automatically adds trace and span IDs to the MDC (Mapped Diagnostic Context) when Micrometer Tracing is active. Logback and Log4j2 can include these IDs in log output:
<!-- logback-spring.xml -->
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - traceId=%X{traceId} spanId=%X{spanId} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</configuration>With this pattern, every log line includes the trace ID. Searching logs by trace ID returns all messages from a single request across all services, correlating logs with spans in Jaeger or Tempo.
Common Interview Questions on Spring Boot Observability
Interviewers assess both conceptual understanding and practical experience with observability. These questions appear frequently in senior backend roles.
Q: What is the difference between Micrometer Tracing and OpenTelemetry?
Micrometer Tracing is a vendor-neutral API for distributed tracing, similar to how Micrometer abstracts metrics. OpenTelemetry is a specific implementation and protocol. Spring Boot uses Micrometer Tracing as the API and bridges to OpenTelemetry for export via micrometer-tracing-bridge-otel. This layering lets applications switch backends without code changes.
Q: Why does Spring recommend the Observation API over direct OpenTelemetry instrumentation?
The Observation API provides a single instrumentation point that emits both metrics and traces. Direct OpenTelemetry calls produce only traces. Using Observation.observe() generates a timer metric and a span from the same code, reducing duplication and ensuring consistent naming.
Q: How do you debug a trace that shows a gap between spans?
Gaps indicate missing instrumentation or broken context propagation. Check if the code uses async operations without ContextPropagatingTaskDecorator. Verify that HTTP clients are instrumented (WebClient, RestClient, or a manually wrapped RestTemplate). For message queues, confirm that trace headers propagate through message properties.
Q: What happens if you add a high cardinality tag to a metric?
Each unique tag value creates a new time series. A user ID tag with millions of users creates millions of series, exhausting memory in Prometheus or other TSDB backends. The fix is using highCardinalityKeyValue() instead of lowCardinalityKeyValue(), which adds the tag only to traces where high cardinality is expected.
Building a Distributed Tracing Pipeline with Docker Compose
A local observability stack helps validate instrumentation before deploying to production. This example uses the OpenTelemetry Collector and Jaeger:
# docker-compose.yml
services:
otel-collector:
image: otel/opentelemetry-collector-contrib:0.102.0
command: ["--config", "/etc/otel-collector-config.yml"]
volumes:
- ./otel-collector-config.yml:/etc/otel-collector-config.yml
ports:
- "4318:4318" # OTLP HTTP
- "4317:4317" # OTLP gRPC
jaeger:
image: jaegertracing/jaeger:2.3
ports:
- "16686:16686" # UI
environment:
- COLLECTOR_OTLP_ENABLED=true
app:
build: .
environment:
- MANAGEMENT_OTLP_TRACING_ENDPOINT=http://otel-collector:4318/v1/traces
depends_on:
- otel-collector# otel-collector-config.yml
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
grpc:
endpoint: 0.0.0.0:4317
processors:
batch:
timeout: 1s
send_batch_size: 1024
exporters:
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp/jaeger]The collector receives spans from the Spring Boot application and forwards them to Jaeger. This architecture allows adding exporters (Tempo, Zipkin, cloud backends) without changing application code.
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Key Takeaways for Spring Boot Observability
- The Observation API unifies metrics and tracing: one
Observation.observe()call produces both a timer metric and a span, eliminating dual instrumentation. - Add
micrometer-tracing-bridge-otelandopentelemetry-exporter-otlpto enable OTLP export. Spring Boot 3.4 auto-configures the HTTP exporter with the endpoint frommanagement.otlp.tracing.endpoint. - Use
lowCardinalityKeyValue()for dimensions that appear in metrics (bounded values). UsehighCardinalityKeyValue()for trace-only data (user IDs, request IDs). - Enable context propagation for async code with
spring.task.execution.propagate-context=trueand wrap custom executors withContextPropagatingTaskDecorator. - Filter noisy endpoints like
/actuator/healthusingObservationPredicatebeans to keep traces focused on business operations. - Log patterns should include
%X{traceId}to correlate log lines with distributed traces in your observability backend. - Interview questions on observability focus on cardinality, context propagation failures, and the distinction between the Observation API and direct OpenTelemetry usage.
Can you spot the bug in Spring Boot?
One real snippet, one hidden bug, one attempt a day. No account needed to try.

Written by
Anthony Fillion-MailletFounder of SharpSkill
Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.
Updated on September 19, 2026
Tags
Share
Related articles

Spring Boot Actuator: Production Monitoring with Micrometer and Prometheus
Complete Spring Boot Actuator guide for production monitoring. Micrometer configuration, Prometheus metrics, custom endpoints and alerting setup.

Spring Boot YAML vs Properties: Configuration Comparison and Interview Questions 2026
Compare application.yml and application.properties in Spring Boot 3.4. Learn when to use each format, understand the differences, and prepare for configuration-related interview questions.

Spring Boot Structured Logging in 2026: JSON Logs for Production with Logback and OpenTelemetry
Complete guide to Spring Boot 4.x structured logging. Native JSON support, OpenTelemetry starter, MDC tracing, and ELK Stack integration for production observability.