AWS X-Ray in Java: Complete Distributed Tracing Guide

AWS X-Ray helps developers analyze and debug distributed applications by providing end-to-end tracing capabilities. This guide demonstrates comprehensive X-Ray integration for Java applications running on AWS.

Why Use AWS X-Ray?

  • Distributed Tracing: Track requests across microservices
  • Performance Analysis: Identify bottlenecks and latency issues
  • Error Detection: Pinpoint failures in complex architectures
  • Service Map Visualization: Automatic dependency mapping
  • AWS Integration: Seamless integration with AWS services

Prerequisites

  • AWS Account with appropriate permissions
  • Java 8+ application
  • AWS SDK dependencies
  • X-Ray Daemon or AWS X-Ray API access

Step 1: Project Dependencies

Maven (pom.xml):

<dependencies>
<!-- AWS X-Ray SDK -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-xray-recorder-sdk-core</artifactId>
<version>2.14.0</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-xray-recorder-sdk-aws-sdk</artifactId>
<version>2.14.0</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-xray-recorder-sdk-aws-sdk-v2</artifactId>
<version>2.14.0</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-xray-recorder-sdk-spring</artifactId>
<version>2.14.0</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-xray-recorder-sdk-apache-http</artifactId>
<version>2.14.0</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-xray-recorder-sdk-sql</artifactId>
<version>2.14.0</version>
</dependency>
<!-- AWS SDK v2 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>aws-sdk-java</artifactId>
<version>2.20.0</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>2.7.0</version>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
<!-- Database -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.7.0</version>
</dependency>
</dependencies>

Step 2: AWS X-Ray Configuration

application.yml:

# AWS X-Ray Configuration
aws:
xray:
enabled: true
daemon:
address: 127.0.0.1:2000
sampling:
rules: classpath:xray-sampling-rules.json
logging:
level: INFO
plugins:
ec2: true
ecs: true
elastic-beanstalk: true
context-missing: LOG_ERROR
streaming:
enabled: true
batch-size: 100
batch-timeout: 1s
# Spring Configuration
spring:
application:
name: order-service
profiles:
active: ${ENVIRONMENT:local}
# Logging for X-Ray
logging:
level:
com.amazonaws.xray: DEBUG
software.amazon.awssdk: INFO

xray-sampling-rules.json:

{
"version": 2,
"rules": [
{
"description": "Default rule for all requests",
"host": "*",
"http_method": "*",
"url_path": "*",
"fixed_target": 1,
"rate": 0.05
},
{
"description": "High priority for critical endpoints",
"host": "*",
"http_method": "*",
"url_path": "/api/orders/*",
"fixed_target": 10,
"rate": 0.5
},
{
"description": "Health check - no sampling",
"host": "*",
"http_method": "GET",
"url_path": "/health",
"fixed_target": 0,
"rate": 0
}
],
"default": {
"fixed_target": 1,
"rate": 0.1
}
}

Step 3: X-Ray Configuration Classes

@Configuration
@EnableAwsXRay
@Slf4j
public class XRayConfiguration {
@Value("${spring.application.name:unknown}")
private String applicationName;
@Value("${aws.xray.daemon.address:127.0.0.1:2000}")
private String daemonAddress;
@Bean
@Primary
public AWSXRayRecorder xRayRecorder() {
try {
AWSXRayRecorderBuilder builder = AWSXRayRecorderBuilder.standard()
.withSamplingStrategy(new LocalizedSamplingStrategy())
.withDaemonAddress(daemonAddress)
.withSegmentListener(new DefaultSegmentListener())
.withTraceIdGenerator(new SecureTraceIdGenerator());
// Configure plugins
builder.withPlugin(new EC2Plugin());
builder.withPlugin(new ECSPlugin());
AWSXRayRecorder recorder = builder.build();
// Set global recorder
AWSXRay.setGlobalRecorder(recorder);
log.info("AWS X-Ray Recorder initialized for application: {}", applicationName);
return recorder;
} catch (Exception e) {
log.error("Failed to initialize AWS X-Ray Recorder", e);
throw new RuntimeException("X-Ray initialization failed", e);
}
}
@Bean
public ServletFilter xRayFilter() {
return new AWSXRayServletFilter(applicationName);
}
@Bean
public XRaySqlDataSourceDecorator xRaySqlDataSourceDecorator() {
return new XRaySqlDataSourceDecorator();
}
@Bean
public ApacheHttpClient xRayHttpClient() {
return ApacheHttpClient.builder()
.build();
}
}
@Configuration
@Slf4j
public class AwsSdkXRayConfiguration {
@Bean
public AwsXrayInstrumentor awsXrayInstrumentor() {
return new AwsXrayInstrumentor();
}
@Bean
@Primary
public S3Client xRayS3Client(S3Client s3Client) {
return S3ClientXrayInstrumentor.instrument(s3Client);
}
@Bean
@Primary
public DynamoDbClient xRayDynamoDbClient(DynamoDbClient dynamoDbClient) {
return DynamoDbClientXrayInstrumentor.instrument(dynamoDbClient);
}
@Bean
@Primary
public SqsClient xRaySqsClient(SqsClient sqsClient) {
return SqsClientXrayInstrumentor.instrument(sqsClient);
}
}

Step 4: Core X-Ray Service Classes

@Service
@Slf4j
public class XRayTracingService {
private final AWSXRayRecorder xRayRecorder;
public XRayTracingService(AWSXRayRecorder xRayRecorder) {
this.xRayRecorder = xRayRecorder;
}
// Create custom subsegment
public <T> T traceMethod(String name, String namespace, Supplier<T> operation) {
Subsegment subsegment = xRayRecorder.beginSubsegment(name);
try {
subsegment.putNamespace(namespace);
T result = operation.get();
subsegment.setError(false);
return result;
} catch (Exception e) {
subsegment.addException(e);
subsegment.setError(true);
throw e;
} finally {
xRayRecorder.endSubsegment();
}
}
public void traceMethod(String name, String namespace, Runnable operation) {
Subsegment subsegment = xRayRecorder.beginSubsegment(name);
try {
subsegment.putNamespace(namespace);
operation.run();
subsegment.setError(false);
} catch (Exception e) {
subsegment.addException(e);
subsegment.setError(true);
throw e;
} finally {
xRayRecorder.endSubsegment();
}
}
// Add metadata to current segment
public void addMetadata(String key, Object value) {
try {
Segment segment = AWSXRay.getCurrentSegment();
if (segment != null) {
segment.putMetadata(key, value);
}
} catch (Exception e) {
log.warn("Failed to add metadata to X-Ray segment", e);
}
}
public void addMetadata(String namespace, String key, Object value) {
try {
Segment segment = AWSXRay.getCurrentSegment();
if (segment != null) {
segment.putMetadata(namespace, key, value);
}
} catch (Exception e) {
log.warn("Failed to add namespaced metadata to X-Ray segment", e);
}
}
// Add annotation to current segment
public void addAnnotation(String key, Object value) {
try {
Segment segment = AWSXRay.getCurrentSegment();
if (segment != null) {
segment.putAnnotation(key, value);
}
} catch (Exception e) {
log.warn("Failed to add annotation to X-Ray segment", e);
}
}
// Get current trace ID
public String getTraceId() {
try {
Segment segment = AWSXRay.getCurrentSegment();
return segment != null ? segment.getTraceId().toString() : "unknown";
} catch (Exception e) {
log.warn("Failed to get trace ID from X-Ray", e);
return "unknown";
}
}
// Check if sampling decision is made
public boolean isSampled() {
try {
Segment segment = AWSXRay.getCurrentSegment();
return segment != null && segment.isSampled();
} catch (Exception e) {
return false;
}
}
// Create custom segment for background jobs
public <T> T traceBackgroundJob(String jobName, Supplier<T> job) {
try {
Segment segment = xRayRecorder.beginSegment(jobName);
segment.setService("background-job");
segment.putAnnotation("job_type", "background");
try {
T result = job.get();
segment.setError(false);
return result;
} catch (Exception e) {
segment.addException(e);
segment.setError(true);
throw e;
} finally {
xRayRecorder.endSegment();
}
} catch (Exception e) {
log.warn("Failed to trace background job: {}", jobName, e);
return job.get(); // Fallback to execution without tracing
}
}
// HTTP client tracing
public HttpGet createTracedHttpGet(String url) {
HttpGet httpGet = new HttpGet(url);
try {
// Add trace headers for downstream services
Segment segment = AWSXRay.getCurrentSegment();
if (segment != null) {
TracingHeader tracingHeader = segment.getTraceHeader();
httpGet.setHeader(TracingHeader.HEADER_KEY, tracingHeader.toString());
}
} catch (Exception e) {
log.warn("Failed to add tracing headers to HTTP request", e);
}
return httpGet;
}
}
@Component
@Slf4j
public class XRayHttpClientInterceptor {
private final ApacheHttpClient xrayHttpClient;
public XRayHttpClientInterceptor() {
this.xrayHttpClient = ApacheHttpClient.builder().build();
}
public <T> T executeTracedRequest(HttpUriRequest request, 
ResponseHandler<T> responseHandler) throws IOException {
return xrayHttpClient.execute(request, responseHandler);
}
public HttpResponse executeTracedRequest(HttpUriRequest request) throws IOException {
return xrayHttpClient.execute(request);
}
// For WebClient in reactive applications
public ExchangeFilterFunction xRayTracingFilter() {
return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
// Add X-Ray tracing headers
ClientRequest tracedRequest = ClientRequest.from(clientRequest)
.headers(headers -> addTracingHeaders(headers))
.build();
return Mono.just(tracedRequest);
});
}
private void addTracingHeaders(HttpHeaders headers) {
try {
Segment segment = AWSXRay.getCurrentSegment();
if (segment != null) {
TracingHeader tracingHeader = segment.getTraceHeader();
headers.add(TracingHeader.HEADER_KEY, tracingHeader.toString());
}
} catch (Exception e) {
log.warn("Failed to add X-Ray tracing headers", e);
}
}
}

Step 5: Database Tracing with X-Ray

@Configuration
@Slf4j
public class XRayDataSourceConfiguration {
@Bean
@Primary
public DataSource xRayDataSource(DataSource dataSource) {
log.info("Wrapping DataSource with X-Ray tracing");
return new XRayDataSource(dataSource);
}
}
@Service
@Slf4j
public class DatabaseTracingService {
private final XRayTracingService tracingService;
public DatabaseTracingService(XRayTracingService tracingService) {
this.tracingService = tracingService;
}
public <T> T traceQuery(String queryName, String sql, Supplier<T> query) {
return tracingService.traceMethod(queryName, "remote", () -> {
// Add SQL query as metadata (obfuscated for security)
tracingService.addMetadata("sql", obfuscateSql(sql));
tracingService.addAnnotation("query_type", getQueryType(sql));
long startTime = System.currentTimeMillis();
try {
T result = query.get();
long duration = System.currentTimeMillis() - startTime;
// Add performance metrics
tracingService.addAnnotation("query_duration_ms", duration);
tracingService.addAnnotation("query_success", true);
return result;
} catch (Exception e) {
long duration = System.currentTimeMillis() - startTime;
tracingService.addAnnotation("query_duration_ms", duration);
tracingService.addAnnotation("query_success", false);
throw e;
}
});
}
public void traceUpdate(String operationName, String sql, Runnable update) {
tracingService.traceMethod(operationName, "remote", () -> {
tracingService.addMetadata("sql", obfuscateSql(sql));
tracingService.addAnnotation("operation_type", "update");
long startTime = System.currentTimeMillis();
try {
update.run();
long duration = System.currentTimeMillis() - startTime;
tracingService.addAnnotation("operation_duration_ms", duration);
tracingService.addAnnotation("operation_success", true);
} catch (Exception e) {
long duration = System.currentTimeMillis() - startTime;
tracingService.addAnnotation("operation_duration_ms", duration);
tracingService.addAnnotation("operation_success", false);
throw e;
}
});
}
private String obfuscateSql(String sql) {
// Basic SQL obfuscation for security
if (sql == null) return null;
return sql.replaceAll("\\b\\d+\\b", "?")
.replaceAll("'.*?'", "?")
.replaceAll("\\b(?:password|pwd|secret|token|key)\\s*=\\s*'.*?'", "?");
}
private String getQueryType(String sql) {
if (sql == null) return "unknown";
String lowerSql = sql.trim().toLowerCase();
if (lowerSql.startsWith("select")) return "select";
if (lowerSql.startsWith("insert")) return "insert";
if (lowerSql.startsWith("update")) return "update";
if (lowerSql.startsWith("delete")) return "delete";
if (lowerSql.startsWith("create")) return "create";
if (lowerSql.startsWith("drop")) return "drop";
return "other";
}
}

Step 6: Spring Boot Integration

@Aspect
@Component
@Slf4j
public class XRayTracingAspect {
private final XRayTracingService tracingService;
public XRayTracingAspect(XRayTracingService tracingService) {
this.tracingService = tracingService;
}
@Around("@annotation(traceable)")
public Object traceMethod(ProceedingJoinPoint joinPoint, Traceable traceable) throws Throwable {
String methodName = getMethodName(joinPoint);
String namespace = traceable.namespace();
return tracingService.traceMethod(methodName, namespace, () -> {
try {
// Add method parameters as metadata
addMethodParametersToXRay(joinPoint);
return joinPoint.proceed();
} catch (Throwable throwable) {
if (throwable instanceof RuntimeException) {
throw (RuntimeException) throwable;
} else {
throw new RuntimeException(throwable);
}
}
});
}
@Around("execution(* com.yourcompany..*Service.*(..))")
public Object traceServiceMethods(ProceedingJoinPoint joinPoint) throws Throwable {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
String segmentName = className + "." + methodName;
return tracingService.traceMethod(segmentName, "local", () -> {
try {
addMethodParametersToXRay(joinPoint);
return joinPoint.proceed();
} catch (Throwable throwable) {
if (throwable instanceof RuntimeException) {
throw (RuntimeException) throwable;
} else {
throw new RuntimeException(throwable);
}
}
});
}
private String getMethodName(ProceedingJoinPoint joinPoint) {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
return className + "." + methodName;
}
private void addMethodParametersToXRay(ProceedingJoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
String[] parameterNames = getParameterNames(joinPoint);
for (int i = 0; i < args.length; i++) {
String paramName = parameterNames != null && i < parameterNames.length ? 
parameterNames[i] : "param" + i;
Object paramValue = args[i];
if (paramValue != null && !isSensitiveParameter(paramName)) {
tracingService.addMetadata("method_parameters", paramName, paramValue.toString());
}
}
}
private String[] getParameterNames(ProceedingJoinPoint joinPoint) {
// Implementation to get parameter names
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
return signature.getParameterNames();
}
private boolean isSensitiveParameter(String paramName) {
String lowerParamName = paramName.toLowerCase();
return lowerParamName.contains("password") || 
lowerParamName.contains("secret") || 
lowerParamName.contains("token") || 
lowerParamName.contains("key");
}
}
// Custom annotation for tracing
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Traceable {
String namespace() default "local";
boolean captureArgs() default true;
}
@ControllerAdvice
@Slf4j
public class XRayExceptionHandler {
private final XRayTracingService tracingService;
public XRayExceptionHandler(XRayTracingService tracingService) {
this.tracingService = tracingService;
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e, HttpServletRequest request) {
// Record exception in X-Ray
tracingService.addAnnotation("error_type", e.getClass().getSimpleName());
tracingService.addAnnotation("error_message", e.getMessage());
tracingService.addMetadata("exception", "stack_trace", getStackTrace(e));
log.error("Request failed with exception", e);
ErrorResponse errorResponse = new ErrorResponse(
"INTERNAL_ERROR", 
"An unexpected error occurred",
tracingService.getTraceId()
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
}
private String getStackTrace(Exception e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
return sw.toString();
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class ErrorResponse {
private String errorCode;
private String message;
private String traceId;
private Instant timestamp = Instant.now();
}

Step 7: Service Implementation with X-Ray

@Service
@Slf4j
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
private final InventoryService inventoryService;
private final XRayTracingService tracingService;
private final DatabaseTracingService databaseTracingService;
public OrderService(OrderRepository orderRepository,
PaymentService paymentService,
InventoryService inventoryService,
XRayTracingService tracingService,
DatabaseTracingService databaseTracingService) {
this.orderRepository = orderRepository;
this.paymentService = paymentService;
this.inventoryService = inventoryService;
this.tracingService = tracingService;
this.databaseTracingService = databaseTracingService;
}
@Traceable(namespace = "business")
public Order processOrder(Order order) {
// Add business context to X-Ray
tracingService.addAnnotation("order_id", order.getId());
tracingService.addAnnotation("customer_id", order.getCustomerId());
tracingService.addAnnotation("order_amount", order.getAmount());
tracingService.addMetadata("order", "currency", order.getCurrency());
try {
// Validate order
validateOrder(order);
// Process payment with tracing
PaymentResult paymentResult = tracingService.traceMethod(
"processPayment", "remote", 
() -> paymentService.processPayment(order)
);
// Update inventory with tracing
tracingService.traceMethod("updateInventory", "remote", 
() -> inventoryService.updateInventory(order)
);
// Save order to database with tracing
Order savedOrder = databaseTracingService.traceQuery(
"saveOrder", 
"INSERT INTO orders ...", 
() -> orderRepository.save(order)
);
// Record business metrics
tracingService.addAnnotation("order_status", "completed");
tracingService.addMetadata("business", "revenue", order.getAmount());
log.info("Order processed successfully: {}", order.getId());
return savedOrder;
} catch (Exception e) {
tracingService.addAnnotation("order_status", "failed");
tracingService.addMetadata("error", "order_processing_error", e.getMessage());
throw e;
}
}
@Traceable(namespace = "database")
public List<Order> getOrdersByCustomer(String customerId, Pageable pageable) {
tracingService.addAnnotation("customer_id", customerId);
tracingService.addAnnotation("page_size", pageable.getPageSize());
String sql = "SELECT * FROM orders WHERE customer_id = ? LIMIT ? OFFSET ?";
return databaseTracingService.traceQuery(
"getOrdersByCustomer", sql,
() -> orderRepository.findByCustomerId(customerId, pageable)
);
}
@Traceable(namespace = "background")
public void processBatchOrders(List<Order> orders) {
tracingService.addAnnotation("batch_size", orders.size());
int successCount = 0;
int errorCount = 0;
for (Order order : orders) {
try {
processOrder(order);
successCount++;
} catch (Exception e) {
errorCount++;
log.error("Failed to process order: {}", order.getId(), e);
}
}
// Add batch processing results to X-Ray
tracingService.addAnnotation("batch_success_count", successCount);
tracingService.addAnnotation("batch_error_count", errorCount);
tracingService.addAnnotation("batch_success_rate", 
orders.size() > 0 ? (double) successCount / orders.size() : 0);
}
private void validateOrder(Order order) {
tracingService.traceMethod("validateOrder", "local", () -> {
if (order.getAmount() <= 0) {
throw new IllegalArgumentException("Invalid order amount");
}
if (order.getCustomerId() == null || order.getCustomerId().trim().isEmpty()) {
throw new IllegalArgumentException("Customer ID is required");
}
});
}
}
@RestController
@Slf4j
public class OrderController {
private final OrderService orderService;
private final XRayTracingService tracingService;
public OrderController(OrderService orderService, XRayTracingService tracingService) {
this.orderService = orderService;
this.tracingService = tracingService;
}
@PostMapping("/orders")
public ResponseEntity<Order> createOrder(@RequestBody Order order) {
log.info("Creating order for customer: {}", order.getCustomerId());
// Add HTTP request context to X-Ray
tracingService.addAnnotation("http_method", "POST");
tracingService.addAnnotation("http_path", "/orders");
tracingService.addMetadata("request", "content_type", "application/json");
try {
Order processedOrder = orderService.processOrder(order);
// Add response context
tracingService.addAnnotation("http_status", 200);
tracingService.addMetadata("response", "order_id", processedOrder.getId());
return ResponseEntity.ok(processedOrder);
} catch (Exception e) {
tracingService.addAnnotation("http_status", 500);
throw e;
}
}
@GetMapping("/orders/customer/{customerId}")
public ResponseEntity<List<Order>> getCustomerOrders(
@PathVariable String customerId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
tracingService.addAnnotation("http_method", "GET");
tracingService.addAnnotation("http_path", "/orders/customer/{customerId}");
tracingService.addAnnotation("customer_id", customerId);
try {
Pageable pageable = PageRequest.of(page, size);
List<Order> orders = orderService.getOrdersByCustomer(customerId, pageable);
tracingService.addAnnotation("http_status", 200);
tracingService.addAnnotation("result_count", orders.size());
return ResponseEntity.ok(orders);
} catch (Exception e) {
tracingService.addAnnotation("http_status", 500);
throw e;
}
}
@GetMapping("/health")
public ResponseEntity<HealthResponse> healthCheck() {
// Health check endpoint - minimal tracing
HealthResponse health = new HealthResponse("healthy", tracingService.getTraceId());
return ResponseEntity.ok(health);
}
}
@Data
@AllArgsConstructor
class HealthResponse {
private String status;
private String traceId;
private Instant timestamp = Instant.now();
}

Step 8: AWS Service Integration

@Service
@Slf4j
public class AwsServiceIntegration {
private final S3Client s3Client;
private final DynamoDbClient dynamoDbClient;
private final SqsClient sqsClient;
private final XRayTracingService tracingService;
public AwsServiceIntegration(S3Client s3Client,
DynamoDbClient dynamoDbClient,
SqsClient sqsClient,
XRayTracingService tracingService) {
this.s3Client = s3Client;
this.dynamoDbClient = dynamoDbClient;
this.sqsClient = sqsClient;
this.tracingService = tracingService;
}
@Traceable(namespace = "aws")
public String uploadToS3(String bucketName, String key, byte[] data) {
return tracingService.traceMethod("s3Upload", "aws", () -> {
try {
PutObjectRequest request = PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.contentLength((long) data.length)
.build();
s3Client.putObject(request, RequestBody.fromBytes(data));
tracingService.addAnnotation("s3_bucket", bucketName);
tracingService.addAnnotation("s3_key", key);
tracingService.addAnnotation("s3_operation", "putObject");
log.info("Successfully uploaded file to S3: {}/{}", bucketName, key);
return String.format("s3://%s/%s", bucketName, key);
} catch (Exception e) {
tracingService.addAnnotation("s3_operation_failed", true);
throw new RuntimeException("S3 upload failed", e);
}
});
}
@Traceable(namespace = "aws")
public void saveToDynamoDB(String tableName, Map<String, AttributeValue> item) {
tracingService.traceMethod("dynamoDbPut", "aws", () -> {
try {
PutItemRequest request = PutItemRequest.builder()
.tableName(tableName)
.item(item)
.build();
dynamoDbClient.putItem(request);
tracingService.addAnnotation("dynamodb_table", tableName);
tracingService.addAnnotation("dynamodb_operation", "putItem");
log.info("Successfully saved item to DynamoDB table: {}", tableName);
} catch (Exception e) {
tracingService.addAnnotation("dynamodb_operation_failed", true);
throw new RuntimeException("DynamoDB put failed", e);
}
});
}
@Traceable(namespace = "aws")
public String sendSqsMessage(String queueUrl, String messageBody) {
return tracingService.traceMethod("sqsSend", "aws", () -> {
try {
SendMessageRequest request = SendMessageRequest.builder()
.queueUrl(queueUrl)
.messageBody(messageBody)
.build();
SendMessageResponse response = sqsClient.sendMessage(request);
tracingService.addAnnotation("sqs_queue_url", queueUrl);
tracingService.addAnnotation("sqs_operation", "sendMessage");
tracingService.addMetadata("sqs", "message_id", response.messageId());
log.info("Successfully sent message to SQS: {}", response.messageId());
return response.messageId();
} catch (Exception e) {
tracingService.addAnnotation("sqs_operation_failed", true);
throw new RuntimeException("SQS send failed", e);
}
});
}
}

Step 9: Testing and Verification

@SpringBootTest
@Slf4j
class XRayIntegrationTest {
@Autowired
private XRayTracingService tracingService;
@Autowired
private OrderService orderService;
@MockBean
private OrderRepository orderRepository;
@Test
void testTracingService() {
String result = tracingService.traceMethod("testOperation", "test", () -> {
tracingService.addAnnotation("test_key", "test_value");
return "success";
});
assertEquals("success", result);
}
@Test
void testMethodTracingWithException() {
assertThrows(RuntimeException.class, () -> {
tracingService.traceMethod("failingOperation", "test", () -> {
throw new RuntimeException("Test exception");
});
});
}
@Test
void testOrderProcessingTracing() {
Order testOrder = new Order("test-order", "test-customer", 100.0);
when(orderRepository.save(any(Order.class))).thenReturn(testOrder);
Order result = orderService.processOrder(testOrder);
assertNotNull(result);
assertEquals("test-order", result.getId());
// Verify tracing was applied
verify(orderRepository, times(1)).save(any(Order.class));
}
@Test
void testBackgroundJobTracing() {
String result = tracingService.traceBackgroundJob("testJob", () -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "job-completed";
});
assertEquals("job-completed", result);
}
}
// Test configuration
@TestConfiguration
class XRayTestConfig {
@Bean
@Primary
public AWSXRayRecorder testXRayRecorder() {
return AWSXRayRecorderBuilder.standard()
.withSamplingStrategy(new NoSamplingStrategy())
.withSegmentListener(new NoOpSegmentListener())
.build();
}
}

Step 10: Deployment Configuration

Dockerfile:

FROM openjdk:11-jre-slim
# Install X-Ray daemon
RUN apt-get update && apt-get install -y curl
RUN curl -o /tmp/xray.deb https://s3.dualstack.us-east-2.amazonaws.com/aws-xray-assets.us-east-2/xray-daemon/aws-xray-daemon-3.x.deb
RUN dpkg -i /tmp/xray.deb
# Copy application
COPY target/application.jar /app/application.jar
# X-Ray daemon configuration
ENV AWS_REGION=us-east-1
ENV AWS_XRAY_DAEMON_ADDRESS=0.0.0.0:2000
# Start X-Ray daemon and application
CMD xray -b 0.0.0.0:2000 & java -jar /app/application.jar

Kubernetes Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
labels:
app: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
annotations:
# X-Ray sidecar configuration
proxy.istio.io/config: |
tracing:
zipkin:
address: xray-daemon:2000
spec:
containers:
- name: order-service
image: order-service:latest
env:
- name: AWS_XRAY_DAEMON_ADDRESS
value: "xray-daemon:2000"
- name: AWS_REGION
value: "us-east-1"
- name: SPRING_PROFILES_ACTIVE
value: "production"
ports:
- containerPort: 8080
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
# X-Ray daemon sidecar
- name: xray-daemon
image: amazon/aws-xray-daemon:latest
env:
- name: AWS_REGION
value: "us-east-1"
ports:
- containerPort: 2000
protocol: UDP
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "200m"

Key Features Implemented

  1. Distributed Tracing: End-to-end request tracing across services
  2. AWS Service Integration: Automatic tracing for S3, DynamoDB, SQS
  3. Database Tracing: SQL query performance monitoring
  4. HTTP Client Tracing: Outbound HTTP request tracing
  5. Custom Annotations: Business context and metadata
  6. Performance Analysis: Latency and bottleneck identification
  7. Error Tracking: Comprehensive error and exception tracking
  8. Spring Boot Integration: Seamless Spring application integration

Best Practices

  1. Meaningful Segment Names: Use descriptive names for segments and subsegments
  2. Namespace Organization: Use namespaces to categorize tracing data
  3. Security: Obfuscate sensitive data in traces and metadata
  4. Sampling Strategy: Configure appropriate sampling for your workload
  5. Error Handling: Properly capture and annotate exceptions
  6. Performance: Use async operations where appropriate
  7. Monitoring: Set up alerts based on trace data and performance metrics

This comprehensive AWS X-Ray implementation provides robust distributed tracing for Java applications, enabling deep insights into application performance, error detection, and dependency analysis in microservices architectures.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper