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/MongoClientSettings.java b/driver-core/src/main/com/mongodb/MongoClientSettings.java index 8f462df193e..a29aa8afc41 100644 --- a/driver-core/src/main/com/mongodb/MongoClientSettings.java +++ b/driver-core/src/main/com/mongodb/MongoClientSettings.java @@ -17,6 +17,7 @@ package com.mongodb; import com.mongodb.annotations.Alpha; +import com.mongodb.annotations.Beta; import com.mongodb.annotations.Immutable; import com.mongodb.annotations.NotThreadSafe; import com.mongodb.annotations.Reason; @@ -517,7 +518,7 @@ public Builder transportSettings(final TransportSettings transportSettings) { * @see #getObservabilitySettings() * @since 5.7 */ - @Alpha(Reason.CLIENT) + @Beta(Reason.CLIENT) public Builder observabilitySettings(final ObservabilitySettings observabilitySettings) { this.observabilitySettings = notNull("observabilitySettings", observabilitySettings); return this; 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..52114d84bb8 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.observability.micrometer.MongodbObservationContext; 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,7 +454,6 @@ private T sendAndReceiveInternal(final CommandMessage message, final Decoder () -> getDescription().getServerAddress(), () -> getDescription().getConnectionId() ); - boolean isLoggingCommandNeeded = isLoggingCommandNeeded(); boolean isTracingCommandPayloadNeeded = tracingSpan != null && operationContext.getTracingManager().isCommandPayloadEnabled(); @@ -473,7 +472,10 @@ private T sendAndReceiveInternal(final CommandMessage message, final Decoder commandEventSender = new NoOpCommandEventSender(); } if (isTracingCommandPayloadNeeded) { - tracingSpan.tagHighCardinality(QUERY_TEXT.asString(), commandDocument); + tracingSpan.setQueryText(commandDocument); + } + if (tracingSpan != null) { + tracingSpan.openScope(); } try { @@ -481,6 +483,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 +496,7 @@ private T sendAndReceiveInternal(final CommandMessage message, final Decoder } else { commandEventSender.sendSucceededEventForOneWayCommand(); if (tracingSpan != null) { + tracingSpan.closeScope(); tracingSpan.end(); } return null; @@ -585,13 +590,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()))); + MongodbObservationContext ctx = tracingSpan.getMongodbObservationContext(); + 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 +648,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 +656,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()))); + MongodbObservationContext ctx = commandSpan.getMongodbObservationContext(); + 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/MicrometerTracer.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java index a7204a01a71..1202af6ed63 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,19 @@ import com.mongodb.MongoNamespace; import com.mongodb.lang.Nullable; -import io.micrometer.common.KeyValue; -import io.micrometer.common.KeyValues; +import com.mongodb.observability.micrometer.DefaultMongodbObservationConvention; +import com.mongodb.observability.micrometer.MongodbObservationContext; import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; 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,34 +49,30 @@ 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"; + private final ObservationConvention convention; /** * 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. + * @param customConvention A custom observation convention, or null to use the default. */ - public MicrometerTracer(final ObservationRegistry observationRegistry, final boolean allowCommandPayload, final int textMaxLength) { + public MicrometerTracer(final ObservationRegistry observationRegistry, final boolean allowCommandPayload, + final int textMaxLength, @Nullable final ObservationConvention customConvention) { this.allowCommandPayload = allowCommandPayload; this.observationRegistry = observationRegistry; this.textMaxLength = ofNullable(getenv(ENV_OBSERVABILITY_QUERY_TEXT_MAX_LENGTH)) .map(Integer::parseInt) .orElse(textMaxLength); + this.convention = customConvention != null ? customConvention : new DefaultMongodbObservationConvention(); } @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 +81,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 +94,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, () -> { + MongodbObservationContext ctx = new MongodbObservationContext(); + ctx.setObservationType(observationType); + return ctx; + }).observationConvention(convention).contextualName(name); } /** * Represents a Micrometer-based trace context. @@ -135,38 +125,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) { + MongodbObservationContext ctx = getMongodbObservationContext(); + if (ctx != null) { + ctx.setQueryText((queryTextLength < Integer.MAX_VALUE) + ? getTruncatedBsonDocument(commandDocument) + : commandDocument.toString()); + } } @Override @@ -176,11 +171,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 +186,17 @@ public TraceContext context() { @Override @Nullable - public MongoNamespace getNamespace() { - return namespace; + public MongodbObservationContext getMongodbObservationContext() { + if (observation.getContext() instanceof MongodbObservationContext) { + return (MongodbObservationContext) 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/MongodbObservation.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java index 0fbfe165f50..0824d28c37f 100644 --- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java @@ -17,43 +17,66 @@ package com.mongodb.internal.observability.micrometer; import io.micrometer.common.docs.KeyName; -import io.micrometer.observation.Observation; import io.micrometer.observation.docs.ObservationDocumentation; /** - * A MongoDB-based {@link Observation}. + * MongoDB {@link ObservationDocumentation} definitions for operation-level and command-level observations. + *

+ * These are split into two separate observation types so that each has a distinct name and a fixed set + * of low-cardinality tag keys. This is required by Prometheus which rejects meters that share a name + * but have different tag key sets. + *

* * @since 5.7 */ public enum MongodbObservation implements ObservationDocumentation { - MONGODB_OBSERVATION { + /** + * Observation for high-level MongoDB operations (e.g. find, insert, update). + * Created per user-initiated operation, may contain multiple command spans. + */ + MONGODB_OPERATION { + @Override + public String getName() { + return "mongodb.operation"; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return OperationLowCardinalityKeyNames.values(); + } + }, + + /** + * Observation for wire-protocol MongoDB commands sent to the server. + * Created per actual command (nested under an operation span). + */ + MONGODB_COMMAND { @Override public String getName() { - return "mongodb"; + return "mongodb.command"; } @Override public KeyName[] getLowCardinalityKeyNames() { - return LowCardinalityKeyNames.values(); + return CommandLowCardinalityKeyNames.values(); } @Override public KeyName[] getHighCardinalityKeyNames() { return HighCardinalityKeyNames.values(); } - }; /** - * Enums related to low cardinality key names for MongoDB tags. + * Low cardinality key names for operation-level observations. */ - public enum LowCardinalityKeyNames implements KeyName { + public enum OperationLowCardinalityKeyNames implements KeyName { SYSTEM { @Override public String asString() { - return "db.system"; + return "db.system.name"; } }, NAMESPACE { @@ -74,22 +97,41 @@ public String asString() { return "db.operation.name"; } }, - COMMAND_NAME { + OPERATION_SUMMARY { @Override public String asString() { - return "db.command.name"; + return "db.operation.summary"; + } + } + } + + /** + * Low cardinality key names for command-level observations. + */ + public enum CommandLowCardinalityKeyNames implements KeyName { + + SYSTEM { + @Override + public String asString() { + return "db.system.name"; } }, - NETWORK_TRANSPORT { + NAMESPACE { @Override public String asString() { - return "network.transport"; + return "db.namespace"; } }, - OPERATION_SUMMARY { + COLLECTION { @Override public String asString() { - return "db.operation.summary"; + return "db.collection.name"; + } + }, + COMMAND_NAME { + @Override + public String asString() { + return "db.command.name"; } }, QUERY_SUMMARY { @@ -98,10 +140,10 @@ public String asString() { return "db.query.summary"; } }, - CURSOR_ID { + NETWORK_TRANSPORT { @Override public String asString() { - return "db.mongodb.cursor_id"; + return "network.transport"; } }, SERVER_ADDRESS { @@ -116,6 +158,25 @@ public String asString() { return "server.port"; } }, + RESPONSE_STATUS_CODE { + @Override + public String asString() { + return "db.response.status_code"; + } + } + } + + /** + * High cardinality (highly variable values) key names for command-level observations. + */ + public enum HighCardinalityKeyNames implements KeyName { + + QUERY_TEXT { + @Override + public String asString() { + return "db.query.text"; + } + }, CLIENT_CONNECTION_ID { @Override public String asString() { @@ -128,6 +189,12 @@ public String asString() { return "db.mongodb.server_connection_id"; } }, + CURSOR_ID { + @Override + public String asString() { + return "db.mongodb.cursor_id"; + } + }, TRANSACTION_NUMBER { @Override public String asString() { @@ -140,10 +207,10 @@ public String asString() { return "db.mongodb.lsid"; } }, - EXCEPTION_STACKTRACE { + EXCEPTION_MESSAGE { @Override public String asString() { - return "exception.stacktrace"; + return "exception.message"; } }, EXCEPTION_TYPE { @@ -152,29 +219,10 @@ public String asString() { return "exception.type"; } }, - EXCEPTION_MESSAGE { - @Override - public String asString() { - return "exception.message"; - } - }, - RESPONSE_STATUS_CODE { - @Override - public String asString() { - return "db.response.status_code"; - } - } - } - - /** - * Enums related to high cardinality (highly variable values) key names for MongoDB tags. - */ - public enum HighCardinalityKeyNames implements KeyName { - - QUERY_TEXT { + EXCEPTION_STACKTRACE { @Override public String asString() { - return "db.query.text"; + return "exception.stacktrace"; } } } diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/Span.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/Span.java index 84bdbb41672..d060c82b0d4 100644 --- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/Span.java +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/Span.java @@ -18,8 +18,7 @@ import com.mongodb.MongoNamespace; import com.mongodb.lang.Nullable; -import io.micrometer.common.KeyValue; -import io.micrometer.common.KeyValues; +import com.mongodb.observability.micrometer.MongodbObservationContext; import org.bson.BsonDocument; /** @@ -48,15 +47,15 @@ public interface Span { */ Span EMPTY = new Span() { @Override - public void tagLowCardinality(final KeyValue tag) { + public void openScope() { } @Override - public void tagLowCardinality(final KeyValues keyValues) { + public void closeScope() { } @Override - public void tagHighCardinality(final String keyName, final BsonDocument value) { + public void setQueryText(final BsonDocument commandDocument) { } @Override @@ -81,29 +80,32 @@ public TraceContext context() { public MongoNamespace getNamespace() { return null; } + + @Override + @Nullable + public MongodbObservationContext getMongodbObservationContext() { + return null; + } }; /** - * Adds a low-cardinality tag to the span. - * - * @param keyValue The key-value pair representing the tag. + * Opens a scope for this span, making it the current observation on the thread. + * Must be paired with {@link #closeScope()} in a try-finally block. */ - void tagLowCardinality(KeyValue keyValue); + void openScope(); /** - * Adds multiple low-cardinality tags to the span. - * - * @param keyValues The key-value pairs representing the tags. + * Closes the scope previously opened by {@link #openScope()}, restoring the previous observation. */ - void tagLowCardinality(KeyValues keyValues); + void closeScope(); /** - * Adds a high-cardinality (highly variable values) tag to the span with a BSON document value. + * Sets the query text on the observation context from the given command document. + * The document is converted to a JSON string and may be truncated based on configuration. * - * @param keyName The name of the tag. - * @param value The BSON document representing the value of the tag. + * @param commandDocument The BSON command document. */ - void tagHighCardinality(String keyName, BsonDocument value); + void setQueryText(BsonDocument commandDocument); /** * Records an event in the span. @@ -131,6 +133,15 @@ public MongoNamespace getNamespace() { */ TraceContext context(); + /** + * Retrieves the {@link MongodbObservationContext} associated with the span, if any. + * Returns null for no-op spans or non-Micrometer implementations. + * + * @return The MongoDB observation context, or null. + */ + @Nullable + MongodbObservationContext getMongodbObservationContext(); + /** * Retrieves the MongoDB namespace associated with the span, if any. * diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/Tracer.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/Tracer.java index 632580ab40e..fc1ddca68d0 100644 --- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/Tracer.java +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/Tracer.java @@ -30,7 +30,8 @@ public interface Tracer { Tracer NO_OP = new Tracer() { @Override - public Span nextSpan(final String name, @Nullable final TraceContext parent, @Nullable final MongoNamespace namespace) { + public Span nextSpan(final MongodbObservation observationType, final String name, + @Nullable final TraceContext parent, @Nullable final MongoNamespace namespace) { return Span.EMPTY; } @@ -46,14 +47,15 @@ public boolean includeCommandPayload() { }; /** - * Creates a new span with the specified name and optional parent trace context. + * Creates a new span with the specified observation type, name and optional parent trace context. * + * @param observationType The {@link MongodbObservation} type (operation or command). * @param name The name of the span. * @param parent The parent {@link TraceContext}, or null if no parent context is provided. * @param namespace The {@link MongoNamespace} associated with the span, or null if none is provided. * @return A {@link Span} representing the newly created span. */ - Span nextSpan(String name, @Nullable TraceContext parent, @Nullable MongoNamespace namespace); + Span nextSpan(MongodbObservation observationType, String name, @Nullable TraceContext parent, @Nullable MongoNamespace namespace); /** * Indicates whether tracing is enabled. 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 4247ed1c3dd..47347de8482 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 @@ -26,29 +26,17 @@ import com.mongodb.internal.session.SessionContext; import com.mongodb.lang.Nullable; import com.mongodb.observability.ObservabilitySettings; +import com.mongodb.observability.micrometer.DefaultMongodbObservationConvention; import com.mongodb.observability.micrometer.MicrometerObservabilitySettings; -import io.micrometer.common.KeyValues; +import com.mongodb.observability.micrometer.MongodbObservationContext; import io.micrometer.observation.ObservationRegistry; import org.bson.BsonDocument; import java.util.function.Predicate; import java.util.function.Supplier; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.CLIENT_CONNECTION_ID; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.COLLECTION; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.COMMAND_NAME; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.NAMESPACE; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.OPERATION_NAME; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.OPERATION_SUMMARY; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.NETWORK_TRANSPORT; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.QUERY_SUMMARY; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.SERVER_ADDRESS; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.SERVER_CONNECTION_ID; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.SERVER_PORT; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.SESSION_ID; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.SYSTEM; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.TRANSACTION_NUMBER; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.CURSOR_ID; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.MONGODB_COMMAND; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.MONGODB_OPERATION; import static java.lang.System.getenv; /** @@ -101,7 +89,8 @@ public TracingManager(@Nullable final ObservabilitySettings observabilitySetting ObservationRegistry observationRegistry = settings.getObservationRegistry(); tracer = enableTracing && observationRegistry != null - ? new MicrometerTracer(observationRegistry, settings.isEnableCommandPayloadTracing(), settings.getMaxQueryTextLength()) + ? new MicrometerTracer(observationRegistry, settings.isEnableCommandPayloadTracing(), + settings.getMaxQueryTextLength(), settings.getObservationConvention()) : Tracer.NO_OP; this.enableCommandPayload = tracer.includeCommandPayload(); @@ -109,35 +98,31 @@ public TracingManager(@Nullable final ObservabilitySettings observabilitySetting } /** - * Creates a new span with the specified name and parent trace context. - *

- * This method is used to create a span that is linked to a parent context, - * enabling hierarchical tracing of operations. - *

+ * Creates a new span with the specified observation type, name and parent trace context. * - * @param name The name of the span. - * @param parentContext The parent trace context to associate with the span. + * @param observationType The observation type (operation or command). + * @param name The name of the span. + * @param parentContext The parent trace context to associate with the span. * @return The created span. */ - public Span addSpan(final String name, @Nullable final TraceContext parentContext) { - return tracer.nextSpan(name, parentContext, null); + public Span addSpan(final MongodbObservation observationType, final String name, + @Nullable final TraceContext parentContext) { + return tracer.nextSpan(observationType, name, parentContext, null); } /** - * Creates a new span with the specified name, parent trace context, and MongoDB namespace. - *

- * This method is used to create a span that is linked to a parent context, - * enabling hierarchical tracing of operations. The MongoDB namespace can be used - * by nested spans to access the database and collection name (which might not be easily accessible at connection layer). - *

+ * Creates a new span with the specified observation type, name, parent trace context, + * and MongoDB namespace. * - * @param name The name of the span. - * @param parentContext The parent trace context to associate with the span. - * @param namespace The MongoDB namespace associated with the operation. + * @param observationType The observation type (operation or command). + * @param name The name of the span. + * @param parentContext The parent trace context to associate with the span. + * @param namespace The MongoDB namespace associated with the operation. * @return The created span. */ - public Span addSpan(final String name, @Nullable final TraceContext parentContext, final MongoNamespace namespace) { - return tracer.nextSpan(name, parentContext, namespace); + public Span addSpan(final MongodbObservation observationType, final String name, + @Nullable final TraceContext parentContext, final MongoNamespace namespace) { + return tracer.nextSpan(observationType, name, parentContext, namespace); } /** @@ -146,9 +131,7 @@ public Span addSpan(final String name, @Nullable final TraceContext parentContex * @return The created transaction span. */ public Span addTransactionSpan() { - Span span = tracer.nextSpan("transaction", null, null); - span.tagLowCardinality(SYSTEM.withValue("mongodb")); - return span; + return tracer.nextSpan(MONGODB_OPERATION, "transaction", null, null); } /** @@ -173,10 +156,10 @@ public boolean isCommandPayloadEnabled() { /** Create a tracing span for the given command message. *

* The span is only created if tracing is enabled and the command is not security-sensitive. - * It attaches various tags to the span, such as database system, namespace, query summary, opcode, - * server address, port, server type, client and server connection IDs, and, if applicable, - * transaction number and session ID. - * If command payload tracing is enabled, the command document is also attached as a tag. + * It populates domain fields on the span's {@link MongodbObservationContext} (command name, namespace, + * server address, connection ID, session/transaction info, cursor ID for getMore commands). + * The {@link DefaultMongodbObservationConvention} reads these fields at observation stop time + * to produce the final tag key-values. * * @param message the command message to trace * @param operationContext the operation context containing tracing and session information @@ -205,17 +188,9 @@ public Span createTracingSpan(final CommandMessage message, } Span operationSpan = operationContext.getTracingSpan(); - Span span = addSpan(commandName, operationSpan != null ? operationSpan.context() : null); - - if (command.containsKey("getMore")) { - long cursorId = command.getInt64("getMore").longValue(); - span.tagLowCardinality(CURSOR_ID.withValue(String.valueOf(cursorId))); - if (operationSpan != null) { - operationSpan.tagLowCardinality(CURSOR_ID.withValue(String.valueOf(cursorId))); - } - } + Span span = addSpan(MONGODB_COMMAND, commandName, operationSpan != null ? operationSpan.context() : null); - // Tag namespace + // Resolve namespace from parent operation span or message String namespace; String collection = ""; if (operationSpan != null) { @@ -231,39 +206,35 @@ public Span createTracingSpan(final CommandMessage message, } else { namespace = message.getDatabase(); } - String summary = commandName + " " + namespace + (collection.isEmpty() ? "" : "." + collection); - KeyValues keyValues = KeyValues.of( - SYSTEM.withValue("mongodb"), - NAMESPACE.withValue(namespace), - QUERY_SUMMARY.withValue(summary), - COMMAND_NAME.withValue(commandName)); + // Populate domain fields on MongodbObservationContext — the convention reads these to produce tags + MongodbObservationContext mongodbContext = span.getMongodbObservationContext(); + if (mongodbContext != null) { + mongodbContext.setCommandName(commandName); + mongodbContext.setDatabaseName(namespace); + if (!collection.isEmpty()) { + mongodbContext.setCollectionName(collection); + } - if (!collection.isEmpty()) { - keyValues = keyValues.and(COLLECTION.withValue(collection)); - } - span.tagLowCardinality(keyValues); - - // tag server and connection info - ServerAddress serverAddress = serverAddressSupplier.get(); - ConnectionId connectionId = connectionIdSupplier.get(); - span.tagLowCardinality(KeyValues.of( - SERVER_ADDRESS.withValue(serverAddress.getHost()), - SERVER_PORT.withValue(String.valueOf(serverAddress.getPort())), - CLIENT_CONNECTION_ID.withValue(String.valueOf(connectionId.getLocalValue())), - SERVER_CONNECTION_ID.withValue(String.valueOf(connectionId.getServerValue())), - NETWORK_TRANSPORT.withValue(serverAddress instanceof UnixServerAddress ? "unix" : "tcp") - )); - - // tag session and transaction info - SessionContext sessionContext = operationContext.getSessionContext(); - if (sessionContext.hasSession() && !sessionContext.isImplicitSession()) { - span.tagLowCardinality(KeyValues.of( - TRANSACTION_NUMBER.withValue(String.valueOf(sessionContext.getTransactionNumber())), - SESSION_ID.withValue(String.valueOf(sessionContext.getSessionId() - .get(sessionContext.getSessionId().getFirstKey()) - .asBinary().asUuid())) - )); + ServerAddress serverAddress = serverAddressSupplier.get(); + mongodbContext.setServerAddress(serverAddress); + mongodbContext.setUnixSocket(serverAddress instanceof UnixServerAddress); + + ConnectionId connectionId = connectionIdSupplier.get(); + mongodbContext.setConnectionId(connectionId); + + if (command.containsKey("getMore")) { + long cursorId = command.getInt64("getMore").longValue(); + mongodbContext.setCursorId(cursorId); + } + + SessionContext sessionContext = operationContext.getSessionContext(); + if (sessionContext.hasSession() && !sessionContext.isImplicitSession()) { + mongodbContext.setTransactionNumber(sessionContext.getTransactionNumber()); + mongodbContext.setSessionId(String.valueOf(sessionContext.getSessionId() + .get(sessionContext.getSessionId().getFirstKey()) + .asBinary().asUuid())); + } } return span; @@ -297,17 +268,18 @@ public Span createOperationSpan(@Nullable final TransactionSpan transactionSpan, ? "" : "." + namespace.getCollectionName()); - KeyValues keyValues = KeyValues.of( - SYSTEM.withValue("mongodb"), - NAMESPACE.withValue(namespace.getDatabaseName())); - if (!MongoNamespaceHelper.COMMAND_COLLECTION_NAME.equalsIgnoreCase(namespace.getCollectionName())) { - keyValues = keyValues.and(COLLECTION.withValue(namespace.getCollectionName())); + Span span = addSpan(MONGODB_OPERATION, name, parentContext, namespace); + + // Populate domain fields on MongodbObservationContext — the convention reads these to produce tags + MongodbObservationContext mongodbContext = span.getMongodbObservationContext(); + if (mongodbContext != null) { + mongodbContext.setCommandName(commandName); + mongodbContext.setDatabaseName(namespace.getDatabaseName()); + if (!MongoNamespaceHelper.COMMAND_COLLECTION_NAME.equalsIgnoreCase(namespace.getCollectionName())) { + mongodbContext.setCollectionName(namespace.getCollectionName()); + } } - keyValues = keyValues.and(OPERATION_NAME.withValue(commandName), - OPERATION_SUMMARY.withValue(name)); - Span span = addSpan(name, parentContext, namespace); - span.tagLowCardinality(keyValues); operationContext.setTracingSpan(span); return span; } diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/TransactionSpan.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/TransactionSpan.java index 3d16d18a976..03a6301b02b 100644 --- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/TransactionSpan.java +++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/TransactionSpan.java @@ -54,6 +54,7 @@ public void handleTransactionSpanError(final Throwable e) { } if (!isConvenientTransaction) { + span.closeScope(); span.end(); } } @@ -67,6 +68,7 @@ public void finalizeTransactionSpan(final String status) { span.event(status); // clear previous commit error if any if (!isConvenientTransaction) { + span.closeScope(); span.end(); } reportedError = null; // clear previous commit error if any @@ -82,6 +84,7 @@ public void spanFinalizing(final boolean cleanupTransactionContext) { if (reportedError != null) { span.error(reportedError); } + span.closeScope(); span.end(); reportedError = null; // Don't clean up transaction context if we're still retrying (we want the retries to fold under the original transaction span) @@ -109,4 +112,12 @@ public void setIsConvenientTransaction() { public TraceContext getContext() { return span.context(); } + + /** + * Opens a scope for the transaction span, making it the current observation on the thread. + * Must only be called from the sync driver where open and close happen on the same thread. + */ + public void openScope() { + span.openScope(); + } } diff --git a/driver-core/src/main/com/mongodb/observability/ObservabilitySettings.java b/driver-core/src/main/com/mongodb/observability/ObservabilitySettings.java index 7cc3374cbf1..a9e6e43c9c9 100644 --- a/driver-core/src/main/com/mongodb/observability/ObservabilitySettings.java +++ b/driver-core/src/main/com/mongodb/observability/ObservabilitySettings.java @@ -16,7 +16,7 @@ package com.mongodb.observability; -import com.mongodb.annotations.Alpha; +import com.mongodb.annotations.Beta; import com.mongodb.annotations.Immutable; import com.mongodb.annotations.Reason; import com.mongodb.annotations.Sealed; @@ -27,7 +27,7 @@ * * @since 5.7 */ -@Alpha(Reason.CLIENT) +@Beta(Reason.CLIENT) @Sealed @Immutable public abstract class ObservabilitySettings { diff --git a/driver-core/src/main/com/mongodb/observability/micrometer/DefaultMongodbObservationConvention.java b/driver-core/src/main/com/mongodb/observability/micrometer/DefaultMongodbObservationConvention.java new file mode 100644 index 00000000000..fbce064a04f --- /dev/null +++ b/driver-core/src/main/com/mongodb/observability/micrometer/DefaultMongodbObservationConvention.java @@ -0,0 +1,172 @@ +/* + * 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.annotations.Beta; +import com.mongodb.annotations.Reason; +import com.mongodb.connection.ConnectionId; +import com.mongodb.internal.observability.micrometer.MongodbObservation; +import io.micrometer.common.KeyValues; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * Default {@link ObservationConvention} for MongoDB observations. + *

+ * Reads domain fields from {@link MongodbObservationContext} and produces the standard MongoDB + * low-cardinality and high-cardinality key-values. Users can provide a custom convention via + * {@link MicrometerObservabilitySettings.Builder#observationConvention(ObservationConvention)}. + *

+ * + * @since 5.7 + */ +@Beta(Reason.CLIENT) +public class DefaultMongodbObservationConvention implements ObservationConvention { + + @Override + public boolean supportsContext(final Observation.Context context) { + return context instanceof MongodbObservationContext; + } + + @Override + public KeyValues getLowCardinalityKeyValues(final MongodbObservationContext context) { + if (context.getObservationType() == MongodbObservation.MONGODB_OPERATION) { + return getOperationLowCardinalityKeyValues(context); + } else { + return getCommandLowCardinalityKeyValues(context); + } + } + + @Override + public KeyValues getHighCardinalityKeyValues(final MongodbObservationContext context) { + if (context.getObservationType() == MongodbObservation.MONGODB_COMMAND) { + return getCommandHighCardinalityKeyValues(context); + } + return KeyValues.empty(); + } + + private KeyValues getOperationLowCardinalityKeyValues(final MongodbObservationContext 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 MongodbObservationContext 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)); + } + 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 MongodbObservationContext context) { + KeyValues kv = KeyValues.empty(); + + String queryText = context.getQueryText(); + if (queryText != null) { + kv = kv.and(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()))); + } + 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(String.valueOf(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/observability/micrometer/MicrometerObservabilitySettings.java b/driver-core/src/main/com/mongodb/observability/micrometer/MicrometerObservabilitySettings.java index 426e4671185..ad663c063b5 100644 --- a/driver-core/src/main/com/mongodb/observability/micrometer/MicrometerObservabilitySettings.java +++ b/driver-core/src/main/com/mongodb/observability/micrometer/MicrometerObservabilitySettings.java @@ -17,12 +17,13 @@ package com.mongodb.observability.micrometer; import com.mongodb.MongoConfigurationException; -import com.mongodb.annotations.Alpha; +import com.mongodb.annotations.Beta; import com.mongodb.annotations.Immutable; import com.mongodb.annotations.NotThreadSafe; import com.mongodb.annotations.Reason; import com.mongodb.lang.Nullable; import com.mongodb.observability.ObservabilitySettings; +import io.micrometer.observation.ObservationConvention; import io.micrometer.observation.ObservationRegistry; import java.util.Objects; @@ -44,7 +45,7 @@ * * @since 5.7 */ -@Alpha(Reason.CLIENT) +@Beta(Reason.CLIENT) @Immutable public final class MicrometerObservabilitySettings extends ObservabilitySettings { @@ -64,6 +65,8 @@ public final class MicrometerObservabilitySettings extends ObservabilitySettings private final ObservationRegistry observationRegistry; private final int maxQueryTextLength; private final boolean enableCommandPayloadTracing; + @Nullable + private final ObservationConvention observationConvention; /** * Convenience method to create a Builder. @@ -99,6 +102,14 @@ public boolean isEnableCommandPayloadTracing() { return enableCommandPayloadTracing; } + /** + * @return the observation convention, or null to use the default + */ + @Nullable + public ObservationConvention getObservationConvention() { + return observationConvention; + } + /** * @return the maximum length of command payloads captured in tracing spans. */ @@ -115,6 +126,8 @@ public static final class Builder { private ObservationRegistry observationRegistry; private boolean enableCommandPayloadTracing; private int maxQueryTextLength = Integer.MAX_VALUE; + @Nullable + private ObservationConvention observationConvention; private Builder() { if (!OBSERVATION_REGISTRY_AVAILABLE) { @@ -126,6 +139,7 @@ private Builder(final MicrometerObservabilitySettings settings) { this.observationRegistry = settings.observationRegistry; this.enableCommandPayloadTracing = settings.enableCommandPayloadTracing; this.maxQueryTextLength = settings.maxQueryTextLength; + this.observationConvention = settings.observationConvention; } /** @@ -141,6 +155,7 @@ public MicrometerObservabilitySettings.Builder applySettings(final MicrometerObs observationRegistry = settings.observationRegistry; enableCommandPayloadTracing = settings.enableCommandPayloadTracing; maxQueryTextLength = settings.maxQueryTextLength; + observationConvention = settings.observationConvention; return this; } @@ -151,7 +166,7 @@ public MicrometerObservabilitySettings.Builder applySettings(final MicrometerObs * @return this * @since 5.7 */ - @Alpha(Reason.CLIENT) + @Beta(Reason.CLIENT) public Builder observationRegistry(@Nullable final ObservationRegistry observationRegistry) { this.observationRegistry = observationRegistry; return this; @@ -165,7 +180,7 @@ public Builder observationRegistry(@Nullable final ObservationRegistry observati * @return this * @since 5.7 */ - @Alpha(Reason.CLIENT) + @Beta(Reason.CLIENT) public Builder enableCommandPayloadTracing(final boolean enableCommandPayload) { this.enableCommandPayloadTracing = enableCommandPayload; return this; @@ -178,17 +193,32 @@ public Builder enableCommandPayloadTracing(final boolean enableCommandPayload) { * @return this * @since 5.7 */ - @Alpha(Reason.CLIENT) + @Beta(Reason.CLIENT) public Builder maxQueryTextLength(final int maxQueryTextLength) { this.maxQueryTextLength = maxQueryTextLength; return this; } + /** + * Sets a custom {@link ObservationConvention} to control the tag names and values produced by MongoDB observations. + * If not set, the driver uses {@link DefaultMongodbObservationConvention}. + * + * @param observationConvention the custom convention, or null to use the default + * @return this + * @since 5.7 + */ + @Beta(Reason.CLIENT) + public Builder observationConvention(@Nullable final ObservationConvention observationConvention) { + this.observationConvention = observationConvention; + return this; + } + /** * @return the configured settings */ public MicrometerObservabilitySettings build() { - return new MicrometerObservabilitySettings(observationRegistry, enableCommandPayloadTracing, maxQueryTextLength); + return new MicrometerObservabilitySettings(observationRegistry, enableCommandPayloadTracing, maxQueryTextLength, + observationConvention); } } @@ -199,18 +229,21 @@ public boolean equals(final Object o) { } final MicrometerObservabilitySettings that = (MicrometerObservabilitySettings) o; return enableCommandPayloadTracing == that.enableCommandPayloadTracing - && Objects.equals(observationRegistry, that.observationRegistry); + && Objects.equals(observationRegistry, that.observationRegistry) + && Objects.equals(observationConvention, that.observationConvention); } @Override public int hashCode() { - return Objects.hash(observationRegistry, enableCommandPayloadTracing); + return Objects.hash(observationRegistry, enableCommandPayloadTracing, observationConvention); } private MicrometerObservabilitySettings(@Nullable final ObservationRegistry observationRegistry, - final boolean enableCommandPayloadTracing, final int maxQueryTextLength) { + final boolean enableCommandPayloadTracing, final int maxQueryTextLength, + @Nullable final ObservationConvention observationConvention) { this.observationRegistry = observationRegistry; this.enableCommandPayloadTracing = enableCommandPayloadTracing; this.maxQueryTextLength = maxQueryTextLength; + this.observationConvention = observationConvention; } } diff --git a/driver-core/src/main/com/mongodb/observability/micrometer/MongodbObservationContext.java b/driver-core/src/main/com/mongodb/observability/micrometer/MongodbObservationContext.java new file mode 100644 index 00000000000..cbbd12a2e8d --- /dev/null +++ b/driver-core/src/main/com/mongodb/observability/micrometer/MongodbObservationContext.java @@ -0,0 +1,179 @@ +/* + * 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.annotations.Beta; +import com.mongodb.annotations.Reason; +import com.mongodb.connection.ConnectionId; +import com.mongodb.internal.observability.micrometer.MongodbObservation; +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. + *

+ * + * @since 5.7 + */ +@Beta(Reason.CLIENT) +public class MongodbObservationContext extends SenderContext { + + private MongodbObservation observationType; + @Nullable + private String commandName; + @Nullable + private String databaseName; + @Nullable + private String collectionName; + @Nullable + private ServerAddress serverAddress; + @Nullable + private ConnectionId connectionId; + @Nullable + private Long cursorId; + @Nullable + private Long transactionNumber; + @Nullable + private String sessionId; + @Nullable + private String queryText; + @Nullable + private String responseStatusCode; + private boolean isUnixSocket; + + public MongodbObservationContext() { + super((carrier, key, value) -> { }, Kind.CLIENT); + } + + @Nullable + public String getCommandName() { + return commandName; + } + + public void setCommandName(@Nullable final String commandName) { + this.commandName = commandName; + } + + @Nullable + public String getDatabaseName() { + return databaseName; + } + + public void setDatabaseName(@Nullable final String databaseName) { + this.databaseName = databaseName; + } + + @Nullable + public String getCollectionName() { + return collectionName; + } + + public void setCollectionName(@Nullable final String collectionName) { + this.collectionName = collectionName; + } + + @Nullable + public ServerAddress getServerAddress() { + return serverAddress; + } + + public void setServerAddress(@Nullable final ServerAddress serverAddress) { + this.serverAddress = serverAddress; + } + + @Nullable + public ConnectionId getConnectionId() { + return connectionId; + } + + public void setConnectionId(@Nullable final ConnectionId connectionId) { + this.connectionId = connectionId; + } + + @Nullable + public MongodbObservation getObservationType() { + return observationType; + } + + public void setObservationType(final MongodbObservation observationType) { + this.observationType = observationType; + } + + @Nullable + public Long getCursorId() { + return cursorId; + } + + public void setCursorId(@Nullable final Long cursorId) { + this.cursorId = cursorId; + } + + @Nullable + public Long getTransactionNumber() { + return transactionNumber; + } + + public void setTransactionNumber(@Nullable final Long transactionNumber) { + this.transactionNumber = transactionNumber; + } + + @Nullable + public String getSessionId() { + return sessionId; + } + + public void setSessionId(@Nullable final String sessionId) { + this.sessionId = sessionId; + } + + public boolean isUnixSocket() { + return isUnixSocket; + } + + public void setUnixSocket(final boolean unixSocket) { + isUnixSocket = unixSocket; + } + + @Nullable + public String getQueryText() { + return queryText; + } + + public void setQueryText(@Nullable final String queryText) { + this.queryText = queryText; + } + + @Nullable + public String getResponseStatusCode() { + return responseStatusCode; + } + + public void setResponseStatusCode(@Nullable final String responseStatusCode) { + this.responseStatusCode = responseStatusCode; + } +} diff --git a/driver-core/src/main/com/mongodb/observability/micrometer/package-info.java b/driver-core/src/main/com/mongodb/observability/micrometer/package-info.java index 64b044170bc..608e6096ee1 100644 --- a/driver-core/src/main/com/mongodb/observability/micrometer/package-info.java +++ b/driver-core/src/main/com/mongodb/observability/micrometer/package-info.java @@ -19,10 +19,10 @@ * * @since 5.7 */ -@Alpha(Reason.CLIENT) +@Beta(Reason.CLIENT) @NonNullApi package com.mongodb.observability.micrometer; -import com.mongodb.annotations.Alpha; +import com.mongodb.annotations.Beta; import com.mongodb.annotations.Reason; import com.mongodb.lang.NonNullApi; diff --git a/driver-core/src/main/com/mongodb/observability/package-info.java b/driver-core/src/main/com/mongodb/observability/package-info.java index 0e5b4c31c52..e3d72b3bf12 100644 --- a/driver-core/src/main/com/mongodb/observability/package-info.java +++ b/driver-core/src/main/com/mongodb/observability/package-info.java @@ -19,10 +19,10 @@ * * @since 5.7 */ -@Alpha(Reason.CLIENT) +@Beta(Reason.CLIENT) @NonNullApi package com.mongodb.observability; -import com.mongodb.lang.NonNullApi; -import com.mongodb.annotations.Alpha; +import com.mongodb.annotations.Beta; import com.mongodb.annotations.Reason; +import com.mongodb.lang.NonNullApi; diff --git a/driver-scala/src/test/scala-2/org/mongodb/scala/ApiAliasAndCompanionSpec.scala b/driver-scala/src/test/scala-2/org/mongodb/scala/ApiAliasAndCompanionSpec.scala index 3e94d3ee972..049ed92c011 100644 --- a/driver-scala/src/test/scala-2/org/mongodb/scala/ApiAliasAndCompanionSpec.scala +++ b/driver-scala/src/test/scala-2/org/mongodb/scala/ApiAliasAndCompanionSpec.scala @@ -96,7 +96,9 @@ class ApiAliasAndCompanionSpec extends BaseSpec { "BaseClientUpdateOptions", "BaseClientDeleteOptions", "MongoBaseInterfaceAssertions", + "DefaultMongodbObservationConvention", "MicrometerObservabilitySettings", + "MongodbObservationContext", "ObservabilitySettings" ) val scalaExclusions = Set( diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java index aa1414dce5d..c717a539a8f 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java +++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java @@ -178,6 +178,7 @@ private void startTransaction(final TransactionOptions transactionOptions, final if (tracingManager.isEnabled()) { transactionSpan = new TransactionSpan(tracingManager); + transactionSpan.openScope(); } clearTransactionContext(); setTimeoutContext(timeoutContext); diff --git a/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java b/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java index eb36678761a..c7c7180268d 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java +++ b/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java @@ -427,8 +427,9 @@ public T execute(final ReadOperation operation, final ReadPreference r Span span = operationContext.getTracingManager().createOperationSpan( actualClientSession.getTransactionSpan(), operationContext, operation.getCommandName(), operation.getNamespace()); ReadBinding binding = getReadBinding(readPreference, actualClientSession, implicitSession); - - + if (span != null) { + span.openScope(); + } try { if (actualClientSession.hasActiveTransaction() && !binding.getReadPreference().equals(primary())) { throw new MongoClientException("Read preference in a transaction must be primary"); @@ -445,6 +446,7 @@ public T execute(final ReadOperation operation, final ReadPreference r } finally { binding.release(); if (span != null) { + span.closeScope(); span.end(); } } @@ -463,7 +465,9 @@ public T execute(final WriteOperation operation, final ReadConcern readCo Span span = operationContext.getTracingManager().createOperationSpan( actualClientSession.getTransactionSpan(), operationContext, operation.getCommandName(), operation.getNamespace()); WriteBinding binding = getWriteBinding(actualClientSession, isImplicitSession(session)); - + if (span != null) { + span.openScope(); + } try { return operation.execute(binding, operationContext); } catch (MongoException e) { @@ -477,6 +481,7 @@ public T execute(final WriteOperation operation, final ReadConcern readCo } finally { binding.release(); if (span != null) { + span.closeScope(); span.end(); } } diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractMicrometerProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractMicrometerProseTest.java index 746b0ffd8d9..dc22eaa38be 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/AbstractMicrometerProseTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractMicrometerProseTest.java @@ -17,11 +17,16 @@ package com.mongodb.client; import com.mongodb.MongoClientSettings; +import com.mongodb.observability.micrometer.MongodbObservationContext; +import com.mongodb.internal.observability.micrometer.MongodbObservation; import com.mongodb.lang.Nullable; import com.mongodb.observability.ObservabilitySettings; import com.mongodb.client.observability.SpanTree; import com.mongodb.client.observability.SpanTree.SpanNode; import com.mongodb.observability.micrometer.MicrometerObservabilitySettings; +import io.micrometer.common.KeyValues; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; import io.micrometer.observation.ObservationRegistry; import io.micrometer.tracing.exporter.FinishedSpan; import io.micrometer.tracing.test.reporter.inmemory.InMemoryOtelSetup; @@ -52,6 +57,7 @@ import static com.mongodb.internal.observability.micrometer.TracingManager.ENV_OBSERVABILITY_ENABLED; import static com.mongodb.internal.observability.micrometer.TracingManager.ENV_OBSERVABILITY_QUERY_TEXT_MAX_LENGTH; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -317,6 +323,111 @@ void testConcurrentOperationsHaveSeparateSpans() throws Exception { } } + /** + * Verifies that a user-provided {@link ObservationConvention} via + * {@link MicrometerObservabilitySettings.Builder#observationConvention(ObservationConvention)} fully controls + * the tag output. The custom convention: + *
    + *
  • Adds a new tag ({@code custom.tag})
  • + *
  • Renames an existing tag ({@code db.command.name} → {@code mongodb.command}), + * reading the value from {@link MongodbObservationContext} domain fields
  • + *
  • Carries over unmodified tags ({@code db.namespace}, {@code db.system.name})
  • + *
+ * + *

This test is not from the specification.

+ */ + @SuppressWarnings("NullableProblems") + @Test + void testCustomObservationConvention() { + ObservationConvention customConvention = new ObservationConvention() { + @Override + public boolean supportsContext(final Observation.Context context) { + return context instanceof MongodbObservationContext; + } + + @Override + public KeyValues getLowCardinalityKeyValues(final MongodbObservationContext context) { + String commandName = context.getCommandName() != null ? context.getCommandName() : ""; + String databaseName = context.getDatabaseName() != null ? context.getDatabaseName() : ""; + + KeyValues kv = KeyValues.of( + "db.system.name", "mongodb", + "db.namespace", databaseName, + "custom.tag", "custom-value"); + + if (context.getObservationType() == MongodbObservation.MONGODB_COMMAND) { + // Rename: emit command name under "mongodb.command" instead of "db.command.name" + kv = kv.and("mongodb.command", commandName); + } + if (context.getObservationType() == MongodbObservation.MONGODB_OPERATION) { + kv = kv.and("db.operation.name", commandName); + } + return kv; + } + }; + + MongoClientSettings clientSettings = getMongoClientSettingsBuilder() + .observabilitySettings(ObservabilitySettings.micrometerBuilder() + .observationRegistry(observationRegistry) + .observationConvention(customConvention) + .build()) + .build(); + + try (MongoClient client = createMongoClient(clientSettings)) { + MongoDatabase database = client.getDatabase(getDefaultDatabaseName()); + MongoCollection collection = database.getCollection("test"); + collection.find().first(); + + List spans = inMemoryOtel.getFinishedSpans(); + assertEquals(2, spans.size(), "Expected 2 spans (operation + command)."); + + // Find the command span + FinishedSpan commandSpan = spans.stream() + .filter(s -> "find".equals(s.getName())) + .findFirst() + .orElseThrow(() -> new AssertionError("Command span 'find' not found.")); + + Map tags = commandSpan.getTags(); + + // Custom tag is present + assertEquals("custom-value", tags.get("custom.tag"), + "Custom convention should add 'custom.tag'."); + + // Renamed tag: "mongodb.command" instead of "db.command.name" + assertEquals("find", tags.get("mongodb.command"), + "Custom convention should emit command name under 'mongodb.command'."); + assertFalse(tags.containsKey("db.command.name"), "Custom convention should NOT emit the default 'db.command.name' tag."); + + // Unmodified tags carried over + assertEquals(getDefaultDatabaseName(), tags.get("db.namespace"), + "Custom convention should carry over 'db.namespace'."); + assertEquals("mongodb", tags.get("db.system.name"), + "Custom convention should carry over 'db.system.name'."); + + // Find the operation span + FinishedSpan operationSpan = spans.stream() + .filter(s -> s.getName().contains(getDefaultDatabaseName())) + .findFirst() + .orElseThrow(() -> new AssertionError("Operation span not found.")); + + Map opTags = operationSpan.getTags(); + + // Custom tag is present on operation span too + assertEquals("custom-value", opTags.get("custom.tag"), + "Custom convention should add 'custom.tag' to operation span."); + + // Operation span has db.operation.name + assertEquals("find", opTags.get("db.operation.name"), + "Custom convention should emit 'db.operation.name' on operation span."); + + // Unmodified tags on operation span + assertEquals(getDefaultDatabaseName(), opTags.get("db.namespace"), + "Custom convention should carry over 'db.namespace' on operation span."); + assertEquals("mongodb", opTags.get("db.system.name"), + "Custom convention should carry over 'db.system.name' on operation span."); + } + } + @SuppressWarnings("unchecked") private static void setEnv(final String key, @Nullable final String value) throws Exception { // Get the unmodifiable Map from System.getenv() diff --git a/driver-sync/src/test/functional/com/mongodb/client/observability/SpanTree.java b/driver-sync/src/test/functional/com/mongodb/client/observability/SpanTree.java index 7d3bff3224d..547b4541607 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/observability/SpanTree.java +++ b/driver-sync/src/test/functional/com/mongodb/client/observability/SpanTree.java @@ -34,12 +34,12 @@ import java.util.UUID; import java.util.function.BiConsumer; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.CLIENT_CONNECTION_ID; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.CURSOR_ID; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.SERVER_CONNECTION_ID; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.SERVER_PORT; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.SESSION_ID; -import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.TRANSACTION_NUMBER; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.CommandLowCardinalityKeyNames.SERVER_PORT; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.HighCardinalityKeyNames.CLIENT_CONNECTION_ID; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.HighCardinalityKeyNames.CURSOR_ID; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.HighCardinalityKeyNames.SERVER_CONNECTION_ID; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.HighCardinalityKeyNames.SESSION_ID; +import static com.mongodb.internal.observability.micrometer.MongodbObservation.HighCardinalityKeyNames.TRANSACTION_NUMBER; import static org.bson.assertions.Assertions.notNull; import static org.junit.jupiter.api.Assertions.fail; diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java b/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java index 12a4cb5db56..954ea29142f 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java @@ -593,6 +593,7 @@ private void initClient(final BsonDocument entity, final String id, .observabilitySettings(ObservabilitySettings.micrometerBuilder() .observationRegistry(observationRegistry) .enableCommandPayloadTracing(enableCommandPayload).build()); + } MongoClientSettings clientSettings = clientSettingsBuilder.build(); diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java index 02d097688e6..c1c37576c2c 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java @@ -16,7 +16,6 @@ package com.mongodb.client.unified; -import com.mongodb.ClusterFixture; import org.opentest4j.AssertionFailedError; import java.util.ArrayList; @@ -198,24 +197,8 @@ public static void applyCustomizations(final TestDef def) { "timeoutMS can be set to 0 on a MongoClient - dropIndexes on collection"); // OpenTelemetry - def.skipJira("https://jira.mongodb.org/browse/JAVA-5991") - .file("open-telemetry/tests", "operation find") - .file("open-telemetry/tests", "operation find_one_and_update") - .file("open-telemetry/tests", "operation update") - .file("open-telemetry/tests", "operation bulk_write") - .file("open-telemetry/tests", "operation drop collection") - .file("open-telemetry/tests", "transaction spans") - .file("open-telemetry/tests", "convenient transactions") - .file("open-telemetry/tests", "operation atlas_search") - .file("open-telemetry/tests", "operation insert") - .file("open-telemetry/tests", "operation map_reduce") - .file("open-telemetry/tests", "operation find without db.query.text") - .file("open-telemetry/tests", "operation find_retries"); - def.skipAccordingToSpec("Micrometer tests expect the network transport to be tcp") - .when(ClusterFixture::isUnixSocket) - .directory("open-telemetry/tests"); - def.skipJira("https://jira.mongodb.org/browse/JAVA-6094 TODO-JAVA-6094") - .directory("open-telemetry/tests"); + def.skipNoncompliantReactive("withTransaction is not supported in the reactive driver unified test runner") + .file("open-telemetry/tests", "convenient transactions"); // TODO-JAVA-5712