Skip to content

Commit 5287f82

Browse files
committed
feat: add composite api tracer
1 parent dc3ed42 commit 5287f82

7 files changed

Lines changed: 349 additions & 22 deletions

File tree

gax-java/gax/src/main/java/com/google/api/gax/rpc/StubSettings.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@
4646
import com.google.api.gax.core.NoCredentialsProvider;
4747
import com.google.api.gax.tracing.ApiTracerFactory;
4848
import com.google.api.gax.tracing.BaseApiTracerFactory;
49+
import com.google.api.gax.tracing.CompositeApiTracerFactory;
50+
import com.google.api.gax.tracing.OpenTelemetryTracingRecorder;
51+
import com.google.api.gax.tracing.TracingTracerFactory;
52+
import com.google.api.gax.tracing.TracingUtils;
53+
import io.opentelemetry.api.GlobalOpenTelemetry;
4954
import com.google.auth.oauth2.QuotaProjectIdProvider;
5055
import com.google.common.base.MoreObjects;
5156
import com.google.common.base.Preconditions;
@@ -104,13 +109,26 @@ protected StubSettings(Builder builder) {
104109
this.quotaProjectId = builder.quotaProjectId;
105110
this.streamWatchdogProvider = builder.streamWatchdogProvider;
106111
this.streamWatchdogCheckInterval = builder.streamWatchdogCheckInterval;
107-
this.tracerFactory = builder.tracerFactory;
112+
this.tracerFactory = autoConfigureTracerFactory(builder.tracerFactory);
108113
this.deprecatedExecutorProviderSet = builder.deprecatedExecutorProviderSet;
109114
this.gdchApiAudience = builder.gdchApiAudience;
110115
this.endpointContext = buildEndpointContext(builder);
111116
this.apiKey = builder.apiKey;
112117
}
113118

119+
private ApiTracerFactory autoConfigureTracerFactory(ApiTracerFactory factory) {
120+
if (TracingUtils.isTracingEnabled()) {
121+
ApiTracerFactory tracingFactory =
122+
new TracingTracerFactory(
123+
new OpenTelemetryTracingRecorder(GlobalOpenTelemetry.get(), getServiceName()));
124+
if (factory instanceof BaseApiTracerFactory) {
125+
return tracingFactory;
126+
}
127+
return CompositeApiTracerFactory.of(factory, tracingFactory);
128+
}
129+
return factory;
130+
}
131+
114132
/**
115133
* Attempt to build the EndpointContext from the Builder based on all the user configurations
116134
* passed in.
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
31+
package com.google.api.gax.tracing;
32+
33+
import com.google.api.core.InternalApi;
34+
import com.google.common.collect.ImmutableList;
35+
import java.util.List;
36+
import java.util.stream.Collectors;
37+
38+
/**
39+
* A composite implementation of {@link ApiTracer} that broadcasts events to multiple tracers.
40+
*/
41+
@InternalApi
42+
public class CompositeApiTracer implements ApiTracer {
43+
private final List<ApiTracer> tracers;
44+
45+
public CompositeApiTracer(List<ApiTracer> tracers) {
46+
this.tracers = ImmutableList.copyOf(tracers);
47+
}
48+
49+
@Override
50+
public Scope inScope() {
51+
List<Scope> scopes = tracers.stream().map(ApiTracer::inScope).collect(Collectors.toList());
52+
return () -> scopes.forEach(Scope::close);
53+
}
54+
55+
@Override
56+
public void operationSucceeded() {
57+
tracers.forEach(ApiTracer::operationSucceeded);
58+
}
59+
60+
@Override
61+
public void operationCancelled() {
62+
tracers.forEach(ApiTracer::operationCancelled);
63+
}
64+
65+
@Override
66+
public void operationFailed(Throwable error) {
67+
tracers.forEach(t -> t.operationFailed(error));
68+
}
69+
70+
@Override
71+
public void connectionSelected(String id) {
72+
tracers.forEach(t -> t.connectionSelected(id));
73+
}
74+
75+
@Override
76+
public void attemptStarted(int attemptNumber) {
77+
tracers.forEach(t -> t.attemptStarted(attemptNumber));
78+
}
79+
80+
@Override
81+
public void attemptStarted(Object request, int attemptNumber) {
82+
tracers.forEach(t -> t.attemptStarted(request, attemptNumber));
83+
}
84+
85+
@Override
86+
public void attemptSucceeded() {
87+
tracers.forEach(ApiTracer::attemptSucceeded);
88+
}
89+
90+
@Override
91+
public void attemptCancelled() {
92+
tracers.forEach(ApiTracer::attemptCancelled);
93+
}
94+
95+
@Override
96+
public void attemptFailedDuration(Throwable error, java.time.Duration delay) {
97+
tracers.forEach(t -> t.attemptFailedDuration(error, delay));
98+
}
99+
100+
@Override
101+
public void attemptFailedRetriesExhausted(Throwable error) {
102+
tracers.forEach(t -> t.attemptFailedRetriesExhausted(error));
103+
}
104+
105+
@Override
106+
public void attemptPermanentFailure(Throwable error) {
107+
tracers.forEach(t -> t.attemptPermanentFailure(error));
108+
}
109+
110+
@Override
111+
public void lroStartFailed(Throwable error) {
112+
tracers.forEach(t -> t.lroStartFailed(error));
113+
}
114+
115+
@Override
116+
public void lroStartSucceeded() {
117+
tracers.forEach(ApiTracer::lroStartSucceeded);
118+
}
119+
120+
@Override
121+
public void responseReceived() {
122+
tracers.forEach(ApiTracer::responseReceived);
123+
}
124+
125+
@Override
126+
public void requestSent() {
127+
tracers.forEach(ApiTracer::requestSent);
128+
}
129+
130+
@Override
131+
public void batchRequestSent(long elementCount, long requestSize) {
132+
tracers.forEach(t -> t.batchRequestSent(elementCount, requestSize));
133+
}
134+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
31+
package com.google.api.gax.tracing;
32+
33+
import com.google.api.core.InternalApi;
34+
import com.google.common.collect.ImmutableList;
35+
import java.util.List;
36+
import java.util.stream.Collectors;
37+
38+
/**
39+
* A composite implementation of {@link ApiTracerFactory} that creates {@link CompositeApiTracer}s.
40+
*/
41+
@InternalApi
42+
public class CompositeApiTracerFactory implements ApiTracerFactory {
43+
private final List<ApiTracerFactory> factories;
44+
45+
public CompositeApiTracerFactory(List<ApiTracerFactory> factories) {
46+
this.factories = ImmutableList.copyOf(factories);
47+
}
48+
49+
public static ApiTracerFactory of(ApiTracerFactory... factories) {
50+
return new CompositeApiTracerFactory(ImmutableList.copyOf(factories));
51+
}
52+
53+
@Override
54+
public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType) {
55+
List<ApiTracer> tracers =
56+
factories.stream()
57+
.map(f -> f.newTracer(parent, spanName, operationType))
58+
.collect(Collectors.toList());
59+
return new CompositeApiTracer(tracers);
60+
}
61+
}

gax-java/gax/src/main/java/com/google/api/gax/tracing/TracingTracer.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ public TracingTracer(TracingRecorder tracingRecorder) {
5252

5353
@Override
5454
public void attemptStarted(Object request, int attemptNumber) {
55-
// temporary dummy trace to enable further development
5655
tracingRecorder.recordLowLevelNetworkSpan(attributes);
5756
}
5857

gax-java/gax/src/main/java/com/google/api/gax/tracing/TracingTracerFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
@BetaApi
4747
@InternalApi
4848
public class TracingTracerFactory implements ApiTracerFactory {
49-
protected TracingRecorder tracingRecorder;
49+
private final TracingRecorder tracingRecorder;
5050

5151
/** Mapping of client attributes that are set for every TracingTracer */
5252
private final Map<String, String> attributes;

java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelMetrics.java

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
package com.google.showcase.v1beta1.it;
3232

33+
import static com.google.common.truth.Truth.assertThat;
3334
import static org.junit.Assert.assertThrows;
3435

3536
import com.google.api.client.http.javanet.NetHttpTransport;
@@ -42,9 +43,7 @@
4243
import com.google.api.gax.rpc.StatusCode.Code;
4344
import com.google.api.gax.rpc.UnaryCallable;
4445
import com.google.api.gax.rpc.UnavailableException;
45-
import com.google.api.gax.tracing.MetricsTracer;
46-
import com.google.api.gax.tracing.MetricsTracerFactory;
47-
import com.google.api.gax.tracing.OpenTelemetryMetricsRecorder;
46+
import com.google.api.gax.tracing.*;
4847
import com.google.common.collect.ImmutableList;
4948
import com.google.common.collect.ImmutableMap;
5049
import com.google.common.collect.ImmutableSet;
@@ -72,6 +71,10 @@
7271
import io.opentelemetry.sdk.metrics.data.MetricData;
7372
import io.opentelemetry.sdk.metrics.data.PointData;
7473
import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
74+
import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
75+
import io.opentelemetry.sdk.trace.SdkTracerProvider;
76+
import io.opentelemetry.sdk.trace.data.SpanData;
77+
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
7578
import java.io.IOException;
7679
import java.util.ArrayList;
7780
import java.util.HashMap;
@@ -83,7 +86,6 @@
8386
import java.util.concurrent.TimeUnit;
8487
import java.util.function.Predicate;
8588
import java.util.stream.Collectors;
86-
8789
import org.junit.jupiter.api.AfterEach;
8890
import org.junit.jupiter.api.Assertions;
8991
import org.junit.jupiter.api.BeforeEach;
@@ -921,4 +923,55 @@ void grpcOpenTelemetryImplementation_setSampledToLocalTracing_methodFullNameIsRe
921923
echoClient.close();
922924
echoClient.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS);
923925
}
926+
927+
@Test
928+
void testTracingFeatureFlag() throws Exception {
929+
// Test tracing disabled
930+
System.setProperty("GOOGLE_CLOUD_ENABLE_TRACING", "false");
931+
TracingTracerFactory factory = new TracingTracerFactory(null);
932+
ApiTracer tracer =
933+
factory.newTracer(
934+
BaseApiTracer.getInstance(),
935+
SpanName.of("EchoClient", "Echo"),
936+
ApiTracerFactory.OperationType.Unary);
937+
assertThat(tracer).isNotInstanceOf(TracingTracer.class);
938+
assertThat(tracer).isSameInstanceAs(BaseApiTracer.getInstance());
939+
940+
// Test tracing enabled
941+
System.setProperty("GOOGLE_CLOUD_ENABLE_TRACING", "true");
942+
tracer =
943+
factory.newTracer(
944+
BaseApiTracer.getInstance(),
945+
SpanName.of("EchoClient", "Echo"),
946+
ApiTracerFactory.OperationType.Unary);
947+
assertThat(tracer).isInstanceOf(TracingTracer.class);
948+
949+
// Verify dummy network trace
950+
InMemorySpanExporter spanExporter = InMemorySpanExporter.create();
951+
SdkTracerProvider tracerProvider =
952+
SdkTracerProvider.builder()
953+
.addSpanProcessor(SimpleSpanProcessor.create(spanExporter))
954+
.build();
955+
OpenTelemetry openTelemetry =
956+
OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build();
957+
958+
OpenTelemetryTracingRecorder tracingRecorder =
959+
new OpenTelemetryTracingRecorder(openTelemetry, SERVICE_NAME);
960+
TracingTracerFactory tracingTracerFactory = new TracingTracerFactory(tracingRecorder);
961+
962+
EchoClient grpcClient = TestClientInitializer.createGrpcEchoClientOpentelemetry(tracingTracerFactory);
963+
try {
964+
grpcClient.echo(EchoRequest.newBuilder().setContent("test").build());
965+
966+
List<SpanData> spans = spanExporter.getFinishedSpanItems();
967+
assertThat(spans).isNotEmpty();
968+
Optional<SpanData> t4Span =
969+
spans.stream().filter(s -> s.getName().equals("LowLevelNetworkSpan")).findFirst();
970+
assertThat(t4Span.isPresent()).isTrue();
971+
assertThat(t4Span.get().getKind()).isEqualTo(io.opentelemetry.api.trace.SpanKind.CLIENT);
972+
} finally {
973+
grpcClient.close();
974+
grpcClient.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS);
975+
}
976+
}
924977
}

0 commit comments

Comments
 (0)