Skip to main content

Spring Boot

Spring Boot is one of the most widely adopted Java frameworks for building enterprise microservices and web applications. However, understanding application performance, identifying bottlenecks, and troubleshooting issues in distributed Spring Boot environments can be challenging without proper observability. OpenTelemetry provides automatic instrumentation for Spring Boot applications, capturing distributed traces across HTTP requests, database calls, message queues, and external API interactions. With base14 Scout's OpenTelemetry integration, you gain complete visibility into your Spring Boot application's performance with minimal code changes and production-ready configuration.

Spring Boot is the dominant JVM framework. Quarkus and Micronaut are compile-time-optimized alternatives, and Ktor is the Kotlin-first option.

This comprehensive guide demonstrates how to instrument Spring Boot applications using OpenTelemetry, covering everything from basic setup to advanced production scenarios. You'll learn how to automatically capture traces from Spring MVC controllers, Spring Data JPA repositories, RestTemplate and WebClient calls, Kafka consumers, and more. The OpenTelemetry Spring Boot starter provides zero-code instrumentation for most common libraries, while also offering APIs for custom instrumentation when you need fine-grained control over spans and attributes. Integration with Spring Boot Actuator enables health checks and metrics export, making it production-ready from day one.

Whether you're a Java developer adding observability to a new microservice, a DevOps engineer standardizing APM across Spring Boot services, or a platform team implementing OpenTelemetry organization-wide, this guide provides practical examples for every scenario. You'll find solutions for common pain points like context propagation across async boundaries, instrumenting legacy Spring applications, securing sensitive data in traces, optimizing performance overhead, and troubleshooting missing spans. By the end of this guide, you'll have a fully instrumented Spring Boot application sending rich telemetry data to base14 Scout, enabling fast root cause analysis and proactive performance optimization.

TL;DR

Add the opentelemetry-spring-boot-starter dependency to your pom.xml or build.gradle and set otel.exporter.otlp.endpoint in application.properties - the starter auto-instruments Spring MVC, Spring Data JPA, RestTemplate, WebClient, and Kafka with zero code changes. For fine-grained control over specific operations, inject Tracer via the OpenTelemetry API and use startActiveSpan to add custom spans alongside the automatic instrumentation.

Running this in production

Storing and querying this data at production volume is what base14 Scout does. Check out Scout APM.

Overview​

This guide demonstrates how to:

  • Set up OpenTelemetry instrumentation for Spring Boot applications
  • Configure automatic request and response tracing
  • Instrument Spring MVC, Spring Data, and other Spring components
  • Implement custom instrumentation for business logic
  • Collect traces, metrics, and logs
  • Secure sensitive data in telemetry
  • Optimize performance overhead in production
  • Troubleshoot common instrumentation issues
  • Export telemetry data to base14 Scout Collector

Who This Guide Is For​

This guide is designed for multiple roles working with Spring Boot applications:

  • Java Developers building new Spring Boot microservices who want to add observability from the start, understand performance characteristics of their code, and debug issues faster with distributed tracing.

  • DevOps Engineers responsible for deploying and monitoring Spring Boot applications in production environments, who need to standardize APM tooling, configure exporters, and ensure observability across the entire infrastructure.

  • Platform Teams implementing organization-wide OpenTelemetry standards for Java applications, who need to create reusable configuration patterns and integrate with existing observability stacks.

  • Site Reliability Engineers (SREs) who troubleshoot production incidents involving Spring Boot services, need to quickly identify performance bottlenecks, trace requests across distributed systems, and maintain service level objectives (SLOs).

  • Application Architects making technology decisions for microservices platforms, evaluating observability solutions, and designing systems that are observable by default with proper instrumentation patterns.

Prerequisites​

Before implementing OpenTelemetry instrumentation, ensure you have:

  • A Spring Boot application (2.6+, 3.1+ or 4.x)
  • Java Development Kit (JDK) installed
  • Maven 3.6+ or Gradle 7.6+ build tool
  • Access to base14 Scout Collector endpoint (see Collector Setup)
  • Basic understanding of OpenTelemetry concepts (spans, traces, exporters)

Compatibility​

Minimum requirements:

  • Java: 8+ for Spring Boot 2.x, 17+ for Spring Boot 3 and 4 (Java 25 LTS is supported)
  • Spring Boot: 2.6+, 3.1+ or 4.x. Spring Boot 4 needs starter 2.23.0 or later
  • Maven/Gradle: Any recent version

Version details:

OpenTelemetry library versions
ComponentMinimumRecommended
OpenTelemetry Java SDK1.32.01.67+
OpenTelemetry Spring Boot Starter2.1.02.33.0
OpenTelemetry Java Agent1.32.02.33+

Java 25​

Java 25 is an LTS release and works with both the starter and the agent. The spring-boot-java25-postgresql example runs on Java 25 with Spring Boot 4.1.2 and starter 2.33.0.

Choosing Your Approach​

Spring Boot offers three distinct approaches for OpenTelemetry integration. This section helps you choose the right one for your project and understand the trade-offs.

📌 Looking for alternative approaches?

This guide focuses on the OpenTelemetry SDK Integration (recommended for production). For Java Agent (zero-code) and Spring Boot 4 Native Starter approaches, see Spring Boot Alternatives.

OpenTelemetry SDK Integration (Our Recommendation) ⭐​

The community-maintained opentelemetry-spring-boot-starter provides direct OpenTelemetry SDK integration with full control and broad version support. This is our recommended approach for production deployments.

How it works:

  • Add Maven/Gradle dependencies to your pom.xml or build.gradle
  • Configure via application.yml or application.properties
  • Automatic instrumentation for ~30 common libraries
  • Full OpenTelemetry API access for custom spans and metrics
  • Works with Spring Boot 2.6+, 3.1+ and 4.x

Architecture:

  • Direct OpenTelemetry API usage (no abstraction layer)
  • Full access to OpenTelemetry SDK configuration
  • Explicit dependency management via BOM
  • Spring Boot native configuration support
  • Production-proven across thousands of deployments

Best for:

  • Production deployments - Stable, battle-tested approach
  • GraalVM native-image - Full support for native compilation
  • Spring Boot configuration - Use familiar application.yml patterns
  • Custom instrumentation - Full OpenTelemetry API access for business logic
  • Advanced use cases - Custom span processors and exporters
  • Multi-framework - Using OpenTelemetry across Node.js, Go, Python, etc.
  • Version control - Explicit dependency management without bytecode issues

Limitations:

  • More complex setup than Java Agent (requires BOM and dependencies)
  • Requires understanding OpenTelemetry concepts (spans, traces, exporters)
  • Fewer auto-instrumented libraries (30+) compared to Java Agent (150+)
  • Manual configuration needed for advanced scenarios

Java Agent (Zero-Code Alternative)​

The OpenTelemetry Java agent provides zero-code automatic instrumentation using bytecode manipulation at JVM startup. Best for legacy applications where code changes aren't possible, or for quick proof-of-concept testing.

Key characteristics:

  • Download single JAR, attach with -javaagent flag
  • No dependencies or code changes required
  • Automatically instruments 150+ libraries
  • Environment variable configuration only

See the Java Agent guide for complete setup instructions, Docker/Kubernetes examples, and limitations.

Spring Boot 4 Native Starter​

Spring Boot 4 ships its own spring-boot-starter-opentelemetry. It exports Micrometer observations over OTLP and is configured with management.* properties instead of otel.*.

Key characteristics:

  • Single dependency, managed by Spring Boot 4
  • Micrometer Observation and Tracing APIs for custom instrumentation
  • Configured with management.opentelemetry.* and management.otlp.metrics.export.* properties
  • Application logs are not exported over OTLP without extra setup

See the Spring Boot 4 guide for setup instructions and migration guide from OpenTelemetry SDK Integration.

Quick Comparison​

FeatureOpenTelemetry SDK Integration ⭐Java AgentSpring Boot 4 Native
Production Ready✅ Stable✅ Stable✅ Stable (Boot 4 only)
SetupAdd dependenciesAttach JAR at startupAdd single dependency
Code ChangesOptionalNone requiredOptional
Configurationapplication.yml or codeEnvironment variables onlyapplication.yml or code
Custom InstrumentationFull OpenTelemetry APIAnnotations onlyMicrometer API
GraalVM Native✅ Supported❌ Not supported✅ Supported
Auto-instrumentation~30 libraries150+ librariesVia Micrometer
Best ForProduction apps, full controlLegacy apps, zero-codeMicrometer-first teams
Our Recommendation✅ RecommendedAlternativeAlternative

Decision Guide​

Choose OpenTelemetry SDK Integration (Recommended for Production) when:

  • GraalVM native-image: Compiling to native binary
  • Spring Boot configuration: Want to use application.yml for OTEL config
  • Custom instrumentation: Need full OpenTelemetry API access for business logic
  • Version flexibility: Want explicit control over dependency versions
  • Custom span processors: Need advanced SDK customization (processors, exporters)
  • Multi-framework: Using OpenTelemetry across Node.js, Go, Python, etc.
  • Version range: Spring Boot 2.6+, 3.1+ and 4.x
  • Latest features: Want immediate access to new OpenTelemetry capabilities
  • Debugging preference: Prefer explicit dependencies over bytecode manipulation

Choose Java Agent (Zero-Code Alternative) when:

  • Zero-code requirement: Cannot or don't want to modify application code/dependencies
  • Legacy applications: Modifying code is difficult or risky
  • Maximum auto-instrumentation: Need coverage for 150+ libraries out-of-the-box
  • Cross-stack standardization: Using OpenTelemetry across multiple languages
  • Quick evaluation: Want to try OpenTelemetry without code changes
  • Operational control: Ops team manages instrumentation separately from development
  • Not using GraalVM: No native-image compilation requirements
  • Simple Spring Boot apps: Standard libraries, no version compatibility concerns

Choose Spring Boot 4 Native Starter when:

  • Spring Boot 4: The application already runs on Spring Boot 4
  • Micrometer standardization: Team already uses Micrometer abstractions
  • Spring-native conventions: Want Spring Boot auto-configuration patterns
  • Simple setup: Don't need advanced SDK customization
  • Vendor-neutral API: Micrometer can export to multiple backends

This Guide's Coverage​

This guide focuses on the OpenTelemetry SDK Integration approach with comprehensive examples for production use:

  • Complete setup with Maven and Gradle
  • Configuration via application.yml, properties, and programmatic
  • Production deployment with Docker, Kubernetes
  • Framework-specific features (Spring MVC, JPA, RestTemplate, WebClient)
  • Custom instrumentation with full OpenTelemetry API
  • Security, performance, and troubleshooting guidance

Why OpenTelemetry SDK Integration is our recommendation:

  • Production Ready: Stable, well-tested, and widely deployed
  • Broad Compatibility: Works with Spring Boot 2.6+, 3.1+ and 4.x
  • Maximum Flexibility: Full SDK control for complex scenarios
  • GraalVM Support: Native-image compilation compatible
  • Spring Configuration: Uses familiar application.yml patterns

💡 Alternative Approaches: For Java Agent (zero-code) or Spring Boot 4 Native Starter, see Spring Boot Alternatives.

Setup​

pom.xml
<properties>
<!-- Overrides the OpenTelemetry version that Spring Boot manages -->
<opentelemetry.version>1.67.0</opentelemetry.version>
<opentelemetry.instrumentation.version>2.33.0</opentelemetry.instrumentation.version>
</properties>

<dependencyManagement>
<dependencies>
<!-- OpenTelemetry Instrumentation BOM -->
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-instrumentation-bom</artifactId>
<version>${opentelemetry.instrumentation.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>

<!-- OpenTelemetry -->
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-spring-boot-starter</artifactId>
</dependency>
</dependencies>

On Spring Boot 2.x and 3.x, use spring-boot-starter-web instead of spring-boot-starter-webmvc. Do not add micrometer-tracing-bridge-otel next to the starter: on Spring Boot 3.x it creates a second HTTP server span for each request, such as http get /users/.

Configuration​

OpenTelemetry can be configured for Spring Boot applications in multiple ways. Choose the approach that best fits your deployment model.

Recommended for production - Maximum flexibility across environments:

.env
# Service identification
OTEL_SERVICE_NAME=your-service-name
OTEL_RESOURCE_ATTRIBUTES=service.namespace=your-namespace,environment=development

# OTLP Exporter configuration
OTEL_EXPORTER_OTLP_ENDPOINT=http://scout-collector:4318
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_TRACES_EXPORTER=otlp
OTEL_METRICS_EXPORTER=otlp
OTEL_LOGS_EXPORTER=otlp

# Instrumentation control
OTEL_INSTRUMENTATION_SPRING_WEBMVC_ENABLED=true
OTEL_INSTRUMENTATION_JDBC_ENABLED=true

# Semantic convention stability
OTEL_SEMCONV_STABILITY_OPT_IN=http,database

On the OpenTelemetry Java instrumentation 2.x line, HTTP server spans already use the stable conventions by default (http.request.method, http.response.status_code, url.path, http.route), so the http token is effectively a no-op. The database token is what does the work: with it set, JDBC spans emit the stable database conventions (db.query.text, db.system.name, db.namespace, db.query.summary) instead of the legacy db.statement / db.system. Both tokens are kept here so HTTP and database telemetry stay on the stable names together.

All configuration approaches export logs, traces, and metrics to the base14 Scout observability backend.

Traces​

Traces follow a request through your Spring Boot application, from the Spring MVC controller that receives it, through service beans, JDBC queries, RestTemplate / WebClient calls, and Kafka or RabbitMQ messages, and back out as the response.

Automatic Trace Collection​

With the OpenTelemetry Java agent or the Spring Boot starter attached, every request is traced with no per-controller code:

Captured Information:

  • HTTP method, route template, and status code for each Spring MVC endpoint
  • Request duration and a span-by-span timing breakdown
  • JDBC queries (Hibernate, Spring Data JPA, JdbcTemplate), including the SQL
  • Outbound RestTemplate, WebClient, and Feign calls
  • Kafka, RabbitMQ, and JMS producer/consumer spans
  • Exceptions recorded on the failing span with stack traces
  • Distributed context propagation across services (W3C Trace Context)

Trace Hierarchy:

HTTP Request Span (root: GET /api/users/{id})
├── UserController.getUser Span
│ ├── UserService.findById Span
│ │ └── JDBC Query Span (SELECT ... FROM users)
│ └── RestTemplate Span (GET inventory-service)
└── Kafka Producer Span (user.viewed event)

Key Tracing Features​

  • Automatic HTTP tracking: every controller route is traced with no code changes
  • JDBC and ORM visibility: Hibernate and Spring Data queries appear as child spans with the executed SQL
  • Error capturing: thrown exceptions and @ExceptionHandler paths are recorded with full stack traces
  • Context propagation: distributed traces follow requests across HTTP and messaging boundaries
  • Async support: @Async methods, CompletableFuture, and reactive WebFlux pipelines stay correctly parented

View traces in your base14 Scout dashboard to follow request flows and find the slow span in a chain.

Reference​

Official Traces Documentation

Metrics​

Metrics aggregate runtime measurements over time, such as request rate, latency distributions, JVM health, and connection-pool saturation. Where traces explain a single request, metrics power dashboards and alerts across all of them.

Automatic Metrics​

The OpenTelemetry Java agent or Spring Boot starter captures a broad set of metrics without code, exported via the OTLP metrics exporter you configured above (otel.metrics.exporter=otlp):

  • JVM runtime: heap and non-heap usage, GC pause time, thread counts, class loading
  • HTTP server: request count and http.server.request.duration per route and status
  • JDBC / HikariCP: connection-pool size, active and idle connections, wait time
  • System: process CPU and memory

Custom Metrics with Micrometer​

Spring Boot apps already ship with Micrometer, and the OpenTelemetry Spring Boot starter can bridge Micrometer's MeterRegistry to OpenTelemetry. The bridge is off by default. Keep Spring Boot Actuator on the classpath and enable it with otel.instrumentation.micrometer.enabled=true, then inject MeterRegistry:

src/main/java/com/example/metrics/OrderMetrics.java
package com.example.metrics;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Service;

@Service
public class OrderMetrics {

private final Counter ordersPlaced;

public OrderMetrics(MeterRegistry registry) {
this.ordersPlaced = Counter.builder("orders.placed")
.description("Total number of orders placed")
.register(registry);
}

public void recordOrder() {
ordersPlaced.increment();
}
}

View metrics in your base14 Scout dashboard to chart request rate, latency percentiles, JVM health, and custom business counters.

Reference​

Official Metrics Documentation

Production Configuration​

Production environments require careful configuration for performance, reliability, and cost optimization.

Batch Span Processor Configuration​

Use BatchSpanProcessor for optimal production performance:

src/main/java/com/example/config/ProductionTracingConfig.java
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
@Profile("production")
public class ProductionTracingConfig {

@Bean
public BatchSpanProcessor batchSpanProcessor(OtlpHttpSpanExporter exporter) {
return BatchSpanProcessor.builder(exporter)
.setMaxQueueSize(2048)
.setMaxExportBatchSize(512)
.setScheduleDelay(Duration.ofSeconds(5))
.setExporterTimeout(Duration.ofSeconds(30))
.build();
}
}

Resource Attributes for Production​

Add comprehensive resource attributes for better observability:

application-prod.properties
otel.resource.attributes=\
service.name=payment-service,\
service.namespace=production,\
service.version=1.2.3,\
service.instance.id=${HOSTNAME},\
environment=demo,\
deployment.region=us-east-1,\
cloud.provider=aws,\
cloud.platform=aws_eks,\
k8s.cluster.name=prod-cluster,\
k8s.namespace.name=payments,\
k8s.pod.name=${HOSTNAME}

Docker Configuration​

Configure OpenTelemetry for containerized Spring Boot applications:

Dockerfile
FROM eclipse-temurin:25-jre-jammy

WORKDIR /app

# Copy application JAR
COPY target/my-service-1.0.0.jar app.jar

# Set OpenTelemetry environment variables
ENV OTEL_SERVICE_NAME=my-service
ENV OTEL_EXPORTER_OTLP_ENDPOINT=http://scout-collector:4318
ENV OTEL_RESOURCE_ATTRIBUTES=service.namespace=production

# Run application
ENTRYPOINT ["java", "-jar", "app.jar"]

Docker Compose Example​

docker-compose.yml
version: "3.8"

services:
app:
build: .
ports:
- "8080:8080"
environment:
OTEL_SERVICE_NAME: payment-service
OTEL_EXPORTER_OTLP_ENDPOINT: http://scout-collector:4318
OTEL_RESOURCE_ATTRIBUTES: environment=development
depends_on:
- scout-collector

scout-collector:
image: base14/scout-collector:latest
ports:
- "4318:4318"

Kubernetes Deployment​

k8s-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-boot-app
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: spring-boot-app
template:
metadata:
labels:
app: spring-boot-app
spec:
containers:
- name: app
image: myregistry/spring-boot-app:1.2.3
ports:
- containerPort: 8080
env:
- name: OTEL_SERVICE_NAME
value: "spring-boot-app"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "http://scout-collector.observability.svc.cluster.local:4318"
- name: OTEL_RESOURCE_ATTRIBUTES
value: "environment=demo,k8s.cluster.name=prod-cluster"
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace

Health Check Endpoint​

Implement a health check that verifies telemetry export:

src/main/java/com/example/controller/HealthController.java
package com.example.controller;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Tracer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HealthController {

@Autowired
private OpenTelemetry openTelemetry;

@GetMapping("/health/telemetry")
public ResponseEntity<String> checkTelemetry() {
try {
Tracer tracer = openTelemetry.getTracer("health-check");
// Test span creation
tracer.spanBuilder("health-check-test").startSpan().end();
return ResponseEntity.ok("Telemetry OK");
} catch (Exception e) {
return ResponseEntity.status(500).body("Telemetry Error: " + e.getMessage());
}
}
}

Framework-Specific Features​

OpenTelemetry automatically instruments many Spring Boot components. Here's how instrumentation works for common Spring features.

Spring MVC REST Controllers​

REST controllers are automatically instrumented:

src/main/java/com/example/controller/UserController.java
package com.example.controller;

import org.springframework.web.bind.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;

@RestController
@RequestMapping("/api/users")
public class UserController {

@Autowired
private UserService userService;

// Automatically creates span: "GET /api/users/{id}"
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}

// Automatically creates span: "POST /api/users"
@PostMapping
public User createUser(@RequestBody User user) {
return userService.save(user);
}
}

Each HTTP request creates a parent span with attributes like http.request.method, http.route, http.response.status_code, and url.full.

Spring Data JPA Repositories​

Database queries via Spring Data JPA are automatically traced:

src/main/java/com/example/repository/UserRepository.java
package com.example.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

public interface UserRepository extends JpaRepository<User, Long> {

// Automatically creates span: "SELECT User"
User findByEmail(String email);

// Automatically creates span with SQL query
@Query("SELECT u FROM User u WHERE u.active = true")
List<User> findActiveUsers();
}

JDBC instrumentation captures:

  • SQL statements (parameterized)
  • Database connection details
  • Query execution time
  • Connection pool metrics

RestTemplate and WebClient​

Outgoing HTTP calls are automatically instrumented with distributed trace context propagation:

src/main/java/com/example/service/ExternalApiService.java
package com.example.service;

import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@Service
public class ExternalApiService {

private final RestTemplate restTemplate;
private final WebClient webClient;

public ExternalApiService(RestTemplate restTemplate, WebClient.Builder webClientBuilder) {
this.restTemplate = restTemplate;
this.webClient = webClientBuilder.baseUrl("https://api.example.com").build();
}

// Automatically creates span: "GET https://api.example.com/data"
public String fetchDataSync() {
return restTemplate.getForObject("https://api.example.com/data", String.class);
}

// Automatically creates span with async context propagation
public Mono<String> fetchDataAsync() {
return webClient.get()
.uri("/data")
.retrieve()
.bodyToMono(String.class);
}
}

Spring Boot Actuator Integration​

Integrate OpenTelemetry metrics with Spring Boot Actuator:

src/main/java/com/example/config/ActuatorConfig.java
package com.example.config;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.metrics.Meter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ActuatorConfig {

@Bean
public Meter meter(OpenTelemetry openTelemetry) {
return openTelemetry.getMeter("actuator-metrics");
}
}
application.properties
# Expose Actuator endpoints
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always

Custom Instrumentation​

While auto-instrumentation covers most use cases, custom instrumentation allows fine-grained control over spans and attributes.

💡 Java Agent Users: Add io.opentelemetry:opentelemetry-api as a dependency and the code in this section works with the agent too; the agent supplies the implementation at runtime. For @WithSpan annotation examples, see Custom Instrumentation with Agent.

💡 Spring Boot 4 Native Starter Users: Use Micrometer APIs (io.micrometer.tracing.*) instead of OpenTelemetry APIs (io.opentelemetry.api.*). See the Spring Boot 4 guide for API comparisons.

Manual Span Creation​

Create custom spans for business logic:

src/main/java/com/example/service/PaymentService.java
package com.example.service;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.context.Scope;
import org.springframework.stereotype.Service;

@Service
public class PaymentService {

private final Tracer tracer;

public PaymentService(OpenTelemetry openTelemetry) {
this.tracer = openTelemetry.getTracer("payment-service");
}

public PaymentResult processPayment(PaymentRequest request) {
Span span = tracer.spanBuilder("process_payment")
.setAttribute("payment.method", request.getMethod())
.setAttribute("payment.amount", request.getAmount())
.setAttribute("payment.currency", request.getCurrency())
.startSpan();

try (Scope scope = span.makeCurrent()) {
// Business logic
validatePayment(request);
PaymentResult result = chargeCustomer(request);

span.setAttribute("payment.transaction_id", result.getTransactionId());
span.setStatus(StatusCode.OK);

return result;
} catch (PaymentException e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, "Payment failed");
throw e;
} finally {
span.end();
}
}
}

Adding Span Attributes​

Enrich spans with business-specific attributes:

src/main/java/com/example/service/OrderService.java
package com.example.service;

import io.opentelemetry.api.trace.Span;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

public Order createOrder(OrderRequest request) {
Span currentSpan = Span.current();

// Add custom attributes to current span
currentSpan.setAttribute("order.user_id", request.getUserId());
currentSpan.setAttribute("order.item_count", request.getItems().size());
currentSpan.setAttribute("order.total_value", request.getTotalValue());
currentSpan.setAttribute("order.payment_method", request.getPaymentMethod());

// Business logic
Order order = saveOrder(request);

currentSpan.setAttribute("order.id", order.getId());
currentSpan.addEvent("Order created successfully");

return order;
}
}

Exception Handling and Error Recording​

Properly record exceptions in spans:

src/main/java/com/example/service/UserService.java
package com.example.service;

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import org.springframework.stereotype.Service;

@Service
public class UserService {

public User findUserById(Long id) {
Span span = Span.current();

try {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));

span.setAttribute("user.id", user.getId());
span.setAttribute("user.role", user.getRole());

return user;
} catch (UserNotFoundException e) {
// Record exception with full stack trace
span.recordException(e);
span.setStatus(StatusCode.ERROR, "User not found");
throw e;
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, "Unexpected error");
throw new ServiceException("Failed to find user", e);
}
}
}

Async Operation Instrumentation​

Instrument async operations with proper context propagation:

src/main/java/com/example/service/AsyncService.java
package com.example.service;

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Context;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;

@Service
public class AsyncService {

private final Tracer tracer;

public AsyncService(OpenTelemetry openTelemetry) {
this.tracer = openTelemetry.getTracer("async-service");
}

@Async
public CompletableFuture<String> processAsync(String input) {
// Capture current context
Context context = Context.current();

return CompletableFuture.supplyAsync(() -> {
// Restore context in async thread
try (var scope = context.makeCurrent()) {
Span span = tracer.spanBuilder("async_processing")
.setAttribute("input.length", input.length())
.startSpan();

try (var spanScope = span.makeCurrent()) {
String result = performWork(input);
span.setStatus(StatusCode.OK);
return result;
} finally {
span.end();
}
}
});
}
}

Custom Metrics​

Create custom business metrics:

💡 Spring Boot 4 Native Starter: Use Micrometer's MeterRegistry for metrics instead of OpenTelemetry's Meter. Example: meterRegistry.counter("orders.created").increment(). The metrics are automatically exported via OTLP.

src/main/java/com/example/metrics/BusinessMetrics.java
package com.example.metrics;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.LongCounter;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.api.metrics.LongHistogram;
import org.springframework.stereotype.Component;

@Component
public class BusinessMetrics {

private final LongCounter orderCounter;
private final LongHistogram orderValueHistogram;
private final Meter meter;

public BusinessMetrics(OpenTelemetry openTelemetry) {
this.meter = openTelemetry.getMeter("business-metrics");

this.orderCounter = meter.counterBuilder("orders.created")
.setDescription("Total number of orders created")
.build();

this.orderValueHistogram = meter.histogramBuilder("order.value")
.setDescription("Distribution of order values")
.ofLongs()
.build();
}

public void recordOrder(Order order) {
orderCounter.add(1, Attributes.builder()
.put("order.status", order.getStatus())
.put("order.payment_method", order.getPaymentMethod())
.build());

orderValueHistogram.record(order.getTotalValue(), Attributes.builder()
.put("order.currency", order.getCurrency())
.build());
}
}

Running Your Application​

Choose the deployment method that matches your environment:

Run your Spring Boot application locally with OpenTelemetry:

# Set environment variables
export OTEL_SERVICE_NAME=my-service-dev
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318

# Run with Maven
mvn spring-boot:run

# Or run with Gradle
./gradlew bootRun

Troubleshooting​

Common Issues and Solutions​

1. No Traces Appearing in Scout​

Symptoms: Application runs but no traces visible in base14 Scout.

Solutions:

# Verify collector endpoint is reachable
curl -v http://scout-collector:4318/health

# Enable debug logging
export OTEL_LOG_LEVEL=debug
export LOGGING_LEVEL_IO_OPENTELEMETRY=DEBUG
application.properties
# Add debug logging
logging.level.io.opentelemetry=DEBUG
logging.level.io.opentelemetry.exporter=TRACE

2. Two HTTP Server Spans per Request​

Symptoms: Each request shows the starter's span, such as GET /users/, and a second span from the org.springframework.boot scope, such as http get /users/.

Cause: io.micrometer:micrometer-tracing-bridge-otel is on the classpath next to the starter. On Spring Boot 3.x it turns on Micrometer Observation tracing, which creates its own server spans.

Solutions:

  • Remove micrometer-tracing-bridge-otel from the build
  • Or set management.tracing.enabled=false

On Spring Boot 4 the bridge alone creates no spans, so it can be removed there too.

3. ClassNotFoundException or NoClassDefFoundError​

Symptoms: Application fails to start with missing OpenTelemetry classes.

Solutions:

<!-- Ensure BOM is imported in pom.xml -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-instrumentation-bom</artifactId>
<version>2.33.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

4. High Memory Usage​

Symptoms: Application memory usage increases significantly after adding instrumentation.

Solutions:

# Reduce batch size and queue size
otel.bsp.max.queue.size=1024
otel.bsp.max.export.batch.size=256
otel.bsp.schedule.delay=5000

5. Missing Database Query Spans​

Symptoms: HTTP requests traced but database queries not visible.

Solutions:

# Enable JDBC instrumentation explicitly
otel.instrumentation.jdbc.enabled=true
otel.instrumentation.jdbc-datasource.enabled=true

# Show SQL in spans (dev only)
otel.instrumentation.jdbc.statement-sanitizer.enabled=false

6. Verification Test​

Test that instrumentation is working correctly:

src/test/java/com/example/TelemetryTest.java
package com.example;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
class TelemetryTest {

@Autowired
private OpenTelemetry openTelemetry;

@Test
void testTelemetryConfiguration() {
assertThat(openTelemetry).isNotNull();

Tracer tracer = openTelemetry.getTracer("test");
Span span = tracer.spanBuilder("test-span").startSpan();

assertThat(span).isNotNull();
assertThat(span.isRecording()).isTrue();

span.end();
}
}

Security Considerations​

Protecting sensitive data in telemetry is critical for compliance and security.

Sensitive Data Masking​

Mask sensitive data in span attributes:

src/main/java/com/example/security/SensitiveDataMasker.java
package com.example.security;

import io.opentelemetry.api.trace.Span;
import org.springframework.stereotype.Component;

@Component
public class SensitiveDataMasker {

public void addUserAttributes(Span span, User user) {
span.setAttribute("user.id", user.getId());
span.setAttribute("user.role", user.getRole());

// BAD: Exposing PII
// span.setAttribute("user.email", user.getEmail());
// span.setAttribute("user.phone", user.getPhone());

// GOOD: Masked or hashed
span.setAttribute("user.email_domain", extractDomain(user.getEmail()));
span.setAttribute("user.phone_country", extractCountryCode(user.getPhone()));
}

private String extractDomain(String email) {
return email.substring(email.indexOf('@') + 1);
}
}

SQL Query Obfuscation​

Ensure SQL queries don't leak sensitive data:

application.properties
# Enable SQL statement sanitization (production)
otel.instrumentation.jdbc.statement-sanitizer.enabled=true

# Disable raw SQL in spans
otel.instrumentation.jdbc.statement.enabled=false

HTTP Header Filtering​

Filter sensitive HTTP headers from traces:

src/main/java/com/example/config/SecurityConfig.java
package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SecurityConfig {

@Bean
public WebMvcConfigurer headerFilterConfigurer() {
return new WebMvcConfigurer() {
// Automatically filtered by OpenTelemetry:
// - Authorization
// - Cookie
// - Set-Cookie
// - X-API-Key

// Custom filter for additional headers
// Configure via: otel.instrumentation.http.capture-headers.server.request
};
}
}
# Specify headers to capture (whitelist approach)
otel.instrumentation.http.capture-headers.server.request=X-Request-ID,X-Correlation-ID
otel.instrumentation.http.capture-headers.server.response=X-Response-Time

Compliance Considerations​

For GDPR, HIPAA, and PCI-DSS compliance:

  • Disable PII capture: Never add email, phone, SSN, or credit card data to spans
  • Use span redaction: Implement custom span processors to redact data
  • Audit trace data: Regularly review exported spans for sensitive information
  • Implement data retention policies: Configure Scout to delete traces after required period
  • Encrypt in transit: Always use HTTPS/TLS for OTLP exporter endpoints
src/main/java/com/example/config/ComplianceSpanProcessor.java
package com.example.config;

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.trace.ReadWriteSpan;
import io.opentelemetry.sdk.trace.ReadableSpan;
import io.opentelemetry.sdk.trace.SpanProcessor;

import java.util.Set;

public class ComplianceSpanProcessor implements SpanProcessor {

private static final Set<String> SENSITIVE_KEYS = Set.of(
"user.email", "user.phone", "credit_card", "ssn", "password"
);

@Override
public void onStart(Context parentContext, ReadWriteSpan span) {
// No-op on start
}

@Override
public boolean isStartRequired() {
return false;
}

@Override
public void onEnd(ReadableSpan span) {
// Redact sensitive attributes before export
AttributesBuilder builder = Attributes.builder();
span.getAttributes().forEach((key, value) -> {
if (SENSITIVE_KEYS.contains(key.getKey())) {
builder.put(key.getKey(), "[REDACTED]");
} else {
builder.put((AttributeKey<Object>) key, value);
}
});
}

@Override
public boolean isEndRequired() {
return true;
}
}

Performance Considerations​

Understanding and optimizing the performance impact of instrumentation.

Expected Performance Impact​

Typical overhead when using OpenTelemetry with Spring Boot:

MetricImpactNotes
Latency+1-5ms per requestMostly from span creation and export
CPU Usage+2-5%Varies with trace volume
Memory+50-200 MBFor span buffers and exporters
Network~1-5 KB/spanDepends on attribute count

Factors affecting performance:

  • Number of instrumented operations per request
  • Number of custom attributes added
  • Batch processor configuration
  • Network latency to collector

Optimization Best Practices​

1. Optimize Batch Processing​

BatchSpanProcessor.builder(exporter)
.setMaxQueueSize(2048) // Increase queue size
.setMaxExportBatchSize(512) // Larger batches
.setScheduleDelay(Duration.ofSeconds(5)) // Less frequent exports
.build();

2. Limit Attribute Count​

// BAD: Too many attributes
span.setAttribute("item_1", value1);
span.setAttribute("item_2", value2);
// ... 100 more attributes

// GOOD: Aggregate
span.setAttribute("item_count", items.size());
span.setAttribute("total_value", calculateTotal(items));

3. Use Conditional Instrumentation​

public void processOrder(Order order) {
// Only create detailed spans for high-value orders
if (order.getValue() > 10000) {
Span span = tracer.spanBuilder("process_high_value_order")
.setAttribute("order.value", order.getValue())
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Detailed instrumentation
} finally {
span.end();
}
} else {
// Regular processing without extra spans
processRegularOrder(order);
}
}

4. Disable Unnecessary Instrumentation​

# Disable specific instrumentations to reduce overhead
otel.instrumentation.spring-webmvc.enabled=true
otel.instrumentation.jdbc.enabled=true
otel.instrumentation.kafka.enabled=true

# Disable if not needed
otel.instrumentation.logback.enabled=false
otel.instrumentation.annotations.enabled=false

FAQ​

Choosing Between Approaches​

Should I use the Java Agent, SDK integration, or Spring Boot 4 native?​

For most cases:

  • Quick start / Legacy apps: Use Java Agent (zero code changes, fastest setup)
  • Production with flexibility: Use OpenTelemetry SDK Integration (full API access, Spring config, GraalVM support)
  • Spring Boot 4, Micrometer-first teams: Consider Spring Boot 4 Native (Spring-native conventions)

See the Choosing Your Approach section for detailed comparison and decision guide.

What is the difference between the Java Agent and the OpenTelemetry SDK?​

Java Agent:

  • Zero code changes - attach JAR at startup
  • 150+ libraries auto-instrumented
  • Environment variables only for configuration
  • Limited custom instrumentation (annotations only)
  • May have version compatibility issues

OpenTelemetry SDK Integration:

  • Requires adding dependencies
  • ~30 libraries auto-instrumented
  • Spring configuration (application.yml) supported
  • Full OpenTelemetry API for custom instrumentation
  • Better for GraalVM native-image

Can I migrate from Java Agent to OpenTelemetry SDK Integration later?​

Yes! Start with the Java Agent for zero-code setup, then migrate to the OpenTelemetry SDK Integration when you need:

  • Custom span processors or exporters
  • Programmatic span creation (not just annotations)
  • Spring Boot native configuration files
  • GraalVM native-image compilation

The migration just requires adding dependencies and removing the -javaagent flag.

Does the OpenTelemetry Spring Boot starter support Spring Boot 4?​

The OpenTelemetry Spring Boot starter supports Spring Boot 4 from version 2.23.0. It is maintained by the OpenTelemetry project, separately from Spring Boot.

Can I use multiple instrumentation approaches together?​

No, you should use only one approach. They provide overlapping functionality and using multiple approaches will cause conflicts:

  • Don't use Java Agent + OpenTelemetry SDK Integration together
  • Don't use OpenTelemetry SDK Integration + Spring Boot 4 Native together
  • Choose one based on your needs (see decision guide)

Does the Java Agent work with GraalVM native-image?​

No, the Java Agent has poor support for GraalVM native-image compilation due to bytecode manipulation incompatibilities. If you need native-image, use the OpenTelemetry SDK Integration or Spring Boot 4 Native starter instead.

General Questions​

What is the minimum Spring Boot version required for OpenTelemetry?​

The OpenTelemetry Spring Boot starter supports Spring Boot 2.6 and later, 3.1 and later, and 4.x. Spring Boot 3.0 is not supported. Spring Boot 4 needs starter 2.23.0 or later.

Does OpenTelemetry work with Java 25?​

Java 25 works with both the OpenTelemetry Spring Boot starter and the Java agent. The base14 Java 25 examples run on Spring Boot 4.1.2 with starter 2.33.0 or agent 2.33.1.

What is the difference between OpenTelemetry and Java Flight Recorder?​

OpenTelemetry focuses on distributed tracing across microservices with standardized telemetry, while JFR provides deep JVM-level profiling. Java 25's JEP 520 adds method tracing to JFR, which can complement OpenTelemetry. You can use both: JFR for low-level JVM metrics and OpenTelemetry for distributed traces.

How much performance overhead does OpenTelemetry add?​

Typical overhead is 1-5ms per request, 2-5% CPU usage, and 50-200MB memory. Impact varies based on instrumentation scope and collector network latency.

Can I use OpenTelemetry with Spring Boot 2.x?​

The current 2.x starter supports Spring Boot 2.6 and 2.7 on Java 8 or later. Use spring-boot-starter-web instead of spring-boot-starter-webmvc, and keep the instrumentation BOM version the same as on Spring Boot 3 and 4.

Configuration Questions​

How do I prevent specific endpoints from being traced?​

Drop those spans in the OpenTelemetry Collector with a filter processor. The starter has no per-path exclusion setting. The base14 examples drop Actuator spans this way:

otel-config.yml
processors:
filter/noisy:
error_mode: ignore
traces:
span:
- 'IsMatch(name, ".*/actuator/.*")'

Add filter/noisy to the traces pipeline's processors list.

Can I send traces to multiple backends?​

Yes, configure multiple exporters programmatically:

SpanExporter compositeExporter = SpanExporter.composite(
OtlpHttpSpanExporter.builder().setEndpoint("http://scout:4318").build(),
OtlpHttpSpanExporter.builder().setEndpoint("http://backup:4318").build()
);

Troubleshooting Questions​

Why are my database queries not appearing in traces?​

Enable JDBC instrumentation: otel.instrumentation.jdbc.enabled=true and verify DataSource is created after OpenTelemetry initialization.

Why duplicate spans for the same operation?​

Multiple instrumentation libraries may overlap. Disable manual instrumentation (remove @WithSpan annotations) where auto-instrumentation already exists.

Spring-Specific Questions​

How does OpenTelemetry work with Spring Cloud?​

OpenTelemetry integrates seamlessly with Spring Cloud components like Feign clients, Spring Cloud Gateway, and Sleuth. For Spring Cloud 2021.0.3+, OpenTelemetry can replace Sleuth entirely.

Can I use OpenTelemetry with Spring WebFlux?​

Yes, OpenTelemetry fully supports reactive Spring WebFlux applications with automatic context propagation across reactive operators.

How do I instrument multi-tenant Spring Boot applications?​

Add tenant information as span attributes:

Span.current().setAttribute("tenant.id", SecurityContextHolder.getContext().getTenantId());

Configure tenant-based filtering as needed.

What's Next​

Once instrumented, you can view Spring Boot traces and metrics in Scout — monitor REST endpoints, JPA queries, and async operations from a single dashboard.

Advanced Topics​

Deployment Guides​

Complete Example​

For a fully working Spring Boot application with OpenTelemetry instrumentation, refer to our example repository:

Spring Boot OpenTelemetry Example

Another example, on Java 17 with MySQL: spring-boot-java17-mysql

The example includes:

  • Spring Boot 4.1.2 REST API built with Gradle
  • opentelemetry-spring-boot-starter 2.33.0 through the instrumentation BOM
  • Spring Data JPA on PostgreSQL with JDBC spans
  • Logback log records exported over OTLP with trace correlation
  • Docker Compose with PostgreSQL and the OpenTelemetry Collector
  • scripts/test-api.sh and scripts/verify-scout.sh to exercise the API and check the exported telemetry

Quick Start with Example​

# Clone the repository
git clone https://github.com/base-14/examples.git
cd examples/java/spring-boot-java25-postgresql

# Start the app, PostgreSQL and the Collector
docker compose up -d --build

# Exercise the API
curl http://localhost:8080/users/
./scripts/test-api.sh

The example demonstrates:

  • Automatic HTTP request tracing
  • Database query instrumentation
  • Log correlation with trace and span IDs
  • Runtime and HTTP metrics export
  • Production-ready configuration patterns

References​

Was this page helpful?