void sendAndReceiveAsyncInternal(final CommandMessage message, final
try {
message.encode(bsonOutput, operationContext);
- CommandEventSender commandEventSender = createCommandEventSender(message, bsonOutput, operationContext);
+
+ CommandEventSender commandEventSender;
+ if (isLoggingCommandNeeded()) {
+ BsonDocument commandDocument = message.getCommandDocument(bsonOutput);
+ commandEventSender = new LoggingCommandEventSender(
+ SECURITY_SENSITIVE_COMMANDS, SECURITY_SENSITIVE_HELLO_COMMANDS, description, commandListener,
+ operationContext, message, commandDocument,
+ COMMAND_PROTOCOL_LOGGER, loggerSettings);
+ } else {
+ commandEventSender = new NoOpCommandEventSender();
+ }
+
commandEventSender.sendStartedEvent();
Compressor localSendCompressor = sendCompressor;
if (localSendCompressor == null || SECURITY_SENSITIVE_COMMANDS.contains(message.getCommandDocument(bsonOutput).getFirstKey())) {
@@ -952,19 +1022,112 @@ public void onResult(@Nullable final ByteBuf result, @Nullable final Throwable t
private static final StructuredLogger COMMAND_PROTOCOL_LOGGER = new StructuredLogger("protocol.command");
- private CommandEventSender createCommandEventSender(final CommandMessage message, final ByteBufferBsonOutput bsonOutput,
- final OperationContext operationContext) {
+ private boolean isLoggingCommandNeeded() {
boolean listensOrLogs = commandListener != null || COMMAND_PROTOCOL_LOGGER.isRequired(DEBUG, getClusterId());
- if (!recordEverything && (isMonitoringConnection || !opened() || !authenticated.get() || !listensOrLogs)) {
- return new NoOpCommandEventSender();
- }
- return new LoggingCommandEventSender(
- SECURITY_SENSITIVE_COMMANDS, SECURITY_SENSITIVE_HELLO_COMMANDS, description, commandListener,
- operationContext, message, bsonOutput,
- COMMAND_PROTOCOL_LOGGER, loggerSettings);
+ return recordEverything || (!isMonitoringConnection && opened() && authenticated.get() && listensOrLogs);
}
private ClusterId getClusterId() {
return description.getConnectionId().getServerId().getClusterId();
}
+
+ /**
+ * Creates a tracing span for the given command message.
+ *
+ * The span is only created if tracing is enabled and the command is not security-sensitive.
+ * It attaches various tags to the span, such as database system, namespace, query summary, opcode,
+ * server address, port, server type, client and server connection IDs, and, if applicable,
+ * transaction number and session ID. For cursor fetching commands, the parent context is retrieved using the cursor ID.
+ * If command payload tracing is enabled, the command document is also attached as a tag.
+ *
+ * @param message the command message to trace
+ * @param operationContext the operation context containing tracing and session information
+ * @param bsonOutput the BSON output used to serialize the command
+ * @return the created {@link Span}, or {@code null} if tracing is not enabled or the command is security-sensitive
+ */
+ @Nullable
+ private Span createTracingSpan(final CommandMessage message, final OperationContext operationContext, final ByteBufferBsonOutput bsonOutput) {
+
+ TracingManager tracingManager = operationContext.getTracingManager();
+ BsonDocument command = message.getCommandDocument(bsonOutput);
+
+ String commandName = command.getFirstKey();
+
+ if (!tracingManager.isEnabled()
+ || SECURITY_SENSITIVE_COMMANDS.contains(commandName)
+ || SECURITY_SENSITIVE_HELLO_COMMANDS.contains(commandName)) {
+ return null;
+ }
+
+ Span operationSpan = operationContext.getTracingSpan();
+ Span span = tracingManager
+ .addSpan(commandName, operationSpan != null ? operationSpan.context() : null);
+
+ if (command.containsKey("getMore")) {
+ long cursorId = command.getInt64("getMore").longValue();
+ span.tagLowCardinality(CURSOR_ID.withValue(String.valueOf(cursorId)));
+ if (operationSpan != null) {
+ operationSpan.tagLowCardinality(CURSOR_ID.withValue(String.valueOf(cursorId)));
+ }
+ }
+
+ tagNamespace(span, operationSpan, message, commandName);
+ tagServerAndConnectionInfo(span, message);
+ tagSessionAndTransactionInfo(span, operationContext);
+
+ return span;
+ }
+
+ private void tagNamespace(final Span span, @Nullable final Span parentSpan, final CommandMessage message, final String commandName) {
+ String namespace;
+ String collection = "";
+ if (parentSpan != null) {
+ MongoNamespace parentNamespace = parentSpan.getNamespace();
+ if (parentNamespace != null) {
+ namespace = parentNamespace.getDatabaseName();
+ collection =
+ MongoNamespace.COMMAND_COLLECTION_NAME.equalsIgnoreCase(parentNamespace.getCollectionName()) ? ""
+ : parentNamespace.getCollectionName();
+ } else {
+ namespace = message.getDatabase();
+ }
+ } else {
+ namespace = message.getDatabase();
+ }
+ String summary = commandName + " " + namespace + (collection.isEmpty() ? "" : "." + collection);
+
+ KeyValues keyValues = KeyValues.of(
+ SYSTEM.withValue("mongodb"),
+ NAMESPACE.withValue(namespace),
+ QUERY_SUMMARY.withValue(summary),
+ COMMAND_NAME.withValue(commandName));
+
+ if (!collection.isEmpty()) {
+ keyValues = keyValues.and(COLLECTION.withValue(collection));
+ }
+ span.tagLowCardinality(keyValues);
+ }
+
+ private void tagServerAndConnectionInfo(final Span span, final CommandMessage message) {
+ span.tagLowCardinality(KeyValues.of(
+ SERVER_ADDRESS.withValue(serverId.getAddress().getHost()),
+ SERVER_PORT.withValue(String.valueOf(serverId.getAddress().getPort())),
+ SERVER_TYPE.withValue(message.getSettings().getServerType().name()),
+ CLIENT_CONNECTION_ID.withValue(String.valueOf(this.description.getConnectionId().getLocalValue())),
+ SERVER_CONNECTION_ID.withValue(String.valueOf(this.description.getConnectionId().getServerValue())),
+ NETWORK_TRANSPORT.withValue(getServerAddress() instanceof UnixServerAddress ? "unix" : "tcp")
+ ));
+ }
+
+ private void tagSessionAndTransactionInfo(final Span span, final OperationContext operationContext) {
+ SessionContext sessionContext = operationContext.getSessionContext();
+ if (sessionContext.hasSession() && !sessionContext.isImplicitSession()) {
+ span.tagLowCardinality(KeyValues.of(
+ TRANSACTION_NUMBER.withValue(String.valueOf(sessionContext.getTransactionNumber())),
+ SESSION_ID.withValue(String.valueOf(sessionContext.getSessionId()
+ .get(sessionContext.getSessionId().getFirstKey())
+ .asBinary().asUuid()))
+ ));
+ }
+ }
}
diff --git a/driver-core/src/main/com/mongodb/internal/connection/LoggingCommandEventSender.java b/driver-core/src/main/com/mongodb/internal/connection/LoggingCommandEventSender.java
index 044a2113fd8..1972e2caaab 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/LoggingCommandEventSender.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/LoggingCommandEventSender.java
@@ -78,7 +78,7 @@ class LoggingCommandEventSender implements CommandEventSender {
@Nullable final CommandListener commandListener,
final OperationContext operationContext,
final CommandMessage message,
- final ByteBufferBsonOutput bsonOutput,
+ final BsonDocument commandDocument,
final StructuredLogger logger,
final LoggerSettings loggerSettings) {
this.description = description;
@@ -88,7 +88,7 @@ class LoggingCommandEventSender implements CommandEventSender {
this.loggerSettings = loggerSettings;
this.startTimeNanos = System.nanoTime();
this.message = message;
- this.commandDocument = message.getCommandDocument(bsonOutput);
+ this.commandDocument = commandDocument;
this.commandName = commandDocument.getFirstKey();
this.redactionRequired = securitySensitiveCommands.contains(commandName)
|| (securitySensitiveHelloCommands.contains(commandName) && commandDocument.containsKey("speculativeAuthenticate"));
diff --git a/driver-core/src/main/com/mongodb/internal/connection/OperationContext.java b/driver-core/src/main/com/mongodb/internal/connection/OperationContext.java
index 7e0de92da1d..b9da8bfc145 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/OperationContext.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/OperationContext.java
@@ -27,6 +27,8 @@
import com.mongodb.internal.TimeoutSettings;
import com.mongodb.internal.VisibleForTesting;
import com.mongodb.internal.session.SessionContext;
+import com.mongodb.tracing.Span;
+import com.mongodb.internal.tracing.TracingManager;
import com.mongodb.lang.Nullable;
import com.mongodb.selector.ServerSelector;
@@ -47,19 +49,28 @@ public class OperationContext {
private final SessionContext sessionContext;
private final RequestContext requestContext;
private final TimeoutContext timeoutContext;
+ private final TracingManager tracingManager;
@Nullable
private final ServerApi serverApi;
@Nullable
private final String operationName;
+ @Nullable
+ private Span tracingSpan;
public OperationContext(final RequestContext requestContext, final SessionContext sessionContext, final TimeoutContext timeoutContext,
@Nullable final ServerApi serverApi) {
- this(requestContext, sessionContext, timeoutContext, serverApi, null);
+ this(requestContext, sessionContext, timeoutContext, TracingManager.NO_OP, serverApi, null);
}
public OperationContext(final RequestContext requestContext, final SessionContext sessionContext, final TimeoutContext timeoutContext,
- @Nullable final ServerApi serverApi, @Nullable final String operationName) {
- this(NEXT_ID.incrementAndGet(), requestContext, sessionContext, timeoutContext, new ServerDeprioritization(), serverApi, operationName);
+ final TracingManager tracingManager,
+ @Nullable final ServerApi serverApi,
+ @Nullable final String operationName) {
+ this(NEXT_ID.incrementAndGet(), requestContext, sessionContext, timeoutContext, new ServerDeprioritization(),
+ tracingManager,
+ serverApi,
+ operationName,
+ null);
}
public static OperationContext simpleOperationContext(
@@ -68,8 +79,10 @@ public static OperationContext simpleOperationContext(
IgnorableRequestContext.INSTANCE,
NoOpSessionContext.INSTANCE,
new TimeoutContext(timeoutSettings),
+ TracingManager.NO_OP,
serverApi,
- null);
+ null
+ );
}
public static OperationContext simpleOperationContext(final TimeoutContext timeoutContext) {
@@ -77,26 +90,34 @@ public static OperationContext simpleOperationContext(final TimeoutContext timeo
IgnorableRequestContext.INSTANCE,
NoOpSessionContext.INSTANCE,
timeoutContext,
+ TracingManager.NO_OP,
null,
null);
}
public OperationContext withSessionContext(final SessionContext sessionContext) {
- return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, serverApi, operationName);
+ return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, tracingManager, serverApi,
+ operationName, tracingSpan);
}
public OperationContext withTimeoutContext(final TimeoutContext timeoutContext) {
- return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, serverApi, operationName);
+ return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, tracingManager, serverApi,
+ operationName, tracingSpan);
}
public OperationContext withOperationName(final String operationName) {
- return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, serverApi, operationName);
+ return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, tracingManager, serverApi,
+ operationName, tracingSpan);
}
public long getId() {
return id;
}
+ public TracingManager getTracingManager() {
+ return tracingManager;
+ }
+
public SessionContext getSessionContext() {
return sessionContext;
}
@@ -119,37 +140,54 @@ public String getOperationName() {
return operationName;
}
+ @Nullable
+ public Span getTracingSpan() {
+ return tracingSpan;
+ }
+
+ public void setTracingSpan(final Span tracingSpan) {
+ this.tracingSpan = tracingSpan;
+ }
+
@VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE)
public OperationContext(final long id,
- final RequestContext requestContext,
- final SessionContext sessionContext,
- final TimeoutContext timeoutContext,
- final ServerDeprioritization serverDeprioritization,
- @Nullable final ServerApi serverApi,
- @Nullable final String operationName) {
+ final RequestContext requestContext,
+ final SessionContext sessionContext,
+ final TimeoutContext timeoutContext,
+ final ServerDeprioritization serverDeprioritization,
+ final TracingManager tracingManager,
+ @Nullable final ServerApi serverApi,
+ @Nullable final String operationName,
+ @Nullable final Span tracingSpan) {
+
this.id = id;
this.serverDeprioritization = serverDeprioritization;
this.requestContext = requestContext;
this.sessionContext = sessionContext;
this.timeoutContext = timeoutContext;
+ this.tracingManager = tracingManager;
this.serverApi = serverApi;
this.operationName = operationName;
+ this.tracingSpan = tracingSpan;
}
@VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE)
public OperationContext(final long id,
- final RequestContext requestContext,
- final SessionContext sessionContext,
- final TimeoutContext timeoutContext,
- @Nullable final ServerApi serverApi,
- @Nullable final String operationName) {
+ final RequestContext requestContext,
+ final SessionContext sessionContext,
+ final TimeoutContext timeoutContext,
+ final TracingManager tracingManager,
+ @Nullable final ServerApi serverApi,
+ @Nullable final String operationName) {
this.id = id;
this.serverDeprioritization = new ServerDeprioritization();
this.requestContext = requestContext;
this.sessionContext = sessionContext;
this.timeoutContext = timeoutContext;
+ this.tracingManager = tracingManager;
this.serverApi = serverApi;
this.operationName = operationName;
+ this.tracingSpan = null;
}
diff --git a/driver-core/src/main/com/mongodb/internal/operation/AbortTransactionOperation.java b/driver-core/src/main/com/mongodb/internal/operation/AbortTransactionOperation.java
index bc7e6655bc7..4f48722747e 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/AbortTransactionOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/AbortTransactionOperation.java
@@ -17,6 +17,7 @@
package com.mongodb.internal.operation;
import com.mongodb.Function;
+import com.mongodb.MongoNamespace;
import com.mongodb.WriteConcern;
import com.mongodb.internal.TimeoutContext;
import com.mongodb.lang.Nullable;
@@ -48,6 +49,11 @@ public String getCommandName() {
return COMMAND_NAME;
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return new MongoNamespace("admin", MongoNamespace.COMMAND_COLLECTION_NAME); // TODO double check
+ }
+
@Override
CommandCreator getCommandCreator() {
return (operationContext, serverDescription, connectionDescription) -> {
diff --git a/driver-core/src/main/com/mongodb/internal/operation/AbstractWriteSearchIndexOperation.java b/driver-core/src/main/com/mongodb/internal/operation/AbstractWriteSearchIndexOperation.java
index e6643f3c7d2..e513abde505 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/AbstractWriteSearchIndexOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/AbstractWriteSearchIndexOperation.java
@@ -96,7 +96,9 @@ void swallowOrThrow(@Nullable final E mongoExecutionExcept
abstract BsonDocument buildCommand();
- MongoNamespace getNamespace() {
+
+ @Override
+ public MongoNamespace getNamespace() {
return namespace;
}
}
diff --git a/driver-core/src/main/com/mongodb/internal/operation/AggregateOperation.java b/driver-core/src/main/com/mongodb/internal/operation/AggregateOperation.java
index 1c9abfc68ca..55c5c231a21 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/AggregateOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/AggregateOperation.java
@@ -164,7 +164,8 @@ CommandReadOperation createExplainableOperation(@Nullable final ExplainVe
}, resultDecoder);
}
- MongoNamespace getNamespace() {
+ @Override
+ public MongoNamespace getNamespace() {
return wrapped.getNamespace();
}
}
diff --git a/driver-core/src/main/com/mongodb/internal/operation/AggregateOperationImpl.java b/driver-core/src/main/com/mongodb/internal/operation/AggregateOperationImpl.java
index 4c9bc3828b7..e2e8d6fb426 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/AggregateOperationImpl.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/AggregateOperationImpl.java
@@ -92,10 +92,6 @@ class AggregateOperationImpl implements ReadOperationCursor {
this.pipelineCreator = notNull("pipelineCreator", pipelineCreator);
}
- MongoNamespace getNamespace() {
- return namespace;
- }
-
List getPipeline() {
return pipeline;
}
@@ -191,6 +187,11 @@ public String getCommandName() {
return COMMAND_NAME;
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return namespace;
+ }
+
@Override
public BatchCursor execute(final ReadBinding binding) {
return executeRetryableRead(binding, namespace.getDatabaseName(),
diff --git a/driver-core/src/main/com/mongodb/internal/operation/AggregateToCollectionOperation.java b/driver-core/src/main/com/mongodb/internal/operation/AggregateToCollectionOperation.java
index 16f33ad45e5..296b4eabb88 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/AggregateToCollectionOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/AggregateToCollectionOperation.java
@@ -157,6 +157,11 @@ public String getCommandName() {
return COMMAND_NAME;
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return namespace;
+ }
+
@Override
public Void execute(final ReadBinding binding) {
return executeRetryableRead(binding,
diff --git a/driver-core/src/main/com/mongodb/internal/operation/BaseFindAndModifyOperation.java b/driver-core/src/main/com/mongodb/internal/operation/BaseFindAndModifyOperation.java
index c5d56fda81c..8f66333eb02 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/BaseFindAndModifyOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/BaseFindAndModifyOperation.java
@@ -92,6 +92,7 @@ public void executeAsync(final AsyncWriteBinding binding, final SingleResultCall
FindAndModifyHelper.asyncTransformer(), cmd -> cmd, callback);
}
+ @Override
public MongoNamespace getNamespace() {
return namespace;
}
diff --git a/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java b/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java
index 2b9e79f6f06..c94fcc04783 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java
@@ -115,6 +115,7 @@
import java.util.function.Supplier;
import java.util.stream.Stream;
+import static com.mongodb.MongoNamespace.COMMAND_COLLECTION_NAME;
import static com.mongodb.assertions.Assertions.assertFalse;
import static com.mongodb.assertions.Assertions.assertNotNull;
import static com.mongodb.assertions.Assertions.assertTrue;
@@ -182,6 +183,12 @@ public String getCommandName() {
return "bulkWrite";
}
+ @Override
+ public MongoNamespace getNamespace() {
+ // The bulkWrite command is executed on the "admin" database.
+ return new MongoNamespace("admin", COMMAND_COLLECTION_NAME);
+ }
+
@Override
public ClientBulkWriteResult execute(final WriteBinding binding) throws ClientBulkWriteException {
WriteConcern effectiveWriteConcern = validateAndGetEffectiveWriteConcern(binding.getOperationContext().getSessionContext());
diff --git a/driver-core/src/main/com/mongodb/internal/operation/CommandReadOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CommandReadOperation.java
index 6965bfc34a3..0fbc6eb06e9 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/CommandReadOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/CommandReadOperation.java
@@ -16,12 +16,14 @@
package com.mongodb.internal.operation;
+import com.mongodb.MongoNamespace;
import com.mongodb.internal.async.SingleResultCallback;
import com.mongodb.internal.binding.AsyncReadBinding;
import com.mongodb.internal.binding.ReadBinding;
import org.bson.BsonDocument;
import org.bson.codecs.Decoder;
+import static com.mongodb.MongoNamespace.COMMAND_COLLECTION_NAME;
import static com.mongodb.assertions.Assertions.notNull;
import static com.mongodb.internal.operation.AsyncOperationHelper.executeRetryableReadAsync;
import static com.mongodb.internal.operation.CommandOperationHelper.CommandCreator;
@@ -55,6 +57,11 @@ public String getCommandName() {
return commandName;
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return new MongoNamespace(databaseName, COMMAND_COLLECTION_NAME);
+ }
+
@Override
public T execute(final ReadBinding binding) {
return executeRetryableRead(binding, databaseName, commandCreator, decoder,
diff --git a/driver-core/src/main/com/mongodb/internal/operation/CommitTransactionOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CommitTransactionOperation.java
index 998a002f348..97e62ffceac 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/CommitTransactionOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/CommitTransactionOperation.java
@@ -19,6 +19,7 @@
import com.mongodb.Function;
import com.mongodb.MongoException;
import com.mongodb.MongoExecutionTimeoutException;
+import com.mongodb.MongoNamespace;
import com.mongodb.MongoNodeIsRecoveringException;
import com.mongodb.MongoNotPrimaryException;
import com.mongodb.MongoSocketException;
@@ -116,6 +117,11 @@ public String getCommandName() {
return COMMAND_NAME;
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return new MongoNamespace("admin", MongoNamespace.COMMAND_COLLECTION_NAME);
+ }
+
@Override
CommandCreator getCommandCreator() {
CommandCreator creator = (operationContext, serverDescription, connectionDescription) -> {
diff --git a/driver-core/src/main/com/mongodb/internal/operation/CountDocumentsOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CountDocumentsOperation.java
index 9460026062a..157d3660904 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/CountDocumentsOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/CountDocumentsOperation.java
@@ -125,6 +125,11 @@ public String getCommandName() {
return COMMAND_NAME;
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return namespace;
+ }
+
@Override
public Long execute(final ReadBinding binding) {
try (BatchCursor cursor = getAggregateOperation().execute(binding)) {
diff --git a/driver-core/src/main/com/mongodb/internal/operation/CountOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CountOperation.java
index 6d0b7b78f93..d38a7c11333 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/CountOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/CountOperation.java
@@ -115,6 +115,11 @@ public String getCommandName() {
return COMMAND_NAME;
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return namespace;
+ }
+
@Override
public Long execute(final ReadBinding binding) {
return executeRetryableRead(binding, namespace.getDatabaseName(),
diff --git a/driver-core/src/main/com/mongodb/internal/operation/CreateCollectionOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CreateCollectionOperation.java
index 5284076eecb..d8e757054c0 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/CreateCollectionOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/CreateCollectionOperation.java
@@ -18,6 +18,7 @@
import com.mongodb.MongoClientException;
import com.mongodb.MongoException;
+import com.mongodb.MongoNamespace;
import com.mongodb.WriteConcern;
import com.mongodb.client.model.ChangeStreamPreAndPostImagesOptions;
import com.mongodb.client.model.Collation;
@@ -236,6 +237,11 @@ public String getCommandName() {
return "createCollection";
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return new MongoNamespace(databaseName, collectionName);
+ }
+
@Override
public Void execute(final WriteBinding binding) {
return withConnection(binding, connection -> {
diff --git a/driver-core/src/main/com/mongodb/internal/operation/CreateIndexesOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CreateIndexesOperation.java
index b9b4242a3f4..7e634f136e2 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/CreateIndexesOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/CreateIndexesOperation.java
@@ -105,6 +105,11 @@ public String getCommandName() {
return COMMAND_NAME;
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return namespace;
+ }
+
@Override
public Void execute(final WriteBinding binding) {
try {
diff --git a/driver-core/src/main/com/mongodb/internal/operation/CreateViewOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CreateViewOperation.java
index 49b47fb7e9c..61fd58d5a0f 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/CreateViewOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/CreateViewOperation.java
@@ -16,6 +16,7 @@
package com.mongodb.internal.operation;
+import com.mongodb.MongoNamespace;
import com.mongodb.WriteConcern;
import com.mongodb.client.model.Collation;
import com.mongodb.internal.async.SingleResultCallback;
@@ -128,6 +129,11 @@ public String getCommandName() {
return "createView";
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return new MongoNamespace(databaseName, viewName);
+ }
+
@Override
public Void execute(final WriteBinding binding) {
return withConnection(binding, connection -> {
diff --git a/driver-core/src/main/com/mongodb/internal/operation/DistinctOperation.java b/driver-core/src/main/com/mongodb/internal/operation/DistinctOperation.java
index 489e3923bdc..10c4c320100 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/DistinctOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/DistinctOperation.java
@@ -113,6 +113,11 @@ public String getCommandName() {
return COMMAND_NAME;
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return namespace;
+ }
+
@Override
public BatchCursor execute(final ReadBinding binding) {
return executeRetryableRead(binding, namespace.getDatabaseName(), getCommandCreator(), createCommandDecoder(),
diff --git a/driver-core/src/main/com/mongodb/internal/operation/DropCollectionOperation.java b/driver-core/src/main/com/mongodb/internal/operation/DropCollectionOperation.java
index 2926fdec799..6b4c9712c01 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/DropCollectionOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/DropCollectionOperation.java
@@ -91,6 +91,11 @@ public String getCommandName() {
return "dropCollection";
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return namespace;
+ }
+
@Override
public Void execute(final WriteBinding binding) {
BsonDocument localEncryptedFields = getEncryptedFields((ReadWriteBinding) binding);
diff --git a/driver-core/src/main/com/mongodb/internal/operation/DropDatabaseOperation.java b/driver-core/src/main/com/mongodb/internal/operation/DropDatabaseOperation.java
index d619176e8a3..2ee963923fe 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/DropDatabaseOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/DropDatabaseOperation.java
@@ -16,6 +16,7 @@
package com.mongodb.internal.operation;
+import com.mongodb.MongoNamespace;
import com.mongodb.WriteConcern;
import com.mongodb.internal.async.SingleResultCallback;
import com.mongodb.internal.binding.AsyncWriteBinding;
@@ -60,6 +61,11 @@ public String getCommandName() {
return "dropDatabase";
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return new MongoNamespace(databaseName, MongoNamespace.COMMAND_COLLECTION_NAME);
+ }
+
@Override
public Void execute(final WriteBinding binding) {
return withConnection(binding, connection -> {
diff --git a/driver-core/src/main/com/mongodb/internal/operation/DropIndexOperation.java b/driver-core/src/main/com/mongodb/internal/operation/DropIndexOperation.java
index 3671a90aa56..8a3a66e3c50 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/DropIndexOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/DropIndexOperation.java
@@ -70,6 +70,11 @@ public String getCommandName() {
return COMMAND_NAME;
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return namespace;
+ }
+
@Override
public Void execute(final WriteBinding binding) {
try {
diff --git a/driver-core/src/main/com/mongodb/internal/operation/EstimatedDocumentCountOperation.java b/driver-core/src/main/com/mongodb/internal/operation/EstimatedDocumentCountOperation.java
index 427cd40dc40..6308aae56ea 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/EstimatedDocumentCountOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/EstimatedDocumentCountOperation.java
@@ -75,6 +75,11 @@ public String getCommandName() {
return COMMAND_NAME;
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return namespace;
+ }
+
@Override
public Long execute(final ReadBinding binding) {
try {
diff --git a/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java b/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java
index 4e1de40d150..e268c1dd8ce 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java
@@ -99,6 +99,7 @@ public FindOperation(final MongoNamespace namespace, final Decoder decoder) {
this.decoder = notNull("decoder", decoder);
}
+ @Override
public MongoNamespace getNamespace() {
return namespace;
}
@@ -468,9 +469,12 @@ private TimeoutMode getTimeoutMode() {
}
private CommandReadTransformer> transformer() {
- return (result, source, connection) ->
- new CommandBatchCursor<>(getTimeoutMode(), result, batchSize, getMaxTimeForCursor(source.getOperationContext()), decoder,
- comment, source, connection);
+ return (result, source, connection) -> {
+ OperationContext operationContext = source.getOperationContext();
+
+ return new CommandBatchCursor<>(getTimeoutMode(), result, batchSize, getMaxTimeForCursor(operationContext), decoder,
+ comment, source, connection);
+ };
}
private CommandReadTransformerAsync> asyncTransformer() {
diff --git a/driver-core/src/main/com/mongodb/internal/operation/ListCollectionsOperation.java b/driver-core/src/main/com/mongodb/internal/operation/ListCollectionsOperation.java
index 8740986b23f..da3966b26de 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/ListCollectionsOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/ListCollectionsOperation.java
@@ -17,6 +17,7 @@
package com.mongodb.internal.operation;
import com.mongodb.MongoCommandException;
+import com.mongodb.MongoNamespace;
import com.mongodb.client.cursor.TimeoutMode;
import com.mongodb.internal.VisibleForTesting;
import com.mongodb.internal.async.AsyncBatchCursor;
@@ -34,6 +35,7 @@
import java.util.function.Supplier;
+import static com.mongodb.MongoNamespace.COMMAND_COLLECTION_NAME;
import static com.mongodb.assertions.Assertions.notNull;
import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE;
import static com.mongodb.internal.async.ErrorHandlingResultCallback.errorHandlingCallback;
@@ -86,6 +88,11 @@ public ListCollectionsOperation(final String databaseName, final Decoder deco
this.decoder = notNull("decoder", decoder);
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return new MongoNamespace(databaseName, COMMAND_COLLECTION_NAME);
+ }
+
public BsonDocument getFilter() {
return filter;
}
diff --git a/driver-core/src/main/com/mongodb/internal/operation/ListDatabasesOperation.java b/driver-core/src/main/com/mongodb/internal/operation/ListDatabasesOperation.java
index 4787153190b..d51194406b6 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/ListDatabasesOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/ListDatabasesOperation.java
@@ -16,6 +16,7 @@
package com.mongodb.internal.operation;
+import com.mongodb.MongoNamespace;
import com.mongodb.internal.async.AsyncBatchCursor;
import com.mongodb.internal.async.SingleResultCallback;
import com.mongodb.internal.binding.AsyncReadBinding;
@@ -26,6 +27,7 @@
import org.bson.BsonValue;
import org.bson.codecs.Decoder;
+import static com.mongodb.MongoNamespace.COMMAND_COLLECTION_NAME;
import static com.mongodb.assertions.Assertions.notNull;
import static com.mongodb.internal.async.ErrorHandlingResultCallback.errorHandlingCallback;
import static com.mongodb.internal.operation.AsyncOperationHelper.asyncSingleBatchCursorTransformer;
@@ -107,6 +109,11 @@ public String getCommandName() {
return COMMAND_NAME;
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return new MongoNamespace("admin", COMMAND_COLLECTION_NAME);
+ }
+
@Override
public BatchCursor execute(final ReadBinding binding) {
return executeRetryableRead(binding, "admin", getCommandCreator(), CommandResultDocumentCodec.create(decoder, DATABASES),
diff --git a/driver-core/src/main/com/mongodb/internal/operation/ListIndexesOperation.java b/driver-core/src/main/com/mongodb/internal/operation/ListIndexesOperation.java
index a97acd64d58..76900ab296e 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/ListIndexesOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/ListIndexesOperation.java
@@ -122,6 +122,11 @@ public String getCommandName() {
return COMMAND_NAME;
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return namespace;
+ }
+
@Override
public BatchCursor execute(final ReadBinding binding) {
RetryState retryState = initialRetryState(retryReads, binding.getOperationContext().getTimeoutContext());
diff --git a/driver-core/src/main/com/mongodb/internal/operation/ListSearchIndexesOperation.java b/driver-core/src/main/com/mongodb/internal/operation/ListSearchIndexesOperation.java
index 7fadead0b57..3c78297463e 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/ListSearchIndexesOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/ListSearchIndexesOperation.java
@@ -78,6 +78,11 @@ public String getCommandName() {
return COMMAND_NAME;
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return namespace;
+ }
+
@Override
public BatchCursor execute(final ReadBinding binding) {
try {
diff --git a/driver-core/src/main/com/mongodb/internal/operation/MapReduceToCollectionOperation.java b/driver-core/src/main/com/mongodb/internal/operation/MapReduceToCollectionOperation.java
index bfcc73a5aa6..96f5a8418d0 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/MapReduceToCollectionOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/MapReduceToCollectionOperation.java
@@ -87,6 +87,7 @@ public MapReduceToCollectionOperation(final MongoNamespace namespace, final Bson
this.writeConcern = writeConcern;
}
+ @Override
public MongoNamespace getNamespace() {
return namespace;
}
diff --git a/driver-core/src/main/com/mongodb/internal/operation/MapReduceWithInlineResultsOperation.java b/driver-core/src/main/com/mongodb/internal/operation/MapReduceWithInlineResultsOperation.java
index 6661c2a5c77..abbd2fc6ae8 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/MapReduceWithInlineResultsOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/MapReduceWithInlineResultsOperation.java
@@ -76,6 +76,7 @@ public MapReduceWithInlineResultsOperation(final MongoNamespace namespace, final
this.decoder = notNull("decoder", decoder);
}
+ @Override
public MongoNamespace getNamespace() {
return namespace;
}
diff --git a/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java b/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java
index 39ff2dab17f..b17a3bae30b 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java
@@ -101,6 +101,7 @@ public MixedBulkWriteOperation(final MongoNamespace namespace, final List exte
this.retryWrites = retryWrites;
}
+ @Override
public MongoNamespace getNamespace() {
return namespace;
}
diff --git a/driver-core/src/main/com/mongodb/internal/operation/ReadOperation.java b/driver-core/src/main/com/mongodb/internal/operation/ReadOperation.java
index 6a90d490b30..2e198381cf0 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/ReadOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/ReadOperation.java
@@ -16,6 +16,7 @@
package com.mongodb.internal.operation;
+import com.mongodb.MongoNamespace;
import com.mongodb.internal.async.SingleResultCallback;
import com.mongodb.internal.binding.AsyncReadBinding;
import com.mongodb.internal.binding.ReadBinding;
@@ -32,6 +33,11 @@ public interface ReadOperation {
*/
String getCommandName();
+ /**
+ * @return the namespace of the operation
+ */
+ MongoNamespace getNamespace();
+
/**
* General execute which can return anything of type T
*
diff --git a/driver-core/src/main/com/mongodb/internal/operation/RenameCollectionOperation.java b/driver-core/src/main/com/mongodb/internal/operation/RenameCollectionOperation.java
index ea477bf67bd..81d3b0bffe9 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/RenameCollectionOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/RenameCollectionOperation.java
@@ -79,6 +79,11 @@ public String getCommandName() {
return COMMAND_NAME;
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return originalNamespace;
+ }
+
@Override
public Void execute(final WriteBinding binding) {
return withConnection(binding, connection -> executeCommand(binding, "admin", getCommand(), connection,
diff --git a/driver-core/src/main/com/mongodb/internal/operation/WriteOperation.java b/driver-core/src/main/com/mongodb/internal/operation/WriteOperation.java
index 73cec2f416b..b4e2b4a25b4 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/WriteOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/WriteOperation.java
@@ -16,6 +16,7 @@
package com.mongodb.internal.operation;
+import com.mongodb.MongoNamespace;
import com.mongodb.internal.async.SingleResultCallback;
import com.mongodb.internal.binding.AsyncWriteBinding;
import com.mongodb.internal.binding.WriteBinding;
@@ -32,6 +33,11 @@ public interface WriteOperation {
*/
String getCommandName();
+ /**
+ * @return the namespace of the operation
+ */
+ MongoNamespace getNamespace();
+
/**
* General execute which can return anything of type T
*
diff --git a/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java b/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java
new file mode 100644
index 00000000000..84cf91decd8
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java
@@ -0,0 +1,113 @@
+/*
+ * 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.tracing;
+
+import com.mongodb.MongoNamespace;
+import com.mongodb.lang.Nullable;
+import com.mongodb.tracing.Span;
+import com.mongodb.tracing.TraceContext;
+import com.mongodb.tracing.Tracer;
+
+import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.SYSTEM;
+
+/**
+ * Manages tracing spans for MongoDB driver activities.
+ *
+ * This class provides methods to create and manage spans for commands, operations and transactions.
+ * It integrates with a {@link Tracer} to propagate tracing information and record telemetry.
+ *
+ */
+public class TracingManager {
+ /**
+ * A no-op instance of the TracingManager used when tracing is disabled.
+ */
+ public static final TracingManager NO_OP = new TracingManager(Tracer.NO_OP);
+ private final Tracer tracer;
+ private final boolean enableCommandPayload;
+
+ /**
+ * Constructs a new TracingManager with the specified tracer and parent context.
+ * Setting the environment variable {@code OTEL_JAVA_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH} will enable command payload tracing.
+ *
+ * @param tracer The tracer to use for tracing operations.
+ */
+ public TracingManager(final Tracer tracer) {
+ this.tracer = tracer;
+ this.enableCommandPayload = tracer.includeCommandPayload();
+ }
+
+ /**
+ * Creates a new span with the specified name and parent trace context.
+ *
+ * This method is used to create a span that is linked to a parent context,
+ * enabling hierarchical tracing of operations.
+ *
+ *
+ * @param name The name of the span.
+ * @param parentContext The parent trace context to associate with the span.
+ * @return The created span.
+ */
+ public Span addSpan(final String name, @Nullable final TraceContext parentContext) {
+ return tracer.nextSpan(name, parentContext, null);
+ }
+
+ /**
+ * Creates a new span with the specified name, parent trace context, and MongoDB namespace.
+ *
+ * This method is used to create a span that is linked to a parent context,
+ * enabling hierarchical tracing of operations. The MongoDB namespace can be used
+ * by nested spans to access the database and collection name (which might not be easily accessible at connection layer).
+ *
+ *
+ * @param name The name of the span.
+ * @param parentContext The parent trace context to associate with the span.
+ * @param namespace The MongoDB namespace associated with the operation.
+ * @return The created span.
+ */
+ public Span addSpan(final String name, @Nullable final TraceContext parentContext, final MongoNamespace namespace) {
+ return tracer.nextSpan(name, parentContext, namespace);
+ }
+
+ /**
+ * Creates a new transaction span for the specified server session.
+ *
+ * @return The created transaction span.
+ */
+ public Span addTransactionSpan() {
+ Span span = tracer.nextSpan("transaction", null, null);
+ span.tagLowCardinality(SYSTEM.withValue("mongodb"));
+ return span;
+ }
+
+ /**
+ * Checks whether tracing is enabled.
+ *
+ * @return True if tracing is enabled, false otherwise.
+ */
+ public boolean isEnabled() {
+ return tracer.enabled();
+ }
+
+ /**
+ * Checks whether command payload tracing is enabled.
+ *
+ * @return True if command payload tracing is enabled, false otherwise.
+ */
+ public boolean isCommandPayloadEnabled() {
+ return enableCommandPayload;
+ }
+}
diff --git a/driver-core/src/main/com/mongodb/internal/tracing/TransactionSpan.java b/driver-core/src/main/com/mongodb/internal/tracing/TransactionSpan.java
new file mode 100644
index 00000000000..d3133a8238b
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/tracing/TransactionSpan.java
@@ -0,0 +1,114 @@
+/*
+ * 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.tracing;
+
+import com.mongodb.lang.Nullable;
+import com.mongodb.tracing.Span;
+import com.mongodb.tracing.TraceContext;
+
+/**
+ * State class for transaction tracing.
+ *
+ * @since 5.7
+ */
+public class TransactionSpan {
+ private boolean isConvenientTransaction = false;
+ private final Span span;
+ @Nullable
+ private Throwable reportedError;
+
+ public TransactionSpan(final TracingManager tracingManager) {
+ this.span = tracingManager.addTransactionSpan();
+ }
+
+ /**
+ * Handles a transaction span error.
+ *
+ * If the transaction is convenient, the error is reported as an event. This is done since
+ * the error is not fatal and the transaction may be retried.
+ *
+ * If the transaction is not convenient, the error is reported as a span error and the
+ * transaction context is cleaned up.
+ *
+ * @param e The error to report.
+ */
+ public void handleTransactionSpanError(final Throwable e) {
+ if (isConvenientTransaction) {
+ // report error as event (since subsequent retries might succeed, also keep track of the last event
+ span.event(e.toString());
+ reportedError = e;
+ } else {
+ span.error(e);
+ }
+
+ if (!isConvenientTransaction) {
+ span.end();
+ }
+ }
+
+ /**
+ * Finalizes the transaction span by logging the specified status as an event and ending the span.
+ *
+ * @param status The status to log as an event.
+ */
+ public void finalizeTransactionSpan(final String status) {
+ span.event(status);
+ // clear previous commit error if any
+ if (!isConvenientTransaction) {
+ span.end();
+ }
+ reportedError = null; // clear previous commit error if any
+ }
+
+ /**
+ * Finalizes the transaction span by logging any last span event as an error and ending the span.
+ * Optionally cleans up the transaction context if specified.
+ *
+ * @param cleanupTransactionContext A boolean indicating whether to clean up the transaction context.
+ */
+ public void spanFinalizing(final boolean cleanupTransactionContext) {
+ if (reportedError != null) {
+ span.error(reportedError);
+ }
+ span.end();
+ reportedError = null;
+ // Don't clean up transaction context if we're still retrying (we want the retries to fold under the original transaction span)
+ if (cleanupTransactionContext) {
+ isConvenientTransaction = false;
+ }
+ }
+
+ /**
+ * Indicates that the transaction is a convenient transaction.
+ *
+ * This has an impact on how the transaction span is handled. If the transaction is convenient, any errors that occur
+ * during the transaction are reported as events. If the transaction is not convenient, errors are reported as span
+ * errors and the transaction context is cleaned up.
+ */
+ public void setIsConvenientTransaction() {
+ this.isConvenientTransaction = true;
+ }
+
+ /**
+ * Retrieves the trace context associated with the transaction span.
+ *
+ * @return The trace context associated with the transaction span.
+ */
+ public TraceContext getContext() {
+ return span.context();
+ }
+}
diff --git a/driver-core/src/main/com/mongodb/internal/tracing/package-info.java b/driver-core/src/main/com/mongodb/internal/tracing/package-info.java
new file mode 100644
index 00000000000..e7dd3311143
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/tracing/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+/**
+ * Contains classes related to tracing
+ */
+@NonNullApi
+package com.mongodb.internal.tracing;
+
+import com.mongodb.lang.NonNullApi;
diff --git a/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java b/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java
new file mode 100644
index 00000000000..30d81351ef3
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java
@@ -0,0 +1,191 @@
+/*
+ * 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.tracing;
+
+import com.mongodb.MongoNamespace;
+import com.mongodb.lang.Nullable;
+import io.micrometer.common.KeyValue;
+import io.micrometer.common.KeyValues;
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationRegistry;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.EXCEPTION_MESSAGE;
+import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.EXCEPTION_STACKTRACE;
+import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.EXCEPTION_TYPE;
+import static com.mongodb.tracing.MongodbObservation.MONGODB_OBSERVATION;
+
+
+/**
+ * A {@link Tracer} implementation that delegates tracing operations to a Micrometer {@link io.micrometer.observation.ObservationRegistry}.
+ *
+ * This class enables integration of MongoDB driver tracing with Micrometer-based tracing systems.
+ * It provides integration with Micrometer to propagate observations into tracing API.
+ *
+ *
+ * @since 5.7
+ */
+public class MicrometerTracer implements Tracer {
+ private final boolean allowCommandPayload;
+ private final ObservationRegistry observationRegistry;
+
+ /**
+ * Constructs a new {@link MicrometerTracer} instance.
+ *
+ * @param observationRegistry The Micrometer {@link ObservationRegistry} to delegate tracing operations to.
+ */
+ public MicrometerTracer(final ObservationRegistry observationRegistry) {
+ this(observationRegistry, false);
+ }
+
+ /**
+ * Constructs a new {@link MicrometerTracer} instance with an option to allow command payloads.
+ *
+ * @param observationRegistry The Micrometer {@link ObservationRegistry} to delegate tracing operations to.
+ * @param allowCommandPayload Whether to allow command payloads in the trace context.
+ */
+ public MicrometerTracer(final ObservationRegistry observationRegistry, final boolean allowCommandPayload) {
+ this.allowCommandPayload = allowCommandPayload;
+ this.observationRegistry = observationRegistry;
+ }
+
+ @Override
+ public Span nextSpan(final String name, @Nullable final TraceContext parent, @Nullable final MongoNamespace namespace) {
+ if (parent instanceof MicrometerTraceContext) {
+ Observation parentObservation = ((MicrometerTraceContext) parent).observation;
+ if (parentObservation != null) {
+ return new MicrometerSpan(MONGODB_OBSERVATION
+ .observation(observationRegistry)
+ .contextualName(name)
+ .parentObservation(parentObservation)
+ .start(), namespace);
+ }
+ }
+ return new MicrometerSpan(MONGODB_OBSERVATION.observation(observationRegistry).contextualName(name).start(), namespace);
+ }
+
+ @Override
+ public boolean enabled() {
+ return true;
+ }
+
+ @Override
+ public boolean includeCommandPayload() {
+ return allowCommandPayload;
+ }
+
+ /**
+ * Represents a Micrometer-based trace context.
+ */
+ private static class MicrometerTraceContext implements TraceContext {
+ private final Observation observation;
+
+ /**
+ * Constructs a new {@link MicrometerTraceContext} instance with an associated Observation.
+ *
+ * @param observation The Micrometer {@link Observation}, or null if none exists.
+ */
+ MicrometerTraceContext(@Nullable final Observation observation) {
+ this.observation = observation;
+ }
+ }
+
+ /**
+ * Represents a Micrometer-based span.
+ */
+ private static class MicrometerSpan implements Span {
+ private final Observation observation;
+ @Nullable
+ private final MongoNamespace namespace;
+
+ /**
+ * Constructs a new {@link MicrometerSpan} instance with an associated Observation.
+ *
+ * @param observation The Micrometer {@link Observation}, or null if none exists.
+ */
+ MicrometerSpan(final Observation observation) {
+ this.observation = observation;
+ this.namespace = null;
+ }
+
+ /**
+ * Constructs a new {@link MicrometerSpan} instance with an associated Observation and MongoDB namespace.
+ *
+ * @param observation The Micrometer {@link Observation}, or null if none exists.
+ * @param namespace The MongoDB namespace associated with the span.
+ */
+ MicrometerSpan(final Observation observation, @Nullable final MongoNamespace namespace) {
+ this.namespace = namespace;
+ this.observation = observation;
+ }
+
+ @Override
+ public void tagLowCardinality(final KeyValue keyValue) {
+ observation.lowCardinalityKeyValue(keyValue);
+ }
+
+ @Override
+ public void tagLowCardinality(final KeyValues keyValues) {
+ observation.lowCardinalityKeyValues(keyValues);
+ }
+
+ @Override
+ public void tagHighCardinality(final KeyValue keyValue) {
+ observation.highCardinalityKeyValue(keyValue);
+ }
+
+ @Override
+ public void event(final String event) {
+ observation.event(() -> event);
+ }
+
+ @Override
+ public void error(final Throwable throwable) {
+ observation.lowCardinalityKeyValues(KeyValues.of(
+ EXCEPTION_MESSAGE.withValue(throwable.getMessage()),
+ EXCEPTION_TYPE.withValue(throwable.getClass().getName()),
+ EXCEPTION_STACKTRACE.withValue(getStackTraceAsString(throwable))
+ ));
+ observation.error(throwable);
+ }
+
+ @Override
+ public void end() {
+ observation.stop();
+ }
+
+ @Override
+ public TraceContext context() {
+ return new MicrometerTraceContext(observation);
+ }
+
+ @Override
+ @Nullable
+ public MongoNamespace getNamespace() {
+ return namespace;
+ }
+
+ private String getStackTraceAsString(final Throwable throwable) {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ throwable.printStackTrace(pw);
+ return sw.toString();
+ }
+ }
+}
diff --git a/driver-core/src/main/com/mongodb/tracing/MongodbObservation.java b/driver-core/src/main/com/mongodb/tracing/MongodbObservation.java
new file mode 100644
index 00000000000..3a8ee8a867e
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/tracing/MongodbObservation.java
@@ -0,0 +1,237 @@
+/*
+ * 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.tracing;
+
+import io.micrometer.common.KeyValue;
+import io.micrometer.common.docs.KeyName;
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.docs.ObservationDocumentation;
+import org.bson.BsonDocument;
+import org.bson.BsonReader;
+import org.bson.json.JsonMode;
+import org.bson.json.JsonWriter;
+import org.bson.json.JsonWriterSettings;
+
+import java.io.StringWriter;
+
+import static java.lang.System.getenv;
+
+/**
+ * A MongoDB-based {@link Observation}.
+ *
+ * @since 5.7
+ */
+public enum MongodbObservation implements ObservationDocumentation {
+
+ MONGODB_OBSERVATION {
+ @Override
+ public String getName() {
+ return "mongodb";
+ }
+
+ @Override
+ public KeyName[] getLowCardinalityKeyNames() {
+ return LowCardinalityKeyNames.values();
+ }
+
+ @Override
+ public KeyName[] getHighCardinalityKeyNames() {
+ return HighCardinalityKeyNames.values();
+ }
+
+ };
+
+ /**
+ * Enums related to low cardinality key names for MongoDB tags.
+ */
+ public enum LowCardinalityKeyNames implements KeyName {
+
+ SYSTEM {
+ @Override
+ public String asString() {
+ return "db.system";
+ }
+ },
+ NAMESPACE {
+ @Override
+ public String asString() {
+ return "db.namespace";
+ }
+ },
+ COLLECTION {
+ @Override
+ public String asString() {
+ return "db.collection.name";
+ }
+ },
+ OPERATION_NAME {
+ @Override
+ public String asString() {
+ return "db.operation.name";
+ }
+ },
+ COMMAND_NAME {
+ @Override
+ public String asString() {
+ return "db.command.name";
+ }
+ },
+ NETWORK_TRANSPORT {
+ @Override
+ public String asString() {
+ return "network.transport";
+ }
+ },
+ OPERATION_SUMMARY {
+ @Override
+ public String asString() {
+ return "db.operation.summary";
+ }
+ },
+ QUERY_SUMMARY {
+ @Override
+ public String asString() {
+ return "db.query.summary";
+ }
+ },
+ CURSOR_ID {
+ @Override
+ public String asString() {
+ return "db.mongodb.cursor_id";
+ }
+ },
+ SERVER_ADDRESS {
+ @Override
+ public String asString() {
+ return "server.address";
+ }
+ },
+ SERVER_PORT {
+ @Override
+ public String asString() {
+ return "server.port";
+ }
+ },
+ SERVER_TYPE {
+ @Override
+ public String asString() {
+ return "server.type";
+ }
+ },
+ CLIENT_CONNECTION_ID {
+ @Override
+ public String asString() {
+ return "db.mongodb.driver_connection_id";
+ }
+ },
+ SERVER_CONNECTION_ID {
+ @Override
+ public String asString() {
+ return "db.mongodb.server_connection_id";
+ }
+ },
+ TRANSACTION_NUMBER {
+ @Override
+ public String asString() {
+ return "db.mongodb.txn_number";
+ }
+ },
+ SESSION_ID {
+ @Override
+ public String asString() {
+ return "db.mongodb.lsid";
+ }
+ },
+ EXCEPTION_STACKTRACE {
+ @Override
+ public String asString() {
+ return "exception.stacktrace";
+ }
+ },
+ EXCEPTION_TYPE {
+ @Override
+ public String asString() {
+ return "exception.type";
+ }
+ },
+ EXCEPTION_MESSAGE {
+ @Override
+ public String asString() {
+ return "exception.message";
+ }
+ },
+ RESPONSE_STATUS_CODE {
+ @Override
+ public String asString() {
+ return "db.response.status_code";
+ }
+ }
+ }
+
+ /**
+ * Enums related to high cardinality (highly variable values) key names for MongoDB tags.
+ */
+ public enum HighCardinalityKeyNames implements KeyName {
+
+ QUERY_TEXT {
+ @Override
+ public String asString() {
+ return "db.query.text";
+ }
+ };
+
+ private static final String ENV_OTEL_QUERY_TEXT_MAX_LENGTH = "OTEL_JAVA_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH";
+ private final int textMaxLength;
+
+ HighCardinalityKeyNames() {
+ String queryTextMaxLength = getenv(ENV_OTEL_QUERY_TEXT_MAX_LENGTH);
+ if (queryTextMaxLength != null) {
+ this.textMaxLength = Integer.parseInt(queryTextMaxLength);
+ } else {
+ this.textMaxLength = Integer.MAX_VALUE;
+ }
+ }
+
+ public KeyValue withBson(final BsonDocument commandDocument) {
+ if (textMaxLength == Integer.MAX_VALUE) {
+ // no truncation needed
+ return KeyValue.of(asString(), commandDocument.toString());
+ } else {
+ return KeyValue.of(asString(), getTruncatedJsonCommand(commandDocument));
+ }
+ }
+
+ private String getTruncatedJsonCommand(final BsonDocument commandDocument) {
+ StringWriter writer = new StringWriter();
+
+ try (BsonReader bsonReader = commandDocument.asBsonReader()) {
+ JsonWriter jsonWriter = new JsonWriter(writer,
+ JsonWriterSettings.builder().outputMode(JsonMode.RELAXED)
+ .maxLength(textMaxLength)
+ .build());
+
+ jsonWriter.pipe(bsonReader);
+
+ if (jsonWriter.isTruncated()) {
+ writer.append(" ...");
+ }
+
+ return writer.toString();
+ }
+ }
+ }
+}
diff --git a/driver-core/src/main/com/mongodb/tracing/Span.java b/driver-core/src/main/com/mongodb/tracing/Span.java
new file mode 100644
index 00000000000..b1b9ed30c49
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/tracing/Span.java
@@ -0,0 +1,139 @@
+/*
+ * 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.tracing;
+
+import com.mongodb.MongoNamespace;
+import com.mongodb.lang.Nullable;
+import io.micrometer.common.KeyValue;
+import io.micrometer.common.KeyValues;
+
+/**
+ * Represents a tracing span for the driver internal operations.
+ *
+ * A span records information about a single operation, such as tags, events, errors, and its context.
+ * Implementations can be used to propagate tracing information and record telemetry.
+ *
+ *
+ * Spans can be used to trace different aspects of MongoDB driver activity:
+ *
+ * - Command Spans: Trace the execution of MongoDB commands (e.g., find, insert, update).
+ * - Operation Spans: Trace higher-level operations, which may include multiple commands or internal steps.
+ * - Transaction Spans: Trace the lifecycle of a transaction, including all operations and commands within it.
+ *
+ *
+ * @since 5.7
+ */
+public interface Span {
+ /**
+ * An empty / no-op implementation of the Span interface.
+ *
+ * This implementation is used as a default when no actual tracing is required.
+ * All methods in this implementation perform no operations and return default values.
+ *
+ */
+ Span EMPTY = new Span() {
+ @Override
+ public void tagLowCardinality(final KeyValue tag) {
+ }
+
+ @Override
+ public void tagLowCardinality(final KeyValues keyValues) {
+ }
+
+ @Override
+ public void tagHighCardinality(final KeyValue keyValue) {
+ }
+
+ @Override
+ public void event(final String event) {
+ }
+
+ @Override
+ public void error(final Throwable throwable) {
+ }
+
+ @Override
+ public void end() {
+ }
+
+ @Override
+ public TraceContext context() {
+ return TraceContext.EMPTY;
+ }
+
+ @Override
+ @Nullable
+ public MongoNamespace getNamespace() {
+ return null;
+ }
+ };
+
+ /**
+ * Adds a low-cardinality tag to the span.
+ *
+ * @param keyValue The key-value pair representing the tag.
+ */
+ void tagLowCardinality(KeyValue keyValue);
+
+ /**
+ * Adds multiple low-cardinality tags to the span.
+ *
+ * @param keyValues The key-value pairs representing the tags.
+ */
+ void tagLowCardinality(KeyValues keyValues);
+
+ /**
+ * Adds a high-cardinality (highly variable values) tag to the span.
+ *
+ * @param keyValue The key-value pair representing the tag.
+ */
+ void tagHighCardinality(KeyValue keyValue);
+
+ /**
+ * Records an event in the span.
+ *
+ * @param event The event description.
+ */
+ void event(String event);
+
+ /**
+ * Records an error for this span.
+ *
+ * @param throwable The error to record.
+ */
+ void error(Throwable throwable);
+
+ /**
+ * Ends the span, marking it as complete.
+ */
+ void end();
+
+ /**
+ * Retrieves the context associated with the span.
+ *
+ * @return The trace context associated with the span.
+ */
+ TraceContext context();
+
+ /**
+ * Retrieves the MongoDB namespace associated with the span, if any.
+ *
+ * @return The MongoDB namespace, or null if none is associated.
+ */
+ @Nullable
+ MongoNamespace getNamespace();
+}
diff --git a/driver-core/src/main/com/mongodb/tracing/TraceContext.java b/driver-core/src/main/com/mongodb/tracing/TraceContext.java
new file mode 100644
index 00000000000..06763f96111
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/tracing/TraceContext.java
@@ -0,0 +1,23 @@
+/*
+ * 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.tracing;
+
+@SuppressWarnings("InterfaceIsType")
+public interface TraceContext {
+ TraceContext EMPTY = new TraceContext() {
+ };
+}
diff --git a/driver-core/src/main/com/mongodb/tracing/Tracer.java b/driver-core/src/main/com/mongodb/tracing/Tracer.java
new file mode 100644
index 00000000000..cc88e82b5ac
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/tracing/Tracer.java
@@ -0,0 +1,75 @@
+/*
+ * 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.tracing;
+
+import com.mongodb.MongoNamespace;
+import com.mongodb.lang.Nullable;
+
+/**
+ * A Tracer interface that provides methods for tracing commands, operations and transactions.
+ *
+ * This interface defines methods to retrieve the current trace context, create new spans, and check if tracing is enabled.
+ * It also includes a no-operation (NO_OP) implementation for cases where tracing is not required.
+ *
+ *
+ * Note: You can use the environment variable {@code OTEL_JAVA_INSTRUMENTATION_MONGODB_ENABLED} to override the behaviour of enabling/disabling
+ * tracing before you create the {@link com.mongodb.MongoClientSettings} instance.
+ * You can also use the environment variable {@code OTEL_JAVA_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH} to enable or disable command payload when tracing is enabled. .
+ *
+ * @since 5.7
+ */
+public interface Tracer {
+ Tracer NO_OP = new Tracer() {
+ @Override
+ public Span nextSpan(final String name, @Nullable final TraceContext parent, @Nullable final MongoNamespace namespace) {
+ return Span.EMPTY;
+ }
+
+ @Override
+ public boolean enabled() {
+ return false;
+ }
+
+ @Override
+ public boolean includeCommandPayload() {
+ return false;
+ }
+ };
+
+ /**
+ * Creates a new span with the specified name and optional parent trace context.
+ *
+ * @param name The name of the span.
+ * @param parent The parent {@link TraceContext}, or null if no parent context is provided.
+ * @param namespace The {@link MongoNamespace} associated with the span, or null if none is provided.
+ * @return A {@link Span} representing the newly created span.
+ */
+ Span nextSpan(String name, @Nullable TraceContext parent, @Nullable MongoNamespace namespace);
+
+ /**
+ * Indicates whether tracing is enabled.
+ *
+ * @return {@code true} if tracing is enabled, {@code false} otherwise.
+ */
+ boolean enabled();
+
+ /**
+ * Indicates whether command payloads are included in the trace context.
+ *
+ * @return {@code true} if command payloads are allowed, {@code false} otherwise.
+ */
+ boolean includeCommandPayload();
+}
diff --git a/driver-core/src/main/com/mongodb/tracing/package-info.java b/driver-core/src/main/com/mongodb/tracing/package-info.java
new file mode 100644
index 00000000000..247576d1537
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/tracing/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+/**
+ * This package defines the API for MongoDB driver tracing.
+ *
+ * @since 5.7
+ */
+@NonNullApi
+package com.mongodb.tracing;
+
+import com.mongodb.lang.NonNullApi;
diff --git a/driver-core/src/test/functional/com/mongodb/internal/connection/CommandHelperSpecification.groovy b/driver-core/src/test/functional/com/mongodb/internal/connection/CommandHelperSpecification.groovy
index 83ce94f7075..ecc9d3f64a5 100644
--- a/driver-core/src/test/functional/com/mongodb/internal/connection/CommandHelperSpecification.groovy
+++ b/driver-core/src/test/functional/com/mongodb/internal/connection/CommandHelperSpecification.groovy
@@ -25,6 +25,7 @@ import com.mongodb.connection.SocketSettings
import com.mongodb.internal.connection.netty.NettyStreamFactory
import org.bson.BsonDocument
import org.bson.BsonInt32
+import spock.lang.Ignore
import spock.lang.Specification
import java.util.concurrent.CountDownLatch
@@ -54,6 +55,7 @@ class CommandHelperSpecification extends Specification {
connection?.close()
}
+ @Ignore("5982")
def 'should execute command asynchronously'() {
when:
BsonDocument receivedDocument = null
diff --git a/driver-core/src/test/functional/com/mongodb/internal/operation/AsyncCommandBatchCursorTest.java b/driver-core/src/test/functional/com/mongodb/internal/operation/AsyncCommandBatchCursorTest.java
index e9a30686d5f..3708081fc26 100644
--- a/driver-core/src/test/functional/com/mongodb/internal/operation/AsyncCommandBatchCursorTest.java
+++ b/driver-core/src/test/functional/com/mongodb/internal/operation/AsyncCommandBatchCursorTest.java
@@ -32,6 +32,7 @@
import com.mongodb.internal.binding.AsyncConnectionSource;
import com.mongodb.internal.connection.AsyncConnection;
import com.mongodb.internal.connection.OperationContext;
+import com.mongodb.internal.tracing.TracingManager;
import org.bson.BsonArray;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
@@ -96,6 +97,7 @@ void setUp() {
connectionSource = mock(AsyncConnectionSource.class);
operationContext = mock(OperationContext.class);
+ when(operationContext.getTracingManager()).thenReturn(TracingManager.NO_OP);
timeoutContext = new TimeoutContext(TimeoutSettings.create(
MongoClientSettings.builder().timeout(TIMEOUT.toMillis(), MILLISECONDS).build()));
serverDescription = mock(ServerDescription.class);
diff --git a/driver-core/src/test/unit/com/mongodb/MongoClientSettingsSpecification.groovy b/driver-core/src/test/unit/com/mongodb/MongoClientSettingsSpecification.groovy
index ec5d92b1e49..02b33aa8d27 100644
--- a/driver-core/src/test/unit/com/mongodb/MongoClientSettingsSpecification.groovy
+++ b/driver-core/src/test/unit/com/mongodb/MongoClientSettingsSpecification.groovy
@@ -555,7 +555,7 @@ class MongoClientSettingsSpecification extends Specification {
'heartbeatConnectTimeoutMS', 'heartbeatSocketTimeoutMS', 'inetAddressResolver', 'loggerSettingsBuilder',
'readConcern', 'readPreference', 'retryReads',
'retryWrites', 'serverApi', 'serverSettingsBuilder', 'socketSettingsBuilder', 'sslSettingsBuilder',
- 'timeoutMS', 'transportSettings', 'uuidRepresentation', 'writeConcern']
+ 'timeoutMS', 'tracer', 'transportSettings', 'uuidRepresentation', 'writeConcern']
then:
actual == expected
@@ -570,7 +570,7 @@ class MongoClientSettingsSpecification extends Specification {
'applyToSslSettings', 'autoEncryptionSettings', 'build', 'codecRegistry', 'commandListenerList',
'compressorList', 'contextProvider', 'credential', 'dnsClient', 'heartbeatConnectTimeoutMS',
'heartbeatSocketTimeoutMS', 'inetAddressResolver', 'readConcern', 'readPreference', 'retryReads', 'retryWrites',
- 'serverApi', 'timeout', 'transportSettings', 'uuidRepresentation', 'writeConcern']
+ 'serverApi', 'timeout', 'tracer', 'transportSettings', 'uuidRepresentation', 'writeConcern']
then:
actual == expected
diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/LoggingCommandEventSenderSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/connection/LoggingCommandEventSenderSpecification.groovy
index 6aa30aa4aa6..e6f6afb02e0 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/connection/LoggingCommandEventSenderSpecification.groovy
+++ b/driver-core/src/test/unit/com/mongodb/internal/connection/LoggingCommandEventSenderSpecification.groovy
@@ -65,7 +65,8 @@ class LoggingCommandEventSenderSpecification extends Specification {
}
def operationContext = OPERATION_CONTEXT
def sender = new LoggingCommandEventSender([] as Set, [] as Set, connectionDescription, commandListener,
- operationContext, message, bsonOutput, new StructuredLogger(logger), LoggerSettings.builder().build())
+ operationContext, message, message.getCommandDocument(bsonOutput),
+ new StructuredLogger(logger), LoggerSettings.builder().build())
when:
sender.sendStartedEvent()
@@ -110,7 +111,7 @@ class LoggingCommandEventSenderSpecification extends Specification {
}
def operationContext = OPERATION_CONTEXT
def sender = new LoggingCommandEventSender([] as Set, [] as Set, connectionDescription, commandListener,
- operationContext, message, bsonOutput, new StructuredLogger(logger),
+ operationContext, message, message.getCommandDocument(bsonOutput), new StructuredLogger(logger),
LoggerSettings.builder().build())
when:
sender.sendStartedEvent()
@@ -168,7 +169,7 @@ class LoggingCommandEventSenderSpecification extends Specification {
def operationContext = OPERATION_CONTEXT
def sender = new LoggingCommandEventSender([] as Set, [] as Set, connectionDescription, null, operationContext,
- message, bsonOutput, new StructuredLogger(logger), LoggerSettings.builder().build())
+ message, message.getCommandDocument(bsonOutput), new StructuredLogger(logger), LoggerSettings.builder().build())
when:
sender.sendStartedEvent()
@@ -201,7 +202,8 @@ class LoggingCommandEventSenderSpecification extends Specification {
}
def operationContext = OPERATION_CONTEXT
def sender = new LoggingCommandEventSender(['createUser'] as Set, [] as Set, connectionDescription, null,
- operationContext, message, bsonOutput, new StructuredLogger(logger), LoggerSettings.builder().build())
+ operationContext, message, message.getCommandDocument(bsonOutput), new StructuredLogger(logger),
+ LoggerSettings.builder().build())
when:
sender.sendStartedEvent()
diff --git a/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncChangeStreamBatchCursorSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncChangeStreamBatchCursorSpecification.groovy
index 998c0a28b6e..b7f8f0e9408 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncChangeStreamBatchCursorSpecification.groovy
+++ b/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncChangeStreamBatchCursorSpecification.groovy
@@ -22,6 +22,7 @@ import com.mongodb.internal.TimeoutContext
import com.mongodb.internal.async.SingleResultCallback
import com.mongodb.internal.binding.AsyncReadBinding
import com.mongodb.internal.connection.OperationContext
+import com.mongodb.internal.tracing.TracingManager
import org.bson.Document
import spock.lang.Specification
@@ -34,6 +35,7 @@ class AsyncChangeStreamBatchCursorSpecification extends Specification {
def changeStreamOpertation = Stub(ChangeStreamOperation)
def binding = Mock(AsyncReadBinding)
def operationContext = Mock(OperationContext)
+ operationContext.getTracingManager() >> TracingManager.NO_OP
def timeoutContext = Mock(TimeoutContext)
binding.getOperationContext() >> operationContext
operationContext.getTimeoutContext() >> timeoutContext
@@ -78,6 +80,7 @@ class AsyncChangeStreamBatchCursorSpecification extends Specification {
def changeStreamOpertation = Stub(ChangeStreamOperation)
def binding = Mock(AsyncReadBinding)
def operationContext = Mock(OperationContext)
+ operationContext.getTracingManager() >> TracingManager.NO_OP
def timeoutContext = Mock(TimeoutContext)
binding.getOperationContext() >> operationContext
operationContext.getTimeoutContext() >> timeoutContext
@@ -111,6 +114,7 @@ class AsyncChangeStreamBatchCursorSpecification extends Specification {
def changeStreamOpertation = Stub(ChangeStreamOperation)
def binding = Mock(AsyncReadBinding)
def operationContext = Mock(OperationContext)
+ operationContext.getTracingManager() >> TracingManager.NO_OP
def timeoutContext = Mock(TimeoutContext)
binding.getOperationContext() >> operationContext
operationContext.getTimeoutContext() >> timeoutContext
diff --git a/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncCommandBatchCursorSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncCommandBatchCursorSpecification.groovy
index d2bcd0804bb..e3f2e525146 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncCommandBatchCursorSpecification.groovy
+++ b/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncCommandBatchCursorSpecification.groovy
@@ -35,6 +35,7 @@ import com.mongodb.internal.async.SingleResultCallback
import com.mongodb.internal.binding.AsyncConnectionSource
import com.mongodb.internal.connection.AsyncConnection
import com.mongodb.internal.connection.OperationContext
+import com.mongodb.internal.tracing.TracingManager
import org.bson.BsonArray
import org.bson.BsonDocument
import org.bson.BsonInt32
@@ -524,6 +525,7 @@ class AsyncCommandBatchCursorSpecification extends Specification {
.build()
}
OperationContext operationContext = Mock(OperationContext)
+ operationContext.getTracingManager() >> TracingManager.NO_OP
def timeoutContext = Spy(new TimeoutContext(TimeoutSettings.create(
MongoClientSettings.builder().timeout(3, TimeUnit.SECONDS).build())))
operationContext.getTimeoutContext() >> timeoutContext
diff --git a/driver-core/src/test/unit/com/mongodb/internal/operation/ChangeStreamBatchCursorTest.java b/driver-core/src/test/unit/com/mongodb/internal/operation/ChangeStreamBatchCursorTest.java
index 48c3a50e79a..552afaea95b 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/operation/ChangeStreamBatchCursorTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/operation/ChangeStreamBatchCursorTest.java
@@ -25,6 +25,7 @@
import com.mongodb.internal.binding.ReadBinding;
import com.mongodb.internal.connection.Connection;
import com.mongodb.internal.connection.OperationContext;
+import com.mongodb.internal.tracing.TracingManager;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.Document;
@@ -296,6 +297,7 @@ void setUp() {
doNothing().when(timeoutContext).resetTimeoutIfPresent();
operationContext = mock(OperationContext.class);
+ when(operationContext.getTracingManager()).thenReturn(TracingManager.NO_OP);
when(operationContext.getTimeoutContext()).thenReturn(timeoutContext);
connection = mock(Connection.class);
when(connection.command(any(), any(), any(), any(), any(), any())).thenReturn(null);
diff --git a/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorSpecification.groovy
index c95a119134a..3190c1f6289 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorSpecification.groovy
+++ b/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorSpecification.groovy
@@ -35,6 +35,7 @@ import com.mongodb.internal.TimeoutSettings
import com.mongodb.internal.binding.ConnectionSource
import com.mongodb.internal.connection.Connection
import com.mongodb.internal.connection.OperationContext
+import com.mongodb.internal.tracing.TracingManager
import org.bson.BsonArray
import org.bson.BsonDocument
import org.bson.BsonInt32
@@ -574,6 +575,7 @@ class CommandBatchCursorSpecification extends Specification {
.build()
}
OperationContext operationContext = Mock(OperationContext)
+ operationContext.getTracingManager() >> TracingManager.NO_OP
def timeoutContext = Spy(new TimeoutContext(TimeoutSettings.create(
MongoClientSettings.builder().timeout(3, TimeUnit.SECONDS).build())))
operationContext.getTimeoutContext() >> timeoutContext
diff --git a/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorTest.java b/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorTest.java
index c3bec291432..b4b4101bd56 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorTest.java
@@ -31,6 +31,7 @@
import com.mongodb.internal.binding.ConnectionSource;
import com.mongodb.internal.connection.Connection;
import com.mongodb.internal.connection.OperationContext;
+import com.mongodb.internal.tracing.TracingManager;
import org.bson.BsonArray;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
@@ -94,6 +95,8 @@ void setUp() {
connectionSource = mock(ConnectionSource.class);
operationContext = mock(OperationContext.class);
+ when(operationContext.getTracingManager()).thenReturn(TracingManager.NO_OP);
+
timeoutContext = new TimeoutContext(TimeoutSettings.create(
MongoClientSettings.builder().timeout(TIMEOUT.toMillis(), MILLISECONDS).build()));
serverDescription = mock(ServerDescription.class);
diff --git a/driver-kotlin-coroutine/build.gradle.kts b/driver-kotlin-coroutine/build.gradle.kts
index 02a2bf047aa..8880c67aad8 100644
--- a/driver-kotlin-coroutine/build.gradle.kts
+++ b/driver-kotlin-coroutine/build.gradle.kts
@@ -38,6 +38,7 @@ dependencies {
integrationTestImplementation(project(path = ":bson", configuration = "testArtifacts"))
integrationTestImplementation(project(path = ":driver-sync", configuration = "testArtifacts"))
integrationTestImplementation(project(path = ":driver-core", configuration = "testArtifacts"))
+ testImplementation(libs.micrometer.tracing.integration.test) { exclude(group = "org.junit.jupiter") }
}
configureMavenPublication {
diff --git a/driver-kotlin-coroutine/src/integrationTest/kotlin/com/mongodb/kotlin/client/coroutine/syncadapter/SyncClientSession.kt b/driver-kotlin-coroutine/src/integrationTest/kotlin/com/mongodb/kotlin/client/coroutine/syncadapter/SyncClientSession.kt
index 83ba91df16b..3c66babd5d8 100644
--- a/driver-kotlin-coroutine/src/integrationTest/kotlin/com/mongodb/kotlin/client/coroutine/syncadapter/SyncClientSession.kt
+++ b/driver-kotlin-coroutine/src/integrationTest/kotlin/com/mongodb/kotlin/client/coroutine/syncadapter/SyncClientSession.kt
@@ -21,6 +21,7 @@ import com.mongodb.TransactionOptions
import com.mongodb.client.ClientSession as JClientSession
import com.mongodb.client.TransactionBody
import com.mongodb.internal.TimeoutContext
+import com.mongodb.internal.tracing.TransactionSpan
import com.mongodb.kotlin.client.coroutine.ClientSession
import com.mongodb.session.ServerSession
import kotlinx.coroutines.runBlocking
@@ -89,4 +90,6 @@ class SyncClientSession(internal val wrapped: ClientSession, private val origina
throw UnsupportedOperationException()
override fun getTimeoutContext(): TimeoutContext? = wrapped.getTimeoutContext()
+
+ override fun getTransactionSpan(): TransactionSpan? = null
}
diff --git a/driver-kotlin-sync/build.gradle.kts b/driver-kotlin-sync/build.gradle.kts
index 5da1a5eec26..74f9b37c219 100644
--- a/driver-kotlin-sync/build.gradle.kts
+++ b/driver-kotlin-sync/build.gradle.kts
@@ -32,6 +32,7 @@ dependencies {
integrationTestImplementation(project(path = ":bson", configuration = "testArtifacts"))
integrationTestImplementation(project(path = ":driver-sync", configuration = "testArtifacts"))
integrationTestImplementation(project(path = ":driver-core", configuration = "testArtifacts"))
+ testImplementation(libs.micrometer.tracing.integration.test) { exclude(group = "org.junit.jupiter") }
}
configureMavenPublication {
diff --git a/driver-kotlin-sync/src/integrationTest/kotlin/com/mongodb/kotlin/client/syncadapter/SyncClientSession.kt b/driver-kotlin-sync/src/integrationTest/kotlin/com/mongodb/kotlin/client/syncadapter/SyncClientSession.kt
index 64cd27b776f..001198dbcd0 100644
--- a/driver-kotlin-sync/src/integrationTest/kotlin/com/mongodb/kotlin/client/syncadapter/SyncClientSession.kt
+++ b/driver-kotlin-sync/src/integrationTest/kotlin/com/mongodb/kotlin/client/syncadapter/SyncClientSession.kt
@@ -21,6 +21,7 @@ import com.mongodb.TransactionOptions
import com.mongodb.client.ClientSession as JClientSession
import com.mongodb.client.TransactionBody
import com.mongodb.internal.TimeoutContext
+import com.mongodb.internal.tracing.TransactionSpan
import com.mongodb.kotlin.client.ClientSession
import com.mongodb.session.ServerSession
import org.bson.BsonDocument
@@ -93,4 +94,6 @@ internal class SyncClientSession(internal val wrapped: ClientSession, private va
throw UnsupportedOperationException()
override fun getTimeoutContext(): TimeoutContext = throw UnsupportedOperationException()
+
+ override fun getTransactionSpan(): TransactionSpan? = null
}
diff --git a/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/ClientSession.kt b/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/ClientSession.kt
index 9103689b251..9786f5592e6 100644
--- a/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/ClientSession.kt
+++ b/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/ClientSession.kt
@@ -18,6 +18,7 @@ package com.mongodb.kotlin.client
import com.mongodb.ClientSessionOptions
import com.mongodb.TransactionOptions
import com.mongodb.client.ClientSession as JClientSession
+import com.mongodb.internal.tracing.TransactionSpan
import java.io.Closeable
import java.util.concurrent.TimeUnit
@@ -86,6 +87,9 @@ public class ClientSession(public val wrapped: JClientSession) : Closeable {
transactionBody: () -> T,
options: TransactionOptions = TransactionOptions.builder().build()
): T = wrapped.withTransaction(transactionBody, options)
+
+ /** Get the transaction span (if started). */
+ public fun getTransactionSpan(): TransactionSpan? = wrapped.getTransactionSpan()
}
/**
diff --git a/driver-kotlin-sync/src/test/kotlin/com/mongodb/kotlin/client/MongoIterableTest.kt b/driver-kotlin-sync/src/test/kotlin/com/mongodb/kotlin/client/MongoIterableTest.kt
index ab16dd08b24..17bcc3c1a12 100644
--- a/driver-kotlin-sync/src/test/kotlin/com/mongodb/kotlin/client/MongoIterableTest.kt
+++ b/driver-kotlin-sync/src/test/kotlin/com/mongodb/kotlin/client/MongoIterableTest.kt
@@ -19,6 +19,7 @@ import com.mongodb.Function
import com.mongodb.client.MongoCursor as JMongoCursor
import com.mongodb.client.MongoIterable as JMongoIterable
import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
import org.bson.Document
import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers
@@ -90,7 +91,7 @@ class MongoIterableTest {
whenever(cursor.next()).thenReturn(documents[0], documents[1], documents[2])
whenever(delegate.cursor()).doReturn(cursor)
- assertContentEquals(documents.subList(0, 2), iterable.use { it.take(2) }.toList())
+ iterable.use { it.take(2).forEachIndexed { index, document -> assertEquals(documents[index], document) } }
verify(delegate, times(1)).cursor()
verify(cursor, times(2)).hasNext()
diff --git a/driver-legacy/src/main/com/mongodb/LegacyMixedBulkWriteOperation.java b/driver-legacy/src/main/com/mongodb/LegacyMixedBulkWriteOperation.java
index 95990833f00..47749129115 100644
--- a/driver-legacy/src/main/com/mongodb/LegacyMixedBulkWriteOperation.java
+++ b/driver-legacy/src/main/com/mongodb/LegacyMixedBulkWriteOperation.java
@@ -97,6 +97,11 @@ public String getCommandName() {
return wrappedOperation.getCommandName();
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return wrappedOperation.getNamespace();
+ }
+
@Override
public WriteConcernResult execute(final WriteBinding binding) {
try {
diff --git a/driver-reactive-streams/build.gradle.kts b/driver-reactive-streams/build.gradle.kts
index f1c758b31da..8f4454d4f4c 100644
--- a/driver-reactive-streams/build.gradle.kts
+++ b/driver-reactive-streams/build.gradle.kts
@@ -44,6 +44,9 @@ dependencies {
// Reactive Streams TCK testing
testImplementation(libs.reactive.streams.tck)
+
+ // Tracing
+ testImplementation(libs.micrometer.tracing.integration.test) { exclude(group = "org.junit.jupiter") }
}
configureMavenPublication {
diff --git a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/MapReducePublisherImpl.java b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/MapReducePublisherImpl.java
index 27e69762a09..46096d6ff58 100644
--- a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/MapReducePublisherImpl.java
+++ b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/MapReducePublisherImpl.java
@@ -240,6 +240,11 @@ public String getCommandName() {
return operation.getCommandName();
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return operation.getNamespace();
+ }
+
@Override
public void executeAsync(final AsyncReadBinding binding, final SingleResultCallback> callback) {
operation.executeAsync(binding, callback::onResult);
@@ -262,6 +267,11 @@ public String getCommandName() {
return operation.getCommandName();
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return operation.getNamespace();
+ }
+
@Override
public Void execute(final WriteBinding binding) {
throw new UnsupportedOperationException("This operation is async only");
diff --git a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/OperationExecutorImpl.java b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/OperationExecutorImpl.java
index 56b0526e4cb..06f64becf5c 100644
--- a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/OperationExecutorImpl.java
+++ b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/OperationExecutorImpl.java
@@ -34,6 +34,7 @@
import com.mongodb.internal.operation.OperationHelper;
import com.mongodb.internal.operation.ReadOperation;
import com.mongodb.internal.operation.WriteOperation;
+import com.mongodb.internal.tracing.TracingManager;
import com.mongodb.lang.Nullable;
import com.mongodb.reactivestreams.client.ClientSession;
import com.mongodb.reactivestreams.client.ReactiveContextProvider;
@@ -204,6 +205,7 @@ private OperationContext getOperationContext(final RequestContext requestContext
requestContext,
new ReadConcernAwareNoOpSessionContext(readConcern),
createTimeoutContext(session, timeoutSettings),
+ TracingManager.NO_OP,
mongoClient.getSettings().getServerApi(),
commandName);
}
diff --git a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/VoidReadOperationThenCursorReadOperation.java b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/VoidReadOperationThenCursorReadOperation.java
index e74949432b9..f5f3ae29969 100644
--- a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/VoidReadOperationThenCursorReadOperation.java
+++ b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/VoidReadOperationThenCursorReadOperation.java
@@ -16,6 +16,7 @@
package com.mongodb.reactivestreams.client.internal;
+import com.mongodb.MongoNamespace;
import com.mongodb.internal.async.AsyncBatchCursor;
import com.mongodb.internal.async.SingleResultCallback;
import com.mongodb.internal.binding.AsyncReadBinding;
@@ -45,6 +46,11 @@ public String getCommandName() {
return readOperation.getCommandName();
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return readOperation.getNamespace();
+ }
+
@Override
public void executeAsync(final AsyncReadBinding binding, final SingleResultCallback> callback) {
readOperation.executeAsync(binding, (result, t) -> {
diff --git a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/VoidWriteOperationThenCursorReadOperation.java b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/VoidWriteOperationThenCursorReadOperation.java
index 428ad21ca26..1a741d7d0f6 100644
--- a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/VoidWriteOperationThenCursorReadOperation.java
+++ b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/VoidWriteOperationThenCursorReadOperation.java
@@ -16,6 +16,7 @@
package com.mongodb.reactivestreams.client.internal;
+import com.mongodb.MongoNamespace;
import com.mongodb.internal.async.AsyncBatchCursor;
import com.mongodb.internal.async.SingleResultCallback;
import com.mongodb.internal.binding.AsyncReadBinding;
@@ -38,6 +39,11 @@ public String getCommandName() {
return writeOperation.getCommandName();
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return writeOperation.getNamespace();
+ }
+
@Override
public void executeAsync(final AsyncReadBinding binding, final SingleResultCallback> callback) {
writeOperation.executeAsync((AsyncWriteBinding) binding, (result, t) -> {
diff --git a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncClientSession.java b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncClientSession.java
index 494e5f8c74e..ab234ad6f3e 100644
--- a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncClientSession.java
+++ b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncClientSession.java
@@ -22,6 +22,7 @@
import com.mongodb.client.ClientSession;
import com.mongodb.client.TransactionBody;
import com.mongodb.internal.TimeoutContext;
+import com.mongodb.internal.tracing.TransactionSpan;
import com.mongodb.lang.Nullable;
import com.mongodb.session.ServerSession;
import org.bson.BsonDocument;
@@ -188,6 +189,12 @@ public TimeoutContext getTimeoutContext() {
return wrapped.getTimeoutContext();
}
+ @Override
+ @Nullable
+ public TransactionSpan getTransactionSpan() {
+ return null;
+ }
+
private static void sleep(final long millis) {
try {
Thread.sleep(millis);
diff --git a/driver-scala/build.gradle.kts b/driver-scala/build.gradle.kts
index 68187889629..c1e0365829b 100644
--- a/driver-scala/build.gradle.kts
+++ b/driver-scala/build.gradle.kts
@@ -36,6 +36,9 @@ dependencies {
// Encryption testing
integrationTestImplementation(project(path = ":mongodb-crypt", configuration = "default"))
+
+ // Tracing
+ testImplementation(libs.micrometer.tracing.integration.test) { exclude(group = "org.junit.jupiter") }
}
configureMavenPublication {
diff --git a/driver-scala/src/integrationTest/scala/org/mongodb/scala/syncadapter/SyncClientSession.scala b/driver-scala/src/integrationTest/scala/org/mongodb/scala/syncadapter/SyncClientSession.scala
index 2866ce7427d..4c0cb19217d 100644
--- a/driver-scala/src/integrationTest/scala/org/mongodb/scala/syncadapter/SyncClientSession.scala
+++ b/driver-scala/src/integrationTest/scala/org/mongodb/scala/syncadapter/SyncClientSession.scala
@@ -19,6 +19,7 @@ package org.mongodb.scala.syncadapter
import com.mongodb.{ ClientSessionOptions, MongoInterruptedException, ServerAddress, TransactionOptions }
import com.mongodb.client.{ ClientSession => JClientSession, TransactionBody }
import com.mongodb.internal.TimeoutContext
+import com.mongodb.internal.tracing.TransactionSpan
import com.mongodb.session.ServerSession
import org.bson.{ BsonDocument, BsonTimestamp }
import org.mongodb.scala._
@@ -96,4 +97,6 @@ case class SyncClientSession(wrapped: ClientSession, originator: Object) extends
}
override def getTimeoutContext: TimeoutContext = wrapped.getTimeoutContext
+
+ override def getTransactionSpan: TransactionSpan = null
}
diff --git a/driver-scala/src/test/scala/org/mongodb/scala/ApiAliasAndCompanionSpec.scala b/driver-scala/src/test/scala/org/mongodb/scala/ApiAliasAndCompanionSpec.scala
index a5b76965651..a15229411ae 100644
--- a/driver-scala/src/test/scala/org/mongodb/scala/ApiAliasAndCompanionSpec.scala
+++ b/driver-scala/src/test/scala/org/mongodb/scala/ApiAliasAndCompanionSpec.scala
@@ -94,7 +94,11 @@ class ApiAliasAndCompanionSpec extends BaseSpec {
"SyncClientEncryption",
"BaseClientUpdateOptions",
"BaseClientDeleteOptions",
- "MongoBaseInterfaceAssertions"
+ "MongoBaseInterfaceAssertions",
+ "MicrometerTracer",
+ "TraceContext",
+ "Span",
+ "Tracer"
)
val scalaExclusions = Set(
"BuildInfo",
diff --git a/driver-sync/build.gradle.kts b/driver-sync/build.gradle.kts
index 95cd0979973..2a5ae5d3b64 100644
--- a/driver-sync/build.gradle.kts
+++ b/driver-sync/build.gradle.kts
@@ -15,6 +15,7 @@
*/
import ProjectExtensions.configureJarManifest
import ProjectExtensions.configureMavenPublication
+import project.DEFAULT_JAVA_VERSION
plugins {
id("project.java")
@@ -35,9 +36,22 @@ dependencies {
testImplementation(project(path = ":bson", configuration = "testArtifacts"))
testImplementation(project(path = ":driver-core", configuration = "testArtifacts"))
+ optionalImplementation(libs.micrometer.observation)
// lambda testing
testImplementation(libs.aws.lambda.core)
+
+ // Tracing testing
+ testImplementation(libs.micrometer.tracing.integration.test) { exclude(group = "org.junit.jupiter") }
+}
+
+tasks.withType {
+ // Needed for MicrometerProseTest to set env variable programmatically (calls
+ // `field.setAccessible(true)`)
+ val testJavaVersion: Int = findProperty("javaVersion")?.toString()?.toInt() ?: DEFAULT_JAVA_VERSION
+ if (testJavaVersion >= DEFAULT_JAVA_VERSION) {
+ jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED")
+ }
}
configureMavenPublication {
diff --git a/driver-sync/src/main/com/mongodb/client/ClientSession.java b/driver-sync/src/main/com/mongodb/client/ClientSession.java
index 5d994b863e8..abf0ff33fbd 100644
--- a/driver-sync/src/main/com/mongodb/client/ClientSession.java
+++ b/driver-sync/src/main/com/mongodb/client/ClientSession.java
@@ -18,6 +18,7 @@
import com.mongodb.ServerAddress;
import com.mongodb.TransactionOptions;
+import com.mongodb.internal.tracing.TransactionSpan;
import com.mongodb.lang.Nullable;
/**
@@ -125,4 +126,13 @@ public interface ClientSession extends com.mongodb.session.ClientSession {
* @since 3.11
*/
T withTransaction(TransactionBody transactionBody, TransactionOptions options);
+
+ /**
+ * Get the transaction span (if started).
+ *
+ * @return the transaction span
+ * @since 5.7
+ */
+ @Nullable
+ TransactionSpan getTransactionSpan();
}
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index b60fc90316a..c1937843a9d 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -36,6 +36,8 @@
import com.mongodb.internal.operation.WriteOperation;
import com.mongodb.internal.session.BaseClientSessionImpl;
import com.mongodb.internal.session.ServerSessionPool;
+import com.mongodb.internal.tracing.TracingManager;
+import com.mongodb.internal.tracing.TransactionSpan;
import com.mongodb.lang.Nullable;
import static com.mongodb.MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL;
@@ -54,11 +56,14 @@ final class ClientSessionImpl extends BaseClientSessionImpl implements ClientSes
private boolean messageSentInCurrentTransaction;
private boolean commitInProgress;
private TransactionOptions transactionOptions;
+ private final TracingManager tracingManager;
+ private TransactionSpan transactionSpan = null;
ClientSessionImpl(final ServerSessionPool serverSessionPool, final Object originator, final ClientSessionOptions options,
- final OperationExecutor operationExecutor) {
+ final OperationExecutor operationExecutor, final TracingManager tracingManager) {
super(serverSessionPool, originator, options);
this.operationExecutor = operationExecutor;
+ this.tracingManager = tracingManager;
}
@Override
@@ -141,6 +146,9 @@ public void abortTransaction() {
} finally {
clearTransactionContext();
cleanupTransaction(TransactionState.ABORTED);
+ if (transactionSpan != null) {
+ transactionSpan.finalizeTransactionSpan(TransactionState.ABORTED.name());
+ }
}
}
@@ -167,6 +175,10 @@ private void startTransaction(final TransactionOptions transactionOptions, final
if (!writeConcern.isAcknowledged()) {
throw new MongoClientException("Transactions do not support unacknowledged write concern");
}
+
+ if (tracingManager.isEnabled()) {
+ transactionSpan = new TransactionSpan(tracingManager);
+ }
clearTransactionContext();
setTimeoutContext(timeoutContext);
}
@@ -187,7 +199,7 @@ private void commitTransaction(final boolean resetTimeout) {
if (transactionState == TransactionState.NONE) {
throw new IllegalStateException("There is no transaction started");
}
-
+ boolean exceptionThrown = false;
try {
if (messageSentInCurrentTransaction) {
ReadConcern readConcern = transactionOptions.getReadConcern();
@@ -206,11 +218,20 @@ private void commitTransaction(final boolean resetTimeout) {
.recoveryToken(getRecoveryToken()), readConcern, this);
}
} catch (MongoException e) {
+ exceptionThrown = true;
clearTransactionContextOnError(e);
+ if (transactionSpan != null) {
+ transactionSpan.handleTransactionSpanError(e);
+ }
throw e;
} finally {
transactionState = TransactionState.COMMITTED;
commitInProgress = false;
+ if (!exceptionThrown) {
+ if (transactionSpan != null) {
+ transactionSpan.finalizeTransactionSpan(TransactionState.COMMITTED.name());
+ }
+ }
}
}
@@ -231,51 +252,72 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
long startTime = ClientSessionClock.INSTANCE.now();
TimeoutContext withTransactionTimeoutContext = createTimeoutContext(options);
- outer:
- while (true) {
- T retVal;
- try {
- startTransaction(options, withTransactionTimeoutContext.copyTimeoutContext());
- retVal = transactionBody.execute();
- } catch (Throwable e) {
- if (transactionState == TransactionState.IN) {
- abortTransaction();
- }
- if (e instanceof MongoException && !(e instanceof MongoOperationTimeoutException)) {
- MongoException exceptionToHandle = OperationHelper.unwrap((MongoException) e);
- if (exceptionToHandle.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)
- && ClientSessionClock.INSTANCE.now() - startTime < MAX_RETRY_TIME_LIMIT_MS) {
- continue;
+ try {
+ outer:
+ while (true) {
+ T retVal;
+ try {
+ startTransaction(options, withTransactionTimeoutContext.copyTimeoutContext());
+ if (transactionSpan != null) {
+ transactionSpan.setIsConvenientTransaction();
}
- }
- throw e;
- }
- if (transactionState == TransactionState.IN) {
- while (true) {
- try {
- commitTransaction(false);
- break;
- } catch (MongoException e) {
- clearTransactionContextOnError(e);
- if (!(e instanceof MongoOperationTimeoutException)
+ retVal = transactionBody.execute();
+ } catch (Throwable e) {
+ if (transactionState == TransactionState.IN) {
+ abortTransaction();
+ }
+ if (e instanceof MongoException && !(e instanceof MongoOperationTimeoutException)) {
+ MongoException exceptionToHandle = OperationHelper.unwrap((MongoException) e);
+ if (exceptionToHandle.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)
&& ClientSessionClock.INSTANCE.now() - startTime < MAX_RETRY_TIME_LIMIT_MS) {
- applyMajorityWriteConcernToTransactionOptions();
+ if (transactionSpan != null) {
+ transactionSpan.spanFinalizing(false);
+ }
+ continue;
+ }
+ }
+ throw e;
+ }
+ if (transactionState == TransactionState.IN) {
+ while (true) {
+ try {
+ commitTransaction(false);
+ break;
+ } catch (MongoException e) {
+ clearTransactionContextOnError(e);
+ if (!(e instanceof MongoOperationTimeoutException)
+ && ClientSessionClock.INSTANCE.now() - startTime < MAX_RETRY_TIME_LIMIT_MS) {
+ applyMajorityWriteConcernToTransactionOptions();
- if (!(e instanceof MongoExecutionTimeoutException)
- && e.hasErrorLabel(UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) {
- continue;
- } else if (e.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)) {
- continue outer;
+ if (!(e instanceof MongoExecutionTimeoutException)
+ && e.hasErrorLabel(UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) {
+ continue;
+ } else if (e.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)) {
+ if (transactionSpan != null) {
+ transactionSpan.spanFinalizing(true);
+ }
+ continue outer;
+ }
}
+ throw e;
}
- throw e;
}
}
+ return retVal;
+ }
+ } finally {
+ if (transactionSpan != null) {
+ transactionSpan.spanFinalizing(true);
}
- return retVal;
}
}
+ @Override
+ @Nullable
+ public TransactionSpan getTransactionSpan() {
+ return transactionSpan;
+ }
+
@Override
public void close() {
try {
diff --git a/driver-sync/src/main/com/mongodb/client/internal/MapReduceIterableImpl.java b/driver-sync/src/main/com/mongodb/client/internal/MapReduceIterableImpl.java
index be3e8ca05e9..b7c05c5ffc2 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/MapReduceIterableImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/MapReduceIterableImpl.java
@@ -240,6 +240,11 @@ public String getCommandName() {
return operation.getCommandName();
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return operation.getNamespace();
+ }
+
@Override
public BatchCursor execute(final ReadBinding binding) {
return operation.execute(binding);
diff --git a/driver-sync/src/main/com/mongodb/client/internal/MongoClientImpl.java b/driver-sync/src/main/com/mongodb/client/internal/MongoClientImpl.java
index 6870277b1c6..b227539557f 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/MongoClientImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/MongoClientImpl.java
@@ -48,6 +48,7 @@
import com.mongodb.internal.diagnostics.logging.Logger;
import com.mongodb.internal.diagnostics.logging.Loggers;
import com.mongodb.internal.session.ServerSessionPool;
+import com.mongodb.internal.tracing.TracingManager;
import com.mongodb.lang.Nullable;
import org.bson.BsonDocument;
import org.bson.Document;
@@ -106,7 +107,8 @@ public MongoClientImpl(final Cluster cluster,
operationExecutor, settings.getReadConcern(), settings.getReadPreference(), settings.getRetryReads(),
settings.getRetryWrites(), settings.getServerApi(),
new ServerSessionPool(cluster, TimeoutSettings.create(settings), settings.getServerApi()),
- TimeoutSettings.create(settings), settings.getUuidRepresentation(), settings.getWriteConcern());
+ TimeoutSettings.create(settings), settings.getUuidRepresentation(),
+ settings.getWriteConcern(), new TracingManager(settings.getTracer()));
this.closed = new AtomicBoolean();
BsonDocument clientMetadataDocument = delegate.getCluster().getClientMetadata().getBsonDocument();
diff --git a/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java b/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java
index 058122e9c26..0907ed951b2 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java
@@ -22,6 +22,7 @@
import com.mongodb.MongoClientException;
import com.mongodb.MongoException;
import com.mongodb.MongoInternalException;
+import com.mongodb.MongoNamespace;
import com.mongodb.MongoQueryException;
import com.mongodb.MongoSocketException;
import com.mongodb.MongoTimeoutException;
@@ -43,6 +44,7 @@
import com.mongodb.client.model.bulk.ClientNamespacedWriteModel;
import com.mongodb.internal.IgnorableRequestContext;
import com.mongodb.internal.TimeoutSettings;
+import com.mongodb.internal.binding.BindingContext;
import com.mongodb.internal.binding.ClusterAwareReadWriteBinding;
import com.mongodb.internal.binding.ClusterBinding;
import com.mongodb.internal.binding.ReadBinding;
@@ -57,7 +59,12 @@
import com.mongodb.internal.operation.ReadOperation;
import com.mongodb.internal.operation.WriteOperation;
import com.mongodb.internal.session.ServerSessionPool;
+import com.mongodb.tracing.Span;
+import com.mongodb.tracing.TraceContext;
+import com.mongodb.internal.tracing.TracingManager;
+import com.mongodb.internal.tracing.TransactionSpan;
import com.mongodb.lang.Nullable;
+import io.micrometer.common.KeyValues;
import org.bson.BsonDocument;
import org.bson.Document;
import org.bson.UuidRepresentation;
@@ -71,11 +78,17 @@
import static com.mongodb.MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL;
import static com.mongodb.MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL;
+import static com.mongodb.MongoNamespace.COMMAND_COLLECTION_NAME;
import static com.mongodb.ReadPreference.primary;
import static com.mongodb.assertions.Assertions.isTrue;
import static com.mongodb.assertions.Assertions.isTrueArgument;
import static com.mongodb.assertions.Assertions.notNull;
import static com.mongodb.internal.TimeoutContext.createTimeoutContext;
+import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.COLLECTION;
+import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.NAMESPACE;
+import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.OPERATION_NAME;
+import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.OPERATION_SUMMARY;
+import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.SYSTEM;
final class MongoClusterImpl implements MongoCluster {
@Nullable
@@ -99,6 +112,7 @@ final class MongoClusterImpl implements MongoCluster {
private final UuidRepresentation uuidRepresentation;
private final WriteConcern writeConcern;
private final Operations operations;
+ private final TracingManager tracingManager;
MongoClusterImpl(
@Nullable final AutoEncryptionSettings autoEncryptionSettings, final Cluster cluster, final CodecRegistry codecRegistry,
@@ -106,7 +120,8 @@ final class MongoClusterImpl implements MongoCluster {
@Nullable final OperationExecutor operationExecutor, final ReadConcern readConcern, final ReadPreference readPreference,
final boolean retryReads, final boolean retryWrites, @Nullable final ServerApi serverApi,
final ServerSessionPool serverSessionPool, final TimeoutSettings timeoutSettings, final UuidRepresentation uuidRepresentation,
- final WriteConcern writeConcern) {
+ final WriteConcern writeConcern,
+ final TracingManager tracingManager) {
this.autoEncryptionSettings = autoEncryptionSettings;
this.cluster = cluster;
this.codecRegistry = codecRegistry;
@@ -123,6 +138,7 @@ final class MongoClusterImpl implements MongoCluster {
this.timeoutSettings = timeoutSettings;
this.uuidRepresentation = uuidRepresentation;
this.writeConcern = writeConcern;
+ this.tracingManager = tracingManager;
operations = new Operations<>(
null,
BsonDocument.class,
@@ -166,35 +182,35 @@ public Long getTimeout(final TimeUnit timeUnit) {
public MongoCluster withCodecRegistry(final CodecRegistry codecRegistry) {
return new MongoClusterImpl(autoEncryptionSettings, cluster, codecRegistry, contextProvider, crypt, originator,
operationExecutor, readConcern, readPreference, retryReads, retryWrites, serverApi, serverSessionPool, timeoutSettings,
- uuidRepresentation, writeConcern);
+ uuidRepresentation, writeConcern, tracingManager);
}
@Override
public MongoCluster withReadPreference(final ReadPreference readPreference) {
return new MongoClusterImpl(autoEncryptionSettings, cluster, codecRegistry, contextProvider, crypt, originator,
operationExecutor, readConcern, readPreference, retryReads, retryWrites, serverApi, serverSessionPool, timeoutSettings,
- uuidRepresentation, writeConcern);
+ uuidRepresentation, writeConcern, tracingManager);
}
@Override
public MongoCluster withWriteConcern(final WriteConcern writeConcern) {
return new MongoClusterImpl(autoEncryptionSettings, cluster, codecRegistry, contextProvider, crypt, originator,
operationExecutor, readConcern, readPreference, retryReads, retryWrites, serverApi, serverSessionPool, timeoutSettings,
- uuidRepresentation, writeConcern);
+ uuidRepresentation, writeConcern, tracingManager);
}
@Override
public MongoCluster withReadConcern(final ReadConcern readConcern) {
return new MongoClusterImpl(autoEncryptionSettings, cluster, codecRegistry, contextProvider, crypt, originator,
operationExecutor, readConcern, readPreference, retryReads, retryWrites, serverApi, serverSessionPool, timeoutSettings,
- uuidRepresentation, writeConcern);
+ uuidRepresentation, writeConcern, tracingManager);
}
@Override
public MongoCluster withTimeout(final long timeout, final TimeUnit timeUnit) {
return new MongoClusterImpl(autoEncryptionSettings, cluster, codecRegistry, contextProvider, crypt, originator,
operationExecutor, readConcern, readPreference, retryReads, retryWrites, serverApi, serverSessionPool,
- timeoutSettings.withTimeout(timeout, timeUnit), uuidRepresentation, writeConcern);
+ timeoutSettings.withTimeout(timeout, timeUnit), uuidRepresentation, writeConcern, tracingManager);
}
@Override
@@ -249,7 +265,7 @@ public ClientSession startSession(final ClientSessionOptions options) {
.readPreference(readPreference)
.build()))
.build();
- return new ClientSessionImpl(serverSessionPool, originator, mergedOptions, operationExecutor);
+ return new ClientSessionImpl(serverSessionPool, originator, mergedOptions, operationExecutor, tracingManager);
}
@Override
@@ -419,6 +435,8 @@ public T execute(final ReadOperation operation, final ReadPreference r
ReadBinding binding = getReadBinding(readPreference, readConcern, actualClientSession, session == null,
operation.getCommandName());
+ Span span = createOperationSpan(actualClientSession, binding, operation.getCommandName(), operation.getNamespace());
+
try {
if (actualClientSession.hasActiveTransaction() && !binding.getReadPreference().equals(primary())) {
throw new MongoClientException("Read preference in a transaction must be primary");
@@ -428,9 +446,15 @@ public T execute(final ReadOperation operation, final ReadPreference r
MongoException exceptionToHandle = OperationHelper.unwrap(e);
labelException(actualClientSession, exceptionToHandle);
clearTransactionContextOnTransientTransactionError(session, exceptionToHandle);
+ if (span != null) {
+ span.error(e);
+ }
throw e;
} finally {
binding.release();
+ if (span != null) {
+ span.end();
+ }
}
}
@@ -444,15 +468,22 @@ public T execute(final WriteOperation operation, final ReadConcern readCo
ClientSession actualClientSession = getClientSession(session);
WriteBinding binding = getWriteBinding(readConcern, actualClientSession, session == null, operation.getCommandName());
+ Span span = createOperationSpan(actualClientSession, binding, operation.getCommandName(), operation.getNamespace());
try {
return operation.execute(binding);
} catch (MongoException e) {
MongoException exceptionToHandle = OperationHelper.unwrap(e);
labelException(actualClientSession, exceptionToHandle);
clearTransactionContextOnTransientTransactionError(session, exceptionToHandle);
+ if (span != null) {
+ span.error(e);
+ }
throw e;
} finally {
binding.release();
+ if (span != null) {
+ span.end();
+ }
}
}
@@ -499,6 +530,7 @@ private OperationContext getOperationContext(final ClientSession session, final
getRequestContext(),
new ReadConcernAwareNoOpSessionContext(readConcern),
createTimeoutContext(session, executorTimeoutSettings),
+ tracingManager,
serverApi,
commandName);
}
@@ -556,5 +588,51 @@ ClientSession getClientSession(@Nullable final ClientSession clientSessionFromOp
}
return session;
}
+
+ /**
+ * Create a tracing span for the given operation, and set it on operation context.
+ *
+ * @param actualClientSession the session that the operation is part of
+ * @param binding the binding for the operation
+ * @param commandName the name of the command
+ * @param namespace the namespace of the command
+ * @return the created span, or null if tracing is not enabled
+ */
+ @Nullable
+ private Span createOperationSpan(final ClientSession actualClientSession, final BindingContext binding, final String commandName, final MongoNamespace namespace) {
+ TracingManager tracingManager = binding.getOperationContext().getTracingManager();
+ if (tracingManager.isEnabled()) {
+ TraceContext parentContext = null;
+ TransactionSpan transactionSpan = actualClientSession.getTransactionSpan();
+ if (transactionSpan != null) {
+ parentContext = transactionSpan.getContext();
+ }
+ String name = commandName + " " + namespace.getDatabaseName() + (COMMAND_COLLECTION_NAME.equalsIgnoreCase(namespace.getCollectionName())
+ ? ""
+ : "." + namespace.getCollectionName());
+
+ KeyValues keyValues = KeyValues.of(
+ SYSTEM.withValue("mongodb"),
+ NAMESPACE.withValue(namespace.getDatabaseName()));
+ if (!COMMAND_COLLECTION_NAME.equalsIgnoreCase(namespace.getCollectionName())) {
+ keyValues = keyValues.and(COLLECTION.withValue(namespace.getCollectionName()));
+ }
+ keyValues = keyValues.and(OPERATION_NAME.withValue(commandName),
+ OPERATION_SUMMARY.withValue(name));
+
+ Span span = binding
+ .getOperationContext()
+ .getTracingManager()
+ .addSpan(name, parentContext, namespace);
+
+ span.tagLowCardinality(keyValues);
+
+ binding.getOperationContext().setTracingSpan(span);
+ return span;
+
+ } else {
+ return null;
+ }
+ }
}
}
diff --git a/driver-sync/src/test/functional/com/mongodb/client/tracing/MicrometerProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/tracing/MicrometerProseTest.java
new file mode 100644
index 00000000000..5b0947ac3eb
--- /dev/null
+++ b/driver-sync/src/test/functional/com/mongodb/client/tracing/MicrometerProseTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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.client.tracing;
+
+import com.mongodb.MongoClientSettings;
+import com.mongodb.client.Fixture;
+import com.mongodb.client.MongoClient;
+import com.mongodb.client.MongoClients;
+import com.mongodb.client.MongoCollection;
+import com.mongodb.client.MongoDatabase;
+import com.mongodb.tracing.MicrometerTracer;
+import io.micrometer.observation.ObservationRegistry;
+import io.micrometer.tracing.test.reporter.inmemory.InMemoryOtelSetup;
+import org.bson.Document;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.util.Map;
+
+import static com.mongodb.ClusterFixture.getDefaultDatabaseName;
+import static com.mongodb.tracing.MongodbObservation.HighCardinalityKeyNames.QUERY_TEXT;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Implementation of the prose tests for Micrometer OpenTelemetry tracing.
+ */
+public class MicrometerProseTest {
+ private ObservationRegistry observationRegistry = ObservationRegistry.create();
+ private InMemoryOtelSetup memoryOtelSetup;
+ private InMemoryOtelSetup.Builder.OtelBuildingBlocks inMemoryOtel;
+
+ @BeforeEach
+ void setUp() {
+ memoryOtelSetup = InMemoryOtelSetup.builder().register(observationRegistry);
+ inMemoryOtel = memoryOtelSetup.getBuildingBlocks();
+ }
+
+ @AfterEach
+ void tearDown() {
+ memoryOtelSetup.close();
+ }
+
+ // Test 1: Tracing Enable/Disable via Environment Variable
+ @Test
+ void testControlOtelInstrumentationViaEnvironmentVariable() throws Exception {
+ MicrometerTracer tracer = new MicrometerTracer(observationRegistry);
+ setEnv("OTEL_JAVA_INSTRUMENTATION_MONGODB_ENABLED", "false");
+
+ MongoClientSettings clientSettings = Fixture.getMongoClientSettingsBuilder()
+ .tracer(tracer).build();
+
+ try (MongoClient client = MongoClients.create(clientSettings)) {
+ MongoDatabase database = client.getDatabase(getDefaultDatabaseName());
+ MongoCollection collection = database.getCollection("test");
+ collection.find().first();
+
+ // Assert that no OpenTelemetry tracing spans are emitted for the operation.
+ assertTrue(inMemoryOtel.getFinishedSpans().isEmpty(), "Spans should not be emitted when instrumentation is enabled.");
+ }
+
+ setEnv("OTEL_JAVA_INSTRUMENTATION_MONGODB_ENABLED", "true");
+ clientSettings = Fixture.getMongoClientSettingsBuilder()
+ .tracer(tracer).build();
+ try (MongoClient client = MongoClients.create(clientSettings)) {
+ MongoDatabase database = client.getDatabase(getDefaultDatabaseName());
+ MongoCollection collection = database.getCollection("test");
+ collection.find().first();
+
+ // Assert that OpenTelemetry tracing spans are emitted for the operation.
+ assertEquals(2, inMemoryOtel.getFinishedSpans().size(), "Spans should be emitted when instrumentation is disabled.");
+ assertEquals("find", inMemoryOtel.getFinishedSpans().get(0).getName());
+ assertEquals("find " + getDefaultDatabaseName() + ".test", inMemoryOtel.getFinishedSpans().get(1).getName());
+ }
+ }
+
+ @Test
+ void testControlCommandPayloadViaEnvironmentVariable() throws Exception {
+ MicrometerTracer tracer = new MicrometerTracer(observationRegistry, true);
+ setEnv("OTEL_JAVA_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH", "42");
+
+ MongoClientSettings clientSettings = Fixture.getMongoClientSettingsBuilder()
+ .tracer(tracer).build();
+
+ try (MongoClient client = MongoClients.create(clientSettings)) {
+ MongoDatabase database = client.getDatabase(getDefaultDatabaseName());
+ MongoCollection collection = database.getCollection("test");
+ collection.find().first();
+
+ // Assert that the emitted tracing span includes the db.query.text attribute.
+ assertEquals(2, inMemoryOtel.getFinishedSpans().size(), "Spans should be emitted when instrumentation is disabled.");
+ assertEquals("find", inMemoryOtel.getFinishedSpans().get(0).getName());
+
+ Map.Entry queryTag = inMemoryOtel.getFinishedSpans().get(0).getTags().entrySet()
+ .stream()
+ .filter(entry -> entry.getKey().equals(QUERY_TEXT.asString()))
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("Attribute " + QUERY_TEXT.asString() + " not found."));
+ assertEquals(46, queryTag.getValue().length(), "Query text length should be 46."); // 42 truncated string + " ..."
+ } finally {
+ memoryOtelSetup.close();
+ }
+
+ memoryOtelSetup = InMemoryOtelSetup.builder().register(observationRegistry);
+ inMemoryOtel = memoryOtelSetup.getBuildingBlocks();
+ tracer = new MicrometerTracer(observationRegistry); // don't enable command payload by default
+ setEnv("OTEL_JAVA_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH", null); // Unset the environment variable
+
+ clientSettings = Fixture.getMongoClientSettingsBuilder()
+ .tracer(tracer).build();
+ try (MongoClient client = MongoClients.create(clientSettings)) {
+ MongoDatabase database = client.getDatabase(getDefaultDatabaseName());
+ MongoCollection collection = database.getCollection("test");
+ collection.find().first();
+
+ // Assert no query.text tag is emitted
+ assertTrue(
+ inMemoryOtel.getFinishedSpans().get(0).getTags().entrySet().stream()
+ .noneMatch(entry -> entry.getKey().equals(QUERY_TEXT.asString())),
+ "Tag " + QUERY_TEXT.asString() + " should not exist."
+ );
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void setEnv(final String key, final String value) throws Exception {
+ // Get the unmodifiable Map from System.getenv()
+ Map env = System.getenv();
+
+ // Use reflection to get the class of the unmodifiable map
+ Class> unmodifiableMapClass = env.getClass();
+
+ // Get the 'm' field which holds the actual modifiable map
+ Field mField = unmodifiableMapClass.getDeclaredField("m");
+ mField.setAccessible(true);
+
+ // Get the modifiable map from the 'm' field
+ Map modifiableEnv = (Map) mField.get(env);
+
+ // Modify the map
+ if (value == null) {
+ modifiableEnv.remove(key);
+ } else {
+ modifiableEnv.put(key, value);
+ }
+ }
+}
diff --git a/driver-sync/src/test/functional/com/mongodb/client/tracing/SpanTree.java b/driver-sync/src/test/functional/com/mongodb/client/tracing/SpanTree.java
new file mode 100644
index 00000000000..c5ec573ad76
--- /dev/null
+++ b/driver-sync/src/test/functional/com/mongodb/client/tracing/SpanTree.java
@@ -0,0 +1,347 @@
+/*
+ * 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.client.tracing;
+
+import com.mongodb.lang.Nullable;
+import io.micrometer.tracing.exporter.FinishedSpan;
+import io.micrometer.tracing.test.simple.SimpleSpan;
+import org.bson.BsonArray;
+import org.bson.BsonBinary;
+import org.bson.BsonDocument;
+import org.bson.BsonInt64;
+import org.bson.BsonString;
+import org.bson.BsonValue;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.function.BiConsumer;
+
+import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.CLIENT_CONNECTION_ID;
+import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.CURSOR_ID;
+import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.SERVER_CONNECTION_ID;
+import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.SERVER_PORT;
+import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.SESSION_ID;
+import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.TRANSACTION_NUMBER;
+import static org.bson.assertions.Assertions.notNull;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * Represents a tree structure of spans, where each span can have nested spans as children.
+ * This class provides methods to create a span tree from various sources and to validate the spans against expected values.
+ */
+public class SpanTree {
+ private final List roots = new ArrayList<>();
+
+ /**
+ * Creates a SpanTree from a BsonArray of spans.
+ *
+ * @param spans the BsonArray containing span documents
+ * @return a SpanTree constructed from the provided spans
+ */
+ public static SpanTree from(final BsonArray spans) {
+ SpanTree spanTree = new SpanTree();
+ for (final BsonValue span : spans) {
+ if (span.isDocument()) {
+ final BsonDocument spanDoc = span.asDocument();
+ final String name = spanDoc.getString("name").getValue();
+ final SpanNode rootNode = new SpanNode(name);
+ spanTree.roots.add(rootNode);
+
+ if (spanDoc.containsKey("attributes")) {
+ rootNode.tags = spanDoc.getDocument("attributes");
+ }
+
+ if (spanDoc.containsKey("nested")) {
+ for (final BsonValue nestedSpan : spanDoc.getArray("nested")) {
+ addNestedSpans(rootNode, nestedSpan.asDocument());
+ }
+ }
+ }
+ }
+
+ return spanTree;
+ }
+
+ /**
+ * Creates a SpanTree from a JSON string representation of spans.
+ *
+ * @param spansAsJson the JSON string containing span documents
+ * @return a SpanTree constructed from the provided JSON spans
+ */
+ public static SpanTree from(final String spansAsJson) {
+ BsonArray spans = BsonArray.parse(spansAsJson);
+ return from(spans);
+ }
+
+ /**
+ * Creates a SpanTree from a Deque of SimpleSpan objects.
+ * This method is typically used to build a tree based on the actual collected tracing spans.
+ *
+ * @param spans the Deque containing SimpleSpan objects
+ * @return a SpanTree constructed from the provided spans
+ */
+ public static SpanTree from(final Deque spans) {
+ final SpanTree spanTree = new SpanTree();
+ final Map idToSpanNode = new HashMap<>();
+ for (final SimpleSpan span : spans) {
+ final SpanNode spanNode = new SpanNode(span.getName());
+ for (final Map.Entry tag : span.getTags().entrySet()) {
+ // handle special case of session id (needs to be parsed into a BsonBinary)
+ // this is needed because the SimpleTracer reports all the collected tags as strings
+ if (tag.getKey().equals(SESSION_ID.asString())) {
+ spanNode.tags.append(tag.getKey(), new BsonDocument().append("id", new BsonBinary(UUID.fromString(tag.getValue()))));
+
+ } else if (tag.getKey().equals(CURSOR_ID.asString())
+ || tag.getKey().equals(SERVER_PORT.asString())
+ || tag.getKey().equals(TRANSACTION_NUMBER.asString())
+ || tag.getKey().equals(CLIENT_CONNECTION_ID.asString())
+ || tag.getKey().equals(SERVER_CONNECTION_ID.asString())) {
+ spanNode.tags.append(tag.getKey(), new BsonInt64(Long.parseLong(tag.getValue())));
+ } else {
+ spanNode.tags.append(tag.getKey(), new BsonString(tag.getValue()));
+ }
+ }
+ idToSpanNode.put(span.context().spanId(), spanNode);
+ }
+
+ for (final SimpleSpan span : spans) {
+ final String parentId = span.context().parentId();
+ final SpanNode node = idToSpanNode.get(span.context().spanId());
+
+ if (!parentId.isEmpty() && idToSpanNode.containsKey(parentId)) {
+ idToSpanNode.get(parentId).children.add(node);
+ } else { // doesn't have a parent, so it is a root node
+ spanTree.roots.add(node);
+ }
+ }
+ return spanTree;
+ }
+
+ public static SpanTree from(final List spans) {
+ final SpanTree spanTree = new SpanTree();
+ final Map idToSpanNode = new HashMap<>();
+ for (final FinishedSpan span : spans) {
+ final SpanNode spanNode = new SpanNode(span.getName());
+ for (final Map.Entry tag : span.getTags().entrySet()) {
+ // handle special case of session id (needs to be parsed into a BsonBinary)
+ // this is needed because the SimpleTracer reports all the collected tags as strings
+ if (tag.getKey().equals(SESSION_ID.asString())) {
+ spanNode.tags.append(tag.getKey(), new BsonDocument().append("id", new BsonBinary(UUID.fromString(tag.getValue()))));
+
+ } else if (tag.getKey().equals(CURSOR_ID.asString())
+ || tag.getKey().equals(SERVER_PORT.asString())
+ || tag.getKey().equals(TRANSACTION_NUMBER.asString())
+ || tag.getKey().equals(CLIENT_CONNECTION_ID.asString())
+ || tag.getKey().equals(SERVER_CONNECTION_ID.asString())) {
+ spanNode.tags.append(tag.getKey(), new BsonInt64(Long.parseLong(tag.getValue())));
+ } else {
+ spanNode.tags.append(tag.getKey(), new BsonString(tag.getValue()));
+ }
+ }
+ idToSpanNode.put(span.getSpanId(), spanNode);
+ }
+
+ for (final FinishedSpan span : spans) {
+ final String parentId = span.getParentId();
+ final SpanNode node = idToSpanNode.get(span.getSpanId());
+
+ if (parentId != null && !parentId.isEmpty() && idToSpanNode.containsKey(parentId)) {
+ idToSpanNode.get(parentId).children.add(node);
+ } else { // doesn't have a parent, so it is a root node
+ spanTree.roots.add(node);
+ }
+ }
+ return spanTree;
+ }
+
+ /**
+ * Adds nested spans to the parent node based on the provided BsonDocument.
+ * This method recursively adds child spans to the parent span node.
+ *
+ * @param parentNode the parent span node to which nested spans will be added
+ * @param nestedSpan the BsonDocument representing a nested span
+ */
+ private static void addNestedSpans(final SpanNode parentNode, final BsonDocument nestedSpan) {
+ final String name = nestedSpan.getString("name").getValue();
+ final SpanNode childNode = new SpanNode(name, parentNode);
+
+ if (nestedSpan.containsKey("attributes")) {
+ childNode.tags = nestedSpan.getDocument("attributes");
+ }
+
+ if (nestedSpan.containsKey("nested")) {
+ for (final BsonValue nested : nestedSpan.getArray("nested")) {
+ addNestedSpans(childNode, nested.asDocument());
+ }
+ }
+ }
+
+ /**
+ * Asserts that the reported spans are valid against the expected spans.
+ * This method checks that the reported spans match the expected spans in terms of names, tags, and structure.
+ *
+ * @param reportedSpans the SpanTree containing the reported spans
+ * @param expectedSpans the SpanTree containing the expected spans
+ * @param valueMatcher a BiConsumer to match values of tags between reported and expected spans
+ * @param ignoreExtraSpans if true, allows reported spans to contain extra spans not present in expected spans
+ */
+ public static void assertValid(final SpanTree reportedSpans, final SpanTree expectedSpans,
+ final BiConsumer valueMatcher,
+ final boolean ignoreExtraSpans) {
+ if (ignoreExtraSpans) {
+ // remove from the reported spans all the nodes that are not expected
+ reportedSpans.roots.removeIf(node ->
+ expectedSpans.roots.stream().noneMatch(expectedNode -> expectedNode.getName().equalsIgnoreCase(node.getName()))
+ );
+ }
+
+ // check that we have the same root spans
+ if (reportedSpans.roots.size() != expectedSpans.roots.size()) {
+ fail("The number of reported spans does not match expected spans size. "
+ + "Reported: " + reportedSpans.roots.size()
+ + ", Expected: " + expectedSpans.roots.size()
+ + " ignoreExtraSpans: " + ignoreExtraSpans);
+ }
+
+ for (int i = 0; i < reportedSpans.roots.size(); i++) {
+ assertValid(reportedSpans.roots.get(i), expectedSpans.roots.get(i), valueMatcher);
+ }
+ }
+
+ /**
+ * Asserts that a reported span node is valid against an expected span node.
+ * This method checks that the reported span's name, tags, and children match the expected span.
+ *
+ * @param reportedNode the reported span node to validate
+ * @param expectedNode the expected span node to validate against
+ * @param valueMatcher a BiConsumer to match values of tags between reported and expected spans
+ */
+ private static void assertValid(final SpanNode reportedNode, final SpanNode expectedNode,
+ final BiConsumer valueMatcher) {
+ // Check that the span names match
+ if (!reportedNode.getName().equalsIgnoreCase(expectedNode.getName())) {
+ fail("Reported span name "
+ + reportedNode.getName()
+ + " does not match expected span name "
+ + expectedNode.getName());
+ }
+
+ valueMatcher.accept(expectedNode.tags, reportedNode.tags);
+
+ // Spans should have the same number of children
+ if (reportedNode.children.size() != expectedNode.children.size()) {
+ fail("Reported span " + reportedNode.getName()
+ + " has " + reportedNode.children.size()
+ + " children, but expected " + expectedNode.children.size());
+ }
+
+ // For every reported child span make sure it is valid against the expected child span
+ for (int i = 0; i < reportedNode.children.size(); i++) {
+ assertValid(reportedNode.children.get(i), expectedNode.children.get(i), valueMatcher);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "SpanTree{"
+ + "roots=" + roots
+ + '}';
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ final SpanTree spanTree = (SpanTree) o;
+ return Objects.deepEquals(roots, spanTree.roots);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(roots);
+ }
+
+ /**
+ * Represents a node in the span tree, which can have nested child spans.
+ * Each span node contains a name, tags, and a list of child span nodes.
+ */
+ public static class SpanNode {
+ private final String name;
+ private BsonDocument tags = new BsonDocument();
+ private final List children = new ArrayList<>();
+
+ public SpanNode(final String name) {
+ this.name = notNull("name", name);
+ }
+
+ public SpanNode(final String name, @Nullable final SpanNode parent) {
+ this.name = notNull("name", name);
+ if (parent != null) {
+ parent.children.add(this);
+ }
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public List getChildren() {
+ return Collections.unmodifiableList(children);
+ }
+
+ @Override
+ public String toString() {
+ return "SpanNode{"
+ + "name='" + name + '\''
+ + ", tags=" + tags
+ + ", children=" + children
+ + '}';
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ final SpanNode spanNode = (SpanNode) o;
+ return name.equalsIgnoreCase(spanNode.name)
+ && Objects.equals(tags, spanNode.tags)
+ && Objects.equals(children, spanNode.children);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = name.hashCode();
+ result = 31 * result + tags.hashCode();
+ result = 31 * result + children.hashCode();
+ return result;
+ }
+ }
+}
diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java b/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java
index 6f6e5bb66c8..d8497a97143 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java
@@ -47,6 +47,9 @@
import com.mongodb.internal.logging.LogMessage;
import com.mongodb.lang.Nullable;
import com.mongodb.logging.TestLoggingInterceptor;
+import com.mongodb.tracing.MicrometerTracer;
+import io.micrometer.observation.ObservationRegistry;
+import io.micrometer.tracing.test.reporter.inmemory.InMemoryOtelSetup;
import org.bson.BsonArray;
import org.bson.BsonBoolean;
import org.bson.BsonDocument;
@@ -90,7 +93,7 @@ public final class Entities {
private static final Set SUPPORTED_CLIENT_ENTITY_OPTIONS = new HashSet<>(
asList(
"id", "autoEncryptOpts", "uriOptions", "serverApi", "useMultipleMongoses", "storeEventsAsEntities",
- "observeEvents", "observeLogMessages", "observeSensitiveCommands", "ignoreCommandMonitoringEvents"));
+ "observeEvents", "observeLogMessages", "observeSensitiveCommands", "ignoreCommandMonitoringEvents", "observeTracingMessages"));
private final Set entityNames = new HashSet<>();
private final Map threads = new HashMap<>();
private final Map>> tasks = new HashMap<>();
@@ -104,6 +107,8 @@ public final class Entities {
private final Map clientEncryptions = new HashMap<>();
private final Map clientCommandListeners = new HashMap<>();
private final Map clientLoggingInterceptors = new HashMap<>();
+ private final Map clientTracing = new HashMap<>();
+ private final Set inMemoryOTelInstances = new HashSet<>();
private final Map clientConnectionPoolListeners = new HashMap<>();
private final Map clientServerListeners = new HashMap<>();
private final Map clientClusterListeners = new HashMap<>();
@@ -220,6 +225,10 @@ public TestLoggingInterceptor getClientLoggingInterceptor(final String id) {
return getEntity(id + "-logging-interceptor", clientLoggingInterceptors, "logging interceptor");
}
+ public InMemoryOtelSetup.Builder.OtelBuildingBlocks getClientTracer(final String id) {
+ return getEntity(id + "-tracing", clientTracing, "micrometer tracing");
+ }
+
public TestConnectionPoolListener getConnectionPoolListener(final String id) {
return getEntity(id + "-connection-pool-listener", clientConnectionPoolListeners, "connection pool listener");
}
@@ -565,6 +574,17 @@ private void initClient(final BsonDocument entity, final String id,
clientSettingsBuilder.autoEncryptionSettings(builder.build());
}
+ if (entity.containsKey("observeTracingMessages")) {
+ boolean enableCommandPayload = entity.getDocument("observeTracingMessages").get("enableCommandPayload", BsonBoolean.FALSE).asBoolean().getValue();
+ ObservationRegistry observationRegistry = ObservationRegistry.create();
+ InMemoryOtelSetup inMemoryOtel = InMemoryOtelSetup.builder().register(observationRegistry);
+ InMemoryOtelSetup.Builder.OtelBuildingBlocks tracer = inMemoryOtel.getBuildingBlocks();
+
+ putEntity(id + "-tracing", tracer, clientTracing);
+ inMemoryOTelInstances.add(inMemoryOtel);
+ clientSettingsBuilder.tracer(new MicrometerTracer(observationRegistry, enableCommandPayload));
+ }
+
MongoClientSettings clientSettings = clientSettingsBuilder.build();
if (entity.containsKey("observeLogMessages")) {
@@ -756,5 +776,6 @@ public void close() {
clients.values().forEach(MongoClient::close);
clientLoggingInterceptors.values().forEach(TestLoggingInterceptor::close);
threads.values().forEach(ExecutorService::shutdownNow);
+ inMemoryOTelInstances.forEach(InMemoryOtelSetup::close);
}
}
diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/MicrometerTracingTest.java b/driver-sync/src/test/functional/com/mongodb/client/unified/MicrometerTracingTest.java
new file mode 100644
index 00000000000..8c65317d257
--- /dev/null
+++ b/driver-sync/src/test/functional/com/mongodb/client/unified/MicrometerTracingTest.java
@@ -0,0 +1,27 @@
+/*
+ * 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.client.unified;
+
+import org.junit.jupiter.params.provider.Arguments;
+
+import java.util.Collection;
+
+final class MicrometerTracingTest extends UnifiedSyncTest {
+ private static Collection data() {
+ return getTestData("open-telemetry/tests");
+ }
+}
diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java
index 79b2a9c9da9..f64d219c953 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java
@@ -28,6 +28,7 @@
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.model.Filters;
import com.mongodb.client.test.CollectionHelper;
+import com.mongodb.client.tracing.SpanTree;
import com.mongodb.client.unified.UnifiedTestModifications.TestDef;
import com.mongodb.client.vault.ClientEncryption;
import com.mongodb.connection.ClusterDescription;
@@ -44,6 +45,7 @@
import com.mongodb.lang.Nullable;
import com.mongodb.logging.TestLoggingInterceptor;
import com.mongodb.test.AfterBeforeParameterResolver;
+import io.micrometer.tracing.test.reporter.inmemory.InMemoryOtelSetup;
import org.bson.BsonArray;
import org.bson.BsonBoolean;
import org.bson.BsonDocument;
@@ -109,7 +111,7 @@ public abstract class UnifiedTest {
private static final Set PRESTART_POOL_ASYNC_WORK_MANAGER_FILE_DESCRIPTIONS = Collections.singleton(
"wait queue timeout errors include details about checked out connections");
- private static final String MAX_SUPPORTED_SCHEMA_VERSION = "1.25";
+ private static final String MAX_SUPPORTED_SCHEMA_VERSION = "1.27";
private static final List MAX_SUPPORTED_SCHEMA_VERSION_COMPONENTS = Arrays.stream(MAX_SUPPORTED_SCHEMA_VERSION.split("\\."))
.map(Integer::parseInt)
.collect(Collectors.toList());
@@ -376,6 +378,11 @@ public void shouldPassAllOutcomes(
}
compareLogMessages(rootContext, definition, tweaks);
}
+
+ if (definition.containsKey("expectTracingMessages")) {
+ compareTracingSpans(definition);
+ }
+
} catch (TestAbortedException e) {
// if a test is ignored, we do not retry
throw e;
@@ -483,6 +490,22 @@ private void compareLogMessages(final UnifiedTestContext rootContext, final Bson
}
}
+ private void compareTracingSpans(final BsonDocument definition) {
+ BsonArray curTracingSpansForClients = definition.getArray("expectTracingMessages");
+ for (BsonValue tracingSpan : curTracingSpansForClients) {
+ BsonDocument curTracingSpansForClient = tracingSpan.asDocument();
+ String clientId = curTracingSpansForClient.getString("client").getValue();
+
+ // Get the tracer for the client
+ InMemoryOtelSetup.Builder.OtelBuildingBlocks micrometerTracer = entities.getClientTracer(clientId);
+
+ SpanTree expectedSpans = SpanTree.from(curTracingSpansForClient.getArray("spans"));
+ SpanTree reportedSpans = SpanTree.from(micrometerTracer.getFinishedSpans());
+ boolean ignoreExtraSpans = curTracingSpansForClient.getBoolean("ignoreExtraSpans", BsonBoolean.TRUE).getValue();
+ SpanTree.assertValid(reportedSpans, expectedSpans, rootContext.valueMatcher::assertValuesMatch, ignoreExtraSpans);
+ }
+ }
+
private void assertOutcome(final UnifiedTestContext context) {
for (BsonValue cur : definition.getArray("outcome")) {
BsonDocument curDocument = cur.asDocument();
diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java
index 327cc3f3da8..dbaa9b600b0 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java
@@ -164,6 +164,10 @@ public static void applyCustomizations(final TestDef def) {
.test("client-side-operations-timeout", "timeoutMS can be configured on a MongoClient",
"timeoutMS can be set to 0 on a MongoClient - dropIndexes on collection");
+ // There are more than 44 tests using 'awaitMinPoolSizeMS' this will be fixed in JAVA-5957
+ def.skipJira("https://jira.mongodb.org/browse/JAVA-5957")
+ .directory("client-side-operations-timeout");
+
// TODO-JAVA-5712
// collection-management
diff --git a/driver-sync/src/test/unit/com/mongodb/client/internal/MongoClusterSpecification.groovy b/driver-sync/src/test/unit/com/mongodb/client/internal/MongoClusterSpecification.groovy
index 563528e7dce..975b0114ff4 100644
--- a/driver-sync/src/test/unit/com/mongodb/client/internal/MongoClusterSpecification.groovy
+++ b/driver-sync/src/test/unit/com/mongodb/client/internal/MongoClusterSpecification.groovy
@@ -28,6 +28,7 @@ import com.mongodb.internal.TimeoutSettings
import com.mongodb.internal.client.model.changestream.ChangeStreamLevel
import com.mongodb.internal.connection.Cluster
import com.mongodb.internal.session.ServerSessionPool
+import com.mongodb.internal.tracing.TracingManager
import org.bson.BsonDocument
import org.bson.Document
import org.bson.codecs.UuidCodec
@@ -258,6 +259,7 @@ class MongoClusterSpecification extends Specification {
MongoClusterImpl createMongoCluster(final MongoClientSettings settings, final OperationExecutor operationExecutor) {
new MongoClusterImpl(null, cluster, settings.codecRegistry, null, null,
originator, operationExecutor, settings.readConcern, settings.readPreference, settings.retryReads, settings.retryWrites,
- null, serverSessionPool, TimeoutSettings.create(settings), settings.uuidRepresentation, settings.writeConcern)
+ null, serverSessionPool, TimeoutSettings.create(settings), settings.uuidRepresentation,
+ settings.writeConcern, TracingManager.NO_OP)
}
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 8b8222d66e5..057e3bc5430 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -24,6 +24,8 @@ reactive-streams = "1.0.4"
snappy = "1.1.10.3"
zstd = "1.5.5-3"
jetbrains-annotations = "26.0.2"
+micrometer = "1.6.0-M3" # This version has a fix for https://github.com/micrometer-metrics/tracing/issues/1092
+micrometer-observation = "1.15.4"
kotlin = "1.8.10"
kotlinx-coroutines-bom = "1.6.4"
@@ -93,6 +95,7 @@ reactive-streams = { module = " org.reactivestreams:reactive-streams", version.r
slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
snappy-java = { module = "org.xerial.snappy:snappy-java", version.ref = "snappy" }
zstd-jni = { module = "com.github.luben:zstd-jni", version.ref = "zstd" }
+micrometer-observation = { module = "io.micrometer:micrometer-observation", version.ref = "micrometer-observation" }
graal-sdk = { module = "org.graalvm.sdk:graal-sdk", version.ref = "graal-sdk" }
graal-sdk-nativeimage = { module = "org.graalvm.sdk:nativeimage", version.ref = "graal-sdk" }
@@ -171,7 +174,7 @@ objenesis = { module = "org.objenesis:objenesis", version.ref = "objenesis" }
project-reactor-test = { module = "io.projectreactor:reactor-test" }
reactive-streams-tck = { module = " org.reactivestreams:reactive-streams-tck", version.ref = "reactive-streams" }
reflections = { module = "org.reflections:reflections", version.ref = "reflections" }
-
+micrometer-tracing-integration-test = { module = " io.micrometer:micrometer-tracing-integration-test", version.ref = "micrometer" }
[bundles]
aws-java-sdk-v1 = ["aws-java-sdk-v1-core", "aws-java-sdk-v1-sts"]