# 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. - Published: 2026-09-19 - Updated: 2026-09-19 - Author: Anthony Fillion-Maillet - Tags: spring-boot, observability, opentelemetry, distributed-tracing, micrometer - Reading time: 12 min --- 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. > **The Observation API Pattern** > > 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. ```java // ObservabilityConfig.java @Configuration public class ObservabilityConfig { @Bean public ObservationRegistryCustomizer 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. ```xml org.springframework.boot spring-boot-starter-actuator io.micrometer micrometer-tracing-bridge-otel io.opentelemetry opentelemetry-exporter-otlp ``` 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`. ```yaml # 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: staging ``` The `resource-attributes` attach metadata to every span. Backends like [Grafana Tempo](https://grafana.com/oss/tempo/) and [Jaeger](https://www.jaegertracing.io/) 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). ```java // PaymentService.java @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. > **Interview Question: Tag Cardinality** > > 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. ## 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: ```yaml # application.yml spring: reactor: context-propagation: auto task: execution: propagate-context: true ``` For custom `TaskExecutor` beans, wrap them with `ContextPropagatingTaskDecorator`: ```java // AsyncConfig.java @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: ```xml org.springframework.boot spring-boot-starter-aop ``` ```yaml # application.yml management: observations: annotations: enabled: true ``` ```java // InventoryService.java @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](https://docs.spring.io/spring-boot/reference/actuator/observability.html) lists all auto-instrumented components. Third-party libraries like [Datasource Micrometer](https://github.com/jdbc-observations/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: ```java // ObservationFilterConfig.java @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: ```yaml # application.yml management: observations: enable: spring.security: false # Disable Spring Security observations http.server.requests.actuator: false # Custom predicate name ``` > **Interview Context: Trace Sampling vs. Filtering** > > Sampling 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: ```xml %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - traceId=%X{traceId} spanId=%X{spanId} - %msg%n ``` 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](https://opentelemetry.io/docs/collector/) and Jaeger: ```yaml # 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 ``` ```yaml # 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. ## 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-otel` and `opentelemetry-exporter-otlp` to enable OTLP export. Spring Boot 3.4 auto-configures the HTTP exporter with the endpoint from `management.otlp.tracing.endpoint`. - Use `lowCardinalityKeyValue()` for dimensions that appear in metrics (bounded values). Use `highCardinalityKeyValue()` for trace-only data (user IDs, request IDs). - Enable context propagation for async code with `spring.task.execution.propagate-context=true` and wrap custom executors with `ContextPropagatingTaskDecorator`. - Filter noisy endpoints like `/actuator/health` using `ObservationPredicate` beans 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/spring-boot/spring-boot-observability-opentelemetry-distributed-tracing