Google Cloud Trace in Java: Complete Distributed Tracing Guide

Introduction to Google Cloud Trace

Google Cloud Trace is a distributed tracing system that collects latency data from applications and displays it in the Google Cloud Console. It helps developers understand how requests propagate through microservices and identify performance bottlenecks.

Key Features

  • Distributed Tracing: Track requests across microservices
  • Performance Analysis: Identify latency issues and bottlenecks
  • Integration with Google Cloud: Native integration with GCP services
  • Real-time Monitoring: Live trace data and analysis
  • Root Cause Analysis: Quickly identify failure points

Implementation Guide

Dependencies

Add to your pom.xml:

<properties>
<google.cloud.trace.version>2.27.0</google.cloud.trace.version>
<opentelemetry.version>1.34.1</opentelemetry.version>
<grpc.version>1.59.0</grpc.version>
</properties>
<dependencies>
<!-- Google Cloud Trace -->
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-trace</artifactId>
<version>${google.cloud.trace.version}</version>
</dependency>
<!-- OpenTelemetry for tracing -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>${opentelemetry.version}</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<version>${opentelemetry.version}</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-google-cloud-trace</artifactId>
<version>1.0.0</version>
</dependency>
<!-- For gRPC services -->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>${grpc.version}</version>
</dependency>
<!-- For web applications -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.1.5</version>
</dependency>
<!-- For HTTP client tracing -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
</dependency>
</dependencies>

Core Cloud Trace Implementation

Configuration and Setup

package com.example.gcptrace;
import com.google.cloud.trace.v2.TraceServiceClient;
import com.google.devtools.cloudtrace.v2.TraceSpan;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.trace.TracerProvider;
import io.opentelemetry.exporter.cloudtrace.CloudTraceSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class CloudTraceConfig {
private static final String INSTRUMENTATION_NAME = "com.example.gcptrace";
private static Tracer tracer;
private static TraceServiceClient traceServiceClient;
public static void initialize() throws IOException {
// Initialize Cloud Trace client
traceServiceClient = TraceServiceClient.create();
// Initialize OpenTelemetry with Cloud Trace exporter
CloudTraceSpanExporter cloudTraceExporter = CloudTraceSpanExporter.create();
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(
BatchSpanProcessor.builder(cloudTraceExporter)
.setScheduleDelay(100, TimeUnit.MILLISECONDS)
.setMaxExportBatchSize(512)
.build()
)
.build();
OpenTelemetrySdk openTelemetrySdk = OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.build();
GlobalOpenTelemetry.set(openTelemetrySdk);
tracer = openTelemetrySdk.getTracer(INSTRUMENTATION_NAME);
}
public static Tracer getTracer() {
if (tracer == null) {
throw new IllegalStateException("CloudTrace not initialized. Call initialize() first.");
}
return tracer;
}
public static TraceServiceClient getTraceServiceClient() {
if (traceServiceClient == null) {
throw new IllegalStateException("TraceServiceClient not initialized. Call initialize() first.");
}
return traceServiceClient;
}
public static void shutdown() {
if (traceServiceClient != null) {
traceServiceClient.close();
}
OpenTelemetrySdk openTelemetrySdk = (OpenTelemetrySdk) GlobalOpenTelemetry.get();
if (openTelemetrySdk != null) {
openTelemetrySdk.getSdkTracerProvider().shutdown();
}
}
}

Cloud Trace Manager

package com.example.gcptrace;
import com.google.devtools.cloudtrace.v2.Span;
import com.google.devtools.cloudtrace.v2.TruncatableString;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.SpanContext;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
public class CloudTraceManager {
private static final AttributeKey<String> CUSTOM_ATTRIBUTE_PREFIX = AttributeKey.stringKey("custom.");
public static <T> T traceOperation(String operationName, Supplier<T> operation) {
return traceOperation(operationName, SpanKind.INTERNAL, Map.of(), operation);
}
public static <T> T traceOperation(String operationName, Map<String, Object> attributes, 
Supplier<T> operation) {
return traceOperation(operationName, SpanKind.INTERNAL, attributes, operation);
}
public static <T> T traceOperation(String operationName, SpanKind spanKind, 
Map<String, Object> attributes, Supplier<T> operation) {
io.opentelemetry.api.trace.Span span = CloudTraceConfig.getTracer()
.spanBuilder(operationName)
.setSpanKind(spanKind)
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Set attributes
attributes.forEach((key, value) -> {
if (value instanceof String) {
span.setAttribute(key, (String) value);
} else if (value instanceof Long) {
span.setAttribute(AttributeKey.longKey(key), (Long) value);
} else if (value instanceof Double) {
span.setAttribute(AttributeKey.doubleKey(key), (Double) value);
} else if (value instanceof Boolean) {
span.setAttribute(AttributeKey.booleanKey(key), (Boolean) value);
} else {
span.setAttribute(key, value.toString());
}
});
T result = operation.get();
span.setStatus(StatusCode.OK);
return result;
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
span.end();
}
}
public static void traceOperation(String operationName, Runnable operation) {
traceOperation(operationName, SpanKind.INTERNAL, Map.of(), operation);
}
public static void traceOperation(String operationName, Map<String, Object> attributes, 
Runnable operation) {
traceOperation(operationName, SpanKind.INTERNAL, attributes, operation);
}
public static void traceOperation(String operationName, SpanKind spanKind, 
Map<String, Object> attributes, Runnable operation) {
io.opentelemetry.api.trace.Span span = CloudTraceConfig.getTracer()
.spanBuilder(operationName)
.setSpanKind(spanKind)
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Set attributes
attributes.forEach((key, value) -> {
if (value instanceof String) {
span.setAttribute(key, (String) value);
} else if (value instanceof Long) {
span.setAttribute(AttributeKey.longKey(key), (Long) value);
} else if (value instanceof Double) {
span.setAttribute(AttributeKey.doubleKey(key), (Double) value);
} else if (value instanceof Boolean) {
span.setAttribute(AttributeKey.booleanKey(key), (Boolean) value);
} else {
span.setAttribute(key, value.toString());
}
});
operation.run();
span.setStatus(StatusCode.OK);
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
span.end();
}
}
public static void addCustomAttribute(String key, Object value) {
io.opentelemetry.api.trace.Span currentSpan = Span.current();
if (!currentSpan.getSpanContext().isValid()) {
return; // No active span
}
String attributeKey = CUSTOM_ATTRIBUTE_PREFIX.getKey() + key;
if (value instanceof String) {
currentSpan.setAttribute(attributeKey, (String) value);
} else if (value instanceof Long) {
currentSpan.setAttribute(AttributeKey.longKey(attributeKey), (Long) value);
} else if (value instanceof Double) {
currentSpan.setAttribute(AttributeKey.doubleKey(attributeKey), (Double) value);
} else if (value instanceof Boolean) {
currentSpan.setAttribute(AttributeKey.booleanKey(attributeKey), (Boolean) value);
} else {
currentSpan.setAttribute(attributeKey, value.toString());
}
}
public static void addEvent(String eventName, Map<String, Object> attributes) {
io.opentelemetry.api.trace.Span currentSpan = Span.current();
if (!currentSpan.getSpanContext().isValid()) {
return;
}
Attributes eventAttributes = buildAttributes(attributes);
currentSpan.addEvent(eventName, eventAttributes);
}
public static String getCurrentTraceId() {
SpanContext spanContext = Span.current().getSpanContext();
return spanContext.isValid() ? spanContext.getTraceId() : null;
}
public static String getCurrentSpanId() {
SpanContext spanContext = Span.current().getSpanContext();
return spanContext.isValid() ? spanContext.getSpanId() : null;
}
public static boolean isSampled() {
SpanContext spanContext = Span.current().getSpanContext();
return spanContext.isValid() && spanContext.isSampled();
}
private static Attributes buildAttributes(Map<String, Object> attributes) {
Attributes.Builder builder = Attributes.builder();
attributes.forEach((key, value) -> {
if (value instanceof String) {
builder.put(AttributeKey.stringKey(key), (String) value);
} else if (value instanceof Long) {
builder.put(AttributeKey.longKey(key), (Long) value);
} else if (value instanceof Double) {
builder.put(AttributeKey.doubleKey(key), (Double) value);
} else if (value instanceof Boolean) {
builder.put(AttributeKey.booleanKey(key), (Boolean) value);
} else {
builder.put(AttributeKey.stringKey(key), value.toString());
}
});
return builder.build();
}
public static Map<String, String> getTraceContext() {
SpanContext spanContext = Span.current().getSpanContext();
if (!spanContext.isValid()) {
return Map.of();
}
Map<String, String> context = new HashMap<>();
context.put("traceId", spanContext.getTraceId());
context.put("spanId", spanContext.getSpanId());
context.put("traceFlags", spanContext.getTraceFlags().asHex());
context.put("traceState", spanContext.getTraceState().toString());
context.put("sampled", String.valueOf(spanContext.isSampled()));
return context;
}
}

Spring Boot Integration

Configuration Class

package com.example.gcptrace.config;
import com.example.gcptrace.CloudTraceConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PreDestroy;
import java.io.IOException;
@Configuration
public class TraceConfig {
private static final Logger logger = LoggerFactory.getLogger(TraceConfig.class);
@Bean
public void initializeCloudTrace() {
try {
CloudTraceConfig.initialize();
logger.info("Google Cloud Trace initialized successfully");
} catch (IOException e) {
logger.error("Failed to initialize Google Cloud Trace", e);
throw new RuntimeException("Cloud Trace initialization failed", e);
}
}
@PreDestroy
public void shutdownCloudTrace() {
try {
CloudTraceConfig.shutdown();
logger.info("Google Cloud Trace shutdown successfully");
} catch (Exception e) {
logger.error("Error during Cloud Trace shutdown", e);
}
}
}

HTTP Filter for Trace Propagation

package com.example.gcptrace.web;
import com.example.gcptrace.CloudTraceManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;
@Component
public class CloudTraceFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(CloudTraceFilter.class);
private static final String TRACE_HEADER = "X-Cloud-Trace-Context";
private static final String TRACE_ID_HEADER = "X-Trace-ID";
private static final String SPAN_ID_HEADER = "X-Span-ID";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, 
FilterChain filterChain) throws ServletException, IOException {
// Extract or generate trace context
String traceContext = extractOrGenerateTraceContext(request);
// Create span for the HTTP request
CloudTraceManager.traceOperation("HTTP " + request.getMethod(), 
Map.of(
"http.method", request.getMethod(),
"http.url", request.getRequestURL().toString(),
"http.path", request.getRequestURI(),
"http.user_agent", request.getHeader("User-Agent"),
"http.client_ip", getClientIp(request)
),
() -> {
try {
// Add trace headers to response
addTraceHeadersToResponse(response);
// Continue with the filter chain
filterChain.doFilter(request, response);
// Add response attributes
CloudTraceManager.addCustomAttribute("http.status_code", 
response.getStatus());
CloudTraceManager.addCustomAttribute("http.response_size", 
response.getBufferSize());
} catch (Exception e) {
CloudTraceManager.addCustomAttribute("http.error", e.getMessage());
throw new RuntimeException(e);
}
});
}
private String extractOrGenerateTraceContext(HttpServletRequest request) {
String traceHeader = request.getHeader(TRACE_HEADER);
if (traceHeader != null && !traceHeader.trim().isEmpty()) {
return traceHeader;
}
// Generate new trace context
String traceId = generateTraceId();
String spanId = generateSpanId();
return String.format("%s/%s;o=1", traceId, spanId);
}
private String generateTraceId() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 32);
}
private String generateSpanId() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}
private void addTraceHeadersToResponse(HttpServletResponse response) {
String traceId = CloudTraceManager.getCurrentTraceId();
String spanId = CloudTraceManager.getCurrentSpanId();
if (traceId != null && spanId != null) {
response.setHeader(TRACE_ID_HEADER, traceId);
response.setHeader(SPAN_ID_HEADER, spanId);
response.setHeader(TRACE_HEADER, 
String.format("%s/%s;o=1", traceId, spanId));
}
}
private String getClientIp(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
}

REST Controller with Tracing

package com.example.gcptrace.controller;
import com.example.gcptrace.CloudTraceManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api")
public class OrderController {
private static final Logger logger = LoggerFactory.getLogger(OrderController.class);
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping("/orders")
public ResponseEntity<Map<String, Object>> createOrder(@RequestBody Map<String, Object> orderRequest) {
return CloudTraceManager.traceOperation("CreateOrder", 
Map.of(
"order.customer_id", orderRequest.get("customerId"),
"order.total_amount", orderRequest.get("totalAmount"),
"order.item_count", orderRequest.getOrDefault("items", Map.of()).toString().length()
),
() -> {
try {
logger.info("Creating new order for customer: {}", orderRequest.get("customerId"));
// Process the order
Map<String, Object> order = orderService.processOrder(orderRequest);
// Add success event
CloudTraceManager.addEvent("order.created", 
Map.of("orderId", order.get("orderId"), "status", "success"));
logger.info("Order created successfully: {}", order.get("orderId"));
return ResponseEntity.ok(order);
} catch (Exception e) {
logger.error("Failed to create order", e);
CloudTraceManager.addEvent("order.failed", 
Map.of("error", e.getMessage()));
throw e;
}
});
}
@GetMapping("/orders/{orderId}")
public ResponseEntity<Map<String, Object>> getOrder(@PathVariable String orderId) {
return CloudTraceManager.traceOperation("GetOrder", 
Map.of("order.id", orderId),
() -> {
logger.debug("Fetching order: {}", orderId);
Map<String, Object> order = orderService.findOrderById(orderId);
if (order == null) {
CloudTraceManager.addCustomAttribute("order.found", false);
return ResponseEntity.notFound().build();
}
CloudTraceManager.addCustomAttribute("order.found", true);
CloudTraceManager.addCustomAttribute("order.status", order.get("status"));
logger.debug("Order found: {}", orderId);
return ResponseEntity.ok(order);
});
}
@GetMapping("/orders/{orderId}/items")
public ResponseEntity<Map<String, Object>> getOrderItems(@PathVariable String orderId) {
return CloudTraceManager.traceOperation("GetOrderItems", 
Map.of("order.id", orderId),
() -> {
logger.debug("Fetching items for order: {}", orderId);
Map<String, Object> items = orderService.getOrderItems(orderId);
CloudTraceManager.addCustomAttribute("items.count", 
items.getOrDefault("count", 0));
return ResponseEntity.ok(items);
});
}
}

Service Layer with Distributed Tracing

package com.example.gcptrace.service;
import com.example.gcptrace.CloudTraceManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Service
public class OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
private final InventoryService inventoryService;
private final PaymentService paymentService;
private final ShippingService shippingService;
public OrderService(InventoryService inventoryService, 
PaymentService paymentService,
ShippingService shippingService) {
this.inventoryService = inventoryService;
this.paymentService = paymentService;
this.shippingService = shippingService;
}
public Map<String, Object> processOrder(Map<String, Object> orderRequest) {
return CloudTraceManager.traceOperation("OrderService.processOrder", 
Map.of(
"customer.id", orderRequest.get("customerId"),
"order.total", orderRequest.get("totalAmount"),
"order.currency", orderRequest.getOrDefault("currency", "USD")
),
() -> {
String orderId = generateOrderId();
logger.info("Processing order {} for customer {}", 
orderId, orderRequest.get("customerId"));
// Step 1: Validate inventory
CloudTraceManager.traceOperation("ValidateInventory", () -> {
boolean inventoryValid = inventoryService.validateInventory(orderRequest);
if (!inventoryValid) {
throw new RuntimeException("Inventory validation failed");
}
CloudTraceManager.addEvent("inventory.validated", 
Map.of("orderId", orderId, "result", "success"));
});
// Step 2: Process payment
CloudTraceManager.traceOperation("ProcessPayment", 
Map.of("payment.amount", orderRequest.get("totalAmount")),
() -> {
String paymentId = paymentService.processPayment(orderRequest);
CloudTraceManager.addCustomAttribute("payment.id", paymentId);
CloudTraceManager.addEvent("payment.processed", 
Map.of("paymentId", paymentId, "status", "completed"));
});
// Step 3: Create shipping
CloudTraceManager.traceOperation("CreateShipping", () -> {
String shippingId = shippingService.createShipping(orderRequest);
CloudTraceManager.addCustomAttribute("shipping.id", shippingId);
CloudTraceManager.addEvent("shipping.created", 
Map.of("shippingId", shippingId, "status", "created"));
});
// Step 4: Update inventory
CloudTraceManager.traceOperation("UpdateInventory", () -> {
inventoryService.updateInventory(orderRequest);
CloudTraceManager.addEvent("inventory.updated", 
Map.of("orderId", orderId, "action", "deduct"));
});
Map<String, Object> order = new HashMap<>();
order.put("orderId", orderId);
order.put("status", "COMPLETED");
order.put("message", "Order processed successfully");
order.put("timestamp", System.currentTimeMillis());
order.put("traceId", CloudTraceManager.getCurrentTraceId());
logger.info("Order {} processed successfully", orderId);
return order;
});
}
public Map<String, Object> findOrderById(String orderId) {
return CloudTraceManager.traceOperation("OrderService.findOrderById", 
Map.of("order.id", orderId),
() -> {
logger.debug("Looking up order: {}", orderId);
// Simulate database lookup
try {
Thread.sleep(50); // Simulate DB latency
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Order lookup interrupted", e);
}
Map<String, Object> order = new HashMap<>();
order.put("orderId", orderId);
order.put("status", "COMPLETED");
order.put("customerId", "cust-" + orderId.hashCode());
order.put("totalAmount", 99.99);
order.put("createdAt", System.currentTimeMillis() - 3600000);
CloudTraceManager.addCustomAttribute("order.exists", true);
return order;
});
}
public Map<String, Object> getOrderItems(String orderId) {
return CloudTraceManager.traceOperation("OrderService.getOrderItems", 
Map.of("order.id", orderId),
() -> {
logger.debug("Fetching items for order: {}", orderId);
// Simulate items lookup
try {
Thread.sleep(30);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Items lookup interrupted", e);
}
Map<String, Object> items = new HashMap<>();
items.put("orderId", orderId);
items.put("count", 3);
items.put("items", new String[]{"item1", "item2", "item3"});
CloudTraceManager.addCustomAttribute("items.found", 3);
return items;
});
}
private String generateOrderId() {
return "order-" + UUID.randomUUID().toString().substring(0, 8);
}
}

External Service Integration

HTTP Client with Trace Propagation

package com.example.gcptrace.http;
import com.example.gcptrace.CloudTraceManager;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
@Component
public class TraceAwareHttpClient {
private static final Logger logger = LoggerFactory.getLogger(TraceAwareHttpClient.class);
private final CloseableHttpClient httpClient;
public TraceAwareHttpClient() {
this.httpClient = HttpClients.createDefault();
}
public String get(String url) throws IOException {
return CloudTraceManager.traceOperation("HTTP_GET", 
Map.of("http.url", url, "http.method", "GET"),
() -> {
HttpGet request = new HttpGet(url);
injectTraceHeaders(request);
logger.debug("Making GET request to: {}", url);
return httpClient.execute(request, response -> {
int statusCode = response.getCode();
CloudTraceManager.addCustomAttribute("http.status_code", statusCode);
if (statusCode >= 200 && statusCode < 300) {
return EntityUtils.toString(response.getEntity());
} else {
throw new IOException("HTTP request failed with status: " + statusCode);
}
});
});
}
public String post(String url, String body, String contentType) throws IOException {
return CloudTraceManager.traceOperation("HTTP_POST", 
Map.of("http.url", url, "http.method", "POST", "content.type", contentType),
() -> {
HttpPost request = new HttpPost(url);
injectTraceHeaders(request);
StringEntity entity = new StringEntity(body);
entity.setContentType(contentType);
request.setEntity(entity);
logger.debug("Making POST request to: {}", url);
return httpClient.execute(request, response -> {
int statusCode = response.getCode();
CloudTraceManager.addCustomAttribute("http.status_code", statusCode);
if (statusCode >= 200 && statusCode < 300) {
return EntityUtils.toString(response.getEntity());
} else {
throw new IOException("HTTP request failed with status: " + statusCode);
}
});
});
}
private void injectTraceHeaders(HttpUriRequest request) {
Map<String, String> traceContext = CloudTraceManager.getTraceContext();
// Add W3C Trace Context headers
if (traceContext.containsKey("traceId") && traceContext.containsKey("spanId")) {
String traceParent = String.format("00-%s-%s-01", 
traceContext.get("traceId"), traceContext.get("spanId"));
request.setHeader("traceparent", traceParent);
}
// Add Google Cloud Trace headers
if (traceContext.containsKey("traceId")) {
String cloudTraceContext = String.format("%s/%s;o=1", 
traceContext.get("traceId"), traceContext.get("spanId"));
request.setHeader("X-Cloud-Trace-Context", cloudTraceContext);
}
// Add custom trace headers
traceContext.forEach((key, value) -> {
request.setHeader("X-Trace-" + key.toUpperCase().replace("_", "-"), value);
});
}
public void close() throws IOException {
httpClient.close();
}
}

Database Service with Tracing

package com.example.gcptrace.database;
import com.example.gcptrace.CloudTraceManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Repository;
import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Repository
public class OrderRepository {
private static final Logger logger = LoggerFactory.getLogger(OrderRepository.class);
private final Connection connection;
public OrderRepository() throws SQLException {
// In production, use connection pool like HikariCP
String url = System.getenv("DATABASE_URL");
String username = System.getenv("DATABASE_USERNAME");
String password = System.getenv("DATABASE_PASSWORD");
this.connection = DriverManager.getConnection(url, username, password);
}
public void saveOrder(Map<String, Object> order) {
CloudTraceManager.traceOperation("Database.SaveOrder", 
Map.of(
"db.operation", "INSERT",
"db.table", "orders",
"order.id", order.get("orderId")
),
() -> {
String sql = "INSERT INTO orders (order_id, customer_id, total_amount, status, created_at) " +
"VALUES (?, ?, ?, ?, ?)";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, (String) order.get("orderId"));
stmt.setString(2, (String) order.get("customerId"));
stmt.setDouble(3, (Double) order.get("totalAmount"));
stmt.setString(4, (String) order.get("status"));
stmt.setTimestamp(5, new Timestamp(System.currentTimeMillis()));
int rowsAffected = stmt.executeUpdate();
CloudTraceManager.addCustomAttribute("db.rows_affected", rowsAffected);
logger.debug("Saved order {} to database", order.get("orderId"));
} catch (SQLException e) {
logger.error("Failed to save order to database", e);
CloudTraceManager.addCustomAttribute("db.error", e.getMessage());
throw new RuntimeException("Database operation failed", e);
}
});
}
public Map<String, Object> findOrderById(String orderId) {
return CloudTraceManager.traceOperation("Database.FindOrderById", 
Map.of(
"db.operation", "SELECT",
"db.table", "orders",
"order.id", orderId
),
() -> {
String sql = "SELECT * FROM orders WHERE order_id = ?";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, orderId);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
Map<String, Object> order = new HashMap<>();
order.put("orderId", rs.getString("order_id"));
order.put("customerId", rs.getString("customer_id"));
order.put("totalAmount", rs.getDouble("total_amount"));
order.put("status", rs.getString("status"));
order.put("createdAt", rs.getTimestamp("created_at"));
CloudTraceManager.addCustomAttribute("db.row_found", true);
logger.debug("Found order {} in database", orderId);
return order;
} else {
CloudTraceManager.addCustomAttribute("db.row_found", false);
logger.debug("Order {} not found in database", orderId);
return null;
}
}
} catch (SQLException e) {
logger.error("Failed to find order in database", e);
CloudTraceManager.addCustomAttribute("db.error", e.getMessage());
throw new RuntimeException("Database operation failed", e);
}
});
}
public List<Map<String, Object>> findOrdersByCustomer(String customerId) {
return CloudTraceManager.traceOperation("Database.FindOrdersByCustomer", 
Map.of(
"db.operation", "SELECT",
"db.table", "orders",
"customer.id", customerId
),
() -> {
String sql = "SELECT * FROM orders WHERE customer_id = ? ORDER BY created_at DESC";
List<Map<String, Object>> orders = new ArrayList<>();
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, customerId);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
Map<String, Object> order = new HashMap<>();
order.put("orderId", rs.getString("order_id"));
order.put("customerId", rs.getString("customer_id"));
order.put("totalAmount", rs.getDouble("total_amount"));
order.put("status", rs.getString("status"));
order.put("createdAt", rs.getTimestamp("created_at"));
orders.add(order);
}
}
CloudTraceManager.addCustomAttribute("db.rows_returned", orders.size());
logger.debug("Found {} orders for customer {}", orders.size(), customerId);
return orders;
} catch (SQLException e) {
logger.error("Failed to find orders by customer", e);
CloudTraceManager.addCustomAttribute("db.error", e.getMessage());
throw new RuntimeException("Database operation failed", e);
}
});
}
}

Advanced Tracing Features

Custom Span Exporter for Additional Processing

package com.example.gcptrace.export;
import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.trace.data.SpanData;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
public class CustomSpanExporter implements SpanExporter {
private static final Logger logger = LoggerFactory.getLogger(CustomSpanExporter.class);
private final SpanExporter delegate;
private final BlockingQueue<SpanData> spanQueue;
private final Thread processingThread;
private volatile boolean running = true;
public CustomSpanExporter(SpanExporter delegate) {
this.delegate = delegate;
this.spanQueue = new LinkedBlockingQueue<>(1000);
this.processingThread = new Thread(this::processSpans, "span-processor");
this.processingThread.setDaemon(true);
this.processingThread.start();
}
@Override
public CompletableResultCode export(Collection<SpanData> spans) {
// Add spans to queue for async processing
for (SpanData span : spans) {
if (!spanQueue.offer(span)) {
logger.warn("Span queue is full, dropping span: {}", span.getName());
}
}
// Also export via delegate
return delegate.export(spans);
}
private void processSpans() {
while (running) {
try {
SpanData span = spanQueue.poll(100, TimeUnit.MILLISECONDS);
if (span != null) {
processSpan(span);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
logger.error("Error processing span", e);
}
}
}
private void processSpan(SpanData span) {
// Custom processing logic
logger.debug("Processing span: {} (duration: {}ms)", 
span.getName(), 
(span.getEndEpochNanos() - span.getStartEpochNanos()) / 1_000_000);
// Example: Alert on slow spans
long durationMs = (span.getEndEpochNanos() - span.getStartEpochNanos()) / 1_000_000;
if (durationMs > 1000) {
logger.warn("Slow span detected: {} took {}ms", span.getName(), durationMs);
}
// Example: Track span statistics
// This could be sent to a metrics system
}
@Override
public CompletableResultCode flush() {
return delegate.flush();
}
@Override
public CompletableResultCode shutdown() {
running = false;
processingThread.interrupt();
return delegate.shutdown();
}
}

Trace Analytics and Monitoring

package com.example.gcptrace.analytics;
import com.example.gcptrace.CloudTraceManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicLong;
@Component
public class TraceAnalytics {
private static final Logger logger = LoggerFactory.getLogger(TraceAnalytics.class);
private final AtomicLong totalSpans = new AtomicLong(0);
private final AtomicLong errorSpans = new AtomicLong(0);
private final AtomicLong totalDuration = new AtomicLong(0);
public void recordSpan(String spanName, long durationMs, boolean error) {
totalSpans.incrementAndGet();
totalDuration.addAndGet(durationMs);
if (error) {
errorSpans.incrementAndGet();
}
// Record custom metrics
CloudTraceManager.addCustomAttribute("analytics.span.count", totalSpans.get());
CloudTraceManager.addCustomAttribute("analytics.span.errors", errorSpans.get());
if (durationMs > 1000) {
logger.warn("Slow operation detected: {} took {}ms", spanName, durationMs);
}
}
@Scheduled(fixedRate = 60000) // Every minute
public void reportMetrics() {
long total = totalSpans.get();
long errors = errorSpans.get();
long avgDuration = total > 0 ? totalDuration.get() / total : 0;
double errorRate = total > 0 ? (errors * 100.0) / total : 0;
logger.info("Trace Analytics - Total: {}, Errors: {}, Error Rate: {}%, Avg Duration: {}ms",
total, errors, String.format("%.2f", errorRate), avgDuration);
// Reset counters for next period
totalSpans.set(0);
errorSpans.set(0);
totalDuration.set(0);
}
public long getTotalSpans() {
return totalSpans.get();
}
public long getErrorSpans() {
return errorSpans.get();
}
public double getErrorRate() {
long total = totalSpans.get();
return total > 0 ? (errorSpans.get() * 100.0) / total : 0;
}
}

Testing Cloud Trace

Unit Tests

package com.example.gcptrace;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class CloudTraceManagerTest {
@Mock
private io.opentelemetry.api.trace.Tracer tracer;
@Mock
private io.opentelemetry.api.trace.Span span;
@Mock
private io.opentelemetry.api.trace.SpanBuilder spanBuilder;
@BeforeEach
void setUp() {
// Mock tracer setup
when(tracer.spanBuilder(any(String.class))).thenReturn(spanBuilder);
when(spanBuilder.setSpanKind(any())).thenReturn(spanBuilder);
when(spanBuilder.startSpan()).thenReturn(span);
}
@Test
void testTraceOperationSuccess() {
AtomicBoolean operationExecuted = new AtomicBoolean(false);
String result = CloudTraceManager.traceOperation("testOperation", 
Map.of("testKey", "testValue"),
() -> {
operationExecuted.set(true);
return "success";
});
assertTrue(operationExecuted.get());
assertEquals("success", result);
}
@Test
void testTraceOperationException() {
assertThrows(RuntimeException.class, () -> {
CloudTraceManager.traceOperation("testOperation", () -> {
throw new RuntimeException("Test exception");
});
});
}
}
package com.example.gcptrace.service;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private InventoryService inventoryService;
@Mock
private PaymentService paymentService;
@Mock
private ShippingService shippingService;
@InjectMocks
private OrderService orderService;
@Test
void testProcessOrderSuccess() {
when(inventoryService.validateInventory(any())).thenReturn(true);
when(paymentService.processPayment(any())).thenReturn("payment-123");
when(shippingService.createShipping(any())).thenReturn("shipping-456");
Map<String, Object> orderRequest = Map.of(
"customerId", "cust-123",
"totalAmount", 99.99,
"currency", "USD"
);
Map<String, Object> result = orderService.processOrder(orderRequest);
assertNotNull(result);
assertEquals("COMPLETED", result.get("status"));
assertTrue(result.containsKey("orderId"));
assertTrue(result.containsKey("traceId"));
verify(inventoryService).validateInventory(any());
verify(paymentService).processPayment(any());
verify(shippingService).createShipping(any());
}
}

Deployment Configuration

application.yaml

spring:
application:
name: order-service
datasource:
url: ${DATABASE_URL:jdbc:postgresql://localhost:5432/orders}
username: ${DATABASE_USERNAME:postgres}
password: ${DATABASE_PASSWORD:password}
jackson:
serialization:
write-dates-as-timestamps: false
logging:
level:
com.example.gcptrace: DEBUG
io.opentelemetry: INFO
pattern:
level: "%5p [%X{traceId},%X{spanId}]"
# Google Cloud Configuration
google:
cloud:
project-id: ${GOOGLE_CLOUD_PROJECT:my-project}
trace:
enabled: true
sampling-rate: 1.0
# Custom tracing configuration
tracing:
enabled: true
slow-operation-threshold-ms: 1000
include-query-parameters: false

Dockerfile

FROM eclipse-temurin:17-jre
# Add Google Cloud SDK for authentication
RUN apt-get update && apt-get install -y curl
# Install Google Cloud Trace agent
RUN curl -sSL "https://github.com/GoogleCloudPlatform/cloud-trace-java/releases/download/v2.27.0/cloud-trace-java-2.27.0.zip" -o cloud-trace.zip && \
unzip cloud-trace.zip -d /opt && \
rm cloud-trace.zip
WORKDIR /app
COPY target/order-service.jar app.jar
# Google Cloud Trace configuration
ENV GOOGLE_APPLICATION_CREDENTIALS=/app/credentials.json
ENV ENABLE_CLOUD_TRACE=true
ENV CLOUD_TRACE_SAMPLING_RATE=1.0
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

Best Practices

1. Trace Context Propagation

package com.example.gcptrace.propagation;
import com.example.gcptrace.CloudTraceManager;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import java.io.IOException;
import java.util.Map;
public class TracePropagationInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, 
ClientHttpRequestExecution execution) throws IOException {
// Propagate trace context to downstream services
Map<String, String> traceContext = CloudTraceManager.getTraceContext();
if (traceContext.containsKey("traceId")) {
// Add W3C Trace Context header
String traceParent = String.format("00-%s-%s-01", 
traceContext.get("traceId"), traceContext.get("spanId"));
request.getHeaders().add("traceparent", traceParent);
// Add Google Cloud Trace header
String cloudTraceContext = String.format("%s/%s;o=1", 
traceContext.get("traceId"), traceContext.get("spanId"));
request.getHeaders().add("X-Cloud-Trace-Context", cloudTraceContext);
}
return execution.execute(request, body);
}
}

2. Error Handling and Retry with Tracing

package com.example.gcptrace.resilience;
import com.example.gcptrace.CloudTraceManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.function.Supplier;
public class RetryWithTracing {
private static final Logger logger = LoggerFactory.getLogger(RetryWithTracing.class);
public static <T> T withRetry(String operationName, Supplier<T> operation, int maxRetries) {
int attempt = 0;
Exception lastException = null;
while (attempt <= maxRetries) {
try {
return CloudTraceManager.traceOperation(
operationName + (attempt > 0 ? "-retry-" + attempt : ""),
Map.of("attempt", attempt, "maxRetries", maxRetries),
operation);
} catch (Exception e) {
lastException = e;
attempt++;
if (attempt <= maxRetries) {
logger.warn("Operation {} failed, retrying (attempt {}/{})", 
operationName, attempt, maxRetries);
CloudTraceManager.addEvent("operation.retry", 
Map.of("attempt", attempt, "error", e.getMessage()));
try {
Thread.sleep(calculateBackoff(attempt));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("Retry interrupted", ie);
}
}
}
}
throw new RuntimeException("Operation failed after " + maxRetries + " retries", lastException);
}
private static long calculateBackoff(int attempt) {
return Math.min(1000 * (long) Math.pow(2, attempt), 30000);
}
}

Conclusion

This comprehensive Google Cloud Trace implementation for Java provides:

  • Distributed tracing across microservices and external calls
  • Spring Boot integration for easy adoption
  • Automatic context propagation for HTTP calls and messaging
  • Custom span attributes for business context
  • Performance monitoring and alerting
  • Error tracking and resilience patterns

Key benefits include:

  1. End-to-end visibility across your microservices architecture
  2. Performance optimization through latency analysis
  3. Root cause analysis for production issues
  4. Integration with Google Cloud ecosystem
  5. Production-ready with proper error handling and monitoring

The implementation follows cloud-native best practices and provides a solid foundation for building observable, maintainable applications on Google Cloud Platform.

Leave a Reply

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


Macro Nepal Helper