Skip to content

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

Merged
merged 48 commits into from
Jul 2, 2025
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
9753f28
Add Client Metadata update support.
vbabanin May 6, 2025
889d5c8
Add prose tests.
vbabanin May 8, 2025
4b3065c
Add prose tests.
vbabanin May 8, 2025
2a684bf
Add ClientMetadata entity.
vbabanin May 8, 2025
28c1844
Update tests.
vbabanin May 8, 2025
371ce0b
Merge branch 'main' into JAVA-5854
vbabanin May 8, 2025
972fe9d
Fix tests.
vbabanin May 8, 2025
bcf9cc8
Fix tests.
vbabanin May 8, 2025
179b262
Spotless fix.
vbabanin May 8, 2025
bc43ba0
Skip tests when auth is enabled.
vbabanin May 9, 2025
3f5fc91
Apply Scala spotless.
vbabanin May 9, 2025
61192d8
Update tests.
vbabanin May 10, 2025
e9f8dd6
Fix tests.
vbabanin May 10, 2025
95c1bb1
Add parametrized tests.
vbabanin May 19, 2025
dd63a49
Apply Kotlin spotless.
vbabanin May 19, 2025
89d67bb
Move update to separate variables.
vbabanin May 19, 2025
8a18d39
Merge branch 'main' into JAVA-5854
vbabanin May 20, 2025
f296d0c
Merge branch 'main' into JAVA-5854
vbabanin May 20, 2025
a8dc4fb
Add lock for updates.
vbabanin May 21, 2025
8ade58b
Clone document before passing it as an argument.
vbabanin May 21, 2025
71350fa
Rename to "appendMetadata".
vbabanin May 21, 2025
9890aa1
Add ReadWriteLock and rename method.
vbabanin May 27, 2025
28f7d88
Add parametrized tests.
vbabanin Jun 4, 2025
246a040
Add readLock.
vbabanin Jun 4, 2025
8a58294
Fix readLock issue.
vbabanin Jun 4, 2025
15ecef8
Merge branch 'main' into JAVA-5854
vbabanin Jun 4, 2025
5c7a6e3
Add Kotlin and Scala API.
vbabanin Jun 6, 2025
f466d08
Add unified tests.
vbabanin Jun 10, 2025
3132541
Merge branch 'main' into JAVA-5854
vbabanin Jun 10, 2025
d961447
Fix static checks.
vbabanin Jun 10, 2025
74d8558
Change specification submodule commit.
vbabanin Jun 10, 2025
31ef18e
Revert specifications submodule URL to upstream repository.
vbabanin Jun 17, 2025
38ba5e6
Update specifications submodule commit hash.
vbabanin Jun 17, 2025
edafc54
Merge branch 'main' into JAVA-5854
vbabanin Jun 17, 2025
6aa0a1e
Merge branch 'main' into JAVA-5854
vbabanin Jun 17, 2025
dab5451
Remove duplicate import of CLIENT_METADATA in MultiServerClusterSpeci…
vbabanin Jun 17, 2025
1550122
Merge branch 'main' into JAVA-5854
vbabanin Jun 19, 2025
b5c4c20
Remove ClientMetadataHelper.
vbabanin Jun 20, 2025
b6b4d67
Update native-image.properties.
vbabanin Jun 20, 2025
d43972d
Refactor tests to use JUnit assertions and remove unused dependencies.
vbabanin Jun 24, 2025
73bdbd8
Fix test.
vbabanin Jun 24, 2025
66a5913
Add internal driver information.
vbabanin Jun 25, 2025
26246f0
Fix tests.
vbabanin Jun 25, 2025
843d890
Merge branch 'main' into JAVA-5854
vbabanin Jun 25, 2025
850ebe7
Add metadata logging.
vbabanin Jun 26, 2025
d0cabe4
Merge branch 'main' into JAVA-5854
vbabanin Jun 30, 2025
cb40b6a
Add test assertion.
vbabanin Jun 30, 2025
c44200f
Fix GSSAPI test.
vbabanin Jun 30, 2025
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
4 changes: 0 additions & 4 deletions buildSrc/src/main/kotlin/conventions/testing-base.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,3 @@ testlogger {
showFailedStandardStreams = true
logLevel = LogLevel.LIFECYCLE
}

dependencies {
testImplementation(libs.assertj)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}

Expand All @@ -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"));
Copy link
Contributor

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)

boolean isRunningInContainer() {
    // Check for /.dockerenv
    if (Files.exists(Paths.get("/.dockerenv"))) return true;

    // Check cgroup for Docker/container patterns
    try {
        List<String> lines = Files.readAllLines(Paths.get("/proc/1/cgroup"));
        for (String line : lines) {
            if (line.contains("docker") || line.contains("kubepods") || line.contains("containerd")) {
                return true;
            }
        }
    } catch (IOException ignored) {}

    // Check for common container environment variable
    if (System.getenv("container") != null) return true;

    return false;
}

Copy link
Member Author

@vbabanin vbabanin Jun 24, 2025

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 the ClientMetadata 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, and nameStartsWith were also moved from ClientMetadataHelper. 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.

} catch (Exception e) {
return false;
// NOOP. This could be a SecurityException.
}
}
},
UNKNOWN(null);
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we interested to track other containers like Podman?

Copy link
Member Author

Choose a reason for hiding this comment

The 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;
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe also check for /etc/kubernetes per node if available?

Copy link
Member Author

Choose a reason for hiding this comment

The 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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() {
return driverNames;
}

public List<String> getAllDriverVersions() {
return driverVersions;
}

public List<String> getAllDriverPlatforms() {
return driverPlatforms;
}
}
}
Loading