diff --git a/gax-java/gax/src/main/java/com/google/api/gax/rpc/StubSettings.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/StubSettings.java index f97f808ca7..35a07a5536 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/rpc/StubSettings.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/StubSettings.java @@ -46,9 +46,14 @@ import com.google.api.gax.core.NoCredentialsProvider; import com.google.api.gax.tracing.ApiTracerFactory; import com.google.api.gax.tracing.BaseApiTracerFactory; +import com.google.api.gax.tracing.CompositeApiTracerFactory; +import com.google.api.gax.tracing.OpenTelemetryTracingRecorder; +import com.google.api.gax.tracing.TracingTracerFactory; +import com.google.api.gax.tracing.TracingUtils; import com.google.auth.oauth2.QuotaProjectIdProvider; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; +import io.opentelemetry.api.GlobalOpenTelemetry; import java.io.IOException; import java.util.concurrent.Executor; import javax.annotation.Nonnull; @@ -104,13 +109,26 @@ protected StubSettings(Builder builder) { this.quotaProjectId = builder.quotaProjectId; this.streamWatchdogProvider = builder.streamWatchdogProvider; this.streamWatchdogCheckInterval = builder.streamWatchdogCheckInterval; - this.tracerFactory = builder.tracerFactory; + this.tracerFactory = autoConfigureTracerFactory(builder.tracerFactory); this.deprecatedExecutorProviderSet = builder.deprecatedExecutorProviderSet; this.gdchApiAudience = builder.gdchApiAudience; this.endpointContext = buildEndpointContext(builder); this.apiKey = builder.apiKey; } + private ApiTracerFactory autoConfigureTracerFactory(ApiTracerFactory factory) { + if (TracingUtils.isTracingEnabled()) { + ApiTracerFactory tracingFactory = + new TracingTracerFactory( + new OpenTelemetryTracingRecorder(GlobalOpenTelemetry.get(), getServiceName())); + if (factory instanceof BaseApiTracerFactory) { + return tracingFactory; + } + return CompositeApiTracerFactory.of(factory, tracingFactory); + } + return factory; + } + /** * Attempt to build the EndpointContext from the Builder based on all the user configurations * passed in. diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeApiTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeApiTracer.java new file mode 100644 index 0000000000..69e850d522 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeApiTracer.java @@ -0,0 +1,132 @@ +/* + * 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.List; +import java.util.stream.Collectors; + +/** A composite implementation of {@link ApiTracer} that broadcasts events to multiple tracers. */ +@InternalApi +public class CompositeApiTracer implements ApiTracer { + private final List tracers; + + public CompositeApiTracer(List tracers) { + this.tracers = ImmutableList.copyOf(tracers); + } + + @Override + public Scope inScope() { + List scopes = tracers.stream().map(ApiTracer::inScope).collect(Collectors.toList()); + return () -> scopes.forEach(Scope::close); + } + + @Override + public void operationSucceeded() { + tracers.forEach(ApiTracer::operationSucceeded); + } + + @Override + public void operationCancelled() { + tracers.forEach(ApiTracer::operationCancelled); + } + + @Override + public void operationFailed(Throwable error) { + tracers.forEach(t -> t.operationFailed(error)); + } + + @Override + public void connectionSelected(String id) { + tracers.forEach(t -> t.connectionSelected(id)); + } + + @Override + public void attemptStarted(int attemptNumber) { + tracers.forEach(t -> t.attemptStarted(attemptNumber)); + } + + @Override + public void attemptStarted(Object request, int attemptNumber) { + tracers.forEach(t -> t.attemptStarted(request, attemptNumber)); + } + + @Override + public void attemptSucceeded() { + tracers.forEach(ApiTracer::attemptSucceeded); + } + + @Override + public void attemptCancelled() { + tracers.forEach(ApiTracer::attemptCancelled); + } + + @Override + public void attemptFailedDuration(Throwable error, java.time.Duration delay) { + tracers.forEach(t -> t.attemptFailedDuration(error, delay)); + } + + @Override + public void attemptFailedRetriesExhausted(Throwable error) { + tracers.forEach(t -> t.attemptFailedRetriesExhausted(error)); + } + + @Override + public void attemptPermanentFailure(Throwable error) { + tracers.forEach(t -> t.attemptPermanentFailure(error)); + } + + @Override + public void lroStartFailed(Throwable error) { + tracers.forEach(t -> t.lroStartFailed(error)); + } + + @Override + public void lroStartSucceeded() { + tracers.forEach(ApiTracer::lroStartSucceeded); + } + + @Override + public void responseReceived() { + tracers.forEach(ApiTracer::responseReceived); + } + + @Override + public void requestSent() { + tracers.forEach(ApiTracer::requestSent); + } + + @Override + public void batchRequestSent(long elementCount, long requestSize) { + tracers.forEach(t -> t.batchRequestSent(elementCount, requestSize)); + } +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeApiTracerFactory.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeApiTracerFactory.java new file mode 100644 index 0000000000..450f6778fd --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeApiTracerFactory.java @@ -0,0 +1,61 @@ +/* + * 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.List; +import java.util.stream.Collectors; + +/** + * A composite implementation of {@link ApiTracerFactory} that creates {@link CompositeApiTracer}s. + */ +@InternalApi +public class CompositeApiTracerFactory implements ApiTracerFactory { + private final List factories; + + public CompositeApiTracerFactory(List factories) { + this.factories = ImmutableList.copyOf(factories); + } + + public static ApiTracerFactory of(ApiTracerFactory... factories) { + return new CompositeApiTracerFactory(ImmutableList.copyOf(factories)); + } + + @Override + public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType) { + List tracers = + factories.stream() + .map(f -> f.newTracer(parent, spanName, operationType)) + .collect(Collectors.toList()); + return new CompositeApiTracer(tracers); + } +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTracingRecorder.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTracingRecorder.java new file mode 100644 index 0000000000..c97097d6c5 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTracingRecorder.java @@ -0,0 +1,75 @@ +/* + * 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.BetaApi; +import com.google.api.core.InternalApi; +import com.google.api.gax.core.GaxProperties; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import java.util.Map; + +/** + * OpenTelemetry implementation of recording traces. This implementation collects the measurements + * related to the lifecyle of an RPC. + */ +@BetaApi +@InternalApi +public class OpenTelemetryTracingRecorder implements TracingRecorder { + public static final String GAX_TRACER_NAME = "gax-java"; + private final Tracer tracer; + private final String serviceName; + + /** + * Creates a new instance of OpenTelemetryTracingRecorder. + * + * @param openTelemetry OpenTelemetry instance + * @param serviceName Service Name + */ + public OpenTelemetryTracingRecorder(OpenTelemetry openTelemetry, String serviceName) { + this.tracer = + openTelemetry + .tracerBuilder(GAX_TRACER_NAME) + .setInstrumentationVersion(GaxProperties.getGaxVersion()) + .build(); + this.serviceName = serviceName; + } + + @Override + public void recordLowLevelNetworkSpan(Map attributes) { + Span span = tracer.spanBuilder(serviceName + "/low-level-network-span").startSpan(); + if (attributes != null) { + attributes.forEach(span::setAttribute); + } + span.end(); + } +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracingRecorder.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracingRecorder.java new file mode 100644 index 0000000000..2aa932bb0e --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracingRecorder.java @@ -0,0 +1,54 @@ +/* + * 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.BetaApi; +import com.google.api.core.InternalApi; +import java.util.Map; + +/** + * Provides an interface for tracing recording. The implementer is expected to use an observability + * framework, e.g. OpenTelemetry. There should be only one instance of TracingRecorder per client, + * all the methods in this class are expected to be called from multiple threads, hence the + * implementation must be thread safe. + */ +@BetaApi +@InternalApi +public interface TracingRecorder { + + /** + * Represents a low-level network span (T4) and serves as a placeholder for further development. + * This represents a single network request/attempt. + * + * @param attributes Map of the attributes to store on the span. + */ + default void recordLowLevelNetworkSpan(Map attributes) {} +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracingTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracingTracer.java new file mode 100644 index 0000000000..001d8fccc7 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracingTracer.java @@ -0,0 +1,67 @@ +/* + * 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.BetaApi; +import com.google.api.core.InternalApi; +import java.util.HashMap; +import java.util.Map; + +/** + * This class computes generic traces that can be observed in the lifecycle of an RPC operation. The + * responsibility of recording traces should delegate to {@link TracingRecorder}, hence this class + * should not have any knowledge about the observability framework used for tracing recording. + */ +@BetaApi +@InternalApi +public class TracingTracer extends BaseApiTracer { + private final TracingRecorder tracingRecorder; + private final Map attributes = new HashMap<>(); + + public TracingTracer(TracingRecorder tracingRecorder) { + this.tracingRecorder = tracingRecorder; + } + + @Override + public void attemptStarted(Object request, int attemptNumber) { + tracingRecorder.recordLowLevelNetworkSpan(attributes); + } + + /** Add attributes that will be attached to all spans. */ + public void addAttributes(String key, String value) { + attributes.put(key, value); + } + + /** Add attributes that will be attached to all spans. */ + public void addAttributes(Map attributes) { + this.attributes.putAll(attributes); + } +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracingTracerFactory.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracingTracerFactory.java new file mode 100644 index 0000000000..8c65252282 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracingTracerFactory.java @@ -0,0 +1,77 @@ +/* + * 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.BetaApi; +import com.google.api.core.InternalApi; +import com.google.common.collect.ImmutableMap; +import java.util.Map; + +/** + * A {@link ApiTracerFactory} to build instances of {@link TracingTracer}. + * + *

This class wraps the {@link TracingRecorder} and pass it to {@link TracingTracer}. It will be + * used to record traces in {@link TracingTracer}. + * + *

This class is expected to be initialized once during client initialization. + */ +@BetaApi +@InternalApi +public class TracingTracerFactory implements ApiTracerFactory { + private final TracingRecorder tracingRecorder; + + /** Mapping of client attributes that are set for every TracingTracer */ + private final Map attributes; + + /** Creates a TracingTracerFactory with no additional client level attributes. */ + public TracingTracerFactory(TracingRecorder tracingRecorder) { + this(tracingRecorder, ImmutableMap.of()); + } + + /** + * Pass in a Map of client level attributes which will be added to every single TracingTracer + * created from the ApiTracerFactory. + */ + public TracingTracerFactory(TracingRecorder tracingRecorder, Map attributes) { + this.tracingRecorder = tracingRecorder; + this.attributes = ImmutableMap.copyOf(attributes); + } + + @Override + public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType) { + if (!TracingUtils.isTracingEnabled()) { + return BaseApiTracer.getInstance(); + } + TracingTracer tracingTracer = new TracingTracer(tracingRecorder); + attributes.forEach(tracingTracer::addAttributes); + return tracingTracer; + } +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracingUtils.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracingUtils.java new file mode 100644 index 0000000000..161f93f558 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracingUtils.java @@ -0,0 +1,59 @@ +/* + * 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.annotations.VisibleForTesting; + +/** Internal utility class to manage tracing configuration. */ +@InternalApi +public class TracingUtils { + static final String GOOGLE_CLOUD_ENABLE_TRACING = "GOOGLE_CLOUD_ENABLE_TRACING"; + + private TracingUtils() {} + + /** + * Returns true if tracing is enabled via the GOOGLE_CLOUD_ENABLE_TRACING environment variable or + * system property. + */ + public static boolean isTracingEnabled() { + String enableTracing = System.getProperty(GOOGLE_CLOUD_ENABLE_TRACING); + if (enableTracing == null) { + enableTracing = System.getenv(GOOGLE_CLOUD_ENABLE_TRACING); + } + return isTracingEnabled(enableTracing); + } + + @VisibleForTesting + static boolean isTracingEnabled(String envValue) { + return "true".equalsIgnoreCase(envValue); + } +} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/TracingTracerFactoryTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/TracingTracerFactoryTest.java new file mode 100644 index 0000000000..9169b14abd --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/TracingTracerFactoryTest.java @@ -0,0 +1,79 @@ +/* + * 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 com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; + +import com.google.api.gax.tracing.ApiTracerFactory.OperationType; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class TracingTracerFactoryTest { + private TracingRecorder tracingRecorder; + private TracingTracerFactory factory; + private ApiTracer parent; + private SpanName spanName; + + @BeforeEach + void setUp() { + tracingRecorder = mock(TracingRecorder.class); + factory = new TracingTracerFactory(tracingRecorder); + parent = mock(ApiTracer.class); + spanName = mock(SpanName.class); + } + + @AfterEach + void tearDown() { + System.clearProperty("GOOGLE_CLOUD_ENABLE_TRACING"); + } + + @Test + void testNewTracer_enabled() { + System.setProperty("GOOGLE_CLOUD_ENABLE_TRACING", "true"); + ApiTracer tracer = factory.newTracer(parent, spanName, OperationType.Unary); + assertThat(tracer).isInstanceOf(TracingTracer.class); + } + + @Test + void testNewTracer_disabled() { + System.setProperty("GOOGLE_CLOUD_ENABLE_TRACING", "false"); + ApiTracer tracer = factory.newTracer(parent, spanName, OperationType.Unary); + assertThat(tracer).isSameInstanceAs(BaseApiTracer.getInstance()); + } + + @Test + void testNewTracer_defaultDisabled() { + ApiTracer tracer = factory.newTracer(parent, spanName, OperationType.Unary); + assertThat(tracer).isSameInstanceAs(BaseApiTracer.getInstance()); + } +} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/TracingTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/TracingTracerTest.java new file mode 100644 index 0000000000..f8506d9f06 --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/TracingTracerTest.java @@ -0,0 +1,46 @@ +/* + * 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 com.google.common.truth.Truth.assertThat; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class TracingTracerTest { + + @Test + void testConstructor() { + TracingRecorder recorder = Mockito.mock(TracingRecorder.class); + TracingTracer tracer = new TracingTracer(recorder); + assertThat(tracer).isNotNull(); + } +} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/TracingUtilsTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/TracingUtilsTest.java new file mode 100644 index 0000000000..9449fc571f --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/TracingUtilsTest.java @@ -0,0 +1,56 @@ +/* + * 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 com.google.common.truth.Truth.assertThat; + +import org.junit.jupiter.api.Test; + +class TracingUtilsTest { + + @Test + void testIsTracingEnabled_defaultToFalse() { + assertThat(TracingUtils.isTracingEnabled(null)).isFalse(); + } + + @Test + void testIsTracingEnabled_true() { + assertThat(TracingUtils.isTracingEnabled("true")).isTrue(); + assertThat(TracingUtils.isTracingEnabled("TRUE")).isTrue(); + } + + @Test + void testIsTracingEnabled_false() { + assertThat(TracingUtils.isTracingEnabled("false")).isFalse(); + assertThat(TracingUtils.isTracingEnabled("random")).isFalse(); + assertThat(TracingUtils.isTracingEnabled("")).isFalse(); + } +} diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java new file mode 100644 index 0000000000..14df6dcef6 --- /dev/null +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java @@ -0,0 +1,164 @@ +/* + * 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.ApiTracer; +import com.google.api.gax.tracing.ApiTracerFactory; +import com.google.api.gax.tracing.BaseApiTracer; +import com.google.api.gax.tracing.MetricsTracerFactory; +import com.google.api.gax.tracing.OpenTelemetryMetricsRecorder; +import com.google.api.gax.tracing.SpanName; +import com.google.api.gax.tracing.TracingTracer; +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.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +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 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 ITOtelTracing { + private static final String SERVICE_NAME = "ShowcaseTracingTest"; + private InMemorySpanExporter spanExporter; + private InMemoryMetricReader metricReader; + private OpenTelemetryMetricsRecorder metricsRecorder; + private OpenTelemetrySdk openTelemetrySdk; + + @BeforeEach + void setup() { + spanExporter = InMemorySpanExporter.create(); + metricReader = InMemoryMetricReader.create(); + + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build(); + + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + + openTelemetrySdk = + OpenTelemetrySdk.builder() + .setTracerProvider(tracerProvider) + .setMeterProvider(meterProvider) + .buildAndRegisterGlobal(); + + metricsRecorder = new OpenTelemetryMetricsRecorder(openTelemetrySdk, SERVICE_NAME); + } + + @AfterEach + void tearDown() { + System.clearProperty("GOOGLE_CLOUD_ENABLE_TRACING"); + if (openTelemetrySdk != null) { + openTelemetrySdk.close(); + } + GlobalOpenTelemetry.resetForTest(); + } + + @Test + void testTracingFeatureFlag() throws Exception { + // Test tracing disabled + System.setProperty("GOOGLE_CLOUD_ENABLE_TRACING", "false"); + try (EchoClient client = TestClientInitializer.createGrpcEchoClient()) { + ApiTracer tracer = + client + .getSettings() + .getStubSettings() + .getTracerFactory() + .newTracer( + BaseApiTracer.getInstance(), + SpanName.of("EchoClient", "Echo"), + ApiTracerFactory.OperationType.Unary); + assertThat(tracer).isNotInstanceOf(TracingTracer.class); + } + + // Test tracing enabled + System.setProperty("GOOGLE_CLOUD_ENABLE_TRACING", "true"); + + try (EchoClient client = TestClientInitializer.createGrpcEchoClient()) { + ApiTracer tracer = + client + .getSettings() + .getStubSettings() + .getTracerFactory() + .newTracer( + BaseApiTracer.getInstance(), + SpanName.of("EchoClient", "Echo"), + ApiTracerFactory.OperationType.Unary); + assertThat(tracer).isInstanceOf(TracingTracer.class); + } + } + + @Test + void testTracingAndMetrics_recordedSimultaneously() throws Exception { + System.setProperty("GOOGLE_CLOUD_ENABLE_TRACING", "true"); + + MetricsTracerFactory metricsFactory = new MetricsTracerFactory(metricsRecorder); + + try (EchoClient client = TestClientInitializer.createGrpcEchoClient()) { + client.echo(EchoRequest.newBuilder().setContent("tracing-test").build()); + + List spans = spanExporter.getFinishedSpanItems(); + assertThat(spans).isNotEmpty(); + boolean foundLowLevelSpan = + spans.stream() + // .anyMatch(span -> span.getName().equals(SERVICE_NAME + "/low-level-network-span")); + .anyMatch(span -> span.getName().equals("/low-level-network-span")); + assertThat(foundLowLevelSpan).isTrue(); + } + + spanExporter.reset(); + try (EchoClient client = + TestClientInitializer.createGrpcEchoClientOpentelemetry(metricsFactory)) { + client.echo(EchoRequest.newBuilder().setContent("metrics-test").build()); + + Collection metrics = metricReader.collectAllMetrics(); + List spans = spanExporter.getFinishedSpanItems(); + assertThat(metrics).isNotEmpty(); + assertThat(spans).isNotEmpty(); + boolean foundAttemptCount = + metrics.stream().anyMatch(m -> m.getName().equals(SERVICE_NAME + "/attempt_count")); + assertThat(foundAttemptCount).isTrue(); + } + } +}