From 4912105093d4854d246b979c49b8d55cc585e51e Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 13 Apr 2026 11:49:21 +0100 Subject: [PATCH 1/7] Split Micrometer observation into separate operation and command types The driver used a single observation name ("mongodb") for both operation-level and command-level spans, which have different sets of low-cardinality tag keys. Prometheus requires all meters sharing a name to have identical tag key sets, causing the second observation type to be silently dropped. Split MongodbObservation.MONGODB_OBSERVATION into MONGODB_OPERATION (name "mongodb.operation") and MONGODB_COMMAND (name "mongodb.command"), each declaring its own low-cardinality key set. Updated Tracer and TracingManager to pass the observation type through span creation. --- .../connection/InternalStreamConnection.java | 2 +- .../micrometer/MicrometerTracer.java | 16 +-- .../micrometer/MongodbObservation.java | 81 +++++++++++--- .../observability/micrometer/Tracer.java | 8 +- .../micrometer/TracingManager.java | 101 ++++++++---------- .../client/observability/SpanTree.java | 12 +-- 6 files changed, 128 insertions(+), 92 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 7e454debed..5064a081c6 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java +++ b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java @@ -95,7 +95,7 @@ import static com.mongodb.internal.connection.ProtocolHelper.isCommandOk; import static com.mongodb.internal.logging.LogMessage.Level.DEBUG; import static com.mongodb.internal.observability.micrometer.MongodbObservation.HighCardinalityKeyNames.QUERY_TEXT; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.RESPONSE_STATUS_CODE; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames.RESPONSE_STATUS_CODE; import static com.mongodb.internal.thread.InterruptionUtil.translateInterruptedException; import static java.util.Arrays.asList; diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java index a7204a01a7..83fc719f0c 100644 --- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java @@ -33,10 +33,9 @@ import java.io.PrintWriter; import java.io.StringWriter; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.EXCEPTION_MESSAGE; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.EXCEPTION_STACKTRACE; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.EXCEPTION_TYPE; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.MONGODB_OBSERVATION; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames.EXCEPTION_MESSAGE; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames.EXCEPTION_STACKTRACE; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames.EXCEPTION_TYPE; import static com.mongodb.internal.observability.micrometer.TracingManager.ENV_OBSERVABILITY_QUERY_TEXT_MAX_LENGTH; import static java.lang.System.getenv; import static java.util.Optional.ofNullable; @@ -81,8 +80,9 @@ public MicrometerTracer(final ObservationRegistry observationRegistry, final boo } @Override - public Span nextSpan(final String name, @Nullable final TraceContext parent, @Nullable final MongoNamespace namespace) { - Observation observation = getObservation(name); + public Span nextSpan(final MongodbObservation observationType, final String name, + @Nullable final TraceContext parent, @Nullable final MongoNamespace namespace) { + Observation observation = getObservation(observationType, name); if (parent instanceof MicrometerTraceContext) { Observation parentObservation = ((MicrometerTraceContext) parent).observation; @@ -104,8 +104,8 @@ public boolean includeCommandPayload() { return allowCommandPayload; } - private Observation getObservation(final String name) { - Observation observation = MONGODB_OBSERVATION.observation(observationRegistry, + private Observation getObservation(final MongodbObservation observationType, final String name) { + Observation observation = observationType.observation(observationRegistry, () -> new SenderContext<>((carrier, key, value) -> {}, Kind.CLIENT)) .contextualName(name); observation.getContext().put(QUERY_TEXT_LENGTH_CONTEXT_KEY, textMaxLength); diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java index 0fbfe165f5..5b60aa5d9f 100644 --- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java @@ -21,34 +21,58 @@ import io.micrometer.observation.docs.ObservationDocumentation; /** - * A MongoDB-based {@link Observation}. + * MongoDB {@link ObservationDocumentation} definitions for operation-level and command-level observations. + *

+ * These are split into two separate observation types so that each has a distinct name and a fixed set + * of low-cardinality tag keys. This is required by Prometheus which rejects meters that share a name + * but have different tag key sets. + *

* * @since 5.7 */ public enum MongodbObservation implements ObservationDocumentation { - MONGODB_OBSERVATION { + /** + * Observation for high-level MongoDB operations (e.g. find, insert, update). + * Created per user-initiated operation, may contain multiple command spans. + */ + MONGODB_OPERATION { @Override public String getName() { - return "mongodb"; + return "mongodb.operation"; } @Override public KeyName[] getLowCardinalityKeyNames() { - return LowCardinalityKeyNames.values(); + return OperationLowCardinalityKeyNames.values(); + } + }, + + /** + * Observation for wire-protocol MongoDB commands sent to the server. + * Created per actual command (nested under an operation span). + */ + MONGODB_COMMAND { + @Override + public String getName() { + return "mongodb.command"; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return CommandLowCardinalityKeyNames.values(); } @Override public KeyName[] getHighCardinalityKeyNames() { return HighCardinalityKeyNames.values(); } - }; /** - * Enums related to low cardinality key names for MongoDB tags. + * Low cardinality key names for operation-level observations. */ - public enum LowCardinalityKeyNames implements KeyName { + public enum OperationLowCardinalityKeyNames implements KeyName { SYSTEM { @Override @@ -74,22 +98,41 @@ public String asString() { return "db.operation.name"; } }, - COMMAND_NAME { + OPERATION_SUMMARY { @Override public String asString() { - return "db.command.name"; + return "db.operation.summary"; + } + } + } + + /** + * Low cardinality key names for command-level observations. + */ + public enum CommandLowCardinalityKeyNames implements KeyName { + + SYSTEM { + @Override + public String asString() { + return "db.system"; } }, - NETWORK_TRANSPORT { + NAMESPACE { @Override public String asString() { - return "network.transport"; + return "db.namespace"; } }, - OPERATION_SUMMARY { + COLLECTION { @Override public String asString() { - return "db.operation.summary"; + return "db.collection.name"; + } + }, + COMMAND_NAME { + @Override + public String asString() { + return "db.command.name"; } }, QUERY_SUMMARY { @@ -98,10 +141,10 @@ public String asString() { return "db.query.summary"; } }, - CURSOR_ID { + NETWORK_TRANSPORT { @Override public String asString() { - return "db.mongodb.cursor_id"; + return "network.transport"; } }, SERVER_ADDRESS { @@ -128,6 +171,12 @@ public String asString() { return "db.mongodb.server_connection_id"; } }, + CURSOR_ID { + @Override + public String asString() { + return "db.mongodb.cursor_id"; + } + }, TRANSACTION_NUMBER { @Override public String asString() { @@ -167,7 +216,7 @@ public String asString() { } /** - * Enums related to high cardinality (highly variable values) key names for MongoDB tags. + * High cardinality (highly variable values) key names for command-level observations. */ public enum HighCardinalityKeyNames implements KeyName { diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/Tracer.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/Tracer.java index 632580ab40..fc1ddca68d 100644 --- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/Tracer.java +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/Tracer.java @@ -30,7 +30,8 @@ public interface Tracer { Tracer NO_OP = new Tracer() { @Override - public Span nextSpan(final String name, @Nullable final TraceContext parent, @Nullable final MongoNamespace namespace) { + public Span nextSpan(final MongodbObservation observationType, final String name, + @Nullable final TraceContext parent, @Nullable final MongoNamespace namespace) { return Span.EMPTY; } @@ -46,14 +47,15 @@ public boolean includeCommandPayload() { }; /** - * Creates a new span with the specified name and optional parent trace context. + * Creates a new span with the specified observation type, name and optional parent trace context. * + * @param observationType The {@link MongodbObservation} type (operation or command). * @param name The name of the span. * @param parent The parent {@link TraceContext}, or null if no parent context is provided. * @param namespace The {@link MongoNamespace} associated with the span, or null if none is provided. * @return A {@link Span} representing the newly created span. */ - Span nextSpan(String name, @Nullable TraceContext parent, @Nullable MongoNamespace namespace); + Span nextSpan(MongodbObservation observationType, String name, @Nullable TraceContext parent, @Nullable MongoNamespace namespace); /** * Indicates whether tracing is enabled. diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/TracingManager.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/TracingManager.java index 4247ed1c3d..6f01a4c0b0 100644 --- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/TracingManager.java +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/TracingManager.java @@ -34,21 +34,10 @@ import java.util.function.Predicate; import java.util.function.Supplier; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.CLIENT_CONNECTION_ID; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.COLLECTION; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.COMMAND_NAME; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.NAMESPACE; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.OPERATION_NAME; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.OPERATION_SUMMARY; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.NETWORK_TRANSPORT; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.QUERY_SUMMARY; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.SERVER_ADDRESS; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.SERVER_CONNECTION_ID; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.SERVER_PORT; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.SESSION_ID; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.SYSTEM; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.TRANSACTION_NUMBER; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.CURSOR_ID; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.MONGODB_COMMAND; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.MONGODB_OPERATION; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.OperationLowCardinalityKeyNames; import static java.lang.System.getenv; /** @@ -109,35 +98,31 @@ public TracingManager(@Nullable final ObservabilitySettings observabilitySetting } /** - * 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. - *

+ * Creates a new span with the specified observation type, name and parent trace context. * - * @param name The name of the span. - * @param parentContext The parent trace context to associate with the span. + * @param observationType The observation type (operation or command). + * @param name The name of the span. + * @param parentContext The parent trace context to associate with the span. * @return The created span. */ - public Span addSpan(final String name, @Nullable final TraceContext parentContext) { - return tracer.nextSpan(name, parentContext, null); + public Span addSpan(final MongodbObservation observationType, final String name, + @Nullable final TraceContext parentContext) { + return tracer.nextSpan(observationType, 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). - *

+ * Creates a new span with the specified observation type, name, parent trace context, + * and MongoDB namespace. * - * @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. + * @param observationType The observation type (operation or command). + * @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); + public Span addSpan(final MongodbObservation observationType, final String name, + @Nullable final TraceContext parentContext, final MongoNamespace namespace) { + return tracer.nextSpan(observationType, name, parentContext, namespace); } /** @@ -146,8 +131,8 @@ public Span addSpan(final String name, @Nullable final TraceContext parentContex * @return The created transaction span. */ public Span addTransactionSpan() { - Span span = tracer.nextSpan("transaction", null, null); - span.tagLowCardinality(SYSTEM.withValue("mongodb")); + Span span = tracer.nextSpan(MONGODB_OPERATION, "transaction", null, null); + span.tagLowCardinality(OperationLowCardinalityKeyNames.SYSTEM.withValue("mongodb")); return span; } @@ -205,13 +190,13 @@ public Span createTracingSpan(final CommandMessage message, } Span operationSpan = operationContext.getTracingSpan(); - Span span = addSpan(commandName, operationSpan != null ? operationSpan.context() : null); + Span span = addSpan(MONGODB_COMMAND, commandName, operationSpan != null ? operationSpan.context() : null); if (command.containsKey("getMore")) { long cursorId = command.getInt64("getMore").longValue(); - span.tagLowCardinality(CURSOR_ID.withValue(String.valueOf(cursorId))); + span.tagLowCardinality(CommandLowCardinalityKeyNames.CURSOR_ID.withValue(String.valueOf(cursorId))); if (operationSpan != null) { - operationSpan.tagLowCardinality(CURSOR_ID.withValue(String.valueOf(cursorId))); + operationSpan.tagLowCardinality(CommandLowCardinalityKeyNames.CURSOR_ID.withValue(String.valueOf(cursorId))); } } @@ -234,13 +219,13 @@ public Span createTracingSpan(final CommandMessage message, 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)); + CommandLowCardinalityKeyNames.SYSTEM.withValue("mongodb"), + CommandLowCardinalityKeyNames.NAMESPACE.withValue(namespace), + CommandLowCardinalityKeyNames.QUERY_SUMMARY.withValue(summary), + CommandLowCardinalityKeyNames.COMMAND_NAME.withValue(commandName)); if (!collection.isEmpty()) { - keyValues = keyValues.and(COLLECTION.withValue(collection)); + keyValues = keyValues.and(CommandLowCardinalityKeyNames.COLLECTION.withValue(collection)); } span.tagLowCardinality(keyValues); @@ -248,19 +233,19 @@ public Span createTracingSpan(final CommandMessage message, ServerAddress serverAddress = serverAddressSupplier.get(); ConnectionId connectionId = connectionIdSupplier.get(); span.tagLowCardinality(KeyValues.of( - SERVER_ADDRESS.withValue(serverAddress.getHost()), - SERVER_PORT.withValue(String.valueOf(serverAddress.getPort())), - CLIENT_CONNECTION_ID.withValue(String.valueOf(connectionId.getLocalValue())), - SERVER_CONNECTION_ID.withValue(String.valueOf(connectionId.getServerValue())), - NETWORK_TRANSPORT.withValue(serverAddress instanceof UnixServerAddress ? "unix" : "tcp") + CommandLowCardinalityKeyNames.SERVER_ADDRESS.withValue(serverAddress.getHost()), + CommandLowCardinalityKeyNames.SERVER_PORT.withValue(String.valueOf(serverAddress.getPort())), + CommandLowCardinalityKeyNames.CLIENT_CONNECTION_ID.withValue(String.valueOf(connectionId.getLocalValue())), + CommandLowCardinalityKeyNames.SERVER_CONNECTION_ID.withValue(String.valueOf(connectionId.getServerValue())), + CommandLowCardinalityKeyNames.NETWORK_TRANSPORT.withValue(serverAddress instanceof UnixServerAddress ? "unix" : "tcp") )); // tag session and transaction info SessionContext sessionContext = operationContext.getSessionContext(); if (sessionContext.hasSession() && !sessionContext.isImplicitSession()) { span.tagLowCardinality(KeyValues.of( - TRANSACTION_NUMBER.withValue(String.valueOf(sessionContext.getTransactionNumber())), - SESSION_ID.withValue(String.valueOf(sessionContext.getSessionId() + CommandLowCardinalityKeyNames.TRANSACTION_NUMBER.withValue(String.valueOf(sessionContext.getTransactionNumber())), + CommandLowCardinalityKeyNames.SESSION_ID.withValue(String.valueOf(sessionContext.getSessionId() .get(sessionContext.getSessionId().getFirstKey()) .asBinary().asUuid())) )); @@ -298,15 +283,15 @@ public Span createOperationSpan(@Nullable final TransactionSpan transactionSpan, : "." + namespace.getCollectionName()); KeyValues keyValues = KeyValues.of( - SYSTEM.withValue("mongodb"), - NAMESPACE.withValue(namespace.getDatabaseName())); + OperationLowCardinalityKeyNames.SYSTEM.withValue("mongodb"), + OperationLowCardinalityKeyNames.NAMESPACE.withValue(namespace.getDatabaseName())); if (!MongoNamespaceHelper.COMMAND_COLLECTION_NAME.equalsIgnoreCase(namespace.getCollectionName())) { - keyValues = keyValues.and(COLLECTION.withValue(namespace.getCollectionName())); + keyValues = keyValues.and(OperationLowCardinalityKeyNames.COLLECTION.withValue(namespace.getCollectionName())); } - keyValues = keyValues.and(OPERATION_NAME.withValue(commandName), - OPERATION_SUMMARY.withValue(name)); + keyValues = keyValues.and(OperationLowCardinalityKeyNames.OPERATION_NAME.withValue(commandName), + OperationLowCardinalityKeyNames.OPERATION_SUMMARY.withValue(name)); - Span span = addSpan(name, parentContext, namespace); + Span span = addSpan(MONGODB_OPERATION, name, parentContext, namespace); span.tagLowCardinality(keyValues); operationContext.setTracingSpan(span); return span; diff --git a/driver-sync/src/test/functional/com/mongodb/client/observability/SpanTree.java b/driver-sync/src/test/functional/com/mongodb/client/observability/SpanTree.java index 7d3bff3224..2f4ad8eabe 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/observability/SpanTree.java +++ b/driver-sync/src/test/functional/com/mongodb/client/observability/SpanTree.java @@ -34,12 +34,12 @@ import java.util.UUID; import java.util.function.BiConsumer; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.CLIENT_CONNECTION_ID; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.CURSOR_ID; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.SERVER_CONNECTION_ID; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.SERVER_PORT; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.SESSION_ID; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.TRANSACTION_NUMBER; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames.CLIENT_CONNECTION_ID; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames.CURSOR_ID; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames.SERVER_CONNECTION_ID; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames.SERVER_PORT; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames.SESSION_ID; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames.TRANSACTION_NUMBER; import static org.bson.assertions.Assertions.notNull; import static org.junit.jupiter.api.Assertions.fail; From 815a250bb0f2e4aad86c9407ed1f113d055e5d4a Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 13 Apr 2026 12:28:16 +0100 Subject: [PATCH 2/7] Move high-cardinality tags from low-cardinality to high-cardinality Connection IDs, cursor IDs, session IDs, transaction numbers, and exception details were tagged as low-cardinality, causing unbounded Prometheus metric cardinality since their values change per-connection, per-cursor, or per-error. Moved CLIENT_CONNECTION_ID, SERVER_CONNECTION_ID, CURSOR_ID,TRANSACTION_NUMBER, SESSION_ID, EXCEPTION_MESSAGE, EXCEPTION_TYPE, and EXCEPTION_STACKTRACE from CommandLowCardinalityKeyNames to HighCardinalityKeyNames so they appear only in traces, not in metrics. Added tagHighCardinality(KeyValue) and tagHighCardinality(KeyValues) to the Span interface to support string-valued high-cardinality tags alongside the existing BsonDocument overload. --- .../micrometer/MicrometerTracer.java | 18 ++++++-- .../micrometer/MongodbObservation.java | 46 +++++++++---------- .../observability/micrometer/Span.java | 22 +++++++++ .../micrometer/TracingManager.java | 21 +++++---- .../client/observability/SpanTree.java | 10 ++-- 5 files changed, 77 insertions(+), 40 deletions(-) diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java index 83fc719f0c..01c9240b63 100644 --- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java @@ -33,9 +33,9 @@ import java.io.PrintWriter; import java.io.StringWriter; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames.EXCEPTION_MESSAGE; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames.EXCEPTION_STACKTRACE; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames.EXCEPTION_TYPE; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.HighCardinalityKeyNames.EXCEPTION_MESSAGE; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.HighCardinalityKeyNames.EXCEPTION_STACKTRACE; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.HighCardinalityKeyNames.EXCEPTION_TYPE; import static com.mongodb.internal.observability.micrometer.TracingManager.ENV_OBSERVABILITY_QUERY_TEXT_MAX_LENGTH; import static java.lang.System.getenv; import static java.util.Optional.ofNullable; @@ -161,6 +161,16 @@ public void tagLowCardinality(final KeyValues keyValues) { observation.lowCardinalityKeyValues(keyValues); } + @Override + public void tagHighCardinality(final KeyValue keyValue) { + observation.highCardinalityKeyValue(keyValue); + } + + @Override + public void tagHighCardinality(final KeyValues keyValues) { + observation.highCardinalityKeyValues(keyValues); + } + @Override public void tagHighCardinality(final String keyName, final BsonDocument value) { observation.highCardinalityKeyValue(keyName, @@ -176,7 +186,7 @@ public void event(final String event) { @Override public void error(final Throwable throwable) { - observation.lowCardinalityKeyValues(KeyValues.of( + observation.highCardinalityKeyValues(KeyValues.of( EXCEPTION_MESSAGE.withValue(throwable.getMessage()), EXCEPTION_TYPE.withValue(throwable.getClass().getName()), EXCEPTION_STACKTRACE.withValue(getStackTraceAsString(throwable)) diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java index 5b60aa5d9f..4cd74362d3 100644 --- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java @@ -159,6 +159,25 @@ public String asString() { return "server.port"; } }, + RESPONSE_STATUS_CODE { + @Override + public String asString() { + return "db.response.status_code"; + } + } + } + + /** + * High cardinality (highly variable values) key names for command-level observations. + */ + public enum HighCardinalityKeyNames implements KeyName { + + QUERY_TEXT { + @Override + public String asString() { + return "db.query.text"; + } + }, CLIENT_CONNECTION_ID { @Override public String asString() { @@ -189,10 +208,10 @@ public String asString() { return "db.mongodb.lsid"; } }, - EXCEPTION_STACKTRACE { + EXCEPTION_MESSAGE { @Override public String asString() { - return "exception.stacktrace"; + return "exception.message"; } }, EXCEPTION_TYPE { @@ -201,29 +220,10 @@ 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"; - } - } - } - - /** - * High cardinality (highly variable values) key names for command-level observations. - */ - public enum HighCardinalityKeyNames implements KeyName { - - QUERY_TEXT { + EXCEPTION_STACKTRACE { @Override public String asString() { - return "db.query.text"; + return "exception.stacktrace"; } } } diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/Span.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/Span.java index 84bdbb4167..f5223cda28 100644 --- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/Span.java +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/Span.java @@ -55,6 +55,14 @@ public void tagLowCardinality(final KeyValue tag) { public void tagLowCardinality(final KeyValues keyValues) { } + @Override + public void tagHighCardinality(final KeyValue keyValue) { + } + + @Override + public void tagHighCardinality(final KeyValues keyValues) { + } + @Override public void tagHighCardinality(final String keyName, final BsonDocument value) { } @@ -97,6 +105,20 @@ public MongoNamespace getNamespace() { */ void tagLowCardinality(KeyValues keyValues); + /** + * Adds a high-cardinality tag to the span. + * + * @param keyValue The key-value pair representing the tag. + */ + void tagHighCardinality(KeyValue keyValue); + + /** + * Adds multiple high-cardinality tags to the span. + * + * @param keyValues The key-value pairs representing the tags. + */ + void tagHighCardinality(KeyValues keyValues); + /** * Adds a high-cardinality (highly variable values) tag to the span with a BSON document value. * diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/TracingManager.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/TracingManager.java index 6f01a4c0b0..8d4ca8ea50 100644 --- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/TracingManager.java +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/TracingManager.java @@ -37,6 +37,7 @@ import static com.mongodb.internal.observability.micrometer.MongodbObservation.MONGODB_COMMAND; import static com.mongodb.internal.observability.micrometer.MongodbObservation.MONGODB_OPERATION; import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.HighCardinalityKeyNames; import static com.mongodb.internal.observability.micrometer.MongodbObservation.OperationLowCardinalityKeyNames; import static java.lang.System.getenv; @@ -194,9 +195,9 @@ public Span createTracingSpan(final CommandMessage message, if (command.containsKey("getMore")) { long cursorId = command.getInt64("getMore").longValue(); - span.tagLowCardinality(CommandLowCardinalityKeyNames.CURSOR_ID.withValue(String.valueOf(cursorId))); + span.tagHighCardinality(HighCardinalityKeyNames.CURSOR_ID.withValue(String.valueOf(cursorId))); if (operationSpan != null) { - operationSpan.tagLowCardinality(CommandLowCardinalityKeyNames.CURSOR_ID.withValue(String.valueOf(cursorId))); + operationSpan.tagHighCardinality(HighCardinalityKeyNames.CURSOR_ID.withValue(String.valueOf(cursorId))); } } @@ -231,21 +232,25 @@ public Span createTracingSpan(final CommandMessage message, // tag server and connection info ServerAddress serverAddress = serverAddressSupplier.get(); - ConnectionId connectionId = connectionIdSupplier.get(); span.tagLowCardinality(KeyValues.of( CommandLowCardinalityKeyNames.SERVER_ADDRESS.withValue(serverAddress.getHost()), CommandLowCardinalityKeyNames.SERVER_PORT.withValue(String.valueOf(serverAddress.getPort())), - CommandLowCardinalityKeyNames.CLIENT_CONNECTION_ID.withValue(String.valueOf(connectionId.getLocalValue())), - CommandLowCardinalityKeyNames.SERVER_CONNECTION_ID.withValue(String.valueOf(connectionId.getServerValue())), CommandLowCardinalityKeyNames.NETWORK_TRANSPORT.withValue(serverAddress instanceof UnixServerAddress ? "unix" : "tcp") )); + // tag connection info + ConnectionId connectionId = connectionIdSupplier.get(); + span.tagHighCardinality(KeyValues.of( + HighCardinalityKeyNames.CLIENT_CONNECTION_ID.withValue(String.valueOf(connectionId.getLocalValue())), + HighCardinalityKeyNames.SERVER_CONNECTION_ID.withValue(String.valueOf(connectionId.getServerValue())) + )); + // tag session and transaction info SessionContext sessionContext = operationContext.getSessionContext(); if (sessionContext.hasSession() && !sessionContext.isImplicitSession()) { - span.tagLowCardinality(KeyValues.of( - CommandLowCardinalityKeyNames.TRANSACTION_NUMBER.withValue(String.valueOf(sessionContext.getTransactionNumber())), - CommandLowCardinalityKeyNames.SESSION_ID.withValue(String.valueOf(sessionContext.getSessionId() + span.tagHighCardinality(KeyValues.of( + HighCardinalityKeyNames.TRANSACTION_NUMBER.withValue(String.valueOf(sessionContext.getTransactionNumber())), + HighCardinalityKeyNames.SESSION_ID.withValue(String.valueOf(sessionContext.getSessionId() .get(sessionContext.getSessionId().getFirstKey()) .asBinary().asUuid())) )); diff --git a/driver-sync/src/test/functional/com/mongodb/client/observability/SpanTree.java b/driver-sync/src/test/functional/com/mongodb/client/observability/SpanTree.java index 2f4ad8eabe..547b454160 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/observability/SpanTree.java +++ b/driver-sync/src/test/functional/com/mongodb/client/observability/SpanTree.java @@ -34,12 +34,12 @@ import java.util.UUID; import java.util.function.BiConsumer; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames.CLIENT_CONNECTION_ID; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames.CURSOR_ID; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames.SERVER_CONNECTION_ID; import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames.SERVER_PORT; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames.SESSION_ID; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames.TRANSACTION_NUMBER; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.HighCardinalityKeyNames.CLIENT_CONNECTION_ID; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.HighCardinalityKeyNames.CURSOR_ID; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.HighCardinalityKeyNames.SERVER_CONNECTION_ID; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.HighCardinalityKeyNames.SESSION_ID; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.HighCardinalityKeyNames.TRANSACTION_NUMBER; import static org.bson.assertions.Assertions.notNull; import static org.junit.jupiter.api.Assertions.fail; From 27b3a97fafa287fe8e669a11dcf528a94f7d376d Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 13 Apr 2026 12:52:08 +0100 Subject: [PATCH 3/7] Remove QUERY_TEXT_MAX_LENGTH from Observation.Context The query text max length configuration constant was stored in every Observation.Context and extracted back in the MicrometerSpan constructor. This value never changes between observations and is not output as any signal. Pass it directly via constructor parameter instead. --- .../micrometer/MicrometerTracer.java | 29 +++++-------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java index 01c9240b63..45cc3fa01e 100644 --- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java @@ -54,22 +54,13 @@ public class MicrometerTracer implements Tracer { private final ObservationRegistry observationRegistry; private final boolean allowCommandPayload; private final int textMaxLength; - private static final String QUERY_TEXT_LENGTH_CONTEXT_KEY = "QUERY_TEXT_MAX_LENGTH"; /** * Constructs a new {@link MicrometerTracer} instance. * * @param observationRegistry The Micrometer {@link ObservationRegistry} to delegate tracing operations to. - */ - public MicrometerTracer(final ObservationRegistry observationRegistry) { - this(observationRegistry, false, 0); - } - - /** - * Constructs a new {@link MicrometerTracer} instance with an option to allow command payloads. - * - * @param observationRegistry The Micrometer {@link ObservationRegistry} to delegate tracing operations to. * @param allowCommandPayload Whether to allow command payloads in the trace context. + * @param textMaxLength The maximum length for query text truncation. */ public MicrometerTracer(final ObservationRegistry observationRegistry, final boolean allowCommandPayload, final int textMaxLength) { this.allowCommandPayload = allowCommandPayload; @@ -91,7 +82,7 @@ public Span nextSpan(final MongodbObservation observationType, final String name } } - return new MicrometerSpan(observation.start(), namespace); + return new MicrometerSpan(observation.start(), namespace, textMaxLength); } @Override @@ -105,11 +96,9 @@ public boolean includeCommandPayload() { } private Observation getObservation(final MongodbObservation observationType, final String name) { - Observation observation = observationType.observation(observationRegistry, + return observationType.observation(observationRegistry, () -> new SenderContext<>((carrier, key, value) -> {}, Kind.CLIENT)) .contextualName(name); - observation.getContext().put(QUERY_TEXT_LENGTH_CONTEXT_KEY, textMaxLength); - return observation; } /** * Represents a Micrometer-based trace context. @@ -139,16 +128,14 @@ private static class MicrometerSpan implements Span { /** * 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. + * @param observation The Micrometer {@link Observation}, or null if none exists. + * @param namespace The MongoDB namespace associated with the span. + * @param queryTextLength The maximum length for query text truncation. */ - MicrometerSpan(final Observation observation, @Nullable final MongoNamespace namespace) { + MicrometerSpan(final Observation observation, @Nullable final MongoNamespace namespace, final int queryTextLength) { this.namespace = namespace; this.observation = observation; - this.queryTextLength = ofNullable(observation.getContext().get(QUERY_TEXT_LENGTH_CONTEXT_KEY)) - .filter(Integer.class::isInstance) - .map(Integer.class::cast) - .orElse(Integer.MAX_VALUE); + this.queryTextLength = queryTextLength; } @Override From 52efa2521dd6d476035e6fe14f6305c84e9696b4 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 13 Apr 2026 13:28:43 +0100 Subject: [PATCH 4/7] Use custom MongodbContext instead of generic SenderContext Observations were created with Micrometer's generic SenderContext, preventing users from filtering or customizing MongoDB observations by context type. This blocks the ObservationConvention pattern that Spring Boot needs for tag alignment. Introduced MongodbContext extending SenderContext with Kind.CLIENT, giving users a MongoDB-specific type to register ObservationHandler or ObservationConvention instances scoped to only MongoDB observations. --- .../micrometer/MicrometerTracer.java | 5 +-- .../micrometer/MongodbContext.java | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbContext.java diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java index 45cc3fa01e..257a83f36d 100644 --- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java @@ -22,8 +22,6 @@ import io.micrometer.common.KeyValues; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; -import io.micrometer.observation.transport.Kind; -import io.micrometer.observation.transport.SenderContext; import org.bson.BsonDocument; import org.bson.BsonReader; import org.bson.json.JsonMode; @@ -96,8 +94,7 @@ public boolean includeCommandPayload() { } private Observation getObservation(final MongodbObservation observationType, final String name) { - return observationType.observation(observationRegistry, - () -> new SenderContext<>((carrier, key, value) -> {}, Kind.CLIENT)) + return observationType.observation(observationRegistry, MongodbContext::new) .contextualName(name); } /** diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbContext.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbContext.java new file mode 100644 index 0000000000..629f777712 --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbContext.java @@ -0,0 +1,36 @@ +/* + * 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.observability.micrometer; + +import io.micrometer.observation.transport.Kind; +import io.micrometer.observation.transport.SenderContext; + +/** + * A MongoDB-specific {@link SenderContext} for Micrometer observations. + *

+ * Extends {@link SenderContext} with {@link Kind#CLIENT} to preserve the client span kind + * in the tracing bridge. Provides a MongoDB-specific type that users can filter on + * when registering {@code ObservationHandler} or {@code ObservationConvention} instances. + *

+ * + * @since 5.7 + */ +public class MongodbContext extends SenderContext { + public MongodbContext() { + super((carrier, key, value) -> { }, Kind.CLIENT); + } +} From bf91627ba3ec41841b53b93a1cd352a58f302d27 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 13 Apr 2026 18:42:46 +0100 Subject: [PATCH 5/7] Add DefaultMongodbObservationConvention and move tag production out of TracingManager Replaced all imperative tagLowCardinality/tagHighCardinality calls with a convention-based approach. TracingManager and InternalStreamConnection now populate domain fields on MongodbContext, and DefaultMongodbObservationConvention reads those fields at stop time to produce the final key-values. This decouples tag naming from span creation, enabling users to register a GlobalObservationConvention to customize tag names for their environment (e.g. Spring Boot tag alignment with their existing DefaultMongoCommandTagsProvider). Added domain fields to MongodbContext: observationType, commandName, databaseName, collectionName, serverAddress, connectionId, cursorId, transactionNumber, sessionId, queryText, responseStatusCode. Removed tagLowCardinality/tagHighCardinality from the Span interface as they are no longer used. --- config/checkstyle/suppressions.xml | 2 +- .../connection/InternalStreamConnection.java | 19 +- .../DefaultMongodbObservationConvention.java | 166 ++++++++++++++++++ .../micrometer/MicrometerTracer.java | 70 +++----- .../micrometer/MongodbContext.java | 138 +++++++++++++++ .../micrometer/MongodbObservation.java | 1 - .../observability/micrometer/Span.java | 71 +++----- .../micrometer/TracingManager.java | 104 +++++------ .../unified/UnifiedTestModifications.java | 20 --- 9 files changed, 406 insertions(+), 185 deletions(-) create mode 100644 driver-core/src/main/com/mongodb/internal/observability/micrometer/DefaultMongodbObservationConvention.java diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml index f3e6d3ef2f..b2552959cd 100644 --- a/config/checkstyle/suppressions.xml +++ b/config/checkstyle/suppressions.xml @@ -60,7 +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 5064a081c6..11ee963cfa 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java +++ b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java @@ -49,6 +49,7 @@ import com.mongodb.internal.diagnostics.logging.Logger; import com.mongodb.internal.diagnostics.logging.Loggers; import com.mongodb.internal.logging.StructuredLogger; +import com.mongodb.internal.observability.micrometer.MongodbContext; import com.mongodb.internal.observability.micrometer.Span; import com.mongodb.internal.session.SessionContext; import com.mongodb.internal.time.Timeout; @@ -94,8 +95,7 @@ import static com.mongodb.internal.connection.ProtocolHelper.getSnapshotTimestamp; import static com.mongodb.internal.connection.ProtocolHelper.isCommandOk; import static com.mongodb.internal.logging.LogMessage.Level.DEBUG; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.HighCardinalityKeyNames.QUERY_TEXT; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames.RESPONSE_STATUS_CODE; + import static com.mongodb.internal.thread.InterruptionUtil.translateInterruptedException; import static java.util.Arrays.asList; @@ -473,7 +473,7 @@ private T sendAndReceiveInternal(final CommandMessage message, final Decoder commandEventSender = new NoOpCommandEventSender(); } if (isTracingCommandPayloadNeeded) { - tracingSpan.tagHighCardinality(QUERY_TEXT.asString(), commandDocument); + tracingSpan.setQueryText(commandDocument); } try { @@ -585,7 +585,10 @@ private T receiveCommandMessageResponse(final Decoder decoder, final Comm } if (tracingSpan != null) { if (e instanceof MongoCommandException) { - tracingSpan.tagLowCardinality(RESPONSE_STATUS_CODE.withValue(String.valueOf(((MongoCommandException) e).getErrorCode()))); + MongodbContext ctx = tracingSpan.getMongodbContext(); + if (ctx != null) { + ctx.setResponseStatusCode(String.valueOf(((MongoCommandException) e).getErrorCode())); + } } tracingSpan.error(e); } @@ -639,7 +642,7 @@ private void sendAndReceiveAsyncInternal(final CommandMessage message, final commandEventSender = new NoOpCommandEventSender(); } if (isTracingCommandPayloadNeeded) { - tracingSpan.tagHighCardinality(QUERY_TEXT.asString(), commandDocument); + tracingSpan.setQueryText(commandDocument); } final Span commandSpan = tracingSpan; @@ -647,8 +650,10 @@ private void sendAndReceiveAsyncInternal(final CommandMessage message, final try { if (t != null) { if (t instanceof MongoCommandException) { - commandSpan.tagLowCardinality( - RESPONSE_STATUS_CODE.withValue(String.valueOf(((MongoCommandException) t).getErrorCode()))); + MongodbContext ctx = commandSpan.getMongodbContext(); + if (ctx != null) { + ctx.setResponseStatusCode(String.valueOf(((MongoCommandException) t).getErrorCode())); + } } commandSpan.error(t); } diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/DefaultMongodbObservationConvention.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/DefaultMongodbObservationConvention.java new file mode 100644 index 0000000000..b7b13ea5f1 --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/DefaultMongodbObservationConvention.java @@ -0,0 +1,166 @@ +/* + * 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.observability.micrometer; + +import io.micrometer.common.KeyValues; +import io.micrometer.observation.GlobalObservationConvention; +import io.micrometer.observation.Observation; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * Default {@link ObservationConvention} for MongoDB observations. + *

+ * Reads domain fields from {@link MongodbContext} and produces the standard MongoDB + * low-cardinality and high-cardinality key-values. Users can override this by registering + * a {@code GlobalObservationConvention} on their {@code ObservationRegistry}. + *

+ * + * @since 5.7 + */ +public class DefaultMongodbObservationConvention implements GlobalObservationConvention { + + @Override + public boolean supportsContext(final Observation.Context context) { + return context instanceof MongodbContext; + } + + @Override + public KeyValues getLowCardinalityKeyValues(final MongodbContext context) { + if (context.getObservationType() == MongodbObservation.MONGODB_OPERATION) { + return getOperationLowCardinalityKeyValues(context); + } else { + return getCommandLowCardinalityKeyValues(context); + } + } + + @Override + public KeyValues getHighCardinalityKeyValues(final MongodbContext context) { + if (context.getObservationType() == MongodbObservation.MONGODB_COMMAND) { + return getCommandHighCardinalityKeyValues(context); + } + return KeyValues.empty(); + } + + private KeyValues getOperationLowCardinalityKeyValues(final MongodbContext context) { + String commandName = context.getCommandName(); + String databaseName = context.getDatabaseName(); + String collectionName = context.getCollectionName(); + + KeyValues kv = KeyValues.of( + MongodbObservation.OperationLowCardinalityKeyNames.SYSTEM.withValue("mongodb")); + + if (databaseName != null) { + kv = kv.and(MongodbObservation.OperationLowCardinalityKeyNames.NAMESPACE.withValue(databaseName)); + } + if (collectionName != null) { + kv = kv.and(MongodbObservation.OperationLowCardinalityKeyNames.COLLECTION.withValue(collectionName)); + } + if (commandName != null) { + String dbName = databaseName != null ? databaseName : ""; + String summary = commandName + " " + dbName + + (collectionName != null ? "." + collectionName : ""); + kv = kv.and( + MongodbObservation.OperationLowCardinalityKeyNames.OPERATION_NAME.withValue(commandName), + MongodbObservation.OperationLowCardinalityKeyNames.OPERATION_SUMMARY.withValue(summary)); + } + return kv; + } + + private KeyValues getCommandLowCardinalityKeyValues(final MongodbContext context) { + String commandName = context.getCommandName(); + String databaseName = context.getDatabaseName(); + String collectionName = context.getCollectionName(); + String cmdName = commandName != null ? commandName : ""; + String dbName = databaseName != null ? databaseName : ""; + String summary = cmdName + " " + dbName + + (collectionName != null ? "." + collectionName : ""); + + KeyValues kv = KeyValues.of( + MongodbObservation.CommandLowCardinalityKeyNames.SYSTEM.withValue("mongodb"), + MongodbObservation.CommandLowCardinalityKeyNames.NAMESPACE.withValue(dbName), + MongodbObservation.CommandLowCardinalityKeyNames.QUERY_SUMMARY.withValue(summary), + MongodbObservation.CommandLowCardinalityKeyNames.COMMAND_NAME.withValue(cmdName)); + if (collectionName != null) { + kv = kv.and(MongodbObservation.CommandLowCardinalityKeyNames.COLLECTION.withValue(collectionName)); + } + com.mongodb.ServerAddress serverAddress = context.getServerAddress(); + if (serverAddress != null) { + kv = kv.and( + MongodbObservation.CommandLowCardinalityKeyNames.SERVER_ADDRESS.withValue(serverAddress.getHost()), + MongodbObservation.CommandLowCardinalityKeyNames.SERVER_PORT.withValue( + String.valueOf(serverAddress.getPort())), + MongodbObservation.CommandLowCardinalityKeyNames.NETWORK_TRANSPORT.withValue( + context.isUnixSocket() ? "unix" : "tcp")); + } + String responseStatusCode = context.getResponseStatusCode(); + if (responseStatusCode != null) { + kv = kv.and(MongodbObservation.CommandLowCardinalityKeyNames.RESPONSE_STATUS_CODE.withValue(responseStatusCode)); + } + return kv; + } + + private KeyValues getCommandHighCardinalityKeyValues(final MongodbContext context) { + KeyValues kv = KeyValues.empty(); + + String queryText = context.getQueryText(); + if (queryText != null) { + kv = kv.and(MongodbObservation.HighCardinalityKeyNames.QUERY_TEXT.withValue(queryText)); + } + com.mongodb.connection.ConnectionId connectionId = context.getConnectionId(); + if (connectionId != null) { + kv = kv.and( + MongodbObservation.HighCardinalityKeyNames.CLIENT_CONNECTION_ID.withValue( + String.valueOf(connectionId.getLocalValue())), + MongodbObservation.HighCardinalityKeyNames.SERVER_CONNECTION_ID.withValue( + String.valueOf(connectionId.getServerValue()))); + } + Long cursorId = context.getCursorId(); + if (cursorId != null) { + kv = kv.and(MongodbObservation.HighCardinalityKeyNames.CURSOR_ID.withValue( + String.valueOf(cursorId))); + } + Long transactionNumber = context.getTransactionNumber(); + if (transactionNumber != null) { + kv = kv.and(MongodbObservation.HighCardinalityKeyNames.TRANSACTION_NUMBER.withValue( + String.valueOf(transactionNumber))); + } + String sessionId = context.getSessionId(); + if (sessionId != null) { + kv = kv.and(MongodbObservation.HighCardinalityKeyNames.SESSION_ID.withValue(sessionId)); + } + + // Exception tags from observation error + Throwable error = context.getError(); + if (error != null) { + kv = kv.and( + MongodbObservation.HighCardinalityKeyNames.EXCEPTION_MESSAGE.withValue(error.getMessage()), + MongodbObservation.HighCardinalityKeyNames.EXCEPTION_TYPE.withValue(error.getClass().getName()), + MongodbObservation.HighCardinalityKeyNames.EXCEPTION_STACKTRACE.withValue(getStackTraceAsString(error))); + } + + return kv; + } + + private static String getStackTraceAsString(final Throwable throwable) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + throwable.printStackTrace(pw); + return sw.toString(); + } +} diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java index 257a83f36d..3dac2840da 100644 --- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java @@ -18,8 +18,6 @@ import com.mongodb.MongoNamespace; import com.mongodb.lang.Nullable; -import io.micrometer.common.KeyValue; -import io.micrometer.common.KeyValues; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import org.bson.BsonDocument; @@ -28,12 +26,8 @@ import org.bson.json.JsonWriter; import org.bson.json.JsonWriterSettings; -import java.io.PrintWriter; import java.io.StringWriter; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.HighCardinalityKeyNames.EXCEPTION_MESSAGE; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.HighCardinalityKeyNames.EXCEPTION_STACKTRACE; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.HighCardinalityKeyNames.EXCEPTION_TYPE; import static com.mongodb.internal.observability.micrometer.TracingManager.ENV_OBSERVABILITY_QUERY_TEXT_MAX_LENGTH; import static java.lang.System.getenv; import static java.util.Optional.ofNullable; @@ -66,6 +60,10 @@ public MicrometerTracer(final ObservationRegistry observationRegistry, final boo this.textMaxLength = ofNullable(getenv(ENV_OBSERVABILITY_QUERY_TEXT_MAX_LENGTH)) .map(Integer::parseInt) .orElse(textMaxLength); + // Register default convention. Users can override by registering their own GlobalObservationConvention + // after the MongoClient is created — the last matching convention wins. + DefaultMongodbObservationConvention defaultConvention = new DefaultMongodbObservationConvention(); + observationRegistry.observationConfig().observationConvention(defaultConvention); } @Override @@ -94,8 +92,11 @@ public boolean includeCommandPayload() { } private Observation getObservation(final MongodbObservation observationType, final String name) { - return observationType.observation(observationRegistry, MongodbContext::new) - .contextualName(name); + return observationType.observation(observationRegistry, () -> { + MongodbContext ctx = new MongodbContext(); + ctx.setObservationType(observationType); + return ctx; + }).contextualName(name); } /** * Represents a Micrometer-based trace context. @@ -136,31 +137,13 @@ private static class MicrometerSpan implements Span { } @Override - public void tagLowCardinality(final KeyValue keyValue) { - observation.lowCardinalityKeyValue(keyValue); - } - - @Override - public void tagLowCardinality(final KeyValues keyValues) { - observation.lowCardinalityKeyValues(keyValues); - } - - @Override - public void tagHighCardinality(final KeyValue keyValue) { - observation.highCardinalityKeyValue(keyValue); - } - - @Override - public void tagHighCardinality(final KeyValues keyValues) { - observation.highCardinalityKeyValues(keyValues); - } - - @Override - public void tagHighCardinality(final String keyName, final BsonDocument value) { - observation.highCardinalityKeyValue(keyName, - (queryTextLength < Integer.MAX_VALUE) // truncate values that are too long - ? getTruncatedBsonDocument(value) - : value.toString()); + public void setQueryText(final BsonDocument commandDocument) { + MongodbContext ctx = getMongodbContext(); + if (ctx != null) { + ctx.setQueryText((queryTextLength < Integer.MAX_VALUE) + ? getTruncatedBsonDocument(commandDocument) + : commandDocument.toString()); + } } @Override @@ -170,11 +153,6 @@ public void event(final String event) { @Override public void error(final Throwable throwable) { - observation.highCardinalityKeyValues(KeyValues.of( - EXCEPTION_MESSAGE.withValue(throwable.getMessage()), - EXCEPTION_TYPE.withValue(throwable.getClass().getName()), - EXCEPTION_STACKTRACE.withValue(getStackTraceAsString(throwable)) - )); observation.error(throwable); } @@ -190,15 +168,17 @@ public TraceContext context() { @Override @Nullable - public MongoNamespace getNamespace() { - return namespace; + public MongodbContext getMongodbContext() { + if (observation.getContext() instanceof MongodbContext) { + return (MongodbContext) observation.getContext(); + } + return null; } - private String getStackTraceAsString(final Throwable throwable) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - throwable.printStackTrace(pw); - return sw.toString(); + @Override + @Nullable + public MongoNamespace getNamespace() { + return namespace; } private String getTruncatedBsonDocument(final BsonDocument commandDocument) { diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbContext.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbContext.java index 629f777712..db7c54977f 100644 --- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbContext.java +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbContext.java @@ -16,6 +16,9 @@ package com.mongodb.internal.observability.micrometer; +import com.mongodb.ServerAddress; +import com.mongodb.connection.ConnectionId; +import com.mongodb.lang.Nullable; import io.micrometer.observation.transport.Kind; import io.micrometer.observation.transport.SenderContext; @@ -26,11 +29,146 @@ * in the tracing bridge. Provides a MongoDB-specific type that users can filter on * when registering {@code ObservationHandler} or {@code ObservationConvention} instances. *

+ *

+ * Domain fields (commandName, databaseName, etc.) are populated by the driver after + * the observation is started and before it is stopped. The {@code ObservationConvention} + * reads these fields at stop time to produce the final tag key-values. + *

* * @since 5.7 */ public class MongodbContext extends SenderContext { + + private MongodbObservation observationType; + @Nullable + private String commandName; + @Nullable + private String databaseName; + @Nullable + private String collectionName; + @Nullable + private ServerAddress serverAddress; + @Nullable + private ConnectionId connectionId; + @Nullable + private Long cursorId; + @Nullable + private Long transactionNumber; + @Nullable + private String sessionId; + @Nullable + private String queryText; + @Nullable + private String responseStatusCode; + private boolean isUnixSocket; + public MongodbContext() { super((carrier, key, value) -> { }, Kind.CLIENT); } + + @Nullable + public String getCommandName() { + return commandName; + } + + public void setCommandName(@Nullable final String commandName) { + this.commandName = commandName; + } + + @Nullable + public String getDatabaseName() { + return databaseName; + } + + public void setDatabaseName(@Nullable final String databaseName) { + this.databaseName = databaseName; + } + + @Nullable + public String getCollectionName() { + return collectionName; + } + + public void setCollectionName(@Nullable final String collectionName) { + this.collectionName = collectionName; + } + + @Nullable + public ServerAddress getServerAddress() { + return serverAddress; + } + + public void setServerAddress(@Nullable final ServerAddress serverAddress) { + this.serverAddress = serverAddress; + } + + @Nullable + public ConnectionId getConnectionId() { + return connectionId; + } + + public void setConnectionId(@Nullable final ConnectionId connectionId) { + this.connectionId = connectionId; + } + + public MongodbObservation getObservationType() { + return observationType; + } + + public void setObservationType(final MongodbObservation observationType) { + this.observationType = observationType; + } + + @Nullable + public Long getCursorId() { + return cursorId; + } + + public void setCursorId(@Nullable final Long cursorId) { + this.cursorId = cursorId; + } + + @Nullable + public Long getTransactionNumber() { + return transactionNumber; + } + + public void setTransactionNumber(@Nullable final Long transactionNumber) { + this.transactionNumber = transactionNumber; + } + + @Nullable + public String getSessionId() { + return sessionId; + } + + public void setSessionId(@Nullable final String sessionId) { + this.sessionId = sessionId; + } + + public boolean isUnixSocket() { + return isUnixSocket; + } + + public void setUnixSocket(final boolean unixSocket) { + isUnixSocket = unixSocket; + } + + @Nullable + public String getQueryText() { + return queryText; + } + + public void setQueryText(@Nullable final String queryText) { + this.queryText = queryText; + } + + @Nullable + public String getResponseStatusCode() { + return responseStatusCode; + } + + public void setResponseStatusCode(@Nullable final String responseStatusCode) { + this.responseStatusCode = responseStatusCode; + } } diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java index 4cd74362d3..69935c5b70 100644 --- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java @@ -17,7 +17,6 @@ package com.mongodb.internal.observability.micrometer; import io.micrometer.common.docs.KeyName; -import io.micrometer.observation.Observation; import io.micrometer.observation.docs.ObservationDocumentation; /** diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/Span.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/Span.java index f5223cda28..fbd57bece2 100644 --- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/Span.java +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/Span.java @@ -18,8 +18,6 @@ import com.mongodb.MongoNamespace; import com.mongodb.lang.Nullable; -import io.micrometer.common.KeyValue; -import io.micrometer.common.KeyValues; import org.bson.BsonDocument; /** @@ -48,23 +46,7 @@ public interface Span { */ Span EMPTY = new Span() { @Override - public void tagLowCardinality(final KeyValue tag) { - } - - @Override - public void tagLowCardinality(final KeyValues keyValues) { - } - - @Override - public void tagHighCardinality(final KeyValue keyValue) { - } - - @Override - public void tagHighCardinality(final KeyValues keyValues) { - } - - @Override - public void tagHighCardinality(final String keyName, final BsonDocument value) { + public void setQueryText(final BsonDocument commandDocument) { } @Override @@ -89,43 +71,21 @@ public TraceContext context() { public MongoNamespace getNamespace() { return null; } - }; - - /** - * Adds a low-cardinality tag to the span. - * - * @param keyValue The key-value pair representing the tag. - */ - void tagLowCardinality(KeyValue keyValue); - - /** - * Adds multiple low-cardinality tags to the span. - * - * @param keyValues The key-value pairs representing the tags. - */ - void tagLowCardinality(KeyValues keyValues); - - /** - * Adds a high-cardinality tag to the span. - * - * @param keyValue The key-value pair representing the tag. - */ - void tagHighCardinality(KeyValue keyValue); - /** - * Adds multiple high-cardinality tags to the span. - * - * @param keyValues The key-value pairs representing the tags. - */ - void tagHighCardinality(KeyValues keyValues); + @Override + @Nullable + public MongodbContext getMongodbContext() { + return null; + } + }; /** - * Adds a high-cardinality (highly variable values) tag to the span with a BSON document value. + * Sets the query text on the observation context from the given command document. + * The document is converted to a JSON string and may be truncated based on configuration. * - * @param keyName The name of the tag. - * @param value The BSON document representing the value of the tag. + * @param commandDocument The BSON command document. */ - void tagHighCardinality(String keyName, BsonDocument value); + void setQueryText(BsonDocument commandDocument); /** * Records an event in the span. @@ -153,6 +113,15 @@ public MongoNamespace getNamespace() { */ TraceContext context(); + /** + * Retrieves the {@link MongodbContext} associated with the span, if any. + * Returns null for no-op spans or non-Micrometer implementations. + * + * @return The MongoDB observation context, or null. + */ + @Nullable + MongodbContext getMongodbContext(); + /** * Retrieves the MongoDB namespace associated with the span, if any. * diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/TracingManager.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/TracingManager.java index 8d4ca8ea50..51b0bec614 100644 --- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/TracingManager.java +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/TracingManager.java @@ -27,7 +27,6 @@ import com.mongodb.lang.Nullable; import com.mongodb.observability.ObservabilitySettings; import com.mongodb.observability.micrometer.MicrometerObservabilitySettings; -import io.micrometer.common.KeyValues; import io.micrometer.observation.ObservationRegistry; import org.bson.BsonDocument; @@ -36,9 +35,6 @@ import static com.mongodb.internal.observability.micrometer.MongodbObservation.MONGODB_COMMAND; import static com.mongodb.internal.observability.micrometer.MongodbObservation.MONGODB_OPERATION; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.HighCardinalityKeyNames; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.OperationLowCardinalityKeyNames; import static java.lang.System.getenv; /** @@ -132,9 +128,7 @@ public Span addSpan(final MongodbObservation observationType, final String name, * @return The created transaction span. */ public Span addTransactionSpan() { - Span span = tracer.nextSpan(MONGODB_OPERATION, "transaction", null, null); - span.tagLowCardinality(OperationLowCardinalityKeyNames.SYSTEM.withValue("mongodb")); - return span; + return tracer.nextSpan(MONGODB_OPERATION, "transaction", null, null); } /** @@ -193,15 +187,7 @@ public Span createTracingSpan(final CommandMessage message, Span operationSpan = operationContext.getTracingSpan(); Span span = addSpan(MONGODB_COMMAND, commandName, operationSpan != null ? operationSpan.context() : null); - if (command.containsKey("getMore")) { - long cursorId = command.getInt64("getMore").longValue(); - span.tagHighCardinality(HighCardinalityKeyNames.CURSOR_ID.withValue(String.valueOf(cursorId))); - if (operationSpan != null) { - operationSpan.tagHighCardinality(HighCardinalityKeyNames.CURSOR_ID.withValue(String.valueOf(cursorId))); - } - } - - // Tag namespace + // Resolve namespace from parent operation span or message String namespace; String collection = ""; if (operationSpan != null) { @@ -217,43 +203,40 @@ public Span createTracingSpan(final CommandMessage message, } else { namespace = message.getDatabase(); } - String summary = commandName + " " + namespace + (collection.isEmpty() ? "" : "." + collection); - KeyValues keyValues = KeyValues.of( - CommandLowCardinalityKeyNames.SYSTEM.withValue("mongodb"), - CommandLowCardinalityKeyNames.NAMESPACE.withValue(namespace), - CommandLowCardinalityKeyNames.QUERY_SUMMARY.withValue(summary), - CommandLowCardinalityKeyNames.COMMAND_NAME.withValue(commandName)); + // Populate domain fields on MongodbContext — the convention reads these to produce tags + MongodbContext mongodbContext = span.getMongodbContext(); + if (mongodbContext != null) { + mongodbContext.setCommandName(commandName); + mongodbContext.setDatabaseName(namespace); + if (!collection.isEmpty()) { + mongodbContext.setCollectionName(collection); + } - if (!collection.isEmpty()) { - keyValues = keyValues.and(CommandLowCardinalityKeyNames.COLLECTION.withValue(collection)); - } - span.tagLowCardinality(keyValues); - - // tag server and connection info - ServerAddress serverAddress = serverAddressSupplier.get(); - span.tagLowCardinality(KeyValues.of( - CommandLowCardinalityKeyNames.SERVER_ADDRESS.withValue(serverAddress.getHost()), - CommandLowCardinalityKeyNames.SERVER_PORT.withValue(String.valueOf(serverAddress.getPort())), - CommandLowCardinalityKeyNames.NETWORK_TRANSPORT.withValue(serverAddress instanceof UnixServerAddress ? "unix" : "tcp") - )); - - // tag connection info - ConnectionId connectionId = connectionIdSupplier.get(); - span.tagHighCardinality(KeyValues.of( - HighCardinalityKeyNames.CLIENT_CONNECTION_ID.withValue(String.valueOf(connectionId.getLocalValue())), - HighCardinalityKeyNames.SERVER_CONNECTION_ID.withValue(String.valueOf(connectionId.getServerValue())) - )); - - // tag session and transaction info - SessionContext sessionContext = operationContext.getSessionContext(); - if (sessionContext.hasSession() && !sessionContext.isImplicitSession()) { - span.tagHighCardinality(KeyValues.of( - HighCardinalityKeyNames.TRANSACTION_NUMBER.withValue(String.valueOf(sessionContext.getTransactionNumber())), - HighCardinalityKeyNames.SESSION_ID.withValue(String.valueOf(sessionContext.getSessionId() - .get(sessionContext.getSessionId().getFirstKey()) - .asBinary().asUuid())) - )); + ServerAddress serverAddress = serverAddressSupplier.get(); + mongodbContext.setServerAddress(serverAddress); + mongodbContext.setUnixSocket(serverAddress instanceof UnixServerAddress); + + ConnectionId connectionId = connectionIdSupplier.get(); + mongodbContext.setConnectionId(connectionId); + + if (command.containsKey("getMore")) { + long cursorId = command.getInt64("getMore").longValue(); + mongodbContext.setCursorId(cursorId); + // Also set on parent operation span context for correlation + MongodbContext parentContext = operationSpan != null ? operationSpan.getMongodbContext() : null; + if (parentContext != null) { + parentContext.setCursorId(cursorId); + } + } + + SessionContext sessionContext = operationContext.getSessionContext(); + if (sessionContext.hasSession() && !sessionContext.isImplicitSession()) { + mongodbContext.setTransactionNumber(sessionContext.getTransactionNumber()); + mongodbContext.setSessionId(String.valueOf(sessionContext.getSessionId() + .get(sessionContext.getSessionId().getFirstKey()) + .asBinary().asUuid())); + } } return span; @@ -287,17 +270,18 @@ public Span createOperationSpan(@Nullable final TransactionSpan transactionSpan, ? "" : "." + namespace.getCollectionName()); - KeyValues keyValues = KeyValues.of( - OperationLowCardinalityKeyNames.SYSTEM.withValue("mongodb"), - OperationLowCardinalityKeyNames.NAMESPACE.withValue(namespace.getDatabaseName())); - if (!MongoNamespaceHelper.COMMAND_COLLECTION_NAME.equalsIgnoreCase(namespace.getCollectionName())) { - keyValues = keyValues.and(OperationLowCardinalityKeyNames.COLLECTION.withValue(namespace.getCollectionName())); + Span span = addSpan(MONGODB_OPERATION, name, parentContext, namespace); + + // Populate domain fields on MongodbContext — the convention reads these to produce tags + MongodbContext mongodbContext = span.getMongodbContext(); + if (mongodbContext != null) { + mongodbContext.setCommandName(commandName); + mongodbContext.setDatabaseName(namespace.getDatabaseName()); + if (!MongoNamespaceHelper.COMMAND_COLLECTION_NAME.equalsIgnoreCase(namespace.getCollectionName())) { + mongodbContext.setCollectionName(namespace.getCollectionName()); + } } - keyValues = keyValues.and(OperationLowCardinalityKeyNames.OPERATION_NAME.withValue(commandName), - OperationLowCardinalityKeyNames.OPERATION_SUMMARY.withValue(name)); - Span span = addSpan(MONGODB_OPERATION, name, parentContext, namespace); - span.tagLowCardinality(keyValues); operationContext.setTracingSpan(span); return span; } 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 02d097688e..fcfb0fc6c7 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 @@ -197,26 +197,6 @@ public static void applyCustomizations(final TestDef def) { .test("client-side-operations-timeout", "timeoutMS can be configured on a MongoClient", "timeoutMS can be set to 0 on a MongoClient - dropIndexes on collection"); - // OpenTelemetry - def.skipJira("https://jira.mongodb.org/browse/JAVA-5991") - .file("open-telemetry/tests", "operation find") - .file("open-telemetry/tests", "operation find_one_and_update") - .file("open-telemetry/tests", "operation update") - .file("open-telemetry/tests", "operation bulk_write") - .file("open-telemetry/tests", "operation drop collection") - .file("open-telemetry/tests", "transaction spans") - .file("open-telemetry/tests", "convenient transactions") - .file("open-telemetry/tests", "operation atlas_search") - .file("open-telemetry/tests", "operation insert") - .file("open-telemetry/tests", "operation map_reduce") - .file("open-telemetry/tests", "operation find without db.query.text") - .file("open-telemetry/tests", "operation find_retries"); - def.skipAccordingToSpec("Micrometer tests expect the network transport to be tcp") - .when(ClusterFixture::isUnixSocket) - .directory("open-telemetry/tests"); - def.skipJira("https://jira.mongodb.org/browse/JAVA-6094 TODO-JAVA-6094") - .directory("open-telemetry/tests"); - // TODO-JAVA-5712 // collection-management From da4a3c1ed859a79af5edaf298033f36bb4f44ea8 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Tue, 14 Apr 2026 11:54:37 +0100 Subject: [PATCH 6/7] Fixes JAVA-6094 Update attribute name for OpenTelemetry --- .../internal/observability/micrometer/MongodbObservation.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java index 69935c5b70..0824d28c37 100644 --- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java @@ -76,7 +76,7 @@ public enum OperationLowCardinalityKeyNames implements KeyName { SYSTEM { @Override public String asString() { - return "db.system"; + return "db.system.name"; } }, NAMESPACE { @@ -113,7 +113,7 @@ public enum CommandLowCardinalityKeyNames implements KeyName { SYSTEM { @Override public String asString() { - return "db.system"; + return "db.system.name"; } }, NAMESPACE { From cf4660c03418469fd68ab2dcef976b1b28e97796 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Tue, 14 Apr 2026 13:17:05 +0100 Subject: [PATCH 7/7] Open and close observation scopes for sync driver context propagation The driver called observation.start()/stop() but never openScope()/ closeScope(). Without scopes, registry.getCurrentObservation() returned null during MongoDB operations, breaking context propagation for any downstream code (Spring interceptors, user observations, MDC logging). For example, in withTransaction, a user observation created inside the callback would attach to the Spring HTTP parent instead of the MongoDB transaction span, because the transaction observation was never made "current" on the thread. Added openScope()/closeScope() to the Span interface with scope lifecycle management in MongoClusterImpl (operation spans), InternalStreamConnection (command spans), and TransactionSpan. --- .../connection/InternalStreamConnection.java | 7 +++++++ .../micrometer/MicrometerTracer.java | 15 +++++++++++++++ .../observability/micrometer/Span.java | 19 +++++++++++++++++++ .../micrometer/TransactionSpan.java | 4 ++++ .../client/internal/MongoClusterImpl.java | 9 ++++++++- .../com/mongodb/client/unified/Entities.java | 9 +++++++++ 6 files changed, 62 insertions(+), 1 deletion(-) 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 11ee963cfa..98b9ab800d 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java +++ b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java @@ -454,6 +454,9 @@ private T sendAndReceiveInternal(final CommandMessage message, final Decoder () -> getDescription().getServerAddress(), () -> getDescription().getConnectionId() ); + if (tracingSpan != null) { + tracingSpan.openScope(); + } boolean isLoggingCommandNeeded = isLoggingCommandNeeded(); boolean isTracingCommandPayloadNeeded = tracingSpan != null && operationContext.getTracingManager().isCommandPayloadEnabled(); @@ -481,6 +484,8 @@ private T sendAndReceiveInternal(final CommandMessage message, final Decoder } catch (Exception e) { if (tracingSpan != null) { tracingSpan.error(e); + tracingSpan.closeScope(); + tracingSpan.end(); } commandEventSender.sendFailedEvent(e); throw e; @@ -492,6 +497,7 @@ private T sendAndReceiveInternal(final CommandMessage message, final Decoder } else { commandEventSender.sendSucceededEventForOneWayCommand(); if (tracingSpan != null) { + tracingSpan.closeScope(); tracingSpan.end(); } return null; @@ -595,6 +601,7 @@ private T receiveCommandMessageResponse(final Decoder decoder, final Comm throw e; } finally { if (tracingSpan != null) { + tracingSpan.closeScope(); tracingSpan.end(); } } diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java index 3dac2840da..4ea9c7e159 100644 --- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java @@ -122,6 +122,8 @@ private static class MicrometerSpan implements Span { @Nullable private final MongoNamespace namespace; private final int queryTextLength; + @Nullable + private Observation.Scope scope; /** * Constructs a new {@link MicrometerSpan} instance with an associated Observation and MongoDB namespace. @@ -136,6 +138,19 @@ private static class MicrometerSpan implements Span { this.queryTextLength = queryTextLength; } + @Override + public void openScope() { + this.scope = observation.openScope(); + } + + @Override + public void closeScope() { + if (scope != null) { + scope.close(); + scope = null; + } + } + @Override public void setQueryText(final BsonDocument commandDocument) { MongodbContext ctx = getMongodbContext(); diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/Span.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/Span.java index fbd57bece2..372a54c7da 100644 --- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/Span.java +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/Span.java @@ -45,6 +45,14 @@ public interface Span { *

*/ Span EMPTY = new Span() { + @Override + public void openScope() { + } + + @Override + public void closeScope() { + } + @Override public void setQueryText(final BsonDocument commandDocument) { } @@ -79,6 +87,17 @@ public MongodbContext getMongodbContext() { } }; + /** + * Opens a scope for this span, making it the current observation on the thread. + * Must be paired with {@link #closeScope()} in a try-finally block. + */ + void openScope(); + + /** + * Closes the scope previously opened by {@link #openScope()}, restoring the previous observation. + */ + void closeScope(); + /** * Sets the query text on the observation context from the given command document. * The document is converted to a JSON string and may be truncated based on configuration. diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/TransactionSpan.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/TransactionSpan.java index 3d16d18a97..ad25ba902d 100644 --- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/TransactionSpan.java +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/TransactionSpan.java @@ -31,6 +31,7 @@ public class TransactionSpan { public TransactionSpan(final TracingManager tracingManager) { this.span = tracingManager.addTransactionSpan(); + this.span.openScope(); } /** @@ -54,6 +55,7 @@ public void handleTransactionSpanError(final Throwable e) { } if (!isConvenientTransaction) { + span.closeScope(); span.end(); } } @@ -67,6 +69,7 @@ public void finalizeTransactionSpan(final String status) { span.event(status); // clear previous commit error if any if (!isConvenientTransaction) { + span.closeScope(); span.end(); } reportedError = null; // clear previous commit error if any @@ -82,6 +85,7 @@ public void spanFinalizing(final boolean cleanupTransactionContext) { if (reportedError != null) { span.error(reportedError); } + span.closeScope(); 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) 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 eb36678761..0f83fd4e70 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java +++ b/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java @@ -426,9 +426,11 @@ public T execute(final ReadOperation operation, final ReadPreference r .withSessionContext(new ClientSessionBinding.SyncClientSessionContext(actualClientSession, readConcern, implicitSession)); Span span = operationContext.getTracingManager().createOperationSpan( actualClientSession.getTransactionSpan(), operationContext, operation.getCommandName(), operation.getNamespace()); + if (span != null) { + span.openScope(); + } ReadBinding binding = getReadBinding(readPreference, actualClientSession, implicitSession); - try { if (actualClientSession.hasActiveTransaction() && !binding.getReadPreference().equals(primary())) { throw new MongoClientException("Read preference in a transaction must be primary"); @@ -445,6 +447,7 @@ public T execute(final ReadOperation operation, final ReadPreference r } finally { binding.release(); if (span != null) { + span.closeScope(); span.end(); } } @@ -462,6 +465,9 @@ public T execute(final WriteOperation operation, final ReadConcern readCo .withSessionContext(new ClientSessionBinding.SyncClientSessionContext(actualClientSession, readConcern, isImplicitSession(session))); Span span = operationContext.getTracingManager().createOperationSpan( actualClientSession.getTransactionSpan(), operationContext, operation.getCommandName(), operation.getNamespace()); + if (span != null) { + span.openScope(); + } WriteBinding binding = getWriteBinding(actualClientSession, isImplicitSession(session)); try { @@ -477,6 +483,7 @@ public T execute(final WriteOperation operation, final ReadConcern readCo } finally { binding.release(); if (span != null) { + span.closeScope(); span.end(); } } 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 12a4cb5db5..478f84e74d 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 @@ -49,6 +49,7 @@ import com.mongodb.lang.Nullable; import com.mongodb.logging.TestLoggingInterceptor; import com.mongodb.observability.ObservabilitySettings; +import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import io.micrometer.tracing.test.reporter.inmemory.InMemoryOtelSetup; import org.bson.BsonArray; @@ -113,6 +114,7 @@ public final class Entities { private final Map clientLoggingInterceptors = new HashMap<>(); private final Map clientTracing = new HashMap<>(); private final Set inMemoryOTelInstances = new HashSet<>(); + private final Set observationScopes = new HashSet<>(); private final Map clientConnectionPoolListeners = new HashMap<>(); private final Map clientServerListeners = new HashMap<>(); private final Map clientClusterListeners = new HashMap<>(); @@ -593,6 +595,12 @@ private void initClient(final BsonDocument entity, final String id, .observabilitySettings(ObservabilitySettings.micrometerBuilder() .observationRegistry(observationRegistry) .enableCommandPayloadTracing(enableCommandPayload).build()); + + // Simulate what Spring Boot's observation does + // open a parent observation's scope before running the MongoDB operation + Observation parentObservation = Observation.createNotStarted("http.request", observationRegistry) + .start(); + observationScopes.add(parentObservation.openScope()); } MongoClientSettings clientSettings = clientSettingsBuilder.build(); @@ -816,5 +824,6 @@ public void close() { clientLoggingInterceptors.values().forEach(TestLoggingInterceptor::close); threads.values().forEach(ExecutorService::shutdownNow); inMemoryOTelInstances.forEach(InMemoryOtelSetup::close); + observationScopes.forEach(Observation.Scope::close); } }