From a5c8441dc1e3b4bc6eb3b6599036a1521e357a63 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Thu, 15 Oct 2026 14:02:43 +0100 Subject: [PATCH] Gate OpenTelemetry attribute building on the sampling decision Benchmarking against the OpenTelemetry spec's "Performance Implications" section (DRIVERS-3620) showed that the Java driver paid almost the same cost under a 1% sampler as with every span recorded. A CPU profile placed the cost in three places: building the tag set on span stop (chained KeyValues.and() merges, run on every command regardless of sampling), decoding the just-encoded BSON command to read its name, and scope handling inside the Micrometer/OpenTelemetry bridge. This change addresses the first two: - MongodbObservationContext gains a `recording` flag. After the observation starts, MicrometerTracer asks TracingSpanRecordingProbe whether the tracing SDK is recording the span and clears the flag if not. The probe resolves the micrometer-tracing span type reflectively, since driver-core depends only on micrometer-observation; when that module is absent every span is treated as recording, as before. - DefaultMongodbObservationConvention returns no high-cardinality key values for a non-recording span, and builds all key-value sets from a single array instead of repeated KeyValues.and() merges. Low-cardinality values are still produced for non-recording spans because metrics handlers on the same registry consume them. - CommandMessage exposes getCommandName(); TracingManager uses it and decodes the command only for getMore, to read the cursor id. Measured on the spec's benchmark (5 interleaved repetitions, medians): added CPU per operation roughly halved in every configuration, and the api-only cost is within 1% of tracing off on all four tasks. Span counts and tag contents are unchanged. --- .../internal/connection/CommandMessage.java | 8 ++ .../micrometer/MicrometerTracer.java | 9 +- .../micrometer/TracingManager.java | 9 +- .../micrometer/TracingSpanRecordingProbe.java | 90 +++++++++++++ .../DefaultMongodbObservationConvention.java | 104 ++++++++------- .../micrometer/MongodbObservationContext.java | 26 ++++ .../connection/CommandMessageTest.java | 8 ++ ...faultMongodbObservationConventionTest.java | 125 ++++++++++++++++++ 8 files changed, 325 insertions(+), 54 deletions(-) create mode 100644 driver-core/src/main/com/mongodb/internal/observability/micrometer/TracingSpanRecordingProbe.java create mode 100644 driver-core/src/test/unit/com/mongodb/observability/micrometer/DefaultMongodbObservationConventionTest.java 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 { @Nullable private String responseStatusCode; private boolean isUnixSocket; + private boolean recording = true; /** * Creates a new {@code MongodbObservationContext} with {@link Kind#CLIENT} kind. @@ -248,4 +249,29 @@ public String getResponseStatusCode() { public void setResponseStatusCode(@Nullable final String responseStatusCode) { this.responseStatusCode = responseStatusCode; } + + /** + * Whether the span backing this observation is being recorded by the tracing SDK. + *

+ * Defaults to {@code true}. The driver sets it to {@code false} once it learns from the tracing SDK that the + * sampler dropped the span, which lets {@link DefaultMongodbObservationConvention} skip producing attributes + * that nobody will read. + * + * @return true if the span is recorded + * @since 5.10 + */ + public boolean isRecording() { + return recording; + } + + /** + * Sets whether the span backing this observation is being recorded. + * + * @param recording true if the span is recorded + * @see #isRecording() + * @since 5.10 + */ + public void setRecording(final boolean recording) { + this.recording = recording; + } } diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/CommandMessageTest.java b/driver-core/src/test/unit/com/mongodb/internal/connection/CommandMessageTest.java index 1d57d04b4a8..8db1934e74d 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/connection/CommandMessageTest.java +++ b/driver-core/src/test/unit/com/mongodb/internal/connection/CommandMessageTest.java @@ -63,6 +63,14 @@ class CommandMessageTest { private static final MongoNamespace NAMESPACE = new MongoNamespace("db.test"); private static final BsonDocument COMMAND = new BsonDocument("find", new BsonString(NAMESPACE.getCollectionName())); + @Test + void getCommandNameShouldReturnFirstKeyWithoutDecoding() { + CommandMessage commandMessage = new CommandMessage(NAMESPACE.getDatabaseName(), COMMAND, NoOpFieldNameValidator.INSTANCE, ReadPreference.primary(), + MessageSettings.builder().maxWireVersion(LATEST_WIRE_VERSION).build(), + true, EmptyMessageSequences.INSTANCE, ClusterConnectionMode.MULTIPLE, null); + assertEquals("find", commandMessage.getCommandName()); + } + @Test void encodeShouldThrowTimeoutExceptionWhenTimeoutContextIsCalled() { //given diff --git a/driver-core/src/test/unit/com/mongodb/observability/micrometer/DefaultMongodbObservationConventionTest.java b/driver-core/src/test/unit/com/mongodb/observability/micrometer/DefaultMongodbObservationConventionTest.java new file mode 100644 index 00000000000..6e86e0eeb33 --- /dev/null +++ b/driver-core/src/test/unit/com/mongodb/observability/micrometer/DefaultMongodbObservationConventionTest.java @@ -0,0 +1,125 @@ +/* + * 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.observability.micrometer; + +import com.mongodb.ServerAddress; +import com.mongodb.connection.ConnectionId; +import com.mongodb.connection.ServerId; +import com.mongodb.connection.ClusterId; +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DefaultMongodbObservationConventionTest { + + private final DefaultMongodbObservationConvention convention = new DefaultMongodbObservationConvention(); + + private static MongodbObservationContext commandContext() { + MongodbObservationContext ctx = new MongodbObservationContext(); + ctx.setObservationType(MongodbObservation.MONGODB_COMMAND); + ctx.setCommandName("find"); + ctx.setDatabaseName("db"); + ctx.setCollectionName("coll"); + ctx.setServerAddress(new ServerAddress("localhost", 27017)); + ctx.setConnectionId(new ConnectionId(new ServerId(new ClusterId(), new ServerAddress()), 3, 42L)); + ctx.setCursorId(7L); + ctx.setTransactionNumber(2L); + ctx.setSessionId("session"); + ctx.setQueryText("{find: 'coll'}"); + return ctx; + } + + private static Map toMap(final KeyValues keyValues) { + Map map = new HashMap<>(); + for (KeyValue kv : keyValues) { + map.put(kv.getKey(), kv.getValue()); + } + return map; + } + + @Test + void commandLowCardinalityKeyValuesAreComplete() { + Map kv = toMap(convention.getLowCardinalityKeyValues(commandContext())); + assertEquals("mongodb", kv.get("db.system.name")); + assertEquals("db", kv.get("db.namespace")); + assertEquals("coll", kv.get("db.collection.name")); + assertEquals("find", kv.get("db.command.name")); + assertEquals("find db.coll", kv.get("db.query.summary")); + assertEquals("localhost", kv.get("server.address")); + assertEquals("27017", kv.get("server.port")); + assertEquals("tcp", kv.get("network.transport")); + assertEquals(8, kv.size()); + } + + @Test + void commandLowCardinalityKeyValuesWithoutCollectionOmitCollection() { + MongodbObservationContext ctx = commandContext(); + ctx.setCollectionName(null); + Map kv = toMap(convention.getLowCardinalityKeyValues(ctx)); + assertFalse(kv.containsKey("db.collection.name")); + assertEquals("find db", kv.get("db.query.summary")); + } + + @Test + void operationLowCardinalityKeyValuesAreComplete() { + MongodbObservationContext ctx = new MongodbObservationContext(); + ctx.setObservationType(MongodbObservation.MONGODB_OPERATION); + ctx.setCommandName("insert"); + ctx.setDatabaseName("db"); + ctx.setCollectionName("coll"); + Map kv = toMap(convention.getLowCardinalityKeyValues(ctx)); + assertEquals("mongodb", kv.get("db.system.name")); + assertEquals("db", kv.get("db.namespace")); + assertEquals("coll", kv.get("db.collection.name")); + assertEquals("insert", kv.get("db.operation.name")); + assertEquals("insert db.coll", kv.get("db.operation.summary")); + assertEquals(5, kv.size()); + } + + @Test + void commandHighCardinalityKeyValuesAreCompleteWhenRecording() { + Map kv = toMap(convention.getHighCardinalityKeyValues(commandContext())); + assertEquals("{find: 'coll'}", kv.get("db.query.text")); + assertEquals("3", kv.get("db.mongodb.driver_connection_id")); + assertEquals("42", kv.get("db.mongodb.server_connection_id")); + assertEquals("7", kv.get("db.mongodb.cursor_id")); + assertEquals("2", kv.get("db.mongodb.txn_number")); + assertEquals("session", kv.get("db.mongodb.lsid")); + assertEquals(6, kv.size()); + } + + @Test + void commandHighCardinalityKeyValuesAreSkippedWhenSpanIsNotRecording() { + MongodbObservationContext ctx = commandContext(); + ctx.setRecording(false); + assertTrue(toMap(convention.getHighCardinalityKeyValues(ctx)).isEmpty()); + // low-cardinality values are still produced: metrics handlers on the same registry consume them + assertEquals(8, toMap(convention.getLowCardinalityKeyValues(ctx)).size()); + } + + @Test + void recordingDefaultsToTrue() { + assertTrue(new MongodbObservationContext().isRecording()); + } +}