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
+ *
+ * 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);
}
}