diff --git a/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java b/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java index 5969ed4693..af5ef2648d 100644 --- a/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java +++ b/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java @@ -32,7 +32,6 @@ import com.google.api.core.ApiFunction; import com.google.api.core.BetaApi; import com.google.api.core.InternalApi; -import com.google.api.core.InternalExtensionOnly; import com.google.api.gax.core.ExecutorProvider; import com.google.api.gax.rpc.FixedHeaderProvider; import com.google.api.gax.rpc.HeaderProvider; @@ -82,13 +81,26 @@ *

The client lib header and generator header values are used to form a value that goes into the * http header of requests to the service. */ -@InternalExtensionOnly public final class InstantiatingGrpcChannelProvider implements TransportChannelProvider { + + static String systemProductName; + + static { + try { + systemProductName = + Files.asCharSource(new File("/sys/class/dmi/id/product_name"), StandardCharsets.UTF_8) + .readFirstLine(); + } catch (IOException e) { + // If not on Compute Engine, FileNotFoundException will be thrown. Use empty string + // as it won't match with the GCE_PRODUCTION_NAME constants + systemProductName = ""; + } + } + @VisibleForTesting static final Logger LOG = Logger.getLogger(InstantiatingGrpcChannelProvider.class.getName()); - private static final String DIRECT_PATH_ENV_DISABLE_DIRECT_PATH = - "GOOGLE_CLOUD_DISABLE_DIRECT_PATH"; + static final String DIRECT_PATH_ENV_DISABLE_DIRECT_PATH = "GOOGLE_CLOUD_DISABLE_DIRECT_PATH"; private static final String DIRECT_PATH_ENV_ENABLE_XDS = "GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS"; static final long DIRECT_PATH_KEEP_ALIVE_TIME_SECONDS = 3600; static final long DIRECT_PATH_KEEP_ALIVE_TIMEOUT_SECONDS = 20; @@ -147,6 +159,19 @@ private InstantiatingGrpcChannelProvider(Builder builder) { : builder.directPathServiceConfig; } + /** + * Package-Private constructor that is only visible for testing DirectPath functionality inside + * tests. This overrides the computed systemProductName when the class is initialized to help + * configure the result of {@link #isOnComputeEngine()} check. + * + *

If productName is null, that represents the result of an IOException + */ + @VisibleForTesting + InstantiatingGrpcChannelProvider(Builder builder, String productName) { + this(builder); + systemProductName = productName; + } + /** * @deprecated If executor is not set, this channel provider will create channels with default * grpc executor. @@ -257,8 +282,8 @@ private boolean isDirectPathEnabled() { return false; } - @VisibleForTesting - boolean isDirectPathXdsEnabled() { + @InternalApi + public boolean isDirectPathXdsEnabled() { // Method 1: Enable DirectPath xDS by option. if (Boolean.TRUE.equals(attemptDirectPathXds)) { return true; @@ -320,15 +345,9 @@ boolean isCredentialDirectPathCompatible() { static boolean isOnComputeEngine() { String osName = System.getProperty("os.name"); if ("Linux".equals(osName)) { - try { - String result = - Files.asCharSource(new File("/sys/class/dmi/id/product_name"), StandardCharsets.UTF_8) - .readFirstLine(); - return result.contains(GCE_PRODUCTION_NAME_PRIOR_2016) - || result.contains(GCE_PRODUCTION_NAME_AFTER_2016); - } catch (IOException ignored) { - return false; - } + // systemProductName will be empty string if not on Compute Engine + return systemProductName.contains(GCE_PRODUCTION_NAME_PRIOR_2016) + || systemProductName.contains(GCE_PRODUCTION_NAME_AFTER_2016); } return false; } @@ -370,10 +389,7 @@ private ManagedChannel createSingleChannel() throws IOException { // Check DirectPath traffic. boolean useDirectPathXds = false; - if (isDirectPathEnabled() - && isCredentialDirectPathCompatible() - && isOnComputeEngine() - && canUseDirectPathWithUniverseDomain()) { + if (canUseDirectPath()) { CallCredentials callCreds = MoreCallCredentials.from(credentials); ChannelCredentials channelCreds = GoogleDefaultChannelCredentials.newBuilder().callCredentials(callCreds).build(); @@ -446,6 +462,24 @@ && canUseDirectPathWithUniverseDomain()) { return managedChannel; } + /** + * Marked as Internal Api and intended for internal use. DirectPath must be enabled via the + * settings and a few other configurations/settings must also be valid for the request to go + * through DirectPath. + * + *

Checks: 1. Credentials are compatible 2.Running on Compute Engine 3. Universe Domain is + * configured to for the Google Default Universe + * + * @return if DirectPath is enabled for the client AND if the configurations are valid + */ + @InternalApi + public boolean canUseDirectPath() { + return isDirectPathEnabled() + && isCredentialDirectPathCompatible() + && isOnComputeEngine() + && canUseDirectPathWithUniverseDomain(); + } + /** The endpoint to be used for the channel. */ @Override public String getEndpoint() { @@ -753,6 +787,12 @@ public Builder setAttemptDirectPathXds() { return this; } + @VisibleForTesting + Builder setEnvProvider(EnvironmentProvider envProvider) { + this.envProvider = envProvider; + return this; + } + /** * Sets a service config for direct path. If direct path is not enabled, the provided service * config will be ignored. diff --git a/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProviderTest.java b/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProviderTest.java index 2647ac6d13..d982ec98c3 100644 --- a/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProviderTest.java +++ b/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProviderTest.java @@ -29,6 +29,7 @@ */ package com.google.api.gax.grpc; +import static com.google.api.gax.grpc.InstantiatingGrpcChannelProvider.GCE_PRODUCTION_NAME_AFTER_2016; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -37,13 +38,17 @@ import com.google.api.core.ApiFunction; import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider.Builder; import com.google.api.gax.rpc.HeaderProvider; +import com.google.api.gax.rpc.TransportChannel; import com.google.api.gax.rpc.TransportChannelProvider; +import com.google.api.gax.rpc.internal.EnvironmentProvider; import com.google.api.gax.rpc.mtls.AbstractMtlsTransportChannelTest; import com.google.api.gax.rpc.mtls.MtlsProvider; +import com.google.auth.Credentials; import com.google.auth.oauth2.CloudShellCredentials; import com.google.auth.oauth2.ComputeEngineCredentials; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.truth.Truth; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.alts.ComputeEngineChannelBuilder; @@ -57,16 +62,39 @@ import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import java.util.logging.Handler; import java.util.logging.LogRecord; import java.util.stream.Collectors; import javax.annotation.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.threeten.bp.Duration; class InstantiatingGrpcChannelProviderTest extends AbstractMtlsTransportChannelTest { + private static final String DEFAULT_ENDPOINT = "test.googleapis.com:443"; + private static String originalOSName; + private ComputeEngineCredentials computeEngineCredentials; + + @BeforeAll + public static void setupClass() { + originalOSName = System.getProperty("os.name"); + } + + @BeforeEach + public void setup() throws IOException { + System.setProperty("os.name", "Linux"); + computeEngineCredentials = Mockito.mock(ComputeEngineCredentials.class); + } + + @AfterEach + public void cleanup() { + System.setProperty("os.name", originalOSName); + } @Test void testEndpoint() { @@ -300,7 +328,7 @@ void testDirectPathWithGDUEndpoint() { InstantiatingGrpcChannelProvider.newBuilder() .setAttemptDirectPath(true) .setAttemptDirectPathXds() - .setEndpoint("test.googleapis.com:443") + .setEndpoint(DEFAULT_ENDPOINT) .build(); assertThat(provider.canUseDirectPathWithUniverseDomain()).isTrue(); } @@ -322,7 +350,7 @@ void testDirectPathXdsEnabled() throws IOException { InstantiatingGrpcChannelProvider.newBuilder() .setAttemptDirectPath(true) .setAttemptDirectPathXds() - .setEndpoint("test.googleapis.com:443") + .setEndpoint(DEFAULT_ENDPOINT) .build(); assertThat(provider.isDirectPathXdsEnabled()).isTrue(); @@ -552,13 +580,16 @@ void testLogDirectPathMisconfigAttrempDirectPathNotSet() throws Exception { .setEndpoint("localhost:8080") .build(); - provider.getTransportChannel(); + TransportChannel transportChannel = provider.getTransportChannel(); assertThat(logHandler.getAllMessages()) .contains( "DirectPath is misconfigured. Please set the attemptDirectPath option along with the" + " attemptDirectPathXds option."); InstantiatingGrpcChannelProvider.LOG.removeHandler(logHandler); + + transportChannel.close(); + transportChannel.awaitTermination(10, TimeUnit.SECONDS); } @Test @@ -584,16 +615,19 @@ void testLogDirectPathMisconfigWrongCredential() throws Exception { .setAttemptDirectPath(true) .setHeaderProvider(Mockito.mock(HeaderProvider.class)) .setExecutor(Mockito.mock(Executor.class)) - .setEndpoint("test.googleapis.com:443") + .setEndpoint(DEFAULT_ENDPOINT) .build(); - provider.getTransportChannel(); + TransportChannel transportChannel = provider.getTransportChannel(); assertThat(logHandler.getAllMessages()) .contains( "DirectPath is misconfigured. Please make sure the credential is an instance of" + " com.google.auth.oauth2.ComputeEngineCredentials ."); InstantiatingGrpcChannelProvider.LOG.removeHandler(logHandler); + + transportChannel.close(); + transportChannel.awaitTermination(10, TimeUnit.SECONDS); } @Test @@ -607,10 +641,10 @@ void testLogDirectPathMisconfigNotOnGCE() throws Exception { .setAllowNonDefaultServiceAccount(true) .setHeaderProvider(Mockito.mock(HeaderProvider.class)) .setExecutor(Mockito.mock(Executor.class)) - .setEndpoint("test.googleapis.com:443") + .setEndpoint(DEFAULT_ENDPOINT) .build(); - provider.getTransportChannel(); + TransportChannel transportChannel = provider.getTransportChannel(); if (!InstantiatingGrpcChannelProvider.isOnComputeEngine()) { assertThat(logHandler.getAllMessages()) @@ -618,6 +652,161 @@ void testLogDirectPathMisconfigNotOnGCE() throws Exception { "DirectPath is misconfigured. DirectPath is only available in a GCE environment."); } InstantiatingGrpcChannelProvider.LOG.removeHandler(logHandler); + + transportChannel.close(); + transportChannel.awaitTermination(10, TimeUnit.SECONDS); + } + + @Test + public void canUseDirectPath_happyPath() { + EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class); + Mockito.when( + envProvider.getenv( + InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("false"); + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder() + .setAttemptDirectPath(true) + .setCredentials(computeEngineCredentials) + .setEndpoint(DEFAULT_ENDPOINT) + .setEnvProvider(envProvider); + InstantiatingGrpcChannelProvider provider = + new InstantiatingGrpcChannelProvider(builder, GCE_PRODUCTION_NAME_AFTER_2016); + Truth.assertThat(provider.canUseDirectPath()).isTrue(); + } + + @Test + public void canUseDirectPath_directPathEnvVarDisabled() { + EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class); + Mockito.when( + envProvider.getenv( + InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("true"); + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder() + .setAttemptDirectPath(true) + .setCredentials(computeEngineCredentials) + .setEndpoint(DEFAULT_ENDPOINT) + .setEnvProvider(envProvider); + InstantiatingGrpcChannelProvider provider = + new InstantiatingGrpcChannelProvider(builder, GCE_PRODUCTION_NAME_AFTER_2016); + Truth.assertThat(provider.canUseDirectPath()).isFalse(); + } + + @Test + public void canUseDirectPath_directPathEnvVarNotSet_attemptDirectPathIsTrue() { + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder() + .setAttemptDirectPath(true) + .setCredentials(computeEngineCredentials) + .setEndpoint(DEFAULT_ENDPOINT); + InstantiatingGrpcChannelProvider provider = + new InstantiatingGrpcChannelProvider(builder, GCE_PRODUCTION_NAME_AFTER_2016); + Truth.assertThat(provider.canUseDirectPath()).isTrue(); + } + + @Test + public void canUseDirectPath_directPathEnvVarNotSet_attemptDirectPathIsFalse() { + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder() + .setAttemptDirectPath(false) + .setCredentials(computeEngineCredentials) + .setEndpoint(DEFAULT_ENDPOINT); + InstantiatingGrpcChannelProvider provider = + new InstantiatingGrpcChannelProvider(builder, GCE_PRODUCTION_NAME_AFTER_2016); + Truth.assertThat(provider.canUseDirectPath()).isFalse(); + } + + @Test + public void canUseDirectPath_nonComputeCredentials() { + Credentials credentials = Mockito.mock(Credentials.class); + EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class); + Mockito.when( + envProvider.getenv( + InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("false"); + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder() + .setAttemptDirectPath(true) + .setCredentials(credentials) + .setEndpoint(DEFAULT_ENDPOINT) + .setEnvProvider(envProvider); + InstantiatingGrpcChannelProvider provider = + new InstantiatingGrpcChannelProvider(builder, GCE_PRODUCTION_NAME_AFTER_2016); + Truth.assertThat(provider.canUseDirectPath()).isFalse(); + } + + @Test + public void canUseDirectPath_isNotOnComputeEngine_invalidOsNameSystemProperty() { + System.setProperty("os.name", "Not Linux"); + EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class); + Mockito.when( + envProvider.getenv( + InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("false"); + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder() + .setAttemptDirectPath(true) + .setCredentials(computeEngineCredentials) + .setEndpoint(DEFAULT_ENDPOINT) + .setEnvProvider(envProvider); + InstantiatingGrpcChannelProvider provider = + new InstantiatingGrpcChannelProvider(builder, GCE_PRODUCTION_NAME_AFTER_2016); + Truth.assertThat(provider.canUseDirectPath()).isFalse(); + } + + @Test + public void canUseDirectPath_isNotOnComputeEngine_invalidSystemProductName() { + EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class); + Mockito.when( + envProvider.getenv( + InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("false"); + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder() + .setAttemptDirectPath(true) + .setCredentials(computeEngineCredentials) + .setEndpoint(DEFAULT_ENDPOINT) + .setEnvProvider(envProvider); + InstantiatingGrpcChannelProvider provider = + new InstantiatingGrpcChannelProvider(builder, "testing"); + Truth.assertThat(provider.canUseDirectPath()).isFalse(); + } + + @Test + public void canUseDirectPath_isNotOnComputeEngine_unableToGetSystemProductName() { + EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class); + Mockito.when( + envProvider.getenv( + InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("false"); + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder() + .setAttemptDirectPath(true) + .setCredentials(computeEngineCredentials) + .setEndpoint(DEFAULT_ENDPOINT) + .setEnvProvider(envProvider); + InstantiatingGrpcChannelProvider provider = new InstantiatingGrpcChannelProvider(builder, ""); + Truth.assertThat(provider.canUseDirectPath()).isFalse(); + } + + @Test + public void canUseDirectPath_nonGDUUniverseDomain() { + EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class); + Mockito.when( + envProvider.getenv( + InstantiatingGrpcChannelProvider.DIRECT_PATH_ENV_DISABLE_DIRECT_PATH)) + .thenReturn("false"); + String nonGDUEndpoint = "test.random.com:443"; + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder() + .setAttemptDirectPath(true) + .setCredentials(computeEngineCredentials) + .setEndpoint(nonGDUEndpoint) + .setEnvProvider(envProvider); + InstantiatingGrpcChannelProvider provider = + new InstantiatingGrpcChannelProvider(builder, GCE_PRODUCTION_NAME_AFTER_2016); + Truth.assertThat(provider.canUseDirectPath()).isFalse(); } private static class FakeLogHandler extends Handler { 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 7938bde82b..abbc9138dc 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 @@ -61,6 +61,7 @@ public class MetricsTracer implements ApiTracer { "Operation has already been completed"; private Stopwatch attemptTimer; private final Stopwatch operationTimer = Stopwatch.createStarted(); + // These are RPC specific attributes and pertain to a specific API Trace private final Map attributes = new HashMap<>(); private final MetricsRecorder metricsRecorder; private final AtomicBoolean operationFinished; 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 d2b8d87fb4..3aa17bfb6c 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 @@ -31,6 +31,8 @@ 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 MetricsTracer}. @@ -45,13 +47,29 @@ public class MetricsTracerFactory implements ApiTracerFactory { protected MetricsRecorder metricsRecorder; + /** Mapping of client attributes that are set for every MetricsTracer */ + private final Map attributes; + + /** Creates a MetricsTracerFactory with no additional client level attributes. */ public MetricsTracerFactory(MetricsRecorder metricsRecorder) { + this(metricsRecorder, ImmutableMap.of()); + } + + /** + * Pass in a Map of client level attributes which will be added to every single MetricsTracer + * created from the ApiTracerFactory. + */ + public MetricsTracerFactory(MetricsRecorder metricsRecorder, Map attributes) { this.metricsRecorder = metricsRecorder; + this.attributes = ImmutableMap.copyOf(attributes); } @Override public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType) { - return new MetricsTracer( - MethodName.of(spanName.getClientName(), spanName.getMethodName()), metricsRecorder); + MetricsTracer metricsTracer = + new MetricsTracer( + MethodName.of(spanName.getClientName(), spanName.getMethodName()), metricsRecorder); + attributes.forEach(metricsTracer::addAttributes); + return metricsTracer; } } 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 16e2078bc0..d5459921e5 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 @@ -33,12 +33,16 @@ import static org.mockito.Mockito.when; import com.google.api.gax.tracing.ApiTracerFactory.OperationType; +import com.google.common.collect.ImmutableMap; import com.google.common.truth.Truth; +import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; class MetricsTracerFactoryTest { + private static final int DEFAULT_ATTRIBUTES_COUNT = 2; + @Mock private MetricsRecorder metricsRecorder; @Mock private ApiTracer parent; private SpanName spanName; @@ -60,22 +64,36 @@ void testNewTracer_notNull() { ApiTracer apiTracer = metricsTracerFactory.newTracer(parent, spanName, OperationType.Unary); // Assert that the apiTracer created has expected type and not null - Truth.assertThat(apiTracer).isInstanceOf(MetricsTracer.class); Truth.assertThat(apiTracer).isNotNull(); + Truth.assertThat(apiTracer).isInstanceOf(MetricsTracer.class); } @Test - void testNewTracer_HasCorrectParameters() { - - // Call the newTracer method - ApiTracer apiTracer = metricsTracerFactory.newTracer(parent, spanName, OperationType.Unary); + void testNewTracer_hasCorrectNumberAttributes_hasDefaultAttributes() { + MetricsTracer metricsTracer = + (MetricsTracer) metricsTracerFactory.newTracer(parent, spanName, OperationType.Unary); + Map attributes = metricsTracer.getAttributes(); + Truth.assertThat(attributes.size()).isEqualTo(DEFAULT_ATTRIBUTES_COUNT); + Truth.assertThat(attributes.get(MetricsTracer.METHOD_NAME_ATTRIBUTE)) + .isEqualTo("testService.testMethod"); + Truth.assertThat(attributes.get(MetricsTracer.LANGUAGE_ATTRIBUTE)) + .isEqualTo(MetricsTracer.DEFAULT_LANGUAGE); + } - // Assert that the apiTracer created has expected type and not null - Truth.assertThat(apiTracer).isInstanceOf(MetricsTracer.class); - Truth.assertThat(apiTracer).isNotNull(); + @Test + void testClientAttributes_additionalClientAttributes() { + Map clientAttributes = + ImmutableMap.of("attribute1", "value1", "attribute2", "value2"); + MetricsTracerFactory metricsTracerFactory = + new MetricsTracerFactory(metricsRecorder, clientAttributes); - MetricsTracer metricsTracer = (MetricsTracer) apiTracer; - Truth.assertThat(metricsTracer.getAttributes().get("method_name")) - .isEqualTo("testService.testMethod"); + MetricsTracer metricsTracer = + (MetricsTracer) metricsTracerFactory.newTracer(parent, spanName, OperationType.Unary); + Map attributes = metricsTracer.getAttributes(); + Truth.assertThat(attributes.size()) + .isEqualTo(DEFAULT_ATTRIBUTES_COUNT + clientAttributes.size()); + // Default attributes already tested above + Truth.assertThat(attributes.containsKey("attribute1")).isTrue(); + Truth.assertThat(attributes.containsKey("attribute2")).isTrue(); } } diff --git a/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelMetrics.java b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelMetrics.java index c2ecff034d..a744a51432 100644 --- a/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelMetrics.java +++ b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelMetrics.java @@ -35,6 +35,7 @@ import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.core.ApiFuture; import com.google.api.gax.core.NoCredentialsProvider; +import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider; import com.google.api.gax.retrying.RetrySettings; import com.google.api.gax.rpc.InvalidArgumentException; import com.google.api.gax.rpc.StatusCode.Code; @@ -69,7 +70,9 @@ import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.metrics.data.PointData; import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -100,7 +103,7 @@ class ITOtelMetrics { private static final String OPERATION_COUNT = SERVICE_NAME + "/operation_count"; private static final String ATTEMPT_LATENCY = SERVICE_NAME + "/attempt_latency"; private static final String OPERATION_LATENCY = SERVICE_NAME + "/operation_latency"; - private static final int NUM_METRICS = 4; + private static final int NUM_DEFAULT_METRICS = 4; private static final int NUM_COLLECTION_FLUSH_ATTEMPTS = 10; private InMemoryMetricReader inMemoryMetricReader; private EchoClient grpcClient; @@ -272,16 +275,22 @@ private void verifyStatusAttribute( } } + /** Uses the default InMemoryMetricReader configured for showcase tests. */ + private List getMetricDataList() throws InterruptedException { + return getMetricDataList(inMemoryMetricReader); + } + /** - * Attempts to retrieve the metrics from the InMemoryMetricsReader. Sleep every second for at most - * 10s to try and retrieve all the metrics available. If it is unable to retrieve all the metrics, - * fail the test. + * Attempts to retrieve the metrics from a custom InMemoryMetricsReader. Sleep every second for at + * most 10s to try and retrieve all the metrics available. If it is unable to retrieve all the + * metrics, fail the test. */ - private List getMetricDataList() throws InterruptedException { + private List getMetricDataList(InMemoryMetricReader metricReader) + throws InterruptedException { for (int i = 0; i < NUM_COLLECTION_FLUSH_ATTEMPTS; i++) { Thread.sleep(1000L); - List metricData = new ArrayList<>(inMemoryMetricReader.collectAllMetrics()); - if (metricData.size() == NUM_METRICS) { + List metricData = new ArrayList<>(metricReader.collectAllMetrics()); + if (metricData.size() == NUM_DEFAULT_METRICS) { return metricData; } } @@ -296,19 +305,19 @@ void testGrpc_operationSucceeded_recordsMetrics() throws InterruptedException { EchoRequest.newBuilder().setContent("test_grpc_operation_succeeded").build(); grpcClient.echo(echoRequest); - List metricDataList = getMetricDataList(); - verifyPointDataSum(metricDataList, attemptCount); + List actualMetricDataList = getMetricDataList(); + verifyPointDataSum(actualMetricDataList, attemptCount); - Map attributeMapping = + Map expectedAttributes = ImmutableMap.of( MetricsTracer.METHOD_NAME_ATTRIBUTE, "Echo.Echo", MetricsTracer.LANGUAGE_ATTRIBUTE, MetricsTracer.DEFAULT_LANGUAGE); - verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + verifyDefaultMetricsAttributes(actualMetricDataList, expectedAttributes); List statusCountList = ImmutableList.of(new StatusCount(Code.OK)); - verifyStatusAttribute(metricDataList, statusCountList); + verifyStatusAttribute(actualMetricDataList, statusCountList); } @Disabled("https://github.com/googleapis/sdk-platform-java/issues/2503") @@ -319,19 +328,19 @@ void testHttpJson_operationSucceeded_recordsMetrics() throws InterruptedExceptio EchoRequest.newBuilder().setContent("test_http_operation_succeeded").build(); httpClient.echo(echoRequest); - List metricDataList = getMetricDataList(); - verifyPointDataSum(metricDataList, attemptCount); + List actualMetricDataList = getMetricDataList(); + verifyPointDataSum(actualMetricDataList, attemptCount); - Map attributeMapping = + Map expectedAttributes = ImmutableMap.of( MetricsTracer.METHOD_NAME_ATTRIBUTE, "google.showcase.v1beta1.Echo/Echo", MetricsTracer.LANGUAGE_ATTRIBUTE, MetricsTracer.DEFAULT_LANGUAGE); - verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + verifyDefaultMetricsAttributes(actualMetricDataList, expectedAttributes); List statusCountList = ImmutableList.of(new StatusCount(Code.OK)); - verifyStatusAttribute(metricDataList, statusCountList); + verifyStatusAttribute(actualMetricDataList, statusCountList); } @Test @@ -349,19 +358,19 @@ void testGrpc_operationCancelled_recordsMetrics() throws Exception { Thread.sleep(1000); blockResponseApiFuture.cancel(true); - List metricDataList = getMetricDataList(); - verifyPointDataSum(metricDataList, attemptCount); + List actualMetricDataList = getMetricDataList(); + verifyPointDataSum(actualMetricDataList, attemptCount); - Map attributeMapping = + Map expectedAttributes = ImmutableMap.of( MetricsTracer.METHOD_NAME_ATTRIBUTE, "Echo.Block", MetricsTracer.LANGUAGE_ATTRIBUTE, MetricsTracer.DEFAULT_LANGUAGE); - verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + verifyDefaultMetricsAttributes(actualMetricDataList, expectedAttributes); List statusCountList = ImmutableList.of(new StatusCount(Code.CANCELLED)); - verifyStatusAttribute(metricDataList, statusCountList); + verifyStatusAttribute(actualMetricDataList, statusCountList); } @Disabled("https://github.com/googleapis/sdk-platform-java/issues/2503") @@ -377,19 +386,19 @@ void testHttpJson_operationCancelled_recordsMetrics() throws Exception { Thread.sleep(1000); blockResponseApiFuture.cancel(true); - List metricDataList = getMetricDataList(); - verifyPointDataSum(metricDataList, attemptCount); + List actualMetricDataList = getMetricDataList(); + verifyPointDataSum(actualMetricDataList, attemptCount); - Map attributeMapping = + Map expectedAttributes = ImmutableMap.of( MetricsTracer.METHOD_NAME_ATTRIBUTE, "google.showcase.v1beta1.Echo/Block", MetricsTracer.LANGUAGE_ATTRIBUTE, MetricsTracer.DEFAULT_LANGUAGE); - verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + verifyDefaultMetricsAttributes(actualMetricDataList, expectedAttributes); List statusCountList = ImmutableList.of(new StatusCount(Code.CANCELLED)); - verifyStatusAttribute(metricDataList, statusCountList); + verifyStatusAttribute(actualMetricDataList, statusCountList); } @Test @@ -406,19 +415,19 @@ void testGrpc_operationFailed_recordsMetrics() throws InterruptedException { ApiFuture blockResponseApiFuture = blockCallable.futureCall(blockRequest); assertThrows(ExecutionException.class, blockResponseApiFuture::get); - List metricDataList = getMetricDataList(); - verifyPointDataSum(metricDataList, attemptCount); + List actualMetricDataList = getMetricDataList(); + verifyPointDataSum(actualMetricDataList, attemptCount); - Map attributeMapping = + Map expectedAttributes = ImmutableMap.of( MetricsTracer.METHOD_NAME_ATTRIBUTE, "Echo.Block", MetricsTracer.LANGUAGE_ATTRIBUTE, MetricsTracer.DEFAULT_LANGUAGE); - verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + verifyDefaultMetricsAttributes(actualMetricDataList, expectedAttributes); List statusCountList = ImmutableList.of(new StatusCount(statusCode)); - verifyStatusAttribute(metricDataList, statusCountList); + verifyStatusAttribute(actualMetricDataList, statusCountList); } @Disabled("https://github.com/googleapis/sdk-platform-java/issues/2503") @@ -436,19 +445,19 @@ void testHttpJson_operationFailed_recordsMetrics() throws InterruptedException { ApiFuture blockResponseApiFuture = blockCallable.futureCall(blockRequest); assertThrows(ExecutionException.class, blockResponseApiFuture::get); - List metricDataList = getMetricDataList(); - verifyPointDataSum(metricDataList, attemptCount); + List actualMetricDataList = getMetricDataList(); + verifyPointDataSum(actualMetricDataList, attemptCount); - Map attributeMapping = + Map expectedAttributes = ImmutableMap.of( MetricsTracer.METHOD_NAME_ATTRIBUTE, "google.showcase.v1beta1.Echo/Block", MetricsTracer.LANGUAGE_ATTRIBUTE, MetricsTracer.DEFAULT_LANGUAGE); - verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + verifyDefaultMetricsAttributes(actualMetricDataList, expectedAttributes); List statusCountList = ImmutableList.of(new StatusCount(statusCode)); - verifyStatusAttribute(metricDataList, statusCountList); + verifyStatusAttribute(actualMetricDataList, statusCountList); } @Test @@ -499,19 +508,19 @@ void testGrpc_attemptFailedRetriesExhausted_recordsMetrics() throws Exception { assertThrows(UnavailableException.class, () -> grpcClient.echo(echoRequest)); - List metricDataList = getMetricDataList(); - verifyPointDataSum(metricDataList, attemptCount); + List actualMetricDataList = getMetricDataList(); + verifyPointDataSum(actualMetricDataList, attemptCount); - Map attributeMapping = + Map expectedAttributes = ImmutableMap.of( MetricsTracer.METHOD_NAME_ATTRIBUTE, "Echo.Echo", MetricsTracer.LANGUAGE_ATTRIBUTE, MetricsTracer.DEFAULT_LANGUAGE); - verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + verifyDefaultMetricsAttributes(actualMetricDataList, expectedAttributes); List statusCountList = ImmutableList.of(new StatusCount(statusCode, 3)); - verifyStatusAttribute(metricDataList, statusCountList); + verifyStatusAttribute(actualMetricDataList, statusCountList); grpcClient.close(); grpcClient.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); @@ -567,19 +576,19 @@ void testHttpJson_attemptFailedRetriesExhausted_recordsMetrics() throws Exceptio assertThrows(UnavailableException.class, () -> httpClient.echo(echoRequest)); - List metricDataList = getMetricDataList(); - verifyPointDataSum(metricDataList, attemptCount); + List actualMetricDataList = getMetricDataList(); + verifyPointDataSum(actualMetricDataList, attemptCount); - Map attributeMapping = + Map expectedAttributes = ImmutableMap.of( MetricsTracer.METHOD_NAME_ATTRIBUTE, "google.showcase.v1beta1.Echo/Echo", MetricsTracer.LANGUAGE_ATTRIBUTE, MetricsTracer.DEFAULT_LANGUAGE); - verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + verifyDefaultMetricsAttributes(actualMetricDataList, expectedAttributes); List statusCountList = ImmutableList.of(new StatusCount(statusCode, 3)); - verifyStatusAttribute(metricDataList, statusCountList); + verifyStatusAttribute(actualMetricDataList, statusCountList); httpClient.close(); httpClient.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); @@ -597,19 +606,19 @@ void testGrpc_attemptPermanentFailure_recordsMetrics() throws InterruptedExcepti assertThrows(InvalidArgumentException.class, () -> grpcClient.block(blockRequest)); - List metricDataList = getMetricDataList(); - verifyPointDataSum(metricDataList, attemptCount); + List actualMetricDataList = getMetricDataList(); + verifyPointDataSum(actualMetricDataList, attemptCount); - Map attributeMapping = + Map expectedAttributes = ImmutableMap.of( MetricsTracer.METHOD_NAME_ATTRIBUTE, "Echo.Block", MetricsTracer.LANGUAGE_ATTRIBUTE, MetricsTracer.DEFAULT_LANGUAGE); - verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + verifyDefaultMetricsAttributes(actualMetricDataList, expectedAttributes); List statusCountList = ImmutableList.of(new StatusCount(statusCode)); - verifyStatusAttribute(metricDataList, statusCountList); + verifyStatusAttribute(actualMetricDataList, statusCountList); } @Disabled("https://github.com/googleapis/sdk-platform-java/issues/2503") @@ -625,19 +634,19 @@ void testHttpJson_attemptPermanentFailure_recordsMetrics() throws InterruptedExc assertThrows(InvalidArgumentException.class, () -> httpClient.block(blockRequest)); - List metricDataList = getMetricDataList(); - verifyPointDataSum(metricDataList, attemptCount); + List actualMetricDataList = getMetricDataList(); + verifyPointDataSum(actualMetricDataList, attemptCount); - Map attributeMapping = + Map expectedAttributes = ImmutableMap.of( MetricsTracer.METHOD_NAME_ATTRIBUTE, "google.showcase.v1beta1.Echo/Block", MetricsTracer.LANGUAGE_ATTRIBUTE, MetricsTracer.DEFAULT_LANGUAGE); - verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + verifyDefaultMetricsAttributes(actualMetricDataList, expectedAttributes); List statusCountList = ImmutableList.of(new StatusCount(statusCode)); - verifyStatusAttribute(metricDataList, statusCountList); + verifyStatusAttribute(actualMetricDataList, statusCountList); } @Test @@ -694,20 +703,20 @@ void testGrpc_multipleFailedAttempts_successfulOperation() throws Exception { grpcClient.block(blockRequest); - List metricDataList = getMetricDataList(); - verifyPointDataSum(metricDataList, attemptCount); + List actualMetricDataList = getMetricDataList(); + verifyPointDataSum(actualMetricDataList, attemptCount); - Map attributeMapping = + Map expectedAttributes = ImmutableMap.of( MetricsTracer.METHOD_NAME_ATTRIBUTE, "Echo.Block", MetricsTracer.LANGUAGE_ATTRIBUTE, MetricsTracer.DEFAULT_LANGUAGE); - verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + verifyDefaultMetricsAttributes(actualMetricDataList, expectedAttributes); List statusCountList = ImmutableList.of(new StatusCount(Code.DEADLINE_EXCEEDED, 2), new StatusCount(Code.OK)); - verifyStatusAttribute(metricDataList, statusCountList); + verifyStatusAttribute(actualMetricDataList, statusCountList); grpcClient.close(); grpcClient.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); @@ -764,18 +773,86 @@ void testHttpJson_multipleFailedAttempts_successfulOperation() throws Exception grpcClient.block(blockRequest); - List metricDataList = getMetricDataList(); - verifyPointDataSum(metricDataList, attemptCount); + List actualMetricDataList = getMetricDataList(); + verifyPointDataSum(actualMetricDataList, attemptCount); - Map attributeMapping = + Map expectedAttributes = ImmutableMap.of( MetricsTracer.METHOD_NAME_ATTRIBUTE, "google.showcase.v1beta1.Echo/Block", MetricsTracer.LANGUAGE_ATTRIBUTE, MetricsTracer.DEFAULT_LANGUAGE); - verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + verifyDefaultMetricsAttributes(actualMetricDataList, expectedAttributes); httpClient.close(); httpClient.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); } + + @Test + void recordsCustomAttributes() throws InterruptedException, IOException { + InstantiatingGrpcChannelProvider channelProvider = + EchoSettings.defaultGrpcTransportProviderBuilder() + .setChannelConfigurator(ManagedChannelBuilder::usePlaintext) + .setAttemptDirectPathXds() + .build(); + + // Add custom attributes to be added as client level attributes + Map customAttributes = new HashMap<>(); + String directpathEnabled = "directpath_enabled"; + customAttributes.put(directpathEnabled, String.valueOf(channelProvider.canUseDirectPath())); + String directpathXdsEnabled = "directpathxds_enabled"; + customAttributes.put( + directpathXdsEnabled, String.valueOf(channelProvider.isDirectPathXdsEnabled())); + String randomAttributeKey1 = "testing"; + String randomAttributeValue1 = "showcase"; + String randomAttributeKey2 = "hello"; + String randomAttributeValue2 = "world"; + customAttributes.put(randomAttributeKey1, randomAttributeValue1); + customAttributes.put(randomAttributeKey2, randomAttributeValue2); + + InMemoryMetricReader inMemoryMetricReader = InMemoryMetricReader.create(); + OpenTelemetryMetricsRecorder otelMetricsRecorder = + createOtelMetricsRecorder(inMemoryMetricReader); + MetricsTracerFactory metricsTracerFactory = + new MetricsTracerFactory(otelMetricsRecorder, customAttributes); + + EchoSettings grpcEchoSettings = + EchoSettings.newBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider(channelProvider) + .setEndpoint(TestClientInitializer.DEFAULT_GRPC_ENDPOINT) + .build(); + + EchoStubSettings echoStubSettings = + (EchoStubSettings) + grpcEchoSettings + .getStubSettings() + .toBuilder() + .setTracerFactory(metricsTracerFactory) + .build(); + EchoStub stub = echoStubSettings.createStub(); + EchoClient grpcClient = EchoClient.create(stub); + + EchoRequest echoRequest = EchoRequest.newBuilder().setContent("content").build(); + grpcClient.echo(echoRequest); + + List actualMetricDataList = getMetricDataList(inMemoryMetricReader); + Map expectedAttributes = + ImmutableMap.of( + MetricsTracer.METHOD_NAME_ATTRIBUTE, + "Echo.Echo", + MetricsTracer.LANGUAGE_ATTRIBUTE, + MetricsTracer.DEFAULT_LANGUAGE, + directpathEnabled, + "false", + directpathXdsEnabled, + "true", + randomAttributeKey1, + randomAttributeValue1, + randomAttributeKey2, + randomAttributeValue2); + verifyDefaultMetricsAttributes(actualMetricDataList, expectedAttributes); + + inMemoryMetricReader.close(); + } }