CubeAPM
CubeAPM CubeAPM

How to Add OpenTelemetry Tracing to a Spring Boot Application 

How to Add OpenTelemetry Tracing to a Spring Boot Application 

Table of Contents

There are three ways to add OpenTelemetry tracing to a Spring Boot application in 2026, and choosing the wrong one for your setup causes configuration conflicts that are hard to debug. The OTel Java agent attaches at JVM startup via -javaagent with zero code changes and the widest library coverage. The OTel Spring Boot community starter adds tracing as a Spring dependency and integrates with application.properties. 

The Spring Boot 4 native starter (spring-boot-starter-opentelemetry) ships with Spring Boot 4 itself and uses Micrometer internally with management.* configuration properties. This guide covers all three, when to use each, and how to add manual spans on top of any approach.

Key Takeaways

  • Spring Boot 4.0.6 (latest stable as of April 23, 2026) ships its own native OTel starter (org.springframework.boot:spring-boot-starter-opentelemetry) that is separate from and incompatible with the community OTel starter. It uses Micrometer internally and management.* config properties
  • The OTel Java agent (v2.28.1, latest as of May 2026) is the default recommendation from the official OTel docs for Spring Boot applications because it provides more out-of-the-box instrumentation than either starter
  • The community OTel Spring Boot starter (io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter, BOM version 2.28.0) supports Spring Boot 2.6+ and 3.x and uses otel.* config properties. Do not mix it with the Spring Boot 4 native starter
  • Default OTLP export protocol since Java agent v2.0.0 is HTTP/protobuf (port 4318), not gRPC
  • @WithSpan for declarative span creation requires both opentelemetry-instrumentation-annotations and spring-boot-starter-aop when using the OTel community starter. Without AOP, the annotation has no effect
  • The Java agent does not work with Spring Boot Native Image (GraalVM). Use the OTel community starter or Spring Boot 4 native starter for native image builds

The Three Approaches at a Glance

ApproachSpring Boot versionZero code changesNative Image supportConfig prefix
OTel Java agent (v2.28.1)AnyYes (javaagent flag)Nootel.* (system properties or env vars)
OTel community starter (BOM 2.28.0)2.6+ and 3.xNo (add dependency)Yesotel.* (in application.properties)
Spring Boot 4 native starter4.x onlyNo (add dependency)Yesmanagement.*

Option 1: OTel Java Agent (Recommended Starting Point)

The Java agent is the official OTel recommendation for Spring Boot applications. It attaches to the JVM at startup, rewrites bytecode at class load time, and instruments HTTP, JDBC, gRPC, Kafka, Redis, and hundreds of other libraries without any code changes.

Download the agent

curl -L -o opentelemetry-javaagent.jar \

https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar

Start your Spring Boot application with the agent

java -javaagent:opentelemetry-javaagent.jar \

  -Dotel.service.name=order-service \

  -Dotel.service.version=1.0.0 \

  -Dotel.resource.attributes=deployment.environment=production \

  -Dotel.exporter.otlp.endpoint=http://otel-collector:4318 \

  -Dotel.exporter.otlp.protocol=http/protobuf \

  -jar order-service.jar

Alternatively, use environment variables (these take precedence over system properties):

export OTEL_SERVICE_NAME=order-service

export OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318

export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf

java -javaagent:opentelemetry-javaagent.jar -jar order-service.jar

Docker Compose

services:

  order-service:

    image: order-service:latest

    environment:

      OTEL_SERVICE_NAME: order-service

      OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318

      OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf

      JAVA_TOOL_OPTIONS: "-javaagent:/app/opentelemetry-javaagent.jar"

    volumes:

      - ./opentelemetry-javaagent.jar:/app/opentelemetry-javaagent.jar

What the agent instruments automatically

With no code changes, every incoming HTTP request to your Spring Boot controller gets a span with the method, URL template, status code, and duration. Every outgoing HTTP call via RestTemplate, WebClient, or HttpClient gets a span. Every JDBC query gets a span with the SQL statement. Kafka producer and consumer operations, Redis calls via Jedis or Lettuce, gRPC calls, and Spring Scheduling tasks are also instrumented. W3C TraceContext headers are propagated on all outbound calls automatically.

When not to use the agent

  • Spring Boot Native Image (GraalVM): the agent requires bytecode manipulation at class load time, which does not work with ahead-of-time compiled native images
  • If another Java agent is already attached: two agents can conflict. Check compatibility before using both
  • If JVM startup overhead is a concern: the agent adds measurable startup latency due to class instrumentation

Option 2: OTel Community Spring Boot Starter (Spring Boot 2.6 and 3.x)

The OpenTelemetry community starter adds instrumentation as a Spring dependency. It supports application.properties and application.yml for configuration and works with Spring Boot Native Image builds.

Maven (using the BOM)

<dependencyManagement>

  <dependencies>

    <dependency>

      <groupId>io.opentelemetry.instrumentation</groupId>

      <artifactId>opentelemetry-instrumentation-bom</artifactId>

      <version>2.28.0</version>

      <type>pom</type>

      <scope>import</scope>

    </dependency>

  </dependencies>

</dependencyManagement>

<dependencies>

  <dependency>

    <groupId>io.opentelemetry.instrumentation</groupId>

    <artifactId>opentelemetry-spring-boot-starter</artifactId>

  </dependency>

</dependencies>

Gradle

implementation(platform("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom:2.28.0"))

implementation("io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter")

Note: do not combine the BOM import with the io.spring.dependency-management plugin in Gradle. Use Gradle’s native platform import instead, as the official OTel docs explicitly warn against mixing these two approaches.

Configure via application.properties

otel.service.name=order-service

otel.service.version=1.0.0

otel.resource.attributes=deployment.environment=production

otel.exporter.otlp.endpoint=http://otel-collector:4318

otel.exporter.otlp.protocol=http/protobuf

Or application.yml:

otel:

  service:

    name: order-service

  exporter:

    otlp:

      endpoint: http://otel-collector:4318

      protocol: http/protobuf

Option 3: Spring Boot 4 Native Starter

Spring Boot 4 ships spring-boot-starter-opentelemetry as a first-party dependency. It uses Micrometer’s tracing bridge internally and exports via OTLP. Configuration uses the management.* property namespace, not otel.*.

Do not combine this with the community OTel starter. They configure different underlying components that will conflict.

Maven

<dependency>

  <groupId>org.springframework.boot</groupId>

  <artifactId>spring-boot-starter-opentelemetry</artifactId>

</dependency>

Configure via application.properties (Spring Boot 4)

management.opentelemetry.resource-attributes.service.name=order-service

management.opentelemetry.tracing.export.otlp.endpoint=http://otel-collector:4318

management.tracing.sampling.probability=1.0

The management.tracing.sampling.probability=1.0 setting is required to collect all traces in development. The default sampling rate in Spring Boot 4 is 0.1 (10%), which means 90% of traces are dropped unless you raise this value.

Step: Add Manual Spans for Business Logic

All three instrumentation approaches automatically capture HTTP, database, and messaging spans. For business logic inside your handlers, add manual spans.

Using @WithSpan (declarative, requires AOP)

When using the OTel community starter or Java agent, add these dependencies for @WithSpan:

<!-- Required for @WithSpan annotation -->

<dependency>

  <groupId>io.opentelemetry.instrumentation</groupId>

  <artifactId>opentelemetry-instrumentation-annotations</artifactId>

</dependency>

<!-- Required for AOP processing of @WithSpan when using the OTel starter -->

<dependency>

  <groupId>org.springframework.boot</groupId>

  <artifactId>spring-boot-starter-aop</artifactId>

</dependency>

Then annotate methods:

import io.opentelemetry.instrumentation.annotations.WithSpan;

import io.opentelemetry.instrumentation.annotations.SpanAttribute;

@Service

public class OrderService {

    @WithSpan("order.validate")

    public void validateOrder(

            @SpanAttribute("order.id") String orderId,

            @SpanAttribute("order.amount") double amount) {

        // validation logic

    }

}

@WithSpan only works on Spring-managed beans and on methods called from outside the bean via proxy interception. Internal calls within the same class do not create new spans. This is a standard Spring AOP limitation.

Using the Tracer API (programmatic, works everywhere)

import io.opentelemetry.api.trace.Tracer;

import io.opentelemetry.api.trace.Span;

import io.opentelemetry.api.trace.StatusCode;

import io.opentelemetry.context.Scope;

@RestController

public class OrderController {

    private final Tracer tracer;

    public OrderController(Tracer tracer) {

        this.tracer = tracer;

    }

    @PostMapping("/orders")

    public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request) {

        Span span = tracer.spanBuilder("order.process")

            .setAttribute("order.customer_id", request.getCustomerId())

            .setAttribute("order.item_count", request.getItems().size())

            .startSpan();

        try (Scope scope = span.makeCurrent()) {

            Order order = processOrder(request);

            span.setAttribute("order.id", order.getId());

            return ResponseEntity.ok(order);

        } catch (Exception e) {

            span.recordException(e);

            span.setStatus(StatusCode.ERROR, e.getMessage());

            throw e;

        } finally {

            span.end();

        }

    }

}

span.makeCurrent() sets the span as the active span on the current thread. Any child spans created during the try block automatically become children of this span. Always call span.end() in a finally block. A span that is not ended is never exported.

Adding attributes to the current active span

import io.opentelemetry.api.trace.Span;

@GetMapping("/orders/{orderId}")

public Order getOrder(@PathVariable String orderId) {

    Span.current().setAttribute("order.id", orderId);

    return orderRepository.findById(orderId);

}

Step: Configure the OTel Collector

All three approaches export OTLP to a Collector. A minimal otel-collector-config.yaml:

receivers:

  otlp:

    protocols:

      grpc:

        endpoint: 0.0.0.0:4317

      http:

        endpoint: 0.0.0.0:4318

processors:

  batch:

    timeout: 5s

    send_batch_size: 512

exporters:

  otlp:

    endpoint: your-backend:4317

service:

  pipelines:

    traces:

      receivers: [otlp]

      processors: [batch]

      exporters: [otlp]

    metrics:

      receivers: [otlp]

      processors: [batch]

      exporters: [otlp]

    logs:

      receivers: [otlp]

      processors: [batch]

      exporters: [otlp]

Common Setup Problems

ProblemLikely causeFix
No traces appearing in the backendSampling probability is too lowFor the Spring Boot 4 native starter, set management.tracing.sampling.probability=1.0. For the Java agent or OTel starter, set OTEL_TRACES_SAMPLER=always_on during development
@WithSpan annotation has no effectspring-boot-starter-aop is missing, or the method is called internally within the same classAdd the AOP dependency and ensure the method is called via Spring’s proxy, not directly from within the same bean
Traces appear but have no database spansUsing the OTel community starter without JDBC library instrumentationSwitch to the Java agent for full automatic JDBC instrumentation, or add the opentelemetry-jdbc library instrumentation manually
Spring Boot 4 app has no tracesmanagement.tracing.sampling.probability defaults to 0.1Set it to 1.0 in development. For production, set a value that matches your expected trace volume
Two starters cause bean definition conflictsMixing io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter with org.springframework.boot:spring-boot-starter-opentelemetryUse only one. For Spring Boot 4, use the native starter. For Spring Boot 3.x, use the OTel community starter
Java agent not working with native imageGraalVM ahead-of-time compilation is incompatible with bytecode manipulationUse the OTel community starter or Spring Boot 4 native starter for native image builds
Gradle BOM import not resolving versionsUsing BOM platform import alongside the io.spring.dependency-management pluginUse Gradle’s native platform() import without the Spring dependency management plugin, as the official OTel docs explicitly warn against mixing these

From Traces to Root Cause: Where CubeAPM Fits

Distributed tracing in CubeAPM

Once OpenTelemetry tracing is flowing from your Spring Boot application, the value it delivers depends on what the backend does with the trace data. A trace viewer that shows individual spans is a starting point. What matters in production is whether you can move from a slow span to the infrastructure metric that explains it, the logs from that same request, or the downstream service call that caused the latency.

CubeAPM accepts OTLP directly from your existing OTel Java agent or Spring Boot starter with no additional configuration. It auto-instruments Spring Boot, Hibernate, Tomcat, and Kafka via the OTel Java agent and correlates distributed request traces directly with JVM metrics such as GC pause time, heap usage, and thread activity. When a slow trace appears, CubeAPM links it to the specific database query, downstream service call, or JVM event responsible. It runs self-hosted inside your own infrastructure at $0.15/GB ingestion with no per-user fees, and its smart sampling preserves slow, error-prone, and unusual traces while reducing ingestion volume by up to 80%.

Summary

The Java agent is the right starting point for most Spring Boot applications because it provides the widest automatic library coverage with no code changes. Use the OTel community starter for Spring Boot 3.x applications, where you need native image support or prefer application.properties-based configuration. Use the Spring Boot 4 native starter for Spring Boot 4.x applications and remember that its default 10% sampling rate must be raised during development. Add @WithSpan for declarative span creation on service methods, and use the Tracer API directly when you need programmatic control over span lifecycle.

ApproachSpring Boot versionBest forKey caveat
Java agent v2.28.1AnyMost teams. Zero code changes, widest library coverageDoes not work with GraalVM native image
OTel community starter (BOM 2.28.0)2.6+ and 3.xNative image builds, properties-based configLess automatic coverage than the agent. Requires AOP for @WithSpan
Spring Boot 4 native starter4.x onlySpring Boot 4 applicationsUses management.* config. Default 10% sampling rate. Do not mix with community starter
@WithSpan annotationAny (with AOP)Declarative span creation on service methodsSpring-managed beans only. Requires spring-boot-starter-aop with OTel community starter
Tracer APIAnyProgrammatic spans, exception recording, conditional span creationMore verbose but works in all contexts including internal method calls

Disclaimer : Spring Boot version (4.0.6, April 23, 2026), OTel Java agent version (v2.28.1, confirmed from opentelemetry.io/docs/languages/java last modified May 21, 2026), OTel instrumentation BOM version (2.28.0, from opentelemetry.io/docs/zero-code/java/spring-boot-starter/getting-started last modified April 23, 2026), and Spring Boot 4 native starter behavior are verified against spring.io/blog, opentelemetry.io/docs/zero-code/java/spring-boot-starter, and the opentelemetry-java-instrumentation GitHub releases page as of May 2026.

Also read:

What is the Difference Between Synthetic monitoring and Real User Monitoring (RUM)?

What Are the Best Synthetic Monitoring Tools in 2026? 

What Is Synthetic Monitoring and How Does It Differ From Uptime Monitoring? 

×
×