diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BuiltInMetricsProvider.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BuiltInMetricsProvider.java index d35c69b499..76becf25c9 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BuiltInMetricsProvider.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BuiltInMetricsProvider.java @@ -68,12 +68,16 @@ final class BuiltInMetricsProvider { private BuiltInMetricsProvider() {} OpenTelemetry getOrCreateOpenTelemetry( - String projectId, @Nullable Credentials credentials, @Nullable String monitoringHost) { + String projectId, + @Nullable Credentials credentials, + @Nullable String monitoringHost, + String universeDomain) { try { if (this.openTelemetry == null) { SdkMeterProviderBuilder sdkMeterProviderBuilder = SdkMeterProvider.builder(); BuiltInMetricsView.registerBuiltinMetrics( - SpannerCloudMonitoringExporter.create(projectId, credentials, monitoringHost), + SpannerCloudMonitoringExporter.create( + projectId, credentials, monitoringHost, universeDomain), sdkMeterProviderBuilder); sdkMeterProviderBuilder.setResource(Resource.create(createResourceAttributes(projectId))); SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); @@ -95,10 +99,13 @@ void enableGrpcMetrics( InstantiatingGrpcChannelProvider.Builder channelProviderBuilder, String projectId, @Nullable Credentials credentials, - @Nullable String monitoringHost) { + @Nullable String monitoringHost, + String universeDomain) { GrpcOpenTelemetry grpcOpenTelemetry = GrpcOpenTelemetry.newBuilder() - .sdk(this.getOrCreateOpenTelemetry(projectId, credentials, monitoringHost)) + .sdk( + this.getOrCreateOpenTelemetry( + projectId, credentials, monitoringHost, universeDomain)) .enableMetrics(BuiltInMetricsConstant.GRPC_METRICS_TO_ENABLE) // Disable gRPCs default metrics as they are not needed for Spanner. .disableMetrics(BuiltInMetricsConstant.GRPC_METRICS_ENABLED_BY_DEFAULT) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerCloudMonitoringExporter.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerCloudMonitoringExporter.java index b43cf43b25..40202a0eef 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerCloudMonitoringExporter.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerCloudMonitoringExporter.java @@ -27,6 +27,7 @@ import com.google.cloud.monitoring.v3.MetricServiceClient; import com.google.cloud.monitoring.v3.MetricServiceSettings; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; import com.google.common.collect.Iterables; import com.google.common.util.concurrent.MoreExecutors; import com.google.monitoring.v3.CreateTimeSeriesRequest; @@ -71,7 +72,10 @@ class SpannerCloudMonitoringExporter implements MetricExporter { private final String spannerProjectId; static SpannerCloudMonitoringExporter create( - String projectId, @Nullable Credentials credentials, @Nullable String monitoringHost) + String projectId, + @Nullable Credentials credentials, + @Nullable String monitoringHost, + String universeDomain) throws IOException { MetricServiceSettings.Builder settingsBuilder = MetricServiceSettings.newBuilder(); CredentialsProvider credentialsProvider; @@ -84,6 +88,9 @@ static SpannerCloudMonitoringExporter create( if (monitoringHost != null) { settingsBuilder.setEndpoint(monitoringHost); } + if (!Strings.isNullOrEmpty(universeDomain)) { + settingsBuilder.setUniverseDomain(universeDomain); + } Duration timeout = Duration.ofMinutes(1); // TODO: createServiceTimeSeries needs special handling if the request failed. Leaving @@ -110,6 +117,11 @@ public CompletableResultCode export(@Nonnull Collection collection) return exportSpannerClientMetrics(collection); } + @VisibleForTesting + MetricServiceClient getMetricServiceClient() { + return client; + } + /** Export client built in metrics */ private CompletableResultCode exportSpannerClientMetrics(Collection collection) { // Filter spanner metrics. Only include metrics that contain a valid project. diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index 2fcb9c0934..af4ed58bad 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -120,7 +120,8 @@ public class SpannerOptions extends ServiceOptions { private static final String PG_ADAPTER_CLIENT_LIB_TOKEN = "pg-adapter"; private static final String API_SHORT_NAME = "Spanner"; - private static final String DEFAULT_HOST = "https://spanner.googleapis.com"; + private static final String SPANNER_SERVICE_NAME = "spanner"; + private static final String GOOGLE_DEFAULT_UNIVERSE = "googleapis.com"; private static final String EXPERIMENTAL_HOST_PROJECT_ID = "default"; private static final ImmutableSet SCOPES = @@ -780,9 +781,19 @@ protected SpannerOptions(Builder builder) { databaseRole = builder.databaseRole; sessionLabels = builder.sessionLabels; try { - spannerStubSettings = builder.spannerStubSettingsBuilder.build(); - instanceAdminStubSettings = builder.instanceAdminStubSettingsBuilder.build(); - databaseAdminStubSettings = builder.databaseAdminStubSettingsBuilder.build(); + String resolvedUniversalDomain = getResolvedUniverseDomain(); + spannerStubSettings = + builder.spannerStubSettingsBuilder.setUniverseDomain(resolvedUniversalDomain).build(); + instanceAdminStubSettings = + builder + .instanceAdminStubSettingsBuilder + .setUniverseDomain(resolvedUniversalDomain) + .build(); + databaseAdminStubSettings = + builder + .databaseAdminStubSettingsBuilder + .setUniverseDomain(resolvedUniversalDomain) + .build(); } catch (IOException e) { throw SpannerExceptionFactory.newSpannerException(e); } @@ -824,6 +835,11 @@ protected SpannerOptions(Builder builder) { defaultTransactionOptions = builder.defaultTransactionOptions; } + private String getResolvedUniverseDomain() { + String universeDomain = getUniverseDomain(); + return Strings.isNullOrEmpty(universeDomain) ? GOOGLE_DEFAULT_UNIVERSE : universeDomain; + } + /** * The environment to read configuration values from. The default implementation uses environment * variables. @@ -871,6 +887,10 @@ default boolean isEnableEndToEndTracing() { return false; } + @Deprecated + @ObsoleteApi( + "This will be removed in an upcoming version without a major version bump. You should use" + + " universalDomain to configure the built-in metrics endpoint for a partner universe.") default String getMonitoringHost() { return null; } @@ -1665,6 +1685,10 @@ public Builder setBuiltInMetricsEnabled(boolean enableBuiltInMetrics) { } /** Sets the monitoring host to be used for Built-in client side metrics */ + @Deprecated + @ObsoleteApi( + "This will be removed in an upcoming version without a major version bump. You should use" + + " universalDomain to configure the built-in metrics endpoint for a partner universe.") public Builder setMonitoringHost(String monitoringHost) { this.monitoringHost = monitoringHost; return this; @@ -2035,7 +2059,11 @@ public ApiTracerFactory getApiTracerFactory() { public void enablegRPCMetrics(InstantiatingGrpcChannelProvider.Builder channelProviderBuilder) { if (SpannerOptions.environment.isEnableGRPCBuiltInMetrics()) { this.builtInMetricsProvider.enableGrpcMetrics( - channelProviderBuilder, this.getProjectId(), getCredentials(), this.monitoringHost); + channelProviderBuilder, + this.getProjectId(), + getCredentials(), + this.monitoringHost, + getUniverseDomain()); } } @@ -2081,7 +2109,7 @@ private ApiTracerFactory getDefaultApiTracerFactory() { private ApiTracerFactory createMetricsApiTracerFactory() { OpenTelemetry openTelemetry = this.builtInMetricsProvider.getOrCreateOpenTelemetry( - this.getProjectId(), getCredentials(), this.monitoringHost); + this.getProjectId(), getCredentials(), this.monitoringHost, getUniverseDomain()); return openTelemetry != null ? new BuiltInMetricsTracerFactory( @@ -2181,7 +2209,11 @@ public static GrpcTransportOptions getDefaultGrpcTransportOptions() { @Override protected String getDefaultHost() { - return DEFAULT_HOST; + String universeDomain = getUniverseDomain(); + if (Strings.isNullOrEmpty(universeDomain)) { + universeDomain = GOOGLE_DEFAULT_UNIVERSE; + } + return String.format("https://%s.%s", SPANNER_SERVICE_NAME, universeDomain); } private static class SpannerDefaults implements ServiceDefaults { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java index 2c86192443..741fa5db2b 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java @@ -49,6 +49,7 @@ import static com.google.cloud.spanner.connection.ConnectionProperties.TRACING_PREFIX; import static com.google.cloud.spanner.connection.ConnectionProperties.TRACK_CONNECTION_LEAKS; import static com.google.cloud.spanner.connection.ConnectionProperties.TRACK_SESSION_LEAKS; +import static com.google.cloud.spanner.connection.ConnectionProperties.UNIVERSE_DOMAIN; import static com.google.cloud.spanner.connection.ConnectionProperties.USER_AGENT; import static com.google.cloud.spanner.connection.ConnectionProperties.USE_AUTO_SAVEPOINTS_FOR_EMULATOR; import static com.google.cloud.spanner.connection.ConnectionProperties.USE_PLAIN_TEXT; @@ -76,6 +77,7 @@ import com.google.cloud.spanner.SpannerOptions; import com.google.cloud.spanner.connection.StatementExecutor.StatementExecutorType; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.base.Suppliers; @@ -769,7 +771,7 @@ static String determineHost( boolean autoConfigEmulator, boolean usePlainText, Map environment) { - String host; + String host = null; if (Objects.equals(endpoint, DEFAULT_ENDPOINT) && matcher.group(Builder.HOST_GROUP) == null) { if (autoConfigEmulator) { if (Strings.isNullOrEmpty(environment.get(SPANNER_EMULATOR_HOST_ENV_VAR))) { @@ -777,8 +779,6 @@ static String determineHost( } else { return PLAIN_TEXT_PROTOCOL + "//" + environment.get(SPANNER_EMULATOR_HOST_ENV_VAR); } - } else { - return DEFAULT_HOST; } } else if (!Objects.equals(endpoint, DEFAULT_ENDPOINT)) { // Add '//' at the start of the endpoint to conform to the standard URL specification. @@ -792,6 +792,9 @@ static String determineHost( host = String.format("%s:15000", host); } } + if (host == null) { + return null; + } if (usePlainText) { return PLAIN_TEXT_PROTOCOL + host; } @@ -968,7 +971,7 @@ public TransportChannelProvider getChannelProvider() { return null; } try { - URL url = new URL(host); + URL url = new URL(MoreObjects.firstNonNull(host, DEFAULT_HOST)); ExternalChannelProvider provider = ExternalChannelProvider.class.cast(Class.forName(channelProvider).newInstance()); return provider.getChannelProvider(url.getHost(), url.getPort()); @@ -1086,6 +1089,10 @@ Boolean isEnableDirectAccess() { return getInitialConnectionPropertyValue(ENABLE_DIRECT_ACCESS); } + String getUniverseDomain() { + return getInitialConnectionPropertyValue(UNIVERSE_DOMAIN); + } + String getClientCertificate() { return getInitialConnectionPropertyValue(CLIENT_CERTIFICATE); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java index febccc3a15..10797df88c 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java @@ -200,6 +200,14 @@ public class ConnectionProperties { BOOLEANS, BooleanConverter.INSTANCE, Context.STARTUP); + static final ConnectionProperty UNIVERSE_DOMAIN = + create( + "universeDomain", + "Configure the connection to try to connect to Spanner using " + + "a different partner Google Universe than GDU (googleapis.com).", + "googleapis.com", + StringValueConverter.INSTANCE, + Context.STARTUP); static final ConnectionProperty USE_AUTO_SAVEPOINTS_FOR_EMULATOR = create( "useAutoSavepointsForEmulator", diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java index b21e8c84db..c1cf3ae679 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java @@ -165,6 +165,7 @@ static class SpannerPoolKey { private final String clientCertificateKey; private final boolean isExperimentalHost; private final Boolean enableDirectAccess; + private final String universeDomain; @VisibleForTesting static SpannerPoolKey of(ConnectionOptions options) { @@ -200,6 +201,7 @@ private SpannerPoolKey(ConnectionOptions options) throws IOException { this.clientCertificateKey = options.getClientCertificateKey(); this.isExperimentalHost = options.isExperimentalHost(); this.enableDirectAccess = options.isEnableDirectAccess(); + this.universeDomain = options.getUniverseDomain(); } @Override @@ -226,7 +228,8 @@ public boolean equals(Object o) { && Objects.equals(this.clientCertificate, other.clientCertificate) && Objects.equals(this.clientCertificateKey, other.clientCertificateKey) && Objects.equals(this.isExperimentalHost, other.isExperimentalHost) - && Objects.equals(this.enableDirectAccess, other.enableDirectAccess); + && Objects.equals(this.enableDirectAccess, other.enableDirectAccess) + && Objects.equals(this.universeDomain, other.universeDomain); } @Override @@ -249,7 +252,8 @@ public int hashCode() { this.clientCertificate, this.clientCertificateKey, this.isExperimentalHost, - this.enableDirectAccess); + this.enableDirectAccess, + this.universeDomain); } } @@ -419,6 +423,9 @@ Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) { if (key.enableDirectAccess != null) { builder.setEnableDirectAccess(key.enableDirectAccess); } + if (key.universeDomain != null) { + builder.setUniverseDomain(key.universeDomain); + } if (options.getConfigurator() != null) { options.getConfigurator().configure(builder); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerCloudMonitoringExporterTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerCloudMonitoringExporterTest.java index 86053d1b71..c52cab0546 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerCloudMonitoringExporterTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerCloudMonitoringExporterTest.java @@ -30,6 +30,7 @@ import static com.google.cloud.spanner.BuiltInMetricsConstant.OPERATION_LATENCIES_NAME; import static com.google.cloud.spanner.BuiltInMetricsConstant.PROJECT_ID_KEY; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -39,6 +40,7 @@ import com.google.api.core.ApiFutures; import com.google.api.gax.rpc.UnaryCallable; import com.google.cloud.monitoring.v3.MetricServiceClient; +import com.google.cloud.monitoring.v3.MetricServiceSettings; import com.google.cloud.monitoring.v3.stub.MetricServiceStub; import com.google.common.collect.ImmutableList; import com.google.monitoring.v3.CreateTimeSeriesRequest; @@ -454,11 +456,30 @@ public void testExportingHistogramDataWithExemplars() { @Test public void getAggregationTemporality() throws IOException { SpannerCloudMonitoringExporter actualExporter = - SpannerCloudMonitoringExporter.create(projectId, null, null); + SpannerCloudMonitoringExporter.create(projectId, null, null, null); assertThat(actualExporter.getAggregationTemporality(InstrumentType.COUNTER)) .isEqualTo(AggregationTemporality.CUMULATIVE); } + @Test + public void testUniverseDomain() throws IOException { + SpannerCloudMonitoringExporter actualExporter = + SpannerCloudMonitoringExporter.create(projectId, null, null, "abc.goog"); + MetricServiceSettings metricServiceSettings = + actualExporter.getMetricServiceClient().getSettings(); + + assertEquals("abc.goog", metricServiceSettings.getUniverseDomain()); + assertEquals("monitoring.abc.goog:443", metricServiceSettings.getEndpoint()); + + actualExporter = + SpannerCloudMonitoringExporter.create( + projectId, null, "monitoringa.abc.goog:443", "abc.goog"); + metricServiceSettings = actualExporter.getMetricServiceClient().getSettings(); + + assertEquals("abc.goog", metricServiceSettings.getUniverseDomain()); + assertEquals("monitoringa.abc.goog:443", metricServiceSettings.getEndpoint()); + } + private static class FakeMetricServiceClient extends MetricServiceClient { protected FakeMetricServiceClient(MetricServiceStub stub) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsTest.java index e274c0e6c0..e9af95d9de 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsTest.java @@ -26,6 +26,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; import com.google.api.gax.core.CredentialsProvider; import com.google.api.gax.core.NoCredentialsProvider; @@ -36,6 +37,7 @@ import com.google.auth.oauth2.ServiceAccountCredentials; import com.google.cloud.NoCredentials; import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.SpannerOptions; import com.google.common.collect.ImmutableMap; @@ -47,6 +49,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Matcher; import org.junit.Test; import org.junit.function.ThrowingRunnable; @@ -57,7 +60,7 @@ public class ConnectionOptionsTest { private static final String FILE_TEST_PATH = Objects.requireNonNull(ConnectionOptionsTest.class.getResource("test-key.json")).getFile(); - private static final String DEFAULT_HOST = "https://spanner.googleapis.com"; + private static final String DEFAULT_HOST = null; private static final String TEST_PROJECT = "test-project-123"; private static final String TEST_INSTANCE = "test-instance-123"; private static final String TEST_DATABASE = "test-database-123"; @@ -1329,4 +1332,88 @@ public void testEnableDirectAccess() { "spanner://localhost:15000/projects/default/instances/default/databases/singers-db;usePlainText=true;enableDirectAccess=true"); assertTrue(builderWithDirectPathParam.build().isEnableDirectAccess()); } + + @Test + public void testUniverseDomain() { + ConnectionImpl connection = mock(ConnectionImpl.class); + + // No universeDomain + AtomicBoolean executedConfigurator = new AtomicBoolean(false); + ConnectionOptions optionsWithNoUniverseDomainParam = + ConnectionOptions.newBuilder() + .setUri( + "cloudspanner:/projects/default/instances/default/databases/singers-db?usePlainText=true") + .setConfigurator( + optionsBuilder -> { + executedConfigurator.set(true); + SpannerOptions spannerOptions = optionsBuilder.build(); + assertEquals("googleapis.com", spannerOptions.getUniverseDomain()); + assertEquals("https://spanner.googleapis.com", spannerOptions.getHost()); + }) + .build(); + Spanner spanner = SpannerPool.INSTANCE.getSpanner(optionsWithNoUniverseDomainParam, connection); + spanner.close(); + SpannerPool.INSTANCE.removeConnection(optionsWithNoUniverseDomainParam, connection); + assertTrue(executedConfigurator.get()); + + // only configuring universal domain + executedConfigurator.set(false); + ConnectionOptions optionsWithUniverseDomainParam = + ConnectionOptions.newBuilder() + .setUri( + "cloudspanner:/projects/default/instances/default/databases/singers-db;universeDomain=abc.goog;usePlainText=true") + .setConfigurator( + optionsBuilder -> { + executedConfigurator.set(true); + SpannerOptions spannerOptions = optionsBuilder.build(); + assertEquals("abc.goog", spannerOptions.getUniverseDomain()); + assertEquals("https://spanner.abc.goog", spannerOptions.getHost()); + }) + .build(); + spanner = SpannerPool.INSTANCE.getSpanner(optionsWithUniverseDomainParam, connection); + spanner.close(); + SpannerPool.INSTANCE.removeConnection(optionsWithUniverseDomainParam, connection); + assertTrue(executedConfigurator.get()); + + // configuring both universal domain and host + executedConfigurator.set(false); + ConnectionOptions optionsWithHostAndUniverseDomainParam = + ConnectionOptions.newBuilder() + .setUri( + "cloudspanner://spanner.abc.goog/projects/default/instances/default/databases/singers-db;universeDomain=abc.goog;usePlainText=true") + .setConfigurator( + optionsBuilder -> { + executedConfigurator.set(true); + SpannerOptions spannerOptions = optionsBuilder.build(); + assertEquals("abc.goog", spannerOptions.getUniverseDomain()); + assertEquals("http://spanner.abc.goog", spannerOptions.getHost()); + }) + .build(); + spanner = SpannerPool.INSTANCE.getSpanner(optionsWithHostAndUniverseDomainParam, connection); + spanner.close(); + SpannerPool.INSTANCE.removeConnection(optionsWithHostAndUniverseDomainParam, connection); + assertTrue(executedConfigurator.get()); + + // configuring both universal domain and host(localhost) + executedConfigurator.set(false); + ConnectionOptions optionsWithLocalHostAndUniverseDomainParam = + ConnectionOptions.newBuilder() + .setUri( + "cloudspanner://localhost:15000/projects/default/instances/default/databases/singers-db;usePlainText=true;universeDomain=abc.goog") + .setConfigurator( + optionsBuilder -> { + executedConfigurator.set(true); + SpannerOptions spannerOptions = optionsBuilder.build(); + assertEquals("abc.goog", spannerOptions.getUniverseDomain()); + assertEquals("http://localhost:15000", spannerOptions.getHost()); + }) + .build(); + spanner = + SpannerPool.INSTANCE.getSpanner(optionsWithLocalHostAndUniverseDomainParam, connection); + spanner.close(); + SpannerPool.INSTANCE.removeConnection(optionsWithLocalHostAndUniverseDomainParam, connection); + assertTrue(executedConfigurator.get()); + + connection.close(); + } }