Skip to content

Add server detection based on host names during MongoClient construct… #1214

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
Expand All @@ -52,6 +58,7 @@
*/
@SuppressWarnings("deprecation")
public final class DefaultClusterFactory {
private static final Logger LOGGER = Loggers.getLogger("DefaultClusterFactory");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a specification for structured logging messages that ensures consistent logging within drivers. However, since those log messages do not adhere to any specification, the regular approach to logging is being used.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack


public Cluster createCluster(final ClusterSettings originalClusterSettings, final ServerSettings originalServerSettings,
final ConnectionPoolSettings connectionPoolSettings,
Expand All @@ -65,6 +72,8 @@ public Cluster createCluster(final ClusterSettings originalClusterSettings, fina
final List<MongoCompressor> compressorList, @Nullable final ServerApi serverApi,
@Nullable final DnsClient dnsClient, @Nullable final InetAddressResolver inetAddressResolver) {

detectAndLogClusterEnvironment(originalClusterSettings);

ClusterId clusterId = new ClusterId(applicationName);
ClusterSettings clusterSettings;
ServerSettings serverSettings;
Expand Down Expand Up @@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Arguments> 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<ILoggingEvent> loggedEvents = MEMORY_APPENDER.search(expectedLogMessage);

Assertions.assertEquals(1, loggedEvents.size());
Assertions.assertEquals(Level.INFO, loggedEvents.get(0).getLevel());

}

static Stream<String> 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<ILoggingEvent> {
public void reset() {
this.list.clear();
}

public List<ILoggingEvent> search(final String message) {
return this.list.stream()
.filter(event -> event.getFormattedMessage().contains(message))
.collect(Collectors.toList());
}
}
}