From aaa4036113a25a3380e813c6a9e6c332d76d3a02 Mon Sep 17 00:00:00 2001 From: blakeli Date: Mon, 30 Mar 2026 17:47:46 -0400 Subject: [PATCH 01/10] feat(sdk-platform-java): Add CompositeTracer and CompositeTracerFactory. --- .../com/google/api/gax/rpc/ClientContext.java | 15 +- .../api/gax/tracing/ApiTracerFactory.java | 7 +- .../api/gax/tracing/CompositeTracer.java | 193 ++++++++++++++++ .../gax/tracing/CompositeTracerFactory.java | 87 ++++++++ .../GoldenSignalsMetricsTracerFactory.java | 5 + .../api/gax/tracing/LoggingTracerFactory.java | 10 +- .../api/gax/tracing/SpanTracerFactory.java | 4 +- .../google/api/gax/rpc/ClientContextTest.java | 1 + .../tracing/CompositeTracerFactoryTest.java | 146 ++++++++++++ .../api/gax/tracing/CompositeTracerTest.java | 210 ++++++++++++++++++ ...GoldenSignalsMetricsTracerFactoryTest.java | 19 ++ .../gax/tracing/LoggingTracerFactoryTest.java | 20 +- .../gax/tracing/SpanTracerFactoryTest.java | 20 ++ .../v1beta1/it/ITCompositeTracing.java | 145 ++++++++++++ 14 files changed, 865 insertions(+), 17 deletions(-) create mode 100644 sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracer.java create mode 100644 sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracerFactory.java create mode 100644 sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerFactoryTest.java create mode 100644 sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerTest.java create mode 100644 sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITCompositeTracing.java diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java index 43fdd848b6c7..6b4ad99f843d 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java @@ -271,14 +271,15 @@ public static ClientContext create(StubSettings settings) throws IOException { if (watchdogProvider != null && watchdogProvider.shouldAutoClose()) { backgroundResources.add(watchdog); } - ApiTracerContext apiTracerContext = - ApiTracerContext.newBuilder() - .setServerAddress(endpointContext.resolvedServerAddress()) - .setServerPort(endpointContext.resolvedServerPort()) - .setLibraryMetadata(settings.getLibraryMetadata()) - .build(); + ApiTracerFactory apiTracerFactory = settings.getTracerFactory(); - if (apiTracerFactory instanceof SpanTracerFactory) { + if (apiTracerFactory.needsContext()) { + ApiTracerContext apiTracerContext = + ApiTracerContext.newBuilder() + .setServerAddress(endpointContext.resolvedServerAddress()) + .setServerPort(endpointContext.resolvedServerPort()) + .setLibraryMetadata(settings.getLibraryMetadata()) + .build(); apiTracerFactory = apiTracerFactory.withContext(apiTracerContext); } diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerFactory.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerFactory.java index 2d763440dbb6..43cdf3f6caa0 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerFactory.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerFactory.java @@ -73,11 +73,8 @@ default ApiTracer newTracer(ApiTracer parent, ApiTracerContext tracerContext) { return newTracer(parent, spanName, tracerContext.operationType()); } - /** - * @return the {@link ApiTracerContext} for this factory - */ - default ApiTracerContext getApiTracerContext() { - return ApiTracerContext.empty(); + default boolean needsContext() { + return false; } /** diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracer.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracer.java new file mode 100644 index 000000000000..693968ea71ca --- /dev/null +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracer.java @@ -0,0 +1,193 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.tracing; + +import com.google.api.core.InternalApi; +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * A composite implementation of {@link ApiTracer} that delegates all tracing events to a list of + * underlying tracers. + * + *

For internal use only. + */ +@InternalApi +class CompositeTracer extends BaseApiTracer { + private final List children; + + public CompositeTracer(List children) { + this.children = ImmutableList.copyOf(children); + } + + @Override + public Scope inScope() { + final List childScopes = new ArrayList<>(children.size()); + + for (ApiTracer child : children) { + childScopes.add(child.inScope()); + } + + return () -> { + for (Scope childScope : childScopes) { + childScope.close(); + } + }; + } + + @Override + public void operationSucceeded() { + for (ApiTracer child : children) { + child.operationSucceeded(); + } + } + + @Override + public void operationCancelled() { + for (ApiTracer child : children) { + child.operationCancelled(); + } + } + + @Override + public void operationFailed(Throwable error) { + for (ApiTracer child : children) { + child.operationFailed(error); + } + } + + @Override + public void connectionSelected(String id) { + for (ApiTracer child : children) { + child.connectionSelected(id); + } + } + + @Override + @Deprecated + public void attemptStarted(int attemptNumber) { + for (ApiTracer child : children) { + child.attemptStarted(attemptNumber); + } + } + + @Override + public void attemptStarted(Object request, int attemptNumber) { + for (ApiTracer child : children) { + child.attemptStarted(request, attemptNumber); + } + } + + @Override + public void attemptSucceeded() { + for (ApiTracer child : children) { + child.attemptSucceeded(); + } + } + + @Override + public void attemptCancelled() { + for (ApiTracer child : children) { + child.attemptCancelled(); + } + } + + @Override + public void attemptFailed(Throwable error, org.threeten.bp.Duration delay) { + for (ApiTracer child : children) { + child.attemptFailed(error, delay); + } + } + + @Override + public void attemptFailedDuration(Throwable error, java.time.Duration delay) { + for (ApiTracer child : children) { + child.attemptFailedDuration(error, delay); + } + } + + @Override + public void attemptFailedRetriesExhausted(Throwable error) { + for (ApiTracer child : children) { + child.attemptFailedRetriesExhausted(error); + } + } + + @Override + public void attemptPermanentFailure(Throwable error) { + for (ApiTracer child : children) { + child.attemptPermanentFailure(error); + } + } + + @Override + public void lroStartFailed(Throwable error) { + for (ApiTracer child : children) { + child.lroStartFailed(error); + } + } + + @Override + public void lroStartSucceeded() { + for (ApiTracer child : children) { + child.lroStartSucceeded(); + } + } + + @Override + public void responseReceived() { + for (ApiTracer child : children) { + child.responseReceived(); + } + } + + @Override + public void responseHeadersReceived(Map headers) { + for (ApiTracer child : children) { + child.responseHeadersReceived(headers); + } + } + + @Override + public void requestSent() { + for (ApiTracer child : children) { + child.requestSent(); + } + } + + @Override + public void batchRequestSent(long elementCount, long requestSize) { + for (ApiTracer child : children) { + child.batchRequestSent(elementCount, requestSize); + } + } +} diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracerFactory.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracerFactory.java new file mode 100644 index 000000000000..bdcacb7fe396 --- /dev/null +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracerFactory.java @@ -0,0 +1,87 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.tracing; + +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; + +/** + * A composite implementation of {@link ApiTracerFactory} that bundles multiple tracing factories + * and produces a {@link CompositeTracer} out of them. + * + */ +public class CompositeTracerFactory extends BaseApiTracerFactory { + private final List apiTracerFactories; + + public CompositeTracerFactory(List apiTracerFactories) { + this.apiTracerFactories = ImmutableList.copyOf(apiTracerFactories); + } + + @Override + public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType) { + List children = new ArrayList<>(apiTracerFactories.size()); + + for (ApiTracerFactory factory : apiTracerFactories) { + children.add(factory.newTracer(parent, spanName, operationType)); + } + return new CompositeTracer(children); + } + + @Override + public ApiTracer newTracer(ApiTracer parent, ApiTracerContext tracerContext) { + List children = new ArrayList<>(apiTracerFactories.size()); + + for (ApiTracerFactory factory : apiTracerFactories) { + children.add(factory.newTracer(parent, tracerContext)); + } + return new CompositeTracer(children); + } + + @Override + public boolean needsContext() { + for (ApiTracerFactory factory : apiTracerFactories) { + if (factory.needsContext()) { + return true; + } + } + return false; + } + + @Override + public ApiTracerFactory withContext(ApiTracerContext context) { + List contextualizedChildren = new ArrayList<>(apiTracerFactories.size()); + + for (ApiTracerFactory factory : apiTracerFactories) { + contextualizedChildren.add(factory.withContext(context)); + } + return new CompositeTracerFactory(contextualizedChildren); + } +} diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactory.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactory.java index 9ee5567cf755..c5fee2f614c7 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactory.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactory.java @@ -74,6 +74,11 @@ public ApiTracer newTracer(ApiTracer parent, ApiTracerContext methodLevelTracerC return new GoldenSignalsMetricsTracer(metricsRecorder, mergedTracerContext); } + @Override + public boolean needsContext() { + return clientLevelTracerContext == null || clientLevelTracerContext.equals(ApiTracerContext.empty()); + } + @Override public ApiTracerFactory withContext(ApiTracerContext context) { if (context == null) { diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracerFactory.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracerFactory.java index 101500aaf53b..825163dfa7c5 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracerFactory.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracerFactory.java @@ -32,6 +32,7 @@ import com.google.api.core.BetaApi; import com.google.api.core.InternalApi; +import com.google.common.annotations.VisibleForTesting; /** A {@link ApiTracerFactory} that creates instances of {@link LoggingTracer}. */ @BetaApi @@ -57,11 +58,16 @@ public ApiTracer newTracer(ApiTracer parent, ApiTracerContext context) { return new LoggingTracer(apiTracerContext.merge(context)); } - @Override - public ApiTracerContext getApiTracerContext() { + @VisibleForTesting + ApiTracerContext getApiTracerContext() { return apiTracerContext; } + @Override + public boolean needsContext() { + return apiTracerContext == null || apiTracerContext.equals(ApiTracerContext.empty()); + } + @Override public ApiTracerFactory withContext(ApiTracerContext context) { return new LoggingTracerFactory(apiTracerContext.merge(context)); diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracerFactory.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracerFactory.java index cfa074dd6e4a..4c4dafb88f78 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracerFactory.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracerFactory.java @@ -94,8 +94,8 @@ public ApiTracer newTracer(ApiTracer parent, ApiTracerContext apiTracerContext) } @Override - public ApiTracerContext getApiTracerContext() { - return apiTracerContext; + public boolean needsContext() { + return apiTracerContext == null || apiTracerContext.equals(ApiTracerContext.empty()); } /** diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientContextTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientContextTest.java index d3c202d99ef1..73da324a4267 100644 --- a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientContextTest.java +++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientContextTest.java @@ -1298,6 +1298,7 @@ void testCreate_withTracerFactoryReturningNullWithContext() throws IOException { FixedCredentialsProvider.create(Mockito.mock(Credentials.class))); ApiTracerFactory apiTracerFactory = Mockito.mock(SpanTracerFactory.class); + Mockito.doReturn(true).when(apiTracerFactory).needsContext(); Mockito.doReturn(apiTracerFactory).when(apiTracerFactory).withContext(Mockito.any()); FakeStubSettings settings = Mockito.spy(builder.build()); diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerFactoryTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerFactoryTest.java new file mode 100644 index 000000000000..c138e1e88574 --- /dev/null +++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerFactoryTest.java @@ -0,0 +1,146 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.tracing; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class CompositeTracerFactoryTest { + + private ApiTracerFactory childFactory1; + private ApiTracerFactory childFactory2; + private CompositeTracerFactory compositeFactory; + + @BeforeEach + void setUp() { + childFactory1 = mock(ApiTracerFactory.class); + childFactory2 = mock(ApiTracerFactory.class); + compositeFactory = new CompositeTracerFactory(Arrays.asList(childFactory1, childFactory2)); + } + + @Test + void testNewTracerWithParentAndSpanName() { + ApiTracer parent = mock(ApiTracer.class); + SpanName spanName = SpanName.of("TestClient", "TestMethod"); + ApiTracerFactory.OperationType operationType = ApiTracerFactory.OperationType.Unary; + + ApiTracer tracer1 = mock(ApiTracer.class); + ApiTracer tracer2 = mock(ApiTracer.class); + + when(childFactory1.newTracer(parent, spanName, operationType)).thenReturn(tracer1); + when(childFactory2.newTracer(parent, spanName, operationType)).thenReturn(tracer2); + + ApiTracer compositeTracer = compositeFactory.newTracer(parent, spanName, operationType); + + // Verify that the composite delegates operation succeeded to its internal children + compositeTracer.operationSucceeded(); + + verify(childFactory1).newTracer(parent, spanName, operationType); + verify(childFactory2).newTracer(parent, spanName, operationType); + verify(tracer1).operationSucceeded(); + verify(tracer2).operationSucceeded(); + } + + @Test + void testNewTracerWithApiTracerContext() { + ApiTracer parent = mock(ApiTracer.class); + ApiTracerContext context = ApiTracerContext.empty(); + + ApiTracer tracer1 = mock(ApiTracer.class); + ApiTracer tracer2 = mock(ApiTracer.class); + + when(childFactory1.newTracer(parent, context)).thenReturn(tracer1); + when(childFactory2.newTracer(parent, context)).thenReturn(tracer2); + + ApiTracer compositeTracer = compositeFactory.newTracer(parent, context); + + // Verify that the composite delegates correctly + compositeTracer.operationSucceeded(); + + verify(childFactory1).newTracer(parent, context); + verify(childFactory2).newTracer(parent, context); + verify(tracer1).operationSucceeded(); + verify(tracer2).operationSucceeded(); + } + + @Test + void testWithContext() { + ApiTracerContext context = ApiTracerContext.empty(); + + ApiTracerFactory contextualizedFactory1 = mock(ApiTracerFactory.class); + ApiTracerFactory contextualizedFactory2 = mock(ApiTracerFactory.class); + + when(childFactory1.withContext(context)).thenReturn(contextualizedFactory1); + when(childFactory2.withContext(context)).thenReturn(contextualizedFactory2); + + ApiTracerFactory newCompositeFactory = compositeFactory.withContext(context); + + // Create tracer from the new compositeFactory and verify it delegates to the contextualized children + ApiTracer parent = mock(ApiTracer.class); + ApiTracerContext tracerContext = ApiTracerContext.empty(); + + ApiTracer tracer1 = mock(ApiTracer.class); + ApiTracer tracer2 = mock(ApiTracer.class); + + when(contextualizedFactory1.newTracer(parent, tracerContext)).thenReturn(tracer1); + when(contextualizedFactory2.newTracer(parent, tracerContext)).thenReturn(tracer2); + + ApiTracer compositeTracer = newCompositeFactory.newTracer(parent, tracerContext); + compositeTracer.operationSucceeded(); + + verify(childFactory1).withContext(context); + verify(childFactory2).withContext(context); + verify(contextualizedFactory1).newTracer(parent, tracerContext); + verify(contextualizedFactory2).newTracer(parent, tracerContext); + verify(tracer1).operationSucceeded(); + verify(tracer2).operationSucceeded(); + } + + @Test + void testNeedsContext_returnsFalseWhenNoChildrenNeedContext() { + when(childFactory1.needsContext()).thenReturn(false); + when(childFactory2.needsContext()).thenReturn(false); + + org.junit.jupiter.api.Assertions.assertFalse(compositeFactory.needsContext()); + } + + @Test + void testNeedsContext_returnsTrueWhenAtLeastOneChildNeedsContext() { + when(childFactory1.needsContext()).thenReturn(true); + when(childFactory2.needsContext()).thenReturn(false); + + org.junit.jupiter.api.Assertions.assertTrue(compositeFactory.needsContext()); + } +} diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerTest.java new file mode 100644 index 000000000000..c0713352b6c2 --- /dev/null +++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerTest.java @@ -0,0 +1,210 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.tracing; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableMap; +import java.util.Arrays; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class CompositeTracerTest { + + private ApiTracer child1; + private ApiTracer child2; + private CompositeTracer compositeTracer; + + @BeforeEach + void setUp() { + child1 = mock(ApiTracer.class); + child2 = mock(ApiTracer.class); + compositeTracer = new CompositeTracer(Arrays.asList(child1, child2)); + } + + @Test + void testInScope() { + ApiTracer.Scope scope1 = mock(ApiTracer.Scope.class); + ApiTracer.Scope scope2 = mock(ApiTracer.Scope.class); + + when(child1.inScope()).thenReturn(scope1); + when(child2.inScope()).thenReturn(scope2); + + ApiTracer.Scope compositeScope = compositeTracer.inScope(); + compositeScope.close(); + + verify(child1).inScope(); + verify(child2).inScope(); + verify(scope1).close(); + verify(scope2).close(); + } + + @Test + void testOperationSucceeded() { + compositeTracer.operationSucceeded(); + verify(child1).operationSucceeded(); + verify(child2).operationSucceeded(); + } + + @Test + void testOperationCancelled() { + compositeTracer.operationCancelled(); + verify(child1).operationCancelled(); + verify(child2).operationCancelled(); + } + + @Test + void testOperationFailed() { + Throwable error = new RuntimeException("test error"); + compositeTracer.operationFailed(error); + verify(child1).operationFailed(error); + verify(child2).operationFailed(error); + } + + @Test + void testConnectionSelected() { + String id = "connection_id_1"; + compositeTracer.connectionSelected(id); + verify(child1).connectionSelected(id); + verify(child2).connectionSelected(id); + } + + @Test + @SuppressWarnings("deprecation") + void testAttemptStartedDeprecated() { + compositeTracer.attemptStarted(2); + verify(child1).attemptStarted(2); + verify(child2).attemptStarted(2); + } + + @Test + void testAttemptStarted() { + Object request = new Object(); + compositeTracer.attemptStarted(request, 3); + verify(child1).attemptStarted(request, 3); + verify(child2).attemptStarted(request, 3); + } + + @Test + void testAttemptSucceeded() { + compositeTracer.attemptSucceeded(); + verify(child1).attemptSucceeded(); + verify(child2).attemptSucceeded(); + } + + @Test + void testAttemptCancelled() { + compositeTracer.attemptCancelled(); + verify(child1).attemptCancelled(); + verify(child2).attemptCancelled(); + } + + @Test + @SuppressWarnings("deprecation") + void testAttemptFailedDeprecated() { + Throwable error = new RuntimeException("test error"); + org.threeten.bp.Duration delay = org.threeten.bp.Duration.ofSeconds(1); + compositeTracer.attemptFailed(error, delay); + verify(child1).attemptFailed(error, delay); + verify(child2).attemptFailed(error, delay); + } + + @Test + void testAttemptFailedDuration() { + Throwable error = new RuntimeException("test error"); + java.time.Duration delay = java.time.Duration.ofSeconds(1); + compositeTracer.attemptFailedDuration(error, delay); + verify(child1).attemptFailedDuration(error, delay); + verify(child2).attemptFailedDuration(error, delay); + } + + @Test + void testAttemptFailedRetriesExhausted() { + Throwable error = new RuntimeException("test error"); + compositeTracer.attemptFailedRetriesExhausted(error); + verify(child1).attemptFailedRetriesExhausted(error); + verify(child2).attemptFailedRetriesExhausted(error); + } + + @Test + void testAttemptPermanentFailure() { + Throwable error = new RuntimeException("test error"); + compositeTracer.attemptPermanentFailure(error); + verify(child1).attemptPermanentFailure(error); + verify(child2).attemptPermanentFailure(error); + } + + @Test + void testLroStartFailed() { + Throwable error = new RuntimeException("test error"); + compositeTracer.lroStartFailed(error); + verify(child1).lroStartFailed(error); + verify(child2).lroStartFailed(error); + } + + @Test + void testLroStartSucceeded() { + compositeTracer.lroStartSucceeded(); + verify(child1).lroStartSucceeded(); + verify(child2).lroStartSucceeded(); + } + + @Test + void testResponseReceived() { + compositeTracer.responseReceived(); + verify(child1).responseReceived(); + verify(child2).responseReceived(); + } + + @Test + void testResponseHeadersReceived() { + Map headers = ImmutableMap.of("testHeader", "testValue"); + compositeTracer.responseHeadersReceived(headers); + verify(child1).responseHeadersReceived(headers); + verify(child2).responseHeadersReceived(headers); + } + + @Test + void testRequestSent() { + compositeTracer.requestSent(); + verify(child1).requestSent(); + verify(child2).requestSent(); + } + + @Test + void testBatchRequestSent() { + compositeTracer.batchRequestSent(10L, 100L); + verify(child1).batchRequestSent(10L, 100L); + verify(child2).batchRequestSent(10L, 100L); + } +} diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactoryTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactoryTest.java index 04d164619176..d94013ac453c 100644 --- a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactoryTest.java +++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactoryTest.java @@ -130,4 +130,23 @@ void newTracerWithApiTracerContext_shouldCreateBaseTracer_ifMetricsRecorderIsNul assertThat(actual).isInstanceOf(BaseApiTracer.class); } + + @Test + void testNeedsContext_returnsTrueWhenContextIsEmpty() { + GoldenSignalsMetricsTracerFactory factoryWithoutContext = + new GoldenSignalsMetricsTracerFactory(OpenTelemetry.noop()); + + assertThat(factoryWithoutContext.needsContext()).isTrue(); + } + + @Test + void testNeedsContext_returnsFalseWhenContextIsNotEmpty() { + LibraryMetadata metadata = + LibraryMetadata.newBuilder().setArtifactName("gax-java").setVersion("1.0").build(); + ApiTracerContext context = ApiTracerContext.newBuilder().setLibraryMetadata(metadata).build(); + + tracerFactory.withContext(context); + + assertThat(tracerFactory.needsContext()).isFalse(); + } } diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerFactoryTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerFactoryTest.java index db36746ab56a..ee0f2b30dd56 100644 --- a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerFactoryTest.java +++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerFactoryTest.java @@ -65,10 +65,28 @@ void testWithContext_ReturnsNewFactoryWithMergedContext() { LoggingTracerFactory factory = new LoggingTracerFactory(); ApiTracerContext context = ApiTracerContext.empty().toBuilder().setServerAddress("address").build(); - ApiTracerFactory updatedFactory = factory.withContext(context); + LoggingTracerFactory updatedFactory = (LoggingTracerFactory) factory.withContext(context); assertNotNull(updatedFactory); assertTrue(updatedFactory instanceof LoggingTracerFactory); assertEquals("address", updatedFactory.getApiTracerContext().serverAddress()); } + + @Test + void testNeedsContext_returnsTrueWhenContextIsEmpty() { + LoggingTracerFactory factoryWithoutContext = new LoggingTracerFactory(); + assertTrue(factoryWithoutContext.needsContext()); + } + + @Test + void testNeedsContext_returnsFalseWhenContextIsNotEmpty() { + LoggingTracerFactory factoryWithoutContext = new LoggingTracerFactory(); + + ApiTracerContext context = + ApiTracerContext.empty().toBuilder().setServerAddress("address").build(); + LoggingTracerFactory factoryWithContext = + (LoggingTracerFactory) factoryWithoutContext.withContext(context); + + org.junit.jupiter.api.Assertions.assertFalse(factoryWithContext.needsContext()); + } } diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerFactoryTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerFactoryTest.java index f534d41edfbb..89867bc8889c 100644 --- a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerFactoryTest.java +++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerFactoryTest.java @@ -384,4 +384,24 @@ void testWithContext_nullTracer_returnsBaseApiTracerFactory() { ApiTracerFactory factoryWithContext = factory.withContext(context); assertThat(factoryWithContext).isInstanceOf(BaseApiTracerFactory.class); } + + @Test + void testNeedsContext_returnsTrueWhenContextIsEmpty() { + SpanTracerFactory factoryWithoutContext = + new SpanTracerFactory(openTelemetry, tracer, ApiTracerContext.empty()); + assertThat(factoryWithoutContext.needsContext()).isTrue(); + } + + @Test + void testNeedsContext_returnsFalseWhenContextIsNotEmpty() { + ApiTracerContext context = + ApiTracerContext.newBuilder() + .setLibraryMetadata(validMetadata) + .setServerAddress("test-address") + .build(); + SpanTracerFactory factoryWithContext = + new SpanTracerFactory(openTelemetry, tracer, context); + + assertThat(factoryWithContext.needsContext()).isFalse(); + } } diff --git a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITCompositeTracing.java b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITCompositeTracing.java new file mode 100644 index 000000000000..21459609d06d --- /dev/null +++ b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITCompositeTracing.java @@ -0,0 +1,145 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.showcase.v1beta1.it; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.gax.tracing.CompositeTracerFactory; +import com.google.api.gax.tracing.GoldenSignalsMetricsTracerFactory; +import com.google.api.gax.tracing.LoggingTracerFactory; +import com.google.api.gax.tracing.ObservabilityAttributes; +import com.google.api.gax.tracing.SpanTracerFactory; +import com.google.showcase.v1beta1.EchoClient; +import com.google.showcase.v1beta1.EchoRequest; +import com.google.showcase.v1beta1.it.util.TestClientInitializer; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ITCompositeTracing { + private static final String SHOWCASE_SERVER_ADDRESS = "localhost"; + private static final String SHOWCASE_ARTIFACT = "com.google.cloud:gapic-showcase"; + + private InMemorySpanExporter spanExporter; + private InMemoryMetricReader metricReader; + private OpenTelemetrySdk openTelemetrySdk; + + @BeforeEach + void setup() { + spanExporter = InMemorySpanExporter.create(); + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build(); + + metricReader = InMemoryMetricReader.create(); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + + openTelemetrySdk = + OpenTelemetrySdk.builder() + .setTracerProvider(tracerProvider) + .setMeterProvider(meterProvider) + .buildAndRegisterGlobal(); + } + + @AfterEach + void tearDown() { + if (openTelemetrySdk != null) { + openTelemetrySdk.close(); + } + GlobalOpenTelemetry.resetForTest(); + } + + private CompositeTracerFactory createCompositeTracerFactory() { + SpanTracerFactory spanTracerFactory = new SpanTracerFactory(openTelemetrySdk); + LoggingTracerFactory loggingTracerFactory = new LoggingTracerFactory(); + GoldenSignalsMetricsTracerFactory metricsTracerFactory = new GoldenSignalsMetricsTracerFactory(openTelemetrySdk); + + return new CompositeTracerFactory( + Arrays.asList(spanTracerFactory, loggingTracerFactory, metricsTracerFactory)); + } + + @Test + void testCompositeTracer() throws Exception { + try (EchoClient client = + TestClientInitializer.createGrpcEchoClientOpentelemetry(createCompositeTracerFactory())) { + + client.echo(EchoRequest.newBuilder().setContent("composite-tracing-test").build()); + + // Verify Span name and one basic attribute server.address + List actualSpans = spanExporter.getFinishedSpanItems(); + assertThat(actualSpans).isNotEmpty(); + + SpanData attemptSpan = + actualSpans.stream() + .filter(span -> span.getName().equals("google.showcase.v1beta1.Echo/Echo")) + .findFirst() + .orElseThrow(() -> new AssertionError("Incorrect span name")); + assertThat(attemptSpan.getInstrumentationScopeInfo().getName()).isEqualTo(SHOWCASE_ARTIFACT); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(ObservabilityAttributes.SERVER_ADDRESS_ATTRIBUTE))) + .isEqualTo(SHOWCASE_SERVER_ADDRESS); + + // Verify metric name and one basic attribute server.address + Collection actualMetrics = metricReader.collectAllMetrics(); + + assertThat(actualMetrics).isNotEmpty(); + MetricData metricData = actualMetrics.stream() + .filter(metricData1 -> metricData1.getName().equals("gcp.client.request.duration")) + .findFirst() + .orElseThrow(() -> new AssertionError("Incorrect metric name")); + assertThat(metricData.getInstrumentationScopeInfo().getName()).isEqualTo(SHOWCASE_ARTIFACT); + + assertThat(metricData.getHistogramData().getPoints().iterator().next() + .getAttributes() + .get(AttributeKey.stringKey(ObservabilityAttributes.SERVER_ADDRESS_ATTRIBUTE))) + .isEqualTo(SHOWCASE_SERVER_ADDRESS); + + // TODO: Verify Logging with basic attributes + } + } +} From 84384bfd1282d2ea26ddc058ad82f640698ba3cb Mon Sep 17 00:00:00 2001 From: blakeli Date: Mon, 30 Mar 2026 17:56:43 -0400 Subject: [PATCH 02/10] docs: Add javadoc --- .../java/com/google/api/gax/tracing/ApiTracerFactory.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerFactory.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerFactory.java index 43cdf3f6caa0..4814d5c8815f 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerFactory.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerFactory.java @@ -73,6 +73,12 @@ default ApiTracer newTracer(ApiTracer parent, ApiTracerContext tracerContext) { return newTracer(parent, spanName, tracerContext.operationType()); } + /** + * Indicates whether this factory requires an {@link ApiTracerContext} to be injected via {@link + * #withContext(ApiTracerContext)} before creating tracers. + * + * @return {@code true} if an {@link ApiTracerContext} should be injected, {@code false} otherwise. + */ default boolean needsContext() { return false; } From 56114b57611628862b87e51ae9b26923811fd3b3 Mon Sep 17 00:00:00 2001 From: blakeli Date: Tue, 31 Mar 2026 10:31:43 -0400 Subject: [PATCH 03/10] chore:format --- .../java/com/google/api/gax/rpc/ClientContext.java | 11 +++++------ .../google/api/gax/tracing/ApiTracerFactory.java | 3 ++- .../api/gax/tracing/CompositeTracerFactory.java | 1 - .../tracing/GoldenSignalsMetricsTracerFactory.java | 3 ++- .../api/gax/tracing/CompositeTracerFactoryTest.java | 13 +++++++------ .../GoldenSignalsMetricsTracerFactoryTest.java | 2 +- .../api/gax/tracing/LoggingTracerFactoryTest.java | 2 +- .../api/gax/tracing/SpanTracerFactoryTest.java | 3 +-- 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java index 6b4ad99f843d..b0daed0a7122 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java @@ -44,7 +44,6 @@ import com.google.api.gax.tracing.ApiTracerContext; import com.google.api.gax.tracing.ApiTracerFactory; import com.google.api.gax.tracing.BaseApiTracerFactory; -import com.google.api.gax.tracing.SpanTracerFactory; import com.google.auth.ApiKeyCredentials; import com.google.auth.CredentialTypeForMetrics; import com.google.auth.Credentials; @@ -275,11 +274,11 @@ public static ClientContext create(StubSettings settings) throws IOException { ApiTracerFactory apiTracerFactory = settings.getTracerFactory(); if (apiTracerFactory.needsContext()) { ApiTracerContext apiTracerContext = - ApiTracerContext.newBuilder() - .setServerAddress(endpointContext.resolvedServerAddress()) - .setServerPort(endpointContext.resolvedServerPort()) - .setLibraryMetadata(settings.getLibraryMetadata()) - .build(); + ApiTracerContext.newBuilder() + .setServerAddress(endpointContext.resolvedServerAddress()) + .setServerPort(endpointContext.resolvedServerPort()) + .setLibraryMetadata(settings.getLibraryMetadata()) + .build(); apiTracerFactory = apiTracerFactory.withContext(apiTracerContext); } diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerFactory.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerFactory.java index 4814d5c8815f..ee2bf66ab00b 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerFactory.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerFactory.java @@ -77,7 +77,8 @@ default ApiTracer newTracer(ApiTracer parent, ApiTracerContext tracerContext) { * Indicates whether this factory requires an {@link ApiTracerContext} to be injected via {@link * #withContext(ApiTracerContext)} before creating tracers. * - * @return {@code true} if an {@link ApiTracerContext} should be injected, {@code false} otherwise. + * @return {@code true} if an {@link ApiTracerContext} should be injected, {@code false} + * otherwise. */ default boolean needsContext() { return false; diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracerFactory.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracerFactory.java index bdcacb7fe396..3e4c0041e1fc 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracerFactory.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracerFactory.java @@ -36,7 +36,6 @@ /** * A composite implementation of {@link ApiTracerFactory} that bundles multiple tracing factories * and produces a {@link CompositeTracer} out of them. - * */ public class CompositeTracerFactory extends BaseApiTracerFactory { private final List apiTracerFactories; diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactory.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactory.java index c5fee2f614c7..34da82de09d8 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactory.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactory.java @@ -76,7 +76,8 @@ public ApiTracer newTracer(ApiTracer parent, ApiTracerContext methodLevelTracerC @Override public boolean needsContext() { - return clientLevelTracerContext == null || clientLevelTracerContext.equals(ApiTracerContext.empty()); + return clientLevelTracerContext == null + || clientLevelTracerContext.equals(ApiTracerContext.empty()); } @Override diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerFactoryTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerFactoryTest.java index c138e1e88574..d508a3e3883b 100644 --- a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerFactoryTest.java +++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerFactoryTest.java @@ -63,10 +63,10 @@ void testNewTracerWithParentAndSpanName() { when(childFactory2.newTracer(parent, spanName, operationType)).thenReturn(tracer2); ApiTracer compositeTracer = compositeFactory.newTracer(parent, spanName, operationType); - + // Verify that the composite delegates operation succeeded to its internal children compositeTracer.operationSucceeded(); - + verify(childFactory1).newTracer(parent, spanName, operationType); verify(childFactory2).newTracer(parent, spanName, operationType); verify(tracer1).operationSucceeded(); @@ -85,10 +85,10 @@ void testNewTracerWithApiTracerContext() { when(childFactory2.newTracer(parent, context)).thenReturn(tracer2); ApiTracer compositeTracer = compositeFactory.newTracer(parent, context); - + // Verify that the composite delegates correctly compositeTracer.operationSucceeded(); - + verify(childFactory1).newTracer(parent, context); verify(childFactory2).newTracer(parent, context); verify(tracer1).operationSucceeded(); @@ -106,8 +106,9 @@ void testWithContext() { when(childFactory2.withContext(context)).thenReturn(contextualizedFactory2); ApiTracerFactory newCompositeFactory = compositeFactory.withContext(context); - - // Create tracer from the new compositeFactory and verify it delegates to the contextualized children + + // Create tracer from the new compositeFactory and verify it delegates to the contextualized + // children ApiTracer parent = mock(ApiTracer.class); ApiTracerContext tracerContext = ApiTracerContext.empty(); diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactoryTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactoryTest.java index d94013ac453c..65b18689686b 100644 --- a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactoryTest.java +++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactoryTest.java @@ -144,7 +144,7 @@ void testNeedsContext_returnsFalseWhenContextIsNotEmpty() { LibraryMetadata metadata = LibraryMetadata.newBuilder().setArtifactName("gax-java").setVersion("1.0").build(); ApiTracerContext context = ApiTracerContext.newBuilder().setLibraryMetadata(metadata).build(); - + tracerFactory.withContext(context); assertThat(tracerFactory.needsContext()).isFalse(); diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerFactoryTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerFactoryTest.java index ee0f2b30dd56..379ab1420686 100644 --- a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerFactoryTest.java +++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerFactoryTest.java @@ -81,7 +81,7 @@ void testNeedsContext_returnsTrueWhenContextIsEmpty() { @Test void testNeedsContext_returnsFalseWhenContextIsNotEmpty() { LoggingTracerFactory factoryWithoutContext = new LoggingTracerFactory(); - + ApiTracerContext context = ApiTracerContext.empty().toBuilder().setServerAddress("address").build(); LoggingTracerFactory factoryWithContext = diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerFactoryTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerFactoryTest.java index 89867bc8889c..6b8305c90e6e 100644 --- a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerFactoryTest.java +++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerFactoryTest.java @@ -399,8 +399,7 @@ void testNeedsContext_returnsFalseWhenContextIsNotEmpty() { .setLibraryMetadata(validMetadata) .setServerAddress("test-address") .build(); - SpanTracerFactory factoryWithContext = - new SpanTracerFactory(openTelemetry, tracer, context); + SpanTracerFactory factoryWithContext = new SpanTracerFactory(openTelemetry, tracer, context); assertThat(factoryWithContext.needsContext()).isFalse(); } From d21c83829e4c02899857ebebe36811a731059d7e Mon Sep 17 00:00:00 2001 From: blakeli Date: Wed, 1 Apr 2026 00:52:29 -0400 Subject: [PATCH 04/10] fix(tracing): ensure CompositeTracer scopes are closed safely in LIFO order --- .../api/gax/tracing/CompositeTracer.java | 31 +++++++++-- .../google/api/gax/tracing/SpanTracer.java | 9 ++++ .../api/gax/tracing/CompositeTracerTest.java | 54 ++++++++++++++++++- 3 files changed, 88 insertions(+), 6 deletions(-) diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracer.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracer.java index 693968ea71ca..421f35fa3145 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracer.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracer.java @@ -53,13 +53,36 @@ public CompositeTracer(List children) { public Scope inScope() { final List childScopes = new ArrayList<>(children.size()); - for (ApiTracer child : children) { - childScopes.add(child.inScope()); + try { + for (ApiTracer child : children) { + childScopes.add(child.inScope()); + } + } catch (RuntimeException e) { + for (int i = childScopes.size() - 1; i >= 0; i--) { + try { + childScopes.get(i).close(); + } catch (RuntimeException suppressed) { + e.addSuppressed(suppressed); + } + } + throw e; } return () -> { - for (Scope childScope : childScopes) { - childScope.close(); + RuntimeException exception = null; + for (int i = childScopes.size() - 1; i >= 0; i--) { + try { + childScopes.get(i).close(); + } catch (RuntimeException e) { + if (exception == null) { + exception = e; + } else { + exception.addSuppressed(e); + } + } + } + if (exception != null) { + throw exception; } }; } diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java index eae14b1d9dd1..ef13a7f98400 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java @@ -85,6 +85,15 @@ public SpanTracer(Tracer tracer, ApiTracerContext apiTracerContext) { buildAttributes(); } + @Override + public Scope inScope() { + if (attemptSpan == null) { + return () -> {}; + } + final io.opentelemetry.context.Scope otelScope = attemptSpan.makeCurrent(); + return () -> otelScope.close(); + } + private static String resolveAttemptSpanName(ApiTracerContext apiTracerContext) { if (apiTracerContext.transport() == ApiTracerContext.Transport.GRPC) { // gRPC Uses the full method name as span name. diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerTest.java index c0713352b6c2..64fdc43e096f 100644 --- a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerTest.java +++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerTest.java @@ -29,6 +29,11 @@ */ package com.google.api.gax.tracing; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -38,6 +43,7 @@ import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.InOrder; class CompositeTracerTest { @@ -53,7 +59,7 @@ void setUp() { } @Test - void testInScope() { + void testInScope_lifoOrder() { ApiTracer.Scope scope1 = mock(ApiTracer.Scope.class); ApiTracer.Scope scope2 = mock(ApiTracer.Scope.class); @@ -63,10 +69,54 @@ void testInScope() { ApiTracer.Scope compositeScope = compositeTracer.inScope(); compositeScope.close(); + verify(child1).inScope(); + verify(child2).inScope(); + + InOrder inOrder = inOrder(scope2, scope1); + inOrder.verify(scope2).close(); + inOrder.verify(scope1).close(); + } + + @Test + void testInScope_childInScopeThrows() { + ApiTracer.Scope scope1 = mock(ApiTracer.Scope.class); + RuntimeException exception = new RuntimeException("Runtime Error"); + + when(child1.inScope()).thenReturn(scope1); + when(child2.inScope()).thenThrow(exception); + + RuntimeException thrown = assertThrows(RuntimeException.class, () -> compositeTracer.inScope()); + + assertEquals(exception, thrown); verify(child1).inScope(); verify(child2).inScope(); verify(scope1).close(); - verify(scope2).close(); + } + + @Test + void testInScope_childScopeCloseThrows() { + ApiTracer.Scope scope1 = mock(ApiTracer.Scope.class); + ApiTracer.Scope scope2 = mock(ApiTracer.Scope.class); + + RuntimeException exception2 = new RuntimeException("Scope 2 close Error"); + RuntimeException exception1 = new RuntimeException("Scope 1 close Error"); + + when(child1.inScope()).thenReturn(scope1); + when(child2.inScope()).thenReturn(scope2); + + doThrow(exception2).when(scope2).close(); + doThrow(exception1).when(scope1).close(); + + ApiTracer.Scope compositeScope = compositeTracer.inScope(); + + RuntimeException thrown = assertThrows(RuntimeException.class, () -> compositeScope.close()); + + assertEquals(exception2, thrown); + assertTrue(Arrays.asList(thrown.getSuppressed()).contains(exception1)); + + InOrder inOrder = inOrder(scope2, scope1); + inOrder.verify(scope2).close(); + inOrder.verify(scope1).close(); } @Test From cbe47407f99dfc01bad2ad019d4e2fcecf6a9583 Mon Sep 17 00:00:00 2001 From: blakeli Date: Wed, 1 Apr 2026 01:00:44 -0400 Subject: [PATCH 05/10] add LoggingTracerFactory if logging is enabled --- sdk-platform-java/gax-java/gax/pom.xml | 4 +- .../google/api/gax/logging/LoggingUtils.java | 4 +- .../com/google/api/gax/rpc/ClientContext.java | 38 ++++++++--- .../google/api/gax/rpc/ClientContextTest.java | 67 ++++++++++++++++++- .../api/gax/rpc/testing/FakeStubSettings.java | 6 ++ 5 files changed, 103 insertions(+), 16 deletions(-) diff --git a/sdk-platform-java/gax-java/gax/pom.xml b/sdk-platform-java/gax-java/gax/pom.xml index 2feb9c9aba81..9c2589279b88 100644 --- a/sdk-platform-java/gax-java/gax/pom.xml +++ b/sdk-platform-java/gax-java/gax/pom.xml @@ -139,7 +139,7 @@ @{argLine} -Djava.util.logging.SimpleFormatter.format="%1$tY %1$tl:%1$tM:%1$tS.%1$tL %2$s %4$s: %5$s%6$s%n" - !EndpointContextTest#endpointContextBuild_universeDomainEnvVarSet+endpointContextBuild_multipleUniverseDomainConfigurations_clientSettingsHasPriority,!LoggingEnabledTest,!LoggingTracerTest + !EndpointContextTest#endpointContextBuild_universeDomainEnvVarSet+endpointContextBuild_multipleUniverseDomainConfigurations_clientSettingsHasPriority,!LoggingEnabledTest,!LoggingTracerTest,!ClientContextTest#testGetApiTracerFactory_loggingEnabled @@ -154,7 +154,7 @@ org.apache.maven.plugins maven-surefire-plugin - EndpointContextTest#endpointContextBuild_universeDomainEnvVarSet+endpointContextBuild_multipleUniverseDomainConfigurations_clientSettingsHasPriority,LoggingEnabledTest,LoggingTracerTest + EndpointContextTest#endpointContextBuild_universeDomainEnvVarSet+endpointContextBuild_multipleUniverseDomainConfigurations_clientSettingsHasPriority,LoggingEnabledTest,LoggingTracerTest, ClientContextTest#testGetApiTracerFactory_loggingEnabled diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/logging/LoggingUtils.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/logging/LoggingUtils.java index 0797527bc8b8..f3d504ff59f4 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/logging/LoggingUtils.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/logging/LoggingUtils.java @@ -36,7 +36,7 @@ @InternalApi public class LoggingUtils { - static final String GOOGLE_SDK_JAVA_LOGGING = "GOOGLE_SDK_JAVA_LOGGING"; + private static final String GOOGLE_SDK_JAVA_LOGGING = "GOOGLE_SDK_JAVA_LOGGING"; private static boolean loggingEnabled = checkLoggingEnabled(GOOGLE_SDK_JAVA_LOGGING); @@ -45,7 +45,7 @@ public class LoggingUtils { * * @return true if logging is enabled, false otherwise. */ - static boolean isLoggingEnabled() { + public static boolean isLoggingEnabled() { return loggingEnabled; } diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java index b0daed0a7122..fe8ebd2a14f3 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java @@ -40,10 +40,13 @@ import com.google.api.gax.core.BackgroundResource; import com.google.api.gax.core.ExecutorAsBackgroundResource; import com.google.api.gax.core.ExecutorProvider; +import com.google.api.gax.logging.LoggingUtils; import com.google.api.gax.rpc.internal.QuotaProjectIdHidingCredentials; import com.google.api.gax.tracing.ApiTracerContext; import com.google.api.gax.tracing.ApiTracerFactory; import com.google.api.gax.tracing.BaseApiTracerFactory; +import com.google.api.gax.tracing.CompositeTracerFactory; +import com.google.api.gax.tracing.LoggingTracerFactory; import com.google.auth.ApiKeyCredentials; import com.google.auth.CredentialTypeForMetrics; import com.google.auth.Credentials; @@ -271,16 +274,7 @@ public static ClientContext create(StubSettings settings) throws IOException { backgroundResources.add(watchdog); } - ApiTracerFactory apiTracerFactory = settings.getTracerFactory(); - if (apiTracerFactory.needsContext()) { - ApiTracerContext apiTracerContext = - ApiTracerContext.newBuilder() - .setServerAddress(endpointContext.resolvedServerAddress()) - .setServerPort(endpointContext.resolvedServerPort()) - .setLibraryMetadata(settings.getLibraryMetadata()) - .build(); - apiTracerFactory = apiTracerFactory.withContext(apiTracerContext); - } + ApiTracerFactory apiTracerFactory = getApiTracerFactory(settings, endpointContext); return newBuilder() .setBackgroundResources(backgroundResources.build()) @@ -301,6 +295,30 @@ public static ClientContext create(StubSettings settings) throws IOException { .build(); } + @VisibleForTesting + static ApiTracerFactory getApiTracerFactory( + StubSettings settings, EndpointContext endpointContext) { + ApiTracerFactory apiTracerFactory = settings.getTracerFactory(); + + if (LoggingUtils.isLoggingEnabled()) { + apiTracerFactory = + new CompositeTracerFactory( + ImmutableList.of(new LoggingTracerFactory(), apiTracerFactory)); + } + + if (apiTracerFactory.needsContext()) { + ApiTracerContext apiTracerContext = + ApiTracerContext.newBuilder() + .setServerAddress(endpointContext.resolvedServerAddress()) + .setServerPort(endpointContext.resolvedServerPort()) + .setLibraryMetadata(settings.getLibraryMetadata()) + .build(); + apiTracerFactory = apiTracerFactory.withContext(apiTracerContext); + } + + return apiTracerFactory; + } + /** Determines which credentials to use. API key overrides credentials provided by provider. */ private static Credentials getCredentials(StubSettings settings) throws IOException { Credentials credentials; diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientContextTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientContextTest.java index 73da324a4267..9852f04cdbde 100644 --- a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientContextTest.java +++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientContextTest.java @@ -54,7 +54,6 @@ import com.google.api.gax.rpc.testing.FakeStubSettings; import com.google.api.gax.rpc.testing.FakeTransportChannel; import com.google.api.gax.tracing.ApiTracerFactory; -import com.google.api.gax.tracing.SpanTracerFactory; import com.google.auth.ApiKeyCredentials; import com.google.auth.CredentialTypeForMetrics; import com.google.auth.Credentials; @@ -1297,7 +1296,7 @@ void testCreate_withTracerFactoryReturningNullWithContext() throws IOException { builder.setCredentialsProvider( FixedCredentialsProvider.create(Mockito.mock(Credentials.class))); - ApiTracerFactory apiTracerFactory = Mockito.mock(SpanTracerFactory.class); + ApiTracerFactory apiTracerFactory = Mockito.mock(ApiTracerFactory.class); Mockito.doReturn(true).when(apiTracerFactory).needsContext(); Mockito.doReturn(apiTracerFactory).when(apiTracerFactory).withContext(Mockito.any()); @@ -1308,4 +1307,68 @@ void testCreate_withTracerFactoryReturningNullWithContext() throws IOException { assertThat(context.getTracerFactory()).isSameInstanceAs(apiTracerFactory); verify(apiTracerFactory, times(1)).withContext(Mockito.any()); } + + @Test + void testGetApiTracerFactory_noContextNeeded() throws java.io.IOException { + ApiTracerFactory mockTracerFactory = Mockito.mock(ApiTracerFactory.class); + when(mockTracerFactory.needsContext()).thenReturn(false); + when(mockTracerFactory.withContext( + Mockito.any(com.google.api.gax.tracing.ApiTracerContext.class))) + .thenReturn(mockTracerFactory); + + FakeStubSettings.Builder builder = FakeStubSettings.newBuilder(); + builder.setTracerFactory(mockTracerFactory); + + EndpointContext endpointContext = Mockito.mock(EndpointContext.class); + + ApiTracerFactory apiTracerFactory = + ClientContext.getApiTracerFactory(builder.build(), endpointContext); + + assertThat(apiTracerFactory).isSameInstanceAs(mockTracerFactory); + } + + @Test + void testGetApiTracerFactory_contextNeeded() throws java.io.IOException { + ApiTracerFactory mockTracerFactory = Mockito.mock(ApiTracerFactory.class); + ApiTracerFactory withContextTracerFactory = Mockito.mock(ApiTracerFactory.class); + when(mockTracerFactory.needsContext()).thenReturn(true); + when(mockTracerFactory.withContext( + Mockito.any(com.google.api.gax.tracing.ApiTracerContext.class))) + .thenReturn(withContextTracerFactory); + + FakeStubSettings.Builder builder = FakeStubSettings.newBuilder(); + builder.setTracerFactory(mockTracerFactory); + + EndpointContext endpointContext = Mockito.mock(EndpointContext.class); + when(endpointContext.resolvedServerAddress()).thenReturn("test-address"); + when(endpointContext.resolvedServerPort()).thenReturn(443); + + ApiTracerFactory apiTracerFactory = + ClientContext.getApiTracerFactory(builder.build(), endpointContext); + + assertThat(apiTracerFactory).isSameInstanceAs(withContextTracerFactory); + verify(mockTracerFactory, times(1)) + .withContext(Mockito.any(com.google.api.gax.tracing.ApiTracerContext.class)); + } + + // This test should only run when the maven profile `EnvVarTest` is enabled. + @Test + void testGetApiTracerFactory_loggingEnabled() throws java.io.IOException { + ApiTracerFactory mockTracerFactory = Mockito.mock(ApiTracerFactory.class); + when(mockTracerFactory.needsContext()).thenReturn(false); + when(mockTracerFactory.withContext( + Mockito.any(com.google.api.gax.tracing.ApiTracerContext.class))) + .thenReturn(mockTracerFactory); + + FakeStubSettings.Builder builder = FakeStubSettings.newBuilder(); + builder.setTracerFactory(mockTracerFactory); + + EndpointContext endpointContext = Mockito.mock(EndpointContext.class); + + ApiTracerFactory apiTracerFactory = + ClientContext.getApiTracerFactory(builder.build(), endpointContext); + + assertThat(apiTracerFactory) + .isInstanceOf(com.google.api.gax.tracing.CompositeTracerFactory.class); + } } diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/testing/FakeStubSettings.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/testing/FakeStubSettings.java index 4004a2440479..39ba0053bdf6 100644 --- a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/testing/FakeStubSettings.java +++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/testing/FakeStubSettings.java @@ -32,6 +32,7 @@ import com.google.api.core.InternalApi; import com.google.api.gax.core.GoogleCredentialsProvider; import com.google.api.gax.rpc.ClientContext; +import com.google.api.gax.rpc.LibraryMetadata; import com.google.api.gax.rpc.StubSettings; import com.google.common.collect.ImmutableList; import java.io.IOException; @@ -55,6 +56,11 @@ public String getServiceName() { return "test"; } + @Override + protected LibraryMetadata getLibraryMetadata() { + return LibraryMetadata.newBuilder().build(); + } + @Override public StubSettings.Builder toBuilder() { return new Builder(this); From 8f11f7f234950fc49cff556d30b7cb7cfdc586e3 Mon Sep 17 00:00:00 2001 From: blakeli Date: Wed, 1 Apr 2026 01:12:55 -0400 Subject: [PATCH 06/10] add javadoc. Remove LoggingTracerFactory from ITCompositeTracer --- .../com/google/api/gax/tracing/LoggingTracerFactory.java | 7 ++++++- .../{ITCompositeTracing.java => ITCompositeTracer.java} | 8 ++------ 2 files changed, 8 insertions(+), 7 deletions(-) rename sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/{ITCompositeTracing.java => ITCompositeTracer.java} (95%) diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracerFactory.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracerFactory.java index 825163dfa7c5..1f208202c3bf 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracerFactory.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracerFactory.java @@ -32,9 +32,14 @@ import com.google.api.core.BetaApi; import com.google.api.core.InternalApi; +import com.google.api.gax.logging.LoggingUtils; import com.google.common.annotations.VisibleForTesting; -/** A {@link ApiTracerFactory} that creates instances of {@link LoggingTracer}. */ +/** A {@link ApiTracerFactory} that creates instances of {@link LoggingTracer}. + * This class is intended for internal framework use only. Manual instantiation + * is discouraged; the lifecycle is managed automatically by the system, + * when {@link LoggingUtils#isLoggingEnabled()} returning {@code true}. + * */ @BetaApi @InternalApi public class LoggingTracerFactory implements ApiTracerFactory { diff --git a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITCompositeTracing.java b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITCompositeTracer.java similarity index 95% rename from sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITCompositeTracing.java rename to sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITCompositeTracer.java index 21459609d06d..eba2161eadda 100644 --- a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITCompositeTracing.java +++ b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITCompositeTracer.java @@ -57,7 +57,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -class ITCompositeTracing { +class ITCompositeTracer { private static final String SHOWCASE_SERVER_ADDRESS = "localhost"; private static final String SHOWCASE_ARTIFACT = "com.google.cloud:gapic-showcase"; @@ -94,11 +94,9 @@ void tearDown() { private CompositeTracerFactory createCompositeTracerFactory() { SpanTracerFactory spanTracerFactory = new SpanTracerFactory(openTelemetrySdk); - LoggingTracerFactory loggingTracerFactory = new LoggingTracerFactory(); GoldenSignalsMetricsTracerFactory metricsTracerFactory = new GoldenSignalsMetricsTracerFactory(openTelemetrySdk); - return new CompositeTracerFactory( - Arrays.asList(spanTracerFactory, loggingTracerFactory, metricsTracerFactory)); + return new CompositeTracerFactory(Arrays.asList(spanTracerFactory, metricsTracerFactory)); } @Test @@ -138,8 +136,6 @@ void testCompositeTracer() throws Exception { .getAttributes() .get(AttributeKey.stringKey(ObservabilityAttributes.SERVER_ADDRESS_ATTRIBUTE))) .isEqualTo(SHOWCASE_SERVER_ADDRESS); - - // TODO: Verify Logging with basic attributes } } } From 40a26cce8597191e0e2ffecc4af45e68ce285df0 Mon Sep 17 00:00:00 2001 From: blakeli Date: Wed, 1 Apr 2026 01:21:48 -0400 Subject: [PATCH 07/10] fix(tracing): execute completion callbacks in reverse LIFO order --- .../api/gax/tracing/CompositeTracer.java | 52 +++++++-------- .../api/gax/tracing/CompositeTracerTest.java | 65 +++++++++++-------- 2 files changed, 65 insertions(+), 52 deletions(-) diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracer.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracer.java index 421f35fa3145..c3851cba8e7a 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracer.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracer.java @@ -89,22 +89,22 @@ public Scope inScope() { @Override public void operationSucceeded() { - for (ApiTracer child : children) { - child.operationSucceeded(); + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).operationSucceeded(); } } @Override public void operationCancelled() { - for (ApiTracer child : children) { - child.operationCancelled(); + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).operationCancelled(); } } @Override public void operationFailed(Throwable error) { - for (ApiTracer child : children) { - child.operationFailed(error); + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).operationFailed(error); } } @@ -132,71 +132,71 @@ public void attemptStarted(Object request, int attemptNumber) { @Override public void attemptSucceeded() { - for (ApiTracer child : children) { - child.attemptSucceeded(); + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).attemptSucceeded(); } } @Override public void attemptCancelled() { - for (ApiTracer child : children) { - child.attemptCancelled(); + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).attemptCancelled(); } } @Override public void attemptFailed(Throwable error, org.threeten.bp.Duration delay) { - for (ApiTracer child : children) { - child.attemptFailed(error, delay); + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).attemptFailed(error, delay); } } @Override public void attemptFailedDuration(Throwable error, java.time.Duration delay) { - for (ApiTracer child : children) { - child.attemptFailedDuration(error, delay); + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).attemptFailedDuration(error, delay); } } @Override public void attemptFailedRetriesExhausted(Throwable error) { - for (ApiTracer child : children) { - child.attemptFailedRetriesExhausted(error); + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).attemptFailedRetriesExhausted(error); } } @Override public void attemptPermanentFailure(Throwable error) { - for (ApiTracer child : children) { - child.attemptPermanentFailure(error); + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).attemptPermanentFailure(error); } } @Override public void lroStartFailed(Throwable error) { - for (ApiTracer child : children) { - child.lroStartFailed(error); + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).lroStartFailed(error); } } @Override public void lroStartSucceeded() { - for (ApiTracer child : children) { - child.lroStartSucceeded(); + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).lroStartSucceeded(); } } @Override public void responseReceived() { - for (ApiTracer child : children) { - child.responseReceived(); + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).responseReceived(); } } @Override public void responseHeadersReceived(Map headers) { - for (ApiTracer child : children) { - child.responseHeadersReceived(headers); + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).responseHeadersReceived(headers); } } diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerTest.java index 64fdc43e096f..ff6484dfd3c3 100644 --- a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerTest.java +++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerTest.java @@ -122,23 +122,26 @@ void testInScope_childScopeCloseThrows() { @Test void testOperationSucceeded() { compositeTracer.operationSucceeded(); - verify(child1).operationSucceeded(); - verify(child2).operationSucceeded(); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).operationSucceeded(); + inOrder.verify(child1).operationSucceeded(); } @Test void testOperationCancelled() { compositeTracer.operationCancelled(); - verify(child1).operationCancelled(); - verify(child2).operationCancelled(); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).operationCancelled(); + inOrder.verify(child1).operationCancelled(); } @Test void testOperationFailed() { Throwable error = new RuntimeException("test error"); compositeTracer.operationFailed(error); - verify(child1).operationFailed(error); - verify(child2).operationFailed(error); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).operationFailed(error); + inOrder.verify(child1).operationFailed(error); } @Test @@ -168,15 +171,17 @@ void testAttemptStarted() { @Test void testAttemptSucceeded() { compositeTracer.attemptSucceeded(); - verify(child1).attemptSucceeded(); - verify(child2).attemptSucceeded(); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).attemptSucceeded(); + inOrder.verify(child1).attemptSucceeded(); } @Test void testAttemptCancelled() { compositeTracer.attemptCancelled(); - verify(child1).attemptCancelled(); - verify(child2).attemptCancelled(); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).attemptCancelled(); + inOrder.verify(child1).attemptCancelled(); } @Test @@ -185,8 +190,9 @@ void testAttemptFailedDeprecated() { Throwable error = new RuntimeException("test error"); org.threeten.bp.Duration delay = org.threeten.bp.Duration.ofSeconds(1); compositeTracer.attemptFailed(error, delay); - verify(child1).attemptFailed(error, delay); - verify(child2).attemptFailed(error, delay); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).attemptFailed(error, delay); + inOrder.verify(child1).attemptFailed(error, delay); } @Test @@ -194,54 +200,61 @@ void testAttemptFailedDuration() { Throwable error = new RuntimeException("test error"); java.time.Duration delay = java.time.Duration.ofSeconds(1); compositeTracer.attemptFailedDuration(error, delay); - verify(child1).attemptFailedDuration(error, delay); - verify(child2).attemptFailedDuration(error, delay); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).attemptFailedDuration(error, delay); + inOrder.verify(child1).attemptFailedDuration(error, delay); } @Test void testAttemptFailedRetriesExhausted() { Throwable error = new RuntimeException("test error"); compositeTracer.attemptFailedRetriesExhausted(error); - verify(child1).attemptFailedRetriesExhausted(error); - verify(child2).attemptFailedRetriesExhausted(error); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).attemptFailedRetriesExhausted(error); + inOrder.verify(child1).attemptFailedRetriesExhausted(error); } @Test void testAttemptPermanentFailure() { Throwable error = new RuntimeException("test error"); compositeTracer.attemptPermanentFailure(error); - verify(child1).attemptPermanentFailure(error); - verify(child2).attemptPermanentFailure(error); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).attemptPermanentFailure(error); + inOrder.verify(child1).attemptPermanentFailure(error); } @Test void testLroStartFailed() { Throwable error = new RuntimeException("test error"); compositeTracer.lroStartFailed(error); - verify(child1).lroStartFailed(error); - verify(child2).lroStartFailed(error); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).lroStartFailed(error); + inOrder.verify(child1).lroStartFailed(error); } @Test void testLroStartSucceeded() { compositeTracer.lroStartSucceeded(); - verify(child1).lroStartSucceeded(); - verify(child2).lroStartSucceeded(); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).lroStartSucceeded(); + inOrder.verify(child1).lroStartSucceeded(); } @Test void testResponseReceived() { compositeTracer.responseReceived(); - verify(child1).responseReceived(); - verify(child2).responseReceived(); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).responseReceived(); + inOrder.verify(child1).responseReceived(); } @Test void testResponseHeadersReceived() { Map headers = ImmutableMap.of("testHeader", "testValue"); compositeTracer.responseHeadersReceived(headers); - verify(child1).responseHeadersReceived(headers); - verify(child2).responseHeadersReceived(headers); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).responseHeadersReceived(headers); + inOrder.verify(child1).responseHeadersReceived(headers); } @Test From 17025f874c980047b6739a631809a02964124dd6 Mon Sep 17 00:00:00 2001 From: blakeli Date: Wed, 1 Apr 2026 01:23:55 -0400 Subject: [PATCH 08/10] chore: format and update dependencies for LoggingTracer --- .../api/gax/tracing/LoggingTracerFactory.java | 11 ++++++----- .../java-showcase/gapic-showcase/pom.xml | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracerFactory.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracerFactory.java index 1f208202c3bf..8f75cded244c 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracerFactory.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracerFactory.java @@ -35,11 +35,12 @@ import com.google.api.gax.logging.LoggingUtils; import com.google.common.annotations.VisibleForTesting; -/** A {@link ApiTracerFactory} that creates instances of {@link LoggingTracer}. - * This class is intended for internal framework use only. Manual instantiation - * is discouraged; the lifecycle is managed automatically by the system, - * when {@link LoggingUtils#isLoggingEnabled()} returning {@code true}. - * */ +/** + * A {@link ApiTracerFactory} that creates instances of {@link LoggingTracer}. This class is + * intended for internal framework use only. Manual instantiation is discouraged; the lifecycle is + * managed automatically by the system, when {@link LoggingUtils#isLoggingEnabled()} returning + * {@code true}. + */ @BetaApi @InternalApi public class LoggingTracerFactory implements ApiTracerFactory { diff --git a/sdk-platform-java/java-showcase/gapic-showcase/pom.xml b/sdk-platform-java/java-showcase/gapic-showcase/pom.xml index bc8d2cc812b6..913925e02996 100644 --- a/sdk-platform-java/java-showcase/gapic-showcase/pom.xml +++ b/sdk-platform-java/java-showcase/gapic-showcase/pom.xml @@ -233,6 +233,24 @@ + + org.slf4j + slf4j-api + 2.0.16 + test + + + ch.qos.logback + logback-classic + ${slf4j2-logback.version} + test + + + ch.qos.logback + logback-core + ${slf4j2-logback.version} + test + From eb8a07cc0361f6132cff1662086001e56baf4a3c Mon Sep 17 00:00:00 2001 From: blakeli Date: Wed, 1 Apr 2026 01:36:31 -0400 Subject: [PATCH 09/10] revert changes in SpanTracer --- .../main/java/com/google/api/gax/tracing/SpanTracer.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java index ef13a7f98400..eae14b1d9dd1 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java @@ -85,15 +85,6 @@ public SpanTracer(Tracer tracer, ApiTracerContext apiTracerContext) { buildAttributes(); } - @Override - public Scope inScope() { - if (attemptSpan == null) { - return () -> {}; - } - final io.opentelemetry.context.Scope otelScope = attemptSpan.makeCurrent(); - return () -> otelScope.close(); - } - private static String resolveAttemptSpanName(ApiTracerContext apiTracerContext) { if (apiTracerContext.transport() == ApiTracerContext.Transport.GRPC) { // gRPC Uses the full method name as span name. From 50b2429f4719560e6919240e409a3aff2c972b79 Mon Sep 17 00:00:00 2001 From: blakeli Date: Wed, 1 Apr 2026 14:32:50 -0400 Subject: [PATCH 10/10] fix(rpc): propagate serviceName to ApiTracerContext when creating TracerFactory --- .../gax/src/main/java/com/google/api/gax/rpc/ClientContext.java | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java index fe8ebd2a14f3..44687c698a12 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java @@ -311,6 +311,7 @@ static ApiTracerFactory getApiTracerFactory( ApiTracerContext.newBuilder() .setServerAddress(endpointContext.resolvedServerAddress()) .setServerPort(endpointContext.resolvedServerPort()) + .setServiceName(endpointContext.serviceName()) .setLibraryMetadata(settings.getLibraryMetadata()) .build(); apiTracerFactory = apiTracerFactory.withContext(apiTracerContext);