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 +1016,82 @@ 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("Command " + commandName, operationSpan != null ? operationSpan.context() : null)
+ .tag(SYSTEM, "mongodb")
+ .tag(NAMESPACE, message.getNamespace().getDatabaseName())
+ .tag(COLLECTION, message.getCollectionName())
+ .tag(QUERY_SUMMARY, commandName)
+ .tag(COMMAND_NAME, commandName)
+ .tag(NETWORK_TRANSPORT, getServerAddress() instanceof UnixServerAddress ? "unix" : "tcp");
+
+ if (command.containsKey("getMore")) {
+ long cursorId = command.getInt64("getMore").longValue();
+ span.tag(CURSOR_ID, cursorId);
+ if (operationSpan != null) {
+ operationSpan.tag(CURSOR_ID, cursorId);
+ }
+ }
+
+ tagServerAndConnectionInfo(span, message);
+ tagSessionAndTransactionInfo(span, operationContext);
+
+ return span;
+ }
+
+ private void tagServerAndConnectionInfo(final Span span, final CommandMessage message) {
+ span.tag(SERVER_ADDRESS, serverId.getAddress().getHost())
+ .tag(SERVER_PORT, String.valueOf(serverId.getAddress().getPort()))
+ .tag(SERVER_TYPE, message.getSettings().getServerType().name())
+ .tag(CLIENT_CONNECTION_ID, this.description.getConnectionId().toString())
+ .tag(SERVER_CONNECTION_ID, String.valueOf(this.description.getConnectionId().getServerValue()));
+ }
+
+ private void tagSessionAndTransactionInfo(final Span span, final OperationContext operationContext) {
+ SessionContext sessionContext = operationContext.getSessionContext();
+ if (sessionContext.hasSession() && !sessionContext.isImplicitSession()) {
+ span.tag(TRANSACTION_NUMBER, String.valueOf(sessionContext.getTransactionNumber()))
+ .tag(SESSION_ID, 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 3821ca947c6..136ec10a1dd 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..bc4a785d545 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.internal.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 6ebcfda6dbe..ba033eb949a 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..ad380781f73 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java
@@ -182,6 +182,11 @@ public String getCommandName() {
return "bulkWrite";
}
+ @Override
+ public MongoNamespace getNamespace() {
+ return getNamespacedModel(models, 0).getNamespace();
+ }
+
@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 5f61f2980f8..6bdbfa7bbcd 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 04d4d7afd67..b18fc4ca1e0 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/Span.java b/driver-core/src/main/com/mongodb/internal/tracing/Span.java
new file mode 100644
index 00000000000..41f6ef0afdf
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/tracing/Span.java
@@ -0,0 +1,117 @@
+/*
+ * 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;
+
+
+/**
+ * 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.6
+ */
+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 Span tag(final String key, final String value) {
+ return this;
+ }
+
+ @Override
+ public Span tag(final String key, final Long value) {
+ return this;
+ }
+
+ @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;
+ }
+ };
+
+ /**
+ * Adds a tag to the span with a key-value pair.
+ *
+ * @param key The tag key.
+ * @param value The tag value.
+ * @return The current instance of the span.
+ */
+ Span tag(String key, String value);
+
+ /**
+ * Adds a tag to the span with a key and a numeric value.
+ *
+ * @param key The tag key.
+ * @param value The numeric tag value.
+ * @return The current instance of the span.
+ */
+ Span tag(String key, Long value);
+
+ /**
+ * 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();
+}
diff --git a/driver-core/src/main/com/mongodb/internal/tracing/Tags.java b/driver-core/src/main/com/mongodb/internal/tracing/Tags.java
new file mode 100644
index 00000000000..c7261a63af4
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/tracing/Tags.java
@@ -0,0 +1,49 @@
+/*
+ * 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;
+
+/**
+ * Contains constant tag names used for tracing and monitoring MongoDB operations.
+ * These tags are typically used to annotate spans or events with relevant metadata.
+ *
+ * @since 5.6
+ */
+public final class Tags {
+ private Tags() {
+ }
+
+ public static final String SYSTEM = "db.system";
+ public static final String NAMESPACE = "db.namespace";
+ public static final String COLLECTION = "db.collection.name";
+ public static final String OPERATION_NAME = "db.operation.name";
+ public static final String COMMAND_NAME = "db.command.name";
+ public static final String NETWORK_TRANSPORT = "network.transport";
+ public static final String OPERATION_SUMMARY = "db.operation.summary";
+ public static final String QUERY_SUMMARY = "db.query.summary";
+ public static final String QUERY_TEXT = "db.query.text";
+ public static final String CURSOR_ID = "db.mongodb.cursor_id";
+ public static final String SERVER_ADDRESS = "server.address";
+ public static final String SERVER_PORT = "server.port";
+ public static final String SERVER_TYPE = "server.type";
+ public static final String CLIENT_CONNECTION_ID = "db.mongodb.driver_connection_id";
+ public static final String SERVER_CONNECTION_ID = "db.mongodb.server_connection_id";
+ public static final String TRANSACTION_NUMBER = "db.mongodb.txnNumber";
+ public static final String SESSION_ID = "db.mongodb.lsid";
+ public static final String EXCEPTION_STACKTRACE = "exception.stacktrace";
+ public static final String EXCEPTION_TYPE = "exception.type";
+ public static final String EXCEPTION_MESSAGE = "exception.message";
+}
diff --git a/driver-core/src/main/com/mongodb/internal/tracing/TraceContext.java b/driver-core/src/main/com/mongodb/internal/tracing/TraceContext.java
new file mode 100644
index 00000000000..cb2f6ef1020
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/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.internal.tracing;
+
+@SuppressWarnings("InterfaceIsType")
+public interface TraceContext {
+ TraceContext EMPTY = new TraceContext() {
+ };
+}
diff --git a/driver-core/src/main/com/mongodb/internal/tracing/Tracer.java b/driver-core/src/main/com/mongodb/internal/tracing/Tracer.java
new file mode 100644
index 00000000000..c2881e9e2fb
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/tracing/Tracer.java
@@ -0,0 +1,97 @@
+/*
+ * 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;
+
+/**
+ * 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.
+ *
+ *
+ * @since 5.6
+ */
+public interface Tracer {
+ Tracer NO_OP = new Tracer() {
+
+ @Override
+ public TraceContext currentContext() {
+ return TraceContext.EMPTY;
+ }
+
+ @Override
+ public Span nextSpan(final String name) {
+ return Span.EMPTY;
+ }
+
+ @Override
+ public Span nextSpan(final String name, @Nullable final TraceContext parent) {
+ return Span.EMPTY;
+ }
+
+ @Override
+ public boolean enabled() {
+ return false;
+ }
+
+ @Override
+ public boolean includeCommandPayload() {
+ return false;
+ }
+ };
+
+ /**
+ * Retrieves the current trace context from the Micrometer tracer.
+ *
+ * @return A {@link TraceContext} representing the underlying {@link io.micrometer.tracing.TraceContext}.
+ * exists.
+ */
+ TraceContext currentContext();
+
+ /**
+ * Creates a new span with the specified name.
+ *
+ * @param name The name of the span.
+ * @return A {@link Span} representing the newly created span.
+ */
+ Span nextSpan(String name); // uses current active span
+
+ /**
+ * 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.
+ * @return A {@link Span} representing the newly created span.
+ */
+ Span nextSpan(String name, @Nullable TraceContext parent); // manually attach the next span to the provided parent
+
+ /**
+ * 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(); // whether the tracer allows command payloads in the trace context
+}
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..6db92f0d75a
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java
@@ -0,0 +1,112 @@
+/*
+ * 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 static com.mongodb.internal.tracing.Tags.SYSTEM;
+import static java.lang.System.getenv;
+
+/**
+ * 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 static final String ENV_ALLOW_COMMAND_PAYLOAD = "MONGODB_TRACING_ALLOW_COMMAND_PAYLOAD";
+
+ private final Tracer tracer;
+ private final TraceContext parentContext;
+ private final boolean enableCommandPayload;
+
+ /**
+ * Constructs a new TracingManager with the specified tracer.
+ *
+ * @param tracer The tracer to use for tracing operations.
+ */
+ public TracingManager(final Tracer tracer) {
+ this(tracer, tracer.currentContext());
+ }
+
+ /**
+ * Constructs a new TracingManager with the specified tracer and parent context.
+ * Setting the environment variable {@code MONGODB_TRACING_ALLOW_COMMAND_PAYLOAD} to "true" will enable command payload tracing.
+ *
+ * @param tracer The tracer to use for tracing operations.
+ * @param parentContext The parent trace context.
+ */
+ public TracingManager(final Tracer tracer, final TraceContext parentContext) {
+ this.tracer = tracer;
+ this.parentContext = parentContext;
+ String envAllowCommandPayload = getenv(ENV_ALLOW_COMMAND_PAYLOAD);
+ if (envAllowCommandPayload != null) {
+ this.enableCommandPayload = Boolean.parseBoolean(envAllowCommandPayload);
+ } else {
+ 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);
+ }
+
+ /**
+ * Creates a new transaction span for the specified server session.
+ *
+ * @return The created transaction span.
+ */
+ public Span addTransactionSpan() {
+ Span span = tracer.nextSpan("transaction", parentContext);
+ span.tag(SYSTEM, "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..d975f6931e0
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/tracing/TransactionSpan.java
@@ -0,0 +1,110 @@
+/*
+ * 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;
+
+/**
+ * State class for transaction tracing.
+ */
+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..7ed0119d21d
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java
@@ -0,0 +1,181 @@
+/*
+ * 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.internal.tracing.Span;
+import com.mongodb.internal.tracing.TraceContext;
+import com.mongodb.internal.tracing.Tracer;
+import com.mongodb.lang.Nullable;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import static com.mongodb.internal.tracing.Tags.EXCEPTION_MESSAGE;
+import static com.mongodb.internal.tracing.Tags.EXCEPTION_STACKTRACE;
+import static com.mongodb.internal.tracing.Tags.EXCEPTION_TYPE;
+
+/**
+ * A {@link Tracer} implementation that delegates tracing operations to a Micrometer {@link io.micrometer.tracing.Tracer}.
+ *
+ * This class enables integration of MongoDB driver tracing with Micrometer-based tracing systems.
+ * It provides methods to create and manage spans using the Micrometer tracing API.
+ *
+ *
+ * @since 5.6
+ */
+public class MicrometerTracer implements Tracer {
+ private final io.micrometer.tracing.Tracer tracer;
+ private final boolean allowCommandPayload;
+
+ /**
+ * Constructs a new {@link MicrometerTracer} instance.
+ *
+ * @param tracer The Micrometer {@link io.micrometer.tracing.Tracer} to delegate tracing operations to.
+ */
+ public MicrometerTracer(final io.micrometer.tracing.Tracer tracer) {
+ this(tracer, false);
+ }
+
+ /**
+ * Constructs a new {@link MicrometerTracer} instance with an option to allow command payloads.
+ *
+ * @param tracer The Micrometer {@link io.micrometer.tracing.Tracer} to delegate tracing operations to.
+ * @param allowCommandPayload Whether to allow command payloads in the trace context.
+ */
+ public MicrometerTracer(final io.micrometer.tracing.Tracer tracer, final boolean allowCommandPayload) {
+ this.tracer = tracer;
+ this.allowCommandPayload = allowCommandPayload;
+ }
+
+ @Override
+ public TraceContext currentContext() {
+ return new MicrometerTraceContext(tracer.currentTraceContext().context());
+ }
+
+ @Override
+ public Span nextSpan(final String name) {
+ return new MicrometerSpan(tracer.nextSpan().name(name).start());
+ }
+
+ @Override
+ public Span nextSpan(final String name, @Nullable final TraceContext parent) {
+ if (parent instanceof MicrometerTraceContext) {
+ io.micrometer.tracing.TraceContext micrometerContext = ((MicrometerTraceContext) parent).getTraceContext();
+ if (micrometerContext != null) {
+ return new MicrometerSpan(tracer.spanBuilder()
+ .name(name)
+ .setParent(micrometerContext)
+ .start());
+ }
+ }
+ return nextSpan(name);
+ }
+
+ @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 io.micrometer.tracing.TraceContext traceContext;
+
+ /**
+ * Constructs a new {@link MicrometerTraceContext} instance.
+ *
+ * @param traceContext The Micrometer {@link io.micrometer.tracing.TraceContext}, or null if none exists.
+ */
+ MicrometerTraceContext(@Nullable final io.micrometer.tracing.TraceContext traceContext) {
+ this.traceContext = traceContext;
+ }
+
+ /**
+ * Retrieves the underlying Micrometer trace context.
+ *
+ * @return The Micrometer {@link io.micrometer.tracing.TraceContext}, or null if none exists.
+ */
+ @Nullable
+ public io.micrometer.tracing.TraceContext getTraceContext() {
+ return traceContext;
+ }
+ }
+
+ /**
+ * Represents a Micrometer-based span.
+ */
+ private static class MicrometerSpan implements Span {
+ private final io.micrometer.tracing.Span span;
+
+ /**
+ * Constructs a new {@link MicrometerSpan} instance.
+ *
+ * @param span The Micrometer {@link io.micrometer.tracing.Span} to delegate operations to.
+ */
+ MicrometerSpan(final io.micrometer.tracing.Span span) {
+ this.span = span;
+ }
+
+ @Override
+ public Span tag(final String key, final String value) {
+ span.tag(key, value);
+ return this;
+ }
+
+ @Override
+ public Span tag(final String key, final Long value) {
+ span.tag(key, value);
+ return this;
+ }
+
+ @Override
+ public void event(final String event) {
+ span.event(event);
+ }
+
+ @Override
+ public void error(final Throwable throwable) {
+ span.tag(EXCEPTION_MESSAGE, throwable.getMessage());
+ span.tag(EXCEPTION_TYPE, throwable.getClass().getName());
+ span.tag(EXCEPTION_STACKTRACE, getStackTraceAsString(throwable));
+ span.error(throwable);
+ }
+
+ @Override
+ public void end() {
+ span.end();
+ }
+
+ @Override
+ public TraceContext context() {
+ return new MicrometerTraceContext(span.context());
+ }
+
+ 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/package-info.java b/driver-core/src/main/com/mongodb/tracing/package-info.java
new file mode 100644
index 00000000000..43e82603a09
--- /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.6
+ */
+@NonNullApi
+package com.mongodb.tracing;
+
+import com.mongodb.lang.NonNullApi;
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/resources/specifications b/driver-core/src/test/resources/specifications
index c13d23b91b4..8466c1b9df5 160000
--- a/driver-core/src/test/resources/specifications
+++ b/driver-core/src/test/resources/specifications
@@ -1 +1 @@
-Subproject commit c13d23b91b422b348c54195fe1c49406fc457559
+Subproject commit 8466c1b9df5b2c4b293bb9223b55aa16d34e9e0f
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 6f8eaf33314..35bced971fa 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
@@ -66,7 +66,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()
@@ -111,7 +112,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()
@@ -169,7 +170,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()
@@ -202,7 +203,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..dd127a4dd6a 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"))
+ integrationTestImplementation(libs.micrometer)
}
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..b6113200628 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"))
+ integrationTestImplementation(libs.micrometer)
}
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-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..f9ac5301625 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)
}
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/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..4e93a331776 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,8 @@ class ApiAliasAndCompanionSpec extends BaseSpec {
"SyncClientEncryption",
"BaseClientUpdateOptions",
"BaseClientDeleteOptions",
- "MongoBaseInterfaceAssertions"
+ "MongoBaseInterfaceAssertions",
+ "MicrometerTracer"
)
val scalaExclusions = Set(
"BuildInfo",
diff --git a/driver-sync/build.gradle.kts b/driver-sync/build.gradle.kts
index 95cd0979973..b37d0226295 100644
--- a/driver-sync/build.gradle.kts
+++ b/driver-sync/build.gradle.kts
@@ -38,6 +38,9 @@ dependencies {
// lambda testing
testImplementation(libs.aws.lambda.core)
+
+ // Tracing
+ testImplementation(libs.bundles.micrometer.test)
}
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..6af2b4be664 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.6
+ */
+ @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..db8fd03adaf 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,6 +59,11 @@
import com.mongodb.internal.operation.ReadOperation;
import com.mongodb.internal.operation.WriteOperation;
import com.mongodb.internal.session.ServerSessionPool;
+import com.mongodb.internal.tracing.Span;
+import com.mongodb.internal.tracing.Tags;
+import com.mongodb.internal.tracing.TraceContext;
+import com.mongodb.internal.tracing.TracingManager;
+import com.mongodb.internal.tracing.TransactionSpan;
import com.mongodb.lang.Nullable;
import org.bson.BsonDocument;
import org.bson.Document;
@@ -99,6 +106,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 +114,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 +132,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 +176,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 +259,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 +429,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 +440,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 +462,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 +524,7 @@ private OperationContext getOperationContext(final ClientSession session, final
getRequestContext(),
new ReadConcernAwareNoOpSessionContext(readConcern),
createTimeoutContext(session, executorTimeoutSettings),
+ tracingManager,
serverApi,
commandName);
}
@@ -556,5 +582,41 @@ 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.getFullName();
+ Span span = binding
+ .getOperationContext()
+ .getTracingManager()
+ .addSpan(name, parentContext);
+ binding.getOperationContext().setTracingSpan(span);
+ span.tag(Tags.SYSTEM, "mongodb");
+ span.tag(Tags.NAMESPACE, namespace.getDatabaseName());
+ span.tag(Tags.COLLECTION, namespace.getCollectionName());
+ span.tag(Tags.OPERATION_NAME, commandName);
+ span.tag(Tags.OPERATION_SUMMARY, name);
+ return span;
+
+ } else {
+ return null;
+ }
+ }
}
}
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..bca04e6647a
--- /dev/null
+++ b/driver-sync/src/test/functional/com/mongodb/client/tracing/SpanTree.java
@@ -0,0 +1,295 @@
+/*
+ * 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.internal.tracing.Tags;
+import com.mongodb.lang.Nullable;
+import io.micrometer.tracing.test.simple.SimpleSpan;
+import org.bson.BsonArray;
+import org.bson.BsonBinary;
+import org.bson.BsonDocument;
+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 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("tags")) {
+ rootNode.tags = spanDoc.getDocument("tags");
+ }
+
+ 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(Tags.SESSION_ID)) {
+ spanNode.tags.append(tag.getKey(), new BsonDocument().append("id", new BsonBinary(UUID.fromString(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;
+ }
+
+ /**
+ * 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("tags")) {
+ childNode.tags = nestedSpan.getDocument("tags");
+ }
+
+ 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.contains(node));
+
+ }
+
+ // 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/tracing/ZipkinTracer.java b/driver-sync/src/test/functional/com/mongodb/client/tracing/ZipkinTracer.java
new file mode 100644
index 00000000000..186835c6ca2
--- /dev/null
+++ b/driver-sync/src/test/functional/com/mongodb/client/tracing/ZipkinTracer.java
@@ -0,0 +1,95 @@
+/*
+ * 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 io.micrometer.tracing.Tracer;
+import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext;
+import io.micrometer.tracing.otel.bridge.OtelTracer;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.context.propagation.ContextPropagators;
+import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter;
+import io.opentelemetry.extension.trace.propagation.B3Propagator;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.resources.Resource;
+import io.opentelemetry.sdk.trace.SdkTracerProvider;
+import io.opentelemetry.sdk.trace.SpanProcessor;
+import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
+import io.opentelemetry.semconv.ResourceAttributes;
+
+/**
+ * A utility class to create a Zipkin tracer using OpenTelemetry protocol, useful for visualizing spans in Zipkin UI
+ * This tracer can be used to send spans to a Zipkin server.
+ *
+ * Spans are visible in the Zipkin UI at http://localhost:9411.
+ *
+ * To Start Zipkin server, you can use the following command:
+ *
{@code
+ * docker run -d -p 9411:9411 openzipkin/zipkin
+ * }
+ */
+public final class ZipkinTracer {
+ private static final String ENDPOINT = "http://localhost:9411/api/v2/spans";
+
+ private ZipkinTracer() {
+ }
+
+ /**
+ * Creates a Zipkin tracer with the specified service name.
+ *
+ * @param serviceName the name of the service to be used in the tracer
+ * @return a Tracer instance configured to send spans to Zipkin
+ */
+ public static Tracer getTracer(final String serviceName) {
+ ZipkinSpanExporter zipkinExporter = ZipkinSpanExporter.builder()
+ .setEndpoint(ENDPOINT)
+ .build();
+
+ Resource resource = Resource.getDefault()
+ .merge(Resource.create(
+ Attributes.of(
+ ResourceAttributes.SERVICE_NAME, serviceName,
+ ResourceAttributes.SERVICE_VERSION, "1.0.0"
+ )
+ ));
+
+ SpanProcessor spanProcessor = SimpleSpanProcessor.create(zipkinExporter);
+
+ SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
+ .addSpanProcessor(spanProcessor)
+ .setResource(resource)
+ .build();
+
+ OpenTelemetry openTelemetry = OpenTelemetrySdk.builder()
+ .setTracerProvider(tracerProvider)
+ .setPropagators(ContextPropagators.create(
+ B3Propagator.injectingSingleHeader()
+ ))
+ .build();
+
+ io.opentelemetry.api.trace.Tracer otelTracer = openTelemetry.getTracer("my-java-service", "1.0.0");
+
+ OtelCurrentTraceContext otelCurrentTraceContext = new OtelCurrentTraceContext();
+
+ return new OtelTracer(
+ otelTracer,
+ otelCurrentTraceContext,
+ null // EventPublisher can be null for basic usage
+ );
+ }
+
+}
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 f1429431690..c0d33b06d91 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
@@ -66,6 +66,9 @@
import com.mongodb.lang.NonNull;
import com.mongodb.lang.Nullable;
import com.mongodb.logging.TestLoggingInterceptor;
+import com.mongodb.tracing.MicrometerTracer;
+import io.micrometer.tracing.Tracer;
+import io.micrometer.tracing.test.simple.SimpleTracer;
import org.bson.BsonArray;
import org.bson.BsonBoolean;
import org.bson.BsonDocument;
@@ -112,7 +115,7 @@ public final class Entities {
private static final Set SUPPORTED_CLIENT_ENTITY_OPTIONS = new HashSet<>(
asList(
"id", "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<>();
@@ -126,6 +129,7 @@ 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 Map clientConnectionPoolListeners = new HashMap<>();
private final Map clientServerListeners = new HashMap<>();
private final Map clientClusterListeners = new HashMap<>();
@@ -294,6 +298,10 @@ public TestLoggingInterceptor getClientLoggingInterceptor(final String id) {
return getEntity(id + "-logging-interceptor", clientLoggingInterceptors, "logging interceptor");
}
+ public Tracer 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");
}
@@ -604,6 +612,22 @@ private void initClient(final BsonDocument entity, final String id,
}
clientSettingsBuilder.serverApi(serverApiBuilder.build());
}
+
+ if (entity.containsKey("observeTracingMessages")) {
+ boolean enableCommandPayload = entity.getDocument("observeTracingMessages").get("enableCommandPayload", BsonBoolean.FALSE).asBoolean().getValue();
+ /* To enable Zipkin backend, uncomment the following lines and ensure you have the server started
+ (docker run -d -p 9411:9411 openzipkin/zipkin). The tests will fail but the captured spans will be
+ visible in the Zipkin UI at http://localhost:9411 for debugging purpose.
+ *
+ * Tracer tracer = ZipkinTracer.getTracer("UTR");
+ * putEntity(id + "-tracing", new SimpleTracer(), clientTracing);
+ */
+ Tracer tracer = new SimpleTracer();
+ putEntity(id + "-tracing", tracer, clientTracing);
+
+ clientSettingsBuilder.tracer(new MicrometerTracer(tracer, enableCommandPayload));
+ }
+
MongoClientSettings clientSettings = clientSettingsBuilder.build();
if (entity.containsKey("observeLogMessages")) {
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 e067e36d993..8e91e85205b 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,8 @@
import com.mongodb.lang.Nullable;
import com.mongodb.logging.TestLoggingInterceptor;
import com.mongodb.test.AfterBeforeParameterResolver;
+import io.micrometer.tracing.Tracer;
+import io.micrometer.tracing.test.simple.SimpleTracer;
import org.bson.BsonArray;
import org.bson.BsonBoolean;
import org.bson.BsonDocument;
@@ -111,7 +114,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.22";
+ private static final String MAX_SUPPORTED_SCHEMA_VERSION = "1.26";
private static final List MAX_SUPPORTED_SCHEMA_VERSION_COMPONENTS = Arrays.stream(MAX_SUPPORTED_SCHEMA_VERSION.split("\\."))
.map(Integer::parseInt)
.collect(Collectors.toList());
@@ -380,6 +383,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;
@@ -487,6 +495,20 @@ private void compareLogMessages(final UnifiedTestContext rootContext, final Bson
}
}
+ private void compareTracingSpans(final BsonDocument definition) {
+ BsonDocument curTracingSpansForClient = definition.getDocument("expectTracingMessages");
+ String clientId = curTracingSpansForClient.getString("client").getValue();
+
+ // Get the tracer for the client
+ Tracer micrometerTracer = entities.getClientTracer(clientId);
+ SimpleTracer simpleTracer = (SimpleTracer) micrometerTracer;
+
+ SpanTree expectedSpans = SpanTree.from(curTracingSpansForClient.getArray("spans"));
+ SpanTree reportedSpans = SpanTree.from(simpleTracer.getSpans());
+ 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/unit/com/mongodb/client/internal/MongoClusterSpecification.groovy b/driver-sync/src/test/unit/com/mongodb/client/internal/MongoClusterSpecification.groovy
index 62c16330950..f7b099a604d 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..38805b02353 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -24,6 +24,9 @@ reactive-streams = "1.0.4"
snappy = "1.1.10.3"
zstd = "1.5.5-3"
jetbrains-annotations = "26.0.2"
+micrometer = "1.4.5"
+zipkin-reporter = "2.16.3"
+opentelemetry-exporter-zipkin = "1.30.0"
kotlin = "1.8.10"
kotlinx-coroutines-bom = "1.6.4"
@@ -93,6 +96,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 = { module = "io.micrometer:micrometer-tracing", version.ref = "micrometer" }
graal-sdk = { module = "org.graalvm.sdk:graal-sdk", version.ref = "graal-sdk" }
graal-sdk-nativeimage = { module = "org.graalvm.sdk:nativeimage", version.ref = "graal-sdk" }
@@ -172,6 +176,12 @@ 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-test = { module = " io.micrometer:micrometer-tracing-test", version.ref = "micrometer" }
+micrometer-tracing-bridge-brave = { module = " io.micrometer:micrometer-tracing-bridge-brave", version.ref = "micrometer" }
+micrometer-tracing = { module = " io.micrometer:micrometer-tracing", version.ref = "micrometer" }
+micrometer-tracing-bridge-otel = { module = " io.micrometer:micrometer-tracing-bridge-otel", version.ref = "micrometer" }
+zipkin-reporter = { module = " io.zipkin.reporter2:zipkin-reporter", version.ref = "zipkin-reporter" }
+opentelemetry-exporter-zipkin = { module = " io.opentelemetry:opentelemetry-exporter-zipkin", version.ref = "opentelemetry-exporter-zipkin" }
[bundles]
aws-java-sdk-v1 = ["aws-java-sdk-v1-core", "aws-java-sdk-v1-sts"]
@@ -198,6 +208,9 @@ scala-test-v2-v12 = ["scala-test-flatspec-v2-v12", "scala-test-shouldmatchers-v2
scala-test-v2-v11 = ["scala-test-flatspec-v2-v11", "scala-test-shouldmatchers-v2-v11", "scala-test-mockito-v2-v11",
"scala-test-junit-runner-v2-v11", "reflections"]
+micrometer-test = ["micrometer-tracing-test", "micrometer-tracing-bridge-brave", "micrometer-tracing-bridge-otel",
+ "micrometer-tracing", "zipkin-reporter", "opentelemetry-exporter-zipkin"]
+
[plugins]
kotlin-gradle = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
bnd = { id = "biz.aQute.bnd.builder", version.ref = "plugin-bnd" }