diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml
index f3e6d3ef2ff..b2552959cdf 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 7e454debedd..98b9ab800de 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.LowCardinalityKeyNames.RESPONSE_STATUS_CODE;
+
import static com.mongodb.internal.thread.InterruptionUtil.translateInterruptedException;
import static java.util.Arrays.asList;
@@ -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();
@@ -473,7 +476,7 @@ private T sendAndReceiveInternal(final CommandMessage message, final Decoder
commandEventSender = new NoOpCommandEventSender();
}
if (isTracingCommandPayloadNeeded) {
- tracingSpan.tagHighCardinality(QUERY_TEXT.asString(), commandDocument);
+ tracingSpan.setQueryText(commandDocument);
}
try {
@@ -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;
@@ -585,13 +591,17 @@ 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);
}
throw e;
} finally {
if (tracingSpan != null) {
+ tracingSpan.closeScope();
tracingSpan.end();
}
}
@@ -639,7 +649,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 +657,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 00000000000..b7b13ea5f1f
--- /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 a7204a01a71..4ea9c7e159e 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,25 +18,16 @@
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 io.micrometer.observation.transport.Kind;
-import io.micrometer.observation.transport.SenderContext;
import org.bson.BsonDocument;
import org.bson.BsonReader;
import org.bson.json.JsonMode;
import org.bson.json.JsonWriter;
import org.bson.json.JsonWriterSettings;
-import java.io.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.TracingManager.ENV_OBSERVABILITY_QUERY_TEXT_MAX_LENGTH;
import static java.lang.System.getenv;
import static java.util.Optional.ofNullable;
@@ -55,22 +46,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;
@@ -78,11 +60,16 @@ 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
- 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;
@@ -91,7 +78,7 @@ public Span nextSpan(final String name, @Nullable final TraceContext parent, @Nu
}
}
- return new MicrometerSpan(observation.start(), namespace);
+ return new MicrometerSpan(observation.start(), namespace, textMaxLength);
}
@Override
@@ -104,12 +91,12 @@ public boolean includeCommandPayload() {
return allowCommandPayload;
}
- private Observation getObservation(final String name) {
- Observation observation = MONGODB_OBSERVATION.observation(observationRegistry,
- () -> new SenderContext<>((carrier, key, value) -> {}, Kind.CLIENT))
- .contextualName(name);
- observation.getContext().put(QUERY_TEXT_LENGTH_CONTEXT_KEY, textMaxLength);
- return observation;
+ private Observation getObservation(final MongodbObservation observationType, final String name) {
+ return observationType.observation(observationRegistry, () -> {
+ MongodbContext ctx = new MongodbContext();
+ ctx.setObservationType(observationType);
+ return ctx;
+ }).contextualName(name);
}
/**
* Represents a Micrometer-based trace context.
@@ -135,38 +122,43 @@ 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.
*
- * @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
- public void tagLowCardinality(final KeyValue keyValue) {
- observation.lowCardinalityKeyValue(keyValue);
+ public void openScope() {
+ this.scope = observation.openScope();
}
@Override
- public void tagLowCardinality(final KeyValues keyValues) {
- observation.lowCardinalityKeyValues(keyValues);
+ public void closeScope() {
+ if (scope != null) {
+ scope.close();
+ scope = null;
+ }
}
@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
@@ -176,11 +168,6 @@ public void event(final String event) {
@Override
public void error(final Throwable throwable) {
- observation.lowCardinalityKeyValues(KeyValues.of(
- EXCEPTION_MESSAGE.withValue(throwable.getMessage()),
- EXCEPTION_TYPE.withValue(throwable.getClass().getName()),
- EXCEPTION_STACKTRACE.withValue(getStackTraceAsString(throwable))
- ));
observation.error(throwable);
}
@@ -196,15 +183,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
new file mode 100644
index 00000000000..db7c54977fc
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbContext.java
@@ -0,0 +1,174 @@
+/*
+ * 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 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;
+
+/**
+ * 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.
+ *
+ *
+ * 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.
+ *