From fc6a68a4361f3f10ca7c74edc81db1e14f627e9b Mon Sep 17 00:00:00 2001 From: Viacheslav Babanin Date: Fri, 6 Oct 2023 17:19:14 -0700 Subject: [PATCH] Add server detection based on host names (#1214) --- .../connection/DefaultClusterFactory.java | 68 ++++++++ .../connection/DefaultClusterFactoryTest.java | 145 ++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 driver-core/src/test/unit/com/mongodb/internal/connection/DefaultClusterFactoryTest.java diff --git a/driver-core/src/main/com/mongodb/internal/connection/DefaultClusterFactory.java b/driver-core/src/main/com/mongodb/internal/connection/DefaultClusterFactory.java index 22badc93a9d..431df80c698 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/DefaultClusterFactory.java +++ b/driver-core/src/main/com/mongodb/internal/connection/DefaultClusterFactory.java @@ -20,6 +20,7 @@ import com.mongodb.MongoCompressor; import com.mongodb.MongoCredential; import com.mongodb.MongoDriverInformation; +import com.mongodb.ServerAddress; import com.mongodb.ServerApi; import com.mongodb.connection.ClusterConnectionMode; import com.mongodb.connection.ClusterId; @@ -31,18 +32,23 @@ import com.mongodb.event.CommandListener; import com.mongodb.event.ServerListener; import com.mongodb.event.ServerMonitorListener; +import com.mongodb.internal.VisibleForTesting; +import com.mongodb.internal.diagnostics.logging.Logger; +import com.mongodb.internal.diagnostics.logging.Loggers; import com.mongodb.lang.Nullable; import com.mongodb.spi.dns.DnsClient; import com.mongodb.spi.dns.InetAddressResolver; import java.util.List; +import static com.mongodb.internal.connection.DefaultClusterFactory.ClusterEnvironment.detectCluster; import static com.mongodb.internal.event.EventListenerHelper.NO_OP_CLUSTER_LISTENER; import static com.mongodb.internal.event.EventListenerHelper.NO_OP_SERVER_LISTENER; import static com.mongodb.internal.event.EventListenerHelper.NO_OP_SERVER_MONITOR_LISTENER; import static com.mongodb.internal.event.EventListenerHelper.clusterListenerMulticaster; import static com.mongodb.internal.event.EventListenerHelper.serverListenerMulticaster; import static com.mongodb.internal.event.EventListenerHelper.serverMonitorListenerMulticaster; +import static java.lang.String.format; import static java.util.Collections.singletonList; /** @@ -52,6 +58,7 @@ */ @SuppressWarnings("deprecation") public final class DefaultClusterFactory { + private static final Logger LOGGER = Loggers.getLogger("DefaultClusterFactory"); public Cluster createCluster(final ClusterSettings originalClusterSettings, final ServerSettings originalServerSettings, final ConnectionPoolSettings connectionPoolSettings, @@ -65,6 +72,8 @@ public Cluster createCluster(final ClusterSettings originalClusterSettings, fina final List compressorList, @Nullable final ServerApi serverApi, @Nullable final DnsClient dnsClient, @Nullable final InetAddressResolver inetAddressResolver) { + detectAndLogClusterEnvironment(originalClusterSettings); + ClusterId clusterId = new ClusterId(applicationName); ClusterSettings clusterSettings; ServerSettings serverSettings; @@ -143,4 +152,63 @@ private static ServerMonitorListener getServerMonitorListener(final ServerSettin ? NO_OP_SERVER_MONITOR_LISTENER : serverMonitorListenerMulticaster(serverSettings.getServerMonitorListeners()); } + + @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) + public void detectAndLogClusterEnvironment(final ClusterSettings clusterSettings) { + String srvHost = clusterSettings.getSrvHost(); + ClusterEnvironment clusterEnvironment; + if (srvHost != null) { + clusterEnvironment = detectCluster(srvHost); + } else { + clusterEnvironment = detectCluster(clusterSettings.getHosts() + .stream() + .map(ServerAddress::getHost) + .toArray(String[]::new)); + } + + if (clusterEnvironment != null) { + LOGGER.info(format("You appear to be connected to a %s cluster. For more information regarding feature compatibility" + + " and support please visit %s", clusterEnvironment.clusterProductName, clusterEnvironment.documentationUrl)); + } + } + + enum ClusterEnvironment { + AZURE("https://www.mongodb.com/supportability/cosmosdb", + "CosmosDB", + ".cosmos.azure.com"), + AWS("https://www.mongodb.com/supportability/documentdb", + "DocumentDB", + ".docdb.amazonaws.com", ".docdb-elastic.amazonaws.com"); + + private final String documentationUrl; + private final String clusterProductName; + private final String[] hostSuffixes; + + ClusterEnvironment(final String url, final String name, final String... hostSuffixes) { + this.hostSuffixes = hostSuffixes; + this.documentationUrl = url; + this.clusterProductName = name; + } + @Nullable + public static ClusterEnvironment detectCluster(final String... hosts) { + for (String host : hosts) { + for (ClusterEnvironment clusterEnvironment : values()) { + if (clusterEnvironment.isExternalClusterProvider(host)) { + return clusterEnvironment; + } + } + } + return null; + } + + private boolean isExternalClusterProvider(final String host) { + for (String hostSuffix : hostSuffixes) { + String lowerCaseHost = host.toLowerCase(); + if (lowerCaseHost.endsWith(hostSuffix)) { + return true; + } + } + return false; + } + } } diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/DefaultClusterFactoryTest.java b/driver-core/src/test/unit/com/mongodb/internal/connection/DefaultClusterFactoryTest.java new file mode 100644 index 00000000000..2e6190a4be8 --- /dev/null +++ b/driver-core/src/test/unit/com/mongodb/internal/connection/DefaultClusterFactoryTest.java @@ -0,0 +1,145 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mongodb.internal.connection; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import com.mongodb.ConnectionString; +import com.mongodb.connection.ClusterSettings; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +class DefaultClusterFactoryTest { + private static final String EXPECTED_COSMOS_DB_MESSAGE = + "You appear to be connected to a CosmosDB cluster. For more information regarding " + + "feature compatibility and support please visit https://www.mongodb.com/supportability/cosmosdb"; + + private static final String EXPECTED_DOCUMENT_DB_MESSAGE = + "You appear to be connected to a DocumentDB cluster. For more information regarding " + + "feature compatibility and support please visit https://www.mongodb.com/supportability/documentdb"; + + private static final Logger LOGGER = (Logger) LoggerFactory.getLogger("org.mongodb.driver.DefaultClusterFactory"); + private static final MemoryAppender MEMORY_APPENDER = new MemoryAppender(); + + @BeforeAll + public static void setUp() { + MEMORY_APPENDER.setContext((LoggerContext) LoggerFactory.getILoggerFactory()); + LOGGER.setLevel(Level.DEBUG); + LOGGER.addAppender(MEMORY_APPENDER); + MEMORY_APPENDER.start(); + } + + @AfterAll + public static void cleanUp() { + LOGGER.detachAppender(MEMORY_APPENDER); + } + + @AfterEach + public void reset() { + MEMORY_APPENDER.reset(); + } + + static Stream shouldLogAllegedClusterEnvironmentWhenNonGenuineHostsSpecified() { + return Stream.of( + Arguments.of("mongodb://a.MONGO.COSMOS.AZURE.COM:19555", EXPECTED_COSMOS_DB_MESSAGE), + Arguments.of("mongodb://a.mongo.cosmos.azure.com:19555", EXPECTED_COSMOS_DB_MESSAGE), + Arguments.of("mongodb://a.DOCDB-ELASTIC.AMAZONAWS.COM:27017/", EXPECTED_DOCUMENT_DB_MESSAGE), + Arguments.of("mongodb://a.docdb-elastic.amazonaws.com:27017/", EXPECTED_DOCUMENT_DB_MESSAGE), + Arguments.of("mongodb://a.DOCDB.AMAZONAWS.COM", EXPECTED_DOCUMENT_DB_MESSAGE), + Arguments.of("mongodb://a.docdb.amazonaws.com", EXPECTED_DOCUMENT_DB_MESSAGE), + + /* SRV matching */ + Arguments.of("mongodb+srv://A.MONGO.COSMOS.AZURE.COM", EXPECTED_COSMOS_DB_MESSAGE), + Arguments.of("mongodb+srv://a.mongo.cosmos.azure.com", EXPECTED_COSMOS_DB_MESSAGE), + Arguments.of("mongodb+srv://a.DOCDB.AMAZONAWS.COM/", EXPECTED_DOCUMENT_DB_MESSAGE), + Arguments.of("mongodb+srv://a.docdb.amazonaws.com/", EXPECTED_DOCUMENT_DB_MESSAGE), + Arguments.of("mongodb+srv://a.DOCDB-ELASTIC.AMAZONAWS.COM/", EXPECTED_DOCUMENT_DB_MESSAGE), + Arguments.of("mongodb+srv://a.docdb-elastic.amazonaws.com/", EXPECTED_DOCUMENT_DB_MESSAGE), + + /* Mixing genuine and nongenuine hosts (unlikely in practice) */ + Arguments.of("mongodb://a.example.com:27017,b.mongo.cosmos.azure.com:19555/", EXPECTED_COSMOS_DB_MESSAGE), + Arguments.of("mongodb://a.example.com:27017,b.docdb.amazonaws.com:27017/", EXPECTED_DOCUMENT_DB_MESSAGE), + Arguments.of("mongodb://a.example.com:27017,b.docdb-elastic.amazonaws.com:27017/", EXPECTED_DOCUMENT_DB_MESSAGE) + ); + } + + @ParameterizedTest + @MethodSource + void shouldLogAllegedClusterEnvironmentWhenNonGenuineHostsSpecified(final String connectionString, final String expectedLogMessage) { + //when + ClusterSettings clusterSettings = toClusterSettings(new ConnectionString(connectionString)); + new DefaultClusterFactory().detectAndLogClusterEnvironment(clusterSettings); + + //then + List loggedEvents = MEMORY_APPENDER.search(expectedLogMessage); + + Assertions.assertEquals(1, loggedEvents.size()); + Assertions.assertEquals(Level.INFO, loggedEvents.get(0).getLevel()); + + } + + static Stream shouldNotLogClusterEnvironmentWhenGenuineHostsSpecified() { + return Stream.of( + "mongodb://a.mongo.cosmos.azure.com.tld:19555", + "mongodb://a.docdb-elastic.amazonaws.com.t", + "mongodb+srv://a.example.com", + "mongodb+srv://a.mongodb.net/", + "mongodb+srv://a.mongo.cosmos.azure.com.tld/", + "mongodb+srv://a.docdb-elastic.amazonaws.com.tld/" + ); + } + + @ParameterizedTest + @MethodSource + void shouldNotLogClusterEnvironmentWhenGenuineHostsSpecified(final String connectionUrl) { + //when + ClusterSettings clusterSettings = toClusterSettings(new ConnectionString(connectionUrl)); + new DefaultClusterFactory().detectAndLogClusterEnvironment(clusterSettings); + + //then + Assertions.assertEquals(0, MEMORY_APPENDER.search(EXPECTED_COSMOS_DB_MESSAGE).size()); + Assertions.assertEquals(0, MEMORY_APPENDER.search(EXPECTED_DOCUMENT_DB_MESSAGE).size()); + } + + private static ClusterSettings toClusterSettings(final ConnectionString connectionUrl) { + return ClusterSettings.builder().applyConnectionString(connectionUrl).build(); + } + + public static class MemoryAppender extends ListAppender { + public void reset() { + this.list.clear(); + } + + public List search(final String message) { + return this.list.stream() + .filter(event -> event.getFormattedMessage().contains(message)) + .collect(Collectors.toList()); + } + } +}