diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracer.java index 16553dd118..5986c54abd 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracer.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 Google LLC + * Copyright 2026 Google LLC * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are @@ -44,6 +44,7 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Logger; import javax.annotation.Nullable; /** @@ -55,6 +56,9 @@ @BetaApi @InternalApi public class MetricsTracer implements ApiTracer { + private static final Logger LOGGER = Logger.getLogger(MetricsTracer.class.getName()); + private static final AtomicBoolean WARNING_LOGGED = new AtomicBoolean(); + public static final String METHOD_ATTRIBUTE = "method"; public static final String LANGUAGE_ATTRIBUTE = "language"; public static final String STATUS_ATTRIBUTE = "status"; @@ -69,6 +73,14 @@ public class MetricsTracer implements ApiTracer { private final AtomicBoolean operationFinished; public MetricsTracer(MethodName methodName, MetricsRecorder metricsRecorder) { + if (!MetricsUtils.isMetricsEnabled()) { + if (WARNING_LOGGED.compareAndSet(false, true)) { + LOGGER.info( + "Metrics are currently disabled. To enable metrics, set the environment variable " + + "GOOGLE_CLOUD_ENABLE_METRICS=true or the system property " + + "GOOGLE_CLOUD_ENABLE_METRICS=true."); + } + } this.attributes.put(METHOD_ATTRIBUTE, methodName.toString()); this.attributes.put(LANGUAGE_ATTRIBUTE, DEFAULT_LANGUAGE); this.metricsRecorder = metricsRecorder; diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracerFactory.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracerFactory.java index 3aa17bfb6c..86d8b6319d 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracerFactory.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 Google LLC + * Copyright 2026 Google LLC * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are @@ -66,6 +66,9 @@ public MetricsTracerFactory(MetricsRecorder metricsRecorder, Map @Override public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType) { + if (!MetricsUtils.isMetricsEnabled()) { + return BaseApiTracer.getInstance(); + } MetricsTracer metricsTracer = new MetricsTracer( MethodName.of(spanName.getClientName(), spanName.getMethodName()), metricsRecorder); diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsUtils.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsUtils.java new file mode 100644 index 0000000000..9eb1abb43c --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsUtils.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 metrics configuration. */ +@InternalApi +public class MetricsUtils { + static final String GOOGLE_CLOUD_ENABLE_METRICS = "GOOGLE_CLOUD_ENABLE_METRICS"; + + private MetricsUtils() {} + + /** + * Returns true if metrics are enabled via the GOOGLE_CLOUD_ENABLE_METRICS environment variable or + * system property. + */ + public static boolean isMetricsEnabled() { + String enableMetrics = System.getProperty(GOOGLE_CLOUD_ENABLE_METRICS); + if (enableMetrics == null) { + enableMetrics = System.getenv(GOOGLE_CLOUD_ENABLE_METRICS); + } + return isMetricsEnabled(enableMetrics); + } + + @VisibleForTesting + static boolean isMetricsEnabled(String envValue) { + return "true".equalsIgnoreCase(envValue); + } +} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/MetricsTracerFactoryTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/MetricsTracerFactoryTest.java index 28a621f5f8..fc17805769 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/MetricsTracerFactoryTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/MetricsTracerFactoryTest.java @@ -36,10 +36,16 @@ import com.google.common.collect.ImmutableMap; import com.google.common.truth.Truth; import java.util.Map; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +@ExtendWith(MockitoExtension.class) class MetricsTracerFactoryTest { private static final int DEFAULT_ATTRIBUTES_COUNT = 2; @@ -50,6 +56,9 @@ class MetricsTracerFactoryTest { @BeforeEach void setUp() { + // Enable metrics for tests by default, as most tests expect MetricsTracer + System.setProperty("GOOGLE_CLOUD_ENABLE_METRICS", "true"); + // Create an instance of MetricsTracerFactory with the mocked MetricsRecorder metricsTracerFactory = new MetricsTracerFactory(metricsRecorder); @@ -58,6 +67,11 @@ void setUp() { when(spanName.getMethodName()).thenReturn("testMethod"); } + @AfterEach + void tearDown() { + System.clearProperty("GOOGLE_CLOUD_ENABLE_METRICS"); + } + @Test void testNewTracer_notNull() { // Call the newTracer method @@ -68,6 +82,19 @@ void testNewTracer_notNull() { Truth.assertThat(apiTracer).isInstanceOf(MetricsTracer.class); } + /** + * The LENIENT strictness flag avoids unnecessary mocks (from setUp() method) from being called + * out in this test. This test renders them unecessary because metrics are disabled here. + */ + @Test + @MockitoSettings(strictness = Strictness.LENIENT) + void testNewTracer_disabledMetrics_returnsBaseApiTracer() { + System.setProperty("GOOGLE_CLOUD_ENABLE_METRICS", "false"); + ApiTracer apiTracer = metricsTracerFactory.newTracer(parent, spanName, OperationType.Unary); + + Truth.assertThat(apiTracer).isSameInstanceAs(BaseApiTracer.getInstance()); + } + @Test void testNewTracer_hasCorrectNumberAttributes_hasDefaultAttributes() { MetricsTracer metricsTracer = diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelMetrics.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelMetrics.java index 06f46deee8..54b87584fb 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelMetrics.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelMetrics.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 Google LLC + * Copyright 2026 Google LLC * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are @@ -30,6 +30,7 @@ package com.google.showcase.v1beta1.it; +import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import com.google.api.client.http.javanet.NetHttpTransport; @@ -42,9 +43,13 @@ import com.google.api.gax.rpc.StatusCode.Code; import com.google.api.gax.rpc.UnaryCallable; import com.google.api.gax.rpc.UnavailableException; +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.MetricsTracer; import com.google.api.gax.tracing.MetricsTracerFactory; import com.google.api.gax.tracing.OpenTelemetryMetricsRecorder; +import com.google.api.gax.tracing.SpanName; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -154,6 +159,9 @@ private OpenTelemetryMetricsRecorder createOtelMetricsRecorder( @BeforeEach void setup() throws Exception { + // Enable metrics for these tests + System.setProperty("GOOGLE_CLOUD_ENABLE_METRICS", "true"); + inMemoryMetricReader = InMemoryMetricReader.create(); OpenTelemetryMetricsRecorder otelMetricsRecorder = createOtelMetricsRecorder(inMemoryMetricReader); @@ -167,6 +175,8 @@ void setup() throws Exception { @AfterEach void cleanup() throws InterruptedException, IOException { + System.clearProperty("GOOGLE_CLOUD_ENABLE_METRICS"); + inMemoryMetricReader.close(); inMemoryMetricReader.shutdown(); @@ -919,4 +929,32 @@ void grpcOpenTelemetryImplementation_setSampledToLocalTracing_methodFullNameIsRe echoClient.close(); echoClient.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); } + + @Test + void testMetricsFeatureFlag() throws Exception { + // Test metrics disabled + System.setProperty("GOOGLE_CLOUD_ENABLE_METRICS", "false"); + + try (InMemoryMetricReader reader = InMemoryMetricReader.create()) { + + OpenTelemetryMetricsRecorder recorder = createOtelMetricsRecorder(reader); + MetricsTracerFactory factory = new MetricsTracerFactory(recorder); + ApiTracer tracer = + factory.newTracer( + BaseApiTracer.getInstance(), + SpanName.of("EchoClient", "Echo"), + ApiTracerFactory.OperationType.Unary); + assertThat(tracer).isNotInstanceOf(MetricsTracer.class); + assertThat(tracer).isSameInstanceAs(BaseApiTracer.getInstance()); + + // Test metrics enabled + System.setProperty("GOOGLE_CLOUD_ENABLE_METRICS", "true"); + tracer = + factory.newTracer( + BaseApiTracer.getInstance(), + SpanName.of("EchoClient", "Echo"), + ApiTracerFactory.OperationType.Unary); + assertThat(tracer).isInstanceOf(MetricsTracer.class); + } + } }