OpenTelemetry Spring Boot: Java Agent, Starter, and Manual Instrumentation

Vladimir Mihailenco
April 15, 2026
7 min read

This guide covers three ways to add OpenTelemetry to a Spring Boot application: the Java agent for zero-code setup, the OTel Spring Boot Starter for fine-grained control, and the new official starter introduced in Spring Boot 4. For other Java instrumentation options, see the OpenTelemetry Java guide.

Which approach to use

ApproachSpring Boot versionCode changesGraalVM Native
Java AgentAnyNoneNo
OTel Spring Boot Starter (io.opentelemetry.instrumentation)2.6+, 3.xYesYes
Spring Boot 4 Starter (spring-boot-starter-opentelemetry)4.x onlyMinimalPartial

The Java agent is the right default for most applications running on the JVM — attach it at startup and get traces, metrics, and logs with no code changes. Use the OTel Starter when you need GraalVM Native Image support, application.properties-based config, or fine-grained control over instrumentation. Switch to the Spring Boot 4 Starter only when your app is already on Spring Boot 4 — it uses Micrometer internally and exports via OTLP.

Option 1: Java Agent

The Java agent instruments 150+ libraries via bytecode manipulation — Spring MVC, WebFlux, JDBC, JPA, Kafka, gRPC, HTTP clients — without touching your source code.

Download the agent:

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

Configure via environment variables:

Note: The examples below use Uptrace as the OTLP backend. Replace the endpoint and headers with Jaeger, Grafana Tempo, or your own collector.

bash
export OTEL_SERVICE_NAME=spring-app
export OTEL_TRACES_EXPORTER=otlp
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.uptrace.dev:4317
export OTEL_EXPORTER_OTLP_HEADERS=uptrace-dsn=<your_uptrace_dsn>
export OTEL_EXPORTER_OTLP_COMPRESSION=gzip

Replace the endpoint and headers with Jaeger (http://localhost:4317), Grafana Tempo, or any OTLP-compatible backend.

Start with the agent:

bash
java -javaagent:opentelemetry-javaagent.jar -jar your-spring-app.jar

For detailed Java Agent configuration, see the OpenTelemetry Java Agent for Spring Boot guide.

Adding custom spans with @WithSpan

The agent supports @WithSpan and @SpanAttribute when you add opentelemetry-instrumentation-annotations to your dependencies. The annotations use Spring AOP proxies, so they only work on Spring-managed beans and on external method calls — not on internal calls within the same class.

Add the dependency (version managed by the BOM below, or pin manually):

xml
<dependency>
  <groupId>io.opentelemetry.instrumentation</groupId>
  <artifactId>opentelemetry-instrumentation-annotations</artifactId>
</dependency>
java
import io.opentelemetry.instrumentation.annotations.WithSpan;
import io.opentelemetry.instrumentation.annotations.SpanAttribute;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @WithSpan("order.process")
    public Order processOrder(
            @SpanAttribute("order.id") String orderId,
            @SpanAttribute("customer.id") String customerId) {
        return repository.findAndProcess(orderId, customerId);
    }
}

@WithSpan creates a child span named "order.process" with order.id and customer.id as attributes. Disable with otel.instrumentation.annotations.enabled=false.

Option 2: OTel Spring Boot Starter

The OTel Spring Boot Starter (opentelemetry-spring-boot-starter from io.opentelemetry.instrumentation) integrates with Spring's auto-configuration system and supports application.properties/application.yml configuration. It works with Spring Boot 2.6+ and 3.x and is the only option for GraalVM Native Image compilation.

Add the BOM and starter:

Maven — import the BOM before spring-boot-dependencies:

xml
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>io.opentelemetry.instrumentation</groupId>
      <artifactId>opentelemetry-instrumentation-bom</artifactId>
      <version>2.26.1</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:

gradle
dependencies {
  implementation platform('io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom:2.26.1')
  implementation 'io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter'
}

application.yml configuration:

Note: The examples below use Uptrace as the OTLP backend. Replace the endpoint and headers with Jaeger, Grafana Tempo, or your own collector.

yaml
spring:
  application:
    name: spring-app

otel:
  exporter:
    otlp:
      protocol: grpc
      endpoint: https://api.uptrace.dev:4317
      headers:
        uptrace-dsn: <your_uptrace_dsn>
      compression: gzip
  traces:
    sampler: parentbased_traceidratio
    sampler.arg: "0.1"
  resource:
    attributes:
      service.version: 1.0.0
      deployment.environment: production

The starter instruments Spring MVC, WebFlux, JDBC, R2DBC, Kafka, and MongoDB automatically. Log correlation via Logback MDC is enabled by default — trace and span IDs appear in your log output without any logback.xml changes.

GraalVM Native Image

The Java agent does not work with GraalVM Native Image compilation. Use opentelemetry-spring-boot-starter with the GraalVM build tools instead:

gradle
plugins {
  id 'org.graalvm.buildtools.native' version '0.10.4'
  id 'org.springframework.boot' version '3.5.0'
}
bash
./gradlew nativeCompile
./build/native/nativeCompile/your-app

Native images start in milliseconds and have significantly lower memory footprint than JVM deployments.

@WithSpan with the OTel Starter

With the OTel Starter, @WithSpan requires spring-boot-starter-aop in addition to opentelemetry-instrumentation-annotations. The setup and limitations are the same as with the Java agent: Spring-managed beans only, no internal method calls.

xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Option 3: Spring Boot 4 Starter

Spring Boot 4 (current stable: 4.0.5 as of March 2026) ships with its own official starter spring-boot-starter-opentelemetry. This is a different library from the community OTel starter above. It uses Micrometer internally and exports traces and metrics via OTLP, so configuration properties follow the management.* prefix rather than otel.*.

xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-opentelemetry</artifactId>
</dependency>

application.properties:

properties
# Tracing
management.opentelemetry.resource-attributes.service.name=spring-app
management.opentelemetry.tracing.export.otlp.endpoint=http://localhost:4318/v1/traces
management.tracing.sampling.probability=0.1

# Metrics via Micrometer OTLP registry
management.otlp.metrics.export.url=http://localhost:4318/v1/metrics
management.otlp.metrics.export.enabled=true

The key difference from Options 1 and 2: instrumentation goes through Micrometer's Observation API. You get @Observed for custom spans instead of @WithSpan. It's the right path for teams already on Spring Boot 4 who want a single-dependency setup without pulling in the community OTel BOM.

Production configuration

At high request volume, sampling 100% of traces is too expensive. Both the Java agent and OTel Starter support parentbased_traceidratio — it samples 10% of new root traces while respecting upstream sampling decisions so distributed traces stay coherent.

Java agent (environment variables):

bash
export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1
export OTEL_BSP_MAX_EXPORT_BATCH_SIZE=512
export OTEL_BSP_SCHEDULE_DELAY=5000

OTel Starter (application.properties):

properties
otel.traces.sampler=parentbased_traceidratio
otel.traces.sampler.arg=0.1
otel.bsp.max.export.batch.size=512
otel.bsp.schedule.delay=5000

Adjust SAMPLER_ARG based on traffic volume. For high-throughput services, 0.01 (1%) is reasonable. For critical paths like payment flows, keep sampling higher or use a head-based sampler that always traces specific routes.

Manual instrumentation

For business logic that needs custom spans or metrics beyond what auto-instrumentation captures, inject the OpenTelemetry bean (available with both the OTel Starter and Spring Boot 4 Starter):

java
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.trace.StatusCode;
import org.springframework.stereotype.Service;

@Service
public class PaymentService {

    private final Tracer tracer;

    public PaymentService(OpenTelemetry openTelemetry) {
        this.tracer = openTelemetry.getTracer("com.example.payments", "1.0.0");
    }

    public PaymentResult charge(String orderId, double amount) {
        var span = tracer.spanBuilder("payment.charge")
            .setAttribute("order.id", orderId)
            .setAttribute("payment.amount", amount)
            .startSpan();

        try (var scope = span.makeCurrent()) {
            var result = paymentGateway.charge(orderId, amount);
            span.setAttribute("payment.status", result.getStatus());
            span.setStatus(StatusCode.OK);
            return result;
        } catch (Exception e) {
            span.recordException(e);
            span.setStatus(StatusCode.ERROR, e.getMessage());
            throw e;
        } finally {
            span.end();
        }
    }
}

With the Java agent, inject OpenTelemetry via GlobalOpenTelemetry.get() instead of constructor injection, since the agent registers the SDK globally at startup.

Troubleshooting

No telemetry data appearing

Enable debug logging to confirm the agent or SDK is initializing and the exporter is connecting:

bash
# Java Agent
export OTEL_JAVAAGENT_DEBUG=true

# OTel Starter (application.properties)
logging.level.io.opentelemetry=DEBUG

Check that the OTLP endpoint is reachable: curl -v http://your-collector:4318/v1/traces.

Slow startup with the Java agent

The agent inspects all loaded classes at startup. Disable instrumentation for libraries you don't use:

bash
export OTEL_INSTRUMENTATION_COMMON_DEFAULT_ENABLED=false
export OTEL_INSTRUMENTATION_SPRING_WEBMVC_ENABLED=true
export OTEL_INSTRUMENTATION_JDBC_ENABLED=true

Or switch to the OTel Starter, which has a smaller startup overhead.

Broken distributed traces across services

Verify that both services use the W3C Trace Context propagator (default) and that your HTTP client is instrumented. RestTemplate and WebClient are both instrumented automatically by the agent and OTel Starter.

Duplicate metrics when using Spring Boot 4 Starter

If you have both the Micrometer OTLP registry and the OTel bridge active, metrics will be exported twice. Disable the Micrometer registry export when using the bridge:

properties
management.otlp.metrics.export.enabled=false

What is Uptrace?

Uptrace is an OpenTelemetry APM that supports distributed tracing, metrics, and logs. You can use it to monitor applications and troubleshoot issues.

Uptrace comes with an intuitive query builder, rich dashboards, alerting rules with notifications, and integrations for most languages and frameworks. It can process billions of spans on a single server at a fraction of the cost of hosted alternatives.

Try it via the cloud demo (no login required) or run it locally with Docker. Source code on GitHub.

FAQ

  1. Does the Java agent work with Spring Boot 3? Yes. The Java agent works with any Spring Boot version on the JVM. Download the latest opentelemetry-javaagent.jar and attach it with -javaagent. No code changes required.
  2. Does @WithSpan work with the Java agent? Yes, with an additional dependency. Add io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations to your project. The agent processes the annotation at runtime. The OTel Starter also requires this dependency plus spring-boot-starter-aop.
  3. What is the difference between the OTel Starter and the Spring Boot 4 Starter? The OTel Starter (opentelemetry-spring-boot-starter from io.opentelemetry.instrumentation) is a community library supporting Spring Boot 2.6+ and 3.x, configured via otel.* properties. The Spring Boot 4 Starter (spring-boot-starter-opentelemetry, groupId org.springframework.boot) ships with Spring Boot 4 itself, uses Micrometer internally, and is configured via management.* properties. They are different implementations — don't mix them.
  4. Does OpenTelemetry support GraalVM Native Image? The Java agent doesn't support native compilation. Use opentelemetry-spring-boot-starter (OTel Starter) from io.opentelemetry.instrumentation — it's tested for native builds with Spring Boot 3.x and the GraalVM build tools plugin.
  5. How do I reduce trace volume in production? Use parentbased_traceidratio sampler with a value below 1.0. At 0.1, 10% of new root traces are sampled while the sampling decision from upstream services is respected. See the Production configuration section for exact properties per setup.

What's next