Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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.
*
* <p>This class is not part of the public API and may be removed or changed at any time</p>
*/
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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();
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public class MongodbObservationContext extends SenderContext<Object> {
@Nullable
private String responseStatusCode;
private boolean isUnixSocket;
private boolean recording = true;

/**
* Creates a new {@code MongodbObservationContext} with {@link Kind#CLIENT} kind.
Expand Down Expand Up @@ -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.
* <p>
* 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading