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 with OpenTelemetry distributed tracing

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.

ObservabilityConfig.javajava
@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.

xml
<!-- 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.

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 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).

PaymentService.javajava
@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.

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:

yaml
# application.yml
spring:
  reactor:
    context-propagation: auto
  task:
    execution:
      propagate-context: true

For custom TaskExecutor beans, wrap them with ContextPropagatingTaskDecorator:

AsyncConfig.javajava
@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
<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
yaml
# application.yml
management:
  observations:
    annotations:
      enabled: true
InventoryService.javajava
@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:

ComponentObservation NameWhat It Captures
Spring MVChttp.server.requestsRequest path, method, status, exception
WebClienthttp.client.requestsOutbound HTTP calls
RestClienthttp.client.requestsOutbound HTTP calls (Spring 6.1+)
Spring Kafkaspring.kafka.listenerConsumer group, topic, partition
Spring Data JPAspring.data.repositoryRepository method, query time
Scheduled Tasksspring.schedulingTask 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:

ObservationFilterConfig.javajava
@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
<!-- 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:

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.

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-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.
Daily challenge

Can you spot the bug in Spring Boot?

One real snippet, one hidden bug, one attempt a day. No account needed to try.

Anthony Fillion-Maillet

Written by

Anthony Fillion-Maillet

Founder of SharpSkill

Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.

Updated on September 19, 2026

Tags

#spring-boot
#observability
#opentelemetry
#distributed-tracing
#micrometer

Share

Related articles