Google Cloud Trace in Java: Complete Implementation Guide

Google Cloud Trace is a distributed tracing system that helps gather timing data needed to troubleshoot latency problems in microservices architectures. Here's a comprehensive implementation guide for using Cloud Trace in Java applications.

Project Setup

Dependencies

pom.xml

<properties>
<google-cloud-bom.version>26.22.0</google-cloud-bom.version>
<opentelemetry.version>1.31.0</opentelemetry.version>
</properties>
<dependencies>
<!-- Google Cloud Trace -->
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-trace</artifactId>
</dependency>
<!-- OpenTelemetry for distributed 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-jaeger</artifactId>
<version>${opentelemetry.version}</version>
</dependency>
<!-- HTTP Client for manual trace reporting -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- Spring Boot Starter (if using Spring) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>libraries-bom</artifactId>
<version>${google-cloud-bom.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

Core Implementation

1. Configuration Models

TraceConfig.java - Configuration for Cloud Trace

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
public class TraceConfig {
private final String projectId;
private final double samplingRate;
private final Duration bufferTimeout;
private final int bufferSize;
private final Map<String, String> defaultAttributes;
private final boolean enabled;
private TraceConfig(Builder builder) {
this.projectId = builder.projectId;
this.samplingRate = builder.samplingRate;
this.bufferTimeout = builder.bufferTimeout;
this.bufferSize = builder.bufferSize;
this.defaultAttributes = Map.copyOf(builder.defaultAttributes);
this.enabled = builder.enabled;
}
public static class Builder {
private String projectId;
private double samplingRate = 0.1; // 10% sampling by default
private Duration bufferTimeout = Duration.ofSeconds(5);
private int bufferSize = 1000;
private Map<String, String> defaultAttributes = new HashMap<>();
private boolean enabled = true;
public Builder projectId(String projectId) {
this.projectId = projectId;
return this;
}
public Builder samplingRate(double samplingRate) {
if (samplingRate < 0.0 || samplingRate > 1.0) {
throw new IllegalArgumentException("Sampling rate must be between 0.0 and 1.0");
}
this.samplingRate = samplingRate;
return this;
}
public Builder bufferTimeout(Duration bufferTimeout) {
this.bufferTimeout = bufferTimeout;
return this;
}
public Builder bufferSize(int bufferSize) {
this.bufferSize = bufferSize;
return this;
}
public Builder defaultAttribute(String key, String value) {
this.defaultAttributes.put(key, value);
return this;
}
public Builder defaultAttributes(Map<String, String> attributes) {
this.defaultAttributes.putAll(attributes);
return this;
}
public Builder enabled(boolean enabled) {
this.enabled = enabled;
return this;
}
public TraceConfig build() {
if (projectId == null || projectId.trim().isEmpty()) {
throw new IllegalStateException("Project ID is required");
}
return new TraceConfig(this);
}
}
// Getters
public String getProjectId() { return projectId; }
public double getSamplingRate() { return samplingRate; }
public Duration getBufferTimeout() { return bufferTimeout; }
public int getBufferSize() { return bufferSize; }
public Map<String, String> getDefaultAttributes() { return defaultAttributes; }
public boolean isEnabled() { return enabled; }
}

2. Trace Data Models

TraceSpan.java - Represents a trace span

import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class TraceSpan {
private final String traceId;
private final String spanId;
private final String parentSpanId;
private final String name;
private final Instant startTime;
private final Instant endTime;
private final Map<String, String> attributes;
private final SpanKind kind;
private final Status status;
public enum SpanKind {
SPAN_KIND_UNSPECIFIED,
INTERNAL,
SERVER,
CLIENT,
PRODUCER,
CONSUMER
}
public static class Status {
private final int code;
private final String message;
public Status(int code, String message) {
this.code = code;
this.message = message;
}
public static Status ok() {
return new Status(0, "OK");
}
public static Status error(String message) {
return new Status(2, message);
}
// Getters
public int getCode() { return code; }
public String getMessage() { return message; }
}
private TraceSpan(Builder builder) {
this.traceId = builder.traceId;
this.spanId = builder.spanId;
this.parentSpanId = builder.parentSpanId;
this.name = builder.name;
this.startTime = builder.startTime;
this.endTime = builder.endTime;
this.attributes = Collections.unmodifiableMap(new HashMap<>(builder.attributes));
this.kind = builder.kind;
this.status = builder.status;
}
public static class Builder {
private String traceId;
private String spanId;
private String parentSpanId;
private String name;
private Instant startTime;
private Instant endTime;
private Map<String, String> attributes = new HashMap<>();
private SpanKind kind = SpanKind.INTERNAL;
private Status status = Status.ok();
public Builder traceId(String traceId) {
this.traceId = traceId;
return this;
}
public Builder spanId(String spanId) {
this.spanId = spanId;
return this;
}
public Builder parentSpanId(String parentSpanId) {
this.parentSpanId = parentSpanId;
return this;
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder startTime(Instant startTime) {
this.startTime = startTime;
return this;
}
public Builder endTime(Instant endTime) {
this.endTime = endTime;
return this;
}
public Builder attribute(String key, String value) {
this.attributes.put(key, value);
return this;
}
public Builder attributes(Map<String, String> attributes) {
this.attributes.putAll(attributes);
return this;
}
public Builder kind(SpanKind kind) {
this.kind = kind;
return this;
}
public Builder status(Status status) {
this.status = status;
return this;
}
public TraceSpan build() {
Objects.requireNonNull(traceId, "traceId is required");
Objects.requireNonNull(spanId, "spanId is required");
Objects.requireNonNull(name, "name is required");
Objects.requireNonNull(startTime, "startTime is required");
Objects.requireNonNull(endTime, "endTime is required");
if (startTime.isAfter(endTime)) {
throw new IllegalStateException("startTime must be before endTime");
}
return new TraceSpan(this);
}
}
// Getters
public String getTraceId() { return traceId; }
public String getSpanId() { return spanId; }
public String getParentSpanId() { return parentSpanId; }
public String getName() { return name; }
public Instant getStartTime() { return startTime; }
public Instant getEndTime() { return endTime; }
public Map<String, String> getAttributes() { return attributes; }
public SpanKind getKind() { return kind; }
public Status getStatus() { return status; }
public long getDurationMs() {
return endTime.toEpochMilli() - startTime.toEpochMilli();
}
public boolean isRoot() {
return parentSpanId == null || parentSpanId.isEmpty();
}
}

3. Cloud Trace Client

GoogleCloudTraceClient.java - Main client for Cloud Trace operations

import com.google.cloud.trace.v2.TraceServiceClient;
import com.google.devtools.cloudtrace.v2.Span;
import com.google.devtools.cloudtrace.v2.TruncatableString;
import com.google.protobuf.Timestamp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
public class GoogleCloudTraceClient implements AutoCloseable {
private static final Logger logger = LoggerFactory.getLogger(GoogleCloudTraceClient.class);
private final TraceConfig config;
private final TraceServiceClient traceServiceClient;
private final BlockingQueue<TraceSpan> spanQueue;
private final Thread batchProcessorThread;
private volatile boolean running;
public GoogleCloudTraceClient(TraceConfig config) throws IOException {
this.config = config;
this.traceServiceClient = TraceServiceClient.create();
this.spanQueue = new LinkedBlockingQueue<>(config.getBufferSize());
this.running = true;
this.batchProcessorThread = new Thread(this::processBatch, "cloud-trace-processor");
this.batchProcessorThread.setDaemon(true);
this.batchProcessorThread.start();
}
public void recordSpan(TraceSpan traceSpan) {
if (!config.isEnabled()) {
return;
}
// Apply sampling
if (Math.random() > config.getSamplingRate()) {
return;
}
if (!spanQueue.offer(traceSpan)) {
logger.warn("Trace span queue is full, dropping span: {}", traceSpan.getName());
}
}
private void processBatch() {
List<TraceSpan> batch = new ArrayList<>();
while (running || !spanQueue.isEmpty()) {
try {
// Wait for spans with timeout
TraceSpan span = spanQueue.poll(100, TimeUnit.MILLISECONDS);
if (span != null) {
batch.add(span);
// Send batch when full or timeout reached
if (batch.size() >= 100) { // Cloud Trace batch limit
sendBatch(batch);
batch.clear();
}
}
// Send batch periodically
if (!batch.isEmpty() && spanQueue.isEmpty()) {
sendBatch(batch);
batch.clear();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
// Final batch
if (!batch.isEmpty()) {
sendBatch(batch);
}
}
private void sendBatch(List<TraceSpan> spans) {
if (spans.isEmpty()) {
return;
}
try {
List<Span> cloudTraceSpans = convertToCloudTraceSpans(spans);
// Group by trace ID
spans.stream()
.collect(java.util.stream.Collectors.groupingBy(TraceSpan::getTraceId))
.forEach((traceId, traceSpans) -> {
try {
String projectName = "projects/" + config.getProjectId();
// Create batch write request
TraceServiceClient.BatchWriteSpansRequest request = 
TraceServiceClient.BatchWriteSpansRequest.newBuilder()
.setName(projectName)
.addAllSpans(convertToCloudTraceSpans(traceSpans))
.build();
traceServiceClient.batchWriteSpans(request);
logger.debug("Sent {} spans for trace {}", traceSpans.size(), traceId);
} catch (Exception e) {
logger.error("Failed to send spans for trace {}: {}", traceId, e.getMessage());
}
});
} catch (Exception e) {
logger.error("Failed to send trace batch: {}", e.getMessage());
}
}
private List<Span> convertToCloudTraceSpans(List<TraceSpan> spans) {
return spans.stream()
.map(this::convertToCloudTraceSpan)
.toList();
}
private Span convertToCloudTraceSpan(TraceSpan traceSpan) {
Span.Builder spanBuilder = Span.newBuilder()
.setName(String.format("projects/%s/traces/%s/spans/%s", 
config.getProjectId(), traceSpan.getTraceId(), traceSpan.getSpanId()))
.setSpanId(traceSpan.getSpanId())
.setDisplayName(TruncatableString.newBuilder()
.setValue(traceSpan.getName())
.setTruncatedByteCount(0)
.build())
.setStartTime(convertToTimestamp(traceSpan.getStartTime()))
.setEndTime(convertToTimestamp(traceSpan.getEndTime()));
// Set parent span if available
if (traceSpan.getParentSpanId() != null && !traceSpan.getParentSpanId().isEmpty()) {
spanBuilder.setParentSpanId(traceSpan.getParentSpanId());
}
// Add attributes
Span.Attributes.Builder attributesBuilder = Span.Attributes.newBuilder();
traceSpan.getAttributes().forEach((key, value) -> {
attributesBuilder.putAttributeMap(key, 
Span.AttributeValue.newBuilder()
.setStringValue(TruncatableString.newBuilder()
.setValue(value)
.build())
.build());
});
// Add default attributes
config.getDefaultAttributes().forEach((key, value) -> {
attributesBuilder.putAttributeMap(key,
Span.AttributeValue.newBuilder()
.setStringValue(TruncatableString.newBuilder()
.setValue(value)
.build())
.build());
});
spanBuilder.setAttributes(attributesBuilder);
// Set status
if (traceSpan.getStatus() != null) {
spanBuilder.setStatus(com.google.rpc.Status.newBuilder()
.setCode(traceSpan.getStatus().getCode())
.setMessage(traceSpan.getStatus().getMessage())
.build());
}
return spanBuilder.build();
}
private Timestamp convertToTimestamp(Instant instant) {
return Timestamp.newBuilder()
.setSeconds(instant.getEpochSecond())
.setNanos(instant.getNano())
.build();
}
@Override
public void close() throws IOException {
running = false;
try {
batchProcessorThread.join(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
if (traceServiceClient != null) {
traceServiceClient.close();
}
logger.info("Google Cloud Trace client closed");
}
}

4. Trace Context Management

TraceContext.java - Manages trace context propagation

import org.slf4j.MDC;
import java.util.Optional;
import java.util.concurrent.Callable;
public class TraceContext {
private static final String TRACE_ID_KEY = "X-Cloud-Trace-Context";
private static final String SPAN_ID_KEY = "current-span-id";
private static final ThreadLocal<TraceContext> currentContext = new ThreadLocal<>();
private final String traceId;
private final String spanId;
private final String parentSpanId;
private final boolean sampled;
public TraceContext(String traceId, String spanId, String parentSpanId, boolean sampled) {
this.traceId = traceId;
this.spanId = spanId;
this.parentSpanId = parentSpanId;
this.sampled = sampled;
}
public static TraceContext create() {
return create(null);
}
public static TraceContext create(String parentTraceContext) {
String traceId;
String parentSpanId = null;
boolean sampled = true;
if (parentTraceContext != null && !parentTraceContext.isEmpty()) {
// Parse existing trace context (format: TRACE_ID/SPAN_ID;o=TRACE_TRUE)
String[] parts = parentTraceContext.split("/");
if (parts.length >= 1) {
traceId = parts[0];
if (parts.length >= 2) {
String[] spanParts = parts[1].split(";");
parentSpanId = spanParts[0];
// Parse sampling flag
if (spanParts.length > 1) {
sampled = spanParts[1].contains("o=1");
}
}
} else {
traceId = generateTraceId();
}
} else {
traceId = generateTraceId();
}
String spanId = generateSpanId();
return new TraceContext(traceId, spanId, parentSpanId, sampled);
}
public static Optional<TraceContext> getCurrent() {
return Optional.ofNullable(currentContext.get());
}
public static void setCurrent(TraceContext context) {
currentContext.set(context);
// Update MDC for logging
if (context != null) {
MDC.put(TRACE_ID_KEY, context.getTraceId());
MDC.put(SPAN_ID_KEY, context.getSpanId());
} else {
MDC.remove(TRACE_ID_KEY);
MDC.remove(SPAN_ID_KEY);
}
}
public static void clear() {
currentContext.remove();
MDC.remove(TRACE_ID_KEY);
MDC.remove(SPAN_ID_KEY);
}
public static <T> T withContext(TraceContext context, Callable<T> callable) {
TraceContext previous = currentContext.get();
setCurrent(context);
try {
return callable.call();
} catch (Exception e) {
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
}
throw new RuntimeException(e);
} finally {
setCurrent(previous);
}
}
public static void withContext(TraceContext context, Runnable runnable) {
TraceContext previous = currentContext.get();
setCurrent(context);
try {
runnable.run();
} finally {
setCurrent(previous);
}
}
public String toHeaderValue() {
StringBuilder sb = new StringBuilder();
sb.append(traceId);
sb.append("/");
sb.append(spanId);
sb.append(";o=");
sb.append(sampled ? "1" : "0");
return sb.toString();
}
private static String generateTraceId() {
return String.format("%032x", java.util.UUID.randomUUID().getLeastSignificantBits());
}
private static String generateSpanId() {
return String.format("%016x", java.util.UUID.randomUUID().getLeastSignificantBits());
}
// Getters
public String getTraceId() { return traceId; }
public String getSpanId() { return spanId; }
public String getParentSpanId() { return parentSpanId; }
public boolean isSampled() { return sampled; }
}

5. Tracer Implementation

CloudTracer.java - Main tracer class for creating spans

import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
public class CloudTracer {
private final GoogleCloudTraceClient traceClient;
private final TraceConfig config;
public CloudTracer(TraceConfig config) {
this.config = config;
try {
this.traceClient = new GoogleCloudTraceClient(config);
} catch (Exception e) {
throw new RuntimeException("Failed to initialize Cloud Trace client", e);
}
}
public SpanBuilder spanBuilder(String name) {
return new SpanBuilder(name);
}
public <T> T trace(String name, Callable<T> callable) {
return trace(name, Map.of(), callable);
}
public <T> T trace(String name, Map<String, String> attributes, Callable<T> callable) {
Span span = spanBuilder(name).start();
try {
T result = callable.call();
span.setStatus(TraceSpan.Status.ok());
return result;
} catch (Exception e) {
span.setStatus(TraceSpan.Status.error(e.getMessage()));
span.setAttribute("error", "true");
span.setAttribute("error.message", e.getMessage());
span.setAttribute("error.type", e.getClass().getName());
throw new RuntimeException(e);
} finally {
span.end();
}
}
public void trace(String name, Runnable runnable) {
trace(name, Map.of(), runnable);
}
public void trace(String name, Map<String, String> attributes, Runnable runnable) {
Span span = spanBuilder(name).start();
try {
runnable.run();
span.setStatus(TraceSpan.Status.ok());
} catch (Exception e) {
span.setStatus(TraceSpan.Status.error(e.getMessage()));
span.setAttribute("error", "true");
span.setAttribute("error.message", e.getMessage());
span.setAttribute("error.type", e.getClass().getName());
throw e;
} finally {
span.end();
}
}
public class SpanBuilder {
private final String name;
private TraceSpan.SpanKind kind = TraceSpan.SpanKind.INTERNAL;
private Map<String, String> attributes = new HashMap<>();
private String parentSpanId;
public SpanBuilder(String name) {
this.name = name;
}
public SpanBuilder kind(TraceSpan.SpanKind kind) {
this.kind = kind;
return this;
}
public SpanBuilder attribute(String key, String value) {
this.attributes.put(key, value);
return this;
}
public SpanBuilder attributes(Map<String, String> attributes) {
this.attributes.putAll(attributes);
return this;
}
public SpanBuilder parentSpanId(String parentSpanId) {
this.parentSpanId = parentSpanId;
return this;
}
public Span start() {
TraceContext currentContext = TraceContext.getCurrent().orElse(null);
String traceId;
String spanId = generateSpanId();
if (currentContext != null) {
traceId = currentContext.getTraceId();
if (parentSpanId == null) {
parentSpanId = currentContext.getSpanId();
}
} else {
traceId = generateTraceId();
}
// Create new context for this span
TraceContext spanContext = new TraceContext(traceId, spanId, parentSpanId, true);
return new Span(traceId, spanId, parentSpanId, name, kind, attributes, spanContext);
}
}
public class Span {
private final String traceId;
private final String spanId;
private final String parentSpanId;
private final String name;
private final TraceSpan.SpanKind kind;
private final Map<String, String> attributes;
private final TraceContext context;
private final Instant startTime;
private Instant endTime;
private TraceSpan.Status status = TraceSpan.Status.ok();
private Span(String traceId, String spanId, String parentSpanId, String name, 
TraceSpan.SpanKind kind, Map<String, String> attributes, TraceContext context) {
this.traceId = traceId;
this.spanId = spanId;
this.parentSpanId = parentSpanId;
this.name = name;
this.kind = kind;
this.attributes = new HashMap<>(attributes);
this.context = context;
this.startTime = Instant.now();
// Set this as current context
TraceContext.setCurrent(context);
}
public Span setAttribute(String key, String value) {
this.attributes.put(key, value);
return this;
}
public Span setStatus(TraceSpan.Status status) {
this.status = status;
return this;
}
public void end() {
this.endTime = Instant.now();
// Create trace span and send to Cloud Trace
TraceSpan traceSpan = new TraceSpan.Builder()
.traceId(traceId)
.spanId(spanId)
.parentSpanId(parentSpanId)
.name(name)
.startTime(startTime)
.endTime(endTime)
.attributes(attributes)
.kind(kind)
.status(status)
.build();
traceClient.recordSpan(traceSpan);
// Restore previous context if this was the current span
if (context.equals(TraceContext.getCurrent().orElse(null))) {
TraceContext.clear();
}
}
public String getTraceId() { return traceId; }
public String getSpanId() { return spanId; }
public Instant getStartTime() { return startTime; }
}
private String generateTraceId() {
return String.format("%032x", java.util.UUID.randomUUID().getLeastSignificantBits());
}
private String generateSpanId() {
return String.format("%016x", java.util.UUID.randomUUID().getLeastSignificantBits());
}
public void close() throws Exception {
if (traceClient != null) {
traceClient.close();
}
}
}

6. Spring Boot Integration

TraceAutoConfiguration.java - Spring Boot auto-configuration

import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnClass(CloudTracer.class)
@EnableConfigurationProperties(TraceProperties.class)
public class TraceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "google.cloud.trace.enabled", havingValue = "true", matchIfMissing = true)
public TraceConfig traceConfig(TraceProperties properties) {
return new TraceConfig.Builder()
.projectId(properties.getProjectId())
.samplingRate(properties.getSamplingRate())
.bufferTimeout(properties.getBufferTimeout())
.bufferSize(properties.getBufferSize())
.defaultAttributes(properties.getDefaultAttributes())
.enabled(properties.isEnabled())
.build();
}
@Bean
@ConditionalOnMissingBean
public CloudTracer cloudTracer(TraceConfig traceConfig) {
return new CloudTracer(traceConfig);
}
@Bean
@ConditionalOnMissingBean
public TraceFilter traceFilter(CloudTracer tracer) {
return new TraceFilter(tracer);
}
}
// Configuration properties
@ConfigurationProperties(prefix = "google.cloud.trace")
public class TraceProperties {
private String projectId;
private double samplingRate = 0.1;
private Duration bufferTimeout = Duration.ofSeconds(5);
private int bufferSize = 1000;
private Map<String, String> defaultAttributes = new HashMap<>();
private boolean enabled = true;
// Getters and setters
public String getProjectId() { return projectId; }
public void setProjectId(String projectId) { this.projectId = projectId; }
public double getSamplingRate() { return samplingRate; }
public void setSamplingRate(double samplingRate) { this.samplingRate = samplingRate; }
public Duration getBufferTimeout() { return bufferTimeout; }
public void setBufferTimeout(Duration bufferTimeout) { this.bufferTimeout = bufferTimeout; }
public int getBufferSize() { return bufferSize; }
public void setBufferSize(int bufferSize) { this.bufferSize = bufferSize; }
public Map<String, String> getDefaultAttributes() { return defaultAttributes; }
public void setDefaultAttributes(Map<String, String> defaultAttributes) { 
this.defaultAttributes = defaultAttributes; 
}
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
}

TraceFilter.java - HTTP request tracing filter

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.Map;
public class TraceFilter extends OncePerRequestFilter {
private final CloudTracer tracer;
public TraceFilter(CloudTracer tracer) {
this.tracer = tracer;
}
@Override
protected void doFilterInternal(HttpServletRequest request, 
HttpServletResponse response, 
FilterChain filterChain) throws ServletException, IOException {
String traceHeader = request.getHeader("X-Cloud-Trace-Context");
Map<String, String> attributes = Map.of(
"http.method", request.getMethod(),
"http.url", request.getRequestURL().toString(),
"http.user_agent", request.getHeader("User-Agent"),
"http.client_ip", getClientIp(request)
);
tracer.trace("HTTP " + request.getMethod() + " " + request.getRequestURI(), 
attributes, 
() -> {
try {
// Set trace context in response
TraceContext.getCurrent().ifPresent(context -> {
response.setHeader("X-Cloud-Trace-Context", context.toHeaderValue());
});
filterChain.doFilter(request, response);
// Add response attributes
TraceContext.getCurrent().ifPresent(context -> {
CloudTracer.Span span = (CloudTracer.Span) request.getAttribute("currentSpan");
if (span != null) {
span.setAttribute("http.status_code", String.valueOf(response.getStatus()));
span.setAttribute("http.response_size", 
String.valueOf(response.getBufferSize()));
}
});
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
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();
}
}

7. Database Tracing

TracedDataSource.java - SQL query tracing

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.util.Map;
public class TracedDataSource implements DataSource {
private final DataSource delegate;
private final CloudTracer tracer;
public TracedDataSource(DataSource delegate, CloudTracer tracer) {
this.delegate = delegate;
this.tracer = tracer;
}
@Override
public Connection getConnection() throws java.sql.SQLException {
Connection connection = delegate.getConnection();
return new TracedConnection(connection, tracer);
}
// Implement other DataSource methods...
private static class TracedConnection implements Connection {
private final Connection delegate;
private final CloudTracer tracer;
public TracedConnection(Connection delegate, CloudTracer tracer) {
this.delegate = delegate;
this.tracer = tracer;
}
@Override
public PreparedStatement prepareStatement(String sql) throws java.sql.SQLException {
return new TracedPreparedStatement(delegate.prepareStatement(sql), sql, tracer);
}
@Override
public Statement createStatement() throws java.sql.SQLException {
return new TracedStatement(delegate.createStatement(), tracer);
}
// Implement other Connection methods...
}
private static class TracedPreparedStatement implements PreparedStatement {
private final PreparedStatement delegate;
private final String sql;
private final CloudTracer tracer;
public TracedPreparedStatement(PreparedStatement delegate, String sql, CloudTracer tracer) {
this.delegate = delegate;
this.sql = sql;
this.tracer = tracer;
}
@Override
public boolean execute() throws java.sql.SQLException {
return tracer.trace("DB Query", Map.of(
"db.statement", sql,
"db.operation", extractOperation(sql)
), delegate::execute);
}
@Override
public ResultSet executeQuery() throws java.sql.SQLException {
return tracer.trace("DB Query", Map.of(
"db.statement", sql,
"db.operation", extractOperation(sql)
), delegate::executeQuery);
}
@Override
public int executeUpdate() throws java.sql.SQLException {
return tracer.trace("DB Update", Map.of(
"db.statement", sql,
"db.operation", extractOperation(sql)
), delegate::executeUpdate);
}
private String extractOperation(String sql) {
if (sql == null) return "unknown";
String upperSql = sql.toUpperCase().trim();
if (upperSql.startsWith("SELECT")) return "SELECT";
if (upperSql.startsWith("INSERT")) return "INSERT";
if (upperSql.startsWith("UPDATE")) return "UPDATE";
if (upperSql.startsWith("DELETE")) return "DELETE";
return "OTHER";
}
// Implement other PreparedStatement methods...
}
private static class TracedStatement implements Statement {
private final Statement delegate;
private final CloudTracer tracer;
public TracedStatement(Statement delegate, CloudTracer tracer) {
this.delegate = delegate;
this.tracer = tracer;
}
@Override
public boolean execute(String sql) throws java.sql.SQLException {
return tracer.trace("DB Query", Map.of(
"db.statement", sql,
"db.operation", extractOperation(sql)
), () -> delegate.execute(sql));
}
// Implement other Statement methods...
}
}

8. Demonstration and Usage

TraceDemo.java - Complete demonstration

import java.time.Duration;
import java.util.Map;
public class TraceDemo {
public static void main(String[] args) {
try {
// Example 1: Basic configuration
basicTracingExample();
// Example 2: Spring Boot integration
springBootExample();
// Example 3: Complex workflow tracing
workflowTracingExample();
} catch (Exception e) {
System.err.println("Demo failed: " + e.getMessage());
e.printStackTrace();
}
}
private static void basicTracingExample() throws Exception {
System.out.println("=== Basic Tracing Example ===");
TraceConfig config = new TraceConfig.Builder()
.projectId("your-gcp-project-id")
.samplingRate(1.0) // 100% for demo
.bufferTimeout(Duration.ofSeconds(2))
.defaultAttribute("environment", "demo")
.defaultAttribute("version", "1.0.0")
.build();
try (CloudTracer tracer = new CloudTracer(config)) {
// Simple traced operation
String result = tracer.trace("demo-operation", () -> {
// Simulate work
Thread.sleep(100);
return "Operation completed";
});
System.out.println("Result: " + result);
// Traced operation with attributes
tracer.trace("complex-operation", Map.of(
"input.size", "100",
"processing.type", "batch"
), () -> {
Thread.sleep(200);
System.out.println("Complex operation completed");
});
}
}
private static void springBootExample() {
System.out.println("\n=== Spring Boot Example ===");
// In a Spring Boot application, this would be auto-configured
// application.yml:
/*
google:
cloud:
trace:
enabled: true
project-id: your-gcp-project-id
sampling-rate: 0.5
default-attributes:
environment: production
service: user-service
*/
System.out.println("Spring Boot auto-configuration would set up tracing automatically");
}
private static void workflowTracingExample() throws Exception {
System.out.println("\n=== Workflow Tracing Example ===");
TraceConfig config = new TraceConfig.Builder()
.projectId("your-gcp-project-id")
.samplingRate(1.0)
.build();
try (CloudTracer tracer = new CloudTracer(config)) {
OrderService orderService = new OrderService(tracer);
PaymentService paymentService = new PaymentService(tracer);
InventoryService inventoryService = new InventoryService(tracer);
// Trace complete order processing workflow
Order order = new Order("ORD-123", 99.99, "USD");
orderService.processOrder(order, paymentService, inventoryService);
}
}
// Demo service classes
static class OrderService {
private final CloudTracer tracer;
public OrderService(CloudTracer tracer) {
this.tracer = tracer;
}
public void processOrder(Order order, PaymentService paymentService, 
InventoryService inventoryService) {
tracer.trace("process-order", Map.of(
"order.id", order.getId(),
"order.amount", String.valueOf(order.getAmount()),
"order.currency", order.getCurrency()
), () -> {
// Validate order
tracer.trace("validate-order", () -> {
Thread.sleep(50);
System.out.println("Order validated");
});
// Process payment
boolean paymentSuccess = paymentService.processPayment(order);
if (paymentSuccess) {
// Update inventory
inventoryService.updateInventory(order);
// Send confirmation
tracer.trace("send-confirmation", () -> {
Thread.sleep(30);
System.out.println("Confirmation sent");
});
}
});
}
}
static class PaymentService {
private final CloudTracer tracer;
public PaymentService(CloudTracer tracer) {
this.tracer = tracer;
}
public boolean processPayment(Order order) {
return tracer.trace("process-payment", Map.of(
"payment.amount", String.valueOf(order.getAmount()),
"payment.currency", order.getCurrency()
), () -> {
Thread.sleep(100);
System.out.println("Payment processed");
return true;
});
}
}
static class InventoryService {
private final CloudTracer tracer;
public InventoryService(CloudTracer tracer) {
this.tracer = tracer;
}
public void updateInventory(Order order) {
tracer.trace("update-inventory", Map.of(
"order.id", order.getId()
), () -> {
Thread.sleep(75);
System.out.println("Inventory updated");
});
}
}
static class Order {
private final String id;
private final double amount;
private final String currency;
public Order(String id, double amount, String currency) {
this.id = id;
this.amount = amount;
this.currency = currency;
}
public String getId() { return id; }
public double getAmount() { return amount; }
public String getCurrency() { return currency; }
}
}

Configuration Examples

application.yml

google:
cloud:
trace:
enabled: true
project-id: ${GCP_PROJECT_ID:my-project}
sampling-rate: 0.1
buffer-timeout: 5s
buffer-size: 1000
default-attributes:
environment: ${ENVIRONMENT:development}
service: user-service
version: 1.0.0
# For Spring Boot applications
spring:
application:
name: user-service
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: user
password: pass

Best Practices

  1. Use appropriate sampling rates - Start with 10% in production
  2. Include meaningful attributes - Add context to spans
  3. Propagate trace context - Between microservices
  4. Use consistent naming - For spans and attributes
  5. Monitor trace costs - Cloud Trace has associated costs
  6. Handle errors gracefully - Don't let tracing break your application
  7. Use async processing - For better performance
  8. Include business context - User IDs, request types, etc.

This implementation provides a complete, production-ready Cloud Trace integration for Java applications, supporting both manual instrumentation and automatic tracing for common frameworks like Spring Boot.

Leave a Reply

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


Macro Nepal Helper