Skip to content
2 changes: 1 addition & 1 deletion config/checkstyle/suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@

<!-- Allow printStackTrace in this file -->
<suppress checks="Regexp" files="CallbackResultHolder"/>
<suppress checks="Regexp" files="MicrometerTracer"/>
<suppress checks="Regexp" files="DefaultMongodbObservationConvention"/>

<!--Do not check documentation tests classes -->
<suppress checks="Javadoc*" files=".*documentation.*"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -454,6 +454,9 @@ private <T> 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();
Expand All @@ -473,14 +476,16 @@ private <T> T sendAndReceiveInternal(final CommandMessage message, final Decoder
commandEventSender = new NoOpCommandEventSender();
}
if (isTracingCommandPayloadNeeded) {
tracingSpan.tagHighCardinality(QUERY_TEXT.asString(), commandDocument);
tracingSpan.setQueryText(commandDocument);
}
Comment on lines 457 to 480
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tracingSpan.openScope() happens before command document hydration and before the sendCommandMessage try/catch. If any exception is thrown between openScope() and the later close sites (e.g., message.getCommandDocument(bsonOutput), event sender construction), the scope will leak. Enclose the entire post-openScope() section in a try/finally that closes the scope (and ends the span as appropriate) on all exceptional paths.

Copilot uses AI. Check for mistakes.

try {
sendCommandMessage(message, bsonOutput, operationContext);
} catch (Exception e) {
if (tracingSpan != null) {
tracingSpan.error(e);
tracingSpan.closeScope();
tracingSpan.end();
}
commandEventSender.sendFailedEvent(e);
throw e;
Expand All @@ -492,6 +497,7 @@ private <T> T sendAndReceiveInternal(final CommandMessage message, final Decoder
} else {
commandEventSender.sendSucceededEventForOneWayCommand();
if (tracingSpan != null) {
tracingSpan.closeScope();
tracingSpan.end();
}
return null;
Expand Down Expand Up @@ -585,13 +591,17 @@ private <T> T receiveCommandMessageResponse(final Decoder<T> 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();
}
}
Expand Down Expand Up @@ -639,16 +649,18 @@ private <T> void sendAndReceiveAsyncInternal(final CommandMessage message, final
commandEventSender = new NoOpCommandEventSender();
}
if (isTracingCommandPayloadNeeded) {
tracingSpan.tagHighCardinality(QUERY_TEXT.asString(), commandDocument);
tracingSpan.setQueryText(commandDocument);
}

final Span commandSpan = tracingSpan;
SingleResultCallback<T> tracingCallback = commandSpan == null ? callback : (result, t) -> {
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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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<MongodbContext>} on their {@code ObservationRegistry}.
* </p>
*
* @since 5.7
*/
public class DefaultMongodbObservationConvention implements GlobalObservationConvention<MongodbContext> {

@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();
}
}
Loading