-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Add Client Metadata Update Support. #1708
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
Changes from 8 commits
9753f28
889d5c8
4b3065c
2a684bf
28c1844
371ce0b
972fe9d
bcf9cc8
179b262
bc43ba0
3f5fc91
61192d8
e9f8dd6
95c1bb1
dd63a49
89d67bb
8a18d39
f296d0c
a8dc4fb
8ade58b
71350fa
9890aa1
28f7d88
246a040
8a58294
15ecef8
5c7a6e3
f466d08
3132541
d961447
74d8558
31ef18e
38ba5e6
edafc54
6aa0a1e
dab5451
1550122
b5c4c20
b6b4d67
d43972d
73bdbd8
66a5913
26246f0
843d890
850ebe7
d0cabe4
cb40b6a
c44200f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,27 +17,58 @@ | |
package com.mongodb.internal.connection; | ||
|
||
import com.mongodb.MongoDriverInformation; | ||
import com.mongodb.annotations.ThreadSafe; | ||
import com.mongodb.internal.VisibleForTesting; | ||
import com.mongodb.internal.build.MongoDriverVersion; | ||
import com.mongodb.lang.Nullable; | ||
import org.bson.BsonBinaryWriter; | ||
import org.bson.BsonDocument; | ||
import org.bson.BsonInt32; | ||
import org.bson.BsonString; | ||
import org.bson.BsonValue; | ||
import org.bson.codecs.BsonDocumentCodec; | ||
import org.bson.codecs.EncoderContext; | ||
import org.bson.io.BasicOutputBuffer; | ||
|
||
import java.io.File; | ||
import java.nio.charset.StandardCharsets; | ||
import java.nio.file.Files; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.concurrent.locks.ReentrantReadWriteLock; | ||
import java.util.function.Consumer; | ||
|
||
import static com.mongodb.assertions.Assertions.isTrueArgument; | ||
import static com.mongodb.internal.Locks.withLock; | ||
import static com.mongodb.internal.connection.ClientMetadataHelper.createClientMetadataDocument; | ||
import static com.mongodb.internal.connection.ClientMetadataHelper.updateClientMetadataDocument; | ||
import static com.mongodb.internal.connection.FaasEnvironment.getFaasEnvironment; | ||
import static java.lang.String.format; | ||
import static java.lang.System.getProperty; | ||
import static java.nio.file.Paths.get; | ||
|
||
/** | ||
* Represents metadata of the current MongoClient. | ||
* | ||
* Metadata is used to identify the client in the server logs and metrics. | ||
* | ||
* <p>This class is not part of the public API and may be removed or changed at any time</p> | ||
*/ | ||
@ThreadSafe | ||
public class ClientMetadata { | ||
private static final String SEPARATOR = "|"; | ||
private static final int MAXIMUM_CLIENT_METADATA_ENCODED_SIZE = 512; | ||
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); | ||
private final String applicationName; | ||
private BsonDocument clientMetadataBsonDocument; | ||
private DriverInformation driverInformation; | ||
|
||
public ClientMetadata(@Nullable final String applicationName, final MongoDriverInformation mongoDriverInformation) { | ||
this.applicationName = applicationName; | ||
withLock(readWriteLock.writeLock(), () -> { | ||
this.clientMetadataBsonDocument = createClientMetadataDocument(applicationName, mongoDriverInformation); | ||
this.driverInformation = DriverInformation.from( | ||
mongoDriverInformation.getDriverNames(), | ||
mongoDriverInformation.getDriverVersions(), | ||
mongoDriverInformation.getDriverPlatforms()); | ||
this.clientMetadataBsonDocument = createClientMetadataDocument(applicationName, driverInformation); | ||
}); | ||
} | ||
|
||
|
@@ -48,10 +79,294 @@ public BsonDocument getBsonDocument() { | |
return withLock(readWriteLock.readLock(), () -> clientMetadataBsonDocument); | ||
} | ||
|
||
public void append(final MongoDriverInformation mongoDriverInformation) { | ||
withLock(readWriteLock.writeLock(), () -> | ||
this.clientMetadataBsonDocument = updateClientMetadataDocument(clientMetadataBsonDocument.clone(), mongoDriverInformation) | ||
); | ||
public void append(final MongoDriverInformation mongoDriverInformationToAppend) { | ||
withLock(readWriteLock.writeLock(), () -> { | ||
this.driverInformation.append( | ||
mongoDriverInformationToAppend.getDriverNames(), | ||
mongoDriverInformationToAppend.getDriverVersions(), | ||
mongoDriverInformationToAppend.getDriverPlatforms()); | ||
this.clientMetadataBsonDocument = createClientMetadataDocument(applicationName, driverInformation); | ||
}); | ||
} | ||
} | ||
|
||
private static BsonDocument createClientMetadataDocument(@Nullable final String applicationName, | ||
final DriverInformation driverInformation) { | ||
if (applicationName != null) { | ||
isTrueArgument("applicationName UTF-8 encoding length <= 128", | ||
applicationName.getBytes(StandardCharsets.UTF_8).length <= 128); | ||
} | ||
|
||
// client fields are added in "preservation" order: | ||
BsonDocument client = new BsonDocument(); | ||
tryWithLimit(client, d -> putAtPath(d, "application.name", applicationName)); | ||
|
||
// required fields: | ||
tryWithLimit(client, d -> { | ||
putAtPath(d, "driver.name", driverInformation.getInitialDriverName()); | ||
putAtPath(d, "driver.version", driverInformation.getInitialDriverVersion()); | ||
}); | ||
tryWithLimit(client, d -> putAtPath(d, "os.type", getOperatingSystemType(getOperatingSystemName()))); | ||
// full driver information: | ||
tryWithLimit(client, d -> { | ||
putAtPath(d, "driver.name", listToString(driverInformation.getAllDriverNames())); | ||
putAtPath(d, "driver.version", listToString(driverInformation.getAllDriverVersions())); | ||
}); | ||
|
||
// optional fields: | ||
FaasEnvironment faasEnvironment = getFaasEnvironment(); | ||
ClientMetadata.ContainerRuntime containerRuntime = ClientMetadata.ContainerRuntime.determineExecutionContainer(); | ||
ClientMetadata.Orchestrator orchestrator = ClientMetadata.Orchestrator.determineExecutionOrchestrator(); | ||
|
||
tryWithLimit(client, d -> putAtPath(d, "platform", driverInformation.getInitialDriverPlatform())); | ||
tryWithLimit(client, d -> putAtPath(d, "platform", listToString(driverInformation.getAllDriverPlatforms()))); | ||
tryWithLimit(client, d -> putAtPath(d, "os.name", getOperatingSystemName())); | ||
tryWithLimit(client, d -> putAtPath(d, "os.architecture", getProperty("os.arch", "unknown"))); | ||
tryWithLimit(client, d -> putAtPath(d, "os.version", getProperty("os.version", "unknown"))); | ||
|
||
tryWithLimit(client, d -> putAtPath(d, "env.name", faasEnvironment.getName())); | ||
tryWithLimit(client, d -> putAtPath(d, "env.timeout_sec", faasEnvironment.getTimeoutSec())); | ||
tryWithLimit(client, d -> putAtPath(d, "env.memory_mb", faasEnvironment.getMemoryMb())); | ||
tryWithLimit(client, d -> putAtPath(d, "env.region", faasEnvironment.getRegion())); | ||
|
||
tryWithLimit(client, d -> putAtPath(d, "env.container.runtime", containerRuntime.getName())); | ||
tryWithLimit(client, d -> putAtPath(d, "env.container.orchestrator", orchestrator.getName())); | ||
|
||
return client; | ||
} | ||
|
||
private static void putAtPath(final BsonDocument d, final String path, @Nullable final String value) { | ||
if (value == null) { | ||
return; | ||
} | ||
putAtPath(d, path, new BsonString(value)); | ||
} | ||
|
||
private static void putAtPath(final BsonDocument d, final String path, @Nullable final Integer value) { | ||
if (value == null) { | ||
return; | ||
} | ||
putAtPath(d, path, new BsonInt32(value)); | ||
} | ||
|
||
/** | ||
* Assumes valid documents (or not set) on path. No-op if value is null. | ||
*/ | ||
private static void putAtPath(final BsonDocument d, final String path, @Nullable final BsonValue value) { | ||
if (value == null) { | ||
return; | ||
} | ||
String[] split = path.split("\\.", 2); | ||
String first = split[0]; | ||
if (split.length == 1) { | ||
d.append(first, value); | ||
} else { | ||
BsonDocument child; | ||
if (d.containsKey(first)) { | ||
child = d.getDocument(first); | ||
} else { | ||
child = new BsonDocument(); | ||
d.append(first, child); | ||
} | ||
String rest = split[1]; | ||
putAtPath(child, rest, value); | ||
} | ||
} | ||
|
||
private static void tryWithLimit(final BsonDocument document, final Consumer<BsonDocument> modifier) { | ||
try { | ||
BsonDocument temp = document.clone(); | ||
modifier.accept(temp); | ||
if (!clientMetadataDocumentTooLarge(temp)) { | ||
modifier.accept(document); | ||
} | ||
} catch (Exception e) { | ||
// do nothing. This could be a SecurityException, or any other issue while building the document | ||
} | ||
} | ||
|
||
static boolean clientMetadataDocumentTooLarge(final BsonDocument document) { | ||
BasicOutputBuffer buffer = new BasicOutputBuffer(MAXIMUM_CLIENT_METADATA_ENCODED_SIZE); | ||
new BsonDocumentCodec().encode(new BsonBinaryWriter(buffer), document, EncoderContext.builder().build()); | ||
return buffer.getPosition() > MAXIMUM_CLIENT_METADATA_ENCODED_SIZE; | ||
} | ||
|
||
private enum ContainerRuntime { | ||
DOCKER("docker") { | ||
@Override | ||
boolean isCurrentRuntimeContainer() { | ||
try { | ||
return Files.exists(get(File.separator + ".dockerenv")); | ||
} catch (Exception e) { | ||
return false; | ||
// NOOP. This could be a SecurityException. | ||
} | ||
} | ||
}, | ||
UNKNOWN(null); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we interested to track other containers like Podman? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replied in #1708 (comment). |
||
|
||
@Nullable | ||
private final String name; | ||
|
||
ContainerRuntime(@Nullable final String name) { | ||
this.name = name; | ||
} | ||
|
||
@Nullable | ||
public String getName() { | ||
return name; | ||
} | ||
|
||
boolean isCurrentRuntimeContainer() { | ||
return false; | ||
} | ||
|
||
static ClientMetadata.ContainerRuntime determineExecutionContainer() { | ||
for (ClientMetadata.ContainerRuntime allegedContainer : ClientMetadata.ContainerRuntime.values()) { | ||
if (allegedContainer.isCurrentRuntimeContainer()) { | ||
return allegedContainer; | ||
} | ||
} | ||
return UNKNOWN; | ||
} | ||
} | ||
|
||
private enum Orchestrator { | ||
K8S("kubernetes") { | ||
@Override | ||
boolean isCurrentOrchestrator() { | ||
return FaasEnvironment.getEnv("KUBERNETES_SERVICE_HOST") != null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe also check for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replied in #1708 (comment). |
||
} | ||
}, | ||
UNKNOWN(null); | ||
|
||
@Nullable | ||
private final String name; | ||
|
||
Orchestrator(@Nullable final String name) { | ||
this.name = name; | ||
} | ||
|
||
@Nullable | ||
public String getName() { | ||
return name; | ||
} | ||
|
||
boolean isCurrentOrchestrator() { | ||
return false; | ||
} | ||
|
||
static ClientMetadata.Orchestrator determineExecutionOrchestrator() { | ||
for (ClientMetadata.Orchestrator alledgedOrchestrator : ClientMetadata.Orchestrator.values()) { | ||
if (alledgedOrchestrator.isCurrentOrchestrator()) { | ||
return alledgedOrchestrator; | ||
} | ||
} | ||
return UNKNOWN; | ||
} | ||
} | ||
|
||
private static String listToString(final List<String> listOfStrings) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't we use String result = listOfStrings.stream()
.collect(Collectors.joining(SEPARATOR)); Instead of this method? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replied in #1708 (comment). |
||
StringBuilder stringBuilder = new StringBuilder(); | ||
int i = 0; | ||
for (String val : listOfStrings) { | ||
if (i > 0) { | ||
stringBuilder.append(SEPARATOR); | ||
} | ||
stringBuilder.append(val); | ||
i++; | ||
} | ||
return stringBuilder.toString(); | ||
} | ||
|
||
@VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) | ||
public static String getOperatingSystemType(final String operatingSystemName) { | ||
if (nameStartsWith(operatingSystemName, "linux")) { | ||
return "Linux"; | ||
} else if (nameStartsWith(operatingSystemName, "mac")) { | ||
return "Darwin"; | ||
} else if (nameStartsWith(operatingSystemName, "windows")) { | ||
return "Windows"; | ||
} else if (nameStartsWith(operatingSystemName, "hp-ux", "aix", "irix", "solaris", "sunos")) { | ||
return "Unix"; | ||
} else { | ||
return "unknown"; | ||
} | ||
} | ||
|
||
private static String getOperatingSystemName() { | ||
return getProperty("os.name", "unknown"); | ||
} | ||
|
||
private static boolean nameStartsWith(final String name, final String... prefixes) { | ||
for (String prefix : prefixes) { | ||
if (name.toLowerCase().startsWith(prefix.toLowerCase())) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
/** | ||
* Holds driver information of client.driver field | ||
* in {@link ClientMetadata#clientMetadataBsonDocument}. | ||
*/ | ||
private static class DriverInformation { | ||
private final List<String> driverNames; | ||
private final List<String> driverVersions; | ||
private final List<String> driverPlatforms; | ||
private final String initialPlatform; | ||
|
||
DriverInformation() { | ||
this.driverNames = new ArrayList<>(); | ||
driverNames.add(MongoDriverVersion.NAME); | ||
|
||
this.driverVersions = new ArrayList<>(); | ||
driverVersions.add(MongoDriverVersion.VERSION); | ||
|
||
this.initialPlatform = format("Java/%s/%s", getProperty("java.vendor", "unknown-vendor"), | ||
getProperty("java.runtime.version", "unknown-version")); | ||
this.driverPlatforms = new ArrayList<>(); | ||
driverPlatforms.add(initialPlatform); | ||
} | ||
|
||
static DriverInformation from(final List<String> driverNames, | ||
final List<String> driverVersions, | ||
final List<String> driverPlatforms) { | ||
DriverInformation driverInformation = new DriverInformation(); | ||
return driverInformation.append(driverNames, driverVersions, driverPlatforms); | ||
} | ||
|
||
DriverInformation append(final List<String> driverNames, | ||
final List<String> driverVersions, | ||
final List<String> driverPlatforms) { | ||
this.driverNames.addAll(driverNames); | ||
this.driverVersions.addAll(driverVersions); | ||
this.driverPlatforms.addAll(driverPlatforms); | ||
return this; | ||
} | ||
|
||
public String getInitialDriverPlatform() { | ||
return initialPlatform; | ||
} | ||
|
||
public String getInitialDriverName() { | ||
return MongoDriverVersion.NAME; | ||
} | ||
|
||
public String getInitialDriverVersion() { | ||
return MongoDriverVersion.VERSION; | ||
} | ||
|
||
public List<String> getAllDriverNames() { | ||
jyemin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return driverNames; | ||
} | ||
|
||
public List<String> getAllDriverVersions() { | ||
return driverVersions; | ||
} | ||
|
||
public List<String> getAllDriverPlatforms() { | ||
return driverPlatforms; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The file is not always guaranteed to be created. we can add other heuristics based on
cgroup
and environment variables (set by Kubernetes for example)Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those are valid points and I agree. However, these changes were just moved from
ClientMetadataHelper
to consolidate them in theClientMetadata
instance; they were already present in the codebase and were not introduced in this PR.You can see the diff here:
https://github.com/mongodb/mongo-java-driver/pull/1708/files#diff-ad857628989f20f23a2f410c3a2c9b49d1fc664d06a2998a489393adb5bd8e8dR161
The methods
getOperatingSystemType
,getOperatingSystemName
, andnameStartsWith
were also moved fromClientMetadataHelper
. They show up as new additions because I relocated them to the bottom of the class for better organization - separating utility methods from the core logic.