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:
Auto-Instrumentation (Recommended)
-
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.jarFor Java 8:
curl -L https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v1.32.0/opentelemetry-javaagent.jar -o opentelemetry-javaagent.jar -
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=otlpexport OTEL_METRICS_EXPORTER=otlpexport OTEL_LOGS_EXPORTER=otlpexport OTEL_EXPORTER_OTLP_PROTOCOL=http/protobufexport OTEL_TRACES_SAMPLER=always_on -
Start your application with the agent
java -javaagent:opentelemetry-javaagent.jar -jar your-spring-boot-app.jar
Manual Instrumentation
-
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>ext {openTelemetryVersion = '1.32.0'openTelemetryInstrumentationVersion = '1.32.0'}dependencies {// OpenTelemetry APIimplementation "io.opentelemetry:opentelemetry-api:${openTelemetryVersion}"// OpenTelemetry SDKimplementation "io.opentelemetry:opentelemetry-sdk:${openTelemetryVersion}"// OTLP Exportersimplementation "io.opentelemetry:opentelemetry-exporter-otlp:${openTelemetryVersion}"// Auto-instrumentation librariesimplementation "io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter:${openTelemetryInstrumentationVersion}"// Spring Boot specific instrumentationsimplementation "io.opentelemetry.instrumentation:opentelemetry-spring-webmvc-6.0:${openTelemetryInstrumentationVersion}"// Database instrumentationsimplementation "io.opentelemetry.instrumentation:opentelemetry-jdbc:${openTelemetryInstrumentationVersion}"// HTTP Client instrumentationsimplementation "io.opentelemetry.instrumentation:opentelemetry-okhttp-3.0:${openTelemetryInstrumentationVersion}"} -
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/protobufservice:name: ${OTEL_SERVICE_NAME:spring-boot-app}resource:attributes:deployment.environment: ${DEPLOYMENT_ENVIRONMENT:production}service.version: ${SERVICE_VERSION:1.0.0}traces:exporter: otlpsampler:probability: 1.0metrics:exporter: otlplogs:exporter: otlp# Spring Boot configurationspring:application:name: spring-boot-appdatasource:url: jdbc:postgresql://localhost:5432/springdbusername: ${DB_USERNAME:user}password: ${DB_PASSWORD:password}driver-class-name: org.postgresql.Driverjpa:hibernate:ddl-auto: validateshow-sql: falseproperties:hibernate:format_sql: trueredis:host: ${REDIS_HOST:localhost}port: ${REDIS_PORT:6379}# Management endpoints for health checks and metricsmanagement:endpoints:web:exposure:include: health, info, metrics, prometheusenabled-by-default: trueendpoint:health:show-details: alwaysmetrics:export:prometheus:enabled: true# OpenTelemetry Configurationotel.exporter.otlp.endpoint=${OTEL_EXPORTER_OTLP_ENDPOINT:$last9_otlp_endpoint}otel.exporter.otlp.headers.authorization=${OTEL_EXPORTER_OTLP_HEADERS:$last9_otlp_auth_header}otel.exporter.otlp.protocol=http/protobufotel.service.name=${OTEL_SERVICE_NAME:spring-boot-app}otel.resource.attributes=deployment.environment=${DEPLOYMENT_ENVIRONMENT:production},service.version=${SERVICE_VERSION:1.0.0}otel.traces.exporter=otlpotel.traces.sampler.probability=1.0otel.metrics.exporter=otlpotel.logs.exporter=otlp# Spring Boot Configurationspring.application.name=spring-boot-appspring.datasource.url=jdbc:postgresql://localhost:5432/springdbspring.datasource.username=${DB_USERNAME:user}spring.datasource.password=${DB_PASSWORD:password}spring.datasource.driver-class-name=org.postgresql.Driver# JPA Configurationspring.jpa.hibernate.ddl-auto=validatespring.jpa.show-sql=falsespring.jpa.properties.hibernate.format_sql=true# Redis Configurationspring.redis.host=${REDIS_HOST:localhost}spring.redis.port=${REDIS_PORT:6379}# Management Configurationmanagement.endpoints.web.exposure.include=health,info,metrics,prometheusmanagement.endpoint.health.show-details=alwaysmanagement.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;
@Configurationpublic 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(); }}package com.example.service;
import io.opentelemetry.api.OpenTelemetry;import io.opentelemetry.api.common.AttributeKey;import io.opentelemetry.api.common.Attributes;import io.opentelemetry.api.logs.Logger;import io.opentelemetry.api.logs.Severity;import io.opentelemetry.api.metrics.Counter;import io.opentelemetry.api.metrics.Histogram;import io.opentelemetry.api.metrics.Meter;import io.opentelemetry.api.trace.Span;import io.opentelemetry.api.trace.SpanKind;import io.opentelemetry.api.trace.StatusCode;import io.opentelemetry.api.trace.Tracer;import io.opentelemetry.context.Context;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;import java.util.concurrent.TimeUnit;import java.util.function.Supplier;
@Servicepublic class TelemetryService {
@Autowired private OpenTelemetry openTelemetry;
@Autowired private Tracer tracer;
private Meter meter; private Logger logger;
// Custom metrics private Counter httpRequestsCounter; private Histogram httpRequestDuration; private Counter businessOperationsCounter; private Counter errorCounter;
@PostConstruct public void initialize() { this.meter = openTelemetry.getMeter("spring-boot-app", "1.0.0"); this.logger = openTelemetry.getLogsBridge().get("spring-boot-app");
// Initialize custom metrics this.httpRequestsCounter = meter.counterBuilder("http_requests_total") .setDescription("Total number of HTTP requests") .build();
this.httpRequestDuration = meter.histogramBuilder("http_request_duration_seconds") .setDescription("HTTP request duration in seconds") .setUnit("s") .build();
this.businessOperationsCounter = meter.counterBuilder("business_operations_total") .setDescription("Total number of business operations") .build();
this.errorCounter = meter.counterBuilder("errors_total") .setDescription("Total number of errors") .build(); }
public <T> T traceOperation(String operationName, Supplier<T> operation) { return traceOperation(operationName, operation, Attributes.empty()); }
public <T> T traceOperation(String operationName, Supplier<T> operation, Attributes attributes) { Span span = tracer.spanBuilder(operationName) .setAllAttributes(attributes) .setSpanKind(SpanKind.INTERNAL) .startSpan();
try (var scope = span.makeCurrent()) { T result = operation.get(); span.setStatus(StatusCode.OK); return result; } catch (Exception e) { span.recordException(e); span.setStatus(StatusCode.ERROR, e.getMessage()); recordError("operation_error", operationName, e.getClass().getSimpleName()); throw e; } finally { span.end(); } }
public void recordHttpRequest(String method, String route, int statusCode, long durationMs) { Attributes attributes = Attributes.builder() .put("method", method) .put("route", route) .put("status_code", statusCode) .build();
httpRequestsCounter.add(1, attributes); httpRequestDuration.record(durationMs / 1000.0, attributes); }
public void recordBusinessOperation(String operation, String status) { Attributes attributes = Attributes.builder() .put("operation", operation) .put("status", status) .build();
businessOperationsCounter.add(1, attributes); }
public void recordError(String errorType, String operation, String errorClass) { Attributes attributes = Attributes.builder() .put("error_type", errorType) .put("operation", operation) .put("error_class", errorClass) .build();
errorCounter.add(1, attributes); }
public void logInfo(String message, Attributes attributes) { logger.logRecordBuilder() .setSeverity(Severity.INFO) .setBody(message) .setAllAttributes(attributes) .emit(); }
public void logError(String message, Throwable throwable, Attributes attributes) { logger.logRecordBuilder() .setSeverity(Severity.ERROR) .setBody(message) .setAllAttributes(attributes) .emit();
// Also add current span context Span currentSpan = Span.current(); if (currentSpan != null && currentSpan.isRecording()) { currentSpan.recordException(throwable); } }
public Span getCurrentSpan() { return Span.current(); }
public Context getCurrentContext() { return Context.current(); }}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;
@Servicepublic 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 checksRUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# Create app directoryWORKDIR /app
# Download OpenTelemetry Java agentADD https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar /app/opentelemetry-javaagent.jar
# Copy application jarCOPY target/spring-boot-app-1.0.0.jar app.jar
# Create non-root userRUN groupadd -r appuser && useradd -r -g appuser appuserRUN chown -R appuser:appuser /appUSER appuser
# Expose portEXPOSE 8080
# Health checkHEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ CMD curl -f http://localhost:8080/actuator/health || exit 1
# Start application with OpenTelemetry agentCMD ["java", "-javaagent:opentelemetry-javaagent.jar", "-jar", "app.jar"]version: "3.8"
services: spring-app: build: . ports: - "8080:8080" environment: # OpenTelemetry Configuration - OTEL_EXPORTER_OTLP_ENDPOINT=$last9_otlp_endpoint - OTEL_EXPORTER_OTLP_HEADERS=Authorization=$last9_otlp_auth_header - OTEL_SERVICE_NAME=spring-boot-app - OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,service.version=1.0.0 - OTEL_TRACES_EXPORTER=otlp - OTEL_METRICS_EXPORTER=otlp - OTEL_LOGS_EXPORTER=otlp - OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
# Application Configuration - SPRING_PROFILES_ACTIVE=production - DB_USERNAME=user - DB_PASSWORD=password - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/springdb - REDIS_HOST=redis - REDIS_PORT=6379
# JVM Configuration - JAVA_OPTS=-Xmx1g -Xms512m depends_on: - postgres - redis healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"] interval: 30s timeout: 10s retries: 3
postgres: image: postgres:15-alpine environment: POSTGRES_DB: springdb POSTGRES_USER: user POSTGRES_PASSWORD: password ports: - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data
redis: image: redis:7-alpine ports: - "6379:6379" volumes: - redis_data:/data
volumes: postgres_data: redis_data:Kubernetes Deployment
apiVersion: apps/v1kind: Deploymentmetadata: name: spring-boot-app labels: app: spring-boot-appspec: 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: v1kind: Servicemetadata: name: spring-boot-app-servicespec: selector: app: spring-boot-app ports: - protocol: TCP port: 80 targetPort: 8080 type: LoadBalancerTesting the Integration
-
Start your application
# With auto-instrumentationjava -javaagent:opentelemetry-javaagent.jar -jar target/spring-boot-app-1.0.0.jar# Or with Dockerdocker-compose up -
Test the endpoints
# Health checkcurl http://localhost:8080/actuator/health# User endpointscurl http://localhost:8080/api/userscurl http://localhost:8080/api/users/1# Create a usercurl -X POST http://localhost:8080/api/users \-H "Content-Type: application/json" \-d '{"name": "Test User", "email": "test@example.com"}' -
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