Lightstep is a powerful distributed tracing system that helps you understand and optimize your microservices architecture. This guide covers comprehensive integration of Lightstep's Java SDK for end-to-end observability.
Dependencies and Setup
Maven Dependencies
<properties>
<lightstep.version>0.21.0</lightstep.version>
<opentelemetry.version>1.28.0</opentelemetry.version>
<grpc.version>1.56.1</grpc.version>
</properties>
<dependencies>
<!-- Lightstep OpenTelemetry SDK -->
<dependency>
<groupId>com.lightstep</groupId>
<artifactId>lightstep-opentelemetry-java</artifactId>
<version>${lightstep.version}</version>
</dependency>
<!-- 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>
<!-- OpenTelemetry Exporter -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
<version>${opentelemetry.version}</version>
</dependency>
<!-- gRPC for OTLP export -->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>${grpc.version}</version>
</dependency>
<!-- Spring Boot Starter (if using Spring) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.1.0</version>
</dependency>
</dependencies>
Core Configuration
1. Lightstep Tracer Configuration
package com.example.tracing.config;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
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.util.concurrent.TimeUnit;
@Configuration
public class LightstepConfig {
@Value("${lightstep.access.token:}")
private String lightstepAccessToken;
@Value("${lightstep.service.name:unknown-service}")
private String serviceName;
@Value("${lightstep.collector.host:ingest.lightstep.com}")
private String collectorHost;
@Value("${lightstep.collector.port:443}")
private int collectorPort;
@Bean
public OpenTelemetry openTelemetry() {
// Create resource with service name
Resource resource = Resource.getDefault()
.merge(Resource.builder()
.put(ResourceAttributes.SERVICE_NAME, serviceName)
.put(ResourceAttributes.SERVICE_VERSION, "1.0.0")
.put(ResourceAttributes.DEPLOYMENT_ENVIRONMENT, getEnvironment())
.build());
// Configure OTLP exporter for Lightstep
OtlpGrpcSpanExporter spanExporter = OtlpGrpcSpanExporter.builder()
.setEndpoint(String.format("https://%s:%d", collectorHost, collectorPort))
.addHeader("lightstep-access-token", lightstepAccessToken)
.setTimeout(30, TimeUnit.SECONDS)
.build();
// Configure SDK
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(spanExporter).build())
.setResource(resource)
.build();
OpenTelemetrySdk openTelemetrySdk = OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
.build();
// Register as global instance
GlobalOpenTelemetry.set(openTelemetrySdk);
return openTelemetrySdk;
}
@Bean
public Tracer tracer(OpenTelemetry openTelemetry) {
return openTelemetry.getTracer(serviceName, "1.0.0");
}
private String getEnvironment() {
return System.getenv().getOrDefault("ENVIRONMENT", "development");
}
}
2. Environment-Specific Configuration
# application.yml
lightstep:
access:
token: ${LIGHTSTEP_ACCESS_TOKEN:}
service:
name: user-service
collector:
host: ingest.lightstep.com
port: 443
management:
tracing:
sampling:
probability: 1.0
// Alternative Programmatic Configuration
public class LightstepTracerFactory {
public static OpenTelemetry createLightstepTracer(String serviceName,
String accessToken,
String environment) {
// Using Lightstep's OpenTelemetry distribution
return LightstepOpenTelemetry
.newBuilder(accessToken)
.setServiceName(serviceName)
.setServiceVersion("1.0.0")
.setCollectorHost("ingest.lightstep.com")
.setCollectorPort(443)
.setCollectorProtocol("https")
.setResourceAttributes(ResourceAttributes.builder()
.put("deployment.environment", environment)
.put("team", "backend")
.build())
.build()
.getOpenTelemetry();
}
}
Instrumentation Implementation
1. HTTP Request Filter for Incoming Requests
package com.example.tracing.filter;
import io.opentelemetry.api.GlobalOpenTelemetry;
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 io.opentelemetry.context.propagation.TextMapGetter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class TracingFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(TracingFilter.class);
private final Tracer tracer;
public TracingFilter(Tracer tracer) {
this.tracer = tracer;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// Extract context from incoming headers
Context extractedContext = GlobalOpenTelemetry.getPropagators().getTextMapPropagator()
.extract(Context.current(), httpRequest, HttpServletRequestGetter.INSTANCE);
// Start span
Span span = tracer.spanBuilder(httpRequest.getMethod() + " " + httpRequest.getRequestURI())
.setSpanKind(SpanKind.SERVER)
.setParent(extractedContext)
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Add attributes to span
span.setAttribute("http.method", httpRequest.getMethod());
span.setAttribute("http.route", httpRequest.getRequestURI());
span.setAttribute("http.url", httpRequest.getRequestURL().toString());
span.setAttribute("http.user_agent", httpRequest.getHeader("User-Agent"));
span.setAttribute("http.client_ip", getClientIp(httpRequest));
// Set span as current
chain.doFilter(request, response);
// Record response status
span.setAttribute("http.status_code", httpResponse.getStatus());
if (httpResponse.getStatus() >= 400) {
span.setStatus(StatusCode.ERROR, "HTTP " + httpResponse.getStatus());
} else {
span.setStatus(StatusCode.OK);
}
} catch (Exception e) {
// Record exception
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
span.end();
}
}
private String getClientIp(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0];
}
return request.getRemoteAddr();
}
private enum HttpServletRequestGetter implements TextMapGetter<HttpServletRequest> {
INSTANCE;
@Override
public Iterable<String> keys(HttpServletRequest carrier) {
return () -> carrier.getHeaderNames().asIterator();
}
@Override
public String get(HttpServletRequest carrier, String key) {
return carrier.getHeader(key);
}
}
}
2. RestTemplate Interceptor for Outgoing HTTP Calls
package com.example.tracing.interceptor;
import io.opentelemetry.api.GlobalOpenTelemetry;
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 io.opentelemetry.context.propagation.TextMapSetter;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class TracingRestTemplateInterceptor implements ClientHttpRequestInterceptor {
private final Tracer tracer;
public TracingRestTemplateInterceptor(Tracer tracer) {
this.tracer = tracer;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
// Create child span for outgoing request
Span span = tracer.spanBuilder("HTTP " + request.getMethod() + " " + request.getURI().getHost())
.setSpanKind(SpanKind.CLIENT)
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Add attributes
span.setAttribute("http.method", request.getMethod().name());
span.setAttribute("http.url", request.getURI().toString());
span.setAttribute("http.target", request.getURI().getPath());
span.setAttribute("net.peer.name", request.getURI().getHost());
span.setAttribute("net.peer.port", request.getURI().getPort());
// Inject tracing headers
GlobalOpenTelemetry.getPropagators().getTextMapPropagator()
.inject(Context.current(), request, HttpRequestSetter.INSTANCE);
// Execute request
ClientHttpResponse response = execution.execute(request, body);
// Record response
span.setAttribute("http.status_code", response.getStatusCode().value());
if (response.getStatusCode().isError()) {
span.setStatus(StatusCode.ERROR, "HTTP " + response.getStatusCode().value());
}
return response;
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
span.end();
}
}
private enum HttpRequestSetter implements TextMapSetter<HttpRequest> {
INSTANCE;
@Override
public void set(HttpRequest carrier, String key, String value) {
carrier.getHeaders().add(key, value);
}
}
}
3. Database Tracing Aspect
package com.example.tracing.aspect;
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 org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Aspect
@Component
public class DatabaseTracingAspect {
private static final Logger logger = LoggerFactory.getLogger(DatabaseTracingAspect.class);
private final Tracer tracer;
public DatabaseTracingAspect(Tracer tracer) {
this.tracer = tracer;
}
@Around("execution(* com.example.repository.*.*(..))")
public Object traceRepositoryMethods(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
Span span = tracer.spanBuilder(className + "." + methodName)
.setSpanKind(SpanKind.INTERNAL)
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Add database operation attributes
span.setAttribute("db.operation", methodName);
span.setAttribute("db.component", "JDBC");
span.setAttribute("db.instance", "users-db");
// Log parameters (be careful with sensitive data)
if (joinPoint.getArgs() != null && joinPoint.getArgs().length > 0) {
span.setAttribute("db.parameters.count", joinPoint.getArgs().length);
}
logger.debug("Executing database operation: {}.{}", className, methodName);
Object result = joinPoint.proceed();
span.setStatus(StatusCode.OK);
return result;
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
logger.error("Database operation failed: {}.{}", className, methodName, e);
throw e;
} finally {
span.end();
}
}
@Around("@annotation(com.example.tracing.annotation.TracedQuery)")
public Object traceCustomQueries(ProceedingJoinPoint joinPoint) throws Throwable {
String queryName = getQueryName(joinPoint);
Span span = tracer.spanBuilder("DB Query: " + queryName)
.setSpanKind(SpanKind.INTERNAL)
.startSpan();
long startTime = System.currentTimeMillis();
try (Scope scope = span.makeCurrent()) {
span.setAttribute("db.operation", "query");
span.setAttribute("db.query.name", queryName);
span.setAttribute("db.system", "postgresql");
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - startTime;
span.setAttribute("db.duration_ms", duration);
span.setStatus(StatusCode.OK);
return result;
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
span.end();
}
}
private String getQueryName(ProceedingJoinPoint joinPoint) {
return joinPoint.getSignature().getName();
}
}
4. Custom Business Operation Tracing
package com.example.tracing.service;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
public class BusinessOperationTracer {
private static final Logger logger = LoggerFactory.getLogger(BusinessOperationTracer.class);
private final Tracer tracer;
public BusinessOperationTracer(Tracer tracer) {
this.tracer = tracer;
}
public <T> T traceOperation(String operationName, Map<String, Object> attributes,
BusinessOperation<T> operation) {
Span span = tracer.spanBuilder(operationName)
.setSpanKind(io.opentelemetry.api.trace.SpanKind.INTERNAL)
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Set operation attributes
if (attributes != null) {
attributes.forEach((key, value) ->
span.setAttribute(key, String.valueOf(value)));
}
span.setAttribute("business.operation", operationName);
span.setAttribute("component", "business-logic");
logger.info("Starting business operation: {}", operationName);
T result = operation.execute();
span.setStatus(StatusCode.OK);
logger.info("Business operation completed: {}", operationName);
return result;
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
logger.error("Business operation failed: {}", operationName, e);
throw e;
} finally {
span.end();
}
}
public void traceAsyncOperation(String operationName, Map<String, Object> attributes,
Runnable operation) {
// Capture current context for async execution
Context context = Context.current();
new Thread(() -> {
try (Scope scope = context.makeCurrent()) {
traceOperation(operationName, attributes, () -> {
operation.run();
return null;
});
}
}).start();
}
@FunctionalInterface
public interface BusinessOperation<T> {
T execute();
}
}
Service Implementation Examples
1. User Service with Comprehensive Tracing
package com.example.service;
import com.example.tracing.service.BusinessOperationTracer;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class UserService {
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
private final Tracer tracer;
private final BusinessOperationTracer businessTracer;
private final UserRepository userRepository;
public UserService(Tracer tracer, BusinessOperationTracer businessTracer,
UserRepository userRepository) {
this.tracer = tracer;
this.businessTracer = businessTracer;
this.userRepository = userRepository;
}
public User createUser(CreateUserRequest request) {
return businessTracer.traceOperation("user.create",
Map.of(
"user.email", request.getEmail(),
"user.role", request.getRole(),
"user.source", request.getSource()
),
() -> {
// Validate user
validateUserRequest(request);
// Check for existing user
if (userRepository.existsByEmail(request.getEmail())) {
throw new UserAlreadyExistsException("User already exists");
}
// Create user entity
User user = new User(request);
User savedUser = userRepository.save(user);
// Add event to current span
Span.current().addEvent("user.created",
io.opentelemetry.api.common.Attributes.of(
io.opentelemetry.api.common.AttributeKey.stringKey("user.id"),
savedUser.getId()
));
logger.info("User created successfully: {}", savedUser.getId());
return savedUser;
});
}
public User findUserById(String userId) {
Span span = tracer.spanBuilder("user.findById")
.setAttribute("user.id", userId)
.startSpan();
try (Scope scope = span.makeCurrent()) {
logger.debug("Searching for user: {}", userId);
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException("User not found"));
span.setAttribute("user.found", true);
span.setAttribute("user.email", user.getEmail());
return user;
} catch (Exception e) {
span.recordException(e);
span.setAttribute("user.found", false);
throw e;
} finally {
span.end();
}
}
public void processUserBatch(List<String> userIds) {
businessTracer.traceAsyncOperation("user.batchProcessing",
Map.of("batch.size", userIds.size()),
() -> {
// Process users in batch
userIds.forEach(this::processSingleUser);
});
}
private void processSingleUser(String userId) {
// This will be traced as part of the parent async operation
User user = findUserById(userId);
// Process user...
}
private void validateUserRequest(CreateUserRequest request) {
if (request.getEmail() == null || !request.getEmail().contains("@")) {
throw new IllegalArgumentException("Invalid email address");
}
}
}
2. Order Service with Distributed Tracing
package com.example.service;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
@Service
public class OrderService {
private final Tracer tracer;
private final RestTemplate restTemplate;
private final BusinessOperationTracer businessTracer;
public OrderService(Tracer tracer, RestTemplate restTemplate,
BusinessOperationTracer businessTracer) {
this.tracer = tracer;
this.restTemplate = restTemplate;
this.businessTracer = businessTracer;
}
public Order processOrder(OrderRequest request) {
return businessTracer.traceOperation("order.process",
Map.of(
"order.amount", request.getAmount(),
"order.currency", request.getCurrency(),
"order.customer_id", request.getCustomerId()
),
() -> {
// Create order span
Span orderSpan = Span.current();
// Step 1: Validate inventory
boolean inventoryAvailable = checkInventory(request.getItems());
orderSpan.setAttribute("order.inventory_available", inventoryAvailable);
if (!inventoryAvailable) {
throw new InventoryUnavailableException("Insufficient inventory");
}
// Step 2: Process payment
PaymentResponse paymentResponse = processPayment(request);
orderSpan.setAttribute("order.payment_id", paymentResponse.getPaymentId());
orderSpan.addEvent("payment.processed");
// Step 3: Create order
Order order = createOrderEntity(request, paymentResponse);
Order savedOrder = orderRepository.save(order);
// Step 4: Update inventory
updateInventory(request.getItems());
orderSpan.addEvent("inventory.updated");
// Step 5: Send notification
sendOrderConfirmation(savedOrder);
orderSpan.addEvent("notification.sent");
logger.info("Order processed successfully: {}", savedOrder.getId());
return savedOrder;
});
}
private boolean checkInventory(List<OrderItem> items) {
Span span = tracer.spanBuilder("inventory.check")
.setAttribute("inventory.items_count", items.size())
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Call inventory service
InventoryCheckRequest checkRequest = new InventoryCheckRequest(items);
ResponseEntity<InventoryCheckResponse> response = restTemplate.postForEntity(
"http://inventory-service/api/inventory/check",
checkRequest,
InventoryCheckResponse.class
);
boolean available = response.getBody().isAvailable();
span.setAttribute("inventory.available", available);
return available;
} finally {
span.end();
}
}
private PaymentResponse processPayment(OrderRequest request) {
return businessTracer.traceOperation("payment.process",
Map.of(
"payment.amount", request.getAmount(),
"payment.currency", request.getCurrency()
),
() -> {
// Call payment service
PaymentRequest paymentRequest = new PaymentRequest(request);
ResponseEntity<PaymentResponse> response = restTemplate.postForEntity(
"http://payment-service/api/payments/process",
paymentRequest,
PaymentResponse.class
);
if (!response.getStatusCode().is2xxSuccessful()) {
throw new PaymentProcessingException("Payment processing failed");
}
return response.getBody();
});
}
}
Configuration Management
1. Spring Boot Auto-Configuration
package com.example.tracing.config;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.spring.webmvc.v6_0.SpringWebMvcTelemetry;
import jakarta.servlet.Filter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnProperty(name = "lightstep.enabled", havingValue = "true", matchIfMissing = true)
public class TracingAutoConfiguration {
@Bean
public Filter tracingFilter(OpenTelemetry openTelemetry) {
return SpringWebMvcTelemetry.create(openTelemetry).createServletFilter();
}
}
2. Environment-Specific Configuration
# application-dev.yml
lightstep:
enabled: true
access:
token: ${LIGHTSTEP_DEV_TOKEN}
service:
name: user-service-dev
collector:
host: ingest.lightstep.com
port: 443
opentelemetry:
tracer:
config:
sampler: always_on
export:
interval: 5000
logging:
level:
io.opentelemetry: INFO
# application-prod.yml
lightstep:
enabled: true
access:
token: ${LIGHTSTEP_PROD_TOKEN}
service:
name: user-service
collector:
host: ingest.lightstep.com
port: 443
opentelemetry:
tracer:
config:
sampler: parentbased_always_on
export:
interval: 1000
management:
endpoints:
web:
exposure:
include: health,info,metrics
Testing and Verification
1. Unit Test for Tracing
package com.example.tracing.test;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension;
import io.opentelemetry.sdk.trace.data.SpanData;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class UserServiceTracingTest {
@RegisterExtension
static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create();
@Autowired
private UserService userService;
@Test
void shouldCreateSpanForUserCreation() {
// Given
CreateUserRequest request = new CreateUserRequest("[email protected]", "USER");
// When
User user = userService.createUser(request);
// Then
List<SpanData> spans = otelTesting.getSpans();
assertEquals(1, spans.size());
SpanData span = spans.get(0);
assertEquals("user.create", span.getName());
assertEquals("[email protected]",
span.getAttributes().get(AttributeKey.stringKey("user.email")));
}
}
2. Integration Test
package com.example.tracing.test;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.ResponseEntity;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class TracingIntegrationTest {
@LocalServerPort
private int port;
private TestRestTemplate restTemplate = new TestRestTemplate();
@Test
void shouldPropagateTraceHeaders() {
// When
ResponseEntity<String> response = restTemplate.getForEntity(
"http://localhost:" + port + "/api/users/123", String.class);
// Then
assertEquals(200, response.getStatusCodeValue());
// Verify trace headers are present in response
assertNotNull(response.getHeaders().get("traceparent"));
}
}
Best Practices
- Consistent Naming: Use consistent span names and attribute keys
- Reasonable Sampling: Adjust sampling rate based on traffic
- Meaningful Attributes: Include business context in spans
- Error Handling: Always record exceptions and set appropriate status
- Performance: Use async operations for non-critical tracing
- Security: Never include sensitive data in span attributes
// Good practice - meaningful attributes
span.setAttribute("business.customer_tier", customer.getTier());
span.setAttribute("business.order_priority", order.getPriority());
// Bad practice - unclear attributes
span.setAttribute("data", customerData);
Conclusion
Lightstep Java SDK provides:
- Distributed tracing across microservices
- Performance monitoring with detailed span timing
- Error analysis with stack traces and context
- Dependency mapping through span relationships
- Business context in operational data
By implementing comprehensive tracing with Lightstep, you gain deep visibility into your application's behavior, enabling faster debugging, performance optimization, and better understanding of system dependencies.