diff --git a/buildSrc/src/main/kotlin/project/Companion.kt b/buildSrc/src/main/kotlin/project/Companion.kt index b4b9650031a..04c39a6dcfe 100644 --- a/buildSrc/src/main/kotlin/project/Companion.kt +++ b/buildSrc/src/main/kotlin/project/Companion.kt @@ -23,4 +23,4 @@ import org.gradle.kotlin.dsl.getByType internal val Project.libs: LibrariesForLibs get() = extensions.getByType() -internal const val DEFAULT_JAVA_VERSION = 17 +const val DEFAULT_JAVA_VERSION = 17 diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml index 6d24f861e08..f3e6d3ef2ff 100644 --- a/config/checkstyle/suppressions.xml +++ b/config/checkstyle/suppressions.xml @@ -60,6 +60,7 @@ + diff --git a/driver-core/build.gradle.kts b/driver-core/build.gradle.kts index 4f06805a6ea..1614d9fb8d4 100644 --- a/driver-core/build.gradle.kts +++ b/driver-core/build.gradle.kts @@ -54,6 +54,7 @@ dependencies { optionalImplementation(libs.snappy.java) optionalImplementation(libs.zstd.jni) + optionalImplementation(libs.micrometer.observation) testImplementation(project(path = ":bson", configuration = "testArtifacts")) testImplementation(libs.reflections) diff --git a/driver-core/src/main/com/mongodb/MongoClientSettings.java b/driver-core/src/main/com/mongodb/MongoClientSettings.java index 1c9ffc5b04c..69cb8233690 100644 --- a/driver-core/src/main/com/mongodb/MongoClientSettings.java +++ b/driver-core/src/main/com/mongodb/MongoClientSettings.java @@ -30,6 +30,7 @@ import com.mongodb.connection.SslSettings; import com.mongodb.connection.TransportSettings; import com.mongodb.event.CommandListener; +import com.mongodb.tracing.Tracer; import com.mongodb.lang.Nullable; import com.mongodb.spi.dns.DnsClient; import com.mongodb.spi.dns.InetAddressResolver; @@ -57,6 +58,7 @@ import static com.mongodb.assertions.Assertions.isTrueArgument; import static com.mongodb.assertions.Assertions.notNull; import static com.mongodb.internal.TimeoutSettings.convertAndValidateTimeout; +import static java.lang.System.getenv; import static java.util.Arrays.asList; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.bson.codecs.configuration.CodecRegistries.fromProviders; @@ -119,6 +121,9 @@ public final class MongoClientSettings { @Nullable private final Long timeoutMS; + private static final String ENV_OTEL_ENABLED = "OTEL_JAVA_INSTRUMENTATION_MONGODB_ENABLED"; + private final Tracer tracer; + /** * Gets the default codec registry. It includes the following providers: * @@ -238,6 +243,7 @@ public static final class Builder { private ContextProvider contextProvider; private DnsClient dnsClient; private InetAddressResolver inetAddressResolver; + private Tracer tracer; private Builder() { } @@ -275,6 +281,7 @@ private Builder(final MongoClientSettings settings) { if (settings.heartbeatSocketTimeoutSetExplicitly) { heartbeatSocketTimeoutMS = settings.heartbeatSocketSettings.getReadTimeout(MILLISECONDS); } + tracer = settings.tracer; } /** @@ -723,6 +730,20 @@ Builder heartbeatSocketTimeoutMS(final int heartbeatSocketTimeoutMS) { return this; } + /** + * Sets the tracer to use for creating Spans for operations, commands and transactions. + * + * @param tracer the tracer + * @see com.mongodb.tracing.MicrometerTracer + * @return this + * @since 5.7 + */ + @Alpha(Reason.CLIENT) + public Builder tracer(final Tracer tracer) { + this.tracer = tracer; + return this; + } + /** * Build an instance of {@code MongoClientSettings}. * @@ -1040,6 +1061,16 @@ public ContextProvider getContextProvider() { return contextProvider; } + /** + * Get the tracer to create Spans for operations, commands and transactions. + * + * @return the configured Tracer + * @since 5.7 + */ + public Tracer getTracer() { + return tracer; + } + @Override public boolean equals(final Object o) { if (this == o) { @@ -1156,5 +1187,14 @@ private MongoClientSettings(final Builder builder) { heartbeatConnectTimeoutSetExplicitly = builder.heartbeatConnectTimeoutMS != 0; contextProvider = builder.contextProvider; timeoutMS = builder.timeoutMS; + + String envOtelInstrumentationEnabled = getenv(ENV_OTEL_ENABLED); + boolean enableTracing = true; + if (envOtelInstrumentationEnabled != null) { + enableTracing = Boolean.parseBoolean(envOtelInstrumentationEnabled); + } + tracer = (builder.tracer == null) ? Tracer.NO_OP + : (enableTracing) ? builder.tracer + : Tracer.NO_OP; } } diff --git a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java index bf009aa1b07..f53ed538dab 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java +++ b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java @@ -23,6 +23,7 @@ import com.mongodb.MongoException; import com.mongodb.MongoInternalException; import com.mongodb.MongoInterruptedException; +import com.mongodb.MongoNamespace; import com.mongodb.MongoOperationTimeoutException; import com.mongodb.MongoSocketClosedException; import com.mongodb.MongoSocketReadException; @@ -30,6 +31,7 @@ import com.mongodb.MongoSocketWriteException; import com.mongodb.MongoSocketWriteTimeoutException; import com.mongodb.ServerAddress; +import com.mongodb.UnixServerAddress; import com.mongodb.annotations.NotThreadSafe; import com.mongodb.connection.AsyncCompletionHandler; import com.mongodb.connection.ClusterConnectionMode; @@ -51,7 +53,10 @@ import com.mongodb.internal.logging.StructuredLogger; import com.mongodb.internal.session.SessionContext; import com.mongodb.internal.time.Timeout; +import com.mongodb.tracing.Span; +import com.mongodb.internal.tracing.TracingManager; import com.mongodb.lang.Nullable; +import io.micrometer.common.KeyValues; import org.bson.BsonBinaryReader; import org.bson.BsonDocument; import org.bson.ByteBuf; @@ -94,6 +99,22 @@ import static com.mongodb.internal.connection.ProtocolHelper.isCommandOk; import static com.mongodb.internal.logging.LogMessage.Level.DEBUG; import static com.mongodb.internal.thread.InterruptionUtil.translateInterruptedException; +import static com.mongodb.tracing.MongodbObservation.HighCardinalityKeyNames.QUERY_TEXT; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.CLIENT_CONNECTION_ID; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.COLLECTION; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.COMMAND_NAME; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.CURSOR_ID; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.NAMESPACE; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.NETWORK_TRANSPORT; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.QUERY_SUMMARY; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.RESPONSE_STATUS_CODE; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.SERVER_ADDRESS; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.SERVER_CONNECTION_ID; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.SERVER_PORT; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.SERVER_TYPE; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.SESSION_ID; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.SYSTEM; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.TRANSACTION_NUMBER; import static java.util.Arrays.asList; /** @@ -432,22 +453,50 @@ public boolean reauthenticationIsTriggered(@Nullable final Throwable t) { private T sendAndReceiveInternal(final CommandMessage message, final Decoder decoder, final OperationContext operationContext) { CommandEventSender commandEventSender; + Span tracingSpan; try (ByteBufferBsonOutput bsonOutput = new ByteBufferBsonOutput(this)) { message.encode(bsonOutput, operationContext); - commandEventSender = createCommandEventSender(message, bsonOutput, operationContext); - commandEventSender.sendStartedEvent(); + tracingSpan = createTracingSpan(message, operationContext, bsonOutput); + + boolean isLoggingCommandNeeded = isLoggingCommandNeeded(); + boolean isTracingCommandPayloadNeeded = tracingSpan != null && operationContext.getTracingManager().isCommandPayloadEnabled(); + + // Only hydrate the command document if necessary + BsonDocument commandDocument = null; + if (isLoggingCommandNeeded || isTracingCommandPayloadNeeded) { + commandDocument = message.getCommandDocument(bsonOutput); + } + if (isLoggingCommandNeeded) { + commandEventSender = new LoggingCommandEventSender( + SECURITY_SENSITIVE_COMMANDS, SECURITY_SENSITIVE_HELLO_COMMANDS, description, commandListener, + operationContext, message, commandDocument, + COMMAND_PROTOCOL_LOGGER, loggerSettings); + commandEventSender.sendStartedEvent(); + } else { + commandEventSender = new NoOpCommandEventSender(); + } + if (isTracingCommandPayloadNeeded) { + tracingSpan.tagHighCardinality(QUERY_TEXT.withBson(commandDocument)); + } + try { sendCommandMessage(message, bsonOutput, operationContext); } catch (Exception e) { + if (tracingSpan != null) { + tracingSpan.error(e); + } commandEventSender.sendFailedEvent(e); throw e; } } if (message.isResponseExpected()) { - return receiveCommandMessageResponse(decoder, commandEventSender, operationContext); + return receiveCommandMessageResponse(decoder, commandEventSender, operationContext, tracingSpan); } else { commandEventSender.sendSucceededEventForOneWayCommand(); + if (tracingSpan != null) { + tracingSpan.end(); + } return null; } } @@ -466,7 +515,7 @@ public void send(final CommandMessage message, final Decoder decoder, fin @Override public T receive(final Decoder decoder, final OperationContext operationContext) { isTrue("Response is expected", hasMoreToCome); - return receiveCommandMessageResponse(decoder, new NoOpCommandEventSender(), operationContext); + return receiveCommandMessageResponse(decoder, new NoOpCommandEventSender(), operationContext, null); } @Override @@ -512,7 +561,7 @@ private void trySendMessage(final CommandMessage message, final ByteBufferBsonOu } private T receiveCommandMessageResponse(final Decoder decoder, final CommandEventSender commandEventSender, - final OperationContext operationContext) { + final OperationContext operationContext, @Nullable final Span tracingSpan) { boolean commandSuccessful = false; try (ResponseBuffers responseBuffers = receiveResponseBuffers(operationContext)) { updateSessionContext(operationContext.getSessionContext(), responseBuffers); @@ -537,7 +586,17 @@ private T receiveCommandMessageResponse(final Decoder decoder, final Comm if (!commandSuccessful) { commandEventSender.sendFailedEvent(e); } + if (tracingSpan != null) { + if (e instanceof MongoCommandException) { + tracingSpan.tagLowCardinality(RESPONSE_STATUS_CODE.withValue(String.valueOf(((MongoCommandException) e).getErrorCode()))); + } + tracingSpan.error(e); + } throw e; + } finally { + if (tracingSpan != null) { + tracingSpan.end(); + } } } @@ -553,7 +612,18 @@ private void sendAndReceiveAsyncInternal(final CommandMessage message, final try { message.encode(bsonOutput, operationContext); - CommandEventSender commandEventSender = createCommandEventSender(message, bsonOutput, operationContext); + + CommandEventSender commandEventSender; + if (isLoggingCommandNeeded()) { + BsonDocument commandDocument = message.getCommandDocument(bsonOutput); + commandEventSender = new LoggingCommandEventSender( + SECURITY_SENSITIVE_COMMANDS, SECURITY_SENSITIVE_HELLO_COMMANDS, description, commandListener, + operationContext, message, commandDocument, + COMMAND_PROTOCOL_LOGGER, loggerSettings); + } else { + commandEventSender = new NoOpCommandEventSender(); + } + commandEventSender.sendStartedEvent(); Compressor localSendCompressor = sendCompressor; if (localSendCompressor == null || SECURITY_SENSITIVE_COMMANDS.contains(message.getCommandDocument(bsonOutput).getFirstKey())) { @@ -952,19 +1022,112 @@ public void onResult(@Nullable final ByteBuf result, @Nullable final Throwable t private static final StructuredLogger COMMAND_PROTOCOL_LOGGER = new StructuredLogger("protocol.command"); - private CommandEventSender createCommandEventSender(final CommandMessage message, final ByteBufferBsonOutput bsonOutput, - final OperationContext operationContext) { + private boolean isLoggingCommandNeeded() { boolean listensOrLogs = commandListener != null || COMMAND_PROTOCOL_LOGGER.isRequired(DEBUG, getClusterId()); - if (!recordEverything && (isMonitoringConnection || !opened() || !authenticated.get() || !listensOrLogs)) { - return new NoOpCommandEventSender(); - } - return new LoggingCommandEventSender( - SECURITY_SENSITIVE_COMMANDS, SECURITY_SENSITIVE_HELLO_COMMANDS, description, commandListener, - operationContext, message, bsonOutput, - COMMAND_PROTOCOL_LOGGER, loggerSettings); + return recordEverything || (!isMonitoringConnection && opened() && authenticated.get() && listensOrLogs); } private ClusterId getClusterId() { return description.getConnectionId().getServerId().getClusterId(); } + + /** + * Creates a tracing span for the given command message. + *

+ * The span is only created if tracing is enabled and the command is not security-sensitive. + * It attaches various tags to the span, such as database system, namespace, query summary, opcode, + * server address, port, server type, client and server connection IDs, and, if applicable, + * transaction number and session ID. For cursor fetching commands, the parent context is retrieved using the cursor ID. + * If command payload tracing is enabled, the command document is also attached as a tag. + * + * @param message the command message to trace + * @param operationContext the operation context containing tracing and session information + * @param bsonOutput the BSON output used to serialize the command + * @return the created {@link Span}, or {@code null} if tracing is not enabled or the command is security-sensitive + */ + @Nullable + private Span createTracingSpan(final CommandMessage message, final OperationContext operationContext, final ByteBufferBsonOutput bsonOutput) { + + TracingManager tracingManager = operationContext.getTracingManager(); + BsonDocument command = message.getCommandDocument(bsonOutput); + + String commandName = command.getFirstKey(); + + if (!tracingManager.isEnabled() + || SECURITY_SENSITIVE_COMMANDS.contains(commandName) + || SECURITY_SENSITIVE_HELLO_COMMANDS.contains(commandName)) { + return null; + } + + Span operationSpan = operationContext.getTracingSpan(); + Span span = tracingManager + .addSpan(commandName, operationSpan != null ? operationSpan.context() : null); + + if (command.containsKey("getMore")) { + long cursorId = command.getInt64("getMore").longValue(); + span.tagLowCardinality(CURSOR_ID.withValue(String.valueOf(cursorId))); + if (operationSpan != null) { + operationSpan.tagLowCardinality(CURSOR_ID.withValue(String.valueOf(cursorId))); + } + } + + tagNamespace(span, operationSpan, message, commandName); + tagServerAndConnectionInfo(span, message); + tagSessionAndTransactionInfo(span, operationContext); + + return span; + } + + private void tagNamespace(final Span span, @Nullable final Span parentSpan, final CommandMessage message, final String commandName) { + String namespace; + String collection = ""; + if (parentSpan != null) { + MongoNamespace parentNamespace = parentSpan.getNamespace(); + if (parentNamespace != null) { + namespace = parentNamespace.getDatabaseName(); + collection = + MongoNamespace.COMMAND_COLLECTION_NAME.equalsIgnoreCase(parentNamespace.getCollectionName()) ? "" + : parentNamespace.getCollectionName(); + } else { + namespace = message.getDatabase(); + } + } else { + namespace = message.getDatabase(); + } + String summary = commandName + " " + namespace + (collection.isEmpty() ? "" : "." + collection); + + KeyValues keyValues = KeyValues.of( + SYSTEM.withValue("mongodb"), + NAMESPACE.withValue(namespace), + QUERY_SUMMARY.withValue(summary), + COMMAND_NAME.withValue(commandName)); + + if (!collection.isEmpty()) { + keyValues = keyValues.and(COLLECTION.withValue(collection)); + } + span.tagLowCardinality(keyValues); + } + + private void tagServerAndConnectionInfo(final Span span, final CommandMessage message) { + span.tagLowCardinality(KeyValues.of( + SERVER_ADDRESS.withValue(serverId.getAddress().getHost()), + SERVER_PORT.withValue(String.valueOf(serverId.getAddress().getPort())), + SERVER_TYPE.withValue(message.getSettings().getServerType().name()), + CLIENT_CONNECTION_ID.withValue(String.valueOf(this.description.getConnectionId().getLocalValue())), + SERVER_CONNECTION_ID.withValue(String.valueOf(this.description.getConnectionId().getServerValue())), + NETWORK_TRANSPORT.withValue(getServerAddress() instanceof UnixServerAddress ? "unix" : "tcp") + )); + } + + private void tagSessionAndTransactionInfo(final Span span, final OperationContext operationContext) { + SessionContext sessionContext = operationContext.getSessionContext(); + if (sessionContext.hasSession() && !sessionContext.isImplicitSession()) { + span.tagLowCardinality(KeyValues.of( + TRANSACTION_NUMBER.withValue(String.valueOf(sessionContext.getTransactionNumber())), + SESSION_ID.withValue(String.valueOf(sessionContext.getSessionId() + .get(sessionContext.getSessionId().getFirstKey()) + .asBinary().asUuid())) + )); + } + } } diff --git a/driver-core/src/main/com/mongodb/internal/connection/LoggingCommandEventSender.java b/driver-core/src/main/com/mongodb/internal/connection/LoggingCommandEventSender.java index 044a2113fd8..1972e2caaab 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/LoggingCommandEventSender.java +++ b/driver-core/src/main/com/mongodb/internal/connection/LoggingCommandEventSender.java @@ -78,7 +78,7 @@ class LoggingCommandEventSender implements CommandEventSender { @Nullable final CommandListener commandListener, final OperationContext operationContext, final CommandMessage message, - final ByteBufferBsonOutput bsonOutput, + final BsonDocument commandDocument, final StructuredLogger logger, final LoggerSettings loggerSettings) { this.description = description; @@ -88,7 +88,7 @@ class LoggingCommandEventSender implements CommandEventSender { this.loggerSettings = loggerSettings; this.startTimeNanos = System.nanoTime(); this.message = message; - this.commandDocument = message.getCommandDocument(bsonOutput); + this.commandDocument = commandDocument; this.commandName = commandDocument.getFirstKey(); this.redactionRequired = securitySensitiveCommands.contains(commandName) || (securitySensitiveHelloCommands.contains(commandName) && commandDocument.containsKey("speculativeAuthenticate")); diff --git a/driver-core/src/main/com/mongodb/internal/connection/OperationContext.java b/driver-core/src/main/com/mongodb/internal/connection/OperationContext.java index 7e0de92da1d..b9da8bfc145 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/OperationContext.java +++ b/driver-core/src/main/com/mongodb/internal/connection/OperationContext.java @@ -27,6 +27,8 @@ import com.mongodb.internal.TimeoutSettings; import com.mongodb.internal.VisibleForTesting; import com.mongodb.internal.session.SessionContext; +import com.mongodb.tracing.Span; +import com.mongodb.internal.tracing.TracingManager; import com.mongodb.lang.Nullable; import com.mongodb.selector.ServerSelector; @@ -47,19 +49,28 @@ public class OperationContext { private final SessionContext sessionContext; private final RequestContext requestContext; private final TimeoutContext timeoutContext; + private final TracingManager tracingManager; @Nullable private final ServerApi serverApi; @Nullable private final String operationName; + @Nullable + private Span tracingSpan; public OperationContext(final RequestContext requestContext, final SessionContext sessionContext, final TimeoutContext timeoutContext, @Nullable final ServerApi serverApi) { - this(requestContext, sessionContext, timeoutContext, serverApi, null); + this(requestContext, sessionContext, timeoutContext, TracingManager.NO_OP, serverApi, null); } public OperationContext(final RequestContext requestContext, final SessionContext sessionContext, final TimeoutContext timeoutContext, - @Nullable final ServerApi serverApi, @Nullable final String operationName) { - this(NEXT_ID.incrementAndGet(), requestContext, sessionContext, timeoutContext, new ServerDeprioritization(), serverApi, operationName); + final TracingManager tracingManager, + @Nullable final ServerApi serverApi, + @Nullable final String operationName) { + this(NEXT_ID.incrementAndGet(), requestContext, sessionContext, timeoutContext, new ServerDeprioritization(), + tracingManager, + serverApi, + operationName, + null); } public static OperationContext simpleOperationContext( @@ -68,8 +79,10 @@ public static OperationContext simpleOperationContext( IgnorableRequestContext.INSTANCE, NoOpSessionContext.INSTANCE, new TimeoutContext(timeoutSettings), + TracingManager.NO_OP, serverApi, - null); + null + ); } public static OperationContext simpleOperationContext(final TimeoutContext timeoutContext) { @@ -77,26 +90,34 @@ public static OperationContext simpleOperationContext(final TimeoutContext timeo IgnorableRequestContext.INSTANCE, NoOpSessionContext.INSTANCE, timeoutContext, + TracingManager.NO_OP, null, null); } public OperationContext withSessionContext(final SessionContext sessionContext) { - return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, serverApi, operationName); + return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, tracingManager, serverApi, + operationName, tracingSpan); } public OperationContext withTimeoutContext(final TimeoutContext timeoutContext) { - return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, serverApi, operationName); + return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, tracingManager, serverApi, + operationName, tracingSpan); } public OperationContext withOperationName(final String operationName) { - return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, serverApi, operationName); + return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, tracingManager, serverApi, + operationName, tracingSpan); } public long getId() { return id; } + public TracingManager getTracingManager() { + return tracingManager; + } + public SessionContext getSessionContext() { return sessionContext; } @@ -119,37 +140,54 @@ public String getOperationName() { return operationName; } + @Nullable + public Span getTracingSpan() { + return tracingSpan; + } + + public void setTracingSpan(final Span tracingSpan) { + this.tracingSpan = tracingSpan; + } + @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) public OperationContext(final long id, - final RequestContext requestContext, - final SessionContext sessionContext, - final TimeoutContext timeoutContext, - final ServerDeprioritization serverDeprioritization, - @Nullable final ServerApi serverApi, - @Nullable final String operationName) { + final RequestContext requestContext, + final SessionContext sessionContext, + final TimeoutContext timeoutContext, + final ServerDeprioritization serverDeprioritization, + final TracingManager tracingManager, + @Nullable final ServerApi serverApi, + @Nullable final String operationName, + @Nullable final Span tracingSpan) { + this.id = id; this.serverDeprioritization = serverDeprioritization; this.requestContext = requestContext; this.sessionContext = sessionContext; this.timeoutContext = timeoutContext; + this.tracingManager = tracingManager; this.serverApi = serverApi; this.operationName = operationName; + this.tracingSpan = tracingSpan; } @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) public OperationContext(final long id, - final RequestContext requestContext, - final SessionContext sessionContext, - final TimeoutContext timeoutContext, - @Nullable final ServerApi serverApi, - @Nullable final String operationName) { + final RequestContext requestContext, + final SessionContext sessionContext, + final TimeoutContext timeoutContext, + final TracingManager tracingManager, + @Nullable final ServerApi serverApi, + @Nullable final String operationName) { this.id = id; this.serverDeprioritization = new ServerDeprioritization(); this.requestContext = requestContext; this.sessionContext = sessionContext; this.timeoutContext = timeoutContext; + this.tracingManager = tracingManager; this.serverApi = serverApi; this.operationName = operationName; + this.tracingSpan = null; } diff --git a/driver-core/src/main/com/mongodb/internal/operation/AbortTransactionOperation.java b/driver-core/src/main/com/mongodb/internal/operation/AbortTransactionOperation.java index bc7e6655bc7..4f48722747e 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/AbortTransactionOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/AbortTransactionOperation.java @@ -17,6 +17,7 @@ package com.mongodb.internal.operation; import com.mongodb.Function; +import com.mongodb.MongoNamespace; import com.mongodb.WriteConcern; import com.mongodb.internal.TimeoutContext; import com.mongodb.lang.Nullable; @@ -48,6 +49,11 @@ public String getCommandName() { return COMMAND_NAME; } + @Override + public MongoNamespace getNamespace() { + return new MongoNamespace("admin", MongoNamespace.COMMAND_COLLECTION_NAME); // TODO double check + } + @Override CommandCreator getCommandCreator() { return (operationContext, serverDescription, connectionDescription) -> { diff --git a/driver-core/src/main/com/mongodb/internal/operation/AbstractWriteSearchIndexOperation.java b/driver-core/src/main/com/mongodb/internal/operation/AbstractWriteSearchIndexOperation.java index e6643f3c7d2..e513abde505 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/AbstractWriteSearchIndexOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/AbstractWriteSearchIndexOperation.java @@ -96,7 +96,9 @@ void swallowOrThrow(@Nullable final E mongoExecutionExcept abstract BsonDocument buildCommand(); - MongoNamespace getNamespace() { + + @Override + public MongoNamespace getNamespace() { return namespace; } } diff --git a/driver-core/src/main/com/mongodb/internal/operation/AggregateOperation.java b/driver-core/src/main/com/mongodb/internal/operation/AggregateOperation.java index 1c9abfc68ca..55c5c231a21 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/AggregateOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/AggregateOperation.java @@ -164,7 +164,8 @@ CommandReadOperation createExplainableOperation(@Nullable final ExplainVe }, resultDecoder); } - MongoNamespace getNamespace() { + @Override + public MongoNamespace getNamespace() { return wrapped.getNamespace(); } } diff --git a/driver-core/src/main/com/mongodb/internal/operation/AggregateOperationImpl.java b/driver-core/src/main/com/mongodb/internal/operation/AggregateOperationImpl.java index 4c9bc3828b7..e2e8d6fb426 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/AggregateOperationImpl.java +++ b/driver-core/src/main/com/mongodb/internal/operation/AggregateOperationImpl.java @@ -92,10 +92,6 @@ class AggregateOperationImpl implements ReadOperationCursor { this.pipelineCreator = notNull("pipelineCreator", pipelineCreator); } - MongoNamespace getNamespace() { - return namespace; - } - List getPipeline() { return pipeline; } @@ -191,6 +187,11 @@ public String getCommandName() { return COMMAND_NAME; } + @Override + public MongoNamespace getNamespace() { + return namespace; + } + @Override public BatchCursor execute(final ReadBinding binding) { return executeRetryableRead(binding, namespace.getDatabaseName(), diff --git a/driver-core/src/main/com/mongodb/internal/operation/AggregateToCollectionOperation.java b/driver-core/src/main/com/mongodb/internal/operation/AggregateToCollectionOperation.java index 16f33ad45e5..296b4eabb88 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/AggregateToCollectionOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/AggregateToCollectionOperation.java @@ -157,6 +157,11 @@ public String getCommandName() { return COMMAND_NAME; } + @Override + public MongoNamespace getNamespace() { + return namespace; + } + @Override public Void execute(final ReadBinding binding) { return executeRetryableRead(binding, diff --git a/driver-core/src/main/com/mongodb/internal/operation/BaseFindAndModifyOperation.java b/driver-core/src/main/com/mongodb/internal/operation/BaseFindAndModifyOperation.java index c5d56fda81c..8f66333eb02 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/BaseFindAndModifyOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/BaseFindAndModifyOperation.java @@ -92,6 +92,7 @@ public void executeAsync(final AsyncWriteBinding binding, final SingleResultCall FindAndModifyHelper.asyncTransformer(), cmd -> cmd, callback); } + @Override public MongoNamespace getNamespace() { return namespace; } diff --git a/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java b/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java index 2b9e79f6f06..c94fcc04783 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java @@ -115,6 +115,7 @@ import java.util.function.Supplier; import java.util.stream.Stream; +import static com.mongodb.MongoNamespace.COMMAND_COLLECTION_NAME; import static com.mongodb.assertions.Assertions.assertFalse; import static com.mongodb.assertions.Assertions.assertNotNull; import static com.mongodb.assertions.Assertions.assertTrue; @@ -182,6 +183,12 @@ public String getCommandName() { return "bulkWrite"; } + @Override + public MongoNamespace getNamespace() { + // The bulkWrite command is executed on the "admin" database. + return new MongoNamespace("admin", COMMAND_COLLECTION_NAME); + } + @Override public ClientBulkWriteResult execute(final WriteBinding binding) throws ClientBulkWriteException { WriteConcern effectiveWriteConcern = validateAndGetEffectiveWriteConcern(binding.getOperationContext().getSessionContext()); diff --git a/driver-core/src/main/com/mongodb/internal/operation/CommandReadOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CommandReadOperation.java index 6965bfc34a3..0fbc6eb06e9 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/CommandReadOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/CommandReadOperation.java @@ -16,12 +16,14 @@ package com.mongodb.internal.operation; +import com.mongodb.MongoNamespace; import com.mongodb.internal.async.SingleResultCallback; import com.mongodb.internal.binding.AsyncReadBinding; import com.mongodb.internal.binding.ReadBinding; import org.bson.BsonDocument; import org.bson.codecs.Decoder; +import static com.mongodb.MongoNamespace.COMMAND_COLLECTION_NAME; import static com.mongodb.assertions.Assertions.notNull; import static com.mongodb.internal.operation.AsyncOperationHelper.executeRetryableReadAsync; import static com.mongodb.internal.operation.CommandOperationHelper.CommandCreator; @@ -55,6 +57,11 @@ public String getCommandName() { return commandName; } + @Override + public MongoNamespace getNamespace() { + return new MongoNamespace(databaseName, COMMAND_COLLECTION_NAME); + } + @Override public T execute(final ReadBinding binding) { return executeRetryableRead(binding, databaseName, commandCreator, decoder, diff --git a/driver-core/src/main/com/mongodb/internal/operation/CommitTransactionOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CommitTransactionOperation.java index 998a002f348..97e62ffceac 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/CommitTransactionOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/CommitTransactionOperation.java @@ -19,6 +19,7 @@ import com.mongodb.Function; import com.mongodb.MongoException; import com.mongodb.MongoExecutionTimeoutException; +import com.mongodb.MongoNamespace; import com.mongodb.MongoNodeIsRecoveringException; import com.mongodb.MongoNotPrimaryException; import com.mongodb.MongoSocketException; @@ -116,6 +117,11 @@ public String getCommandName() { return COMMAND_NAME; } + @Override + public MongoNamespace getNamespace() { + return new MongoNamespace("admin", MongoNamespace.COMMAND_COLLECTION_NAME); + } + @Override CommandCreator getCommandCreator() { CommandCreator creator = (operationContext, serverDescription, connectionDescription) -> { diff --git a/driver-core/src/main/com/mongodb/internal/operation/CountDocumentsOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CountDocumentsOperation.java index 9460026062a..157d3660904 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/CountDocumentsOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/CountDocumentsOperation.java @@ -125,6 +125,11 @@ public String getCommandName() { return COMMAND_NAME; } + @Override + public MongoNamespace getNamespace() { + return namespace; + } + @Override public Long execute(final ReadBinding binding) { try (BatchCursor cursor = getAggregateOperation().execute(binding)) { diff --git a/driver-core/src/main/com/mongodb/internal/operation/CountOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CountOperation.java index 6d0b7b78f93..d38a7c11333 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/CountOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/CountOperation.java @@ -115,6 +115,11 @@ public String getCommandName() { return COMMAND_NAME; } + @Override + public MongoNamespace getNamespace() { + return namespace; + } + @Override public Long execute(final ReadBinding binding) { return executeRetryableRead(binding, namespace.getDatabaseName(), diff --git a/driver-core/src/main/com/mongodb/internal/operation/CreateCollectionOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CreateCollectionOperation.java index 5284076eecb..d8e757054c0 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/CreateCollectionOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/CreateCollectionOperation.java @@ -18,6 +18,7 @@ import com.mongodb.MongoClientException; import com.mongodb.MongoException; +import com.mongodb.MongoNamespace; import com.mongodb.WriteConcern; import com.mongodb.client.model.ChangeStreamPreAndPostImagesOptions; import com.mongodb.client.model.Collation; @@ -236,6 +237,11 @@ public String getCommandName() { return "createCollection"; } + @Override + public MongoNamespace getNamespace() { + return new MongoNamespace(databaseName, collectionName); + } + @Override public Void execute(final WriteBinding binding) { return withConnection(binding, connection -> { diff --git a/driver-core/src/main/com/mongodb/internal/operation/CreateIndexesOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CreateIndexesOperation.java index b9b4242a3f4..7e634f136e2 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/CreateIndexesOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/CreateIndexesOperation.java @@ -105,6 +105,11 @@ public String getCommandName() { return COMMAND_NAME; } + @Override + public MongoNamespace getNamespace() { + return namespace; + } + @Override public Void execute(final WriteBinding binding) { try { diff --git a/driver-core/src/main/com/mongodb/internal/operation/CreateViewOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CreateViewOperation.java index 49b47fb7e9c..61fd58d5a0f 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/CreateViewOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/CreateViewOperation.java @@ -16,6 +16,7 @@ package com.mongodb.internal.operation; +import com.mongodb.MongoNamespace; import com.mongodb.WriteConcern; import com.mongodb.client.model.Collation; import com.mongodb.internal.async.SingleResultCallback; @@ -128,6 +129,11 @@ public String getCommandName() { return "createView"; } + @Override + public MongoNamespace getNamespace() { + return new MongoNamespace(databaseName, viewName); + } + @Override public Void execute(final WriteBinding binding) { return withConnection(binding, connection -> { diff --git a/driver-core/src/main/com/mongodb/internal/operation/DistinctOperation.java b/driver-core/src/main/com/mongodb/internal/operation/DistinctOperation.java index 489e3923bdc..10c4c320100 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/DistinctOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/DistinctOperation.java @@ -113,6 +113,11 @@ public String getCommandName() { return COMMAND_NAME; } + @Override + public MongoNamespace getNamespace() { + return namespace; + } + @Override public BatchCursor execute(final ReadBinding binding) { return executeRetryableRead(binding, namespace.getDatabaseName(), getCommandCreator(), createCommandDecoder(), diff --git a/driver-core/src/main/com/mongodb/internal/operation/DropCollectionOperation.java b/driver-core/src/main/com/mongodb/internal/operation/DropCollectionOperation.java index 2926fdec799..6b4c9712c01 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/DropCollectionOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/DropCollectionOperation.java @@ -91,6 +91,11 @@ public String getCommandName() { return "dropCollection"; } + @Override + public MongoNamespace getNamespace() { + return namespace; + } + @Override public Void execute(final WriteBinding binding) { BsonDocument localEncryptedFields = getEncryptedFields((ReadWriteBinding) binding); diff --git a/driver-core/src/main/com/mongodb/internal/operation/DropDatabaseOperation.java b/driver-core/src/main/com/mongodb/internal/operation/DropDatabaseOperation.java index d619176e8a3..2ee963923fe 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/DropDatabaseOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/DropDatabaseOperation.java @@ -16,6 +16,7 @@ package com.mongodb.internal.operation; +import com.mongodb.MongoNamespace; import com.mongodb.WriteConcern; import com.mongodb.internal.async.SingleResultCallback; import com.mongodb.internal.binding.AsyncWriteBinding; @@ -60,6 +61,11 @@ public String getCommandName() { return "dropDatabase"; } + @Override + public MongoNamespace getNamespace() { + return new MongoNamespace(databaseName, MongoNamespace.COMMAND_COLLECTION_NAME); + } + @Override public Void execute(final WriteBinding binding) { return withConnection(binding, connection -> { diff --git a/driver-core/src/main/com/mongodb/internal/operation/DropIndexOperation.java b/driver-core/src/main/com/mongodb/internal/operation/DropIndexOperation.java index 3671a90aa56..8a3a66e3c50 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/DropIndexOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/DropIndexOperation.java @@ -70,6 +70,11 @@ public String getCommandName() { return COMMAND_NAME; } + @Override + public MongoNamespace getNamespace() { + return namespace; + } + @Override public Void execute(final WriteBinding binding) { try { diff --git a/driver-core/src/main/com/mongodb/internal/operation/EstimatedDocumentCountOperation.java b/driver-core/src/main/com/mongodb/internal/operation/EstimatedDocumentCountOperation.java index 427cd40dc40..6308aae56ea 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/EstimatedDocumentCountOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/EstimatedDocumentCountOperation.java @@ -75,6 +75,11 @@ public String getCommandName() { return COMMAND_NAME; } + @Override + public MongoNamespace getNamespace() { + return namespace; + } + @Override public Long execute(final ReadBinding binding) { try { diff --git a/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java b/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java index 4e1de40d150..e268c1dd8ce 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java @@ -99,6 +99,7 @@ public FindOperation(final MongoNamespace namespace, final Decoder decoder) { this.decoder = notNull("decoder", decoder); } + @Override public MongoNamespace getNamespace() { return namespace; } @@ -468,9 +469,12 @@ private TimeoutMode getTimeoutMode() { } private CommandReadTransformer> transformer() { - return (result, source, connection) -> - new CommandBatchCursor<>(getTimeoutMode(), result, batchSize, getMaxTimeForCursor(source.getOperationContext()), decoder, - comment, source, connection); + return (result, source, connection) -> { + OperationContext operationContext = source.getOperationContext(); + + return new CommandBatchCursor<>(getTimeoutMode(), result, batchSize, getMaxTimeForCursor(operationContext), decoder, + comment, source, connection); + }; } private CommandReadTransformerAsync> asyncTransformer() { diff --git a/driver-core/src/main/com/mongodb/internal/operation/ListCollectionsOperation.java b/driver-core/src/main/com/mongodb/internal/operation/ListCollectionsOperation.java index 8740986b23f..da3966b26de 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/ListCollectionsOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/ListCollectionsOperation.java @@ -17,6 +17,7 @@ package com.mongodb.internal.operation; import com.mongodb.MongoCommandException; +import com.mongodb.MongoNamespace; import com.mongodb.client.cursor.TimeoutMode; import com.mongodb.internal.VisibleForTesting; import com.mongodb.internal.async.AsyncBatchCursor; @@ -34,6 +35,7 @@ import java.util.function.Supplier; +import static com.mongodb.MongoNamespace.COMMAND_COLLECTION_NAME; import static com.mongodb.assertions.Assertions.notNull; import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE; import static com.mongodb.internal.async.ErrorHandlingResultCallback.errorHandlingCallback; @@ -86,6 +88,11 @@ public ListCollectionsOperation(final String databaseName, final Decoder deco this.decoder = notNull("decoder", decoder); } + @Override + public MongoNamespace getNamespace() { + return new MongoNamespace(databaseName, COMMAND_COLLECTION_NAME); + } + public BsonDocument getFilter() { return filter; } diff --git a/driver-core/src/main/com/mongodb/internal/operation/ListDatabasesOperation.java b/driver-core/src/main/com/mongodb/internal/operation/ListDatabasesOperation.java index 4787153190b..d51194406b6 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/ListDatabasesOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/ListDatabasesOperation.java @@ -16,6 +16,7 @@ package com.mongodb.internal.operation; +import com.mongodb.MongoNamespace; import com.mongodb.internal.async.AsyncBatchCursor; import com.mongodb.internal.async.SingleResultCallback; import com.mongodb.internal.binding.AsyncReadBinding; @@ -26,6 +27,7 @@ import org.bson.BsonValue; import org.bson.codecs.Decoder; +import static com.mongodb.MongoNamespace.COMMAND_COLLECTION_NAME; import static com.mongodb.assertions.Assertions.notNull; import static com.mongodb.internal.async.ErrorHandlingResultCallback.errorHandlingCallback; import static com.mongodb.internal.operation.AsyncOperationHelper.asyncSingleBatchCursorTransformer; @@ -107,6 +109,11 @@ public String getCommandName() { return COMMAND_NAME; } + @Override + public MongoNamespace getNamespace() { + return new MongoNamespace("admin", COMMAND_COLLECTION_NAME); + } + @Override public BatchCursor execute(final ReadBinding binding) { return executeRetryableRead(binding, "admin", getCommandCreator(), CommandResultDocumentCodec.create(decoder, DATABASES), diff --git a/driver-core/src/main/com/mongodb/internal/operation/ListIndexesOperation.java b/driver-core/src/main/com/mongodb/internal/operation/ListIndexesOperation.java index a97acd64d58..76900ab296e 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/ListIndexesOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/ListIndexesOperation.java @@ -122,6 +122,11 @@ public String getCommandName() { return COMMAND_NAME; } + @Override + public MongoNamespace getNamespace() { + return namespace; + } + @Override public BatchCursor execute(final ReadBinding binding) { RetryState retryState = initialRetryState(retryReads, binding.getOperationContext().getTimeoutContext()); diff --git a/driver-core/src/main/com/mongodb/internal/operation/ListSearchIndexesOperation.java b/driver-core/src/main/com/mongodb/internal/operation/ListSearchIndexesOperation.java index 7fadead0b57..3c78297463e 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/ListSearchIndexesOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/ListSearchIndexesOperation.java @@ -78,6 +78,11 @@ public String getCommandName() { return COMMAND_NAME; } + @Override + public MongoNamespace getNamespace() { + return namespace; + } + @Override public BatchCursor execute(final ReadBinding binding) { try { diff --git a/driver-core/src/main/com/mongodb/internal/operation/MapReduceToCollectionOperation.java b/driver-core/src/main/com/mongodb/internal/operation/MapReduceToCollectionOperation.java index bfcc73a5aa6..96f5a8418d0 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/MapReduceToCollectionOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/MapReduceToCollectionOperation.java @@ -87,6 +87,7 @@ public MapReduceToCollectionOperation(final MongoNamespace namespace, final Bson this.writeConcern = writeConcern; } + @Override public MongoNamespace getNamespace() { return namespace; } diff --git a/driver-core/src/main/com/mongodb/internal/operation/MapReduceWithInlineResultsOperation.java b/driver-core/src/main/com/mongodb/internal/operation/MapReduceWithInlineResultsOperation.java index 6661c2a5c77..abbd2fc6ae8 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/MapReduceWithInlineResultsOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/MapReduceWithInlineResultsOperation.java @@ -76,6 +76,7 @@ public MapReduceWithInlineResultsOperation(final MongoNamespace namespace, final this.decoder = notNull("decoder", decoder); } + @Override public MongoNamespace getNamespace() { return namespace; } diff --git a/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java b/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java index 39ff2dab17f..b17a3bae30b 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java @@ -101,6 +101,7 @@ public MixedBulkWriteOperation(final MongoNamespace namespace, final List { */ String getCommandName(); + /** + * @return the namespace of the operation + */ + MongoNamespace getNamespace(); + /** * General execute which can return anything of type T * diff --git a/driver-core/src/main/com/mongodb/internal/operation/RenameCollectionOperation.java b/driver-core/src/main/com/mongodb/internal/operation/RenameCollectionOperation.java index ea477bf67bd..81d3b0bffe9 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/RenameCollectionOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/RenameCollectionOperation.java @@ -79,6 +79,11 @@ public String getCommandName() { return COMMAND_NAME; } + @Override + public MongoNamespace getNamespace() { + return originalNamespace; + } + @Override public Void execute(final WriteBinding binding) { return withConnection(binding, connection -> executeCommand(binding, "admin", getCommand(), connection, diff --git a/driver-core/src/main/com/mongodb/internal/operation/WriteOperation.java b/driver-core/src/main/com/mongodb/internal/operation/WriteOperation.java index 73cec2f416b..b4e2b4a25b4 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/WriteOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/WriteOperation.java @@ -16,6 +16,7 @@ package com.mongodb.internal.operation; +import com.mongodb.MongoNamespace; import com.mongodb.internal.async.SingleResultCallback; import com.mongodb.internal.binding.AsyncWriteBinding; import com.mongodb.internal.binding.WriteBinding; @@ -32,6 +33,11 @@ public interface WriteOperation { */ String getCommandName(); + /** + * @return the namespace of the operation + */ + MongoNamespace getNamespace(); + /** * General execute which can return anything of type T * diff --git a/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java b/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java new file mode 100644 index 00000000000..84cf91decd8 --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java @@ -0,0 +1,113 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.internal.tracing; + +import com.mongodb.MongoNamespace; +import com.mongodb.lang.Nullable; +import com.mongodb.tracing.Span; +import com.mongodb.tracing.TraceContext; +import com.mongodb.tracing.Tracer; + +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.SYSTEM; + +/** + * Manages tracing spans for MongoDB driver activities. + *

+ * This class provides methods to create and manage spans for commands, operations and transactions. + * It integrates with a {@link Tracer} to propagate tracing information and record telemetry. + *

+ */ +public class TracingManager { + /** + * A no-op instance of the TracingManager used when tracing is disabled. + */ + public static final TracingManager NO_OP = new TracingManager(Tracer.NO_OP); + private final Tracer tracer; + private final boolean enableCommandPayload; + + /** + * Constructs a new TracingManager with the specified tracer and parent context. + * Setting the environment variable {@code OTEL_JAVA_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH} will enable command payload tracing. + * + * @param tracer The tracer to use for tracing operations. + */ + public TracingManager(final Tracer tracer) { + this.tracer = tracer; + this.enableCommandPayload = tracer.includeCommandPayload(); + } + + /** + * Creates a new span with the specified name and parent trace context. + *

+ * This method is used to create a span that is linked to a parent context, + * enabling hierarchical tracing of operations. + *

+ * + * @param name The name of the span. + * @param parentContext The parent trace context to associate with the span. + * @return The created span. + */ + public Span addSpan(final String name, @Nullable final TraceContext parentContext) { + return tracer.nextSpan(name, parentContext, null); + } + + /** + * Creates a new span with the specified name, parent trace context, and MongoDB namespace. + *

+ * This method is used to create a span that is linked to a parent context, + * enabling hierarchical tracing of operations. The MongoDB namespace can be used + * by nested spans to access the database and collection name (which might not be easily accessible at connection layer). + *

+ * + * @param name The name of the span. + * @param parentContext The parent trace context to associate with the span. + * @param namespace The MongoDB namespace associated with the operation. + * @return The created span. + */ + public Span addSpan(final String name, @Nullable final TraceContext parentContext, final MongoNamespace namespace) { + return tracer.nextSpan(name, parentContext, namespace); + } + + /** + * Creates a new transaction span for the specified server session. + * + * @return The created transaction span. + */ + public Span addTransactionSpan() { + Span span = tracer.nextSpan("transaction", null, null); + span.tagLowCardinality(SYSTEM.withValue("mongodb")); + return span; + } + + /** + * Checks whether tracing is enabled. + * + * @return True if tracing is enabled, false otherwise. + */ + public boolean isEnabled() { + return tracer.enabled(); + } + + /** + * Checks whether command payload tracing is enabled. + * + * @return True if command payload tracing is enabled, false otherwise. + */ + public boolean isCommandPayloadEnabled() { + return enableCommandPayload; + } +} diff --git a/driver-core/src/main/com/mongodb/internal/tracing/TransactionSpan.java b/driver-core/src/main/com/mongodb/internal/tracing/TransactionSpan.java new file mode 100644 index 00000000000..d3133a8238b --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/tracing/TransactionSpan.java @@ -0,0 +1,114 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.internal.tracing; + +import com.mongodb.lang.Nullable; +import com.mongodb.tracing.Span; +import com.mongodb.tracing.TraceContext; + +/** + * State class for transaction tracing. + * + * @since 5.7 + */ +public class TransactionSpan { + private boolean isConvenientTransaction = false; + private final Span span; + @Nullable + private Throwable reportedError; + + public TransactionSpan(final TracingManager tracingManager) { + this.span = tracingManager.addTransactionSpan(); + } + + /** + * Handles a transaction span error. + *

+ * If the transaction is convenient, the error is reported as an event. This is done since + * the error is not fatal and the transaction may be retried. + *

+ * If the transaction is not convenient, the error is reported as a span error and the + * transaction context is cleaned up. + * + * @param e The error to report. + */ + public void handleTransactionSpanError(final Throwable e) { + if (isConvenientTransaction) { + // report error as event (since subsequent retries might succeed, also keep track of the last event + span.event(e.toString()); + reportedError = e; + } else { + span.error(e); + } + + if (!isConvenientTransaction) { + span.end(); + } + } + + /** + * Finalizes the transaction span by logging the specified status as an event and ending the span. + * + * @param status The status to log as an event. + */ + public void finalizeTransactionSpan(final String status) { + span.event(status); + // clear previous commit error if any + if (!isConvenientTransaction) { + span.end(); + } + reportedError = null; // clear previous commit error if any + } + + /** + * Finalizes the transaction span by logging any last span event as an error and ending the span. + * Optionally cleans up the transaction context if specified. + * + * @param cleanupTransactionContext A boolean indicating whether to clean up the transaction context. + */ + public void spanFinalizing(final boolean cleanupTransactionContext) { + if (reportedError != null) { + span.error(reportedError); + } + span.end(); + reportedError = null; + // Don't clean up transaction context if we're still retrying (we want the retries to fold under the original transaction span) + if (cleanupTransactionContext) { + isConvenientTransaction = false; + } + } + + /** + * Indicates that the transaction is a convenient transaction. + *

+ * This has an impact on how the transaction span is handled. If the transaction is convenient, any errors that occur + * during the transaction are reported as events. If the transaction is not convenient, errors are reported as span + * errors and the transaction context is cleaned up. + */ + public void setIsConvenientTransaction() { + this.isConvenientTransaction = true; + } + + /** + * Retrieves the trace context associated with the transaction span. + * + * @return The trace context associated with the transaction span. + */ + public TraceContext getContext() { + return span.context(); + } +} diff --git a/driver-core/src/main/com/mongodb/internal/tracing/package-info.java b/driver-core/src/main/com/mongodb/internal/tracing/package-info.java new file mode 100644 index 00000000000..e7dd3311143 --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/tracing/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Contains classes related to tracing + */ +@NonNullApi +package com.mongodb.internal.tracing; + +import com.mongodb.lang.NonNullApi; diff --git a/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java b/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java new file mode 100644 index 00000000000..30d81351ef3 --- /dev/null +++ b/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java @@ -0,0 +1,191 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.tracing; + +import com.mongodb.MongoNamespace; +import com.mongodb.lang.Nullable; +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.EXCEPTION_MESSAGE; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.EXCEPTION_STACKTRACE; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.EXCEPTION_TYPE; +import static com.mongodb.tracing.MongodbObservation.MONGODB_OBSERVATION; + + +/** + * A {@link Tracer} implementation that delegates tracing operations to a Micrometer {@link io.micrometer.observation.ObservationRegistry}. + *

+ * This class enables integration of MongoDB driver tracing with Micrometer-based tracing systems. + * It provides integration with Micrometer to propagate observations into tracing API. + *

+ * + * @since 5.7 + */ +public class MicrometerTracer implements Tracer { + private final boolean allowCommandPayload; + private final ObservationRegistry observationRegistry; + + /** + * Constructs a new {@link MicrometerTracer} instance. + * + * @param observationRegistry The Micrometer {@link ObservationRegistry} to delegate tracing operations to. + */ + public MicrometerTracer(final ObservationRegistry observationRegistry) { + this(observationRegistry, false); + } + + /** + * Constructs a new {@link MicrometerTracer} instance with an option to allow command payloads. + * + * @param observationRegistry The Micrometer {@link ObservationRegistry} to delegate tracing operations to. + * @param allowCommandPayload Whether to allow command payloads in the trace context. + */ + public MicrometerTracer(final ObservationRegistry observationRegistry, final boolean allowCommandPayload) { + this.allowCommandPayload = allowCommandPayload; + this.observationRegistry = observationRegistry; + } + + @Override + public Span nextSpan(final String name, @Nullable final TraceContext parent, @Nullable final MongoNamespace namespace) { + if (parent instanceof MicrometerTraceContext) { + Observation parentObservation = ((MicrometerTraceContext) parent).observation; + if (parentObservation != null) { + return new MicrometerSpan(MONGODB_OBSERVATION + .observation(observationRegistry) + .contextualName(name) + .parentObservation(parentObservation) + .start(), namespace); + } + } + return new MicrometerSpan(MONGODB_OBSERVATION.observation(observationRegistry).contextualName(name).start(), namespace); + } + + @Override + public boolean enabled() { + return true; + } + + @Override + public boolean includeCommandPayload() { + return allowCommandPayload; + } + + /** + * Represents a Micrometer-based trace context. + */ + private static class MicrometerTraceContext implements TraceContext { + private final Observation observation; + + /** + * Constructs a new {@link MicrometerTraceContext} instance with an associated Observation. + * + * @param observation The Micrometer {@link Observation}, or null if none exists. + */ + MicrometerTraceContext(@Nullable final Observation observation) { + this.observation = observation; + } + } + + /** + * Represents a Micrometer-based span. + */ + private static class MicrometerSpan implements Span { + private final Observation observation; + @Nullable + private final MongoNamespace namespace; + + /** + * Constructs a new {@link MicrometerSpan} instance with an associated Observation. + * + * @param observation The Micrometer {@link Observation}, or null if none exists. + */ + MicrometerSpan(final Observation observation) { + this.observation = observation; + this.namespace = null; + } + + /** + * Constructs a new {@link MicrometerSpan} instance with an associated Observation and MongoDB namespace. + * + * @param observation The Micrometer {@link Observation}, or null if none exists. + * @param namespace The MongoDB namespace associated with the span. + */ + MicrometerSpan(final Observation observation, @Nullable final MongoNamespace namespace) { + this.namespace = namespace; + this.observation = observation; + } + + @Override + public void tagLowCardinality(final KeyValue keyValue) { + observation.lowCardinalityKeyValue(keyValue); + } + + @Override + public void tagLowCardinality(final KeyValues keyValues) { + observation.lowCardinalityKeyValues(keyValues); + } + + @Override + public void tagHighCardinality(final KeyValue keyValue) { + observation.highCardinalityKeyValue(keyValue); + } + + @Override + public void event(final String event) { + observation.event(() -> event); + } + + @Override + public void error(final Throwable throwable) { + observation.lowCardinalityKeyValues(KeyValues.of( + EXCEPTION_MESSAGE.withValue(throwable.getMessage()), + EXCEPTION_TYPE.withValue(throwable.getClass().getName()), + EXCEPTION_STACKTRACE.withValue(getStackTraceAsString(throwable)) + )); + observation.error(throwable); + } + + @Override + public void end() { + observation.stop(); + } + + @Override + public TraceContext context() { + return new MicrometerTraceContext(observation); + } + + @Override + @Nullable + public MongoNamespace getNamespace() { + return namespace; + } + + private String getStackTraceAsString(final Throwable throwable) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + throwable.printStackTrace(pw); + return sw.toString(); + } + } +} diff --git a/driver-core/src/main/com/mongodb/tracing/MongodbObservation.java b/driver-core/src/main/com/mongodb/tracing/MongodbObservation.java new file mode 100644 index 00000000000..3a8ee8a867e --- /dev/null +++ b/driver-core/src/main/com/mongodb/tracing/MongodbObservation.java @@ -0,0 +1,237 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.tracing; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.docs.ObservationDocumentation; +import org.bson.BsonDocument; +import org.bson.BsonReader; +import org.bson.json.JsonMode; +import org.bson.json.JsonWriter; +import org.bson.json.JsonWriterSettings; + +import java.io.StringWriter; + +import static java.lang.System.getenv; + +/** + * A MongoDB-based {@link Observation}. + * + * @since 5.7 + */ +public enum MongodbObservation implements ObservationDocumentation { + + MONGODB_OBSERVATION { + @Override + public String getName() { + return "mongodb"; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return LowCardinalityKeyNames.values(); + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + + }; + + /** + * Enums related to low cardinality key names for MongoDB tags. + */ + public enum LowCardinalityKeyNames implements KeyName { + + SYSTEM { + @Override + public String asString() { + return "db.system"; + } + }, + NAMESPACE { + @Override + public String asString() { + return "db.namespace"; + } + }, + COLLECTION { + @Override + public String asString() { + return "db.collection.name"; + } + }, + OPERATION_NAME { + @Override + public String asString() { + return "db.operation.name"; + } + }, + COMMAND_NAME { + @Override + public String asString() { + return "db.command.name"; + } + }, + NETWORK_TRANSPORT { + @Override + public String asString() { + return "network.transport"; + } + }, + OPERATION_SUMMARY { + @Override + public String asString() { + return "db.operation.summary"; + } + }, + QUERY_SUMMARY { + @Override + public String asString() { + return "db.query.summary"; + } + }, + CURSOR_ID { + @Override + public String asString() { + return "db.mongodb.cursor_id"; + } + }, + SERVER_ADDRESS { + @Override + public String asString() { + return "server.address"; + } + }, + SERVER_PORT { + @Override + public String asString() { + return "server.port"; + } + }, + SERVER_TYPE { + @Override + public String asString() { + return "server.type"; + } + }, + CLIENT_CONNECTION_ID { + @Override + public String asString() { + return "db.mongodb.driver_connection_id"; + } + }, + SERVER_CONNECTION_ID { + @Override + public String asString() { + return "db.mongodb.server_connection_id"; + } + }, + TRANSACTION_NUMBER { + @Override + public String asString() { + return "db.mongodb.txn_number"; + } + }, + SESSION_ID { + @Override + public String asString() { + return "db.mongodb.lsid"; + } + }, + EXCEPTION_STACKTRACE { + @Override + public String asString() { + return "exception.stacktrace"; + } + }, + EXCEPTION_TYPE { + @Override + public String asString() { + return "exception.type"; + } + }, + EXCEPTION_MESSAGE { + @Override + public String asString() { + return "exception.message"; + } + }, + RESPONSE_STATUS_CODE { + @Override + public String asString() { + return "db.response.status_code"; + } + } + } + + /** + * Enums related to high cardinality (highly variable values) key names for MongoDB tags. + */ + public enum HighCardinalityKeyNames implements KeyName { + + QUERY_TEXT { + @Override + public String asString() { + return "db.query.text"; + } + }; + + private static final String ENV_OTEL_QUERY_TEXT_MAX_LENGTH = "OTEL_JAVA_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH"; + private final int textMaxLength; + + HighCardinalityKeyNames() { + String queryTextMaxLength = getenv(ENV_OTEL_QUERY_TEXT_MAX_LENGTH); + if (queryTextMaxLength != null) { + this.textMaxLength = Integer.parseInt(queryTextMaxLength); + } else { + this.textMaxLength = Integer.MAX_VALUE; + } + } + + public KeyValue withBson(final BsonDocument commandDocument) { + if (textMaxLength == Integer.MAX_VALUE) { + // no truncation needed + return KeyValue.of(asString(), commandDocument.toString()); + } else { + return KeyValue.of(asString(), getTruncatedJsonCommand(commandDocument)); + } + } + + private String getTruncatedJsonCommand(final BsonDocument commandDocument) { + StringWriter writer = new StringWriter(); + + try (BsonReader bsonReader = commandDocument.asBsonReader()) { + JsonWriter jsonWriter = new JsonWriter(writer, + JsonWriterSettings.builder().outputMode(JsonMode.RELAXED) + .maxLength(textMaxLength) + .build()); + + jsonWriter.pipe(bsonReader); + + if (jsonWriter.isTruncated()) { + writer.append(" ..."); + } + + return writer.toString(); + } + } + } +} diff --git a/driver-core/src/main/com/mongodb/tracing/Span.java b/driver-core/src/main/com/mongodb/tracing/Span.java new file mode 100644 index 00000000000..b1b9ed30c49 --- /dev/null +++ b/driver-core/src/main/com/mongodb/tracing/Span.java @@ -0,0 +1,139 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.tracing; + +import com.mongodb.MongoNamespace; +import com.mongodb.lang.Nullable; +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; + +/** + * Represents a tracing span for the driver internal operations. + *

+ * A span records information about a single operation, such as tags, events, errors, and its context. + * Implementations can be used to propagate tracing information and record telemetry. + *

+ *

+ * Spans can be used to trace different aspects of MongoDB driver activity: + *

    + *
  • Command Spans: Trace the execution of MongoDB commands (e.g., find, insert, update).
  • + *
  • Operation Spans: Trace higher-level operations, which may include multiple commands or internal steps.
  • + *
  • Transaction Spans: Trace the lifecycle of a transaction, including all operations and commands within it.
  • + *
+ * + * @since 5.7 + */ +public interface Span { + /** + * An empty / no-op implementation of the Span interface. + *

+ * This implementation is used as a default when no actual tracing is required. + * All methods in this implementation perform no operations and return default values. + *

+ */ + Span EMPTY = new Span() { + @Override + public void tagLowCardinality(final KeyValue tag) { + } + + @Override + public void tagLowCardinality(final KeyValues keyValues) { + } + + @Override + public void tagHighCardinality(final KeyValue keyValue) { + } + + @Override + public void event(final String event) { + } + + @Override + public void error(final Throwable throwable) { + } + + @Override + public void end() { + } + + @Override + public TraceContext context() { + return TraceContext.EMPTY; + } + + @Override + @Nullable + public MongoNamespace getNamespace() { + return null; + } + }; + + /** + * Adds a low-cardinality tag to the span. + * + * @param keyValue The key-value pair representing the tag. + */ + void tagLowCardinality(KeyValue keyValue); + + /** + * Adds multiple low-cardinality tags to the span. + * + * @param keyValues The key-value pairs representing the tags. + */ + void tagLowCardinality(KeyValues keyValues); + + /** + * Adds a high-cardinality (highly variable values) tag to the span. + * + * @param keyValue The key-value pair representing the tag. + */ + void tagHighCardinality(KeyValue keyValue); + + /** + * Records an event in the span. + * + * @param event The event description. + */ + void event(String event); + + /** + * Records an error for this span. + * + * @param throwable The error to record. + */ + void error(Throwable throwable); + + /** + * Ends the span, marking it as complete. + */ + void end(); + + /** + * Retrieves the context associated with the span. + * + * @return The trace context associated with the span. + */ + TraceContext context(); + + /** + * Retrieves the MongoDB namespace associated with the span, if any. + * + * @return The MongoDB namespace, or null if none is associated. + */ + @Nullable + MongoNamespace getNamespace(); +} diff --git a/driver-core/src/main/com/mongodb/tracing/TraceContext.java b/driver-core/src/main/com/mongodb/tracing/TraceContext.java new file mode 100644 index 00000000000..06763f96111 --- /dev/null +++ b/driver-core/src/main/com/mongodb/tracing/TraceContext.java @@ -0,0 +1,23 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.tracing; + +@SuppressWarnings("InterfaceIsType") +public interface TraceContext { + TraceContext EMPTY = new TraceContext() { + }; +} diff --git a/driver-core/src/main/com/mongodb/tracing/Tracer.java b/driver-core/src/main/com/mongodb/tracing/Tracer.java new file mode 100644 index 00000000000..cc88e82b5ac --- /dev/null +++ b/driver-core/src/main/com/mongodb/tracing/Tracer.java @@ -0,0 +1,75 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mongodb.tracing; + +import com.mongodb.MongoNamespace; +import com.mongodb.lang.Nullable; + +/** + * A Tracer interface that provides methods for tracing commands, operations and transactions. + *

+ * This interface defines methods to retrieve the current trace context, create new spans, and check if tracing is enabled. + * It also includes a no-operation (NO_OP) implementation for cases where tracing is not required. + *

+ * + * Note: You can use the environment variable {@code OTEL_JAVA_INSTRUMENTATION_MONGODB_ENABLED} to override the behaviour of enabling/disabling + * tracing before you create the {@link com.mongodb.MongoClientSettings} instance. + * You can also use the environment variable {@code OTEL_JAVA_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH} to enable or disable command payload when tracing is enabled. . + * + * @since 5.7 + */ +public interface Tracer { + Tracer NO_OP = new Tracer() { + @Override + public Span nextSpan(final String name, @Nullable final TraceContext parent, @Nullable final MongoNamespace namespace) { + return Span.EMPTY; + } + + @Override + public boolean enabled() { + return false; + } + + @Override + public boolean includeCommandPayload() { + return false; + } + }; + + /** + * Creates a new span with the specified name and optional parent trace context. + * + * @param name The name of the span. + * @param parent The parent {@link TraceContext}, or null if no parent context is provided. + * @param namespace The {@link MongoNamespace} associated with the span, or null if none is provided. + * @return A {@link Span} representing the newly created span. + */ + Span nextSpan(String name, @Nullable TraceContext parent, @Nullable MongoNamespace namespace); + + /** + * Indicates whether tracing is enabled. + * + * @return {@code true} if tracing is enabled, {@code false} otherwise. + */ + boolean enabled(); + + /** + * Indicates whether command payloads are included in the trace context. + * + * @return {@code true} if command payloads are allowed, {@code false} otherwise. + */ + boolean includeCommandPayload(); +} diff --git a/driver-core/src/main/com/mongodb/tracing/package-info.java b/driver-core/src/main/com/mongodb/tracing/package-info.java new file mode 100644 index 00000000000..247576d1537 --- /dev/null +++ b/driver-core/src/main/com/mongodb/tracing/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This package defines the API for MongoDB driver tracing. + * + * @since 5.7 + */ +@NonNullApi +package com.mongodb.tracing; + +import com.mongodb.lang.NonNullApi; diff --git a/driver-core/src/test/functional/com/mongodb/internal/connection/CommandHelperSpecification.groovy b/driver-core/src/test/functional/com/mongodb/internal/connection/CommandHelperSpecification.groovy index 83ce94f7075..ecc9d3f64a5 100644 --- a/driver-core/src/test/functional/com/mongodb/internal/connection/CommandHelperSpecification.groovy +++ b/driver-core/src/test/functional/com/mongodb/internal/connection/CommandHelperSpecification.groovy @@ -25,6 +25,7 @@ import com.mongodb.connection.SocketSettings import com.mongodb.internal.connection.netty.NettyStreamFactory import org.bson.BsonDocument import org.bson.BsonInt32 +import spock.lang.Ignore import spock.lang.Specification import java.util.concurrent.CountDownLatch @@ -54,6 +55,7 @@ class CommandHelperSpecification extends Specification { connection?.close() } + @Ignore("5982") def 'should execute command asynchronously'() { when: BsonDocument receivedDocument = null diff --git a/driver-core/src/test/functional/com/mongodb/internal/operation/AsyncCommandBatchCursorTest.java b/driver-core/src/test/functional/com/mongodb/internal/operation/AsyncCommandBatchCursorTest.java index e9a30686d5f..3708081fc26 100644 --- a/driver-core/src/test/functional/com/mongodb/internal/operation/AsyncCommandBatchCursorTest.java +++ b/driver-core/src/test/functional/com/mongodb/internal/operation/AsyncCommandBatchCursorTest.java @@ -32,6 +32,7 @@ import com.mongodb.internal.binding.AsyncConnectionSource; import com.mongodb.internal.connection.AsyncConnection; import com.mongodb.internal.connection.OperationContext; +import com.mongodb.internal.tracing.TracingManager; import org.bson.BsonArray; import org.bson.BsonDocument; import org.bson.BsonInt32; @@ -96,6 +97,7 @@ void setUp() { connectionSource = mock(AsyncConnectionSource.class); operationContext = mock(OperationContext.class); + when(operationContext.getTracingManager()).thenReturn(TracingManager.NO_OP); timeoutContext = new TimeoutContext(TimeoutSettings.create( MongoClientSettings.builder().timeout(TIMEOUT.toMillis(), MILLISECONDS).build())); serverDescription = mock(ServerDescription.class); diff --git a/driver-core/src/test/unit/com/mongodb/MongoClientSettingsSpecification.groovy b/driver-core/src/test/unit/com/mongodb/MongoClientSettingsSpecification.groovy index ec5d92b1e49..02b33aa8d27 100644 --- a/driver-core/src/test/unit/com/mongodb/MongoClientSettingsSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/MongoClientSettingsSpecification.groovy @@ -555,7 +555,7 @@ class MongoClientSettingsSpecification extends Specification { 'heartbeatConnectTimeoutMS', 'heartbeatSocketTimeoutMS', 'inetAddressResolver', 'loggerSettingsBuilder', 'readConcern', 'readPreference', 'retryReads', 'retryWrites', 'serverApi', 'serverSettingsBuilder', 'socketSettingsBuilder', 'sslSettingsBuilder', - 'timeoutMS', 'transportSettings', 'uuidRepresentation', 'writeConcern'] + 'timeoutMS', 'tracer', 'transportSettings', 'uuidRepresentation', 'writeConcern'] then: actual == expected @@ -570,7 +570,7 @@ class MongoClientSettingsSpecification extends Specification { 'applyToSslSettings', 'autoEncryptionSettings', 'build', 'codecRegistry', 'commandListenerList', 'compressorList', 'contextProvider', 'credential', 'dnsClient', 'heartbeatConnectTimeoutMS', 'heartbeatSocketTimeoutMS', 'inetAddressResolver', 'readConcern', 'readPreference', 'retryReads', 'retryWrites', - 'serverApi', 'timeout', 'transportSettings', 'uuidRepresentation', 'writeConcern'] + 'serverApi', 'timeout', 'tracer', 'transportSettings', 'uuidRepresentation', 'writeConcern'] then: actual == expected diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/LoggingCommandEventSenderSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/connection/LoggingCommandEventSenderSpecification.groovy index 6aa30aa4aa6..e6f6afb02e0 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/connection/LoggingCommandEventSenderSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/internal/connection/LoggingCommandEventSenderSpecification.groovy @@ -65,7 +65,8 @@ class LoggingCommandEventSenderSpecification extends Specification { } def operationContext = OPERATION_CONTEXT def sender = new LoggingCommandEventSender([] as Set, [] as Set, connectionDescription, commandListener, - operationContext, message, bsonOutput, new StructuredLogger(logger), LoggerSettings.builder().build()) + operationContext, message, message.getCommandDocument(bsonOutput), + new StructuredLogger(logger), LoggerSettings.builder().build()) when: sender.sendStartedEvent() @@ -110,7 +111,7 @@ class LoggingCommandEventSenderSpecification extends Specification { } def operationContext = OPERATION_CONTEXT def sender = new LoggingCommandEventSender([] as Set, [] as Set, connectionDescription, commandListener, - operationContext, message, bsonOutput, new StructuredLogger(logger), + operationContext, message, message.getCommandDocument(bsonOutput), new StructuredLogger(logger), LoggerSettings.builder().build()) when: sender.sendStartedEvent() @@ -168,7 +169,7 @@ class LoggingCommandEventSenderSpecification extends Specification { def operationContext = OPERATION_CONTEXT def sender = new LoggingCommandEventSender([] as Set, [] as Set, connectionDescription, null, operationContext, - message, bsonOutput, new StructuredLogger(logger), LoggerSettings.builder().build()) + message, message.getCommandDocument(bsonOutput), new StructuredLogger(logger), LoggerSettings.builder().build()) when: sender.sendStartedEvent() @@ -201,7 +202,8 @@ class LoggingCommandEventSenderSpecification extends Specification { } def operationContext = OPERATION_CONTEXT def sender = new LoggingCommandEventSender(['createUser'] as Set, [] as Set, connectionDescription, null, - operationContext, message, bsonOutput, new StructuredLogger(logger), LoggerSettings.builder().build()) + operationContext, message, message.getCommandDocument(bsonOutput), new StructuredLogger(logger), + LoggerSettings.builder().build()) when: sender.sendStartedEvent() diff --git a/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncChangeStreamBatchCursorSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncChangeStreamBatchCursorSpecification.groovy index 998c0a28b6e..b7f8f0e9408 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncChangeStreamBatchCursorSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncChangeStreamBatchCursorSpecification.groovy @@ -22,6 +22,7 @@ import com.mongodb.internal.TimeoutContext import com.mongodb.internal.async.SingleResultCallback import com.mongodb.internal.binding.AsyncReadBinding import com.mongodb.internal.connection.OperationContext +import com.mongodb.internal.tracing.TracingManager import org.bson.Document import spock.lang.Specification @@ -34,6 +35,7 @@ class AsyncChangeStreamBatchCursorSpecification extends Specification { def changeStreamOpertation = Stub(ChangeStreamOperation) def binding = Mock(AsyncReadBinding) def operationContext = Mock(OperationContext) + operationContext.getTracingManager() >> TracingManager.NO_OP def timeoutContext = Mock(TimeoutContext) binding.getOperationContext() >> operationContext operationContext.getTimeoutContext() >> timeoutContext @@ -78,6 +80,7 @@ class AsyncChangeStreamBatchCursorSpecification extends Specification { def changeStreamOpertation = Stub(ChangeStreamOperation) def binding = Mock(AsyncReadBinding) def operationContext = Mock(OperationContext) + operationContext.getTracingManager() >> TracingManager.NO_OP def timeoutContext = Mock(TimeoutContext) binding.getOperationContext() >> operationContext operationContext.getTimeoutContext() >> timeoutContext @@ -111,6 +114,7 @@ class AsyncChangeStreamBatchCursorSpecification extends Specification { def changeStreamOpertation = Stub(ChangeStreamOperation) def binding = Mock(AsyncReadBinding) def operationContext = Mock(OperationContext) + operationContext.getTracingManager() >> TracingManager.NO_OP def timeoutContext = Mock(TimeoutContext) binding.getOperationContext() >> operationContext operationContext.getTimeoutContext() >> timeoutContext diff --git a/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncCommandBatchCursorSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncCommandBatchCursorSpecification.groovy index d2bcd0804bb..e3f2e525146 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncCommandBatchCursorSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncCommandBatchCursorSpecification.groovy @@ -35,6 +35,7 @@ import com.mongodb.internal.async.SingleResultCallback import com.mongodb.internal.binding.AsyncConnectionSource import com.mongodb.internal.connection.AsyncConnection import com.mongodb.internal.connection.OperationContext +import com.mongodb.internal.tracing.TracingManager import org.bson.BsonArray import org.bson.BsonDocument import org.bson.BsonInt32 @@ -524,6 +525,7 @@ class AsyncCommandBatchCursorSpecification extends Specification { .build() } OperationContext operationContext = Mock(OperationContext) + operationContext.getTracingManager() >> TracingManager.NO_OP def timeoutContext = Spy(new TimeoutContext(TimeoutSettings.create( MongoClientSettings.builder().timeout(3, TimeUnit.SECONDS).build()))) operationContext.getTimeoutContext() >> timeoutContext diff --git a/driver-core/src/test/unit/com/mongodb/internal/operation/ChangeStreamBatchCursorTest.java b/driver-core/src/test/unit/com/mongodb/internal/operation/ChangeStreamBatchCursorTest.java index 48c3a50e79a..552afaea95b 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/operation/ChangeStreamBatchCursorTest.java +++ b/driver-core/src/test/unit/com/mongodb/internal/operation/ChangeStreamBatchCursorTest.java @@ -25,6 +25,7 @@ import com.mongodb.internal.binding.ReadBinding; import com.mongodb.internal.connection.Connection; import com.mongodb.internal.connection.OperationContext; +import com.mongodb.internal.tracing.TracingManager; import org.bson.BsonDocument; import org.bson.BsonInt32; import org.bson.Document; @@ -296,6 +297,7 @@ void setUp() { doNothing().when(timeoutContext).resetTimeoutIfPresent(); operationContext = mock(OperationContext.class); + when(operationContext.getTracingManager()).thenReturn(TracingManager.NO_OP); when(operationContext.getTimeoutContext()).thenReturn(timeoutContext); connection = mock(Connection.class); when(connection.command(any(), any(), any(), any(), any(), any())).thenReturn(null); diff --git a/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorSpecification.groovy index c95a119134a..3190c1f6289 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorSpecification.groovy @@ -35,6 +35,7 @@ import com.mongodb.internal.TimeoutSettings import com.mongodb.internal.binding.ConnectionSource import com.mongodb.internal.connection.Connection import com.mongodb.internal.connection.OperationContext +import com.mongodb.internal.tracing.TracingManager import org.bson.BsonArray import org.bson.BsonDocument import org.bson.BsonInt32 @@ -574,6 +575,7 @@ class CommandBatchCursorSpecification extends Specification { .build() } OperationContext operationContext = Mock(OperationContext) + operationContext.getTracingManager() >> TracingManager.NO_OP def timeoutContext = Spy(new TimeoutContext(TimeoutSettings.create( MongoClientSettings.builder().timeout(3, TimeUnit.SECONDS).build()))) operationContext.getTimeoutContext() >> timeoutContext diff --git a/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorTest.java b/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorTest.java index c3bec291432..b4b4101bd56 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorTest.java +++ b/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorTest.java @@ -31,6 +31,7 @@ import com.mongodb.internal.binding.ConnectionSource; import com.mongodb.internal.connection.Connection; import com.mongodb.internal.connection.OperationContext; +import com.mongodb.internal.tracing.TracingManager; import org.bson.BsonArray; import org.bson.BsonDocument; import org.bson.BsonInt32; @@ -94,6 +95,8 @@ void setUp() { connectionSource = mock(ConnectionSource.class); operationContext = mock(OperationContext.class); + when(operationContext.getTracingManager()).thenReturn(TracingManager.NO_OP); + timeoutContext = new TimeoutContext(TimeoutSettings.create( MongoClientSettings.builder().timeout(TIMEOUT.toMillis(), MILLISECONDS).build())); serverDescription = mock(ServerDescription.class); diff --git a/driver-kotlin-coroutine/build.gradle.kts b/driver-kotlin-coroutine/build.gradle.kts index 02a2bf047aa..8880c67aad8 100644 --- a/driver-kotlin-coroutine/build.gradle.kts +++ b/driver-kotlin-coroutine/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { integrationTestImplementation(project(path = ":bson", configuration = "testArtifacts")) integrationTestImplementation(project(path = ":driver-sync", configuration = "testArtifacts")) integrationTestImplementation(project(path = ":driver-core", configuration = "testArtifacts")) + testImplementation(libs.micrometer.tracing.integration.test) { exclude(group = "org.junit.jupiter") } } configureMavenPublication { diff --git a/driver-kotlin-coroutine/src/integrationTest/kotlin/com/mongodb/kotlin/client/coroutine/syncadapter/SyncClientSession.kt b/driver-kotlin-coroutine/src/integrationTest/kotlin/com/mongodb/kotlin/client/coroutine/syncadapter/SyncClientSession.kt index 83ba91df16b..3c66babd5d8 100644 --- a/driver-kotlin-coroutine/src/integrationTest/kotlin/com/mongodb/kotlin/client/coroutine/syncadapter/SyncClientSession.kt +++ b/driver-kotlin-coroutine/src/integrationTest/kotlin/com/mongodb/kotlin/client/coroutine/syncadapter/SyncClientSession.kt @@ -21,6 +21,7 @@ import com.mongodb.TransactionOptions import com.mongodb.client.ClientSession as JClientSession import com.mongodb.client.TransactionBody import com.mongodb.internal.TimeoutContext +import com.mongodb.internal.tracing.TransactionSpan import com.mongodb.kotlin.client.coroutine.ClientSession import com.mongodb.session.ServerSession import kotlinx.coroutines.runBlocking @@ -89,4 +90,6 @@ class SyncClientSession(internal val wrapped: ClientSession, private val origina throw UnsupportedOperationException() override fun getTimeoutContext(): TimeoutContext? = wrapped.getTimeoutContext() + + override fun getTransactionSpan(): TransactionSpan? = null } diff --git a/driver-kotlin-sync/build.gradle.kts b/driver-kotlin-sync/build.gradle.kts index 5da1a5eec26..74f9b37c219 100644 --- a/driver-kotlin-sync/build.gradle.kts +++ b/driver-kotlin-sync/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { integrationTestImplementation(project(path = ":bson", configuration = "testArtifacts")) integrationTestImplementation(project(path = ":driver-sync", configuration = "testArtifacts")) integrationTestImplementation(project(path = ":driver-core", configuration = "testArtifacts")) + testImplementation(libs.micrometer.tracing.integration.test) { exclude(group = "org.junit.jupiter") } } configureMavenPublication { diff --git a/driver-kotlin-sync/src/integrationTest/kotlin/com/mongodb/kotlin/client/syncadapter/SyncClientSession.kt b/driver-kotlin-sync/src/integrationTest/kotlin/com/mongodb/kotlin/client/syncadapter/SyncClientSession.kt index 64cd27b776f..001198dbcd0 100644 --- a/driver-kotlin-sync/src/integrationTest/kotlin/com/mongodb/kotlin/client/syncadapter/SyncClientSession.kt +++ b/driver-kotlin-sync/src/integrationTest/kotlin/com/mongodb/kotlin/client/syncadapter/SyncClientSession.kt @@ -21,6 +21,7 @@ import com.mongodb.TransactionOptions import com.mongodb.client.ClientSession as JClientSession import com.mongodb.client.TransactionBody import com.mongodb.internal.TimeoutContext +import com.mongodb.internal.tracing.TransactionSpan import com.mongodb.kotlin.client.ClientSession import com.mongodb.session.ServerSession import org.bson.BsonDocument @@ -93,4 +94,6 @@ internal class SyncClientSession(internal val wrapped: ClientSession, private va throw UnsupportedOperationException() override fun getTimeoutContext(): TimeoutContext = throw UnsupportedOperationException() + + override fun getTransactionSpan(): TransactionSpan? = null } diff --git a/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/ClientSession.kt b/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/ClientSession.kt index 9103689b251..9786f5592e6 100644 --- a/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/ClientSession.kt +++ b/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/ClientSession.kt @@ -18,6 +18,7 @@ package com.mongodb.kotlin.client import com.mongodb.ClientSessionOptions import com.mongodb.TransactionOptions import com.mongodb.client.ClientSession as JClientSession +import com.mongodb.internal.tracing.TransactionSpan import java.io.Closeable import java.util.concurrent.TimeUnit @@ -86,6 +87,9 @@ public class ClientSession(public val wrapped: JClientSession) : Closeable { transactionBody: () -> T, options: TransactionOptions = TransactionOptions.builder().build() ): T = wrapped.withTransaction(transactionBody, options) + + /** Get the transaction span (if started). */ + public fun getTransactionSpan(): TransactionSpan? = wrapped.getTransactionSpan() } /** diff --git a/driver-kotlin-sync/src/test/kotlin/com/mongodb/kotlin/client/MongoIterableTest.kt b/driver-kotlin-sync/src/test/kotlin/com/mongodb/kotlin/client/MongoIterableTest.kt index ab16dd08b24..17bcc3c1a12 100644 --- a/driver-kotlin-sync/src/test/kotlin/com/mongodb/kotlin/client/MongoIterableTest.kt +++ b/driver-kotlin-sync/src/test/kotlin/com/mongodb/kotlin/client/MongoIterableTest.kt @@ -19,6 +19,7 @@ import com.mongodb.Function import com.mongodb.client.MongoCursor as JMongoCursor import com.mongodb.client.MongoIterable as JMongoIterable import kotlin.test.assertContentEquals +import kotlin.test.assertEquals import org.bson.Document import org.junit.jupiter.api.Test import org.mockito.ArgumentMatchers @@ -90,7 +91,7 @@ class MongoIterableTest { whenever(cursor.next()).thenReturn(documents[0], documents[1], documents[2]) whenever(delegate.cursor()).doReturn(cursor) - assertContentEquals(documents.subList(0, 2), iterable.use { it.take(2) }.toList()) + iterable.use { it.take(2).forEachIndexed { index, document -> assertEquals(documents[index], document) } } verify(delegate, times(1)).cursor() verify(cursor, times(2)).hasNext() diff --git a/driver-legacy/src/main/com/mongodb/LegacyMixedBulkWriteOperation.java b/driver-legacy/src/main/com/mongodb/LegacyMixedBulkWriteOperation.java index 95990833f00..47749129115 100644 --- a/driver-legacy/src/main/com/mongodb/LegacyMixedBulkWriteOperation.java +++ b/driver-legacy/src/main/com/mongodb/LegacyMixedBulkWriteOperation.java @@ -97,6 +97,11 @@ public String getCommandName() { return wrappedOperation.getCommandName(); } + @Override + public MongoNamespace getNamespace() { + return wrappedOperation.getNamespace(); + } + @Override public WriteConcernResult execute(final WriteBinding binding) { try { diff --git a/driver-reactive-streams/build.gradle.kts b/driver-reactive-streams/build.gradle.kts index f1c758b31da..8f4454d4f4c 100644 --- a/driver-reactive-streams/build.gradle.kts +++ b/driver-reactive-streams/build.gradle.kts @@ -44,6 +44,9 @@ dependencies { // Reactive Streams TCK testing testImplementation(libs.reactive.streams.tck) + + // Tracing + testImplementation(libs.micrometer.tracing.integration.test) { exclude(group = "org.junit.jupiter") } } configureMavenPublication { diff --git a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/MapReducePublisherImpl.java b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/MapReducePublisherImpl.java index 27e69762a09..46096d6ff58 100644 --- a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/MapReducePublisherImpl.java +++ b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/MapReducePublisherImpl.java @@ -240,6 +240,11 @@ public String getCommandName() { return operation.getCommandName(); } + @Override + public MongoNamespace getNamespace() { + return operation.getNamespace(); + } + @Override public void executeAsync(final AsyncReadBinding binding, final SingleResultCallback> callback) { operation.executeAsync(binding, callback::onResult); @@ -262,6 +267,11 @@ public String getCommandName() { return operation.getCommandName(); } + @Override + public MongoNamespace getNamespace() { + return operation.getNamespace(); + } + @Override public Void execute(final WriteBinding binding) { throw new UnsupportedOperationException("This operation is async only"); diff --git a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/OperationExecutorImpl.java b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/OperationExecutorImpl.java index 56b0526e4cb..06f64becf5c 100644 --- a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/OperationExecutorImpl.java +++ b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/OperationExecutorImpl.java @@ -34,6 +34,7 @@ import com.mongodb.internal.operation.OperationHelper; import com.mongodb.internal.operation.ReadOperation; import com.mongodb.internal.operation.WriteOperation; +import com.mongodb.internal.tracing.TracingManager; import com.mongodb.lang.Nullable; import com.mongodb.reactivestreams.client.ClientSession; import com.mongodb.reactivestreams.client.ReactiveContextProvider; @@ -204,6 +205,7 @@ private OperationContext getOperationContext(final RequestContext requestContext requestContext, new ReadConcernAwareNoOpSessionContext(readConcern), createTimeoutContext(session, timeoutSettings), + TracingManager.NO_OP, mongoClient.getSettings().getServerApi(), commandName); } diff --git a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/VoidReadOperationThenCursorReadOperation.java b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/VoidReadOperationThenCursorReadOperation.java index e74949432b9..f5f3ae29969 100644 --- a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/VoidReadOperationThenCursorReadOperation.java +++ b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/VoidReadOperationThenCursorReadOperation.java @@ -16,6 +16,7 @@ package com.mongodb.reactivestreams.client.internal; +import com.mongodb.MongoNamespace; import com.mongodb.internal.async.AsyncBatchCursor; import com.mongodb.internal.async.SingleResultCallback; import com.mongodb.internal.binding.AsyncReadBinding; @@ -45,6 +46,11 @@ public String getCommandName() { return readOperation.getCommandName(); } + @Override + public MongoNamespace getNamespace() { + return readOperation.getNamespace(); + } + @Override public void executeAsync(final AsyncReadBinding binding, final SingleResultCallback> callback) { readOperation.executeAsync(binding, (result, t) -> { diff --git a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/VoidWriteOperationThenCursorReadOperation.java b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/VoidWriteOperationThenCursorReadOperation.java index 428ad21ca26..1a741d7d0f6 100644 --- a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/VoidWriteOperationThenCursorReadOperation.java +++ b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/VoidWriteOperationThenCursorReadOperation.java @@ -16,6 +16,7 @@ package com.mongodb.reactivestreams.client.internal; +import com.mongodb.MongoNamespace; import com.mongodb.internal.async.AsyncBatchCursor; import com.mongodb.internal.async.SingleResultCallback; import com.mongodb.internal.binding.AsyncReadBinding; @@ -38,6 +39,11 @@ public String getCommandName() { return writeOperation.getCommandName(); } + @Override + public MongoNamespace getNamespace() { + return writeOperation.getNamespace(); + } + @Override public void executeAsync(final AsyncReadBinding binding, final SingleResultCallback> callback) { writeOperation.executeAsync((AsyncWriteBinding) binding, (result, t) -> { diff --git a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncClientSession.java b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncClientSession.java index 494e5f8c74e..ab234ad6f3e 100644 --- a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncClientSession.java +++ b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncClientSession.java @@ -22,6 +22,7 @@ import com.mongodb.client.ClientSession; import com.mongodb.client.TransactionBody; import com.mongodb.internal.TimeoutContext; +import com.mongodb.internal.tracing.TransactionSpan; import com.mongodb.lang.Nullable; import com.mongodb.session.ServerSession; import org.bson.BsonDocument; @@ -188,6 +189,12 @@ public TimeoutContext getTimeoutContext() { return wrapped.getTimeoutContext(); } + @Override + @Nullable + public TransactionSpan getTransactionSpan() { + return null; + } + private static void sleep(final long millis) { try { Thread.sleep(millis); diff --git a/driver-scala/build.gradle.kts b/driver-scala/build.gradle.kts index 68187889629..c1e0365829b 100644 --- a/driver-scala/build.gradle.kts +++ b/driver-scala/build.gradle.kts @@ -36,6 +36,9 @@ dependencies { // Encryption testing integrationTestImplementation(project(path = ":mongodb-crypt", configuration = "default")) + + // Tracing + testImplementation(libs.micrometer.tracing.integration.test) { exclude(group = "org.junit.jupiter") } } configureMavenPublication { diff --git a/driver-scala/src/integrationTest/scala/org/mongodb/scala/syncadapter/SyncClientSession.scala b/driver-scala/src/integrationTest/scala/org/mongodb/scala/syncadapter/SyncClientSession.scala index 2866ce7427d..4c0cb19217d 100644 --- a/driver-scala/src/integrationTest/scala/org/mongodb/scala/syncadapter/SyncClientSession.scala +++ b/driver-scala/src/integrationTest/scala/org/mongodb/scala/syncadapter/SyncClientSession.scala @@ -19,6 +19,7 @@ package org.mongodb.scala.syncadapter import com.mongodb.{ ClientSessionOptions, MongoInterruptedException, ServerAddress, TransactionOptions } import com.mongodb.client.{ ClientSession => JClientSession, TransactionBody } import com.mongodb.internal.TimeoutContext +import com.mongodb.internal.tracing.TransactionSpan import com.mongodb.session.ServerSession import org.bson.{ BsonDocument, BsonTimestamp } import org.mongodb.scala._ @@ -96,4 +97,6 @@ case class SyncClientSession(wrapped: ClientSession, originator: Object) extends } override def getTimeoutContext: TimeoutContext = wrapped.getTimeoutContext + + override def getTransactionSpan: TransactionSpan = null } diff --git a/driver-scala/src/test/scala/org/mongodb/scala/ApiAliasAndCompanionSpec.scala b/driver-scala/src/test/scala/org/mongodb/scala/ApiAliasAndCompanionSpec.scala index a5b76965651..a15229411ae 100644 --- a/driver-scala/src/test/scala/org/mongodb/scala/ApiAliasAndCompanionSpec.scala +++ b/driver-scala/src/test/scala/org/mongodb/scala/ApiAliasAndCompanionSpec.scala @@ -94,7 +94,11 @@ class ApiAliasAndCompanionSpec extends BaseSpec { "SyncClientEncryption", "BaseClientUpdateOptions", "BaseClientDeleteOptions", - "MongoBaseInterfaceAssertions" + "MongoBaseInterfaceAssertions", + "MicrometerTracer", + "TraceContext", + "Span", + "Tracer" ) val scalaExclusions = Set( "BuildInfo", diff --git a/driver-sync/build.gradle.kts b/driver-sync/build.gradle.kts index 95cd0979973..2a5ae5d3b64 100644 --- a/driver-sync/build.gradle.kts +++ b/driver-sync/build.gradle.kts @@ -15,6 +15,7 @@ */ import ProjectExtensions.configureJarManifest import ProjectExtensions.configureMavenPublication +import project.DEFAULT_JAVA_VERSION plugins { id("project.java") @@ -35,9 +36,22 @@ dependencies { testImplementation(project(path = ":bson", configuration = "testArtifacts")) testImplementation(project(path = ":driver-core", configuration = "testArtifacts")) + optionalImplementation(libs.micrometer.observation) // lambda testing testImplementation(libs.aws.lambda.core) + + // Tracing testing + testImplementation(libs.micrometer.tracing.integration.test) { exclude(group = "org.junit.jupiter") } +} + +tasks.withType { + // Needed for MicrometerProseTest to set env variable programmatically (calls + // `field.setAccessible(true)`) + val testJavaVersion: Int = findProperty("javaVersion")?.toString()?.toInt() ?: DEFAULT_JAVA_VERSION + if (testJavaVersion >= DEFAULT_JAVA_VERSION) { + jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED") + } } configureMavenPublication { diff --git a/driver-sync/src/main/com/mongodb/client/ClientSession.java b/driver-sync/src/main/com/mongodb/client/ClientSession.java index 5d994b863e8..abf0ff33fbd 100644 --- a/driver-sync/src/main/com/mongodb/client/ClientSession.java +++ b/driver-sync/src/main/com/mongodb/client/ClientSession.java @@ -18,6 +18,7 @@ import com.mongodb.ServerAddress; import com.mongodb.TransactionOptions; +import com.mongodb.internal.tracing.TransactionSpan; import com.mongodb.lang.Nullable; /** @@ -125,4 +126,13 @@ public interface ClientSession extends com.mongodb.session.ClientSession { * @since 3.11 */ T withTransaction(TransactionBody transactionBody, TransactionOptions options); + + /** + * Get the transaction span (if started). + * + * @return the transaction span + * @since 5.7 + */ + @Nullable + TransactionSpan getTransactionSpan(); } diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java index b60fc90316a..c1937843a9d 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java +++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java @@ -36,6 +36,8 @@ import com.mongodb.internal.operation.WriteOperation; import com.mongodb.internal.session.BaseClientSessionImpl; import com.mongodb.internal.session.ServerSessionPool; +import com.mongodb.internal.tracing.TracingManager; +import com.mongodb.internal.tracing.TransactionSpan; import com.mongodb.lang.Nullable; import static com.mongodb.MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL; @@ -54,11 +56,14 @@ final class ClientSessionImpl extends BaseClientSessionImpl implements ClientSes private boolean messageSentInCurrentTransaction; private boolean commitInProgress; private TransactionOptions transactionOptions; + private final TracingManager tracingManager; + private TransactionSpan transactionSpan = null; ClientSessionImpl(final ServerSessionPool serverSessionPool, final Object originator, final ClientSessionOptions options, - final OperationExecutor operationExecutor) { + final OperationExecutor operationExecutor, final TracingManager tracingManager) { super(serverSessionPool, originator, options); this.operationExecutor = operationExecutor; + this.tracingManager = tracingManager; } @Override @@ -141,6 +146,9 @@ public void abortTransaction() { } finally { clearTransactionContext(); cleanupTransaction(TransactionState.ABORTED); + if (transactionSpan != null) { + transactionSpan.finalizeTransactionSpan(TransactionState.ABORTED.name()); + } } } @@ -167,6 +175,10 @@ private void startTransaction(final TransactionOptions transactionOptions, final if (!writeConcern.isAcknowledged()) { throw new MongoClientException("Transactions do not support unacknowledged write concern"); } + + if (tracingManager.isEnabled()) { + transactionSpan = new TransactionSpan(tracingManager); + } clearTransactionContext(); setTimeoutContext(timeoutContext); } @@ -187,7 +199,7 @@ private void commitTransaction(final boolean resetTimeout) { if (transactionState == TransactionState.NONE) { throw new IllegalStateException("There is no transaction started"); } - + boolean exceptionThrown = false; try { if (messageSentInCurrentTransaction) { ReadConcern readConcern = transactionOptions.getReadConcern(); @@ -206,11 +218,20 @@ private void commitTransaction(final boolean resetTimeout) { .recoveryToken(getRecoveryToken()), readConcern, this); } } catch (MongoException e) { + exceptionThrown = true; clearTransactionContextOnError(e); + if (transactionSpan != null) { + transactionSpan.handleTransactionSpanError(e); + } throw e; } finally { transactionState = TransactionState.COMMITTED; commitInProgress = false; + if (!exceptionThrown) { + if (transactionSpan != null) { + transactionSpan.finalizeTransactionSpan(TransactionState.COMMITTED.name()); + } + } } } @@ -231,51 +252,72 @@ public T withTransaction(final TransactionBody transactionBody, final Tra long startTime = ClientSessionClock.INSTANCE.now(); TimeoutContext withTransactionTimeoutContext = createTimeoutContext(options); - outer: - while (true) { - T retVal; - try { - startTransaction(options, withTransactionTimeoutContext.copyTimeoutContext()); - retVal = transactionBody.execute(); - } catch (Throwable e) { - if (transactionState == TransactionState.IN) { - abortTransaction(); - } - if (e instanceof MongoException && !(e instanceof MongoOperationTimeoutException)) { - MongoException exceptionToHandle = OperationHelper.unwrap((MongoException) e); - if (exceptionToHandle.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL) - && ClientSessionClock.INSTANCE.now() - startTime < MAX_RETRY_TIME_LIMIT_MS) { - continue; + try { + outer: + while (true) { + T retVal; + try { + startTransaction(options, withTransactionTimeoutContext.copyTimeoutContext()); + if (transactionSpan != null) { + transactionSpan.setIsConvenientTransaction(); } - } - throw e; - } - if (transactionState == TransactionState.IN) { - while (true) { - try { - commitTransaction(false); - break; - } catch (MongoException e) { - clearTransactionContextOnError(e); - if (!(e instanceof MongoOperationTimeoutException) + retVal = transactionBody.execute(); + } catch (Throwable e) { + if (transactionState == TransactionState.IN) { + abortTransaction(); + } + if (e instanceof MongoException && !(e instanceof MongoOperationTimeoutException)) { + MongoException exceptionToHandle = OperationHelper.unwrap((MongoException) e); + if (exceptionToHandle.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL) && ClientSessionClock.INSTANCE.now() - startTime < MAX_RETRY_TIME_LIMIT_MS) { - applyMajorityWriteConcernToTransactionOptions(); + if (transactionSpan != null) { + transactionSpan.spanFinalizing(false); + } + continue; + } + } + throw e; + } + if (transactionState == TransactionState.IN) { + while (true) { + try { + commitTransaction(false); + break; + } catch (MongoException e) { + clearTransactionContextOnError(e); + if (!(e instanceof MongoOperationTimeoutException) + && ClientSessionClock.INSTANCE.now() - startTime < MAX_RETRY_TIME_LIMIT_MS) { + applyMajorityWriteConcernToTransactionOptions(); - if (!(e instanceof MongoExecutionTimeoutException) - && e.hasErrorLabel(UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) { - continue; - } else if (e.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)) { - continue outer; + if (!(e instanceof MongoExecutionTimeoutException) + && e.hasErrorLabel(UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) { + continue; + } else if (e.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)) { + if (transactionSpan != null) { + transactionSpan.spanFinalizing(true); + } + continue outer; + } } + throw e; } - throw e; } } + return retVal; + } + } finally { + if (transactionSpan != null) { + transactionSpan.spanFinalizing(true); } - return retVal; } } + @Override + @Nullable + public TransactionSpan getTransactionSpan() { + return transactionSpan; + } + @Override public void close() { try { diff --git a/driver-sync/src/main/com/mongodb/client/internal/MapReduceIterableImpl.java b/driver-sync/src/main/com/mongodb/client/internal/MapReduceIterableImpl.java index be3e8ca05e9..b7c05c5ffc2 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/MapReduceIterableImpl.java +++ b/driver-sync/src/main/com/mongodb/client/internal/MapReduceIterableImpl.java @@ -240,6 +240,11 @@ public String getCommandName() { return operation.getCommandName(); } + @Override + public MongoNamespace getNamespace() { + return operation.getNamespace(); + } + @Override public BatchCursor execute(final ReadBinding binding) { return operation.execute(binding); diff --git a/driver-sync/src/main/com/mongodb/client/internal/MongoClientImpl.java b/driver-sync/src/main/com/mongodb/client/internal/MongoClientImpl.java index 6870277b1c6..b227539557f 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/MongoClientImpl.java +++ b/driver-sync/src/main/com/mongodb/client/internal/MongoClientImpl.java @@ -48,6 +48,7 @@ import com.mongodb.internal.diagnostics.logging.Logger; import com.mongodb.internal.diagnostics.logging.Loggers; import com.mongodb.internal.session.ServerSessionPool; +import com.mongodb.internal.tracing.TracingManager; import com.mongodb.lang.Nullable; import org.bson.BsonDocument; import org.bson.Document; @@ -106,7 +107,8 @@ public MongoClientImpl(final Cluster cluster, operationExecutor, settings.getReadConcern(), settings.getReadPreference(), settings.getRetryReads(), settings.getRetryWrites(), settings.getServerApi(), new ServerSessionPool(cluster, TimeoutSettings.create(settings), settings.getServerApi()), - TimeoutSettings.create(settings), settings.getUuidRepresentation(), settings.getWriteConcern()); + TimeoutSettings.create(settings), settings.getUuidRepresentation(), + settings.getWriteConcern(), new TracingManager(settings.getTracer())); this.closed = new AtomicBoolean(); BsonDocument clientMetadataDocument = delegate.getCluster().getClientMetadata().getBsonDocument(); diff --git a/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java b/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java index 058122e9c26..0907ed951b2 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java +++ b/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java @@ -22,6 +22,7 @@ import com.mongodb.MongoClientException; import com.mongodb.MongoException; import com.mongodb.MongoInternalException; +import com.mongodb.MongoNamespace; import com.mongodb.MongoQueryException; import com.mongodb.MongoSocketException; import com.mongodb.MongoTimeoutException; @@ -43,6 +44,7 @@ import com.mongodb.client.model.bulk.ClientNamespacedWriteModel; import com.mongodb.internal.IgnorableRequestContext; import com.mongodb.internal.TimeoutSettings; +import com.mongodb.internal.binding.BindingContext; import com.mongodb.internal.binding.ClusterAwareReadWriteBinding; import com.mongodb.internal.binding.ClusterBinding; import com.mongodb.internal.binding.ReadBinding; @@ -57,7 +59,12 @@ import com.mongodb.internal.operation.ReadOperation; import com.mongodb.internal.operation.WriteOperation; import com.mongodb.internal.session.ServerSessionPool; +import com.mongodb.tracing.Span; +import com.mongodb.tracing.TraceContext; +import com.mongodb.internal.tracing.TracingManager; +import com.mongodb.internal.tracing.TransactionSpan; import com.mongodb.lang.Nullable; +import io.micrometer.common.KeyValues; import org.bson.BsonDocument; import org.bson.Document; import org.bson.UuidRepresentation; @@ -71,11 +78,17 @@ import static com.mongodb.MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL; import static com.mongodb.MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL; +import static com.mongodb.MongoNamespace.COMMAND_COLLECTION_NAME; import static com.mongodb.ReadPreference.primary; import static com.mongodb.assertions.Assertions.isTrue; import static com.mongodb.assertions.Assertions.isTrueArgument; import static com.mongodb.assertions.Assertions.notNull; import static com.mongodb.internal.TimeoutContext.createTimeoutContext; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.COLLECTION; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.NAMESPACE; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.OPERATION_NAME; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.OPERATION_SUMMARY; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.SYSTEM; final class MongoClusterImpl implements MongoCluster { @Nullable @@ -99,6 +112,7 @@ final class MongoClusterImpl implements MongoCluster { private final UuidRepresentation uuidRepresentation; private final WriteConcern writeConcern; private final Operations operations; + private final TracingManager tracingManager; MongoClusterImpl( @Nullable final AutoEncryptionSettings autoEncryptionSettings, final Cluster cluster, final CodecRegistry codecRegistry, @@ -106,7 +120,8 @@ final class MongoClusterImpl implements MongoCluster { @Nullable final OperationExecutor operationExecutor, final ReadConcern readConcern, final ReadPreference readPreference, final boolean retryReads, final boolean retryWrites, @Nullable final ServerApi serverApi, final ServerSessionPool serverSessionPool, final TimeoutSettings timeoutSettings, final UuidRepresentation uuidRepresentation, - final WriteConcern writeConcern) { + final WriteConcern writeConcern, + final TracingManager tracingManager) { this.autoEncryptionSettings = autoEncryptionSettings; this.cluster = cluster; this.codecRegistry = codecRegistry; @@ -123,6 +138,7 @@ final class MongoClusterImpl implements MongoCluster { this.timeoutSettings = timeoutSettings; this.uuidRepresentation = uuidRepresentation; this.writeConcern = writeConcern; + this.tracingManager = tracingManager; operations = new Operations<>( null, BsonDocument.class, @@ -166,35 +182,35 @@ public Long getTimeout(final TimeUnit timeUnit) { public MongoCluster withCodecRegistry(final CodecRegistry codecRegistry) { return new MongoClusterImpl(autoEncryptionSettings, cluster, codecRegistry, contextProvider, crypt, originator, operationExecutor, readConcern, readPreference, retryReads, retryWrites, serverApi, serverSessionPool, timeoutSettings, - uuidRepresentation, writeConcern); + uuidRepresentation, writeConcern, tracingManager); } @Override public MongoCluster withReadPreference(final ReadPreference readPreference) { return new MongoClusterImpl(autoEncryptionSettings, cluster, codecRegistry, contextProvider, crypt, originator, operationExecutor, readConcern, readPreference, retryReads, retryWrites, serverApi, serverSessionPool, timeoutSettings, - uuidRepresentation, writeConcern); + uuidRepresentation, writeConcern, tracingManager); } @Override public MongoCluster withWriteConcern(final WriteConcern writeConcern) { return new MongoClusterImpl(autoEncryptionSettings, cluster, codecRegistry, contextProvider, crypt, originator, operationExecutor, readConcern, readPreference, retryReads, retryWrites, serverApi, serverSessionPool, timeoutSettings, - uuidRepresentation, writeConcern); + uuidRepresentation, writeConcern, tracingManager); } @Override public MongoCluster withReadConcern(final ReadConcern readConcern) { return new MongoClusterImpl(autoEncryptionSettings, cluster, codecRegistry, contextProvider, crypt, originator, operationExecutor, readConcern, readPreference, retryReads, retryWrites, serverApi, serverSessionPool, timeoutSettings, - uuidRepresentation, writeConcern); + uuidRepresentation, writeConcern, tracingManager); } @Override public MongoCluster withTimeout(final long timeout, final TimeUnit timeUnit) { return new MongoClusterImpl(autoEncryptionSettings, cluster, codecRegistry, contextProvider, crypt, originator, operationExecutor, readConcern, readPreference, retryReads, retryWrites, serverApi, serverSessionPool, - timeoutSettings.withTimeout(timeout, timeUnit), uuidRepresentation, writeConcern); + timeoutSettings.withTimeout(timeout, timeUnit), uuidRepresentation, writeConcern, tracingManager); } @Override @@ -249,7 +265,7 @@ public ClientSession startSession(final ClientSessionOptions options) { .readPreference(readPreference) .build())) .build(); - return new ClientSessionImpl(serverSessionPool, originator, mergedOptions, operationExecutor); + return new ClientSessionImpl(serverSessionPool, originator, mergedOptions, operationExecutor, tracingManager); } @Override @@ -419,6 +435,8 @@ public T execute(final ReadOperation operation, final ReadPreference r ReadBinding binding = getReadBinding(readPreference, readConcern, actualClientSession, session == null, operation.getCommandName()); + Span span = createOperationSpan(actualClientSession, binding, operation.getCommandName(), operation.getNamespace()); + try { if (actualClientSession.hasActiveTransaction() && !binding.getReadPreference().equals(primary())) { throw new MongoClientException("Read preference in a transaction must be primary"); @@ -428,9 +446,15 @@ public T execute(final ReadOperation operation, final ReadPreference r MongoException exceptionToHandle = OperationHelper.unwrap(e); labelException(actualClientSession, exceptionToHandle); clearTransactionContextOnTransientTransactionError(session, exceptionToHandle); + if (span != null) { + span.error(e); + } throw e; } finally { binding.release(); + if (span != null) { + span.end(); + } } } @@ -444,15 +468,22 @@ public T execute(final WriteOperation operation, final ReadConcern readCo ClientSession actualClientSession = getClientSession(session); WriteBinding binding = getWriteBinding(readConcern, actualClientSession, session == null, operation.getCommandName()); + Span span = createOperationSpan(actualClientSession, binding, operation.getCommandName(), operation.getNamespace()); try { return operation.execute(binding); } catch (MongoException e) { MongoException exceptionToHandle = OperationHelper.unwrap(e); labelException(actualClientSession, exceptionToHandle); clearTransactionContextOnTransientTransactionError(session, exceptionToHandle); + if (span != null) { + span.error(e); + } throw e; } finally { binding.release(); + if (span != null) { + span.end(); + } } } @@ -499,6 +530,7 @@ private OperationContext getOperationContext(final ClientSession session, final getRequestContext(), new ReadConcernAwareNoOpSessionContext(readConcern), createTimeoutContext(session, executorTimeoutSettings), + tracingManager, serverApi, commandName); } @@ -556,5 +588,51 @@ ClientSession getClientSession(@Nullable final ClientSession clientSessionFromOp } return session; } + + /** + * Create a tracing span for the given operation, and set it on operation context. + * + * @param actualClientSession the session that the operation is part of + * @param binding the binding for the operation + * @param commandName the name of the command + * @param namespace the namespace of the command + * @return the created span, or null if tracing is not enabled + */ + @Nullable + private Span createOperationSpan(final ClientSession actualClientSession, final BindingContext binding, final String commandName, final MongoNamespace namespace) { + TracingManager tracingManager = binding.getOperationContext().getTracingManager(); + if (tracingManager.isEnabled()) { + TraceContext parentContext = null; + TransactionSpan transactionSpan = actualClientSession.getTransactionSpan(); + if (transactionSpan != null) { + parentContext = transactionSpan.getContext(); + } + String name = commandName + " " + namespace.getDatabaseName() + (COMMAND_COLLECTION_NAME.equalsIgnoreCase(namespace.getCollectionName()) + ? "" + : "." + namespace.getCollectionName()); + + KeyValues keyValues = KeyValues.of( + SYSTEM.withValue("mongodb"), + NAMESPACE.withValue(namespace.getDatabaseName())); + if (!COMMAND_COLLECTION_NAME.equalsIgnoreCase(namespace.getCollectionName())) { + keyValues = keyValues.and(COLLECTION.withValue(namespace.getCollectionName())); + } + keyValues = keyValues.and(OPERATION_NAME.withValue(commandName), + OPERATION_SUMMARY.withValue(name)); + + Span span = binding + .getOperationContext() + .getTracingManager() + .addSpan(name, parentContext, namespace); + + span.tagLowCardinality(keyValues); + + binding.getOperationContext().setTracingSpan(span); + return span; + + } else { + return null; + } + } } } diff --git a/driver-sync/src/test/functional/com/mongodb/client/tracing/MicrometerProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/tracing/MicrometerProseTest.java new file mode 100644 index 00000000000..5b0947ac3eb --- /dev/null +++ b/driver-sync/src/test/functional/com/mongodb/client/tracing/MicrometerProseTest.java @@ -0,0 +1,163 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.client.tracing; + +import com.mongodb.MongoClientSettings; +import com.mongodb.client.Fixture; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import com.mongodb.tracing.MicrometerTracer; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.test.reporter.inmemory.InMemoryOtelSetup; +import org.bson.Document; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.util.Map; + +import static com.mongodb.ClusterFixture.getDefaultDatabaseName; +import static com.mongodb.tracing.MongodbObservation.HighCardinalityKeyNames.QUERY_TEXT; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Implementation of the prose tests for Micrometer OpenTelemetry tracing. + */ +public class MicrometerProseTest { + private ObservationRegistry observationRegistry = ObservationRegistry.create(); + private InMemoryOtelSetup memoryOtelSetup; + private InMemoryOtelSetup.Builder.OtelBuildingBlocks inMemoryOtel; + + @BeforeEach + void setUp() { + memoryOtelSetup = InMemoryOtelSetup.builder().register(observationRegistry); + inMemoryOtel = memoryOtelSetup.getBuildingBlocks(); + } + + @AfterEach + void tearDown() { + memoryOtelSetup.close(); + } + + // Test 1: Tracing Enable/Disable via Environment Variable + @Test + void testControlOtelInstrumentationViaEnvironmentVariable() throws Exception { + MicrometerTracer tracer = new MicrometerTracer(observationRegistry); + setEnv("OTEL_JAVA_INSTRUMENTATION_MONGODB_ENABLED", "false"); + + MongoClientSettings clientSettings = Fixture.getMongoClientSettingsBuilder() + .tracer(tracer).build(); + + try (MongoClient client = MongoClients.create(clientSettings)) { + MongoDatabase database = client.getDatabase(getDefaultDatabaseName()); + MongoCollection collection = database.getCollection("test"); + collection.find().first(); + + // Assert that no OpenTelemetry tracing spans are emitted for the operation. + assertTrue(inMemoryOtel.getFinishedSpans().isEmpty(), "Spans should not be emitted when instrumentation is enabled."); + } + + setEnv("OTEL_JAVA_INSTRUMENTATION_MONGODB_ENABLED", "true"); + clientSettings = Fixture.getMongoClientSettingsBuilder() + .tracer(tracer).build(); + try (MongoClient client = MongoClients.create(clientSettings)) { + MongoDatabase database = client.getDatabase(getDefaultDatabaseName()); + MongoCollection collection = database.getCollection("test"); + collection.find().first(); + + // Assert that OpenTelemetry tracing spans are emitted for the operation. + assertEquals(2, inMemoryOtel.getFinishedSpans().size(), "Spans should be emitted when instrumentation is disabled."); + assertEquals("find", inMemoryOtel.getFinishedSpans().get(0).getName()); + assertEquals("find " + getDefaultDatabaseName() + ".test", inMemoryOtel.getFinishedSpans().get(1).getName()); + } + } + + @Test + void testControlCommandPayloadViaEnvironmentVariable() throws Exception { + MicrometerTracer tracer = new MicrometerTracer(observationRegistry, true); + setEnv("OTEL_JAVA_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH", "42"); + + MongoClientSettings clientSettings = Fixture.getMongoClientSettingsBuilder() + .tracer(tracer).build(); + + try (MongoClient client = MongoClients.create(clientSettings)) { + MongoDatabase database = client.getDatabase(getDefaultDatabaseName()); + MongoCollection collection = database.getCollection("test"); + collection.find().first(); + + // Assert that the emitted tracing span includes the db.query.text attribute. + assertEquals(2, inMemoryOtel.getFinishedSpans().size(), "Spans should be emitted when instrumentation is disabled."); + assertEquals("find", inMemoryOtel.getFinishedSpans().get(0).getName()); + + Map.Entry queryTag = inMemoryOtel.getFinishedSpans().get(0).getTags().entrySet() + .stream() + .filter(entry -> entry.getKey().equals(QUERY_TEXT.asString())) + .findFirst() + .orElseThrow(() -> new AssertionError("Attribute " + QUERY_TEXT.asString() + " not found.")); + assertEquals(46, queryTag.getValue().length(), "Query text length should be 46."); // 42 truncated string + " ..." + } finally { + memoryOtelSetup.close(); + } + + memoryOtelSetup = InMemoryOtelSetup.builder().register(observationRegistry); + inMemoryOtel = memoryOtelSetup.getBuildingBlocks(); + tracer = new MicrometerTracer(observationRegistry); // don't enable command payload by default + setEnv("OTEL_JAVA_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH", null); // Unset the environment variable + + clientSettings = Fixture.getMongoClientSettingsBuilder() + .tracer(tracer).build(); + try (MongoClient client = MongoClients.create(clientSettings)) { + MongoDatabase database = client.getDatabase(getDefaultDatabaseName()); + MongoCollection collection = database.getCollection("test"); + collection.find().first(); + + // Assert no query.text tag is emitted + assertTrue( + inMemoryOtel.getFinishedSpans().get(0).getTags().entrySet().stream() + .noneMatch(entry -> entry.getKey().equals(QUERY_TEXT.asString())), + "Tag " + QUERY_TEXT.asString() + " should not exist." + ); + } + } + + @SuppressWarnings("unchecked") + private void setEnv(final String key, final String value) throws Exception { + // Get the unmodifiable Map from System.getenv() + Map env = System.getenv(); + + // Use reflection to get the class of the unmodifiable map + Class unmodifiableMapClass = env.getClass(); + + // Get the 'm' field which holds the actual modifiable map + Field mField = unmodifiableMapClass.getDeclaredField("m"); + mField.setAccessible(true); + + // Get the modifiable map from the 'm' field + Map modifiableEnv = (Map) mField.get(env); + + // Modify the map + if (value == null) { + modifiableEnv.remove(key); + } else { + modifiableEnv.put(key, value); + } + } +} diff --git a/driver-sync/src/test/functional/com/mongodb/client/tracing/SpanTree.java b/driver-sync/src/test/functional/com/mongodb/client/tracing/SpanTree.java new file mode 100644 index 00000000000..c5ec573ad76 --- /dev/null +++ b/driver-sync/src/test/functional/com/mongodb/client/tracing/SpanTree.java @@ -0,0 +1,347 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.client.tracing; + +import com.mongodb.lang.Nullable; +import io.micrometer.tracing.exporter.FinishedSpan; +import io.micrometer.tracing.test.simple.SimpleSpan; +import org.bson.BsonArray; +import org.bson.BsonBinary; +import org.bson.BsonDocument; +import org.bson.BsonInt64; +import org.bson.BsonString; +import org.bson.BsonValue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.function.BiConsumer; + +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.CLIENT_CONNECTION_ID; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.CURSOR_ID; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.SERVER_CONNECTION_ID; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.SERVER_PORT; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.SESSION_ID; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.TRANSACTION_NUMBER; +import static org.bson.assertions.Assertions.notNull; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Represents a tree structure of spans, where each span can have nested spans as children. + * This class provides methods to create a span tree from various sources and to validate the spans against expected values. + */ +public class SpanTree { + private final List roots = new ArrayList<>(); + + /** + * Creates a SpanTree from a BsonArray of spans. + * + * @param spans the BsonArray containing span documents + * @return a SpanTree constructed from the provided spans + */ + public static SpanTree from(final BsonArray spans) { + SpanTree spanTree = new SpanTree(); + for (final BsonValue span : spans) { + if (span.isDocument()) { + final BsonDocument spanDoc = span.asDocument(); + final String name = spanDoc.getString("name").getValue(); + final SpanNode rootNode = new SpanNode(name); + spanTree.roots.add(rootNode); + + if (spanDoc.containsKey("attributes")) { + rootNode.tags = spanDoc.getDocument("attributes"); + } + + if (spanDoc.containsKey("nested")) { + for (final BsonValue nestedSpan : spanDoc.getArray("nested")) { + addNestedSpans(rootNode, nestedSpan.asDocument()); + } + } + } + } + + return spanTree; + } + + /** + * Creates a SpanTree from a JSON string representation of spans. + * + * @param spansAsJson the JSON string containing span documents + * @return a SpanTree constructed from the provided JSON spans + */ + public static SpanTree from(final String spansAsJson) { + BsonArray spans = BsonArray.parse(spansAsJson); + return from(spans); + } + + /** + * Creates a SpanTree from a Deque of SimpleSpan objects. + * This method is typically used to build a tree based on the actual collected tracing spans. + * + * @param spans the Deque containing SimpleSpan objects + * @return a SpanTree constructed from the provided spans + */ + public static SpanTree from(final Deque spans) { + final SpanTree spanTree = new SpanTree(); + final Map idToSpanNode = new HashMap<>(); + for (final SimpleSpan span : spans) { + final SpanNode spanNode = new SpanNode(span.getName()); + for (final Map.Entry tag : span.getTags().entrySet()) { + // handle special case of session id (needs to be parsed into a BsonBinary) + // this is needed because the SimpleTracer reports all the collected tags as strings + if (tag.getKey().equals(SESSION_ID.asString())) { + spanNode.tags.append(tag.getKey(), new BsonDocument().append("id", new BsonBinary(UUID.fromString(tag.getValue())))); + + } else if (tag.getKey().equals(CURSOR_ID.asString()) + || tag.getKey().equals(SERVER_PORT.asString()) + || tag.getKey().equals(TRANSACTION_NUMBER.asString()) + || tag.getKey().equals(CLIENT_CONNECTION_ID.asString()) + || tag.getKey().equals(SERVER_CONNECTION_ID.asString())) { + spanNode.tags.append(tag.getKey(), new BsonInt64(Long.parseLong(tag.getValue()))); + } else { + spanNode.tags.append(tag.getKey(), new BsonString(tag.getValue())); + } + } + idToSpanNode.put(span.context().spanId(), spanNode); + } + + for (final SimpleSpan span : spans) { + final String parentId = span.context().parentId(); + final SpanNode node = idToSpanNode.get(span.context().spanId()); + + if (!parentId.isEmpty() && idToSpanNode.containsKey(parentId)) { + idToSpanNode.get(parentId).children.add(node); + } else { // doesn't have a parent, so it is a root node + spanTree.roots.add(node); + } + } + return spanTree; + } + + public static SpanTree from(final List spans) { + final SpanTree spanTree = new SpanTree(); + final Map idToSpanNode = new HashMap<>(); + for (final FinishedSpan span : spans) { + final SpanNode spanNode = new SpanNode(span.getName()); + for (final Map.Entry tag : span.getTags().entrySet()) { + // handle special case of session id (needs to be parsed into a BsonBinary) + // this is needed because the SimpleTracer reports all the collected tags as strings + if (tag.getKey().equals(SESSION_ID.asString())) { + spanNode.tags.append(tag.getKey(), new BsonDocument().append("id", new BsonBinary(UUID.fromString(tag.getValue())))); + + } else if (tag.getKey().equals(CURSOR_ID.asString()) + || tag.getKey().equals(SERVER_PORT.asString()) + || tag.getKey().equals(TRANSACTION_NUMBER.asString()) + || tag.getKey().equals(CLIENT_CONNECTION_ID.asString()) + || tag.getKey().equals(SERVER_CONNECTION_ID.asString())) { + spanNode.tags.append(tag.getKey(), new BsonInt64(Long.parseLong(tag.getValue()))); + } else { + spanNode.tags.append(tag.getKey(), new BsonString(tag.getValue())); + } + } + idToSpanNode.put(span.getSpanId(), spanNode); + } + + for (final FinishedSpan span : spans) { + final String parentId = span.getParentId(); + final SpanNode node = idToSpanNode.get(span.getSpanId()); + + if (parentId != null && !parentId.isEmpty() && idToSpanNode.containsKey(parentId)) { + idToSpanNode.get(parentId).children.add(node); + } else { // doesn't have a parent, so it is a root node + spanTree.roots.add(node); + } + } + return spanTree; + } + + /** + * Adds nested spans to the parent node based on the provided BsonDocument. + * This method recursively adds child spans to the parent span node. + * + * @param parentNode the parent span node to which nested spans will be added + * @param nestedSpan the BsonDocument representing a nested span + */ + private static void addNestedSpans(final SpanNode parentNode, final BsonDocument nestedSpan) { + final String name = nestedSpan.getString("name").getValue(); + final SpanNode childNode = new SpanNode(name, parentNode); + + if (nestedSpan.containsKey("attributes")) { + childNode.tags = nestedSpan.getDocument("attributes"); + } + + if (nestedSpan.containsKey("nested")) { + for (final BsonValue nested : nestedSpan.getArray("nested")) { + addNestedSpans(childNode, nested.asDocument()); + } + } + } + + /** + * Asserts that the reported spans are valid against the expected spans. + * This method checks that the reported spans match the expected spans in terms of names, tags, and structure. + * + * @param reportedSpans the SpanTree containing the reported spans + * @param expectedSpans the SpanTree containing the expected spans + * @param valueMatcher a BiConsumer to match values of tags between reported and expected spans + * @param ignoreExtraSpans if true, allows reported spans to contain extra spans not present in expected spans + */ + public static void assertValid(final SpanTree reportedSpans, final SpanTree expectedSpans, + final BiConsumer valueMatcher, + final boolean ignoreExtraSpans) { + if (ignoreExtraSpans) { + // remove from the reported spans all the nodes that are not expected + reportedSpans.roots.removeIf(node -> + expectedSpans.roots.stream().noneMatch(expectedNode -> expectedNode.getName().equalsIgnoreCase(node.getName())) + ); + } + + // check that we have the same root spans + if (reportedSpans.roots.size() != expectedSpans.roots.size()) { + fail("The number of reported spans does not match expected spans size. " + + "Reported: " + reportedSpans.roots.size() + + ", Expected: " + expectedSpans.roots.size() + + " ignoreExtraSpans: " + ignoreExtraSpans); + } + + for (int i = 0; i < reportedSpans.roots.size(); i++) { + assertValid(reportedSpans.roots.get(i), expectedSpans.roots.get(i), valueMatcher); + } + } + + /** + * Asserts that a reported span node is valid against an expected span node. + * This method checks that the reported span's name, tags, and children match the expected span. + * + * @param reportedNode the reported span node to validate + * @param expectedNode the expected span node to validate against + * @param valueMatcher a BiConsumer to match values of tags between reported and expected spans + */ + private static void assertValid(final SpanNode reportedNode, final SpanNode expectedNode, + final BiConsumer valueMatcher) { + // Check that the span names match + if (!reportedNode.getName().equalsIgnoreCase(expectedNode.getName())) { + fail("Reported span name " + + reportedNode.getName() + + " does not match expected span name " + + expectedNode.getName()); + } + + valueMatcher.accept(expectedNode.tags, reportedNode.tags); + + // Spans should have the same number of children + if (reportedNode.children.size() != expectedNode.children.size()) { + fail("Reported span " + reportedNode.getName() + + " has " + reportedNode.children.size() + + " children, but expected " + expectedNode.children.size()); + } + + // For every reported child span make sure it is valid against the expected child span + for (int i = 0; i < reportedNode.children.size(); i++) { + assertValid(reportedNode.children.get(i), expectedNode.children.get(i), valueMatcher); + } + } + + @Override + public String toString() { + return "SpanTree{" + + "roots=" + roots + + '}'; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final SpanTree spanTree = (SpanTree) o; + return Objects.deepEquals(roots, spanTree.roots); + } + + @Override + public int hashCode() { + return Objects.hash(roots); + } + + /** + * Represents a node in the span tree, which can have nested child spans. + * Each span node contains a name, tags, and a list of child span nodes. + */ + public static class SpanNode { + private final String name; + private BsonDocument tags = new BsonDocument(); + private final List children = new ArrayList<>(); + + public SpanNode(final String name) { + this.name = notNull("name", name); + } + + public SpanNode(final String name, @Nullable final SpanNode parent) { + this.name = notNull("name", name); + if (parent != null) { + parent.children.add(this); + } + } + + public String getName() { + return name; + } + + public List getChildren() { + return Collections.unmodifiableList(children); + } + + @Override + public String toString() { + return "SpanNode{" + + "name='" + name + '\'' + + ", tags=" + tags + + ", children=" + children + + '}'; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final SpanNode spanNode = (SpanNode) o; + return name.equalsIgnoreCase(spanNode.name) + && Objects.equals(tags, spanNode.tags) + && Objects.equals(children, spanNode.children); + } + + @Override + public int hashCode() { + int result = name.hashCode(); + result = 31 * result + tags.hashCode(); + result = 31 * result + children.hashCode(); + return result; + } + } +} diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java b/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java index 6f6e5bb66c8..d8497a97143 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java @@ -47,6 +47,9 @@ import com.mongodb.internal.logging.LogMessage; import com.mongodb.lang.Nullable; import com.mongodb.logging.TestLoggingInterceptor; +import com.mongodb.tracing.MicrometerTracer; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.test.reporter.inmemory.InMemoryOtelSetup; import org.bson.BsonArray; import org.bson.BsonBoolean; import org.bson.BsonDocument; @@ -90,7 +93,7 @@ public final class Entities { private static final Set SUPPORTED_CLIENT_ENTITY_OPTIONS = new HashSet<>( asList( "id", "autoEncryptOpts", "uriOptions", "serverApi", "useMultipleMongoses", "storeEventsAsEntities", - "observeEvents", "observeLogMessages", "observeSensitiveCommands", "ignoreCommandMonitoringEvents")); + "observeEvents", "observeLogMessages", "observeSensitiveCommands", "ignoreCommandMonitoringEvents", "observeTracingMessages")); private final Set entityNames = new HashSet<>(); private final Map threads = new HashMap<>(); private final Map>> tasks = new HashMap<>(); @@ -104,6 +107,8 @@ public final class Entities { private final Map clientEncryptions = new HashMap<>(); private final Map clientCommandListeners = new HashMap<>(); private final Map clientLoggingInterceptors = new HashMap<>(); + private final Map clientTracing = new HashMap<>(); + private final Set inMemoryOTelInstances = new HashSet<>(); private final Map clientConnectionPoolListeners = new HashMap<>(); private final Map clientServerListeners = new HashMap<>(); private final Map clientClusterListeners = new HashMap<>(); @@ -220,6 +225,10 @@ public TestLoggingInterceptor getClientLoggingInterceptor(final String id) { return getEntity(id + "-logging-interceptor", clientLoggingInterceptors, "logging interceptor"); } + public InMemoryOtelSetup.Builder.OtelBuildingBlocks getClientTracer(final String id) { + return getEntity(id + "-tracing", clientTracing, "micrometer tracing"); + } + public TestConnectionPoolListener getConnectionPoolListener(final String id) { return getEntity(id + "-connection-pool-listener", clientConnectionPoolListeners, "connection pool listener"); } @@ -565,6 +574,17 @@ private void initClient(final BsonDocument entity, final String id, clientSettingsBuilder.autoEncryptionSettings(builder.build()); } + if (entity.containsKey("observeTracingMessages")) { + boolean enableCommandPayload = entity.getDocument("observeTracingMessages").get("enableCommandPayload", BsonBoolean.FALSE).asBoolean().getValue(); + ObservationRegistry observationRegistry = ObservationRegistry.create(); + InMemoryOtelSetup inMemoryOtel = InMemoryOtelSetup.builder().register(observationRegistry); + InMemoryOtelSetup.Builder.OtelBuildingBlocks tracer = inMemoryOtel.getBuildingBlocks(); + + putEntity(id + "-tracing", tracer, clientTracing); + inMemoryOTelInstances.add(inMemoryOtel); + clientSettingsBuilder.tracer(new MicrometerTracer(observationRegistry, enableCommandPayload)); + } + MongoClientSettings clientSettings = clientSettingsBuilder.build(); if (entity.containsKey("observeLogMessages")) { @@ -756,5 +776,6 @@ public void close() { clients.values().forEach(MongoClient::close); clientLoggingInterceptors.values().forEach(TestLoggingInterceptor::close); threads.values().forEach(ExecutorService::shutdownNow); + inMemoryOTelInstances.forEach(InMemoryOtelSetup::close); } } diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/MicrometerTracingTest.java b/driver-sync/src/test/functional/com/mongodb/client/unified/MicrometerTracingTest.java new file mode 100644 index 00000000000..8c65317d257 --- /dev/null +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/MicrometerTracingTest.java @@ -0,0 +1,27 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.client.unified; + +import org.junit.jupiter.params.provider.Arguments; + +import java.util.Collection; + +final class MicrometerTracingTest extends UnifiedSyncTest { + private static Collection data() { + return getTestData("open-telemetry/tests"); + } +} diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java index 79b2a9c9da9..f64d219c953 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java @@ -28,6 +28,7 @@ import com.mongodb.client.gridfs.GridFSBucket; import com.mongodb.client.model.Filters; import com.mongodb.client.test.CollectionHelper; +import com.mongodb.client.tracing.SpanTree; import com.mongodb.client.unified.UnifiedTestModifications.TestDef; import com.mongodb.client.vault.ClientEncryption; import com.mongodb.connection.ClusterDescription; @@ -44,6 +45,7 @@ import com.mongodb.lang.Nullable; import com.mongodb.logging.TestLoggingInterceptor; import com.mongodb.test.AfterBeforeParameterResolver; +import io.micrometer.tracing.test.reporter.inmemory.InMemoryOtelSetup; import org.bson.BsonArray; import org.bson.BsonBoolean; import org.bson.BsonDocument; @@ -109,7 +111,7 @@ public abstract class UnifiedTest { private static final Set PRESTART_POOL_ASYNC_WORK_MANAGER_FILE_DESCRIPTIONS = Collections.singleton( "wait queue timeout errors include details about checked out connections"); - private static final String MAX_SUPPORTED_SCHEMA_VERSION = "1.25"; + private static final String MAX_SUPPORTED_SCHEMA_VERSION = "1.27"; private static final List MAX_SUPPORTED_SCHEMA_VERSION_COMPONENTS = Arrays.stream(MAX_SUPPORTED_SCHEMA_VERSION.split("\\.")) .map(Integer::parseInt) .collect(Collectors.toList()); @@ -376,6 +378,11 @@ public void shouldPassAllOutcomes( } compareLogMessages(rootContext, definition, tweaks); } + + if (definition.containsKey("expectTracingMessages")) { + compareTracingSpans(definition); + } + } catch (TestAbortedException e) { // if a test is ignored, we do not retry throw e; @@ -483,6 +490,22 @@ private void compareLogMessages(final UnifiedTestContext rootContext, final Bson } } + private void compareTracingSpans(final BsonDocument definition) { + BsonArray curTracingSpansForClients = definition.getArray("expectTracingMessages"); + for (BsonValue tracingSpan : curTracingSpansForClients) { + BsonDocument curTracingSpansForClient = tracingSpan.asDocument(); + String clientId = curTracingSpansForClient.getString("client").getValue(); + + // Get the tracer for the client + InMemoryOtelSetup.Builder.OtelBuildingBlocks micrometerTracer = entities.getClientTracer(clientId); + + SpanTree expectedSpans = SpanTree.from(curTracingSpansForClient.getArray("spans")); + SpanTree reportedSpans = SpanTree.from(micrometerTracer.getFinishedSpans()); + boolean ignoreExtraSpans = curTracingSpansForClient.getBoolean("ignoreExtraSpans", BsonBoolean.TRUE).getValue(); + SpanTree.assertValid(reportedSpans, expectedSpans, rootContext.valueMatcher::assertValuesMatch, ignoreExtraSpans); + } + } + private void assertOutcome(final UnifiedTestContext context) { for (BsonValue cur : definition.getArray("outcome")) { BsonDocument curDocument = cur.asDocument(); diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java index 327cc3f3da8..dbaa9b600b0 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java @@ -164,6 +164,10 @@ public static void applyCustomizations(final TestDef def) { .test("client-side-operations-timeout", "timeoutMS can be configured on a MongoClient", "timeoutMS can be set to 0 on a MongoClient - dropIndexes on collection"); + // There are more than 44 tests using 'awaitMinPoolSizeMS' this will be fixed in JAVA-5957 + def.skipJira("https://jira.mongodb.org/browse/JAVA-5957") + .directory("client-side-operations-timeout"); + // TODO-JAVA-5712 // collection-management diff --git a/driver-sync/src/test/unit/com/mongodb/client/internal/MongoClusterSpecification.groovy b/driver-sync/src/test/unit/com/mongodb/client/internal/MongoClusterSpecification.groovy index 563528e7dce..975b0114ff4 100644 --- a/driver-sync/src/test/unit/com/mongodb/client/internal/MongoClusterSpecification.groovy +++ b/driver-sync/src/test/unit/com/mongodb/client/internal/MongoClusterSpecification.groovy @@ -28,6 +28,7 @@ import com.mongodb.internal.TimeoutSettings import com.mongodb.internal.client.model.changestream.ChangeStreamLevel import com.mongodb.internal.connection.Cluster import com.mongodb.internal.session.ServerSessionPool +import com.mongodb.internal.tracing.TracingManager import org.bson.BsonDocument import org.bson.Document import org.bson.codecs.UuidCodec @@ -258,6 +259,7 @@ class MongoClusterSpecification extends Specification { MongoClusterImpl createMongoCluster(final MongoClientSettings settings, final OperationExecutor operationExecutor) { new MongoClusterImpl(null, cluster, settings.codecRegistry, null, null, originator, operationExecutor, settings.readConcern, settings.readPreference, settings.retryReads, settings.retryWrites, - null, serverSessionPool, TimeoutSettings.create(settings), settings.uuidRepresentation, settings.writeConcern) + null, serverSessionPool, TimeoutSettings.create(settings), settings.uuidRepresentation, + settings.writeConcern, TracingManager.NO_OP) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8b8222d66e5..057e3bc5430 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,8 @@ reactive-streams = "1.0.4" snappy = "1.1.10.3" zstd = "1.5.5-3" jetbrains-annotations = "26.0.2" +micrometer = "1.6.0-M3" # This version has a fix for https://github.com/micrometer-metrics/tracing/issues/1092 +micrometer-observation = "1.15.4" kotlin = "1.8.10" kotlinx-coroutines-bom = "1.6.4" @@ -93,6 +95,7 @@ reactive-streams = { module = " org.reactivestreams:reactive-streams", version.r slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } snappy-java = { module = "org.xerial.snappy:snappy-java", version.ref = "snappy" } zstd-jni = { module = "com.github.luben:zstd-jni", version.ref = "zstd" } +micrometer-observation = { module = "io.micrometer:micrometer-observation", version.ref = "micrometer-observation" } graal-sdk = { module = "org.graalvm.sdk:graal-sdk", version.ref = "graal-sdk" } graal-sdk-nativeimage = { module = "org.graalvm.sdk:nativeimage", version.ref = "graal-sdk" } @@ -171,7 +174,7 @@ objenesis = { module = "org.objenesis:objenesis", version.ref = "objenesis" } project-reactor-test = { module = "io.projectreactor:reactor-test" } reactive-streams-tck = { module = " org.reactivestreams:reactive-streams-tck", version.ref = "reactive-streams" } reflections = { module = "org.reflections:reflections", version.ref = "reflections" } - +micrometer-tracing-integration-test = { module = " io.micrometer:micrometer-tracing-integration-test", version.ref = "micrometer" } [bundles] aws-java-sdk-v1 = ["aws-java-sdk-v1-core", "aws-java-sdk-v1-sts"]