From f650d436cb8b872383e980870b41d6cf59b2f986 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Tue, 29 Apr 2025 14:30:04 +0100 Subject: [PATCH 01/14] Add tracing support using Micrometer --- driver-core/build.gradle.kts | 1 + .../main/com/mongodb/MongoClientSettings.java | 31 ++++++ .../internal/connection/CommandMessage.java | 4 + .../connection/InternalStreamConnection.java | 51 +++++++++ .../internal/connection/OperationContext.java | 57 ++++++---- .../operation/CommandBatchCursor.java | 7 +- .../internal/operation/FindOperation.java | 40 +++++-- .../internal/session/ServerSessionPool.java | 3 +- .../com/mongodb/internal/tracing/Span.java | 52 +++++++++ .../internal/tracing/TraceContext.java | 23 ++++ .../internal/tracing/TracingManager.java | 80 ++++++++++++++ .../internal/tracing/package-info.java | 23 ++++ .../com/mongodb/tracing/MicrometerTracer.java | 102 ++++++++++++++++++ .../src/main/com/mongodb/tracing/Tracer.java | 53 +++++++++ .../com/mongodb/tracing/package-info.java | 23 ++++ .../com/mongodb/ClusterFixture.java | 1 + driver-core/src/test/resources/specifications | 2 +- .../connection/CommandHelperTest.java | 4 +- .../connection/CommandMessageTest.java | 3 +- .../src/main/com/mongodb/MongoClient.java | 1 + .../internal/OperationExecutorImpl.java | 2 + .../client/internal/MongoClientImpl.java | 3 +- .../client/internal/MongoClusterImpl.java | 17 +-- gradle/libs.versions.toml | 2 + 24 files changed, 546 insertions(+), 39 deletions(-) create mode 100644 driver-core/src/main/com/mongodb/internal/tracing/Span.java create mode 100644 driver-core/src/main/com/mongodb/internal/tracing/TraceContext.java create mode 100644 driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java create mode 100644 driver-core/src/main/com/mongodb/internal/tracing/package-info.java create mode 100644 driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java create mode 100644 driver-core/src/main/com/mongodb/tracing/Tracer.java create mode 100644 driver-core/src/main/com/mongodb/tracing/package-info.java diff --git a/driver-core/build.gradle.kts b/driver-core/build.gradle.kts index 4f06805a6ea..7e260b18d23 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) 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 31206e56029..642682d1b3a 100644 --- a/driver-core/src/main/com/mongodb/MongoClientSettings.java +++ b/driver-core/src/main/com/mongodb/MongoClientSettings.java @@ -30,9 +30,11 @@ import com.mongodb.connection.SslSettings; import com.mongodb.connection.TransportSettings; import com.mongodb.event.CommandListener; +import com.mongodb.internal.tracing.TracingManager; import com.mongodb.lang.Nullable; import com.mongodb.spi.dns.DnsClient; import com.mongodb.spi.dns.InetAddressResolver; +import com.mongodb.tracing.Tracer; import org.bson.UuidRepresentation; import org.bson.codecs.BsonCodecProvider; import org.bson.codecs.BsonValueCodecProvider; @@ -118,6 +120,7 @@ public final class MongoClientSettings { private final InetAddressResolver inetAddressResolver; @Nullable private final Long timeoutMS; + private final TracingManager tracingManager; /** * Gets the default codec registry. It includes the following providers: @@ -238,6 +241,7 @@ public static final class Builder { private ContextProvider contextProvider; private DnsClient dnsClient; private InetAddressResolver inetAddressResolver; + private TracingManager tracingManager; private Builder() { } @@ -275,6 +279,7 @@ private Builder(final MongoClientSettings settings) { if (settings.heartbeatSocketTimeoutSetExplicitly) { heartbeatSocketTimeoutMS = settings.heartbeatSocketSettings.getReadTimeout(MILLISECONDS); } + tracingManager = settings.tracingManager; } /** @@ -723,6 +728,20 @@ Builder heartbeatSocketTimeoutMS(final int heartbeatSocketTimeoutMS) { return this; } + /** + * Sets the tracer to use for creating Spans for operations and commands. + * + * @param tracer the tracer + * @see com.mongodb.tracing.MicrometerTracer + * @return this + * @since 5.5 + */ + @Alpha(Reason.CLIENT) + public Builder tracer(final Tracer tracer) { + this.tracingManager = new TracingManager(tracer); + return this; + } + /** * Build an instance of {@code MongoClientSettings}. * @@ -1040,6 +1059,17 @@ public ContextProvider getContextProvider() { return contextProvider; } + /** + * Get the tracer to create Spans for operations and commands. + * + * @return this + * @since 5.5 + */ + @Alpha(Reason.CLIENT) + public TracingManager getTracingManager() { + return tracingManager; + } + @Override public boolean equals(final Object o) { if (this == o) { @@ -1156,5 +1186,6 @@ private MongoClientSettings(final Builder builder) { heartbeatConnectTimeoutSetExplicitly = builder.heartbeatConnectTimeoutMS != 0; contextProvider = builder.contextProvider; timeoutMS = builder.timeoutMS; + tracingManager = builder.tracingManager; } } diff --git a/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java b/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java index 12543e92ccb..0660938e4f2 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java +++ b/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java @@ -186,6 +186,10 @@ BsonDocument getCommandDocument(final ByteBufferBsonOutput bsonOutput) { } } + BsonDocument getCommand() { + return command; + } + /** * Get the field name from a buffer positioned at the start of the document sequence identifier of an OP_MSG Section of type * `PAYLOAD_TYPE_1_DOCUMENT_SEQUENCE`. 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..e477239e6dc 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java +++ b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java @@ -51,6 +51,9 @@ import com.mongodb.internal.logging.StructuredLogger; import com.mongodb.internal.session.SessionContext; import com.mongodb.internal.time.Timeout; +import com.mongodb.internal.tracing.Span; +import com.mongodb.internal.tracing.TraceContext; +import com.mongodb.internal.tracing.TracingManager; import com.mongodb.lang.Nullable; import org.bson.BsonBinaryReader; import org.bson.BsonDocument; @@ -374,13 +377,24 @@ public boolean isClosed() { public T sendAndReceive(final CommandMessage message, final Decoder decoder, final OperationContext operationContext) { Supplier sendAndReceiveInternal = () -> sendAndReceiveInternal( message, decoder, operationContext); + + Span tracingSpan = createTracingSpan(message, operationContext); + try { return sendAndReceiveInternal.get(); } catch (MongoCommandException e) { + if (tracingSpan != null) { + tracingSpan.error(e); + } + if (reauthenticationIsTriggered(e)) { return reauthenticateAndRetry(sendAndReceiveInternal, operationContext); } throw e; + } finally { + if (tracingSpan != null) { + tracingSpan.end(); + } } } @@ -391,6 +405,7 @@ public void sendAndReceiveAsync(final CommandMessage message, final Decoder< AsyncSupplier sendAndReceiveAsyncInternal = c -> sendAndReceiveAsyncInternal( message, decoder, operationContext, c); + beginAsync().thenSupply(c -> { sendAndReceiveAsyncInternal.getAsync(c); }).onErrorIf(e -> reauthenticationIsTriggered(e), (t, c) -> { @@ -872,6 +887,42 @@ public ByteBuf getBuffer(final int size) { return stream.getBuffer(size); } + @Nullable + private Span createTracingSpan(final CommandMessage message, final OperationContext operationContext) { + TracingManager tracingManager = operationContext.getTracingManager(); + Span span; + if (tracingManager.isEnabled()) { + BsonDocument command = message.getCommand(); + TraceContext parentContext = null; + long cursorId = -1; + if (command.containsKey("getMore")) { + cursorId = command.getInt64("getMore").longValue(); + parentContext = tracingManager.getCursorParentContext(cursorId); + } else { + parentContext = tracingManager.getParentContext(operationContext.getId()); + } + + span = tracingManager.addSpan("Command " + command.getFirstKey(), parentContext); + span.tag("db.system", "mongodb"); + span.tag("db.namespace", message.getNamespace().getFullName()); + span.tag("db.query.summary", command.getFirstKey()); + span.tag("db.query.opcode", String.valueOf(message.getOpCode())); + span.tag("db.query.text", command.toString()); + if (cursorId != -1) { + span.tag("db.mongodb.cursor_id", String.valueOf(cursorId)); + } + span.tag("server.address", serverId.getAddress().getHost()); + span.tag("server.port", String.valueOf(serverId.getAddress().getPort())); + span.tag("server.type", message.getSettings().getServerType().name()); + + span.tag("db.mongodb.server_connection_id", this.description.getConnectionId().toString()); + } else { + span = null; + } + + return span; + } + private class MessageHeaderCallback implements SingleResultCallback { private final OperationContext operationContext; private final SingleResultCallback callback; 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..c74f4116a3f 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,7 @@ import com.mongodb.internal.TimeoutSettings; import com.mongodb.internal.VisibleForTesting; import com.mongodb.internal.session.SessionContext; +import com.mongodb.internal.tracing.TracingManager; import com.mongodb.lang.Nullable; import com.mongodb.selector.ServerSelector; @@ -47,6 +48,7 @@ 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 @@ -54,12 +56,17 @@ public class OperationContext { 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); } public static OperationContext simpleOperationContext( @@ -68,8 +75,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 +86,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); } 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); } 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); } public long getId() { return id; } + public TracingManager getTracingManager() { + return tracingManager; + } + public SessionContext getSessionContext() { return sessionContext; } @@ -121,33 +138,37 @@ public String getOperationName() { @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) { this.id = id; this.serverDeprioritization = serverDeprioritization; this.requestContext = requestContext; this.sessionContext = sessionContext; this.timeoutContext = timeoutContext; + this.tracingManager = tracingManager; this.serverApi = serverApi; this.operationName = operationName; } @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; } diff --git a/driver-core/src/main/com/mongodb/internal/operation/CommandBatchCursor.java b/driver-core/src/main/com/mongodb/internal/operation/CommandBatchCursor.java index d201976e5ed..a061abafbe9 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/CommandBatchCursor.java +++ b/driver-core/src/main/com/mongodb/internal/operation/CommandBatchCursor.java @@ -75,6 +75,7 @@ class CommandBatchCursor implements AggregateResponseBatchCursor { @Nullable private List nextBatch; private boolean resetTimeoutWhenClosing; + private final long cursorId; CommandBatchCursor( final TimeoutMode timeoutMode, @@ -95,10 +96,13 @@ class CommandBatchCursor implements AggregateResponseBatchCursor { operationContext = connectionSource.getOperationContext(); this.timeoutMode = timeoutMode; + ServerCursor serverCursor = commandCursorResult.getServerCursor(); + this.cursorId = serverCursor != null ? serverCursor.getId() : -1; + operationContext.getTimeoutContext().setMaxTimeOverride(maxTimeMS); Connection connectionToPin = connectionSource.getServerDescription().getType() == ServerType.LOAD_BALANCER ? connection : null; - resourceManager = new ResourceManager(namespace, connectionSource, connectionToPin, commandCursorResult.getServerCursor()); + resourceManager = new ResourceManager(namespace, connectionSource, connectionToPin, serverCursor); resetTimeoutWhenClosing = true; } @@ -169,6 +173,7 @@ public void remove() { @Override public void close() { + operationContext.getTracingManager().removeCursorParentContext(cursorId); resourceManager.close(); } 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 ab37613db13..9d21f41d876 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java @@ -30,6 +30,8 @@ import com.mongodb.internal.binding.AsyncReadBinding; import com.mongodb.internal.binding.ReadBinding; import com.mongodb.internal.connection.OperationContext; +import com.mongodb.internal.tracing.Span; +import com.mongodb.internal.tracing.TracingManager; import com.mongodb.lang.Nullable; import org.bson.BsonBoolean; import org.bson.BsonDocument; @@ -296,13 +298,18 @@ public BatchCursor execute(final ReadBinding binding) { if (invalidTimeoutModeException != null) { throw invalidTimeoutModeException; } + OperationContext operationContext = binding.getOperationContext(); - RetryState retryState = initialRetryState(retryReads, binding.getOperationContext().getTimeoutContext()); - Supplier> read = decorateReadWithRetries(retryState, binding.getOperationContext(), () -> + // Adds a Tracing Span for 'find' operation + TracingManager tracingManager = operationContext.getTracingManager(); + Span tracingSpan = tracingManager.addSpan("find", operationContext.getId()); + + RetryState retryState = initialRetryState(retryReads, operationContext.getTimeoutContext()); + Supplier> read = decorateReadWithRetries(retryState, operationContext, () -> withSourceAndConnection(binding::getReadConnectionSource, false, (source, connection) -> { - retryState.breakAndThrowIfRetryAnd(() -> !canRetryRead(source.getServerDescription(), binding.getOperationContext())); + retryState.breakAndThrowIfRetryAnd(() -> !canRetryRead(source.getServerDescription(), operationContext)); try { - return createReadCommandAndExecute(retryState, binding.getOperationContext(), source, namespace.getDatabaseName(), + return createReadCommandAndExecute(retryState, operationContext, source, namespace.getDatabaseName(), getCommandCreator(), CommandResultDocumentCodec.create(decoder, FIRST_BATCH), transformer(), connection); } catch (MongoCommandException e) { @@ -310,7 +317,16 @@ public BatchCursor execute(final ReadBinding binding) { } }) ); - return read.get(); + try { + return read.get(); + } catch (MongoQueryException e) { + tracingSpan.error(e); + throw e; + } finally { + tracingSpan.end(); + // Clean up the tracing span after the operation is complete + tracingManager.cleanContexts(operationContext.getId()); + } } @Override @@ -473,9 +489,17 @@ 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(); + + // register cursor id with the operation context, so 'getMore' commands can be folded under the 'find' operation + long cursorId = result.getDocument("cursor").getInt64("id").longValue(); + TracingManager tracingManager = operationContext.getTracingManager(); + tracingManager.addCursorParentContext(cursorId, operationContext.getId()); + + 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/session/ServerSessionPool.java b/driver-core/src/main/com/mongodb/internal/session/ServerSessionPool.java index 9111eaed3a9..00da3adb822 100644 --- a/driver-core/src/main/com/mongodb/internal/session/ServerSessionPool.java +++ b/driver-core/src/main/com/mongodb/internal/session/ServerSessionPool.java @@ -29,6 +29,7 @@ import com.mongodb.internal.connection.NoOpSessionContext; import com.mongodb.internal.connection.OperationContext; import com.mongodb.internal.selector.ReadPreferenceServerSelector; +import com.mongodb.internal.tracing.TracingManager; import com.mongodb.internal.validator.NoOpFieldNameValidator; import com.mongodb.lang.Nullable; import com.mongodb.selector.ServerSelector; @@ -70,7 +71,7 @@ interface Clock { public ServerSessionPool(final Cluster cluster, final TimeoutSettings timeoutSettings, @Nullable final ServerApi serverApi) { this(cluster, new OperationContext(IgnorableRequestContext.INSTANCE, NoOpSessionContext.INSTANCE, - new TimeoutContext(timeoutSettings.connectionOnly()), serverApi)); + new TimeoutContext(timeoutSettings.connectionOnly()), serverApi, TracingManager.NO_OP)); } public ServerSessionPool(final Cluster cluster, final OperationContext operationContext) { diff --git a/driver-core/src/main/com/mongodb/internal/tracing/Span.java b/driver-core/src/main/com/mongodb/internal/tracing/Span.java new file mode 100644 index 00000000000..10b95bed5af --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/tracing/Span.java @@ -0,0 +1,52 @@ +/* + * 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; + +public interface Span { + Span EMPTY = new Span() { + @Override + public void tag(final String key, final String value) { + } + + @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; + } + }; + + void tag(String key, String value); + + void event(String event); + + void error(Throwable throwable); + + void end(); + + TraceContext context(); +} diff --git a/driver-core/src/main/com/mongodb/internal/tracing/TraceContext.java b/driver-core/src/main/com/mongodb/internal/tracing/TraceContext.java new file mode 100644 index 00000000000..cb2f6ef1020 --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/tracing/TraceContext.java @@ -0,0 +1,23 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.internal.tracing; + +@SuppressWarnings("InterfaceIsType") +public interface TraceContext { + TraceContext EMPTY = new TraceContext() { + }; +} diff --git a/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java b/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java new file mode 100644 index 00000000000..f6bc5d14d81 --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java @@ -0,0 +1,80 @@ +/* + * 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.tracing.Tracer; + +import java.util.HashMap; + +public class TracingManager { + public static final TracingManager NO_OP = new TracingManager(Tracer.NO_OP); + + private final Tracer tracer; + private final TraceContext parentContext; + + // Map a cursor id to its parent context (useful for folding getMore commands under the parent operation) + private final HashMap cursors = new HashMap<>(); + + // Map an operation's span context so the subsequent commands spans can fold under the parent operation + private final HashMap operationContexts = new HashMap<>(); + + public TracingManager(final Tracer tracer) { + this.tracer = tracer; + this.parentContext = tracer.currentContext(); + } + + public TracingManager(final Tracer tracer, final TraceContext parentContext) { + this.tracer = tracer; + this.parentContext = parentContext; + } + + public Span addSpan(final String name, final Long operationId) { + Span span = tracer.nextSpan(name); + operationContexts.put(operationId, span.context()); + return span; + } + + public Span addSpan(final String name, final TraceContext parentContext) { + return tracer.nextSpan(name, parentContext); + } + + public void cleanContexts(final Long operationId) { + operationContexts.remove(operationId); + } + + public TraceContext getParentContext(final Long operationId) { + assert operationContexts.containsKey(operationId); + return operationContexts.get(operationId); + } + + public void addCursorParentContext(final long cursorId, final long operationId) { + assert operationContexts.containsKey(operationId); + cursors.put(cursorId, operationContexts.get(operationId)); + } + + public TraceContext getCursorParentContext(final long cursorId) { + return cursors.get(cursorId); + } + + public void removeCursorParentContext(final long cursorId) { + cursors.remove(cursorId); + } + + public boolean isEnabled() { + return tracer.enabled(); + } +} 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..6b1f711c20b --- /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 sessions + */ +@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..b34fa2247b4 --- /dev/null +++ b/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java @@ -0,0 +1,102 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.tracing; + +import com.mongodb.internal.tracing.Span; +import com.mongodb.internal.tracing.TraceContext; + +public class MicrometerTracer implements Tracer { + private final io.micrometer.tracing.Tracer tracer; + + public MicrometerTracer(final io.micrometer.tracing.Tracer tracer) { + this.tracer = tracer; + } + + @Override + public TraceContext currentContext() { + return new MicrometerTraceContext(tracer.currentTraceContext().context()); + } + + @Override + public Span nextSpan(final String name) { + return new MicrometerSpan(tracer.nextSpan().name(name).start()); + } + + @Override + public Span nextSpan(final String name, final TraceContext parent) { + if (parent != null) { + io.micrometer.tracing.Span span = tracer.spanBuilder() + .name(name) + .setParent(((MicrometerTraceContext) parent).getTraceContext()) + .start(); + return new MicrometerSpan(span); + } else { + return nextSpan(name); + } + } + + @Override + public boolean enabled() { + return true; + } + + private static class MicrometerTraceContext implements TraceContext { + private final io.micrometer.tracing.TraceContext traceContext; + + MicrometerTraceContext(final io.micrometer.tracing.TraceContext traceContext) { + this.traceContext = traceContext; + } + + public io.micrometer.tracing.TraceContext getTraceContext() { + return traceContext; + } + } + + private static class MicrometerSpan implements Span { + private final io.micrometer.tracing.Span span; + + MicrometerSpan(final io.micrometer.tracing.Span span) { + this.span = span; + } + + @Override + public void tag(final String key, final String value) { + span.tag(key, value); + } + + // TODO add variant with TimeUnit + @Override + public void event(final String event) { + span.event(event); + } + + @Override + public void error(final Throwable throwable) { + span.error(throwable); + } + + @Override + public void end() { + span.end(); + } + + @Override + public TraceContext context() { + return new MicrometerTraceContext(span.context()); + } + } +} 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..14d7093d4cb --- /dev/null +++ b/driver-core/src/main/com/mongodb/tracing/Tracer.java @@ -0,0 +1,53 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.tracing; + +import com.mongodb.internal.tracing.Span; +import com.mongodb.internal.tracing.TraceContext; + +public interface Tracer { + Tracer NO_OP = new Tracer() { + + @Override + public TraceContext currentContext() { + return TraceContext.EMPTY; + } + + @Override + public Span nextSpan(final String name) { + return Span.EMPTY; + } + + @Override + public Span nextSpan(final String name, final TraceContext parent) { + return Span.EMPTY; + } + + @Override + public boolean enabled() { + return false; + } + }; + + TraceContext currentContext(); + + Span nextSpan(String name); // uses current active span + + Span nextSpan(String name, TraceContext parent); // manually attach the next span to the provided parent + + boolean enabled(); +} 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..2ec7551d300 --- /dev/null +++ b/driver-core/src/main/com/mongodb/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 sessions + */ +@NonNullApi +package com.mongodb.tracing; + +import com.mongodb.lang.NonNullApi; diff --git a/driver-core/src/test/functional/com/mongodb/ClusterFixture.java b/driver-core/src/test/functional/com/mongodb/ClusterFixture.java index 30792bf0487..d2916206a7d 100644 --- a/driver-core/src/test/functional/com/mongodb/ClusterFixture.java +++ b/driver-core/src/test/functional/com/mongodb/ClusterFixture.java @@ -71,6 +71,7 @@ import com.mongodb.internal.operation.DropDatabaseOperation; import com.mongodb.internal.operation.ReadOperation; import com.mongodb.internal.operation.WriteOperation; +import com.mongodb.internal.tracing.TracingManager; import com.mongodb.lang.Nullable; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; diff --git a/driver-core/src/test/resources/specifications b/driver-core/src/test/resources/specifications index c13d23b91b4..61270d61656 160000 --- a/driver-core/src/test/resources/specifications +++ b/driver-core/src/test/resources/specifications @@ -1 +1 @@ -Subproject commit c13d23b91b422b348c54195fe1c49406fc457559 +Subproject commit 61270d61656709944e6b75e160453e3bfa658483 diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/CommandHelperTest.java b/driver-core/src/test/unit/com/mongodb/internal/connection/CommandHelperTest.java index f7873379c3b..f40c7d17f20 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/connection/CommandHelperTest.java +++ b/driver-core/src/test/unit/com/mongodb/internal/connection/CommandHelperTest.java @@ -26,6 +26,7 @@ import com.mongodb.internal.IgnorableRequestContext; import com.mongodb.internal.TimeoutContext; import com.mongodb.internal.TimeoutSettings; +import com.mongodb.internal.tracing.TracingManager; import org.bson.BsonDocument; import org.bson.codecs.Decoder; import org.junit.jupiter.api.Test; @@ -118,9 +119,8 @@ void testIsCommandOk() { assertFalse(CommandHelper.isCommandOk(new BsonDocument())); } - OperationContext createOperationContext() { return new OperationContext(IgnorableRequestContext.INSTANCE, NoOpSessionContext.INSTANCE, - new TimeoutContext(TimeoutSettings.DEFAULT), ServerApi.builder().version(ServerApiVersion.V1).build()); + new TimeoutContext(TimeoutSettings.DEFAULT), ServerApi.builder().version(ServerApiVersion.V1).build(), TracingManager.NO_OP); } } diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/CommandMessageTest.java b/driver-core/src/test/unit/com/mongodb/internal/connection/CommandMessageTest.java index 533e74f0d23..3ceb7567eef 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/connection/CommandMessageTest.java +++ b/driver-core/src/test/unit/com/mongodb/internal/connection/CommandMessageTest.java @@ -32,6 +32,7 @@ import com.mongodb.internal.operation.ClientBulkWriteOperation; import com.mongodb.internal.operation.ClientBulkWriteOperation.ClientBulkWriteCommand.OpsAndNsInfo; import com.mongodb.internal.session.SessionContext; +import com.mongodb.internal.tracing.TracingManager; import com.mongodb.internal.validator.NoOpFieldNameValidator; import org.bson.BsonArray; import org.bson.BsonBoolean; @@ -163,7 +164,7 @@ void getCommandDocumentFromClientBulkWrite() { output, new OperationContext( IgnorableRequestContext.INSTANCE, NoOpSessionContext.INSTANCE, - new TimeoutContext(TimeoutSettings.DEFAULT), null)); + new TimeoutContext(TimeoutSettings.DEFAULT), null, TracingManager.NO_OP)); BsonDocument actualCommandDocument = commandMessage.getCommandDocument(output); assertEquals(expectedCommandDocument, actualCommandDocument); } diff --git a/driver-legacy/src/main/com/mongodb/MongoClient.java b/driver-legacy/src/main/com/mongodb/MongoClient.java index 09d58e1b493..9478dc76177 100644 --- a/driver-legacy/src/main/com/mongodb/MongoClient.java +++ b/driver-legacy/src/main/com/mongodb/MongoClient.java @@ -43,6 +43,7 @@ import com.mongodb.internal.diagnostics.logging.Loggers; import com.mongodb.internal.session.ServerSessionPool; import com.mongodb.internal.thread.DaemonThreadFactory; +import com.mongodb.internal.tracing.TracingManager; import com.mongodb.internal.validator.NoOpFieldNameValidator; import com.mongodb.lang.Nullable; import org.bson.BsonArray; 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 dacf0c9b82e..1689c9b31e9 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.AsyncReadOperation; import com.mongodb.internal.operation.AsyncWriteOperation; import com.mongodb.internal.operation.OperationHelper; +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), + mongoClient.getSettings().getTracingManager(), mongoClient.getSettings().getServerApi(), commandName); } 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..acb19fe12d9 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/MongoClientImpl.java +++ b/driver-sync/src/main/com/mongodb/client/internal/MongoClientImpl.java @@ -106,7 +106,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(), settings.getTracingManager()); 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 0430d9407c1..5944edae4e2 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java +++ b/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java @@ -57,6 +57,7 @@ import com.mongodb.internal.operation.SyncOperations; import com.mongodb.internal.operation.WriteOperation; 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; @@ -99,6 +100,7 @@ final class MongoClusterImpl implements MongoCluster { private final UuidRepresentation uuidRepresentation; private final WriteConcern writeConcern; private final SyncOperations operations; + private final TracingManager tracingManager; MongoClusterImpl( @Nullable final AutoEncryptionSettings autoEncryptionSettings, final Cluster cluster, final CodecRegistry codecRegistry, @@ -106,7 +108,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 +126,8 @@ final class MongoClusterImpl implements MongoCluster { this.timeoutSettings = timeoutSettings; this.uuidRepresentation = uuidRepresentation; this.writeConcern = writeConcern; + this.tracingManager = tracingManager; + operations = new SyncOperations<>( null, BsonDocument.class, @@ -165,35 +170,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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8b8222d66e5..90aec4c1c38 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,7 @@ reactive-streams = "1.0.4" snappy = "1.1.10.3" zstd = "1.5.5-3" jetbrains-annotations = "26.0.2" +micrometer = "1.4.5" kotlin = "1.8.10" kotlinx-coroutines-bom = "1.6.4" @@ -93,6 +94,7 @@ reactive-streams = { module = " org.reactivestreams:reactive-streams", version.r slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } snappy-java = { module = "org.xerial.snappy:snappy-java", version.ref = "snappy" } zstd-jni = { module = "com.github.luben:zstd-jni", version.ref = "zstd" } +micrometer = { module = "io.micrometer:micrometer-tracing", version.ref = "micrometer" } graal-sdk = { module = "org.graalvm.sdk:graal-sdk", version.ref = "graal-sdk" } graal-sdk-nativeimage = { module = "org.graalvm.sdk:nativeimage", version.ref = "graal-sdk" } From 4a16c29f00fe1c2a251fea0712243a05a9d9ebe3 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 4 Aug 2025 00:29:29 +0100 Subject: [PATCH 02/14] Simplifying bookkeeping logic for Span grouping --- .../main/com/mongodb/MongoClientSettings.java | 28 +- .../internal/connection/CommandMessage.java | 4 - .../connection/InternalStreamConnection.java | 186 +++++++---- .../connection/LoggingCommandEventSender.java | 4 +- .../internal/connection/OperationContext.java | 27 +- .../operation/CommandBatchCursor.java | 7 +- .../internal/operation/FindOperation.java | 31 +- .../internal/session/ServerSessionPool.java | 3 +- .../com/mongodb/internal/tracing/Span.java | 69 +++- .../com/mongodb/internal/tracing/Tags.java | 42 +++ .../com/mongodb/internal/tracing/Tracer.java | 97 ++++++ .../internal/tracing/TracingManager.java | 106 ++++--- .../internal/tracing/TransactionSpan.java | 110 +++++++ .../internal/tracing/package-info.java | 2 +- .../com/mongodb/tracing/MicrometerTracer.java | 88 +++++- .../src/main/com/mongodb/tracing/Tracer.java | 53 ---- .../com/mongodb/tracing/package-info.java | 4 +- .../com/mongodb/ClusterFixture.java | 1 - .../connection/CommandHelperTest.java | 3 +- .../connection/CommandMessageTest.java | 3 +- .../syncadapter/SyncClientSession.kt | 3 + .../client/syncadapter/SyncClientSession.kt | 3 + .../src/main/com/mongodb/MongoClient.java | 1 - .../internal/OperationExecutorImpl.java | 2 +- .../client/syncadapter/SyncClientSession.java | 7 + .../scala/syncadapter/SyncClientSession.scala | 3 + driver-sync/build.gradle.kts | 3 + .../com/mongodb/client/ClientSession.java | 10 + .../client/internal/ClientSessionImpl.java | 114 ++++--- .../client/internal/MongoClientImpl.java | 3 +- .../client/internal/MongoClusterImpl.java | 53 +++- .../com/mongodb/client/tracing/SpanTree.java | 295 ++++++++++++++++++ .../mongodb/client/tracing/ZipkinTracer.java | 95 ++++++ gradle/libs.versions.toml | 11 + 34 files changed, 1195 insertions(+), 276 deletions(-) create mode 100644 driver-core/src/main/com/mongodb/internal/tracing/Tags.java create mode 100644 driver-core/src/main/com/mongodb/internal/tracing/Tracer.java create mode 100644 driver-core/src/main/com/mongodb/internal/tracing/TransactionSpan.java delete mode 100644 driver-core/src/main/com/mongodb/tracing/Tracer.java create mode 100644 driver-sync/src/test/functional/com/mongodb/client/tracing/SpanTree.java create mode 100644 driver-sync/src/test/functional/com/mongodb/client/tracing/ZipkinTracer.java diff --git a/driver-core/src/main/com/mongodb/MongoClientSettings.java b/driver-core/src/main/com/mongodb/MongoClientSettings.java index 642682d1b3a..938b9c76068 100644 --- a/driver-core/src/main/com/mongodb/MongoClientSettings.java +++ b/driver-core/src/main/com/mongodb/MongoClientSettings.java @@ -30,11 +30,10 @@ import com.mongodb.connection.SslSettings; import com.mongodb.connection.TransportSettings; import com.mongodb.event.CommandListener; -import com.mongodb.internal.tracing.TracingManager; +import com.mongodb.internal.tracing.Tracer; import com.mongodb.lang.Nullable; import com.mongodb.spi.dns.DnsClient; import com.mongodb.spi.dns.InetAddressResolver; -import com.mongodb.tracing.Tracer; import org.bson.UuidRepresentation; import org.bson.codecs.BsonCodecProvider; import org.bson.codecs.BsonValueCodecProvider; @@ -120,7 +119,7 @@ public final class MongoClientSettings { private final InetAddressResolver inetAddressResolver; @Nullable private final Long timeoutMS; - private final TracingManager tracingManager; + private final Tracer tracer; /** * Gets the default codec registry. It includes the following providers: @@ -241,7 +240,7 @@ public static final class Builder { private ContextProvider contextProvider; private DnsClient dnsClient; private InetAddressResolver inetAddressResolver; - private TracingManager tracingManager; + private Tracer tracer; private Builder() { } @@ -279,7 +278,7 @@ private Builder(final MongoClientSettings settings) { if (settings.heartbeatSocketTimeoutSetExplicitly) { heartbeatSocketTimeoutMS = settings.heartbeatSocketSettings.getReadTimeout(MILLISECONDS); } - tracingManager = settings.tracingManager; + tracer = settings.tracer; } /** @@ -729,16 +728,16 @@ Builder heartbeatSocketTimeoutMS(final int heartbeatSocketTimeoutMS) { } /** - * Sets the tracer to use for creating Spans for operations and commands. + * 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.5 + * @since 5.6 */ @Alpha(Reason.CLIENT) public Builder tracer(final Tracer tracer) { - this.tracingManager = new TracingManager(tracer); + this.tracer = tracer; return this; } @@ -1060,14 +1059,13 @@ public ContextProvider getContextProvider() { } /** - * Get the tracer to create Spans for operations and commands. + * Get the tracer to create Spans for operations, commands and transactions. * - * @return this - * @since 5.5 + * @return the configured Tracer + * @since 5.6 */ - @Alpha(Reason.CLIENT) - public TracingManager getTracingManager() { - return tracingManager; + public Tracer getTracer() { + return tracer; } @Override @@ -1186,6 +1184,6 @@ private MongoClientSettings(final Builder builder) { heartbeatConnectTimeoutSetExplicitly = builder.heartbeatConnectTimeoutMS != 0; contextProvider = builder.contextProvider; timeoutMS = builder.timeoutMS; - tracingManager = builder.tracingManager; + tracer = (builder.tracer == null) ? Tracer.NO_OP : builder.tracer; } } diff --git a/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java b/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java index 0660938e4f2..12543e92ccb 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java +++ b/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java @@ -186,10 +186,6 @@ BsonDocument getCommandDocument(final ByteBufferBsonOutput bsonOutput) { } } - BsonDocument getCommand() { - return command; - } - /** * Get the field name from a buffer positioned at the start of the document sequence identifier of an OP_MSG Section of type * `PAYLOAD_TYPE_1_DOCUMENT_SEQUENCE`. 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 e477239e6dc..bd3008cf927 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java +++ b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java @@ -52,7 +52,6 @@ import com.mongodb.internal.session.SessionContext; import com.mongodb.internal.time.Timeout; import com.mongodb.internal.tracing.Span; -import com.mongodb.internal.tracing.TraceContext; import com.mongodb.internal.tracing.TracingManager; import com.mongodb.lang.Nullable; import org.bson.BsonBinaryReader; @@ -97,6 +96,18 @@ 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.internal.tracing.Tags.CLIENT_CONNECTION_ID; +import static com.mongodb.internal.tracing.Tags.CURSOR_ID; +import static com.mongodb.internal.tracing.Tags.NAMESPACE; +import static com.mongodb.internal.tracing.Tags.QUERY_SUMMARY; +import static com.mongodb.internal.tracing.Tags.QUERY_TEXT; +import static com.mongodb.internal.tracing.Tags.SERVER_ADDRESS; +import static com.mongodb.internal.tracing.Tags.SERVER_CONNECTION_ID; +import static com.mongodb.internal.tracing.Tags.SERVER_PORT; +import static com.mongodb.internal.tracing.Tags.SERVER_TYPE; +import static com.mongodb.internal.tracing.Tags.SESSION_ID; +import static com.mongodb.internal.tracing.Tags.SYSTEM; +import static com.mongodb.internal.tracing.Tags.TRANSACTION_NUMBER; import static java.util.Arrays.asList; /** @@ -377,24 +388,13 @@ public boolean isClosed() { public T sendAndReceive(final CommandMessage message, final Decoder decoder, final OperationContext operationContext) { Supplier sendAndReceiveInternal = () -> sendAndReceiveInternal( message, decoder, operationContext); - - Span tracingSpan = createTracingSpan(message, operationContext); - try { return sendAndReceiveInternal.get(); - } catch (MongoCommandException e) { - if (tracingSpan != null) { - tracingSpan.error(e); - } - + } catch (Throwable e) { if (reauthenticationIsTriggered(e)) { return reauthenticateAndRetry(sendAndReceiveInternal, operationContext); } throw e; - } finally { - if (tracingSpan != null) { - tracingSpan.end(); - } } } @@ -406,9 +406,7 @@ public void sendAndReceiveAsync(final CommandMessage message, final Decoder< AsyncSupplier sendAndReceiveAsyncInternal = c -> sendAndReceiveAsyncInternal( message, decoder, operationContext, c); - beginAsync().thenSupply(c -> { - sendAndReceiveAsyncInternal.getAsync(c); - }).onErrorIf(e -> reauthenticationIsTriggered(e), (t, c) -> { + beginAsync().thenSupply(sendAndReceiveAsyncInternal::getAsync).onErrorIf(this::reauthenticationIsTriggered, (t, c) -> { reauthenticateAndRetryAsync(sendAndReceiveAsyncInternal, operationContext, c); }).finish(callback); } @@ -447,15 +445,44 @@ public boolean reauthenticationIsTriggered(@Nullable final Throwable t) { private T sendAndReceiveInternal(final CommandMessage message, final Decoder decoder, final OperationContext operationContext) { CommandEventSender commandEventSender; + try (ByteBufferBsonOutput bsonOutput = new ByteBufferBsonOutput(this)) { message.encode(bsonOutput, operationContext); - commandEventSender = createCommandEventSender(message, bsonOutput, operationContext); - commandEventSender.sendStartedEvent(); + Span 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.tag(QUERY_TEXT, commandDocument.toJson()); + } + try { sendCommandMessage(message, bsonOutput, operationContext); } catch (Exception e) { + if (tracingSpan != null) { + tracingSpan.error(e); + } commandEventSender.sendFailedEvent(e); throw e; + } finally { + if (tracingSpan != null) { + tracingSpan.end(); + } } } @@ -568,7 +595,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())) { @@ -887,42 +925,6 @@ public ByteBuf getBuffer(final int size) { return stream.getBuffer(size); } - @Nullable - private Span createTracingSpan(final CommandMessage message, final OperationContext operationContext) { - TracingManager tracingManager = operationContext.getTracingManager(); - Span span; - if (tracingManager.isEnabled()) { - BsonDocument command = message.getCommand(); - TraceContext parentContext = null; - long cursorId = -1; - if (command.containsKey("getMore")) { - cursorId = command.getInt64("getMore").longValue(); - parentContext = tracingManager.getCursorParentContext(cursorId); - } else { - parentContext = tracingManager.getParentContext(operationContext.getId()); - } - - span = tracingManager.addSpan("Command " + command.getFirstKey(), parentContext); - span.tag("db.system", "mongodb"); - span.tag("db.namespace", message.getNamespace().getFullName()); - span.tag("db.query.summary", command.getFirstKey()); - span.tag("db.query.opcode", String.valueOf(message.getOpCode())); - span.tag("db.query.text", command.toString()); - if (cursorId != -1) { - span.tag("db.mongodb.cursor_id", String.valueOf(cursorId)); - } - span.tag("server.address", serverId.getAddress().getHost()); - span.tag("server.port", String.valueOf(serverId.getAddress().getPort())); - span.tag("server.type", message.getSettings().getServerType().name()); - - span.tag("db.mongodb.server_connection_id", this.description.getConnectionId().toString()); - } else { - span = null; - } - - return span; - } - private class MessageHeaderCallback implements SingleResultCallback { private final OperationContext operationContext; private final SingleResultCallback callback; @@ -1003,19 +1005,75 @@ 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); + +// BsonDocument command = message.getCommand(); + String commandName = command.getFirstKey(); +// Span newSpan = tracingManager.addSpan("_____Command_____[ " + commandName + " ]", myparentContext); + if (!tracingManager.isEnabled() + || SECURITY_SENSITIVE_COMMANDS.contains(commandName) + || SECURITY_SENSITIVE_HELLO_COMMANDS.contains(commandName)) { + return null; + } + + Span span = tracingManager + .addSpan("Command " + commandName, operationContext.getTracingSpanContext()) + .tag(SYSTEM, "mongodb") + .tag(NAMESPACE, message.getNamespace().getDatabaseName()) + .tag(QUERY_SUMMARY, commandName); + + if (command.containsKey("getMore")) { + span.tag(CURSOR_ID, command.getInt64("getMore").longValue()); + } + + tagServerAndConnectionInfo(span, message); + tagSessionAndTransactionInfo(span, operationContext); + + return span; + } + + private void tagServerAndConnectionInfo(final Span span, final CommandMessage message) { + span.tag(SERVER_ADDRESS, serverId.getAddress().getHost()) + .tag(SERVER_PORT, String.valueOf(serverId.getAddress().getPort())) + .tag(SERVER_TYPE, message.getSettings().getServerType().name()) + .tag(CLIENT_CONNECTION_ID, this.description.getConnectionId().toString()) + .tag(SERVER_CONNECTION_ID, String.valueOf(this.description.getConnectionId().getServerValue())); + } + + private void tagSessionAndTransactionInfo(final Span span, final OperationContext operationContext) { + SessionContext sessionContext = operationContext.getSessionContext(); + if (sessionContext.hasSession() && !sessionContext.isImplicitSession()) { + span.tag(TRANSACTION_NUMBER, String.valueOf(sessionContext.getTransactionNumber())) + .tag(SESSION_ID, String.valueOf(sessionContext.getSessionId() + .get(sessionContext.getSessionId().getFirstKey()) + .asBinary().asUuid())); + } + } } diff --git a/driver-core/src/main/com/mongodb/internal/connection/LoggingCommandEventSender.java b/driver-core/src/main/com/mongodb/internal/connection/LoggingCommandEventSender.java index 3821ca947c6..136ec10a1dd 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/LoggingCommandEventSender.java +++ b/driver-core/src/main/com/mongodb/internal/connection/LoggingCommandEventSender.java @@ -78,7 +78,7 @@ class LoggingCommandEventSender implements CommandEventSender { @Nullable final CommandListener commandListener, final OperationContext operationContext, final CommandMessage message, - final ByteBufferBsonOutput bsonOutput, + final BsonDocument commandDocument, final StructuredLogger logger, final LoggerSettings loggerSettings) { this.description = description; @@ -88,7 +88,7 @@ class LoggingCommandEventSender implements CommandEventSender { this.loggerSettings = loggerSettings; this.startTimeNanos = System.nanoTime(); this.message = message; - this.commandDocument = message.getCommandDocument(bsonOutput); + this.commandDocument = commandDocument; this.commandName = commandDocument.getFirstKey(); this.redactionRequired = securitySensitiveCommands.contains(commandName) || (securitySensitiveHelloCommands.contains(commandName) && commandDocument.containsKey("speculativeAuthenticate")); diff --git a/driver-core/src/main/com/mongodb/internal/connection/OperationContext.java b/driver-core/src/main/com/mongodb/internal/connection/OperationContext.java index c74f4116a3f..9f1c232b274 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,7 @@ import com.mongodb.internal.TimeoutSettings; import com.mongodb.internal.VisibleForTesting; import com.mongodb.internal.session.SessionContext; +import com.mongodb.internal.tracing.TraceContext; import com.mongodb.internal.tracing.TracingManager; import com.mongodb.lang.Nullable; import com.mongodb.selector.ServerSelector; @@ -53,6 +54,8 @@ public class OperationContext { private final ServerApi serverApi; @Nullable private final String operationName; + @Nullable + private TraceContext tracingContext; public OperationContext(final RequestContext requestContext, final SessionContext sessionContext, final TimeoutContext timeoutContext, @Nullable final ServerApi serverApi) { @@ -66,7 +69,8 @@ public OperationContext(final RequestContext requestContext, final SessionContex this(NEXT_ID.incrementAndGet(), requestContext, sessionContext, timeoutContext, new ServerDeprioritization(), tracingManager, serverApi, - operationName); + operationName, + null); } public static OperationContext simpleOperationContext( @@ -93,17 +97,17 @@ public static OperationContext simpleOperationContext(final TimeoutContext timeo public OperationContext withSessionContext(final SessionContext sessionContext) { return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, tracingManager, serverApi, - operationName); + operationName, tracingContext); } public OperationContext withTimeoutContext(final TimeoutContext timeoutContext) { return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, tracingManager, serverApi, - operationName); + operationName, tracingContext); } public OperationContext withOperationName(final String operationName) { return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, tracingManager, serverApi, - operationName); + operationName, tracingContext); } public long getId() { @@ -136,6 +140,15 @@ public String getOperationName() { return operationName; } + @Nullable + public TraceContext getTracingSpanContext() { + return tracingContext != null ? tracingContext : null; + } + + public void setTracingContext(final TraceContext tracingContext) { + this.tracingContext = tracingContext; + } + @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) public OperationContext(final long id, final RequestContext requestContext, @@ -144,7 +157,9 @@ public OperationContext(final long id, final ServerDeprioritization serverDeprioritization, final TracingManager tracingManager, @Nullable final ServerApi serverApi, - @Nullable final String operationName) { + @Nullable final String operationName, + @Nullable final TraceContext tracingContext) { + this.id = id; this.serverDeprioritization = serverDeprioritization; this.requestContext = requestContext; @@ -153,6 +168,7 @@ public OperationContext(final long id, this.tracingManager = tracingManager; this.serverApi = serverApi; this.operationName = operationName; + this.tracingContext = tracingContext; } @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) @@ -171,6 +187,7 @@ public OperationContext(final long id, this.tracingManager = tracingManager; this.serverApi = serverApi; this.operationName = operationName; + this.tracingContext = null; } diff --git a/driver-core/src/main/com/mongodb/internal/operation/CommandBatchCursor.java b/driver-core/src/main/com/mongodb/internal/operation/CommandBatchCursor.java index a061abafbe9..d201976e5ed 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/CommandBatchCursor.java +++ b/driver-core/src/main/com/mongodb/internal/operation/CommandBatchCursor.java @@ -75,7 +75,6 @@ class CommandBatchCursor implements AggregateResponseBatchCursor { @Nullable private List nextBatch; private boolean resetTimeoutWhenClosing; - private final long cursorId; CommandBatchCursor( final TimeoutMode timeoutMode, @@ -96,13 +95,10 @@ class CommandBatchCursor implements AggregateResponseBatchCursor { operationContext = connectionSource.getOperationContext(); this.timeoutMode = timeoutMode; - ServerCursor serverCursor = commandCursorResult.getServerCursor(); - this.cursorId = serverCursor != null ? serverCursor.getId() : -1; - operationContext.getTimeoutContext().setMaxTimeOverride(maxTimeMS); Connection connectionToPin = connectionSource.getServerDescription().getType() == ServerType.LOAD_BALANCER ? connection : null; - resourceManager = new ResourceManager(namespace, connectionSource, connectionToPin, serverCursor); + resourceManager = new ResourceManager(namespace, connectionSource, connectionToPin, commandCursorResult.getServerCursor()); resetTimeoutWhenClosing = true; } @@ -173,7 +169,6 @@ public void remove() { @Override public void close() { - operationContext.getTracingManager().removeCursorParentContext(cursorId); resourceManager.close(); } 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 9d21f41d876..84039a03296 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java @@ -30,8 +30,6 @@ import com.mongodb.internal.binding.AsyncReadBinding; import com.mongodb.internal.binding.ReadBinding; import com.mongodb.internal.connection.OperationContext; -import com.mongodb.internal.tracing.Span; -import com.mongodb.internal.tracing.TracingManager; import com.mongodb.lang.Nullable; import org.bson.BsonBoolean; import org.bson.BsonDocument; @@ -298,18 +296,13 @@ public BatchCursor execute(final ReadBinding binding) { if (invalidTimeoutModeException != null) { throw invalidTimeoutModeException; } - OperationContext operationContext = binding.getOperationContext(); - // Adds a Tracing Span for 'find' operation - TracingManager tracingManager = operationContext.getTracingManager(); - Span tracingSpan = tracingManager.addSpan("find", operationContext.getId()); - - RetryState retryState = initialRetryState(retryReads, operationContext.getTimeoutContext()); - Supplier> read = decorateReadWithRetries(retryState, operationContext, () -> + RetryState retryState = initialRetryState(retryReads, binding.getOperationContext().getTimeoutContext()); + Supplier> read = decorateReadWithRetries(retryState, binding.getOperationContext(), () -> withSourceAndConnection(binding::getReadConnectionSource, false, (source, connection) -> { - retryState.breakAndThrowIfRetryAnd(() -> !canRetryRead(source.getServerDescription(), operationContext)); + retryState.breakAndThrowIfRetryAnd(() -> !canRetryRead(source.getServerDescription(), binding.getOperationContext())); try { - return createReadCommandAndExecute(retryState, operationContext, source, namespace.getDatabaseName(), + return createReadCommandAndExecute(retryState, binding.getOperationContext(), source, namespace.getDatabaseName(), getCommandCreator(), CommandResultDocumentCodec.create(decoder, FIRST_BATCH), transformer(), connection); } catch (MongoCommandException e) { @@ -317,16 +310,7 @@ public BatchCursor execute(final ReadBinding binding) { } }) ); - try { - return read.get(); - } catch (MongoQueryException e) { - tracingSpan.error(e); - throw e; - } finally { - tracingSpan.end(); - // Clean up the tracing span after the operation is complete - tracingManager.cleanContexts(operationContext.getId()); - } + return read.get(); } @Override @@ -492,11 +476,6 @@ private CommandReadTransformer> transformer( return (result, source, connection) -> { OperationContext operationContext = source.getOperationContext(); - // register cursor id with the operation context, so 'getMore' commands can be folded under the 'find' operation - long cursorId = result.getDocument("cursor").getInt64("id").longValue(); - TracingManager tracingManager = operationContext.getTracingManager(); - tracingManager.addCursorParentContext(cursorId, operationContext.getId()); - return new CommandBatchCursor<>(getTimeoutMode(), result, batchSize, getMaxTimeForCursor(operationContext), decoder, comment, source, connection); }; diff --git a/driver-core/src/main/com/mongodb/internal/session/ServerSessionPool.java b/driver-core/src/main/com/mongodb/internal/session/ServerSessionPool.java index 00da3adb822..9111eaed3a9 100644 --- a/driver-core/src/main/com/mongodb/internal/session/ServerSessionPool.java +++ b/driver-core/src/main/com/mongodb/internal/session/ServerSessionPool.java @@ -29,7 +29,6 @@ import com.mongodb.internal.connection.NoOpSessionContext; import com.mongodb.internal.connection.OperationContext; import com.mongodb.internal.selector.ReadPreferenceServerSelector; -import com.mongodb.internal.tracing.TracingManager; import com.mongodb.internal.validator.NoOpFieldNameValidator; import com.mongodb.lang.Nullable; import com.mongodb.selector.ServerSelector; @@ -71,7 +70,7 @@ interface Clock { public ServerSessionPool(final Cluster cluster, final TimeoutSettings timeoutSettings, @Nullable final ServerApi serverApi) { this(cluster, new OperationContext(IgnorableRequestContext.INSTANCE, NoOpSessionContext.INSTANCE, - new TimeoutContext(timeoutSettings.connectionOnly()), serverApi, TracingManager.NO_OP)); + new TimeoutContext(timeoutSettings.connectionOnly()), serverApi)); } public ServerSessionPool(final Cluster cluster, final OperationContext operationContext) { diff --git a/driver-core/src/main/com/mongodb/internal/tracing/Span.java b/driver-core/src/main/com/mongodb/internal/tracing/Span.java index 10b95bed5af..41f6ef0afdf 100644 --- a/driver-core/src/main/com/mongodb/internal/tracing/Span.java +++ b/driver-core/src/main/com/mongodb/internal/tracing/Span.java @@ -16,10 +16,41 @@ package com.mongodb.internal.tracing; + +/** + * Represents a tracing span for the driver internal operations. + *

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

+ *

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

    + *
  • Command Spans: Trace the execution of MongoDB commands (e.g., find, insert, update).
  • + *
  • Operation Spans: Trace higher-level operations, which may include multiple commands or internal steps.
  • + *
  • Transaction Spans: Trace the lifecycle of a transaction, including all operations and commands within it.
  • + *
+ *

+ * + * @since 5.6 + */ public interface Span { + /** + * An empty / no-op implementation of the Span interface. + *

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

+ */ Span EMPTY = new Span() { @Override - public void tag(final String key, final String value) { + public Span tag(final String key, final String value) { + return this; + } + + @Override + public Span tag(final String key, final Long value) { + return this; } @Override @@ -40,13 +71,47 @@ public TraceContext context() { } }; - void tag(String key, String value); + /** + * Adds a tag to the span with a key-value pair. + * + * @param key The tag key. + * @param value The tag value. + * @return The current instance of the span. + */ + Span tag(String key, String value); + + /** + * Adds a tag to the span with a key and a numeric value. + * + * @param key The tag key. + * @param value The numeric tag value. + * @return The current instance of the span. + */ + Span tag(String key, Long value); + /** + * Records an event in the span. + * + * @param event The event description. + */ void event(String event); + /** + * Records an error for this span. + * + * @param throwable The error to record. + */ void error(Throwable throwable); + /** + * Ends the span, marking it as complete. + */ void end(); + /** + * Retrieves the context associated with the span. + * + * @return The trace context associated with the span. + */ TraceContext context(); } diff --git a/driver-core/src/main/com/mongodb/internal/tracing/Tags.java b/driver-core/src/main/com/mongodb/internal/tracing/Tags.java new file mode 100644 index 00000000000..e4407f2a401 --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/tracing/Tags.java @@ -0,0 +1,42 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.internal.tracing; + +/** + * Contains constant tag names used for tracing and monitoring MongoDB operations. + * These tags are typically used to annotate spans or events with relevant metadata. + * + * @since 5.6 + */ +public final class Tags { + private Tags() { + } + + public static final String SYSTEM = "db.system"; + public static final String NAMESPACE = "db.namespace"; + public static final String COLLECTION = "db.collection.name"; + public static final String QUERY_SUMMARY = "db.query.summary"; + public static final String QUERY_TEXT = "db.query.text"; + public static final String CURSOR_ID = "db.mongodb.cursor_id"; + public static final String SERVER_ADDRESS = "server.address"; + public static final String SERVER_PORT = "server.port"; + public static final String SERVER_TYPE = "server.type"; + public static final String CLIENT_CONNECTION_ID = "db.mongodb.client_connection_id"; + public static final String SERVER_CONNECTION_ID = "db.mongodb.server_connection_id"; + public static final String TRANSACTION_NUMBER = "db.mongodb.txnNumber"; + public static final String SESSION_ID = "db.mongodb.lsid"; +} diff --git a/driver-core/src/main/com/mongodb/internal/tracing/Tracer.java b/driver-core/src/main/com/mongodb/internal/tracing/Tracer.java new file mode 100644 index 00000000000..c2881e9e2fb --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/tracing/Tracer.java @@ -0,0 +1,97 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.internal.tracing; + +import com.mongodb.lang.Nullable; + +/** + * A Tracer interface that provides methods for tracing commands, operations and transactions. + *

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

+ * + * @since 5.6 + */ +public interface Tracer { + Tracer NO_OP = new Tracer() { + + @Override + public TraceContext currentContext() { + return TraceContext.EMPTY; + } + + @Override + public Span nextSpan(final String name) { + return Span.EMPTY; + } + + @Override + public Span nextSpan(final String name, @Nullable final TraceContext parent) { + return Span.EMPTY; + } + + @Override + public boolean enabled() { + return false; + } + + @Override + public boolean includeCommandPayload() { + return false; + } + }; + + /** + * Retrieves the current trace context from the Micrometer tracer. + * + * @return A {@link TraceContext} representing the underlying {@link io.micrometer.tracing.TraceContext}. + * exists. + */ + TraceContext currentContext(); + + /** + * Creates a new span with the specified name. + * + * @param name The name of the span. + * @return A {@link Span} representing the newly created span. + */ + Span nextSpan(String name); // uses current active span + + /** + * Creates a new span with the specified name and optional parent trace context. + * + * @param name The name of the span. + * @param parent The parent {@link TraceContext}, or null if no parent context is provided. + * @return A {@link Span} representing the newly created span. + */ + Span nextSpan(String name, @Nullable TraceContext parent); // manually attach the next span to the provided parent + + /** + * Indicates whether tracing is enabled. + * + * @return {@code true} if tracing is enabled, {@code false} otherwise. + */ + boolean enabled(); + + /** + * Indicates whether command payloads are included in the trace context. + * + * @return {@code true} if command payloads are allowed, {@code false} otherwise. + */ + boolean includeCommandPayload(); // whether the tracer allows command payloads in the trace context +} diff --git a/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java b/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java index f6bc5d14d81..6db92f0d75a 100644 --- a/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java +++ b/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java @@ -16,65 +16,97 @@ package com.mongodb.internal.tracing; -import com.mongodb.tracing.Tracer; +import com.mongodb.lang.Nullable; -import java.util.HashMap; +import static com.mongodb.internal.tracing.Tags.SYSTEM; +import static java.lang.System.getenv; +/** + * Manages tracing spans for MongoDB driver activities. + *

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

+ */ public class TracingManager { + /** + * A no-op instance of the TracingManager used when tracing is disabled. + */ public static final TracingManager NO_OP = new TracingManager(Tracer.NO_OP); + private static final String ENV_ALLOW_COMMAND_PAYLOAD = "MONGODB_TRACING_ALLOW_COMMAND_PAYLOAD"; private final Tracer tracer; private final TraceContext parentContext; + private final boolean enableCommandPayload; - // Map a cursor id to its parent context (useful for folding getMore commands under the parent operation) - private final HashMap cursors = new HashMap<>(); - - // Map an operation's span context so the subsequent commands spans can fold under the parent operation - private final HashMap operationContexts = new HashMap<>(); - + /** + * Constructs a new TracingManager with the specified tracer. + * + * @param tracer The tracer to use for tracing operations. + */ public TracingManager(final Tracer tracer) { - this.tracer = tracer; - this.parentContext = tracer.currentContext(); + this(tracer, tracer.currentContext()); } + /** + * Constructs a new TracingManager with the specified tracer and parent context. + * Setting the environment variable {@code MONGODB_TRACING_ALLOW_COMMAND_PAYLOAD} to "true" will enable command payload tracing. + * + * @param tracer The tracer to use for tracing operations. + * @param parentContext The parent trace context. + */ public TracingManager(final Tracer tracer, final TraceContext parentContext) { this.tracer = tracer; this.parentContext = parentContext; + String envAllowCommandPayload = getenv(ENV_ALLOW_COMMAND_PAYLOAD); + if (envAllowCommandPayload != null) { + this.enableCommandPayload = Boolean.parseBoolean(envAllowCommandPayload); + } else { + this.enableCommandPayload = tracer.includeCommandPayload(); + } } - public Span addSpan(final String name, final Long operationId) { - Span span = tracer.nextSpan(name); - operationContexts.put(operationId, span.context()); - return span; - } - - public Span addSpan(final String name, final TraceContext parentContext) { + /** + * 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); } - public void cleanContexts(final Long operationId) { - operationContexts.remove(operationId); - } - - public TraceContext getParentContext(final Long operationId) { - assert operationContexts.containsKey(operationId); - return operationContexts.get(operationId); - } - - public void addCursorParentContext(final long cursorId, final long operationId) { - assert operationContexts.containsKey(operationId); - cursors.put(cursorId, operationContexts.get(operationId)); - } - - public TraceContext getCursorParentContext(final long cursorId) { - return cursors.get(cursorId); - } - - public void removeCursorParentContext(final long cursorId) { - cursors.remove(cursorId); + /** + * Creates a new transaction span for the specified server session. + * + * @return The created transaction span. + */ + public Span addTransactionSpan() { + Span span = tracer.nextSpan("transaction", parentContext); + span.tag(SYSTEM, "mongodb"); + return span; } + /** + * Checks whether tracing is enabled. + * + * @return True if tracing is enabled, false otherwise. + */ public boolean isEnabled() { return tracer.enabled(); } + + /** + * Checks whether command payload tracing is enabled. + * + * @return True if command payload tracing is enabled, false otherwise. + */ + public boolean isCommandPayloadEnabled() { + return enableCommandPayload; + } } diff --git a/driver-core/src/main/com/mongodb/internal/tracing/TransactionSpan.java b/driver-core/src/main/com/mongodb/internal/tracing/TransactionSpan.java new file mode 100644 index 00000000000..d975f6931e0 --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/tracing/TransactionSpan.java @@ -0,0 +1,110 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.internal.tracing; + +import com.mongodb.lang.Nullable; + +/** + * State class for transaction tracing. + */ +public class TransactionSpan { + private boolean isConvenientTransaction = false; + private final Span span; + @Nullable + private Throwable reportedError; + + public TransactionSpan(final TracingManager tracingManager) { + this.span = tracingManager.addTransactionSpan(); + } + + /** + * Handles a transaction span error. + *

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

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

+ * This has an impact on how the transaction span is handled. If the transaction is convenient, any errors that occur + * during the transaction are reported as events. If the transaction is not convenient, errors are reported as span + * errors and the transaction context is cleaned up. + */ + public void setIsConvenientTransaction() { + this.isConvenientTransaction = true; + } + + /** + * Retrieves the trace context associated with the transaction span. + * + * @return The trace context associated with the transaction span. + */ + public TraceContext getContext() { + return span.context(); + } +} diff --git a/driver-core/src/main/com/mongodb/internal/tracing/package-info.java b/driver-core/src/main/com/mongodb/internal/tracing/package-info.java index 6b1f711c20b..e7dd3311143 100644 --- a/driver-core/src/main/com/mongodb/internal/tracing/package-info.java +++ b/driver-core/src/main/com/mongodb/internal/tracing/package-info.java @@ -15,7 +15,7 @@ */ /** - * Contains classes related to sessions + * Contains classes related to tracing */ @NonNullApi package com.mongodb.internal.tracing; diff --git a/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java b/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java index b34fa2247b4..ed678b93cab 100644 --- a/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java +++ b/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java @@ -18,12 +18,40 @@ import com.mongodb.internal.tracing.Span; import com.mongodb.internal.tracing.TraceContext; - +import com.mongodb.internal.tracing.Tracer; +import com.mongodb.lang.Nullable; + +/** + * A {@link Tracer} implementation that delegates tracing operations to a Micrometer {@link io.micrometer.tracing.Tracer}. + *

+ * This class enables integration of MongoDB driver tracing with Micrometer-based tracing systems. + * It provides methods to create and manage spans using the Micrometer tracing API. + *

+ * + * @since 5.6 + */ public class MicrometerTracer implements Tracer { private final io.micrometer.tracing.Tracer tracer; + private final boolean allowCommandPayload; + /** + * Constructs a new {@link MicrometerTracer} instance. + * + * @param tracer The Micrometer {@link io.micrometer.tracing.Tracer} to delegate tracing operations to. + */ public MicrometerTracer(final io.micrometer.tracing.Tracer tracer) { + this(tracer, false); + } + + /** + * Constructs a new {@link MicrometerTracer} instance with an option to allow command payloads. + * + * @param tracer The Micrometer {@link io.micrometer.tracing.Tracer} to delegate tracing operations to. + * @param allowCommandPayload Whether to allow command payloads in the trace context. + */ + public MicrometerTracer(final io.micrometer.tracing.Tracer tracer, final boolean allowCommandPayload) { this.tracer = tracer; + this.allowCommandPayload = allowCommandPayload; } @Override @@ -37,16 +65,17 @@ public Span nextSpan(final String name) { } @Override - public Span nextSpan(final String name, final TraceContext parent) { - if (parent != null) { - io.micrometer.tracing.Span span = tracer.spanBuilder() - .name(name) - .setParent(((MicrometerTraceContext) parent).getTraceContext()) - .start(); - return new MicrometerSpan(span); - } else { - return nextSpan(name); + public Span nextSpan(final String name, @Nullable final TraceContext parent) { + if (parent instanceof MicrometerTraceContext) { + io.micrometer.tracing.TraceContext micrometerContext = ((MicrometerTraceContext) parent).getTraceContext(); + if (micrometerContext != null) { + return new MicrometerSpan(tracer.spanBuilder() + .name(name) + .setParent(micrometerContext) + .start()); + } } + return nextSpan(name); } @Override @@ -54,31 +83,64 @@ public boolean enabled() { return true; } + @Override + public boolean includeCommandPayload() { + return allowCommandPayload; + } + + /** + * Represents a Micrometer-based trace context. + */ private static class MicrometerTraceContext implements TraceContext { private final io.micrometer.tracing.TraceContext traceContext; - MicrometerTraceContext(final io.micrometer.tracing.TraceContext traceContext) { + /** + * Constructs a new {@link MicrometerTraceContext} instance. + * + * @param traceContext The Micrometer {@link io.micrometer.tracing.TraceContext}, or null if none exists. + */ + MicrometerTraceContext(@Nullable final io.micrometer.tracing.TraceContext traceContext) { this.traceContext = traceContext; } + /** + * Retrieves the underlying Micrometer trace context. + * + * @return The Micrometer {@link io.micrometer.tracing.TraceContext}, or null if none exists. + */ + @Nullable public io.micrometer.tracing.TraceContext getTraceContext() { return traceContext; } } + /** + * Represents a Micrometer-based span. + */ private static class MicrometerSpan implements Span { private final io.micrometer.tracing.Span span; + /** + * Constructs a new {@link MicrometerSpan} instance. + * + * @param span The Micrometer {@link io.micrometer.tracing.Span} to delegate operations to. + */ MicrometerSpan(final io.micrometer.tracing.Span span) { this.span = span; } @Override - public void tag(final String key, final String value) { + public Span tag(final String key, final String value) { + span.tag(key, value); + return this; + } + + @Override + public Span tag(final String key, final Long value) { span.tag(key, value); + return this; } - // TODO add variant with TimeUnit @Override public void event(final String event) { span.event(event); diff --git a/driver-core/src/main/com/mongodb/tracing/Tracer.java b/driver-core/src/main/com/mongodb/tracing/Tracer.java deleted file mode 100644 index 14d7093d4cb..00000000000 --- a/driver-core/src/main/com/mongodb/tracing/Tracer.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2008-present MongoDB, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.mongodb.tracing; - -import com.mongodb.internal.tracing.Span; -import com.mongodb.internal.tracing.TraceContext; - -public interface Tracer { - Tracer NO_OP = new Tracer() { - - @Override - public TraceContext currentContext() { - return TraceContext.EMPTY; - } - - @Override - public Span nextSpan(final String name) { - return Span.EMPTY; - } - - @Override - public Span nextSpan(final String name, final TraceContext parent) { - return Span.EMPTY; - } - - @Override - public boolean enabled() { - return false; - } - }; - - TraceContext currentContext(); - - Span nextSpan(String name); // uses current active span - - Span nextSpan(String name, TraceContext parent); // manually attach the next span to the provided parent - - boolean enabled(); -} diff --git a/driver-core/src/main/com/mongodb/tracing/package-info.java b/driver-core/src/main/com/mongodb/tracing/package-info.java index 2ec7551d300..43e82603a09 100644 --- a/driver-core/src/main/com/mongodb/tracing/package-info.java +++ b/driver-core/src/main/com/mongodb/tracing/package-info.java @@ -15,7 +15,9 @@ */ /** - * Contains classes related to sessions + * This package defines the API for MongoDB driver tracing. + * + * @since 5.6 */ @NonNullApi package com.mongodb.tracing; diff --git a/driver-core/src/test/functional/com/mongodb/ClusterFixture.java b/driver-core/src/test/functional/com/mongodb/ClusterFixture.java index d2916206a7d..30792bf0487 100644 --- a/driver-core/src/test/functional/com/mongodb/ClusterFixture.java +++ b/driver-core/src/test/functional/com/mongodb/ClusterFixture.java @@ -71,7 +71,6 @@ import com.mongodb.internal.operation.DropDatabaseOperation; import com.mongodb.internal.operation.ReadOperation; import com.mongodb.internal.operation.WriteOperation; -import com.mongodb.internal.tracing.TracingManager; import com.mongodb.lang.Nullable; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/CommandHelperTest.java b/driver-core/src/test/unit/com/mongodb/internal/connection/CommandHelperTest.java index f40c7d17f20..447a1efb5dd 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/connection/CommandHelperTest.java +++ b/driver-core/src/test/unit/com/mongodb/internal/connection/CommandHelperTest.java @@ -26,7 +26,6 @@ import com.mongodb.internal.IgnorableRequestContext; import com.mongodb.internal.TimeoutContext; import com.mongodb.internal.TimeoutSettings; -import com.mongodb.internal.tracing.TracingManager; import org.bson.BsonDocument; import org.bson.codecs.Decoder; import org.junit.jupiter.api.Test; @@ -121,6 +120,6 @@ void testIsCommandOk() { OperationContext createOperationContext() { return new OperationContext(IgnorableRequestContext.INSTANCE, NoOpSessionContext.INSTANCE, - new TimeoutContext(TimeoutSettings.DEFAULT), ServerApi.builder().version(ServerApiVersion.V1).build(), TracingManager.NO_OP); + new TimeoutContext(TimeoutSettings.DEFAULT), ServerApi.builder().version(ServerApiVersion.V1).build()); } } diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/CommandMessageTest.java b/driver-core/src/test/unit/com/mongodb/internal/connection/CommandMessageTest.java index 3ceb7567eef..533e74f0d23 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/connection/CommandMessageTest.java +++ b/driver-core/src/test/unit/com/mongodb/internal/connection/CommandMessageTest.java @@ -32,7 +32,6 @@ import com.mongodb.internal.operation.ClientBulkWriteOperation; import com.mongodb.internal.operation.ClientBulkWriteOperation.ClientBulkWriteCommand.OpsAndNsInfo; import com.mongodb.internal.session.SessionContext; -import com.mongodb.internal.tracing.TracingManager; import com.mongodb.internal.validator.NoOpFieldNameValidator; import org.bson.BsonArray; import org.bson.BsonBoolean; @@ -164,7 +163,7 @@ void getCommandDocumentFromClientBulkWrite() { output, new OperationContext( IgnorableRequestContext.INSTANCE, NoOpSessionContext.INSTANCE, - new TimeoutContext(TimeoutSettings.DEFAULT), null, TracingManager.NO_OP)); + new TimeoutContext(TimeoutSettings.DEFAULT), null)); BsonDocument actualCommandDocument = commandMessage.getCommandDocument(output); assertEquals(expectedCommandDocument, actualCommandDocument); } 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/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-legacy/src/main/com/mongodb/MongoClient.java b/driver-legacy/src/main/com/mongodb/MongoClient.java index 9478dc76177..09d58e1b493 100644 --- a/driver-legacy/src/main/com/mongodb/MongoClient.java +++ b/driver-legacy/src/main/com/mongodb/MongoClient.java @@ -43,7 +43,6 @@ import com.mongodb.internal.diagnostics.logging.Loggers; import com.mongodb.internal.session.ServerSessionPool; import com.mongodb.internal.thread.DaemonThreadFactory; -import com.mongodb.internal.tracing.TracingManager; import com.mongodb.internal.validator.NoOpFieldNameValidator; import com.mongodb.lang.Nullable; import org.bson.BsonArray; 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 1689c9b31e9..250949549f2 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 @@ -205,7 +205,7 @@ private OperationContext getOperationContext(final RequestContext requestContext requestContext, new ReadConcernAwareNoOpSessionContext(readConcern), createTimeoutContext(session, timeoutSettings), - mongoClient.getSettings().getTracingManager(), + TracingManager.NO_OP, mongoClient.getSettings().getServerApi(), commandName); } diff --git a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncClientSession.java b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncClientSession.java index 494e5f8c74e..ab234ad6f3e 100644 --- a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncClientSession.java +++ b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncClientSession.java @@ -22,6 +22,7 @@ import com.mongodb.client.ClientSession; import com.mongodb.client.TransactionBody; import com.mongodb.internal.TimeoutContext; +import com.mongodb.internal.tracing.TransactionSpan; import com.mongodb.lang.Nullable; import com.mongodb.session.ServerSession; import org.bson.BsonDocument; @@ -188,6 +189,12 @@ public TimeoutContext getTimeoutContext() { return wrapped.getTimeoutContext(); } + @Override + @Nullable + public TransactionSpan getTransactionSpan() { + return null; + } + private static void sleep(final long millis) { try { Thread.sleep(millis); diff --git a/driver-scala/src/integrationTest/scala/org/mongodb/scala/syncadapter/SyncClientSession.scala b/driver-scala/src/integrationTest/scala/org/mongodb/scala/syncadapter/SyncClientSession.scala index 2866ce7427d..4c0cb19217d 100644 --- a/driver-scala/src/integrationTest/scala/org/mongodb/scala/syncadapter/SyncClientSession.scala +++ b/driver-scala/src/integrationTest/scala/org/mongodb/scala/syncadapter/SyncClientSession.scala @@ -19,6 +19,7 @@ package org.mongodb.scala.syncadapter import com.mongodb.{ ClientSessionOptions, MongoInterruptedException, ServerAddress, TransactionOptions } import com.mongodb.client.{ ClientSession => JClientSession, TransactionBody } import com.mongodb.internal.TimeoutContext +import com.mongodb.internal.tracing.TransactionSpan import com.mongodb.session.ServerSession import org.bson.{ BsonDocument, BsonTimestamp } import org.mongodb.scala._ @@ -96,4 +97,6 @@ case class SyncClientSession(wrapped: ClientSession, originator: Object) extends } override def getTimeoutContext: TimeoutContext = wrapped.getTimeoutContext + + override def getTransactionSpan: TransactionSpan = null } diff --git a/driver-sync/build.gradle.kts b/driver-sync/build.gradle.kts index 95cd0979973..b37d0226295 100644 --- a/driver-sync/build.gradle.kts +++ b/driver-sync/build.gradle.kts @@ -38,6 +38,9 @@ dependencies { // lambda testing testImplementation(libs.aws.lambda.core) + + // Tracing + testImplementation(libs.bundles.micrometer.test) } configureMavenPublication { diff --git a/driver-sync/src/main/com/mongodb/client/ClientSession.java b/driver-sync/src/main/com/mongodb/client/ClientSession.java index 5d994b863e8..6af2b4be664 100644 --- a/driver-sync/src/main/com/mongodb/client/ClientSession.java +++ b/driver-sync/src/main/com/mongodb/client/ClientSession.java @@ -18,6 +18,7 @@ import com.mongodb.ServerAddress; import com.mongodb.TransactionOptions; +import com.mongodb.internal.tracing.TransactionSpan; import com.mongodb.lang.Nullable; /** @@ -125,4 +126,13 @@ public interface ClientSession extends com.mongodb.session.ClientSession { * @since 3.11 */ T withTransaction(TransactionBody transactionBody, TransactionOptions options); + + /** + * Get the transaction span (if started). + * + * @return the transaction span + * @since 5.6 + */ + @Nullable + TransactionSpan getTransactionSpan(); } diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java index b60fc90316a..c1937843a9d 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java +++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java @@ -36,6 +36,8 @@ import com.mongodb.internal.operation.WriteOperation; import com.mongodb.internal.session.BaseClientSessionImpl; import com.mongodb.internal.session.ServerSessionPool; +import com.mongodb.internal.tracing.TracingManager; +import com.mongodb.internal.tracing.TransactionSpan; import com.mongodb.lang.Nullable; import static com.mongodb.MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL; @@ -54,11 +56,14 @@ final class ClientSessionImpl extends BaseClientSessionImpl implements ClientSes private boolean messageSentInCurrentTransaction; private boolean commitInProgress; private TransactionOptions transactionOptions; + private final TracingManager tracingManager; + private TransactionSpan transactionSpan = null; ClientSessionImpl(final ServerSessionPool serverSessionPool, final Object originator, final ClientSessionOptions options, - final OperationExecutor operationExecutor) { + final OperationExecutor operationExecutor, final TracingManager tracingManager) { super(serverSessionPool, originator, options); this.operationExecutor = operationExecutor; + this.tracingManager = tracingManager; } @Override @@ -141,6 +146,9 @@ public void abortTransaction() { } finally { clearTransactionContext(); cleanupTransaction(TransactionState.ABORTED); + if (transactionSpan != null) { + transactionSpan.finalizeTransactionSpan(TransactionState.ABORTED.name()); + } } } @@ -167,6 +175,10 @@ private void startTransaction(final TransactionOptions transactionOptions, final if (!writeConcern.isAcknowledged()) { throw new MongoClientException("Transactions do not support unacknowledged write concern"); } + + if (tracingManager.isEnabled()) { + transactionSpan = new TransactionSpan(tracingManager); + } clearTransactionContext(); setTimeoutContext(timeoutContext); } @@ -187,7 +199,7 @@ private void commitTransaction(final boolean resetTimeout) { if (transactionState == TransactionState.NONE) { throw new IllegalStateException("There is no transaction started"); } - + boolean exceptionThrown = false; try { if (messageSentInCurrentTransaction) { ReadConcern readConcern = transactionOptions.getReadConcern(); @@ -206,11 +218,20 @@ private void commitTransaction(final boolean resetTimeout) { .recoveryToken(getRecoveryToken()), readConcern, this); } } catch (MongoException e) { + exceptionThrown = true; clearTransactionContextOnError(e); + if (transactionSpan != null) { + transactionSpan.handleTransactionSpanError(e); + } throw e; } finally { transactionState = TransactionState.COMMITTED; commitInProgress = false; + if (!exceptionThrown) { + if (transactionSpan != null) { + transactionSpan.finalizeTransactionSpan(TransactionState.COMMITTED.name()); + } + } } } @@ -231,51 +252,72 @@ public T withTransaction(final TransactionBody transactionBody, final Tra long startTime = ClientSessionClock.INSTANCE.now(); TimeoutContext withTransactionTimeoutContext = createTimeoutContext(options); - outer: - while (true) { - T retVal; - try { - startTransaction(options, withTransactionTimeoutContext.copyTimeoutContext()); - retVal = transactionBody.execute(); - } catch (Throwable e) { - if (transactionState == TransactionState.IN) { - abortTransaction(); - } - if (e instanceof MongoException && !(e instanceof MongoOperationTimeoutException)) { - MongoException exceptionToHandle = OperationHelper.unwrap((MongoException) e); - if (exceptionToHandle.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL) - && ClientSessionClock.INSTANCE.now() - startTime < MAX_RETRY_TIME_LIMIT_MS) { - continue; + try { + outer: + while (true) { + T retVal; + try { + startTransaction(options, withTransactionTimeoutContext.copyTimeoutContext()); + if (transactionSpan != null) { + transactionSpan.setIsConvenientTransaction(); } - } - throw e; - } - if (transactionState == TransactionState.IN) { - while (true) { - try { - commitTransaction(false); - break; - } catch (MongoException e) { - clearTransactionContextOnError(e); - if (!(e instanceof MongoOperationTimeoutException) + retVal = transactionBody.execute(); + } catch (Throwable e) { + if (transactionState == TransactionState.IN) { + abortTransaction(); + } + if (e instanceof MongoException && !(e instanceof MongoOperationTimeoutException)) { + MongoException exceptionToHandle = OperationHelper.unwrap((MongoException) e); + if (exceptionToHandle.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL) && ClientSessionClock.INSTANCE.now() - startTime < MAX_RETRY_TIME_LIMIT_MS) { - applyMajorityWriteConcernToTransactionOptions(); + if (transactionSpan != null) { + transactionSpan.spanFinalizing(false); + } + continue; + } + } + throw e; + } + if (transactionState == TransactionState.IN) { + while (true) { + try { + commitTransaction(false); + break; + } catch (MongoException e) { + clearTransactionContextOnError(e); + if (!(e instanceof MongoOperationTimeoutException) + && ClientSessionClock.INSTANCE.now() - startTime < MAX_RETRY_TIME_LIMIT_MS) { + applyMajorityWriteConcernToTransactionOptions(); - if (!(e instanceof MongoExecutionTimeoutException) - && e.hasErrorLabel(UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) { - continue; - } else if (e.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)) { - continue outer; + if (!(e instanceof MongoExecutionTimeoutException) + && e.hasErrorLabel(UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) { + continue; + } else if (e.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)) { + if (transactionSpan != null) { + transactionSpan.spanFinalizing(true); + } + continue outer; + } } + throw e; } - throw e; } } + return retVal; + } + } finally { + if (transactionSpan != null) { + transactionSpan.spanFinalizing(true); } - return retVal; } } + @Override + @Nullable + public TransactionSpan getTransactionSpan() { + return transactionSpan; + } + @Override public void close() { try { diff --git a/driver-sync/src/main/com/mongodb/client/internal/MongoClientImpl.java b/driver-sync/src/main/com/mongodb/client/internal/MongoClientImpl.java index acb19fe12d9..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; @@ -107,7 +108,7 @@ public MongoClientImpl(final Cluster cluster, settings.getRetryWrites(), settings.getServerApi(), new ServerSessionPool(cluster, TimeoutSettings.create(settings), settings.getServerApi()), TimeoutSettings.create(settings), settings.getUuidRepresentation(), - settings.getWriteConcern(), settings.getTracingManager()); + 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 5944edae4e2..f1ea046b591 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java +++ b/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java @@ -43,6 +43,7 @@ import com.mongodb.client.model.bulk.ClientBulkWriteResult; 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 +58,11 @@ import com.mongodb.internal.operation.SyncOperations; import com.mongodb.internal.operation.WriteOperation; import com.mongodb.internal.session.ServerSessionPool; +import com.mongodb.internal.tracing.Span; +import com.mongodb.internal.tracing.Tags; +import com.mongodb.internal.tracing.TraceContext; import com.mongodb.internal.tracing.TracingManager; +import com.mongodb.internal.tracing.TransactionSpan; import com.mongodb.lang.Nullable; import org.bson.BsonDocument; import org.bson.Document; @@ -253,7 +258,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 @@ -423,6 +428,8 @@ public T execute(final ReadOperation operation, final ReadPreference read ReadBinding binding = getReadBinding(readPreference, readConcern, actualClientSession, session == null, operation.getCommandName()); + Span span = createOperationSpan(actualClientSession, binding, operation.getCommandName()); + try { if (actualClientSession.hasActiveTransaction() && !binding.getReadPreference().equals(primary())) { throw new MongoClientException("Read preference in a transaction must be primary"); @@ -432,9 +439,15 @@ public T execute(final ReadOperation operation, final ReadPreference read 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(); + } } } @@ -448,15 +461,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()); 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(); + } } } @@ -503,6 +523,7 @@ private OperationContext getOperationContext(final ClientSession session, final getRequestContext(), new ReadConcernAwareNoOpSessionContext(readConcern), createTimeoutContext(session, executorTimeoutSettings), + tracingManager, serverApi, commandName); } @@ -560,5 +581,35 @@ 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 + */ + @Nullable + private Span createOperationSpan(ClientSession actualClientSession, BindingContext binding, String commandName) { + TracingManager tracingManager = binding.getOperationContext().getTracingManager(); + if (tracingManager.isEnabled()) { + TraceContext parentContext = null; + TransactionSpan transactionSpan = actualClientSession.getTransactionSpan(); + if (transactionSpan != null) { + parentContext = transactionSpan.getContext(); + } + + Span span = binding + .getOperationContext() + .getTracingManager() + .addSpan(commandName, parentContext); + binding.getOperationContext().setTracingContext(span.context()); + span.tag(Tags.SYSTEM, "mongodb"); + return span; + + } else { + return null; + } + } } } diff --git a/driver-sync/src/test/functional/com/mongodb/client/tracing/SpanTree.java b/driver-sync/src/test/functional/com/mongodb/client/tracing/SpanTree.java new file mode 100644 index 00000000000..bca04e6647a --- /dev/null +++ b/driver-sync/src/test/functional/com/mongodb/client/tracing/SpanTree.java @@ -0,0 +1,295 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.client.tracing; + +import com.mongodb.internal.tracing.Tags; +import com.mongodb.lang.Nullable; +import io.micrometer.tracing.test.simple.SimpleSpan; +import org.bson.BsonArray; +import org.bson.BsonBinary; +import org.bson.BsonDocument; +import org.bson.BsonString; +import org.bson.BsonValue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.function.BiConsumer; + +import static org.bson.assertions.Assertions.notNull; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Represents a tree structure of spans, where each span can have nested spans as children. + * This class provides methods to create a span tree from various sources and to validate the spans against expected values. + */ +public class SpanTree { + private final List roots = new ArrayList<>(); + + /** + * Creates a SpanTree from a BsonArray of spans. + * + * @param spans the BsonArray containing span documents + * @return a SpanTree constructed from the provided spans + */ + public static SpanTree from(final BsonArray spans) { + SpanTree spanTree = new SpanTree(); + for (final BsonValue span : spans) { + if (span.isDocument()) { + final BsonDocument spanDoc = span.asDocument(); + final String name = spanDoc.getString("name").getValue(); + final SpanNode rootNode = new SpanNode(name); + spanTree.roots.add(rootNode); + + if (spanDoc.containsKey("tags")) { + rootNode.tags = spanDoc.getDocument("tags"); + } + + if (spanDoc.containsKey("nested")) { + for (final BsonValue nestedSpan : spanDoc.getArray("nested")) { + addNestedSpans(rootNode, nestedSpan.asDocument()); + } + } + } + } + + return spanTree; + } + + /** + * Creates a SpanTree from a JSON string representation of spans. + * + * @param spansAsJson the JSON string containing span documents + * @return a SpanTree constructed from the provided JSON spans + */ + public static SpanTree from(final String spansAsJson) { + BsonArray spans = BsonArray.parse(spansAsJson); + return from(spans); + } + + /** + * Creates a SpanTree from a Deque of SimpleSpan objects. + * This method is typically used to build a tree based on the actual collected tracing spans. + * + * @param spans the Deque containing SimpleSpan objects + * @return a SpanTree constructed from the provided spans + */ + public static SpanTree from(final Deque spans) { + final SpanTree spanTree = new SpanTree(); + final Map idToSpanNode = new HashMap<>(); + for (final SimpleSpan span : spans) { + final SpanNode spanNode = new SpanNode(span.getName()); + for (final Map.Entry tag : span.getTags().entrySet()) { + // handle special case of session id (needs to be parsed into a BsonBinary) + // this is needed because the SimpleTracer reports all the collected tags as strings + if (tag.getKey().equals(Tags.SESSION_ID)) { + spanNode.tags.append(tag.getKey(), new BsonDocument().append("id", new BsonBinary(UUID.fromString(tag.getValue())))); + } else { + spanNode.tags.append(tag.getKey(), new BsonString(tag.getValue())); + } + } + idToSpanNode.put(span.context().spanId(), spanNode); + } + + for (final SimpleSpan span : spans) { + final String parentId = span.context().parentId(); + final SpanNode node = idToSpanNode.get(span.context().spanId()); + + if (!parentId.isEmpty() && idToSpanNode.containsKey(parentId)) { + idToSpanNode.get(parentId).children.add(node); + } else { // doesn't have a parent, so it is a root node + spanTree.roots.add(node); + } + } + return spanTree; + } + + /** + * Adds nested spans to the parent node based on the provided BsonDocument. + * This method recursively adds child spans to the parent span node. + * + * @param parentNode the parent span node to which nested spans will be added + * @param nestedSpan the BsonDocument representing a nested span + */ + private static void addNestedSpans(final SpanNode parentNode, final BsonDocument nestedSpan) { + final String name = nestedSpan.getString("name").getValue(); + final SpanNode childNode = new SpanNode(name, parentNode); + + if (nestedSpan.containsKey("tags")) { + childNode.tags = nestedSpan.getDocument("tags"); + } + + if (nestedSpan.containsKey("nested")) { + for (final BsonValue nested : nestedSpan.getArray("nested")) { + addNestedSpans(childNode, nested.asDocument()); + } + } + } + + /** + * Asserts that the reported spans are valid against the expected spans. + * This method checks that the reported spans match the expected spans in terms of names, tags, and structure. + * + * @param reportedSpans the SpanTree containing the reported spans + * @param expectedSpans the SpanTree containing the expected spans + * @param valueMatcher a BiConsumer to match values of tags between reported and expected spans + * @param ignoreExtraSpans if true, allows reported spans to contain extra spans not present in expected spans + */ + public static void assertValid(final SpanTree reportedSpans, final SpanTree expectedSpans, + final BiConsumer valueMatcher, + final boolean ignoreExtraSpans) { + if (ignoreExtraSpans) { + // remove from the reported spans all the nodes that are not expected + reportedSpans.roots.removeIf(node -> !expectedSpans.roots.contains(node)); + + } + + // check that we have the same root spans + if (reportedSpans.roots.size() != expectedSpans.roots.size()) { + fail("The number of reported spans does not match expected spans size. " + + "Reported: " + reportedSpans.roots.size() + + ", Expected: " + expectedSpans.roots.size() + + " ignoreExtraSpans: " + ignoreExtraSpans); + } + + for (int i = 0; i < reportedSpans.roots.size(); i++) { + assertValid(reportedSpans.roots.get(i), expectedSpans.roots.get(i), valueMatcher); + } + } + + /** + * Asserts that a reported span node is valid against an expected span node. + * This method checks that the reported span's name, tags, and children match the expected span. + * + * @param reportedNode the reported span node to validate + * @param expectedNode the expected span node to validate against + * @param valueMatcher a BiConsumer to match values of tags between reported and expected spans + */ + private static void assertValid(final SpanNode reportedNode, final SpanNode expectedNode, + final BiConsumer valueMatcher) { + // Check that the span names match + if (!reportedNode.getName().equalsIgnoreCase(expectedNode.getName())) { + fail("Reported span name " + + reportedNode.getName() + + " does not match expected span name " + + expectedNode.getName()); + } + + valueMatcher.accept(expectedNode.tags, reportedNode.tags); + + // Spans should have the same number of children + if (reportedNode.children.size() != expectedNode.children.size()) { + fail("Reported span " + reportedNode.getName() + + " has " + reportedNode.children.size() + + " children, but expected " + expectedNode.children.size()); + } + + // For every reported child span make sure it is valid against the expected child span + for (int i = 0; i < reportedNode.children.size(); i++) { + assertValid(reportedNode.children.get(i), expectedNode.children.get(i), valueMatcher); + } + } + + @Override + public String toString() { + return "SpanTree{" + + "roots=" + roots + + '}'; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final SpanTree spanTree = (SpanTree) o; + return Objects.deepEquals(roots, spanTree.roots); + } + + @Override + public int hashCode() { + return Objects.hash(roots); + } + + /** + * Represents a node in the span tree, which can have nested child spans. + * Each span node contains a name, tags, and a list of child span nodes. + */ + public static class SpanNode { + private final String name; + private BsonDocument tags = new BsonDocument(); + private final List children = new ArrayList<>(); + + public SpanNode(final String name) { + this.name = notNull("name", name); + } + + public SpanNode(final String name, @Nullable final SpanNode parent) { + this.name = notNull("name", name); + if (parent != null) { + parent.children.add(this); + } + } + + public String getName() { + return name; + } + + public List getChildren() { + return Collections.unmodifiableList(children); + } + + @Override + public String toString() { + return "SpanNode{" + + "name='" + name + '\'' + + ", tags=" + tags + + ", children=" + children + + '}'; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final SpanNode spanNode = (SpanNode) o; + return name.equalsIgnoreCase(spanNode.name) + && Objects.equals(tags, spanNode.tags) + && Objects.equals(children, spanNode.children); + } + + @Override + public int hashCode() { + int result = name.hashCode(); + result = 31 * result + tags.hashCode(); + result = 31 * result + children.hashCode(); + return result; + } + } +} diff --git a/driver-sync/src/test/functional/com/mongodb/client/tracing/ZipkinTracer.java b/driver-sync/src/test/functional/com/mongodb/client/tracing/ZipkinTracer.java new file mode 100644 index 00000000000..305e916d213 --- /dev/null +++ b/driver-sync/src/test/functional/com/mongodb/client/tracing/ZipkinTracer.java @@ -0,0 +1,95 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.client.tracing; + +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext; +import io.micrometer.tracing.otel.bridge.OtelTracer; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter; +import io.opentelemetry.extension.trace.propagation.B3Propagator; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.semconv.ResourceAttributes; + +/** + * A utility class to create a Zipkin tracer using OpenTelemetry protocol, useful for visualizing spans in Zipkin UI + * This tracer can be used to send spans to a Zipkin server. + *

+ * Spans are visible in the Zipkin UI at .... + *

+ * To Start Zipkin server, you can use the following command: + *

{@code
+ * docker run -d -p 9411:9411 openzipkin/zipkin
+ * }
+ */ +public final class ZipkinTracer { + private static final String ENDPOINT = "http://localhost:9411/api/v2/spans"; + + private ZipkinTracer() { + } + + /** + * Creates a Zipkin tracer with the specified service name. + * + * @param serviceName the name of the service to be used in the tracer + * @return a Tracer instance configured to send spans to Zipkin + */ + public static Tracer getTracer(final String serviceName) { + ZipkinSpanExporter zipkinExporter = ZipkinSpanExporter.builder() + .setEndpoint(ENDPOINT) + .build(); + + Resource resource = Resource.getDefault() + .merge(Resource.create( + Attributes.of( + ResourceAttributes.SERVICE_NAME, serviceName, + ResourceAttributes.SERVICE_VERSION, "1.0.0" + ) + )); + + SpanProcessor spanProcessor = SimpleSpanProcessor.create(zipkinExporter); + + SdkTracerProvider tracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(spanProcessor) + .setResource(resource) + .build(); + + OpenTelemetry openTelemetry = OpenTelemetrySdk.builder() + .setTracerProvider(tracerProvider) + .setPropagators(ContextPropagators.create( + B3Propagator.injectingSingleHeader() + )) + .build(); + + io.opentelemetry.api.trace.Tracer otelTracer = openTelemetry.getTracer("my-java-service", "1.0.0"); + + OtelCurrentTraceContext otelCurrentTraceContext = new OtelCurrentTraceContext(); + + return new OtelTracer( + otelTracer, + otelCurrentTraceContext, + null // EventPublisher can be null for basic usage + ); + } + +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 90aec4c1c38..38805b02353 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,6 +25,8 @@ snappy = "1.1.10.3" zstd = "1.5.5-3" jetbrains-annotations = "26.0.2" micrometer = "1.4.5" +zipkin-reporter = "2.16.3" +opentelemetry-exporter-zipkin = "1.30.0" kotlin = "1.8.10" kotlinx-coroutines-bom = "1.6.4" @@ -174,6 +176,12 @@ project-reactor-test = { module = "io.projectreactor:reactor-test" } reactive-streams-tck = { module = " org.reactivestreams:reactive-streams-tck", version.ref = "reactive-streams" } reflections = { module = "org.reflections:reflections", version.ref = "reflections" } +micrometer-tracing-test = { module = " io.micrometer:micrometer-tracing-test", version.ref = "micrometer" } +micrometer-tracing-bridge-brave = { module = " io.micrometer:micrometer-tracing-bridge-brave", version.ref = "micrometer" } +micrometer-tracing = { module = " io.micrometer:micrometer-tracing", version.ref = "micrometer" } +micrometer-tracing-bridge-otel = { module = " io.micrometer:micrometer-tracing-bridge-otel", version.ref = "micrometer" } +zipkin-reporter = { module = " io.zipkin.reporter2:zipkin-reporter", version.ref = "zipkin-reporter" } +opentelemetry-exporter-zipkin = { module = " io.opentelemetry:opentelemetry-exporter-zipkin", version.ref = "opentelemetry-exporter-zipkin" } [bundles] aws-java-sdk-v1 = ["aws-java-sdk-v1-core", "aws-java-sdk-v1-sts"] @@ -200,6 +208,9 @@ scala-test-v2-v12 = ["scala-test-flatspec-v2-v12", "scala-test-shouldmatchers-v2 scala-test-v2-v11 = ["scala-test-flatspec-v2-v11", "scala-test-shouldmatchers-v2-v11", "scala-test-mockito-v2-v11", "scala-test-junit-runner-v2-v11", "reflections"] +micrometer-test = ["micrometer-tracing-test", "micrometer-tracing-bridge-brave", "micrometer-tracing-bridge-otel", + "micrometer-tracing", "zipkin-reporter", "opentelemetry-exporter-zipkin"] + [plugins] kotlin-gradle = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } bnd = { id = "biz.aQute.bnd.builder", version.ref = "plugin-bnd" } From 33d46e8ca5f87a7f5ad96318930ab3bfe2aa8e48 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 4 Aug 2025 13:00:02 +0100 Subject: [PATCH 03/14] Merging back missing changes after rebase --- .../AsyncCommandBatchCursorTest.java | 2 ++ .../MongoClientSettingsSpecification.groovy | 4 +-- ...hangeStreamBatchCursorSpecification.groovy | 4 +++ ...syncCommandBatchCursorSpecification.groovy | 2 ++ .../ChangeStreamBatchCursorTest.java | 2 ++ .../CommandBatchCursorSpecification.groovy | 2 ++ .../operation/CommandBatchCursorTest.java | 3 +++ driver-kotlin-coroutine/build.gradle.kts | 1 + driver-kotlin-sync/build.gradle.kts | 1 + .../scala/ApiAliasAndCompanionSpec.scala | 3 ++- .../mongodb/client/tracing/ZipkinTracer.java | 2 +- .../com/mongodb/client/unified/Entities.java | 26 +++++++++++++++++- .../client/unified/MicrometerTracingTest.java | 27 +++++++++++++++++++ .../internal/MongoClusterSpecification.groovy | 4 ++- 14 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 driver-sync/src/test/functional/com/mongodb/client/unified/MicrometerTracingTest.java 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/operation/AsyncChangeStreamBatchCursorSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncChangeStreamBatchCursorSpecification.groovy index 998c0a28b6e..b7f8f0e9408 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncChangeStreamBatchCursorSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncChangeStreamBatchCursorSpecification.groovy @@ -22,6 +22,7 @@ import com.mongodb.internal.TimeoutContext import com.mongodb.internal.async.SingleResultCallback import com.mongodb.internal.binding.AsyncReadBinding import com.mongodb.internal.connection.OperationContext +import com.mongodb.internal.tracing.TracingManager import org.bson.Document import spock.lang.Specification @@ -34,6 +35,7 @@ class AsyncChangeStreamBatchCursorSpecification extends Specification { def changeStreamOpertation = Stub(ChangeStreamOperation) def binding = Mock(AsyncReadBinding) def operationContext = Mock(OperationContext) + operationContext.getTracingManager() >> TracingManager.NO_OP def timeoutContext = Mock(TimeoutContext) binding.getOperationContext() >> operationContext operationContext.getTimeoutContext() >> timeoutContext @@ -78,6 +80,7 @@ class AsyncChangeStreamBatchCursorSpecification extends Specification { def changeStreamOpertation = Stub(ChangeStreamOperation) def binding = Mock(AsyncReadBinding) def operationContext = Mock(OperationContext) + operationContext.getTracingManager() >> TracingManager.NO_OP def timeoutContext = Mock(TimeoutContext) binding.getOperationContext() >> operationContext operationContext.getTimeoutContext() >> timeoutContext @@ -111,6 +114,7 @@ class AsyncChangeStreamBatchCursorSpecification extends Specification { def changeStreamOpertation = Stub(ChangeStreamOperation) def binding = Mock(AsyncReadBinding) def operationContext = Mock(OperationContext) + operationContext.getTracingManager() >> TracingManager.NO_OP def timeoutContext = Mock(TimeoutContext) binding.getOperationContext() >> operationContext operationContext.getTimeoutContext() >> timeoutContext diff --git a/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncCommandBatchCursorSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncCommandBatchCursorSpecification.groovy index d2bcd0804bb..e3f2e525146 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncCommandBatchCursorSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncCommandBatchCursorSpecification.groovy @@ -35,6 +35,7 @@ import com.mongodb.internal.async.SingleResultCallback import com.mongodb.internal.binding.AsyncConnectionSource import com.mongodb.internal.connection.AsyncConnection import com.mongodb.internal.connection.OperationContext +import com.mongodb.internal.tracing.TracingManager import org.bson.BsonArray import org.bson.BsonDocument import org.bson.BsonInt32 @@ -524,6 +525,7 @@ class AsyncCommandBatchCursorSpecification extends Specification { .build() } OperationContext operationContext = Mock(OperationContext) + operationContext.getTracingManager() >> TracingManager.NO_OP def timeoutContext = Spy(new TimeoutContext(TimeoutSettings.create( MongoClientSettings.builder().timeout(3, TimeUnit.SECONDS).build()))) operationContext.getTimeoutContext() >> timeoutContext diff --git a/driver-core/src/test/unit/com/mongodb/internal/operation/ChangeStreamBatchCursorTest.java b/driver-core/src/test/unit/com/mongodb/internal/operation/ChangeStreamBatchCursorTest.java index 48c3a50e79a..552afaea95b 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/operation/ChangeStreamBatchCursorTest.java +++ b/driver-core/src/test/unit/com/mongodb/internal/operation/ChangeStreamBatchCursorTest.java @@ -25,6 +25,7 @@ import com.mongodb.internal.binding.ReadBinding; import com.mongodb.internal.connection.Connection; import com.mongodb.internal.connection.OperationContext; +import com.mongodb.internal.tracing.TracingManager; import org.bson.BsonDocument; import org.bson.BsonInt32; import org.bson.Document; @@ -296,6 +297,7 @@ void setUp() { doNothing().when(timeoutContext).resetTimeoutIfPresent(); operationContext = mock(OperationContext.class); + when(operationContext.getTracingManager()).thenReturn(TracingManager.NO_OP); when(operationContext.getTimeoutContext()).thenReturn(timeoutContext); connection = mock(Connection.class); when(connection.command(any(), any(), any(), any(), any(), any())).thenReturn(null); diff --git a/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorSpecification.groovy index c95a119134a..3190c1f6289 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorSpecification.groovy @@ -35,6 +35,7 @@ import com.mongodb.internal.TimeoutSettings import com.mongodb.internal.binding.ConnectionSource import com.mongodb.internal.connection.Connection import com.mongodb.internal.connection.OperationContext +import com.mongodb.internal.tracing.TracingManager import org.bson.BsonArray import org.bson.BsonDocument import org.bson.BsonInt32 @@ -574,6 +575,7 @@ class CommandBatchCursorSpecification extends Specification { .build() } OperationContext operationContext = Mock(OperationContext) + operationContext.getTracingManager() >> TracingManager.NO_OP def timeoutContext = Spy(new TimeoutContext(TimeoutSettings.create( MongoClientSettings.builder().timeout(3, TimeUnit.SECONDS).build()))) operationContext.getTimeoutContext() >> timeoutContext diff --git a/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorTest.java b/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorTest.java index c3bec291432..b4b4101bd56 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorTest.java +++ b/driver-core/src/test/unit/com/mongodb/internal/operation/CommandBatchCursorTest.java @@ -31,6 +31,7 @@ import com.mongodb.internal.binding.ConnectionSource; import com.mongodb.internal.connection.Connection; import com.mongodb.internal.connection.OperationContext; +import com.mongodb.internal.tracing.TracingManager; import org.bson.BsonArray; import org.bson.BsonDocument; import org.bson.BsonInt32; @@ -94,6 +95,8 @@ void setUp() { connectionSource = mock(ConnectionSource.class); operationContext = mock(OperationContext.class); + when(operationContext.getTracingManager()).thenReturn(TracingManager.NO_OP); + timeoutContext = new TimeoutContext(TimeoutSettings.create( MongoClientSettings.builder().timeout(TIMEOUT.toMillis(), MILLISECONDS).build())); serverDescription = mock(ServerDescription.class); diff --git a/driver-kotlin-coroutine/build.gradle.kts b/driver-kotlin-coroutine/build.gradle.kts index 02a2bf047aa..dd127a4dd6a 100644 --- a/driver-kotlin-coroutine/build.gradle.kts +++ b/driver-kotlin-coroutine/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { integrationTestImplementation(project(path = ":bson", configuration = "testArtifacts")) integrationTestImplementation(project(path = ":driver-sync", configuration = "testArtifacts")) integrationTestImplementation(project(path = ":driver-core", configuration = "testArtifacts")) + integrationTestImplementation(libs.micrometer) } configureMavenPublication { diff --git a/driver-kotlin-sync/build.gradle.kts b/driver-kotlin-sync/build.gradle.kts index 5da1a5eec26..b6113200628 100644 --- a/driver-kotlin-sync/build.gradle.kts +++ b/driver-kotlin-sync/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { integrationTestImplementation(project(path = ":bson", configuration = "testArtifacts")) integrationTestImplementation(project(path = ":driver-sync", configuration = "testArtifacts")) integrationTestImplementation(project(path = ":driver-core", configuration = "testArtifacts")) + integrationTestImplementation(libs.micrometer) } configureMavenPublication { diff --git a/driver-scala/src/test/scala/org/mongodb/scala/ApiAliasAndCompanionSpec.scala b/driver-scala/src/test/scala/org/mongodb/scala/ApiAliasAndCompanionSpec.scala index a5b76965651..4e93a331776 100644 --- a/driver-scala/src/test/scala/org/mongodb/scala/ApiAliasAndCompanionSpec.scala +++ b/driver-scala/src/test/scala/org/mongodb/scala/ApiAliasAndCompanionSpec.scala @@ -94,7 +94,8 @@ class ApiAliasAndCompanionSpec extends BaseSpec { "SyncClientEncryption", "BaseClientUpdateOptions", "BaseClientDeleteOptions", - "MongoBaseInterfaceAssertions" + "MongoBaseInterfaceAssertions", + "MicrometerTracer" ) val scalaExclusions = Set( "BuildInfo", diff --git a/driver-sync/src/test/functional/com/mongodb/client/tracing/ZipkinTracer.java b/driver-sync/src/test/functional/com/mongodb/client/tracing/ZipkinTracer.java index 305e916d213..186835c6ca2 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/tracing/ZipkinTracer.java +++ b/driver-sync/src/test/functional/com/mongodb/client/tracing/ZipkinTracer.java @@ -35,7 +35,7 @@ * A utility class to create a Zipkin tracer using OpenTelemetry protocol, useful for visualizing spans in Zipkin UI * This tracer can be used to send spans to a Zipkin server. *

- * Spans are visible in the Zipkin UI at .... + * Spans are visible in the Zipkin UI at http://localhost:9411. *

* To Start Zipkin server, you can use the following command: *

{@code
diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java b/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java
index f1429431690..4dfedf0a8d5 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java
@@ -66,6 +66,9 @@
 import com.mongodb.lang.NonNull;
 import com.mongodb.lang.Nullable;
 import com.mongodb.logging.TestLoggingInterceptor;
+import com.mongodb.tracing.MicrometerTracer;
+import io.micrometer.tracing.Tracer;
+import io.micrometer.tracing.test.simple.SimpleTracer;
 import org.bson.BsonArray;
 import org.bson.BsonBoolean;
 import org.bson.BsonDocument;
@@ -112,7 +115,7 @@ public final class Entities {
     private static final Set SUPPORTED_CLIENT_ENTITY_OPTIONS = new HashSet<>(
             asList(
                     "id", "uriOptions", "serverApi", "useMultipleMongoses", "storeEventsAsEntities",
-                    "observeEvents", "observeLogMessages", "observeSensitiveCommands", "ignoreCommandMonitoringEvents"));
+                    "observeEvents", "observeLogMessages", "observeSensitiveCommands", "ignoreCommandMonitoringEvents", "tracing"));
     private final Set entityNames = new HashSet<>();
     private final Map threads = new HashMap<>();
     private final Map>> tasks = new HashMap<>();
@@ -126,6 +129,7 @@ public final class Entities {
     private final Map clientEncryptions = new HashMap<>();
     private final Map clientCommandListeners = new HashMap<>();
     private final Map clientLoggingInterceptors = new HashMap<>();
+    private final Map clientTracing = new HashMap<>();
     private final Map clientConnectionPoolListeners = new HashMap<>();
     private final Map clientServerListeners = new HashMap<>();
     private final Map clientClusterListeners = new HashMap<>();
@@ -294,6 +298,10 @@ public TestLoggingInterceptor getClientLoggingInterceptor(final String id) {
         return getEntity(id + "-logging-interceptor", clientLoggingInterceptors, "logging interceptor");
     }
 
+    public Tracer getClientTracer(final String id) {
+        return getEntity(id + "-tracing", clientTracing, "micrometer tracing");
+    }
+
     public TestConnectionPoolListener getConnectionPoolListener(final String id) {
         return getEntity(id + "-connection-pool-listener", clientConnectionPoolListeners, "connection pool listener");
     }
@@ -604,6 +612,22 @@ private void initClient(final BsonDocument entity, final String id,
             }
             clientSettingsBuilder.serverApi(serverApiBuilder.build());
         }
+
+        if (entity.containsKey("tracing")) {
+            boolean enableCommandPayload = entity.getDocument("tracing").get("enableCommandPayload", BsonBoolean.FALSE).asBoolean().getValue();
+            /* To enable Zipkin backend, uncomment the following lines and ensure you have the server started
+            (docker run -d -p 9411:9411 openzipkin/zipkin). The tests will fail but the captured spans will be
+             visible in the Zipkin UI at http://localhost:9411 for debugging purpose.
+             *
+             * Tracer tracer = ZipkinTracer.getTracer("UTR");
+             * putEntity(id + "-tracing", new SimpleTracer(), clientTracing);
+             */
+            Tracer tracer = new SimpleTracer();
+            putEntity(id + "-tracing", tracer, clientTracing);
+
+            clientSettingsBuilder.tracer(new MicrometerTracer(tracer, enableCommandPayload));
+        }
+
         MongoClientSettings clientSettings = clientSettingsBuilder.build();
 
         if (entity.containsKey("observeLogMessages")) {
diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/MicrometerTracingTest.java b/driver-sync/src/test/functional/com/mongodb/client/unified/MicrometerTracingTest.java
new file mode 100644
index 00000000000..84bc0734c5f
--- /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("tracing/tests");
+    }
+}
diff --git a/driver-sync/src/test/unit/com/mongodb/client/internal/MongoClusterSpecification.groovy b/driver-sync/src/test/unit/com/mongodb/client/internal/MongoClusterSpecification.groovy
index 62c16330950..f7b099a604d 100644
--- a/driver-sync/src/test/unit/com/mongodb/client/internal/MongoClusterSpecification.groovy
+++ b/driver-sync/src/test/unit/com/mongodb/client/internal/MongoClusterSpecification.groovy
@@ -28,6 +28,7 @@ import com.mongodb.internal.TimeoutSettings
 import com.mongodb.internal.client.model.changestream.ChangeStreamLevel
 import com.mongodb.internal.connection.Cluster
 import com.mongodb.internal.session.ServerSessionPool
+import com.mongodb.internal.tracing.TracingManager
 import org.bson.BsonDocument
 import org.bson.Document
 import org.bson.codecs.UuidCodec
@@ -258,6 +259,7 @@ class MongoClusterSpecification extends Specification {
     MongoClusterImpl createMongoCluster(final MongoClientSettings settings, final OperationExecutor operationExecutor) {
         new MongoClusterImpl(null, cluster, settings.codecRegistry, null, null,
                 originator, operationExecutor, settings.readConcern, settings.readPreference, settings.retryReads, settings.retryWrites,
-                null, serverSessionPool, TimeoutSettings.create(settings), settings.uuidRepresentation, settings.writeConcern)
+                null, serverSessionPool, TimeoutSettings.create(settings), settings.uuidRepresentation,
+                settings.writeConcern, TracingManager.NO_OP)
     }
 }

From 86f179171381bcb432db45926afcad675133e02f Mon Sep 17 00:00:00 2001
From: Nabil Hachicha 
Date: Tue, 5 Aug 2025 00:27:55 +0100
Subject: [PATCH 04/14] Fixing tests

---
 .../internal/connection/InternalStreamConnection.java  | 10 +++++-----
 .../mongodb/internal/connection/CommandHelperTest.java |  1 +
 .../LoggingCommandEventSenderSpecification.groovy      | 10 ++++++----
 .../kotlin/com/mongodb/kotlin/client/ClientSession.kt  |  4 ++++
 driver-reactive-streams/build.gradle.kts               |  3 +++
 .../com/mongodb/client/internal/MongoClusterImpl.java  |  2 +-
 6 files changed, 20 insertions(+), 10 deletions(-)

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 bd3008cf927..56bcc48ce34 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java
@@ -390,7 +390,7 @@ public  T sendAndReceive(final CommandMessage message, final Decoder decod
                 message, decoder, operationContext);
         try {
             return sendAndReceiveInternal.get();
-        } catch (Throwable e) {
+        } catch (MongoCommandException e) {
             if (reauthenticationIsTriggered(e)) {
                 return reauthenticateAndRetry(sendAndReceiveInternal, operationContext);
             }
@@ -405,8 +405,9 @@ public  void sendAndReceiveAsync(final CommandMessage message, final Decoder<
 
         AsyncSupplier sendAndReceiveAsyncInternal = c -> sendAndReceiveAsyncInternal(
                 message, decoder, operationContext, c);
-
-        beginAsync().thenSupply(sendAndReceiveAsyncInternal::getAsync).onErrorIf(this::reauthenticationIsTriggered, (t, c) -> {
+        beginAsync().thenSupply(c -> {
+            sendAndReceiveAsyncInternal.getAsync(c);
+        }).onErrorIf(e -> reauthenticationIsTriggered(e), (t, c) -> {
             reauthenticateAndRetryAsync(sendAndReceiveAsyncInternal, operationContext, c);
         }).finish(callback);
     }
@@ -1034,9 +1035,8 @@ private Span createTracingSpan(final CommandMessage message, final OperationCont
         TracingManager tracingManager = operationContext.getTracingManager();
         BsonDocument command = message.getCommandDocument(bsonOutput);
 
-//        BsonDocument command = message.getCommand();
         String commandName = command.getFirstKey();
-//        Span newSpan = tracingManager.addSpan("_____Command_____[ " + commandName + " ]", myparentContext);
+
         if (!tracingManager.isEnabled()
                 || SECURITY_SENSITIVE_COMMANDS.contains(commandName)
                 || SECURITY_SENSITIVE_HELLO_COMMANDS.contains(commandName)) {
diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/CommandHelperTest.java b/driver-core/src/test/unit/com/mongodb/internal/connection/CommandHelperTest.java
index 447a1efb5dd..f7873379c3b 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/connection/CommandHelperTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/connection/CommandHelperTest.java
@@ -118,6 +118,7 @@ void testIsCommandOk() {
         assertFalse(CommandHelper.isCommandOk(new BsonDocument()));
     }
 
+
     OperationContext createOperationContext() {
         return new OperationContext(IgnorableRequestContext.INSTANCE, NoOpSessionContext.INSTANCE,
                 new TimeoutContext(TimeoutSettings.DEFAULT), ServerApi.builder().version(ServerApiVersion.V1).build());
diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/LoggingCommandEventSenderSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/connection/LoggingCommandEventSenderSpecification.groovy
index 6f8eaf33314..35bced971fa 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/connection/LoggingCommandEventSenderSpecification.groovy
+++ b/driver-core/src/test/unit/com/mongodb/internal/connection/LoggingCommandEventSenderSpecification.groovy
@@ -66,7 +66,8 @@ class LoggingCommandEventSenderSpecification extends Specification {
         }
         def operationContext = OPERATION_CONTEXT
         def sender = new LoggingCommandEventSender([] as Set, [] as Set, connectionDescription, commandListener,
-                operationContext, message, bsonOutput, new StructuredLogger(logger), LoggerSettings.builder().build())
+                operationContext, message, message.getCommandDocument(bsonOutput),
+                new StructuredLogger(logger), LoggerSettings.builder().build())
 
         when:
         sender.sendStartedEvent()
@@ -111,7 +112,7 @@ class LoggingCommandEventSenderSpecification extends Specification {
         }
         def operationContext = OPERATION_CONTEXT
         def sender = new LoggingCommandEventSender([] as Set, [] as Set, connectionDescription, commandListener,
-                operationContext, message, bsonOutput, new StructuredLogger(logger),
+                operationContext, message, message.getCommandDocument(bsonOutput), new StructuredLogger(logger),
                 LoggerSettings.builder().build())
         when:
         sender.sendStartedEvent()
@@ -169,7 +170,7 @@ class LoggingCommandEventSenderSpecification extends Specification {
         def operationContext = OPERATION_CONTEXT
 
         def sender = new LoggingCommandEventSender([] as Set, [] as Set, connectionDescription, null, operationContext,
-                message, bsonOutput, new StructuredLogger(logger), LoggerSettings.builder().build())
+                message, message.getCommandDocument(bsonOutput), new StructuredLogger(logger), LoggerSettings.builder().build())
 
         when:
         sender.sendStartedEvent()
@@ -202,7 +203,8 @@ class LoggingCommandEventSenderSpecification extends Specification {
         }
         def operationContext = OPERATION_CONTEXT
         def sender = new LoggingCommandEventSender(['createUser'] as Set, [] as Set, connectionDescription, null,
-                operationContext, message, bsonOutput, new StructuredLogger(logger), LoggerSettings.builder().build())
+                operationContext, message, message.getCommandDocument(bsonOutput), new StructuredLogger(logger),
+                LoggerSettings.builder().build())
 
         when:
         sender.sendStartedEvent()
diff --git a/driver-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-reactive-streams/build.gradle.kts b/driver-reactive-streams/build.gradle.kts
index f1c758b31da..f9ac5301625 100644
--- a/driver-reactive-streams/build.gradle.kts
+++ b/driver-reactive-streams/build.gradle.kts
@@ -44,6 +44,9 @@ dependencies {
 
     // Reactive Streams TCK testing
     testImplementation(libs.reactive.streams.tck)
+
+    // Tracing
+    testImplementation(libs.micrometer)
 }
 
 configureMavenPublication {
diff --git a/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java b/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java
index f1ea046b591..ec2fbb5c759 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java
@@ -590,7 +590,7 @@ ClientSession getClientSession(@Nullable final ClientSession clientSessionFromOp
          * @param commandName the name of the command
          */
         @Nullable
-        private Span createOperationSpan(ClientSession actualClientSession, BindingContext binding, String commandName) {
+        private Span createOperationSpan(final ClientSession actualClientSession, final BindingContext binding, final String commandName) {
             TracingManager tracingManager = binding.getOperationContext().getTracingManager();
             if (tracingManager.isEnabled()) {
                 TraceContext parentContext = null;

From da3ac3294db32f2fd20732abb2e0f9935bf49ae2 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha 
Date: Sun, 10 Aug 2025 18:33:33 +0100
Subject: [PATCH 05/14] Adding method to get namespace for Read and Write
 Operations

---
 .../internal/operation/AbortTransactionOperation.java    | 6 ++++++
 .../operation/AbstractWriteSearchIndexOperation.java     | 4 +++-
 .../mongodb/internal/operation/AggregateOperation.java   | 3 ++-
 .../internal/operation/AggregateOperationImpl.java       | 9 +++++----
 .../operation/AggregateToCollectionOperation.java        | 5 +++++
 .../internal/operation/BaseFindAndModifyOperation.java   | 1 +
 .../internal/operation/ClientBulkWriteOperation.java     | 5 +++++
 .../mongodb/internal/operation/CommandReadOperation.java | 7 +++++++
 .../internal/operation/CommitTransactionOperation.java   | 6 ++++++
 .../internal/operation/CountDocumentsOperation.java      | 5 +++++
 .../com/mongodb/internal/operation/CountOperation.java   | 5 +++++
 .../internal/operation/CreateCollectionOperation.java    | 6 ++++++
 .../internal/operation/CreateIndexesOperation.java       | 5 +++++
 .../mongodb/internal/operation/CreateViewOperation.java  | 6 ++++++
 .../mongodb/internal/operation/DistinctOperation.java    | 5 +++++
 .../internal/operation/DropCollectionOperation.java      | 5 +++++
 .../internal/operation/DropDatabaseOperation.java        | 6 ++++++
 .../mongodb/internal/operation/DropIndexOperation.java   | 5 +++++
 .../operation/EstimatedDocumentCountOperation.java       | 5 +++++
 .../com/mongodb/internal/operation/FindOperation.java    | 1 +
 .../internal/operation/ListCollectionsOperation.java     | 7 +++++++
 .../internal/operation/ListDatabasesOperation.java       | 7 +++++++
 .../mongodb/internal/operation/ListIndexesOperation.java | 5 +++++
 .../internal/operation/ListSearchIndexesOperation.java   | 5 +++++
 .../operation/MapReduceToCollectionOperation.java        | 1 +
 .../operation/MapReduceWithInlineResultsOperation.java   | 1 +
 .../internal/operation/MixedBulkWriteOperation.java      | 1 +
 .../com/mongodb/internal/operation/ReadOperation.java    | 6 ++++++
 .../internal/operation/RenameCollectionOperation.java    | 5 +++++
 .../com/mongodb/internal/operation/WriteOperation.java   | 6 ++++++
 .../mongodb/client/internal/MapReduceIterableImpl.java   | 5 +++++
 31 files changed, 143 insertions(+), 6 deletions(-)

diff --git a/driver-core/src/main/com/mongodb/internal/operation/AbortTransactionOperation.java b/driver-core/src/main/com/mongodb/internal/operation/AbortTransactionOperation.java
index bc7e6655bc7..4f48722747e 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/AbortTransactionOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/AbortTransactionOperation.java
@@ -17,6 +17,7 @@
 package com.mongodb.internal.operation;
 
 import com.mongodb.Function;
+import com.mongodb.MongoNamespace;
 import com.mongodb.WriteConcern;
 import com.mongodb.internal.TimeoutContext;
 import com.mongodb.lang.Nullable;
@@ -48,6 +49,11 @@ public String getCommandName() {
         return COMMAND_NAME;
     }
 
+    @Override
+    public MongoNamespace getNamespace() {
+        return new MongoNamespace("admin", MongoNamespace.COMMAND_COLLECTION_NAME); // TODO double check
+    }
+
     @Override
     CommandCreator getCommandCreator() {
         return (operationContext, serverDescription, connectionDescription) -> {
diff --git a/driver-core/src/main/com/mongodb/internal/operation/AbstractWriteSearchIndexOperation.java b/driver-core/src/main/com/mongodb/internal/operation/AbstractWriteSearchIndexOperation.java
index 6ebcfda6dbe..ba033eb949a 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/AbstractWriteSearchIndexOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/AbstractWriteSearchIndexOperation.java
@@ -96,7 +96,9 @@  void swallowOrThrow(@Nullable final E mongoExecutionExcept
 
     abstract BsonDocument buildCommand();
 
-    MongoNamespace getNamespace() {
+
+    @Override
+    public MongoNamespace getNamespace() {
         return namespace;
     }
 }
diff --git a/driver-core/src/main/com/mongodb/internal/operation/AggregateOperation.java b/driver-core/src/main/com/mongodb/internal/operation/AggregateOperation.java
index 1c9abfc68ca..55c5c231a21 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/AggregateOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/AggregateOperation.java
@@ -164,7 +164,8 @@  CommandReadOperation createExplainableOperation(@Nullable final ExplainVe
                 }, resultDecoder);
     }
 
-    MongoNamespace getNamespace() {
+    @Override
+    public MongoNamespace getNamespace() {
         return wrapped.getNamespace();
     }
 }
diff --git a/driver-core/src/main/com/mongodb/internal/operation/AggregateOperationImpl.java b/driver-core/src/main/com/mongodb/internal/operation/AggregateOperationImpl.java
index 4c9bc3828b7..e2e8d6fb426 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/AggregateOperationImpl.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/AggregateOperationImpl.java
@@ -92,10 +92,6 @@ class AggregateOperationImpl implements ReadOperationCursor {
         this.pipelineCreator = notNull("pipelineCreator", pipelineCreator);
     }
 
-    MongoNamespace getNamespace() {
-        return namespace;
-    }
-
     List getPipeline() {
         return pipeline;
     }
@@ -191,6 +187,11 @@ public String getCommandName() {
         return COMMAND_NAME;
     }
 
+    @Override
+    public MongoNamespace getNamespace() {
+        return namespace;
+    }
+
     @Override
     public BatchCursor execute(final ReadBinding binding) {
         return executeRetryableRead(binding, namespace.getDatabaseName(),
diff --git a/driver-core/src/main/com/mongodb/internal/operation/AggregateToCollectionOperation.java b/driver-core/src/main/com/mongodb/internal/operation/AggregateToCollectionOperation.java
index 16f33ad45e5..296b4eabb88 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/AggregateToCollectionOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/AggregateToCollectionOperation.java
@@ -157,6 +157,11 @@ public String getCommandName() {
         return COMMAND_NAME;
     }
 
+    @Override
+    public MongoNamespace getNamespace() {
+        return namespace;
+    }
+
     @Override
     public Void execute(final ReadBinding binding) {
         return executeRetryableRead(binding,
diff --git a/driver-core/src/main/com/mongodb/internal/operation/BaseFindAndModifyOperation.java b/driver-core/src/main/com/mongodb/internal/operation/BaseFindAndModifyOperation.java
index c5d56fda81c..8f66333eb02 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/BaseFindAndModifyOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/BaseFindAndModifyOperation.java
@@ -92,6 +92,7 @@ public void executeAsync(final AsyncWriteBinding binding, final SingleResultCall
                 FindAndModifyHelper.asyncTransformer(), cmd -> cmd, callback);
     }
 
+    @Override
     public MongoNamespace getNamespace() {
         return namespace;
     }
diff --git a/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java b/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java
index 2b9e79f6f06..ad380781f73 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java
@@ -182,6 +182,11 @@ public String getCommandName() {
         return "bulkWrite";
     }
 
+    @Override
+    public MongoNamespace getNamespace() {
+        return getNamespacedModel(models, 0).getNamespace();
+    }
+
     @Override
     public ClientBulkWriteResult execute(final WriteBinding binding) throws ClientBulkWriteException {
         WriteConcern effectiveWriteConcern = validateAndGetEffectiveWriteConcern(binding.getOperationContext().getSessionContext());
diff --git a/driver-core/src/main/com/mongodb/internal/operation/CommandReadOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CommandReadOperation.java
index 6965bfc34a3..0fbc6eb06e9 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/CommandReadOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/CommandReadOperation.java
@@ -16,12 +16,14 @@
 
 package com.mongodb.internal.operation;
 
+import com.mongodb.MongoNamespace;
 import com.mongodb.internal.async.SingleResultCallback;
 import com.mongodb.internal.binding.AsyncReadBinding;
 import com.mongodb.internal.binding.ReadBinding;
 import org.bson.BsonDocument;
 import org.bson.codecs.Decoder;
 
+import static com.mongodb.MongoNamespace.COMMAND_COLLECTION_NAME;
 import static com.mongodb.assertions.Assertions.notNull;
 import static com.mongodb.internal.operation.AsyncOperationHelper.executeRetryableReadAsync;
 import static com.mongodb.internal.operation.CommandOperationHelper.CommandCreator;
@@ -55,6 +57,11 @@ public String getCommandName() {
         return commandName;
     }
 
+    @Override
+    public MongoNamespace getNamespace() {
+        return new MongoNamespace(databaseName, COMMAND_COLLECTION_NAME);
+    }
+
     @Override
     public T execute(final ReadBinding binding) {
         return executeRetryableRead(binding, databaseName, commandCreator, decoder,
diff --git a/driver-core/src/main/com/mongodb/internal/operation/CommitTransactionOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CommitTransactionOperation.java
index 998a002f348..97e62ffceac 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/CommitTransactionOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/CommitTransactionOperation.java
@@ -19,6 +19,7 @@
 import com.mongodb.Function;
 import com.mongodb.MongoException;
 import com.mongodb.MongoExecutionTimeoutException;
+import com.mongodb.MongoNamespace;
 import com.mongodb.MongoNodeIsRecoveringException;
 import com.mongodb.MongoNotPrimaryException;
 import com.mongodb.MongoSocketException;
@@ -116,6 +117,11 @@ public String getCommandName() {
         return COMMAND_NAME;
     }
 
+    @Override
+    public MongoNamespace getNamespace() {
+        return new MongoNamespace("admin", MongoNamespace.COMMAND_COLLECTION_NAME);
+    }
+
     @Override
     CommandCreator getCommandCreator() {
         CommandCreator creator = (operationContext, serverDescription, connectionDescription) -> {
diff --git a/driver-core/src/main/com/mongodb/internal/operation/CountDocumentsOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CountDocumentsOperation.java
index 9460026062a..157d3660904 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/CountDocumentsOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/CountDocumentsOperation.java
@@ -125,6 +125,11 @@ public String getCommandName() {
         return COMMAND_NAME;
     }
 
+    @Override
+    public MongoNamespace getNamespace() {
+        return namespace;
+    }
+
     @Override
     public Long execute(final ReadBinding binding) {
         try (BatchCursor cursor = getAggregateOperation().execute(binding)) {
diff --git a/driver-core/src/main/com/mongodb/internal/operation/CountOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CountOperation.java
index 6d0b7b78f93..d38a7c11333 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/CountOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/CountOperation.java
@@ -115,6 +115,11 @@ public String getCommandName() {
         return COMMAND_NAME;
     }
 
+    @Override
+    public MongoNamespace getNamespace() {
+        return namespace;
+    }
+
     @Override
     public Long execute(final ReadBinding binding) {
         return executeRetryableRead(binding, namespace.getDatabaseName(),
diff --git a/driver-core/src/main/com/mongodb/internal/operation/CreateCollectionOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CreateCollectionOperation.java
index 5284076eecb..d8e757054c0 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/CreateCollectionOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/CreateCollectionOperation.java
@@ -18,6 +18,7 @@
 
 import com.mongodb.MongoClientException;
 import com.mongodb.MongoException;
+import com.mongodb.MongoNamespace;
 import com.mongodb.WriteConcern;
 import com.mongodb.client.model.ChangeStreamPreAndPostImagesOptions;
 import com.mongodb.client.model.Collation;
@@ -236,6 +237,11 @@ public String getCommandName() {
         return "createCollection";
     }
 
+    @Override
+    public MongoNamespace getNamespace() {
+        return new MongoNamespace(databaseName, collectionName);
+    }
+
     @Override
     public Void execute(final WriteBinding binding) {
         return withConnection(binding, connection -> {
diff --git a/driver-core/src/main/com/mongodb/internal/operation/CreateIndexesOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CreateIndexesOperation.java
index b9b4242a3f4..7e634f136e2 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/CreateIndexesOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/CreateIndexesOperation.java
@@ -105,6 +105,11 @@ public String getCommandName() {
         return COMMAND_NAME;
     }
 
+    @Override
+    public MongoNamespace getNamespace() {
+        return namespace;
+    }
+
     @Override
     public Void execute(final WriteBinding binding) {
         try {
diff --git a/driver-core/src/main/com/mongodb/internal/operation/CreateViewOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CreateViewOperation.java
index 49b47fb7e9c..61fd58d5a0f 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/CreateViewOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/CreateViewOperation.java
@@ -16,6 +16,7 @@
 
 package com.mongodb.internal.operation;
 
+import com.mongodb.MongoNamespace;
 import com.mongodb.WriteConcern;
 import com.mongodb.client.model.Collation;
 import com.mongodb.internal.async.SingleResultCallback;
@@ -128,6 +129,11 @@ public String getCommandName() {
         return "createView";
     }
 
+    @Override
+    public MongoNamespace getNamespace() {
+        return new MongoNamespace(databaseName, viewName);
+    }
+
     @Override
     public Void execute(final WriteBinding binding) {
         return withConnection(binding, connection -> {
diff --git a/driver-core/src/main/com/mongodb/internal/operation/DistinctOperation.java b/driver-core/src/main/com/mongodb/internal/operation/DistinctOperation.java
index 489e3923bdc..10c4c320100 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/DistinctOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/DistinctOperation.java
@@ -113,6 +113,11 @@ public String getCommandName() {
         return COMMAND_NAME;
     }
 
+    @Override
+    public MongoNamespace getNamespace() {
+        return namespace;
+    }
+
     @Override
     public BatchCursor execute(final ReadBinding binding) {
         return executeRetryableRead(binding, namespace.getDatabaseName(), getCommandCreator(), createCommandDecoder(),
diff --git a/driver-core/src/main/com/mongodb/internal/operation/DropCollectionOperation.java b/driver-core/src/main/com/mongodb/internal/operation/DropCollectionOperation.java
index 5f61f2980f8..6bdbfa7bbcd 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/DropCollectionOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/DropCollectionOperation.java
@@ -91,6 +91,11 @@ public String getCommandName() {
         return "dropCollection";
     }
 
+    @Override
+    public MongoNamespace getNamespace() {
+        return namespace;
+    }
+
     @Override
     public Void execute(final WriteBinding binding) {
         BsonDocument localEncryptedFields = getEncryptedFields((ReadWriteBinding) binding);
diff --git a/driver-core/src/main/com/mongodb/internal/operation/DropDatabaseOperation.java b/driver-core/src/main/com/mongodb/internal/operation/DropDatabaseOperation.java
index d619176e8a3..2ee963923fe 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/DropDatabaseOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/DropDatabaseOperation.java
@@ -16,6 +16,7 @@
 
 package com.mongodb.internal.operation;
 
+import com.mongodb.MongoNamespace;
 import com.mongodb.WriteConcern;
 import com.mongodb.internal.async.SingleResultCallback;
 import com.mongodb.internal.binding.AsyncWriteBinding;
@@ -60,6 +61,11 @@ public String getCommandName() {
         return "dropDatabase";
     }
 
+    @Override
+    public MongoNamespace getNamespace() {
+        return new MongoNamespace(databaseName, MongoNamespace.COMMAND_COLLECTION_NAME);
+    }
+
     @Override
     public Void execute(final WriteBinding binding) {
         return withConnection(binding, connection -> {
diff --git a/driver-core/src/main/com/mongodb/internal/operation/DropIndexOperation.java b/driver-core/src/main/com/mongodb/internal/operation/DropIndexOperation.java
index 3671a90aa56..8a3a66e3c50 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/DropIndexOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/DropIndexOperation.java
@@ -70,6 +70,11 @@ public String getCommandName() {
         return COMMAND_NAME;
     }
 
+    @Override
+    public MongoNamespace getNamespace() {
+        return namespace;
+    }
+
     @Override
     public Void execute(final WriteBinding binding) {
         try {
diff --git a/driver-core/src/main/com/mongodb/internal/operation/EstimatedDocumentCountOperation.java b/driver-core/src/main/com/mongodb/internal/operation/EstimatedDocumentCountOperation.java
index 427cd40dc40..6308aae56ea 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/EstimatedDocumentCountOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/EstimatedDocumentCountOperation.java
@@ -75,6 +75,11 @@ public String getCommandName() {
         return COMMAND_NAME;
     }
 
+    @Override
+    public MongoNamespace getNamespace() {
+        return namespace;
+    }
+
     @Override
     public Long execute(final ReadBinding binding) {
         try {
diff --git a/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java b/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java
index 2e6560e23c3..b18fc4ca1e0 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java
@@ -99,6 +99,7 @@ public FindOperation(final MongoNamespace namespace, final Decoder decoder) {
         this.decoder = notNull("decoder", decoder);
     }
 
+    @Override
     public MongoNamespace getNamespace() {
         return namespace;
     }
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-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);

From 1a75290ed1a90521b6fa6900eb96f078dc2d4544 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha 
Date: Sun, 10 Aug 2025 18:56:30 +0100
Subject: [PATCH 06/14] Refactoring operations and command spans

---
 config/checkstyle/suppressions.xml            |  1 +
 .../connection/InternalStreamConnection.java  | 42 +++++++++++++------
 .../internal/connection/OperationContext.java | 24 +++++------
 .../com/mongodb/internal/tracing/Tags.java    |  9 +++-
 .../com/mongodb/tracing/MicrometerTracer.java | 17 ++++++++
 driver-core/src/test/resources/specifications |  2 +-
 .../LegacyMixedBulkWriteOperation.java        |  5 +++
 .../internal/MapReducePublisherImpl.java      | 10 +++++
 ...dReadOperationThenCursorReadOperation.java |  6 +++
 ...WriteOperationThenCursorReadOperation.java |  6 +++
 .../client/internal/MongoClusterImpl.java     | 23 ++++++----
 .../com/mongodb/client/unified/Entities.java  |  6 +--
 .../client/unified/MicrometerTracingTest.java |  2 +-
 .../mongodb/client/unified/UnifiedTest.java   | 24 ++++++++++-
 14 files changed, 138 insertions(+), 39 deletions(-)

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/src/main/com/mongodb/internal/connection/InternalStreamConnection.java b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java
index 56bcc48ce34..535c2fb2255 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java
@@ -30,6 +30,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;
@@ -97,8 +98,11 @@
 import static com.mongodb.internal.logging.LogMessage.Level.DEBUG;
 import static com.mongodb.internal.thread.InterruptionUtil.translateInterruptedException;
 import static com.mongodb.internal.tracing.Tags.CLIENT_CONNECTION_ID;
+import static com.mongodb.internal.tracing.Tags.COLLECTION;
+import static com.mongodb.internal.tracing.Tags.COMMAND_NAME;
 import static com.mongodb.internal.tracing.Tags.CURSOR_ID;
 import static com.mongodb.internal.tracing.Tags.NAMESPACE;
+import static com.mongodb.internal.tracing.Tags.NETWORK_TRANSPORT;
 import static com.mongodb.internal.tracing.Tags.QUERY_SUMMARY;
 import static com.mongodb.internal.tracing.Tags.QUERY_TEXT;
 import static com.mongodb.internal.tracing.Tags.SERVER_ADDRESS;
@@ -446,10 +450,10 @@ 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);
-            Span tracingSpan = createTracingSpan(message, operationContext, bsonOutput);
+            tracingSpan = createTracingSpan(message, operationContext, bsonOutput);
 
             boolean isLoggingCommandNeeded = isLoggingCommandNeeded();
             boolean isTracingCommandPayloadNeeded = tracingSpan != null && operationContext.getTracingManager().isCommandPayloadEnabled();
@@ -480,17 +484,16 @@ private  T sendAndReceiveInternal(final CommandMessage message, final Decoder
                 }
                 commandEventSender.sendFailedEvent(e);
                 throw e;
-            } finally {
-                if (tracingSpan != null) {
-                    tracingSpan.end();
-                }
             }
         }
 
         if (message.isResponseExpected()) {
-            return receiveCommandMessageResponse(decoder, commandEventSender, operationContext);
+            return receiveCommandMessageResponse(decoder, commandEventSender, operationContext, tracingSpan);
         } else {
             commandEventSender.sendSucceededEventForOneWayCommand();
+            if (tracingSpan != null) {
+                tracingSpan.end();
+            }
             return null;
         }
     }
@@ -509,7 +512,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
@@ -555,7 +558,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);
@@ -580,7 +583,14 @@ private  T receiveCommandMessageResponse(final Decoder decoder, final Comm
             if (!commandSuccessful) {
                 commandEventSender.sendFailedEvent(e);
             }
+            if (tracingSpan != null) {
+                tracingSpan.error(e);
+            }
             throw e;
+        } finally {
+            if (tracingSpan != null) {
+                tracingSpan.end();
+            }
         }
     }
 
@@ -1043,14 +1053,22 @@ private Span createTracingSpan(final CommandMessage message, final OperationCont
             return null;
         }
 
+         Span operationSpan = operationContext.getTracingSpan();
          Span span = tracingManager
-                .addSpan("Command " + commandName, operationContext.getTracingSpanContext())
+                .addSpan("Command " + commandName,  operationSpan != null ? operationSpan.context() : null)
                 .tag(SYSTEM, "mongodb")
                 .tag(NAMESPACE, message.getNamespace().getDatabaseName())
-                .tag(QUERY_SUMMARY, commandName);
+                .tag(COLLECTION, message.getCollectionName())
+                .tag(QUERY_SUMMARY, commandName)
+                .tag(COMMAND_NAME, commandName)
+                .tag(NETWORK_TRANSPORT, getServerAddress() instanceof UnixServerAddress ? "unix" : "tcp");
 
         if (command.containsKey("getMore")) {
-            span.tag(CURSOR_ID, command.getInt64("getMore").longValue());
+            long cursorId = command.getInt64("getMore").longValue();
+            span.tag(CURSOR_ID, cursorId);
+            if (operationSpan != null) {
+                operationSpan.tag(CURSOR_ID, cursorId);
+            }
         }
 
         tagServerAndConnectionInfo(span, message);
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 9f1c232b274..bc4a785d545 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/OperationContext.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/OperationContext.java
@@ -27,7 +27,7 @@
 import com.mongodb.internal.TimeoutSettings;
 import com.mongodb.internal.VisibleForTesting;
 import com.mongodb.internal.session.SessionContext;
-import com.mongodb.internal.tracing.TraceContext;
+import com.mongodb.internal.tracing.Span;
 import com.mongodb.internal.tracing.TracingManager;
 import com.mongodb.lang.Nullable;
 import com.mongodb.selector.ServerSelector;
@@ -55,7 +55,7 @@ public class OperationContext {
     @Nullable
     private final String operationName;
     @Nullable
-    private TraceContext tracingContext;
+    private Span tracingSpan;
 
     public OperationContext(final RequestContext requestContext, final SessionContext sessionContext, final TimeoutContext timeoutContext,
             @Nullable final ServerApi serverApi) {
@@ -97,17 +97,17 @@ public static OperationContext simpleOperationContext(final TimeoutContext timeo
 
     public OperationContext withSessionContext(final SessionContext sessionContext) {
         return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, tracingManager, serverApi,
-                operationName, tracingContext);
+                operationName, tracingSpan);
     }
 
     public OperationContext withTimeoutContext(final TimeoutContext timeoutContext) {
         return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, tracingManager, serverApi,
-                operationName, tracingContext);
+                operationName, tracingSpan);
     }
 
     public OperationContext withOperationName(final String operationName) {
         return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, tracingManager, serverApi,
-                operationName, tracingContext);
+                operationName, tracingSpan);
     }
 
     public long getId() {
@@ -141,12 +141,12 @@ public String getOperationName() {
     }
 
     @Nullable
-    public TraceContext getTracingSpanContext() {
-        return tracingContext != null ? tracingContext : null;
+    public Span getTracingSpan() {
+        return tracingSpan;
     }
 
-    public void setTracingContext(final TraceContext tracingContext) {
-        this.tracingContext = tracingContext;
+    public void setTracingSpan(final Span tracingSpan) {
+        this.tracingSpan = tracingSpan;
     }
 
     @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE)
@@ -158,7 +158,7 @@ public OperationContext(final long id,
             final TracingManager tracingManager,
             @Nullable final ServerApi serverApi,
             @Nullable final String operationName,
-            @Nullable final TraceContext tracingContext) {
+            @Nullable final Span tracingSpan) {
 
         this.id = id;
         this.serverDeprioritization = serverDeprioritization;
@@ -168,7 +168,7 @@ public OperationContext(final long id,
         this.tracingManager = tracingManager;
         this.serverApi = serverApi;
         this.operationName = operationName;
-        this.tracingContext = tracingContext;
+        this.tracingSpan = tracingSpan;
     }
 
     @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE)
@@ -187,7 +187,7 @@ public OperationContext(final long id,
         this.tracingManager = tracingManager;
         this.serverApi = serverApi;
         this.operationName = operationName;
-        this.tracingContext = null;
+        this.tracingSpan = null;
     }
 
 
diff --git a/driver-core/src/main/com/mongodb/internal/tracing/Tags.java b/driver-core/src/main/com/mongodb/internal/tracing/Tags.java
index e4407f2a401..c7261a63af4 100644
--- a/driver-core/src/main/com/mongodb/internal/tracing/Tags.java
+++ b/driver-core/src/main/com/mongodb/internal/tracing/Tags.java
@@ -29,14 +29,21 @@ private Tags() {
     public static final String SYSTEM = "db.system";
     public static final String NAMESPACE = "db.namespace";
     public static final String COLLECTION = "db.collection.name";
+    public static final String OPERATION_NAME = "db.operation.name";
+    public static final String COMMAND_NAME = "db.command.name";
+    public static final String NETWORK_TRANSPORT = "network.transport";
+    public static final String OPERATION_SUMMARY = "db.operation.summary";
     public static final String QUERY_SUMMARY = "db.query.summary";
     public static final String QUERY_TEXT = "db.query.text";
     public static final String CURSOR_ID = "db.mongodb.cursor_id";
     public static final String SERVER_ADDRESS = "server.address";
     public static final String SERVER_PORT = "server.port";
     public static final String SERVER_TYPE = "server.type";
-    public static final String CLIENT_CONNECTION_ID = "db.mongodb.client_connection_id";
+    public static final String CLIENT_CONNECTION_ID = "db.mongodb.driver_connection_id";
     public static final String SERVER_CONNECTION_ID = "db.mongodb.server_connection_id";
     public static final String TRANSACTION_NUMBER = "db.mongodb.txnNumber";
     public static final String SESSION_ID = "db.mongodb.lsid";
+    public static final String EXCEPTION_STACKTRACE = "exception.stacktrace";
+    public static final String EXCEPTION_TYPE = "exception.type";
+    public static final String EXCEPTION_MESSAGE = "exception.message";
 }
diff --git a/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java b/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java
index ed678b93cab..7ed0119d21d 100644
--- a/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java
+++ b/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java
@@ -21,6 +21,13 @@
 import com.mongodb.internal.tracing.Tracer;
 import com.mongodb.lang.Nullable;
 
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import static com.mongodb.internal.tracing.Tags.EXCEPTION_MESSAGE;
+import static com.mongodb.internal.tracing.Tags.EXCEPTION_STACKTRACE;
+import static com.mongodb.internal.tracing.Tags.EXCEPTION_TYPE;
+
 /**
  * A {@link Tracer} implementation that delegates tracing operations to a Micrometer {@link io.micrometer.tracing.Tracer}.
  * 

@@ -148,6 +155,9 @@ public void event(final String event) { @Override public void error(final Throwable throwable) { + span.tag(EXCEPTION_MESSAGE, throwable.getMessage()); + span.tag(EXCEPTION_TYPE, throwable.getClass().getName()); + span.tag(EXCEPTION_STACKTRACE, getStackTraceAsString(throwable)); span.error(throwable); } @@ -160,5 +170,12 @@ public void end() { public TraceContext context() { return new MicrometerTraceContext(span.context()); } + + private String getStackTraceAsString(final Throwable throwable) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + throwable.printStackTrace(pw); + return sw.toString(); + } } } diff --git a/driver-core/src/test/resources/specifications b/driver-core/src/test/resources/specifications index 61270d61656..8466c1b9df5 160000 --- a/driver-core/src/test/resources/specifications +++ b/driver-core/src/test/resources/specifications @@ -1 +1 @@ -Subproject commit 61270d61656709944e6b75e160453e3bfa658483 +Subproject commit 8466c1b9df5b2c4b293bb9223b55aa16d34e9e0f 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/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/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-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java b/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java index 37f911306fc..db8fd03adaf 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java +++ b/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java @@ -22,6 +22,7 @@ import com.mongodb.MongoClientException; import com.mongodb.MongoException; import com.mongodb.MongoInternalException; +import com.mongodb.MongoNamespace; import com.mongodb.MongoQueryException; import com.mongodb.MongoSocketException; import com.mongodb.MongoTimeoutException; @@ -428,7 +429,7 @@ 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()); + Span span = createOperationSpan(actualClientSession, binding, operation.getCommandName(), operation.getNamespace()); try { if (actualClientSession.hasActiveTransaction() && !binding.getReadPreference().equals(primary())) { @@ -461,7 +462,7 @@ 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()); + Span span = createOperationSpan(actualClientSession, binding, operation.getCommandName(), operation.getNamespace()); try { return operation.execute(binding); } catch (MongoException e) { @@ -586,11 +587,13 @@ ClientSession getClientSession(@Nullable final ClientSession clientSessionFromOp * 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 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) { + 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; @@ -598,13 +601,17 @@ private Span createOperationSpan(final ClientSession actualClientSession, final if (transactionSpan != null) { parentContext = transactionSpan.getContext(); } - + String name = commandName + " " + namespace.getFullName(); Span span = binding .getOperationContext() .getTracingManager() - .addSpan(commandName, parentContext); - binding.getOperationContext().setTracingContext(span.context()); + .addSpan(name, parentContext); + binding.getOperationContext().setTracingSpan(span); span.tag(Tags.SYSTEM, "mongodb"); + span.tag(Tags.NAMESPACE, namespace.getDatabaseName()); + span.tag(Tags.COLLECTION, namespace.getCollectionName()); + span.tag(Tags.OPERATION_NAME, commandName); + span.tag(Tags.OPERATION_SUMMARY, name); return span; } else { 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 4dfedf0a8d5..c0d33b06d91 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java @@ -115,7 +115,7 @@ public final class Entities { private static final Set SUPPORTED_CLIENT_ENTITY_OPTIONS = new HashSet<>( asList( "id", "uriOptions", "serverApi", "useMultipleMongoses", "storeEventsAsEntities", - "observeEvents", "observeLogMessages", "observeSensitiveCommands", "ignoreCommandMonitoringEvents", "tracing")); + "observeEvents", "observeLogMessages", "observeSensitiveCommands", "ignoreCommandMonitoringEvents", "observeTracingMessages")); private final Set entityNames = new HashSet<>(); private final Map threads = new HashMap<>(); private final Map>> tasks = new HashMap<>(); @@ -613,8 +613,8 @@ private void initClient(final BsonDocument entity, final String id, clientSettingsBuilder.serverApi(serverApiBuilder.build()); } - if (entity.containsKey("tracing")) { - boolean enableCommandPayload = entity.getDocument("tracing").get("enableCommandPayload", BsonBoolean.FALSE).asBoolean().getValue(); + if (entity.containsKey("observeTracingMessages")) { + boolean enableCommandPayload = entity.getDocument("observeTracingMessages").get("enableCommandPayload", BsonBoolean.FALSE).asBoolean().getValue(); /* To enable Zipkin backend, uncomment the following lines and ensure you have the server started (docker run -d -p 9411:9411 openzipkin/zipkin). The tests will fail but the captured spans will be visible in the Zipkin UI at http://localhost:9411 for debugging purpose. 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 index 84bc0734c5f..8c65317d257 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/MicrometerTracingTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/MicrometerTracingTest.java @@ -22,6 +22,6 @@ final class MicrometerTracingTest extends UnifiedSyncTest { private static Collection data() { - return getTestData("tracing/tests"); + return getTestData("open-telemetry/tests"); } } diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java index e067e36d993..8e91e85205b 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java @@ -28,6 +28,7 @@ import com.mongodb.client.gridfs.GridFSBucket; import com.mongodb.client.model.Filters; import com.mongodb.client.test.CollectionHelper; +import com.mongodb.client.tracing.SpanTree; import com.mongodb.client.unified.UnifiedTestModifications.TestDef; import com.mongodb.client.vault.ClientEncryption; import com.mongodb.connection.ClusterDescription; @@ -44,6 +45,8 @@ import com.mongodb.lang.Nullable; import com.mongodb.logging.TestLoggingInterceptor; import com.mongodb.test.AfterBeforeParameterResolver; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.test.simple.SimpleTracer; import org.bson.BsonArray; import org.bson.BsonBoolean; import org.bson.BsonDocument; @@ -111,7 +114,7 @@ public abstract class UnifiedTest { private static final Set PRESTART_POOL_ASYNC_WORK_MANAGER_FILE_DESCRIPTIONS = Collections.singleton( "wait queue timeout errors include details about checked out connections"); - private static final String MAX_SUPPORTED_SCHEMA_VERSION = "1.22"; + private static final String MAX_SUPPORTED_SCHEMA_VERSION = "1.26"; private static final List MAX_SUPPORTED_SCHEMA_VERSION_COMPONENTS = Arrays.stream(MAX_SUPPORTED_SCHEMA_VERSION.split("\\.")) .map(Integer::parseInt) .collect(Collectors.toList()); @@ -380,6 +383,11 @@ public void shouldPassAllOutcomes( } compareLogMessages(rootContext, definition, tweaks); } + + if (definition.containsKey("expectTracingMessages")) { + compareTracingSpans(definition); + } + } catch (TestAbortedException e) { // if a test is ignored, we do not retry throw e; @@ -487,6 +495,20 @@ private void compareLogMessages(final UnifiedTestContext rootContext, final Bson } } + private void compareTracingSpans(final BsonDocument definition) { + BsonDocument curTracingSpansForClient = definition.getDocument("expectTracingMessages"); + String clientId = curTracingSpansForClient.getString("client").getValue(); + + // Get the tracer for the client + Tracer micrometerTracer = entities.getClientTracer(clientId); + SimpleTracer simpleTracer = (SimpleTracer) micrometerTracer; + + SpanTree expectedSpans = SpanTree.from(curTracingSpansForClient.getArray("spans")); + SpanTree reportedSpans = SpanTree.from(simpleTracer.getSpans()); + boolean ignoreExtraSpans = curTracingSpansForClient.getBoolean("ignoreExtraSpans", BsonBoolean.TRUE).getValue(); + SpanTree.assertValid(reportedSpans, expectedSpans, rootContext.valueMatcher::assertValuesMatch, ignoreExtraSpans); + } + private void assertOutcome(final UnifiedTestContext context) { for (BsonValue cur : definition.getArray("outcome")) { BsonDocument curDocument = cur.asDocument(); From 26cc57a2bdc953bbeb4f5433cdf1a506aad51a91 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Thu, 2 Oct 2025 14:17:28 +0100 Subject: [PATCH 07/14] Refactor the tracing implementation to use Observation instead of working with micrometer-tracing directly --- driver-core/build.gradle.kts | 2 +- .../connection/InternalStreamConnection.java | 104 ++++++---- .../operation/ClientBulkWriteOperation.java | 4 +- .../com/mongodb/internal/tracing/Span.java | 53 +++-- .../com/mongodb/internal/tracing/Tags.java | 49 ----- .../com/mongodb/internal/tracing/Tracer.java | 38 +--- .../internal/tracing/TracingManager.java | 41 ++-- .../internal/tracing/TransactionSpan.java | 2 + .../com/mongodb/tracing/MicrometerTracer.java | 141 +++++++------ .../mongodb/tracing/MongodbObservation.java | 185 ++++++++++++++++++ .../com/mongodb/tracing/package-info.java | 2 +- driver-kotlin-coroutine/build.gradle.kts | 2 +- driver-kotlin-sync/build.gradle.kts | 2 +- driver-reactive-streams/build.gradle.kts | 2 +- driver-sync/build.gradle.kts | 5 +- .../client/internal/MongoClusterImpl.java | 32 ++- .../com/mongodb/client/tracing/SpanTree.java | 68 ++++++- .../mongodb/client/tracing/ZipkinTracer.java | 95 --------- .../com/mongodb/client/unified/Entities.java | 26 ++- .../mongodb/client/unified/UnifiedTest.java | 29 +-- gradle/libs.versions.toml | 18 +- 21 files changed, 528 insertions(+), 372 deletions(-) delete mode 100644 driver-core/src/main/com/mongodb/internal/tracing/Tags.java create mode 100644 driver-core/src/main/com/mongodb/tracing/MongodbObservation.java delete mode 100644 driver-sync/src/test/functional/com/mongodb/client/tracing/ZipkinTracer.java diff --git a/driver-core/build.gradle.kts b/driver-core/build.gradle.kts index 7e260b18d23..1614d9fb8d4 100644 --- a/driver-core/build.gradle.kts +++ b/driver-core/build.gradle.kts @@ -54,7 +54,7 @@ dependencies { optionalImplementation(libs.snappy.java) optionalImplementation(libs.zstd.jni) - optionalImplementation(libs.micrometer) + optionalImplementation(libs.micrometer.observation) testImplementation(project(path = ":bson", configuration = "testArtifacts")) testImplementation(libs.reflections) 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 535c2fb2255..d4104462c8c 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; @@ -55,6 +56,7 @@ import com.mongodb.internal.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; @@ -97,21 +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.internal.tracing.Tags.CLIENT_CONNECTION_ID; -import static com.mongodb.internal.tracing.Tags.COLLECTION; -import static com.mongodb.internal.tracing.Tags.COMMAND_NAME; -import static com.mongodb.internal.tracing.Tags.CURSOR_ID; -import static com.mongodb.internal.tracing.Tags.NAMESPACE; -import static com.mongodb.internal.tracing.Tags.NETWORK_TRANSPORT; -import static com.mongodb.internal.tracing.Tags.QUERY_SUMMARY; -import static com.mongodb.internal.tracing.Tags.QUERY_TEXT; -import static com.mongodb.internal.tracing.Tags.SERVER_ADDRESS; -import static com.mongodb.internal.tracing.Tags.SERVER_CONNECTION_ID; -import static com.mongodb.internal.tracing.Tags.SERVER_PORT; -import static com.mongodb.internal.tracing.Tags.SERVER_TYPE; -import static com.mongodb.internal.tracing.Tags.SESSION_ID; -import static com.mongodb.internal.tracing.Tags.SYSTEM; -import static com.mongodb.internal.tracing.Tags.TRANSACTION_NUMBER; +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; /** @@ -473,7 +476,7 @@ private T sendAndReceiveInternal(final CommandMessage message, final Decoder commandEventSender = new NoOpCommandEventSender(); } if (isTracingCommandPayloadNeeded) { - tracingSpan.tag(QUERY_TEXT, commandDocument.toJson()); + tracingSpan.tagHighCardinality(QUERY_TEXT.withValue(commandDocument.toJson())); } try { @@ -584,6 +587,9 @@ private T receiveCommandMessageResponse(final Decoder decoder, final Comm 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; @@ -1055,43 +1061,75 @@ private Span createTracingSpan(final CommandMessage message, final OperationCont Span operationSpan = operationContext.getTracingSpan(); Span span = tracingManager - .addSpan("Command " + commandName, operationSpan != null ? operationSpan.context() : null) - .tag(SYSTEM, "mongodb") - .tag(NAMESPACE, message.getNamespace().getDatabaseName()) - .tag(COLLECTION, message.getCollectionName()) - .tag(QUERY_SUMMARY, commandName) - .tag(COMMAND_NAME, commandName) - .tag(NETWORK_TRANSPORT, getServerAddress() instanceof UnixServerAddress ? "unix" : "tcp"); + .addSpan(commandName, operationSpan != null ? operationSpan.context() : null); if (command.containsKey("getMore")) { long cursorId = command.getInt64("getMore").longValue(); - span.tag(CURSOR_ID, cursorId); + span.tagLowCardinality(CURSOR_ID.withValue(String.valueOf(cursorId))); if (operationSpan != null) { - operationSpan.tag(CURSOR_ID, cursorId); + 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.getNamespace().getDatabaseName(); + collection = message.getCollectionName().contains("$cmd") ? "" : message.getCollectionName(); + } + } else { + namespace = message.getNamespace().getDatabaseName(); + collection = message.getCollectionName().contains("$cmd") ? "" : message.getCollectionName(); + } + 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.tag(SERVER_ADDRESS, serverId.getAddress().getHost()) - .tag(SERVER_PORT, String.valueOf(serverId.getAddress().getPort())) - .tag(SERVER_TYPE, message.getSettings().getServerType().name()) - .tag(CLIENT_CONNECTION_ID, this.description.getConnectionId().toString()) - .tag(SERVER_CONNECTION_ID, String.valueOf(this.description.getConnectionId().getServerValue())); + 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.tag(TRANSACTION_NUMBER, String.valueOf(sessionContext.getTransactionNumber())) - .tag(SESSION_ID, String.valueOf(sessionContext.getSessionId() + span.tagLowCardinality(KeyValues.of( + TRANSACTION_NUMBER.withValue(String.valueOf(sessionContext.getTransactionNumber())), + SESSION_ID.withValue(String.valueOf(sessionContext.getSessionId() .get(sessionContext.getSessionId().getFirstKey()) - .asBinary().asUuid())); + .asBinary().asUuid())) + )); } } } 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 ad380781f73..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; @@ -184,7 +185,8 @@ public String getCommandName() { @Override public MongoNamespace getNamespace() { - return getNamespacedModel(models, 0).getNamespace(); + // The bulkWrite command is executed on the "admin" database. + return new MongoNamespace("admin", COMMAND_COLLECTION_NAME); } @Override diff --git a/driver-core/src/main/com/mongodb/internal/tracing/Span.java b/driver-core/src/main/com/mongodb/internal/tracing/Span.java index 41f6ef0afdf..0549a194c6c 100644 --- a/driver-core/src/main/com/mongodb/internal/tracing/Span.java +++ b/driver-core/src/main/com/mongodb/internal/tracing/Span.java @@ -16,6 +16,10 @@ package com.mongodb.internal.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. @@ -32,7 +36,7 @@ * *

* - * @since 5.6 + * @since 5.7 */ public interface Span { /** @@ -44,13 +48,15 @@ public interface Span { */ Span EMPTY = new Span() { @Override - public Span tag(final String key, final String value) { - return this; + public void tagLowCardinality(final KeyValue tag) { } @Override - public Span tag(final String key, final Long value) { - return this; + public void tagLowCardinality(final KeyValues keyValues) { + } + + @Override + public void tagHighCardinality(final KeyValue keyValue) { } @Override @@ -69,25 +75,34 @@ public void end() { public TraceContext context() { return TraceContext.EMPTY; } + + @Override + @Nullable + public MongoNamespace getNamespace() { + return null; + } }; /** - * Adds a tag to the span with a key-value pair. + * 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 key The tag key. - * @param value The tag value. - * @return The current instance of the span. + * @param keyValues The key-value pairs representing the tags. */ - Span tag(String key, String value); + void tagLowCardinality(KeyValues keyValues); /** - * Adds a tag to the span with a key and a numeric value. + * Adds a high-cardinality (highly variable values) tag to the span. * - * @param key The tag key. - * @param value The numeric tag value. - * @return The current instance of the span. + * @param keyValue The key-value pair representing the tag. */ - Span tag(String key, Long value); + void tagHighCardinality(KeyValue keyValue); /** * Records an event in the span. @@ -114,4 +129,12 @@ public TraceContext context() { * @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/internal/tracing/Tags.java b/driver-core/src/main/com/mongodb/internal/tracing/Tags.java deleted file mode 100644 index c7261a63af4..00000000000 --- a/driver-core/src/main/com/mongodb/internal/tracing/Tags.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2008-present MongoDB, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.mongodb.internal.tracing; - -/** - * Contains constant tag names used for tracing and monitoring MongoDB operations. - * These tags are typically used to annotate spans or events with relevant metadata. - * - * @since 5.6 - */ -public final class Tags { - private Tags() { - } - - public static final String SYSTEM = "db.system"; - public static final String NAMESPACE = "db.namespace"; - public static final String COLLECTION = "db.collection.name"; - public static final String OPERATION_NAME = "db.operation.name"; - public static final String COMMAND_NAME = "db.command.name"; - public static final String NETWORK_TRANSPORT = "network.transport"; - public static final String OPERATION_SUMMARY = "db.operation.summary"; - public static final String QUERY_SUMMARY = "db.query.summary"; - public static final String QUERY_TEXT = "db.query.text"; - public static final String CURSOR_ID = "db.mongodb.cursor_id"; - public static final String SERVER_ADDRESS = "server.address"; - public static final String SERVER_PORT = "server.port"; - public static final String SERVER_TYPE = "server.type"; - public static final String CLIENT_CONNECTION_ID = "db.mongodb.driver_connection_id"; - public static final String SERVER_CONNECTION_ID = "db.mongodb.server_connection_id"; - public static final String TRANSACTION_NUMBER = "db.mongodb.txnNumber"; - public static final String SESSION_ID = "db.mongodb.lsid"; - public static final String EXCEPTION_STACKTRACE = "exception.stacktrace"; - public static final String EXCEPTION_TYPE = "exception.type"; - public static final String EXCEPTION_MESSAGE = "exception.message"; -} diff --git a/driver-core/src/main/com/mongodb/internal/tracing/Tracer.java b/driver-core/src/main/com/mongodb/internal/tracing/Tracer.java index c2881e9e2fb..6f5403213d6 100644 --- a/driver-core/src/main/com/mongodb/internal/tracing/Tracer.java +++ b/driver-core/src/main/com/mongodb/internal/tracing/Tracer.java @@ -13,9 +13,9 @@ * 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; /** @@ -25,23 +25,12 @@ * It also includes a no-operation (NO_OP) implementation for cases where tracing is not required. *

* - * @since 5.6 + * @since 5.7 */ public interface Tracer { Tracer NO_OP = new Tracer() { - - @Override - public TraceContext currentContext() { - return TraceContext.EMPTY; - } - - @Override - public Span nextSpan(final String name) { - return Span.EMPTY; - } - @Override - public Span nextSpan(final String name, @Nullable final TraceContext parent) { + public Span nextSpan(final String name, @Nullable final TraceContext parent, @Nullable final MongoNamespace namespace) { return Span.EMPTY; } @@ -56,30 +45,15 @@ public boolean includeCommandPayload() { } }; - /** - * Retrieves the current trace context from the Micrometer tracer. - * - * @return A {@link TraceContext} representing the underlying {@link io.micrometer.tracing.TraceContext}. - * exists. - */ - TraceContext currentContext(); - - /** - * Creates a new span with the specified name. - * - * @param name The name of the span. - * @return A {@link Span} representing the newly created span. - */ - Span nextSpan(String name); // uses current active span - /** * Creates a new span with the specified name and optional parent trace context. * * @param name The name of the span. * @param parent The parent {@link TraceContext}, or null if no parent context is provided. + * @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); // manually attach the next span to the provided parent + Span nextSpan(String name, @Nullable TraceContext parent, @Nullable MongoNamespace namespace); /** * Indicates whether tracing is enabled. @@ -93,5 +67,5 @@ public boolean includeCommandPayload() { * * @return {@code true} if command payloads are allowed, {@code false} otherwise. */ - boolean includeCommandPayload(); // whether the tracer allows command payloads in the trace context + boolean includeCommandPayload(); } diff --git a/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java b/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java index 6db92f0d75a..6fa5613ef47 100644 --- a/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java +++ b/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java @@ -16,9 +16,10 @@ package com.mongodb.internal.tracing; +import com.mongodb.MongoNamespace; import com.mongodb.lang.Nullable; -import static com.mongodb.internal.tracing.Tags.SYSTEM; +import static com.mongodb.tracing.MongodbObservation.LowCardinalityKeyNames.SYSTEM; import static java.lang.System.getenv; /** @@ -34,30 +35,17 @@ public class TracingManager { */ public static final TracingManager NO_OP = new TracingManager(Tracer.NO_OP); private static final String ENV_ALLOW_COMMAND_PAYLOAD = "MONGODB_TRACING_ALLOW_COMMAND_PAYLOAD"; - private final Tracer tracer; - private final TraceContext parentContext; private final boolean enableCommandPayload; - /** - * Constructs a new TracingManager with the specified tracer. - * - * @param tracer The tracer to use for tracing operations. - */ - public TracingManager(final Tracer tracer) { - this(tracer, tracer.currentContext()); - } - /** * Constructs a new TracingManager with the specified tracer and parent context. * Setting the environment variable {@code MONGODB_TRACING_ALLOW_COMMAND_PAYLOAD} to "true" will enable command payload tracing. * * @param tracer The tracer to use for tracing operations. - * @param parentContext The parent trace context. */ - public TracingManager(final Tracer tracer, final TraceContext parentContext) { + public TracingManager(final Tracer tracer) { this.tracer = tracer; - this.parentContext = parentContext; String envAllowCommandPayload = getenv(ENV_ALLOW_COMMAND_PAYLOAD); if (envAllowCommandPayload != null) { this.enableCommandPayload = Boolean.parseBoolean(envAllowCommandPayload); @@ -78,7 +66,24 @@ public TracingManager(final Tracer tracer, final TraceContext parentContext) { * @return The created span. */ public Span addSpan(final String name, @Nullable final TraceContext parentContext) { - return tracer.nextSpan(name, 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); } /** @@ -87,8 +92,8 @@ public Span addSpan(final String name, @Nullable final TraceContext parentContex * @return The created transaction span. */ public Span addTransactionSpan() { - Span span = tracer.nextSpan("transaction", parentContext); - span.tag(SYSTEM, "mongodb"); + Span span = tracer.nextSpan("transaction", null, null); + span.tagLowCardinality(SYSTEM.withValue("mongodb")); return span; } diff --git a/driver-core/src/main/com/mongodb/internal/tracing/TransactionSpan.java b/driver-core/src/main/com/mongodb/internal/tracing/TransactionSpan.java index d975f6931e0..789f259e82c 100644 --- a/driver-core/src/main/com/mongodb/internal/tracing/TransactionSpan.java +++ b/driver-core/src/main/com/mongodb/internal/tracing/TransactionSpan.java @@ -20,6 +20,8 @@ /** * State class for transaction tracing. + * + * @since 5.7 */ public class TransactionSpan { private boolean isConvenientTransaction = false; diff --git a/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java b/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java index 7ed0119d21d..ae5a708ef96 100644 --- a/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java +++ b/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java @@ -16,73 +16,71 @@ package com.mongodb.tracing; +import com.mongodb.MongoNamespace; import com.mongodb.internal.tracing.Span; import com.mongodb.internal.tracing.TraceContext; import com.mongodb.internal.tracing.Tracer; 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.internal.tracing.Tags.EXCEPTION_MESSAGE; -import static com.mongodb.internal.tracing.Tags.EXCEPTION_STACKTRACE; -import static com.mongodb.internal.tracing.Tags.EXCEPTION_TYPE; +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.tracing.Tracer}. + * 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 methods to create and manage spans using the Micrometer tracing API. + * It provides integration with Micrometer to propagate observations into tracing API. *

* - * @since 5.6 + * @since 5.7 */ public class MicrometerTracer implements Tracer { - private final io.micrometer.tracing.Tracer tracer; private final boolean allowCommandPayload; + private final ObservationRegistry observationRegistry; /** * Constructs a new {@link MicrometerTracer} instance. * - * @param tracer The Micrometer {@link io.micrometer.tracing.Tracer} to delegate tracing operations to. + * @param observationRegistry The Micrometer {@link ObservationRegistry} to delegate tracing operations to. */ - public MicrometerTracer(final io.micrometer.tracing.Tracer tracer) { - this(tracer, false); + public MicrometerTracer(final ObservationRegistry observationRegistry) { + this(observationRegistry, false); } /** * Constructs a new {@link MicrometerTracer} instance with an option to allow command payloads. * - * @param tracer The Micrometer {@link io.micrometer.tracing.Tracer} to delegate tracing operations to. + * @param observationRegistry The Micrometer {@link ObservationRegistry} to delegate tracing operations to. * @param allowCommandPayload Whether to allow command payloads in the trace context. */ - public MicrometerTracer(final io.micrometer.tracing.Tracer tracer, final boolean allowCommandPayload) { - this.tracer = tracer; + public MicrometerTracer(final ObservationRegistry observationRegistry, final boolean allowCommandPayload) { this.allowCommandPayload = allowCommandPayload; + this.observationRegistry = observationRegistry; } @Override - public TraceContext currentContext() { - return new MicrometerTraceContext(tracer.currentTraceContext().context()); - } - - @Override - public Span nextSpan(final String name) { - return new MicrometerSpan(tracer.nextSpan().name(name).start()); - } - - @Override - public Span nextSpan(final String name, @Nullable final TraceContext parent) { + public Span nextSpan(final String name, @Nullable final TraceContext parent, @Nullable final MongoNamespace namespace) { if (parent instanceof MicrometerTraceContext) { - io.micrometer.tracing.TraceContext micrometerContext = ((MicrometerTraceContext) parent).getTraceContext(); - if (micrometerContext != null) { - return new MicrometerSpan(tracer.spanBuilder() - .name(name) - .setParent(micrometerContext) - .start()); + Observation parentObservation = ((MicrometerTraceContext) parent).observation; + if (parentObservation != null) { + return new MicrometerSpan(MONGODB_OBSERVATION + .observation(observationRegistry) + .contextualName(name) + .parentObservation(parentObservation) + .start(), namespace); } } - return nextSpan(name); + return new MicrometerSpan(MONGODB_OBSERVATION.observation(observationRegistry).contextualName(name).start(), namespace); } @Override @@ -99,25 +97,15 @@ public boolean includeCommandPayload() { * Represents a Micrometer-based trace context. */ private static class MicrometerTraceContext implements TraceContext { - private final io.micrometer.tracing.TraceContext traceContext; + private final Observation observation; /** - * Constructs a new {@link MicrometerTraceContext} instance. + * Constructs a new {@link MicrometerTraceContext} instance with an associated Observation. * - * @param traceContext The Micrometer {@link io.micrometer.tracing.TraceContext}, or null if none exists. + * @param observation The Micrometer {@link Observation}, or null if none exists. */ - MicrometerTraceContext(@Nullable final io.micrometer.tracing.TraceContext traceContext) { - this.traceContext = traceContext; - } - - /** - * Retrieves the underlying Micrometer trace context. - * - * @return The Micrometer {@link io.micrometer.tracing.TraceContext}, or null if none exists. - */ - @Nullable - public io.micrometer.tracing.TraceContext getTraceContext() { - return traceContext; + MicrometerTraceContext(@Nullable final Observation observation) { + this.observation = observation; } } @@ -125,50 +113,75 @@ public io.micrometer.tracing.TraceContext getTraceContext() { * Represents a Micrometer-based span. */ private static class MicrometerSpan implements Span { - private final io.micrometer.tracing.Span span; + private final Observation observation; + @Nullable + private final MongoNamespace namespace; /** - * Constructs a new {@link MicrometerSpan} instance. + * Constructs a new {@link MicrometerSpan} instance with an associated Observation. * - * @param span The Micrometer {@link io.micrometer.tracing.Span} to delegate operations to. + * @param observation The Micrometer {@link Observation}, or null if none exists. */ - MicrometerSpan(final io.micrometer.tracing.Span span) { - this.span = span; + 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 Span tag(final String key, final String value) { - span.tag(key, value); - return this; + public void tagLowCardinality(final KeyValue keyValue) { + observation.lowCardinalityKeyValue(keyValue); } @Override - public Span tag(final String key, final Long value) { - span.tag(key, value); - return this; + 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) { - span.event(event); + observation.event(() -> event); } @Override public void error(final Throwable throwable) { - span.tag(EXCEPTION_MESSAGE, throwable.getMessage()); - span.tag(EXCEPTION_TYPE, throwable.getClass().getName()); - span.tag(EXCEPTION_STACKTRACE, getStackTraceAsString(throwable)); - span.error(throwable); + 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() { - span.end(); + observation.stop(); } @Override public TraceContext context() { - return new MicrometerTraceContext(span.context()); + return new MicrometerTraceContext(observation); + } + + @Override + @Nullable + public MongoNamespace getNamespace() { + return namespace; } private String getStackTraceAsString(final Throwable throwable) { 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..9b173e11ac9 --- /dev/null +++ b/driver-core/src/main/com/mongodb/tracing/MongodbObservation.java @@ -0,0 +1,185 @@ +/* + * 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.docs.KeyName; +import io.micrometer.observation.docs.ObservationDocumentation; + +/** + * A MongoDB-based {@link io.micrometer.observation.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"; + } + } + } +} diff --git a/driver-core/src/main/com/mongodb/tracing/package-info.java b/driver-core/src/main/com/mongodb/tracing/package-info.java index 43e82603a09..247576d1537 100644 --- a/driver-core/src/main/com/mongodb/tracing/package-info.java +++ b/driver-core/src/main/com/mongodb/tracing/package-info.java @@ -17,7 +17,7 @@ /** * This package defines the API for MongoDB driver tracing. * - * @since 5.6 + * @since 5.7 */ @NonNullApi package com.mongodb.tracing; diff --git a/driver-kotlin-coroutine/build.gradle.kts b/driver-kotlin-coroutine/build.gradle.kts index dd127a4dd6a..49e94bf7e2c 100644 --- a/driver-kotlin-coroutine/build.gradle.kts +++ b/driver-kotlin-coroutine/build.gradle.kts @@ -38,7 +38,7 @@ dependencies { integrationTestImplementation(project(path = ":bson", configuration = "testArtifacts")) integrationTestImplementation(project(path = ":driver-sync", configuration = "testArtifacts")) integrationTestImplementation(project(path = ":driver-core", configuration = "testArtifacts")) - integrationTestImplementation(libs.micrometer) + integrationTestImplementation(libs.micrometer.observation) } configureMavenPublication { diff --git a/driver-kotlin-sync/build.gradle.kts b/driver-kotlin-sync/build.gradle.kts index b6113200628..1a510220351 100644 --- a/driver-kotlin-sync/build.gradle.kts +++ b/driver-kotlin-sync/build.gradle.kts @@ -32,7 +32,7 @@ dependencies { integrationTestImplementation(project(path = ":bson", configuration = "testArtifacts")) integrationTestImplementation(project(path = ":driver-sync", configuration = "testArtifacts")) integrationTestImplementation(project(path = ":driver-core", configuration = "testArtifacts")) - integrationTestImplementation(libs.micrometer) + integrationTestImplementation(libs.micrometer.observation) } configureMavenPublication { diff --git a/driver-reactive-streams/build.gradle.kts b/driver-reactive-streams/build.gradle.kts index f9ac5301625..12c461b3257 100644 --- a/driver-reactive-streams/build.gradle.kts +++ b/driver-reactive-streams/build.gradle.kts @@ -46,7 +46,7 @@ dependencies { testImplementation(libs.reactive.streams.tck) // Tracing - testImplementation(libs.micrometer) + testImplementation(libs.micrometer.observation) } configureMavenPublication { diff --git a/driver-sync/build.gradle.kts b/driver-sync/build.gradle.kts index b37d0226295..1bed5ed69c0 100644 --- a/driver-sync/build.gradle.kts +++ b/driver-sync/build.gradle.kts @@ -35,12 +35,13 @@ 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 - testImplementation(libs.bundles.micrometer.test) + // Tracing testing + testImplementation(libs.micrometer.tracing.integration.test) { exclude(group = "org.junit.jupiter") } } configureMavenPublication { 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 db8fd03adaf..54f476e8a35 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java +++ b/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java @@ -60,11 +60,11 @@ import com.mongodb.internal.operation.WriteOperation; import com.mongodb.internal.session.ServerSessionPool; import com.mongodb.internal.tracing.Span; -import com.mongodb.internal.tracing.Tags; import com.mongodb.internal.tracing.TraceContext; import com.mongodb.internal.tracing.TracingManager; import com.mongodb.internal.tracing.TransactionSpan; import com.mongodb.lang.Nullable; +import io.micrometer.common.KeyValues; import org.bson.BsonDocument; import org.bson.Document; import org.bson.UuidRepresentation; @@ -78,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 @@ -601,17 +607,27 @@ private Span createOperationSpan(final ClientSession actualClientSession, final if (transactionSpan != null) { parentContext = transactionSpan.getContext(); } - String name = commandName + " " + namespace.getFullName(); + 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); + .addSpan(name, parentContext, namespace); + + span.tagLowCardinality(keyValues); + binding.getOperationContext().setTracingSpan(span); - span.tag(Tags.SYSTEM, "mongodb"); - span.tag(Tags.NAMESPACE, namespace.getDatabaseName()); - span.tag(Tags.COLLECTION, namespace.getCollectionName()); - span.tag(Tags.OPERATION_NAME, commandName); - span.tag(Tags.OPERATION_SUMMARY, name); return span; } else { 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 index bca04e6647a..c5ec573ad76 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/tracing/SpanTree.java +++ b/driver-sync/src/test/functional/com/mongodb/client/tracing/SpanTree.java @@ -16,12 +16,13 @@ package com.mongodb.client.tracing; -import com.mongodb.internal.tracing.Tags; 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; @@ -35,6 +36,12 @@ 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; @@ -60,8 +67,8 @@ public static SpanTree from(final BsonArray spans) { final SpanNode rootNode = new SpanNode(name); spanTree.roots.add(rootNode); - if (spanDoc.containsKey("tags")) { - rootNode.tags = spanDoc.getDocument("tags"); + if (spanDoc.containsKey("attributes")) { + rootNode.tags = spanDoc.getDocument("attributes"); } if (spanDoc.containsKey("nested")) { @@ -101,8 +108,15 @@ public static SpanTree from(final Deque spans) { for (final Map.Entry tag : span.getTags().entrySet()) { // handle special case of session id (needs to be parsed into a BsonBinary) // this is needed because the SimpleTracer reports all the collected tags as strings - if (tag.getKey().equals(Tags.SESSION_ID)) { + 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())); } @@ -123,6 +137,43 @@ public static SpanTree from(final Deque spans) { 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. @@ -134,8 +185,8 @@ private static void addNestedSpans(final SpanNode parentNode, final BsonDocument final String name = nestedSpan.getString("name").getValue(); final SpanNode childNode = new SpanNode(name, parentNode); - if (nestedSpan.containsKey("tags")) { - childNode.tags = nestedSpan.getDocument("tags"); + if (nestedSpan.containsKey("attributes")) { + childNode.tags = nestedSpan.getDocument("attributes"); } if (nestedSpan.containsKey("nested")) { @@ -159,8 +210,9 @@ public static void assertValid(final SpanTree reportedSpans, final SpanTree expe final boolean ignoreExtraSpans) { if (ignoreExtraSpans) { // remove from the reported spans all the nodes that are not expected - reportedSpans.roots.removeIf(node -> !expectedSpans.roots.contains(node)); - + reportedSpans.roots.removeIf(node -> + expectedSpans.roots.stream().noneMatch(expectedNode -> expectedNode.getName().equalsIgnoreCase(node.getName())) + ); } // check that we have the same root spans diff --git a/driver-sync/src/test/functional/com/mongodb/client/tracing/ZipkinTracer.java b/driver-sync/src/test/functional/com/mongodb/client/tracing/ZipkinTracer.java deleted file mode 100644 index 186835c6ca2..00000000000 --- a/driver-sync/src/test/functional/com/mongodb/client/tracing/ZipkinTracer.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2008-present MongoDB, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.mongodb.client.tracing; - -import io.micrometer.tracing.Tracer; -import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext; -import io.micrometer.tracing.otel.bridge.OtelTracer; -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.context.propagation.ContextPropagators; -import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter; -import io.opentelemetry.extension.trace.propagation.B3Propagator; -import io.opentelemetry.sdk.OpenTelemetrySdk; -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.trace.SdkTracerProvider; -import io.opentelemetry.sdk.trace.SpanProcessor; -import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; -import io.opentelemetry.semconv.ResourceAttributes; - -/** - * A utility class to create a Zipkin tracer using OpenTelemetry protocol, useful for visualizing spans in Zipkin UI - * This tracer can be used to send spans to a Zipkin server. - *

- * Spans are visible in the Zipkin UI at http://localhost:9411. - *

- * To Start Zipkin server, you can use the following command: - *

{@code
- * docker run -d -p 9411:9411 openzipkin/zipkin
- * }
- */ -public final class ZipkinTracer { - private static final String ENDPOINT = "http://localhost:9411/api/v2/spans"; - - private ZipkinTracer() { - } - - /** - * Creates a Zipkin tracer with the specified service name. - * - * @param serviceName the name of the service to be used in the tracer - * @return a Tracer instance configured to send spans to Zipkin - */ - public static Tracer getTracer(final String serviceName) { - ZipkinSpanExporter zipkinExporter = ZipkinSpanExporter.builder() - .setEndpoint(ENDPOINT) - .build(); - - Resource resource = Resource.getDefault() - .merge(Resource.create( - Attributes.of( - ResourceAttributes.SERVICE_NAME, serviceName, - ResourceAttributes.SERVICE_VERSION, "1.0.0" - ) - )); - - SpanProcessor spanProcessor = SimpleSpanProcessor.create(zipkinExporter); - - SdkTracerProvider tracerProvider = SdkTracerProvider.builder() - .addSpanProcessor(spanProcessor) - .setResource(resource) - .build(); - - OpenTelemetry openTelemetry = OpenTelemetrySdk.builder() - .setTracerProvider(tracerProvider) - .setPropagators(ContextPropagators.create( - B3Propagator.injectingSingleHeader() - )) - .build(); - - io.opentelemetry.api.trace.Tracer otelTracer = openTelemetry.getTracer("my-java-service", "1.0.0"); - - OtelCurrentTraceContext otelCurrentTraceContext = new OtelCurrentTraceContext(); - - return new OtelTracer( - otelTracer, - otelCurrentTraceContext, - null // EventPublisher can be null for basic usage - ); - } - -} diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java b/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java index c0d33b06d91..bff1951c16b 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 @@ -67,8 +67,8 @@ import com.mongodb.lang.Nullable; import com.mongodb.logging.TestLoggingInterceptor; import com.mongodb.tracing.MicrometerTracer; -import io.micrometer.tracing.Tracer; -import io.micrometer.tracing.test.simple.SimpleTracer; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.test.reporter.inmemory.InMemoryOtelSetup; import org.bson.BsonArray; import org.bson.BsonBoolean; import org.bson.BsonDocument; @@ -129,7 +129,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 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<>(); @@ -298,7 +299,7 @@ public TestLoggingInterceptor getClientLoggingInterceptor(final String id) { return getEntity(id + "-logging-interceptor", clientLoggingInterceptors, "logging interceptor"); } - public Tracer getClientTracer(final String id) { + public InMemoryOtelSetup.Builder.OtelBuildingBlocks getClientTracer(final String id) { return getEntity(id + "-tracing", clientTracing, "micrometer tracing"); } @@ -615,17 +616,13 @@ private void initClient(final BsonDocument entity, final String id, if (entity.containsKey("observeTracingMessages")) { boolean enableCommandPayload = entity.getDocument("observeTracingMessages").get("enableCommandPayload", BsonBoolean.FALSE).asBoolean().getValue(); - /* To enable Zipkin backend, uncomment the following lines and ensure you have the server started - (docker run -d -p 9411:9411 openzipkin/zipkin). The tests will fail but the captured spans will be - visible in the Zipkin UI at http://localhost:9411 for debugging purpose. - * - * Tracer tracer = ZipkinTracer.getTracer("UTR"); - * putEntity(id + "-tracing", new SimpleTracer(), clientTracing); - */ - Tracer tracer = new SimpleTracer(); - putEntity(id + "-tracing", tracer, clientTracing); + ObservationRegistry observationRegistry = ObservationRegistry.create(); + InMemoryOtelSetup inMemoryOtel = InMemoryOtelSetup.builder().register(observationRegistry); + InMemoryOtelSetup.Builder.OtelBuildingBlocks tracer = inMemoryOtel.getBuildingBlocks(); - clientSettingsBuilder.tracer(new MicrometerTracer(tracer, enableCommandPayload)); + putEntity(id + "-tracing", tracer, clientTracing); + inMemoryOTelInstances.add(inMemoryOtel); + clientSettingsBuilder.tracer(new MicrometerTracer(observationRegistry, enableCommandPayload)); } MongoClientSettings clientSettings = clientSettingsBuilder.build(); @@ -818,6 +815,7 @@ public void close() { clients.values().forEach(MongoClient::close); clientLoggingInterceptors.values().forEach(TestLoggingInterceptor::close); threads.values().forEach(ExecutorService::shutdownNow); + inMemoryOTelInstances.forEach(InMemoryOtelSetup::close); } private static class EntityCommandListener implements CommandListener { 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 8e91e85205b..9effc2a2e72 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 @@ -45,8 +45,7 @@ import com.mongodb.lang.Nullable; import com.mongodb.logging.TestLoggingInterceptor; import com.mongodb.test.AfterBeforeParameterResolver; -import io.micrometer.tracing.Tracer; -import io.micrometer.tracing.test.simple.SimpleTracer; +import io.micrometer.tracing.test.reporter.inmemory.InMemoryOtelSetup; import org.bson.BsonArray; import org.bson.BsonBoolean; import org.bson.BsonDocument; @@ -114,7 +113,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.26"; + 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()); @@ -496,17 +495,19 @@ private void compareLogMessages(final UnifiedTestContext rootContext, final Bson } private void compareTracingSpans(final BsonDocument definition) { - BsonDocument curTracingSpansForClient = definition.getDocument("expectTracingMessages"); - String clientId = curTracingSpansForClient.getString("client").getValue(); - - // Get the tracer for the client - Tracer micrometerTracer = entities.getClientTracer(clientId); - SimpleTracer simpleTracer = (SimpleTracer) micrometerTracer; - - SpanTree expectedSpans = SpanTree.from(curTracingSpansForClient.getArray("spans")); - SpanTree reportedSpans = SpanTree.from(simpleTracer.getSpans()); - boolean ignoreExtraSpans = curTracingSpansForClient.getBoolean("ignoreExtraSpans", BsonBoolean.TRUE).getValue(); - SpanTree.assertValid(reportedSpans, expectedSpans, rootContext.valueMatcher::assertValuesMatch, ignoreExtraSpans); + 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) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 38805b02353..057e3bc5430 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,9 +24,8 @@ reactive-streams = "1.0.4" snappy = "1.1.10.3" zstd = "1.5.5-3" jetbrains-annotations = "26.0.2" -micrometer = "1.4.5" -zipkin-reporter = "2.16.3" -opentelemetry-exporter-zipkin = "1.30.0" +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" @@ -96,7 +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 = { module = "io.micrometer:micrometer-tracing", version.ref = "micrometer" } +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" } @@ -175,13 +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-test = { module = " io.micrometer:micrometer-tracing-test", version.ref = "micrometer" } -micrometer-tracing-bridge-brave = { module = " io.micrometer:micrometer-tracing-bridge-brave", version.ref = "micrometer" } -micrometer-tracing = { module = " io.micrometer:micrometer-tracing", version.ref = "micrometer" } -micrometer-tracing-bridge-otel = { module = " io.micrometer:micrometer-tracing-bridge-otel", version.ref = "micrometer" } -zipkin-reporter = { module = " io.zipkin.reporter2:zipkin-reporter", version.ref = "zipkin-reporter" } -opentelemetry-exporter-zipkin = { module = " io.opentelemetry:opentelemetry-exporter-zipkin", version.ref = "opentelemetry-exporter-zipkin" } +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"] @@ -208,9 +201,6 @@ scala-test-v2-v12 = ["scala-test-flatspec-v2-v12", "scala-test-shouldmatchers-v2 scala-test-v2-v11 = ["scala-test-flatspec-v2-v11", "scala-test-shouldmatchers-v2-v11", "scala-test-mockito-v2-v11", "scala-test-junit-runner-v2-v11", "reflections"] -micrometer-test = ["micrometer-tracing-test", "micrometer-tracing-bridge-brave", "micrometer-tracing-bridge-otel", - "micrometer-tracing", "zipkin-reporter", "opentelemetry-exporter-zipkin"] - [plugins] kotlin-gradle = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } bnd = { id = "biz.aQute.bnd.builder", version.ref = "plugin-bnd" } From 45f3cb89e095736abdcf5a25f57f81448ac3e64c Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 6 Oct 2025 15:18:55 +0100 Subject: [PATCH 08/14] Added Prose Tests --- .../main/com/mongodb/MongoClientSettings.java | 12 +- .../connection/InternalStreamConnection.java | 2 +- .../com/mongodb/internal/tracing/Tracer.java | 4 + .../internal/tracing/TracingManager.java | 16 +- .../mongodb/tracing/MongodbObservation.java | 32 ++++ driver-sync/build.gradle.kts | 6 + .../client/tracing/MicrometerProseTest.java | 163 ++++++++++++++++++ 7 files changed, 225 insertions(+), 10 deletions(-) create mode 100644 driver-sync/src/test/functional/com/mongodb/client/tracing/MicrometerProseTest.java diff --git a/driver-core/src/main/com/mongodb/MongoClientSettings.java b/driver-core/src/main/com/mongodb/MongoClientSettings.java index 938b9c76068..500f402b405 100644 --- a/driver-core/src/main/com/mongodb/MongoClientSettings.java +++ b/driver-core/src/main/com/mongodb/MongoClientSettings.java @@ -58,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; @@ -88,6 +89,7 @@ public final class MongoClientSettings { new ExpressionCodecProvider(), new Jep395RecordCodecProvider(), new KotlinCodecProvider())); + private static final String ENV_OTEL_ENABLED = "OTEL_JAVA_INSTRUMENTATION_MONGODB_ENABLED"; private final ReadPreference readPreference; private final WriteConcern writeConcern; @@ -1184,6 +1186,14 @@ private MongoClientSettings(final Builder builder) { heartbeatConnectTimeoutSetExplicitly = builder.heartbeatConnectTimeoutMS != 0; contextProvider = builder.contextProvider; timeoutMS = builder.timeoutMS; - tracer = (builder.tracer == null) ? Tracer.NO_OP : builder.tracer; + + 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 d4104462c8c..0953cf68b73 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java +++ b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java @@ -476,7 +476,7 @@ private T sendAndReceiveInternal(final CommandMessage message, final Decoder commandEventSender = new NoOpCommandEventSender(); } if (isTracingCommandPayloadNeeded) { - tracingSpan.tagHighCardinality(QUERY_TEXT.withValue(commandDocument.toJson())); + tracingSpan.tagHighCardinality(QUERY_TEXT.withBson(commandDocument)); } try { diff --git a/driver-core/src/main/com/mongodb/internal/tracing/Tracer.java b/driver-core/src/main/com/mongodb/internal/tracing/Tracer.java index 6f5403213d6..3bdaa4f6293 100644 --- a/driver-core/src/main/com/mongodb/internal/tracing/Tracer.java +++ b/driver-core/src/main/com/mongodb/internal/tracing/Tracer.java @@ -25,6 +25,10 @@ * 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 { diff --git a/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java b/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java index 6fa5613ef47..e6e6ba57433 100644 --- a/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java +++ b/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java @@ -34,23 +34,23 @@ public class TracingManager { * A no-op instance of the TracingManager used when tracing is disabled. */ public static final TracingManager NO_OP = new TracingManager(Tracer.NO_OP); - private static final String ENV_ALLOW_COMMAND_PAYLOAD = "MONGODB_TRACING_ALLOW_COMMAND_PAYLOAD"; + private static final String ENV_OTEL_QUERY_TEXT_MAX_LENGTH = "OTEL_JAVA_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH"; private final Tracer tracer; - private final boolean enableCommandPayload; + private final int queryTextMaxLength; /** * Constructs a new TracingManager with the specified tracer and parent context. - * Setting the environment variable {@code MONGODB_TRACING_ALLOW_COMMAND_PAYLOAD} to "true" will enable command payload tracing. + * 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; - String envAllowCommandPayload = getenv(ENV_ALLOW_COMMAND_PAYLOAD); - if (envAllowCommandPayload != null) { - this.enableCommandPayload = Boolean.parseBoolean(envAllowCommandPayload); + String queryTextMaxLength = getenv(ENV_OTEL_QUERY_TEXT_MAX_LENGTH); + if (queryTextMaxLength != null) { + this.queryTextMaxLength = Integer.parseInt(queryTextMaxLength); } else { - this.enableCommandPayload = tracer.includeCommandPayload(); + this.queryTextMaxLength = tracer.includeCommandPayload() ? Integer.MAX_VALUE : 0; } } @@ -112,6 +112,6 @@ public boolean isEnabled() { * @return True if command payload tracing is enabled, false otherwise. */ public boolean isCommandPayloadEnabled() { - return enableCommandPayload; + return queryTextMaxLength > 0; } } diff --git a/driver-core/src/main/com/mongodb/tracing/MongodbObservation.java b/driver-core/src/main/com/mongodb/tracing/MongodbObservation.java index 9b173e11ac9..5a4f24ebc48 100644 --- a/driver-core/src/main/com/mongodb/tracing/MongodbObservation.java +++ b/driver-core/src/main/com/mongodb/tracing/MongodbObservation.java @@ -16,8 +16,16 @@ package com.mongodb.tracing; +import io.micrometer.common.KeyValue; import io.micrometer.common.docs.KeyName; 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; /** * A MongoDB-based {@link io.micrometer.observation.Observation}. @@ -180,6 +188,30 @@ public enum HighCardinalityKeyNames implements KeyName { public String asString() { return "db.query.text"; } + }; + + public KeyValue withBson(final BsonDocument commandDocument) { + 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(42) + .build()); + + jsonWriter.pipe(bsonReader); + + if (jsonWriter.isTruncated()) { + writer.append(" ..."); + } + + return writer.toString(); + } } + } } diff --git a/driver-sync/build.gradle.kts b/driver-sync/build.gradle.kts index 1bed5ed69c0..ab9bc5c01f4 100644 --- a/driver-sync/build.gradle.kts +++ b/driver-sync/build.gradle.kts @@ -44,6 +44,12 @@ dependencies { 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)`) + jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED") +} + configureMavenPublication { pom { name.set("MongoDB Driver") 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..257f49a49b8 --- /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); // don't enable command payload by default + 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(String key, 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); + } + } +} From 39483e4a0038deae96eac3a6a466fbdadc322aef Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 6 Oct 2025 23:30:56 +0100 Subject: [PATCH 09/14] Fixing test dependencies --- driver-kotlin-coroutine/build.gradle.kts | 2 +- driver-kotlin-sync/build.gradle.kts | 2 +- driver-reactive-streams/build.gradle.kts | 2 +- .../com/mongodb/client/tracing/MicrometerProseTest.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/driver-kotlin-coroutine/build.gradle.kts b/driver-kotlin-coroutine/build.gradle.kts index 49e94bf7e2c..8880c67aad8 100644 --- a/driver-kotlin-coroutine/build.gradle.kts +++ b/driver-kotlin-coroutine/build.gradle.kts @@ -38,7 +38,7 @@ dependencies { integrationTestImplementation(project(path = ":bson", configuration = "testArtifacts")) integrationTestImplementation(project(path = ":driver-sync", configuration = "testArtifacts")) integrationTestImplementation(project(path = ":driver-core", configuration = "testArtifacts")) - integrationTestImplementation(libs.micrometer.observation) + testImplementation(libs.micrometer.tracing.integration.test) { exclude(group = "org.junit.jupiter") } } configureMavenPublication { diff --git a/driver-kotlin-sync/build.gradle.kts b/driver-kotlin-sync/build.gradle.kts index 1a510220351..74f9b37c219 100644 --- a/driver-kotlin-sync/build.gradle.kts +++ b/driver-kotlin-sync/build.gradle.kts @@ -32,7 +32,7 @@ dependencies { integrationTestImplementation(project(path = ":bson", configuration = "testArtifacts")) integrationTestImplementation(project(path = ":driver-sync", configuration = "testArtifacts")) integrationTestImplementation(project(path = ":driver-core", configuration = "testArtifacts")) - integrationTestImplementation(libs.micrometer.observation) + testImplementation(libs.micrometer.tracing.integration.test) { exclude(group = "org.junit.jupiter") } } configureMavenPublication { diff --git a/driver-reactive-streams/build.gradle.kts b/driver-reactive-streams/build.gradle.kts index 12c461b3257..8f4454d4f4c 100644 --- a/driver-reactive-streams/build.gradle.kts +++ b/driver-reactive-streams/build.gradle.kts @@ -46,7 +46,7 @@ dependencies { testImplementation(libs.reactive.streams.tck) // Tracing - testImplementation(libs.micrometer.observation) + testImplementation(libs.micrometer.tracing.integration.test) { exclude(group = "org.junit.jupiter") } } configureMavenPublication { 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 index 257f49a49b8..72cee301d6b 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/tracing/MicrometerProseTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/tracing/MicrometerProseTest.java @@ -139,7 +139,7 @@ void testControlCommandPayloadViaEnvironmentVariable() throws Exception { } @SuppressWarnings("unchecked") - private void setEnv(String key, String value) throws Exception { + private void setEnv(final String key, final String value) throws Exception { // Get the unmodifiable Map from System.getenv() Map env = System.getenv(); From 0ecfb195bc76eec664c35adac04ddcde06151cc5 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Wed, 8 Oct 2025 11:33:05 +0100 Subject: [PATCH 10/14] Skipping non-compliant tests --- .../internal/connection/CommandHelperSpecification.groovy | 2 ++ .../com/mongodb/client/unified/UnifiedTestModifications.java | 4 ++++ 2 files changed, 6 insertions(+) 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-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java index f658b7c5e01..8f746e28f0c 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 @@ -80,6 +80,10 @@ public static void applyCustomizations(final TestDef def) { .test("client-side-operations-timeout", "WaitQueueTimeoutError does not clear the pool", "WaitQueueTimeoutError does not clear the pool"); + // 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 From fb1f1afaa01c7f5e4ee600f052715fdbae16ff27 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Wed, 8 Oct 2025 14:51:15 +0100 Subject: [PATCH 11/14] Fixing test deps for Scala --- .../test/kotlin/com/mongodb/kotlin/client/MongoIterableTest.kt | 3 ++- driver-scala/build.gradle.kts | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) 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-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 { From 0628386af4508d749b018674181ce19e049f0c8f Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Wed, 8 Oct 2025 17:07:45 +0100 Subject: [PATCH 12/14] Refactored some type visibility per PR feedback --- driver-core/src/main/com/mongodb/MongoClientSettings.java | 6 +++--- .../internal/connection/InternalStreamConnection.java | 2 +- .../com/mongodb/internal/connection/OperationContext.java | 2 +- .../main/com/mongodb/internal/tracing/TracingManager.java | 3 +++ .../main/com/mongodb/internal/tracing/TransactionSpan.java | 2 ++ .../src/main/com/mongodb/tracing/MicrometerTracer.java | 3 --- .../src/main/com/mongodb/{internal => }/tracing/Span.java | 2 +- .../com/mongodb/{internal => }/tracing/TraceContext.java | 2 +- .../src/main/com/mongodb/{internal => }/tracing/Tracer.java | 2 +- driver-sync/src/main/com/mongodb/client/ClientSession.java | 2 +- .../main/com/mongodb/client/internal/MongoClusterImpl.java | 4 ++-- .../com/mongodb/client/tracing/MicrometerProseTest.java | 2 +- 12 files changed, 17 insertions(+), 15 deletions(-) rename driver-core/src/main/com/mongodb/{internal => }/tracing/Span.java (99%) rename driver-core/src/main/com/mongodb/{internal => }/tracing/TraceContext.java (95%) rename driver-core/src/main/com/mongodb/{internal => }/tracing/Tracer.java (98%) diff --git a/driver-core/src/main/com/mongodb/MongoClientSettings.java b/driver-core/src/main/com/mongodb/MongoClientSettings.java index 2f51dd4527e..69cb8233690 100644 --- a/driver-core/src/main/com/mongodb/MongoClientSettings.java +++ b/driver-core/src/main/com/mongodb/MongoClientSettings.java @@ -30,7 +30,7 @@ import com.mongodb.connection.SslSettings; import com.mongodb.connection.TransportSettings; import com.mongodb.event.CommandListener; -import com.mongodb.internal.tracing.Tracer; +import com.mongodb.tracing.Tracer; import com.mongodb.lang.Nullable; import com.mongodb.spi.dns.DnsClient; import com.mongodb.spi.dns.InetAddressResolver; @@ -736,7 +736,7 @@ Builder heartbeatSocketTimeoutMS(final int heartbeatSocketTimeoutMS) { * @param tracer the tracer * @see com.mongodb.tracing.MicrometerTracer * @return this - * @since 5.6 + * @since 5.7 */ @Alpha(Reason.CLIENT) public Builder tracer(final Tracer tracer) { @@ -1065,7 +1065,7 @@ public ContextProvider getContextProvider() { * Get the tracer to create Spans for operations, commands and transactions. * * @return the configured Tracer - * @since 5.6 + * @since 5.7 */ public Tracer getTracer() { return tracer; 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 ae37acf249a..f53ed538dab 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java +++ b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java @@ -53,7 +53,7 @@ import com.mongodb.internal.logging.StructuredLogger; import com.mongodb.internal.session.SessionContext; import com.mongodb.internal.time.Timeout; -import com.mongodb.internal.tracing.Span; +import com.mongodb.tracing.Span; import com.mongodb.internal.tracing.TracingManager; import com.mongodb.lang.Nullable; import io.micrometer.common.KeyValues; 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 bc4a785d545..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,7 +27,7 @@ import com.mongodb.internal.TimeoutSettings; import com.mongodb.internal.VisibleForTesting; import com.mongodb.internal.session.SessionContext; -import com.mongodb.internal.tracing.Span; +import com.mongodb.tracing.Span; import com.mongodb.internal.tracing.TracingManager; import com.mongodb.lang.Nullable; import com.mongodb.selector.ServerSelector; diff --git a/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java b/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java index e3f1f247a48..84cf91decd8 100644 --- a/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java +++ b/driver-core/src/main/com/mongodb/internal/tracing/TracingManager.java @@ -18,6 +18,9 @@ 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; diff --git a/driver-core/src/main/com/mongodb/internal/tracing/TransactionSpan.java b/driver-core/src/main/com/mongodb/internal/tracing/TransactionSpan.java index 789f259e82c..d3133a8238b 100644 --- a/driver-core/src/main/com/mongodb/internal/tracing/TransactionSpan.java +++ b/driver-core/src/main/com/mongodb/internal/tracing/TransactionSpan.java @@ -17,6 +17,8 @@ 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. diff --git a/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java b/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java index ae5a708ef96..30d81351ef3 100644 --- a/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java +++ b/driver-core/src/main/com/mongodb/tracing/MicrometerTracer.java @@ -17,9 +17,6 @@ package com.mongodb.tracing; import com.mongodb.MongoNamespace; -import com.mongodb.internal.tracing.Span; -import com.mongodb.internal.tracing.TraceContext; -import com.mongodb.internal.tracing.Tracer; import com.mongodb.lang.Nullable; import io.micrometer.common.KeyValue; import io.micrometer.common.KeyValues; diff --git a/driver-core/src/main/com/mongodb/internal/tracing/Span.java b/driver-core/src/main/com/mongodb/tracing/Span.java similarity index 99% rename from driver-core/src/main/com/mongodb/internal/tracing/Span.java rename to driver-core/src/main/com/mongodb/tracing/Span.java index 0549a194c6c..25efc1d42c0 100644 --- a/driver-core/src/main/com/mongodb/internal/tracing/Span.java +++ b/driver-core/src/main/com/mongodb/tracing/Span.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.mongodb.internal.tracing; +package com.mongodb.tracing; import com.mongodb.MongoNamespace; import com.mongodb.lang.Nullable; diff --git a/driver-core/src/main/com/mongodb/internal/tracing/TraceContext.java b/driver-core/src/main/com/mongodb/tracing/TraceContext.java similarity index 95% rename from driver-core/src/main/com/mongodb/internal/tracing/TraceContext.java rename to driver-core/src/main/com/mongodb/tracing/TraceContext.java index cb2f6ef1020..06763f96111 100644 --- a/driver-core/src/main/com/mongodb/internal/tracing/TraceContext.java +++ b/driver-core/src/main/com/mongodb/tracing/TraceContext.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.mongodb.internal.tracing; +package com.mongodb.tracing; @SuppressWarnings("InterfaceIsType") public interface TraceContext { diff --git a/driver-core/src/main/com/mongodb/internal/tracing/Tracer.java b/driver-core/src/main/com/mongodb/tracing/Tracer.java similarity index 98% rename from driver-core/src/main/com/mongodb/internal/tracing/Tracer.java rename to driver-core/src/main/com/mongodb/tracing/Tracer.java index 3bdaa4f6293..cc88e82b5ac 100644 --- a/driver-core/src/main/com/mongodb/internal/tracing/Tracer.java +++ b/driver-core/src/main/com/mongodb/tracing/Tracer.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.mongodb.internal.tracing; +package com.mongodb.tracing; import com.mongodb.MongoNamespace; import com.mongodb.lang.Nullable; diff --git a/driver-sync/src/main/com/mongodb/client/ClientSession.java b/driver-sync/src/main/com/mongodb/client/ClientSession.java index 6af2b4be664..abf0ff33fbd 100644 --- a/driver-sync/src/main/com/mongodb/client/ClientSession.java +++ b/driver-sync/src/main/com/mongodb/client/ClientSession.java @@ -131,7 +131,7 @@ public interface ClientSession extends com.mongodb.session.ClientSession { * Get the transaction span (if started). * * @return the transaction span - * @since 5.6 + * @since 5.7 */ @Nullable TransactionSpan getTransactionSpan(); 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 54f476e8a35..0907ed951b2 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java +++ b/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java @@ -59,8 +59,8 @@ import com.mongodb.internal.operation.ReadOperation; import com.mongodb.internal.operation.WriteOperation; import com.mongodb.internal.session.ServerSessionPool; -import com.mongodb.internal.tracing.Span; -import com.mongodb.internal.tracing.TraceContext; +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; 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 index 72cee301d6b..5b0947ac3eb 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/tracing/MicrometerProseTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/tracing/MicrometerProseTest.java @@ -92,7 +92,7 @@ void testControlOtelInstrumentationViaEnvironmentVariable() throws Exception { @Test void testControlCommandPayloadViaEnvironmentVariable() throws Exception { - MicrometerTracer tracer = new MicrometerTracer(observationRegistry); // don't enable command payload by default + MicrometerTracer tracer = new MicrometerTracer(observationRegistry, true); setEnv("OTEL_JAVA_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH", "42"); MongoClientSettings clientSettings = Fixture.getMongoClientSettingsBuilder() From d3f2d62cf3efecc79b6316045f4c741289924783 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Wed, 8 Oct 2025 22:15:07 +0100 Subject: [PATCH 13/14] Fixing javadoc & Scala test --- driver-core/src/main/com/mongodb/tracing/Span.java | 1 - .../scala/org/mongodb/scala/ApiAliasAndCompanionSpec.scala | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/driver-core/src/main/com/mongodb/tracing/Span.java b/driver-core/src/main/com/mongodb/tracing/Span.java index 25efc1d42c0..b1b9ed30c49 100644 --- a/driver-core/src/main/com/mongodb/tracing/Span.java +++ b/driver-core/src/main/com/mongodb/tracing/Span.java @@ -34,7 +34,6 @@ *
  • 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 */ 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 4e93a331776..a15229411ae 100644 --- a/driver-scala/src/test/scala/org/mongodb/scala/ApiAliasAndCompanionSpec.scala +++ b/driver-scala/src/test/scala/org/mongodb/scala/ApiAliasAndCompanionSpec.scala @@ -95,7 +95,10 @@ class ApiAliasAndCompanionSpec extends BaseSpec { "BaseClientUpdateOptions", "BaseClientDeleteOptions", "MongoBaseInterfaceAssertions", - "MicrometerTracer" + "MicrometerTracer", + "TraceContext", + "Span", + "Tracer" ) val scalaExclusions = Set( "BuildInfo", From c4901548930db81e3bcc581b5a3a09aef318d966 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Thu, 9 Oct 2025 09:51:34 +0100 Subject: [PATCH 14/14] using JVM option behind a flag --- buildSrc/src/main/kotlin/project/Companion.kt | 2 +- driver-sync/build.gradle.kts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) 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/driver-sync/build.gradle.kts b/driver-sync/build.gradle.kts index ab9bc5c01f4..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") @@ -47,7 +48,10 @@ dependencies { tasks.withType { // Needed for MicrometerProseTest to set env variable programmatically (calls // `field.setAccessible(true)`) - jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED") + 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 {