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
- Distributed Tracing: End-to-end request tracing across services
- AWS Service Integration: Automatic tracing for S3, DynamoDB, SQS
- Database Tracing: SQL query performance monitoring
- HTTP Client Tracing: Outbound HTTP request tracing
- Custom Annotations: Business context and metadata
- Performance Analysis: Latency and bottleneck identification
- Error Tracking: Comprehensive error and exception tracking
- Spring Boot Integration: Seamless Spring application integration
Best Practices
- Meaningful Segment Names: Use descriptive names for segments and subsegments
- Namespace Organization: Use namespaces to categorize tracing data
- Security: Obfuscate sensitive data in traces and metadata
- Sampling Strategy: Configure appropriate sampling for your workload
- Error Handling: Properly capture and annotate exceptions
- Performance: Use async operations where appropriate
- 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.