diff --git a/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java b/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java index 78cc03ea76b..d0d253cba47 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java +++ b/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java @@ -419,6 +419,14 @@ private static boolean isServerVersionKnown(final MessageSettings settings) { * * @return the database name */ + /** + * The name of the command, i.e. the first key of the command document. Available without decoding the + * encoded message. + */ + public String getCommandName() { + return command.getFirstKey(); + } + public String getDatabase() { return database; } 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 fa5c6223daa..8f9257f984e 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 @@ -82,7 +82,14 @@ public Span nextSpan(final MongodbObservation observationType, final String name } } - return new MicrometerSpan(observation.start(), namespace, textMaxLength); + Observation started = observation.start(); + if (!TracingSpanRecordingProbe.isRecording(started)) { + Observation.Context context = started.getContext(); + if (context instanceof MongodbObservationContext) { + ((MongodbObservationContext) context).setRecording(false); + } + } + return new MicrometerSpan(started, namespace, textMaxLength); } @Override 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 d177f02fae7..a7bd33c4d0f 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 @@ -182,8 +182,9 @@ public Span createTracingSpan(final CommandMessage message, if (!isEnabled()) { return null; } - BsonDocument command = commandDocumentSupplier.get(); - String commandName = command.getFirstKey(); + // The command name is known from the message; decoding the encoded command is deferred until a field + // inside it is actually needed (the getMore cursor id). + String commandName = message.getCommandName(); if (isSensitiveCommand.test(commandName)) { return null; } @@ -224,8 +225,8 @@ public Span createTracingSpan(final CommandMessage message, ConnectionId connectionId = connectionIdSupplier.get(); mongodbContext.setConnectionId(connectionId); - if (command.containsKey("getMore")) { - long cursorId = command.getInt64("getMore").longValue(); + if ("getMore".equals(commandName)) { + long cursorId = commandDocumentSupplier.get().getInt64("getMore").longValue(); mongodbContext.setCursorId(cursorId); } diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/TracingSpanRecordingProbe.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/TracingSpanRecordingProbe.java new file mode 100644 index 00000000000..3c40952096d --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/TracingSpanRecordingProbe.java @@ -0,0 +1,90 @@ +/* + * 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.lang.Nullable; +import io.micrometer.observation.Observation; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; + +/** + * Reads, from a started {@link Observation}, whether the tracing span that Micrometer Tracing attached to it is being + * recorded by the tracing SDK. + *
+ * The driver depends only on {@code micrometer-observation}. When {@code micrometer-tracing} is present, its + * {@code TracingObservationHandler} stores the span in the observation context under the + * {@code TracingObservationHandler.TracingContext} class key during {@code onStart}, and the span's {@code isNoop()} + * reports {@code true} when the SDK is not recording it. Both are resolved reflectively once; when either is absent, + * every span is reported as recording, which is the pre-existing behavior. + * + *
This class is not part of the public API and may be removed or changed at any time
+ */ +final class TracingSpanRecordingProbe { + @Nullable + private static final Class> TRACING_CONTEXT_CLASS; + @Nullable + private static final MethodHandle GET_SPAN; + @Nullable + private static final MethodHandle IS_NOOP; + + static { + Class> tracingContextClass = null; + MethodHandle getSpan = null; + MethodHandle isNoop = null; + try { + ClassLoader classLoader = TracingSpanRecordingProbe.class.getClassLoader(); + tracingContextClass = Class.forName("io.micrometer.tracing.handler.TracingObservationHandler$TracingContext", + false, classLoader); + Class> spanClass = Class.forName("io.micrometer.tracing.Span", false, classLoader); + MethodHandles.Lookup lookup = MethodHandles.publicLookup(); + getSpan = lookup.findVirtual(tracingContextClass, "getSpan", MethodType.methodType(spanClass)); + isNoop = lookup.findVirtual(spanClass, "isNoop", MethodType.methodType(boolean.class)); + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | LinkageError e) { + tracingContextClass = null; + getSpan = null; + isNoop = null; + } + TRACING_CONTEXT_CLASS = tracingContextClass; + GET_SPAN = getSpan; + IS_NOOP = isNoop; + } + + private TracingSpanRecordingProbe() { + } + + /** + * @return {@code false} only when Micrometer Tracing is present, has attached a span to the observation, and + * reports that span as not recording; {@code true} in every other case. + */ + static boolean isRecording(final Observation observation) { + if (TRACING_CONTEXT_CLASS == null || GET_SPAN == null || IS_NOOP == null) { + return true; + } + Object tracingContext = observation.getContext().get(TRACING_CONTEXT_CLASS); + if (tracingContext == null) { + return true; + } + try { + Object span = GET_SPAN.invoke(tracingContext); + return span == null || !(boolean) IS_NOOP.invoke(span); + } catch (Throwable t) { + return true; + } + } +} diff --git a/driver-core/src/main/com/mongodb/observability/micrometer/DefaultMongodbObservationConvention.java b/driver-core/src/main/com/mongodb/observability/micrometer/DefaultMongodbObservationConvention.java index 94e508641b7..b19ba6ecdf3 100644 --- a/driver-core/src/main/com/mongodb/observability/micrometer/DefaultMongodbObservationConvention.java +++ b/driver-core/src/main/com/mongodb/observability/micrometer/DefaultMongodbObservationConvention.java @@ -18,12 +18,15 @@ import com.mongodb.ServerAddress; import com.mongodb.connection.ConnectionId; +import com.mongodb.lang.Nullable; +import io.micrometer.common.KeyValue; import io.micrometer.common.KeyValues; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationConvention; import java.io.PrintWriter; import java.io.StringWriter; +import java.util.Arrays; /** * Default {@link ObservationConvention} for MongoDB observations. @@ -58,7 +61,10 @@ public KeyValues getLowCardinalityKeyValues(final MongodbObservationContext cont @Override public KeyValues getHighCardinalityKeyValues(final MongodbObservationContext context) { - if (context.getObservationType() == MongodbObservation.MONGODB_COMMAND) { + // High-cardinality values are only read by a recording span: skip the string formatting when the + // sampler dropped the span. Low-cardinality values are still produced because metrics handlers on the + // same registry consume them. + if (context.getObservationType() == MongodbObservation.MONGODB_COMMAND && context.isRecording()) { return getCommandHighCardinalityKeyValues(context); } return KeyValues.empty(); @@ -69,24 +75,22 @@ private KeyValues getOperationLowCardinalityKeyValues(final MongodbObservationCo String databaseName = context.getDatabaseName(); String collectionName = context.getCollectionName(); - KeyValues kv = KeyValues.of( - MongodbObservation.OperationLowCardinalityKeyNames.SYSTEM.withValue("mongodb")); - + // Built as one array: each KeyValues.and() merges and re-sorts, which dominated the per-span cost. + KeyValue[] values = new KeyValue[5]; + int n = 0; + values[n++] = MongodbObservation.OperationLowCardinalityKeyNames.SYSTEM.withValue("mongodb"); if (databaseName != null) { - kv = kv.and(MongodbObservation.OperationLowCardinalityKeyNames.NAMESPACE.withValue(databaseName)); + values[n++] = MongodbObservation.OperationLowCardinalityKeyNames.NAMESPACE.withValue(databaseName); } if (collectionName != null) { - kv = kv.and(MongodbObservation.OperationLowCardinalityKeyNames.COLLECTION.withValue(collectionName)); + values[n++] = 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)); + values[n++] = MongodbObservation.OperationLowCardinalityKeyNames.OPERATION_NAME.withValue(commandName); + values[n++] = MongodbObservation.OperationLowCardinalityKeyNames.OPERATION_SUMMARY.withValue( + summary(commandName, databaseName, collectionName)); } - return kv; + return KeyValues.of(Arrays.copyOf(values, n)); } private KeyValues getCommandLowCardinalityKeyValues(final MongodbObservationContext context) { @@ -95,73 +99,75 @@ private KeyValues getCommandLowCardinalityKeyValues(final MongodbObservationCont 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)); + KeyValue[] values = new KeyValue[9]; + int n = 0; + values[n++] = MongodbObservation.CommandLowCardinalityKeyNames.SYSTEM.withValue("mongodb"); + values[n++] = MongodbObservation.CommandLowCardinalityKeyNames.NAMESPACE.withValue(dbName); + values[n++] = MongodbObservation.CommandLowCardinalityKeyNames.QUERY_SUMMARY.withValue( + summary(cmdName, dbName, collectionName)); + values[n++] = MongodbObservation.CommandLowCardinalityKeyNames.COMMAND_NAME.withValue(cmdName); if (collectionName != null) { - kv = kv.and(MongodbObservation.CommandLowCardinalityKeyNames.COLLECTION.withValue(collectionName)); + values[n++] = MongodbObservation.CommandLowCardinalityKeyNames.COLLECTION.withValue(collectionName); } 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")); + values[n++] = MongodbObservation.CommandLowCardinalityKeyNames.SERVER_ADDRESS.withValue(serverAddress.getHost()); + values[n++] = MongodbObservation.CommandLowCardinalityKeyNames.SERVER_PORT.withValue( + String.valueOf(serverAddress.getPort())); + values[n++] = 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)); + values[n++] = MongodbObservation.CommandLowCardinalityKeyNames.RESPONSE_STATUS_CODE.withValue(responseStatusCode); } - return kv; + return KeyValues.of(Arrays.copyOf(values, n)); } - private KeyValues getCommandHighCardinalityKeyValues(final MongodbObservationContext context) { - KeyValues kv = KeyValues.empty(); + private static String summary(final String commandName, @Nullable final String databaseName, + @Nullable final String collectionName) { + String dbName = databaseName != null ? databaseName : ""; + return collectionName != null + ? commandName + " " + dbName + "." + collectionName + : commandName + " " + dbName; + } + private KeyValues getCommandHighCardinalityKeyValues(final MongodbObservationContext context) { + KeyValue[] values = new KeyValue[9]; + int n = 0; String queryText = context.getQueryText(); if (queryText != null) { - kv = kv.and(MongodbObservation.HighCardinalityKeyNames.QUERY_TEXT.withValue(queryText)); + values[n++] = MongodbObservation.HighCardinalityKeyNames.QUERY_TEXT.withValue(queryText); } 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()))); + values[n++] = MongodbObservation.HighCardinalityKeyNames.CLIENT_CONNECTION_ID.withValue( + String.valueOf(connectionId.getLocalValue())); + values[n++] = 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))); + values[n++] = 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))); + values[n++] = MongodbObservation.HighCardinalityKeyNames.TRANSACTION_NUMBER.withValue( + String.valueOf(transactionNumber)); } String sessionId = context.getSessionId(); if (sessionId != null) { - kv = kv.and(MongodbObservation.HighCardinalityKeyNames.SESSION_ID.withValue(sessionId)); + values[n++] = 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(String.valueOf(error.getMessage())), - MongodbObservation.HighCardinalityKeyNames.EXCEPTION_TYPE.withValue(error.getClass().getName()), - MongodbObservation.HighCardinalityKeyNames.EXCEPTION_STACKTRACE.withValue(getStackTraceAsString(error))); + values[n++] = MongodbObservation.HighCardinalityKeyNames.EXCEPTION_MESSAGE.withValue(String.valueOf(error.getMessage())); + values[n++] = MongodbObservation.HighCardinalityKeyNames.EXCEPTION_TYPE.withValue(error.getClass().getName()); + values[n++] = MongodbObservation.HighCardinalityKeyNames.EXCEPTION_STACKTRACE.withValue(getStackTraceAsString(error)); } - - return kv; + return n == 0 ? KeyValues.empty() : KeyValues.of(Arrays.copyOf(values, n)); } private static String getStackTraceAsString(final Throwable throwable) { diff --git a/driver-core/src/main/com/mongodb/observability/micrometer/MongodbObservationContext.java b/driver-core/src/main/com/mongodb/observability/micrometer/MongodbObservationContext.java index 2b56ac1206d..45fff829abf 100644 --- a/driver-core/src/main/com/mongodb/observability/micrometer/MongodbObservationContext.java +++ b/driver-core/src/main/com/mongodb/observability/micrometer/MongodbObservationContext.java @@ -62,6 +62,7 @@ public class MongodbObservationContext extends SenderContext