OpenTelemetry Spring Boot: Java Agent, Starter, and Manual Instrumentation
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
| Approach | Spring Boot version | Code changes | GraalVM Native |
|---|---|---|---|
| Java Agent | Any | None | No |
OTel Spring Boot Starter (io.opentelemetry.instrumentation) | 2.6+, 3.x | Yes | Yes |
Spring Boot 4 Starter (spring-boot-starter-opentelemetry) | 4.x only | Minimal | Partial |
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:
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.
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:
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):
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-instrumentation-annotations</artifactId>
</dependency>
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:
<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:
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.
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:
plugins {
id 'org.graalvm.buildtools.native' version '0.10.4'
id 'org.springframework.boot' version '3.5.0'
}
./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.
<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.*.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-opentelemetry</artifactId>
</dependency>
application.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):
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):
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):
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:
# 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:
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:
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
- 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.jarand attach it with-javaagent. No code changes required. - Does @WithSpan work with the Java agent? Yes, with an additional dependency. Add
io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotationsto your project. The agent processes the annotation at runtime. The OTel Starter also requires this dependency plusspring-boot-starter-aop. - What is the difference between the OTel Starter and the Spring Boot 4 Starter? The OTel Starter (
opentelemetry-spring-boot-starterfromio.opentelemetry.instrumentation) is a community library supporting Spring Boot 2.6+ and 3.x, configured viaotel.*properties. The Spring Boot 4 Starter (spring-boot-starter-opentelemetry, groupIdorg.springframework.boot) ships with Spring Boot 4 itself, uses Micrometer internally, and is configured viamanagement.*properties. They are different implementations — don't mix them. - Does OpenTelemetry support GraalVM Native Image? The Java agent doesn't support native compilation. Use
opentelemetry-spring-boot-starter(OTel Starter) fromio.opentelemetry.instrumentation— it's tested for native builds with Spring Boot 3.x and the GraalVM build tools plugin. - How do I reduce trace volume in production? Use
parentbased_traceidratiosampler with a value below1.0. At0.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
- OpenTelemetry Java guide — full Java SDK documentation
- Java Agent for Spring Boot — detailed agent configuration
- OpenTelemetry Logback guide — log correlation setup
- OpenTelemetry vs Micrometer — when to use which
- OpenTelemetry Quarkus — Quarkus with native image support