Skip to content
Last9 named a Gartner Cool Vendor in AI for SRE Observability for 2025! Read more →
Last9

Spring Boot

Learn how to integrate OpenTelemetry with Spring Boot applications and send telemetry data to Last9

This guide shows you how to instrument your Spring Boot application with OpenTelemetry and send traces, metrics, and logs to Last9.

Prerequisites

  • Java 8 or later (Java 11+ recommended)
  • Spring Boot 2.6+ or Spring Boot 3.x
  • Last9 account with OTLP endpoint configured

Installation Methods

Choose between auto-instrumentation (recommended) or manual instrumentation:

  1. Download OpenTelemetry Java Agent

    For Java 11+:

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

    For Java 8:

    curl -L https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v1.32.0/opentelemetry-javaagent.jar -o opentelemetry-javaagent.jar
  2. Configure environment variables

    export OTEL_EXPORTER_OTLP_ENDPOINT="$last9_otlp_endpoint"
    export OTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"
    export OTEL_SERVICE_NAME="spring-boot-app"
    export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,service.version=1.0.0"
    export OTEL_TRACES_EXPORTER=otlp
    export OTEL_METRICS_EXPORTER=otlp
    export OTEL_LOGS_EXPORTER=otlp
    export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
    export OTEL_TRACES_SAMPLER=always_on
  3. Start your application with the agent

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

Manual Instrumentation

  1. Add OpenTelemetry dependencies

    <properties>
    <opentelemetry.version>1.32.0</opentelemetry.version>
    <opentelemetry-instrumentation.version>1.32.0</opentelemetry-instrumentation.version>
    </properties>
    <dependencies>
    <!-- OpenTelemetry API -->
    <dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-api</artifactId>
    <version>${opentelemetry.version}</version>
    </dependency>
    <!-- OpenTelemetry SDK -->
    <dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-sdk</artifactId>
    <version>${opentelemetry.version}</version>
    </dependency>
    <!-- OTLP Exporters -->
    <dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-exporter-otlp</artifactId>
    <version>${opentelemetry.version}</version>
    </dependency>
    <!-- Auto-instrumentation libraries -->
    <dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-spring-boot-starter</artifactId>
    <version>${opentelemetry-instrumentation.version}</version>
    </dependency>
    <!-- Spring Boot specific instrumentations -->
    <dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-spring-webmvc-6.0</artifactId>
    <version>${opentelemetry-instrumentation.version}</version>
    </dependency>
    <!-- Database instrumentations -->
    <dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-jdbc</artifactId>
    <version>${opentelemetry-instrumentation.version}</version>
    </dependency>
    <!-- HTTP Client instrumentations -->
    <dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-okhttp-3.0</artifactId>
    <version>${opentelemetry-instrumentation.version}</version>
    </dependency>
    </dependencies>
  2. Configure OpenTelemetry in Spring Boot

    otel:
    exporter:
    otlp:
    endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:$last9_otlp_endpoint}
    headers:
    authorization: ${OTEL_EXPORTER_OTLP_HEADERS:$last9_otlp_auth_header}
    protocol: http/protobuf
    service:
    name: ${OTEL_SERVICE_NAME:spring-boot-app}
    resource:
    attributes:
    deployment.environment: ${DEPLOYMENT_ENVIRONMENT:production}
    service.version: ${SERVICE_VERSION:1.0.0}
    traces:
    exporter: otlp
    sampler:
    probability: 1.0
    metrics:
    exporter: otlp
    logs:
    exporter: otlp
    # Spring Boot configuration
    spring:
    application:
    name: spring-boot-app
    datasource:
    url: jdbc:postgresql://localhost:5432/springdb
    username: ${DB_USERNAME:user}
    password: ${DB_PASSWORD:password}
    driver-class-name: org.postgresql.Driver
    jpa:
    hibernate:
    ddl-auto: validate
    show-sql: false
    properties:
    hibernate:
    format_sql: true
    redis:
    host: ${REDIS_HOST:localhost}
    port: ${REDIS_PORT:6379}
    # Management endpoints for health checks and metrics
    management:
    endpoints:
    web:
    exposure:
    include: health, info, metrics, prometheus
    enabled-by-default: true
    endpoint:
    health:
    show-details: always
    metrics:
    export:
    prometheus:
    enabled: true

Configuration Classes

OpenTelemetry Configuration

package com.example.config;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter;
import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.extension.trace.propagation.AwsXRayPropagator;
import io.opentelemetry.extension.trace.propagation.B3Propagator;
import io.opentelemetry.extension.trace.propagation.JaegerPropagator;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.logs.LogRecordProcessor;
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import io.opentelemetry.sdk.metrics.export.MetricExporter;
import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import io.opentelemetry.semconv.ResourceAttributes;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
@Configuration
public class OpenTelemetryConfig {
@Value("${otel.exporter.otlp.endpoint}")
private String otlpEndpoint;
@Value("${otel.exporter.otlp.headers.authorization}")
private String authorizationHeader;
@Value("${otel.service.name}")
private String serviceName;
@Value("${otel.resource.attributes:}")
private String resourceAttributes;
@Bean
public OpenTelemetry openTelemetry() {
// Create resource
Resource resource = Resource.getDefault()
.merge(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, serviceName)))
.merge(parseResourceAttributes(resourceAttributes));
// Configure trace exporter
SpanExporter spanExporter = OtlpGrpcSpanExporter.builder()
.setEndpoint(otlpEndpoint + "/v1/traces")
.addHeader("authorization", authorizationHeader)
.setTimeout(Duration.ofSeconds(30))
.build();
// Configure metrics exporter
MetricExporter metricExporter = OtlpGrpcMetricExporter.builder()
.setEndpoint(otlpEndpoint + "/v1/metrics")
.addHeader("authorization", authorizationHeader)
.setTimeout(Duration.ofSeconds(30))
.build();
// Configure logs exporter
OtlpGrpcLogRecordExporter logExporter = OtlpGrpcLogRecordExporter.builder()
.setEndpoint(otlpEndpoint + "/v1/logs")
.addHeader("authorization", authorizationHeader)
.setTimeout(Duration.ofSeconds(30))
.build();
// Create tracer provider
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(spanExporter)
.setMaxExportBatchSize(512)
.setExportTimeout(Duration.ofSeconds(30))
.setScheduleDelay(Duration.ofSeconds(5))
.build())
.setResource(resource)
.setSampler(Sampler.alwaysOn())
.build();
// Create meter provider
SdkMeterProvider meterProvider = SdkMeterProvider.builder()
.registerMetricReader(PeriodicMetricReader.builder(metricExporter)
.setInterval(Duration.ofSeconds(30))
.build())
.setResource(resource)
.build();
// Create logger provider
LogRecordProcessor logProcessor = BatchLogRecordProcessor.builder(logExporter)
.setMaxExportBatchSize(512)
.setExportTimeout(Duration.ofSeconds(30))
.setScheduleDelay(Duration.ofSeconds(5))
.build();
SdkLoggerProvider loggerProvider = SdkLoggerProvider.builder()
.addLogRecordProcessor(logProcessor)
.setResource(resource)
.build();
// Configure context propagators
ContextPropagators propagators = ContextPropagators.create(
TextMapPropagator.composite(
TextMapPropagator.composite(),
B3Propagator.injectingSingleHeader(),
JaegerPropagator.getInstance(),
AwsXRayPropagator.getInstance()
)
);
// Build OpenTelemetry SDK
OpenTelemetry openTelemetry = OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.setMeterProvider(meterProvider)
.setLoggerProvider(loggerProvider)
.setPropagators(propagators)
.build();
// Set as global instance
GlobalOpenTelemetry.set(openTelemetry);
return openTelemetry;
}
@Bean
public Tracer tracer(OpenTelemetry openTelemetry) {
return openTelemetry.getTracer(serviceName, "1.0.0");
}
private Resource parseResourceAttributes(String attributes) {
Resource.Builder builder = Resource.empty().toBuilder();
if (attributes != null && !attributes.isEmpty()) {
String[] pairs = attributes.split(",");
for (String pair : pairs) {
String[] kv = pair.split("=", 2);
if (kv.length == 2) {
builder.put(kv[0].trim(), kv[1].trim());
}
}
}
return builder.build();
}
}

REST Controller Examples

package com.example.controller;
import com.example.model.User;
import com.example.service.TelemetryService;
import com.example.service.UserService;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.instrumentation.annotations.WithSpan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.util.List;
import java.util.Optional;
@RestController
@RequestMapping("/api/users")
public class UserController {
private static final Logger log = LoggerFactory.getLogger(UserController.class);
@Autowired
private UserService userService;
@Autowired
private TelemetryService telemetryService;
@GetMapping
public ResponseEntity<List<User>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
HttpServletRequest request) {
long startTime = System.currentTimeMillis();
Span currentSpan = telemetryService.getCurrentSpan();
try {
// Add custom attributes to current span
currentSpan.setAttributes(Attributes.builder()
.put("controller", "UserController")
.put("action", "getAllUsers")
.put("page", page)
.put("size", size)
.build());
List<User> users = telemetryService.traceOperation("user_service.get_all_users", () ->
userService.getAllUsers(page, size)
);
currentSpan.setAttributes(Attributes.builder()
.put("users_count", users.size())
.build());
telemetryService.recordBusinessOperation("get_users", "success");
telemetryService.logInfo("Successfully retrieved users",
Attributes.builder()
.put("users_count", users.size())
.put("page", page)
.put("size", size)
.build());
return ResponseEntity.ok(users);
} catch (Exception e) {
log.error("Error retrieving users", e);
telemetryService.recordBusinessOperation("get_users", "error");
telemetryService.logError("Failed to retrieve users", e,
Attributes.builder()
.put("page", page)
.put("size", size)
.build());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
} finally {
long duration = System.currentTimeMillis() - startTime;
telemetryService.recordHttpRequest(
request.getMethod(),
"/api/users",
200, // This should be the actual response status
duration
);
}
}
@GetMapping("/{id}")
@WithSpan("get_user_by_id")
public ResponseEntity<User> getUserById(@PathVariable Long id, HttpServletRequest request) {
long startTime = System.currentTimeMillis();
Span currentSpan = telemetryService.getCurrentSpan();
try {
currentSpan.setAttributes(Attributes.builder()
.put("controller", "UserController")
.put("action", "getUserById")
.put("user_id", id)
.build());
Optional<User> user = telemetryService.traceOperation("user_service.get_by_id", () ->
userService.getUserById(id),
Attributes.builder().put("user_id", id).build()
);
if (user.isPresent()) {
currentSpan.setAttributes(Attributes.builder()
.put("user_found", true)
.put("user_email", user.get().getEmail())
.build());
telemetryService.recordBusinessOperation("get_user", "success");
return ResponseEntity.ok(user.get());
} else {
currentSpan.setAttributes(Attributes.builder()
.put("user_found", false)
.build());
telemetryService.recordBusinessOperation("get_user", "not_found");
return ResponseEntity.notFound().build();
}
} catch (Exception e) {
log.error("Error retrieving user with id: " + id, e);
telemetryService.recordBusinessOperation("get_user", "error");
telemetryService.logError("Failed to retrieve user", e,
Attributes.builder().put("user_id", id).build());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
} finally {
long duration = System.currentTimeMillis() - startTime;
telemetryService.recordHttpRequest(
request.getMethod(),
"/api/users/{id}",
200, // Should be actual response status
duration
);
}
}
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody User user, HttpServletRequest request) {
long startTime = System.currentTimeMillis();
Span currentSpan = telemetryService.getCurrentSpan();
try {
currentSpan.setAttributes(Attributes.builder()
.put("controller", "UserController")
.put("action", "createUser")
.put("user_email", user.getEmail())
.build());
User createdUser = telemetryService.traceOperation("user_service.create_user", () ->
userService.createUser(user),
Attributes.builder().put("user_email", user.getEmail()).build()
);
currentSpan.setAttributes(Attributes.builder()
.put("user_created", true)
.put("user_id", createdUser.getId())
.build());
telemetryService.recordBusinessOperation("create_user", "success");
telemetryService.logInfo("Successfully created user",
Attributes.builder()
.put("user_id", createdUser.getId())
.put("user_email", createdUser.getEmail())
.build());
return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
} catch (Exception e) {
log.error("Error creating user", e);
telemetryService.recordBusinessOperation("create_user", "error");
telemetryService.logError("Failed to create user", e,
Attributes.builder().put("user_email", user.getEmail()).build());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
} finally {
long duration = System.currentTimeMillis() - startTime;
telemetryService.recordHttpRequest(
request.getMethod(),
"/api/users",
201, // Should be actual response status
duration
);
}
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @Valid @RequestBody User user, HttpServletRequest request) {
long startTime = System.currentTimeMillis();
Span currentSpan = telemetryService.getCurrentSpan();
try {
currentSpan.setAttributes(Attributes.builder()
.put("controller", "UserController")
.put("action", "updateUser")
.put("user_id", id)
.put("user_email", user.getEmail())
.build());
Optional<User> updatedUser = telemetryService.traceOperation("user_service.update_user", () ->
userService.updateUser(id, user),
Attributes.builder()
.put("user_id", id)
.put("user_email", user.getEmail())
.build()
);
if (updatedUser.isPresent()) {
currentSpan.setAttributes(Attributes.builder()
.put("user_updated", true)
.build());
telemetryService.recordBusinessOperation("update_user", "success");
return ResponseEntity.ok(updatedUser.get());
} else {
currentSpan.setAttributes(Attributes.builder()
.put("user_found", false)
.build());
telemetryService.recordBusinessOperation("update_user", "not_found");
return ResponseEntity.notFound().build();
}
} catch (Exception e) {
log.error("Error updating user with id: " + id, e);
telemetryService.recordBusinessOperation("update_user", "error");
telemetryService.logError("Failed to update user", e,
Attributes.builder()
.put("user_id", id)
.put("user_email", user.getEmail())
.build());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
} finally {
long duration = System.currentTimeMillis() - startTime;
telemetryService.recordHttpRequest(
request.getMethod(),
"/api/users/{id}",
200, // Should be actual response status
duration
);
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id, HttpServletRequest request) {
long startTime = System.currentTimeMillis();
Span currentSpan = telemetryService.getCurrentSpan();
try {
currentSpan.setAttributes(Attributes.builder()
.put("controller", "UserController")
.put("action", "deleteUser")
.put("user_id", id)
.build());
boolean deleted = telemetryService.traceOperation("user_service.delete_user", () ->
userService.deleteUser(id),
Attributes.builder().put("user_id", id).build()
);
if (deleted) {
currentSpan.setAttributes(Attributes.builder()
.put("user_deleted", true)
.build());
telemetryService.recordBusinessOperation("delete_user", "success");
return ResponseEntity.noContent().build();
} else {
currentSpan.setAttributes(Attributes.builder()
.put("user_found", false)
.build());
telemetryService.recordBusinessOperation("delete_user", "not_found");
return ResponseEntity.notFound().build();
}
} catch (Exception e) {
log.error("Error deleting user with id: " + id, e);
telemetryService.recordBusinessOperation("delete_user", "error");
telemetryService.logError("Failed to delete user", e,
Attributes.builder().put("user_id", id).build());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
} finally {
long duration = System.currentTimeMillis() - startTime;
telemetryService.recordHttpRequest(
request.getMethod(),
"/api/users/{id}",
204, // Should be actual response status
duration
);
}
}
}

Service Layer with Database Integration

package com.example.service;
import com.example.model.User;
import com.example.repository.UserRepository;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.instrumentation.annotations.WithSpan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
public class UserService {
private static final Logger log = LoggerFactory.getLogger(UserService.class);
@Autowired
private UserRepository userRepository;
@Autowired
private TelemetryService telemetryService;
@Autowired
private CacheService cacheService;
@Autowired
private EmailService emailService;
@WithSpan("user_service.get_all_users")
public List<User> getAllUsers(int page, int size) {
Span currentSpan = telemetryService.getCurrentSpan();
currentSpan.setAttributes(Attributes.builder()
.put("operation", "get_all_users")
.put("page", page)
.put("size", size)
.build());
try {
Pageable pageable = PageRequest.of(page, size, Sort.by("id").ascending());
Page<User> userPage = userRepository.findAll(pageable);
currentSpan.setAttributes(Attributes.builder()
.put("total_elements", userPage.getTotalElements())
.put("total_pages", userPage.getTotalPages())
.put("results_count", userPage.getContent().size())
.build());
return userPage.getContent();
} catch (Exception e) {
currentSpan.recordException(e);
log.error("Error retrieving users", e);
throw e;
}
}
@WithSpan("user_service.get_by_id")
public Optional<User> getUserById(Long id) {
Span currentSpan = telemetryService.getCurrentSpan();
currentSpan.setAttributes(Attributes.builder()
.put("operation", "get_user_by_id")
.put("user_id", id)
.build());
try {
// Try cache first
Optional<User> cachedUser = telemetryService.traceOperation("cache.get_user", () ->
cacheService.getUser(id),
Attributes.builder().put("cache_key", "user:" + id).build()
);
if (cachedUser.isPresent()) {
currentSpan.setAttributes(Attributes.builder()
.put("cache_hit", true)
.build());
return cachedUser;
}
// Fetch from database
Optional<User> user = telemetryService.traceOperation("database.find_user", () ->
userRepository.findById(id),
Attributes.builder().put("user_id", id).build()
);
if (user.isPresent()) {
// Cache for future requests
telemetryService.traceOperation("cache.set_user", () -> {
cacheService.setUser(id, user.get());
return null;
});
currentSpan.setAttributes(Attributes.builder()
.put("cache_hit", false)
.put("user_found", true)
.build());
} else {
currentSpan.setAttributes(Attributes.builder()
.put("cache_hit", false)
.put("user_found", false)
.build());
}
return user;
} catch (Exception e) {
currentSpan.recordException(e);
log.error("Error retrieving user with id: " + id, e);
throw e;
}
}
@Transactional
@WithSpan("user_service.create_user")
public User createUser(User user) {
Span currentSpan = telemetryService.getCurrentSpan();
currentSpan.setAttributes(Attributes.builder()
.put("operation", "create_user")
.put("user_email", user.getEmail())
.build());
try {
// Check if user already exists
Optional<User> existingUser = telemetryService.traceOperation("database.find_by_email", () ->
userRepository.findByEmail(user.getEmail()),
Attributes.builder().put("email", user.getEmail()).build()
);
if (existingUser.isPresent()) {
throw new IllegalArgumentException("User with email " + user.getEmail() + " already exists");
}
// Save user to database
User savedUser = telemetryService.traceOperation("database.save_user", () ->
userRepository.save(user),
Attributes.builder().put("user_email", user.getEmail()).build()
);
currentSpan.setAttributes(Attributes.builder()
.put("user_created", true)
.put("user_id", savedUser.getId())
.build());
// Send welcome email asynchronously
telemetryService.traceOperation("email.send_welcome", () -> {
emailService.sendWelcomeEmail(savedUser);
return null;
});
// Clear relevant caches
telemetryService.traceOperation("cache.invalidate", () -> {
cacheService.invalidateUserCaches();
return null;
});
return savedUser;
} catch (Exception e) {
currentSpan.recordException(e);
log.error("Error creating user", e);
throw e;
}
}
@Transactional
@WithSpan("user_service.update_user")
public Optional<User> updateUser(Long id, User userDetails) {
Span currentSpan = telemetryService.getCurrentSpan();
currentSpan.setAttributes(Attributes.builder()
.put("operation", "update_user")
.put("user_id", id)
.put("user_email", userDetails.getEmail())
.build());
try {
Optional<User> existingUser = telemetryService.traceOperation("database.find_user", () ->
userRepository.findById(id),
Attributes.builder().put("user_id", id).build()
);
if (existingUser.isPresent()) {
User user = existingUser.get();
user.setName(userDetails.getName());
user.setEmail(userDetails.getEmail());
User updatedUser = telemetryService.traceOperation("database.save_user", () ->
userRepository.save(user),
Attributes.builder()
.put("user_id", id)
.put("user_email", userDetails.getEmail())
.build()
);
currentSpan.setAttributes(Attributes.builder()
.put("user_updated", true)
.build());
// Invalidate cache
telemetryService.traceOperation("cache.invalidate_user", () -> {
cacheService.invalidateUser(id);
return null;
});
return Optional.of(updatedUser);
} else {
currentSpan.setAttributes(Attributes.builder()
.put("user_found", false)
.build());
return Optional.empty();
}
} catch (Exception e) {
currentSpan.recordException(e);
log.error("Error updating user with id: " + id, e);
throw e;
}
}
@Transactional
@WithSpan("user_service.delete_user")
public boolean deleteUser(Long id) {
Span currentSpan = telemetryService.getCurrentSpan();
currentSpan.setAttributes(Attributes.builder()
.put("operation", "delete_user")
.put("user_id", id)
.build());
try {
if (userRepository.existsById(id)) {
telemetryService.traceOperation("database.delete_user", () -> {
userRepository.deleteById(id);
return null;
}, Attributes.builder().put("user_id", id).build());
// Invalidate cache
telemetryService.traceOperation("cache.invalidate_user", () -> {
cacheService.invalidateUser(id);
return null;
});
currentSpan.setAttributes(Attributes.builder()
.put("user_deleted", true)
.build());
return true;
} else {
currentSpan.setAttributes(Attributes.builder()
.put("user_found", false)
.build());
return false;
}
} catch (Exception e) {
currentSpan.recordException(e);
log.error("Error deleting user with id: " + id, e);
throw e;
}
}
}

Production Deployment

Docker Configuration

FROM openjdk:11-jre-slim
# Install curl for health checks
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# Create app directory
WORKDIR /app
# Download OpenTelemetry Java agent
ADD https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar /app/opentelemetry-javaagent.jar
# Copy application jar
COPY target/spring-boot-app-1.0.0.jar app.jar
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
RUN chown -R appuser:appuser /app
USER appuser
# Expose port
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
# Start application with OpenTelemetry agent
CMD ["java", "-javaagent:opentelemetry-javaagent.jar", "-jar", "app.jar"]

Kubernetes Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-boot-app
labels:
app: spring-boot-app
spec:
replicas: 3
selector:
matchLabels:
app: spring-boot-app
template:
metadata:
labels:
app: spring-boot-app
spec:
containers:
- name: spring-boot-app
image: your-registry/spring-boot-app:latest
ports:
- containerPort: 8080
env:
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "$last9_otlp_endpoint"
- name: OTEL_EXPORTER_OTLP_HEADERS
value: "Authorization=$last9_otlp_auth_header"
- name: OTEL_SERVICE_NAME
value: "spring-boot-app"
- name: OTEL_RESOURCE_ATTRIBUTES
value: "deployment.environment=production,service.version=1.0.0,k8s.cluster.name=production"
- name: SPRING_DATASOURCE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url
- name: REDIS_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: redis-host
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 45
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 5
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
---
apiVersion: v1
kind: Service
metadata:
name: spring-boot-app-service
spec:
selector:
app: spring-boot-app
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: LoadBalancer

Testing the Integration

  1. Start your application

    # With auto-instrumentation
    java -javaagent:opentelemetry-javaagent.jar -jar target/spring-boot-app-1.0.0.jar
    # Or with Docker
    docker-compose up
  2. Test the endpoints

    # Health check
    curl http://localhost:8080/actuator/health
    # User endpoints
    curl http://localhost:8080/api/users
    curl http://localhost:8080/api/users/1
    # Create a user
    curl -X POST http://localhost:8080/api/users \
    -H "Content-Type: application/json" \
    -d '{"name": "Test User", "email": "test@example.com"}'
  3. View telemetry in Last9

    Check your Last9 dashboard for:

    • HTTP request traces with Spring Boot specific attributes
    • Database operation spans with JPA/Hibernate details
    • Cache operation traces
    • Custom business logic spans and metrics
    • Error tracking and correlation