diff --git a/pom.xml b/pom.xml
index 1f8da7fc8..6dd374012 100644
--- a/pom.xml
+++ b/pom.xml
@@ -137,6 +137,9 @@
spring-libs-snapshot
https://repo.spring.io/libs-snapshot
+
+ true
+
sonatype-libs-snapshot
diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml
index f6a77ea6f..9b3e641ef 100644
--- a/spring-data-mongodb-distribution/pom.xml
+++ b/spring-data-mongodb-distribution/pom.xml
@@ -1,46 +1,124 @@
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
- 4.0.0
+ 4.0.0
- spring-data-mongodb-distribution
+ spring-data-mongodb-distribution
- pom
+ pom
- Spring Data MongoDB - Distribution
- Distribution build for Spring Data MongoDB
+ Spring Data MongoDB - Distribution
+ Distribution build for Spring Data MongoDB
-
- org.springframework.data
- spring-data-mongodb-parent
- 4.0.0-SNAPSHOT
- ../pom.xml
-
+
+ org.springframework.data
+ spring-data-mongodb-parent
+ 4.0.0-SNAPSHOT
+ ../pom.xml
+
-
- ${basedir}/..
- SDMONGO
-
+
+ ${basedir}/..
+ SDMONGO
-
-
-
- org.apache.maven.plugins
- maven-assembly-plugin
-
-
- org.asciidoctor
- asciidoctor-maven-plugin
-
-
- ${mongo.reactivestreams}
- ${reactor}
-
-
-
-
+
+ 1.0.0-SNAPSHOT
+ ${maven.multiModuleProjectDirectory}/spring-data-mongodb/
+
+ .*
+ ${maven.multiModuleProjectDirectory}/target/
+
+
-
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+
+
+ generate-metrics-metadata
+ prepare-package
+
+ java
+
+
+ io.micrometer.docs.metrics.DocsFromSources
+
+
+
+ generate-tracing-metadata
+ prepare-package
+
+ java
+
+
+ io.micrometer.docs.spans.DocsFromSources
+
+
+
+
+
+ io.micrometer
+ micrometer-docs-generator-spans
+ ${micrometer-docs-generator.version}
+ jar
+
+
+ io.micrometer
+ micrometer-docs-generator-metrics
+ ${micrometer-docs-generator.version}
+ jar
+
+
+
+ true
+
+ ${micrometer-docs-generator.inputPath}
+ ${micrometer-docs-generator.inclusionPattern}
+ ${micrometer-docs-generator.outputPath}
+
+
+
+
+ org.asciidoctor
+ asciidoctor-maven-plugin
+
+
+ ${mongo.reactivestreams}
+ ${reactor}
+
+
+
+
+
+
+
+
+
+ spring-snapshots
+ Spring Snapshots
+ https://repo.spring.io/snapshot
+
+ true
+
+
+ false
+
+
+
+ spring-milestones
+ Spring Milestones
+ https://repo.spring.io/milestone
+
+ false
+
+
+
diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml
index 92b47065f..21f6e7925 100644
--- a/spring-data-mongodb/pom.xml
+++ b/spring-data-mongodb/pom.xml
@@ -1,361 +1,391 @@
-
+
- 4.0.0
+ 4.0.0
- spring-data-mongodb
+ spring-data-mongodb
- Spring Data MongoDB - Core
- MongoDB support for Spring Data
+ Spring Data MongoDB - Core
+ MongoDB support for Spring Data
-
- org.springframework.data
- spring-data-mongodb-parent
- 4.0.0-SNAPSHOT
- ../pom.xml
-
+
+ org.springframework.data
+ spring-data-mongodb-parent
+ 4.0.0-SNAPSHOT
+ ../pom.xml
+
-
- 1.3
- 1.7.8
- spring.data.mongodb
- ${basedir}/..
- 1.01
-
+
+ 1.3
+ 1.7.8
+ spring.data.mongodb
+ ${basedir}/..
+ 1.01
+
-
+
-
-
- org.springframework
- spring-tx
-
-
- org.springframework
- spring-context
-
-
- org.springframework
- spring-beans
-
-
- org.springframework
- spring-core
-
-
- commons-logging
- commons-logging
-
-
-
-
- org.springframework
- spring-expression
-
+
+
+ org.springframework
+ spring-tx
+
+
+ org.springframework
+ spring-context
+
+
+ org.springframework
+ spring-beans
+
+
+ org.springframework
+ spring-core
+
+
+ commons-logging
+ commons-logging
+
+
+
+
+ org.springframework
+ spring-expression
+
-
-
- ${project.groupId}
- spring-data-commons
- ${springdata.commons}
-
+
+
+ ${project.groupId}
+ spring-data-commons
+ ${springdata.commons}
+
-
- com.querydsl
- querydsl-mongodb
- ${querydsl}
- true
-
-
- org.mongodb
- mongo-java-driver
-
-
-
+
+ com.querydsl
+ querydsl-mongodb
+ ${querydsl}
+ true
+
+
+ org.mongodb
+ mongo-java-driver
+
+
+
-
- com.querydsl
- querydsl-apt
- ${querydsl}
- provided
-
+
+ com.querydsl
+ querydsl-apt
+ ${querydsl}
+ provided
+
-
- javax.annotation
- jsr250-api
- 1.0
- true
-
+
+ javax.annotation
+ jsr250-api
+ 1.0
+ true
+
-
- com.google.code.findbugs
- jsr305
- 3.0.2
- true
-
+
+ com.google.code.findbugs
+ jsr305
+ 3.0.2
+ true
+
-
+
-
- org.mongodb
- mongodb-driver-sync
- ${mongo}
- true
-
+
+ org.mongodb
+ mongodb-driver-sync
+ ${mongo}
+ true
+
-
- org.mongodb
- mongodb-driver-reactivestreams
- ${mongo.reactivestreams}
- true
-
+
+ org.mongodb
+ mongodb-driver-reactivestreams
+ ${mongo.reactivestreams}
+ true
+
-
- io.projectreactor
- reactor-core
- true
-
+
+ io.projectreactor
+ reactor-core
+ true
+
-
- io.projectreactor
- reactor-test
- true
-
+
+ io.projectreactor
+ reactor-test
+ true
+
-
- io.reactivex.rxjava3
- rxjava
- ${rxjava3}
- true
-
+
+ io.reactivex.rxjava3
+ rxjava
+ ${rxjava3}
+ true
+
-
-
+
+
-
- javax.interceptor
- javax.interceptor-api
- 1.2.1
- test
-
+
+ javax.interceptor
+ javax.interceptor-api
+ 1.2.1
+ test
+
-
- jakarta.enterprise
- jakarta.enterprise.cdi-api
- ${cdi}
- provided
- true
-
+
+ jakarta.enterprise
+ jakarta.enterprise.cdi-api
+ ${cdi}
+ provided
+ true
+
-
- jakarta.annotation
- jakarta.annotation-api
- ${jakarta-annotation-api}
- test
-
+
+ jakarta.annotation
+ jakarta.annotation-api
+ ${jakarta-annotation-api}
+ test
+
-
- org.apache.openwebbeans
- openwebbeans-se
- jakarta
- ${webbeans}
- test
-
+
+ org.apache.openwebbeans
+ openwebbeans-se
+ jakarta
+ ${webbeans}
+ test
+
-
- org.apache.openwebbeans
- openwebbeans-spi
- jakarta
- ${webbeans}
- test
-
+
+ org.apache.openwebbeans
+ openwebbeans-spi
+ jakarta
+ ${webbeans}
+ test
+
-
- org.apache.openwebbeans
- openwebbeans-impl
- jakarta
- ${webbeans}
- test
-
+
+ org.apache.openwebbeans
+ openwebbeans-impl
+ jakarta
+ ${webbeans}
+ test
+
-
-
- jakarta.validation
- jakarta.validation-api
- ${validation}
- true
-
+
+
+ jakarta.validation
+ jakarta.validation-api
+ ${validation}
+ true
+
-
- org.objenesis
- objenesis
- ${objenesis}
- true
-
+
+ org.objenesis
+ objenesis
+ ${objenesis}
+ true
+
-
- org.hibernate
- hibernate-validator
- 7.0.1.Final
- test
-
+
+ io.micrometer
+ micrometer-observation
+ true
+
-
- jakarta.el
- jakarta.el-api
- 4.0.0
- provided
- true
-
+
+ io.micrometer
+ micrometer-tracing-api
+ true
+
-
- org.glassfish
- jakarta.el
- 4.0.2
- provided
- true
-
+
+ org.hibernate
+ hibernate-validator
+ 7.0.1.Final
+ test
+
-
- com.fasterxml.jackson.core
- jackson-databind
- true
-
+
+ jakarta.el
+ jakarta.el-api
+ 4.0.0
+ provided
+ true
+
-
- nl.jqno.equalsverifier
- equalsverifier
- ${equalsverifier}
- test
-
+
+ org.glassfish
+ jakarta.el
+ 4.0.2
+ provided
+ true
+
-
- org.springframework
- spring-webmvc
- test
-
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ true
+
-
- de.schauderhaft.degraph
- degraph-check
- 0.1.4
- test
-
+
+ nl.jqno.equalsverifier
+ equalsverifier
+ ${equalsverifier}
+ test
+
-
- edu.umd.cs.mtc
- multithreadedtc
- ${multithreadedtc}
- test
-
+
+ org.springframework
+ spring-webmvc
+ test
+
-
- org.junit-pioneer
- junit-pioneer
- 0.5.3
- test
-
+
+ de.schauderhaft.degraph
+ degraph-check
+ 0.1.4
+ test
+
-
- jakarta.transaction
- jakarta.transaction-api
- 2.0.0
- test
-
+
+ edu.umd.cs.mtc
+ multithreadedtc
+ ${multithreadedtc}
+ test
+
-
-
- org.jetbrains.kotlin
- kotlin-stdlib
- true
-
+
+ org.junit-pioneer
+ junit-pioneer
+ 0.5.3
+ test
+
-
- org.jetbrains.kotlin
- kotlin-reflect
- true
-
+
+ jakarta.transaction
+ jakarta.transaction-api
+ 2.0.0
+ test
+
-
- org.jetbrains.kotlinx
- kotlinx-coroutines-core
- true
-
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ true
+
-
- org.jetbrains.kotlinx
- kotlinx-coroutines-reactor
- true
-
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+ true
+
-
- io.mockk
- mockk
- ${mockk}
- test
-
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-core
+ true
+
-
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-reactor
+ true
+
-
- org.jmolecules
- jmolecules-ddd
- ${jmolecules}
- test
-
+
+ io.mockk
+ mockk
+ ${mockk}
+ test
+
-
+
+ io.micrometer
+ micrometer-test
+ test
+
+
+ io.micrometer
+ micrometer-tracing-test
+ test
+
-
+
+ io.micrometer
+ micrometer-tracing-integration-test
+ test
+
-
+
-
- com.mysema.maven
- apt-maven-plugin
- ${apt}
-
-
- com.querydsl
- querydsl-apt
- ${querydsl}
-
-
-
-
- generate-test-sources
-
- test-process
-
-
- target/generated-test-sources
- org.springframework.data.mongodb.repository.support.MongoAnnotationProcessor
-
-
-
-
+
+ org.jmolecules
+ jmolecules-ddd
+ ${jmolecules}
+ test
+
-
- org.apache.maven.plugins
- maven-surefire-plugin
-
- false
- false
-
- **/*Tests.java
-
-
- **/PerformanceTests.java
- **/ReactivePerformanceTests.java
-
-
- src/test/resources/logging.properties
- true
-
-
-
+
-
+
-
+
+
+
+ com.mysema.maven
+ apt-maven-plugin
+ ${apt}
+
+
+ com.querydsl
+ querydsl-apt
+ ${querydsl}
+
+
+
+
+ generate-test-sources
+
+ test-process
+
+
+ target/generated-test-sources
+ org.springframework.data.mongodb.repository.support.MongoAnnotationProcessor
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ false
+ false
+
+ **/*Tests.java
+
+
+ **/PerformanceTests.java
+ **/ReactivePerformanceTests.java
+
+
+ src/test/resources/logging.properties
+ true
+
+
+
+
+
+
+
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerTagsProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerTagsProvider.java
new file mode 100644
index 000000000..24b30b20f
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerTagsProvider.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2013-2022 the original author or authors.
+ *
+ * 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
+ *
+ * https://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 org.springframework.data.mongodb.observability;
+
+import io.micrometer.common.Tag;
+import io.micrometer.common.Tags;
+
+import com.mongodb.connection.ConnectionDescription;
+import com.mongodb.connection.ConnectionId;
+import com.mongodb.event.CommandStartedEvent;
+
+/**
+ * Default {@link MongoHandlerTagsProvider} implementation.
+ *
+ * @author Greg Turnquist
+ * @since 4.0.0
+ */
+public class DefaultMongoHandlerTagsProvider implements MongoHandlerTagsProvider {
+
+ @Override
+ public Tags getLowCardinalityTags(MongoHandlerContext context) {
+
+ Tags tags = Tags.empty();
+
+ if (context.getCollectionName() != null) {
+ tags = tags.and(MongoObservation.LowCardinalityCommandTags.MONGODB_COLLECTION.of(context.getCollectionName()));
+ }
+
+ Tag connectionTag = connectionTag(context.getCommandStartedEvent());
+ if (connectionTag != null) {
+ tags = tags.and(connectionTag);
+ }
+
+ return tags;
+ }
+
+ @Override
+ public Tags getHighCardinalityTags(MongoHandlerContext context) {
+
+ return Tags.of(MongoObservation.HighCardinalityCommandTags.MONGODB_COMMAND
+ .of(context.getCommandStartedEvent().getCommandName()));
+ }
+
+ /**
+ * Extract connection details for a MongoDB connection into a {@link Tag}.
+ *
+ * @param event
+ * @return
+ */
+ private static Tag connectionTag(CommandStartedEvent event) {
+
+ ConnectionDescription connectionDescription = event.getConnectionDescription();
+
+ if (connectionDescription != null) {
+
+ ConnectionId connectionId = connectionDescription.getConnectionId();
+ if (connectionId != null) {
+ return MongoObservation.LowCardinalityCommandTags.MONGODB_CLUSTER_ID
+ .of(connectionId.getServerId().getClusterId().getValue());
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerContext.java
new file mode 100644
index 000000000..7fece69fe
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerContext.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2013-2022 the original author or authors.
+ *
+ * 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
+ *
+ * https://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 org.springframework.data.mongodb.observability;
+
+import io.micrometer.observation.Observation;
+
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import org.bson.BsonDocument;
+import org.bson.BsonValue;
+import org.springframework.lang.Nullable;
+
+import com.mongodb.RequestContext;
+import com.mongodb.event.CommandFailedEvent;
+import com.mongodb.event.CommandStartedEvent;
+import com.mongodb.event.CommandSucceededEvent;
+
+/**
+ * A {@link Observation.Context} that contains MongoDB events.
+ *
+ * @author Marcin Grzejszczak
+ * @author Greg Turnquist
+ * @since 4.0.0
+ */
+public class MongoHandlerContext extends Observation.Context {
+
+ /**
+ * @see https://docs.mongodb.com/manual/reference/command for the command reference
+ */
+ private static final Set COMMANDS_WITH_COLLECTION_NAME = new LinkedHashSet<>(
+ Arrays.asList("aggregate", "count", "distinct", "mapReduce", "geoSearch", "delete", "find", "findAndModify",
+ "insert", "update", "collMod", "compact", "convertToCapped", "create", "createIndexes", "drop", "dropIndexes",
+ "killCursors", "listIndexes", "reIndex"));
+
+ private final CommandStartedEvent commandStartedEvent;
+ private final RequestContext requestContext;
+ private final String collectionName;
+
+ private CommandSucceededEvent commandSucceededEvent;
+ private CommandFailedEvent commandFailedEvent;
+
+ public MongoHandlerContext(CommandStartedEvent commandStartedEvent, RequestContext requestContext) {
+
+ this.commandStartedEvent = commandStartedEvent;
+ this.requestContext = requestContext;
+ this.collectionName = getCollectionName(commandStartedEvent);
+ }
+
+ public CommandStartedEvent getCommandStartedEvent() {
+ return this.commandStartedEvent;
+ }
+
+ public RequestContext getRequestContext() {
+ return this.requestContext;
+ }
+
+ public String getCollectionName() {
+ return this.collectionName;
+ }
+
+ public String getContextualName() {
+
+ if (this.collectionName == null) {
+ return this.commandStartedEvent.getCommandName();
+ }
+
+ return this.commandStartedEvent.getCommandName() + " " + this.collectionName;
+ }
+
+ public void setCommandSucceededEvent(CommandSucceededEvent commandSucceededEvent) {
+ this.commandSucceededEvent = commandSucceededEvent;
+ }
+
+ public void setCommandFailedEvent(CommandFailedEvent commandFailedEvent) {
+ this.commandFailedEvent = commandFailedEvent;
+ }
+
+ /**
+ * Transform the command name into a collection name;
+ *
+ * @param event the {@link CommandStartedEvent}
+ * @return the name of the collection based on the command
+ */
+ @Nullable
+ private static String getCollectionName(CommandStartedEvent event) {
+
+ String commandName = event.getCommandName();
+ BsonDocument command = event.getCommand();
+
+ if (COMMANDS_WITH_COLLECTION_NAME.contains(commandName)) {
+
+ String collectionName = getNonEmptyBsonString(command.get(commandName));
+
+ if (collectionName != null) {
+ return collectionName;
+ }
+ }
+
+ // Some other commands, like getMore, have a field like {"collection": collectionName}.
+ return getNonEmptyBsonString(command.get("collection"));
+ }
+
+ /**
+ * Utility method to convert {@link BsonValue} into a plain string.
+ *
+ * @return trimmed string from {@code bsonValue} or null if the trimmed string was empty or the value wasn't a string
+ */
+ @Nullable
+ private static String getNonEmptyBsonString(BsonValue bsonValue) {
+
+ if (bsonValue == null || !bsonValue.isString()) {
+ return null;
+ }
+
+ String stringValue = bsonValue.asString().getValue().trim();
+
+ return stringValue.isEmpty() ? null : stringValue;
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerTagsProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerTagsProvider.java
new file mode 100644
index 000000000..eec9e9e81
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerTagsProvider.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2013-2022 the original author or authors.
+ *
+ * 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
+ *
+ * https://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 org.springframework.data.mongodb.observability;
+
+import io.micrometer.observation.Observation;
+
+/**
+ * {@link Observation.TagsProvider} for {@link MongoHandlerContext}.
+ *
+ * @author Greg Turnquist
+ * @since 4.0.0
+ */
+public interface MongoHandlerTagsProvider extends Observation.TagsProvider {
+
+ @Override
+ default boolean supportsContext(Observation.Context context) {
+ return context instanceof MongoHandlerContext;
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservation.java
new file mode 100644
index 000000000..6d10250bf
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservation.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2013-2022 the original author or authors.
+ *
+ * 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
+ *
+ * https://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 org.springframework.data.mongodb.observability;
+
+import io.micrometer.common.docs.TagKey;
+import io.micrometer.observation.docs.DocumentedObservation;
+
+/**
+ * A MongoDB-based {@link io.micrometer.observation.Observation}.
+ *
+ * @author Marcin Grzejszczak
+ * @author Greg Turnquist
+ * @since 1.0.0
+ */
+enum MongoObservation implements DocumentedObservation {
+
+ /**
+ * Timer created around a MongoDB command execution.
+ */
+ MONGODB_COMMAND_OBSERVATION {
+
+ @Override
+ public String getName() {
+ return "spring.data.mongodb.command";
+ }
+
+ @Override
+ public TagKey[] getLowCardinalityTagKeys() {
+ return LowCardinalityCommandTags.values();
+ }
+
+ @Override
+ public TagKey[] getHighCardinalityTagKeys() {
+ return HighCardinalityCommandTags.values();
+ }
+
+ @Override
+ public String getPrefix() {
+ return "spring.data.mongodb";
+ }
+ };
+
+ /**
+ * Enums related to low cardinality tags for MongoDB commands.
+ */
+ enum LowCardinalityCommandTags implements TagKey {
+
+ /**
+ * MongoDB collection name.
+ */
+ MONGODB_COLLECTION {
+ @Override
+ public String getKey() {
+ return "spring.data.mongodb.collection";
+ }
+ },
+
+ /**
+ * MongoDB cluster identifier.
+ */
+ MONGODB_CLUSTER_ID {
+ @Override
+ public String getKey() {
+ return "spring.data.mongodb.cluster_id";
+ }
+ }
+ }
+
+ /**
+ * Enums related to high cardinality tags for MongoDB commands.
+ */
+ enum HighCardinalityCommandTags implements TagKey {
+
+ /**
+ * MongoDB command value.
+ */
+ MONGODB_COMMAND {
+ @Override
+ public String getKey() {
+ return "spring.data.mongodb.command";
+ }
+ }
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservationCommandListener.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservationCommandListener.java
new file mode 100644
index 000000000..3a708e590
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservationCommandListener.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2013-2022 the original author or authors.
+ *
+ * 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
+ *
+ * https://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 org.springframework.data.mongodb.observability;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationRegistry;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import com.mongodb.RequestContext;
+import com.mongodb.event.CommandFailedEvent;
+import com.mongodb.event.CommandListener;
+import com.mongodb.event.CommandStartedEvent;
+import com.mongodb.event.CommandSucceededEvent;
+
+/**
+ * Implement MongoDB's {@link CommandListener} using Micrometer's {@link Observation} API.
+ *
+ * @see https://github.com/openzipkin/brave/blob/release-5.13.0/instrumentation/mongodb/src/main/java/brave/mongodb/TraceMongoCommandListener.java
+ * @author OpenZipkin Brave Authors
+ * @author Marcin Grzejszczak
+ * @author Greg Turnquist
+ * @since 4.0.0
+ */
+public final class MongoObservationCommandListener
+ implements CommandListener, Observation.TagsProviderAware {
+
+ private static final Log log = LogFactory.getLog(MongoObservationCommandListener.class);
+
+ private final ObservationRegistry observationRegistry;
+
+ private MongoHandlerTagsProvider tagsProvider;
+
+ public MongoObservationCommandListener(ObservationRegistry observationRegistry) {
+
+ this.observationRegistry = observationRegistry;
+ this.tagsProvider = new DefaultMongoHandlerTagsProvider();
+ }
+
+ @Override
+ public void commandStarted(CommandStartedEvent event) {
+
+ if (log.isDebugEnabled()) {
+ log.debug("Instrumenting the command started event");
+ }
+
+ String databaseName = event.getDatabaseName();
+
+ if ("admin".equals(databaseName)) {
+ return; // don't instrument commands like "endSessions"
+ }
+
+ RequestContext requestContext = event.getRequestContext();
+
+ if (requestContext == null) {
+ return;
+ }
+
+ Observation parent = observationFromContext(requestContext);
+
+ if (log.isDebugEnabled()) {
+ log.debug("Found the following observation passed from the mongo context [" + parent + "]");
+ }
+
+ if (parent == null) {
+ return;
+ }
+
+ setupObservability(event, requestContext);
+ }
+
+ @Override
+ public void commandSucceeded(CommandSucceededEvent event) {
+
+ if (event.getRequestContext() == null) {
+ return;
+ }
+
+ Observation observation = event.getRequestContext().getOrDefault(Observation.class, null);
+ if (observation == null) {
+ return;
+ }
+
+ MongoHandlerContext context = event.getRequestContext().get(MongoHandlerContext.class);
+ context.setCommandSucceededEvent(event);
+
+ if (log.isDebugEnabled()) {
+ log.debug("Command succeeded - will stop observation [" + observation + "]");
+ }
+
+ observation.stop();
+ }
+
+ @Override
+ public void commandFailed(CommandFailedEvent event) {
+
+ if (event.getRequestContext() == null) {
+ return;
+ }
+
+ Observation observation = event.getRequestContext().getOrDefault(Observation.class, null);
+ if (observation == null) {
+ return;
+ }
+
+ MongoHandlerContext context = event.getRequestContext().get(MongoHandlerContext.class);
+ context.setCommandFailedEvent(event);
+
+ if (log.isDebugEnabled()) {
+ log.debug("Command failed - will stop observation [" + observation + "]");
+ }
+
+ observation.error(event.getThrowable());
+ observation.stop();
+ }
+
+ /**
+ * Extract the {@link Observation} from MongoDB's {@link RequestContext}.
+ *
+ * @param context
+ * @return
+ */
+ private static Observation observationFromContext(RequestContext context) {
+
+ Observation observation = context.getOrDefault(Observation.class, null);
+
+ if (observation != null) {
+
+ if (log.isDebugEnabled()) {
+ log.debug("Found a observation in mongo context [" + observation + "]");
+ }
+ return observation;
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("No observation was found - will not create any child spans");
+ }
+
+ return null;
+ }
+
+ private void setupObservability(CommandStartedEvent event, RequestContext requestContext) {
+
+ MongoHandlerContext observationContext = new MongoHandlerContext(event, requestContext);
+
+ Observation observation = MongoObservation.MONGODB_COMMAND_OBSERVATION
+ .observation(this.observationRegistry, observationContext) //
+ .contextualName(observationContext.getContextualName()) //
+ .tagsProvider(this.tagsProvider) //
+ .start();
+
+ requestContext.put(Observation.class, observation);
+ requestContext.put(MongoHandlerContext.class, observationContext);
+
+ if (log.isDebugEnabled()) {
+ log.debug(
+ "Created a child observation [" + observation + "] for mongo instrumentation and put it in mongo context");
+ }
+ }
+
+ @Override
+ public void setTagsProvider(MongoHandlerTagsProvider mongoHandlerTagsProvider) {
+ this.tagsProvider = mongoHandlerTagsProvider;
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoTracingObservationHandler.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoTracingObservationHandler.java
new file mode 100644
index 000000000..aae9d9624
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoTracingObservationHandler.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2013-2022 the original author or authors.
+ *
+ * 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
+ *
+ * https://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 org.springframework.data.mongodb.observability;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.tracing.Span;
+import io.micrometer.tracing.Tracer;
+import io.micrometer.tracing.handler.TracingObservationHandler;
+
+import java.net.InetSocketAddress;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import com.mongodb.MongoSocketException;
+import com.mongodb.connection.ConnectionDescription;
+import com.mongodb.event.CommandStartedEvent;
+
+/**
+ * A {@link TracingObservationHandler} that handles {@link MongoHandlerContext}. It configures a span specific to Mongo
+ * operations.
+ *
+ * @author Marcin Grzejszczak
+ * @author Greg Turnquist
+ * @since 4.0.0
+ */
+public class MongoTracingObservationHandler implements TracingObservationHandler {
+
+ private static final Log log = LogFactory.getLog(MongoTracingObservationHandler.class);
+
+ private final Tracer tracer;
+
+ private boolean setRemoteIpAndPortEnabled;
+
+ public MongoTracingObservationHandler(Tracer tracer) {
+ this.tracer = tracer;
+ }
+
+ @Override
+ public Tracer getTracer() {
+ return this.tracer;
+ }
+
+ @Override
+ public void onStart(MongoHandlerContext context) {
+
+ CommandStartedEvent event = context.getCommandStartedEvent();
+
+ Span.Builder builder = this.tracer.spanBuilder() //
+ .name(context.getContextualName()) //
+ .kind(Span.Kind.CLIENT) //
+ .remoteServiceName("mongodb-" + event.getDatabaseName());
+
+ if (this.setRemoteIpAndPortEnabled) {
+
+ ConnectionDescription connectionDescription = event.getConnectionDescription();
+
+ if (connectionDescription != null) {
+
+ try {
+
+ InetSocketAddress socketAddress = connectionDescription.getServerAddress().getSocketAddress();
+ builder.remoteIpAndPort(socketAddress.getAddress().getHostAddress(), socketAddress.getPort());
+ } catch (MongoSocketException e) {
+ if (log.isDebugEnabled()) {
+ log.debug("Ignored exception when setting remote ip and port", e);
+ }
+ }
+ }
+ }
+
+ getTracingContext(context).setSpan(builder.start());
+ }
+
+ @Override
+ public void onStop(MongoHandlerContext context) {
+
+ Span span = getRequiredSpan(context);
+ tagSpan(context, span);
+
+ context.getRequestContext().delete(Observation.class);
+ context.getRequestContext().delete(MongoHandlerContext.class);
+
+ span.end();
+ }
+
+ @Override
+ public boolean supportsContext(Observation.Context context) {
+ return context instanceof MongoHandlerContext;
+ }
+
+ /**
+ * Should remote ip and port be set on the span.
+ *
+ * @return {@code true} when the remote ip and port should be set
+ */
+ public boolean isSetRemoteIpAndPortEnabled() {
+ return this.setRemoteIpAndPortEnabled;
+ }
+
+ public void setSetRemoteIpAndPortEnabled(boolean setRemoteIpAndPortEnabled) {
+ this.setRemoteIpAndPortEnabled = setRemoteIpAndPortEnabled;
+ }
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerForTracingTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerForTracingTests.java
new file mode 100644
index 000000000..e4da69f6e
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerForTracingTests.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * 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
+ *
+ * https://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 org.springframework.data.mongodb.observability;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.observation.TimerObservationHandler;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationRegistry;
+import io.micrometer.tracing.Span;
+import io.micrometer.tracing.test.simple.SimpleTracer;
+import io.micrometer.tracing.test.simple.SpanAssert;
+import io.micrometer.tracing.test.simple.TracerAssert;
+
+import org.bson.BsonDocument;
+import org.bson.BsonString;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.data.mongodb.observability.MongoObservation.HighCardinalityCommandTags;
+import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandTags;
+
+import com.mongodb.ServerAddress;
+import com.mongodb.connection.ClusterId;
+import com.mongodb.connection.ConnectionDescription;
+import com.mongodb.connection.ServerId;
+import com.mongodb.event.CommandFailedEvent;
+import com.mongodb.event.CommandStartedEvent;
+import com.mongodb.event.CommandSucceededEvent;
+
+/**
+ * Series of test cases exercising {@link MongoObservationCommandListener} to ensure proper creation of {@link Span}s.
+ *
+ * @author Marcin Grzejszczak
+ * @author Greg Turnquist
+ * @since 4.0.0
+ */
+class MongoObservationCommandListenerForTracingTests {
+
+ SimpleTracer simpleTracer;
+
+ MongoTracingObservationHandler handler;
+
+ MeterRegistry meterRegistry;
+ ObservationRegistry observationRegistry;
+
+ MongoObservationCommandListener listener;
+
+ @BeforeEach
+ void setup() {
+
+ this.simpleTracer = new SimpleTracer();
+ this.handler = new MongoTracingObservationHandler(simpleTracer);
+
+ this.meterRegistry = new SimpleMeterRegistry();
+ this.observationRegistry = ObservationRegistry.create();
+ this.observationRegistry.observationConfig().observationHandler(new TimerObservationHandler(meterRegistry));
+ this.observationRegistry.observationConfig().observationHandler(handler);
+
+ this.listener = new MongoObservationCommandListener(observationRegistry);
+ }
+
+ @Test
+ void successfullyCompletedCommandShouldCreateSpanWhenParentSampleInRequestContext() {
+
+ // given
+ TestRequestContext testRequestContext = createTestRequestContextWithParentObservationAndStartIt();
+
+ // when
+ commandStartedAndSucceeded(testRequestContext);
+
+ // then
+ assertThatMongoSpanIsClientWithTags().hasIpThatIsBlank().hasPortThatIsNotSet();
+ }
+
+ @Test
+ void successfullyCompletedCommandShouldCreateSpanWithAddressInfoWhenParentSampleInRequestContextAndHandlerAddressInfoEnabled() {
+
+ // given
+ handler.setSetRemoteIpAndPortEnabled(true);
+ TestRequestContext testRequestContext = createTestRequestContextWithParentObservationAndStartIt();
+
+ // when
+ commandStartedAndSucceeded(testRequestContext);
+
+ // then
+ assertThatMongoSpanIsClientWithTags().hasIpThatIsNotBlank().hasPortThatIsSet();
+ }
+
+ @Test
+ void commandWithErrorShouldCreateTimerWhenParentSampleInRequestContext() {
+
+ // given
+ TestRequestContext testRequestContext = createTestRequestContextWithParentObservationAndStartIt();
+
+ // when
+ listener.commandStarted(new CommandStartedEvent(testRequestContext, 0, //
+ new ConnectionDescription( //
+ new ServerId( //
+ new ClusterId("description"), //
+ new ServerAddress("localhost", 1234))), //
+ "database", "insert", //
+ new BsonDocument("collection", new BsonString("user"))));
+ listener.commandFailed( //
+ new CommandFailedEvent(testRequestContext, 0, null, "insert", 0, new IllegalAccessException()));
+
+ // then
+ assertThatMongoSpanIsClientWithTags().assertThatThrowable().isInstanceOf(IllegalAccessException.class);
+ }
+
+ /**
+ * Create a parent {@link Observation} then wrap it inside a {@link TestRequestContext}.
+ */
+ @NotNull
+ private TestRequestContext createTestRequestContextWithParentObservationAndStartIt() {
+
+ Observation parent = Observation.start("name", observationRegistry);
+ return TestRequestContext.withObservation(parent);
+ }
+
+ /**
+ * Execute MongoDB's {@link com.mongodb.event.CommandListener#commandStarted(CommandStartedEvent)} and
+ * {@link com.mongodb.event.CommandListener#commandSucceeded(CommandSucceededEvent)} operations against the
+ * {@link TestRequestContext} in order to inject some test data.
+ *
+ * @param testRequestContext
+ */
+ private void commandStartedAndSucceeded(TestRequestContext testRequestContext) {
+
+ listener.commandStarted(new CommandStartedEvent(testRequestContext, 0, //
+ new ConnectionDescription( //
+ new ServerId( //
+ new ClusterId("description"), //
+ new ServerAddress("localhost", 1234))), //
+ "database", "insert", //
+ new BsonDocument("collection", new BsonString("user"))));
+
+ listener.commandSucceeded(new CommandSucceededEvent(testRequestContext, 0, null, "insert", null, 0));
+ }
+
+ /**
+ * Create a base MongoDB-based {@link SpanAssert} using Micrometer Tracing's fluent API. Other test methods can apply
+ * additional assertions.
+ *
+ * @return
+ */
+ private SpanAssert assertThatMongoSpanIsClientWithTags() {
+
+ return TracerAssert.assertThat(simpleTracer).onlySpan() //
+ .hasNameEqualTo("insert user") //
+ .hasKindEqualTo(Span.Kind.CLIENT) //
+ .hasRemoteServiceNameEqualTo("mongodb-database") //
+ .hasTag(HighCardinalityCommandTags.MONGODB_COMMAND.getKey(), "insert") //
+ .hasTag(LowCardinalityCommandTags.MONGODB_COLLECTION.getKey(), "user") //
+ .hasTagWithKey(LowCardinalityCommandTags.MONGODB_CLUSTER_ID.getKey());
+ }
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java
new file mode 100644
index 000000000..154b71a4b
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * 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
+ *
+ * https://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 org.springframework.data.mongodb.observability;
+
+import static io.micrometer.core.tck.MeterRegistryAssert.*;
+
+import io.micrometer.common.Tags;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.observation.TimerObservationHandler;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationRegistry;
+
+import org.bson.BsonDocument;
+import org.bson.BsonString;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.data.mongodb.observability.MongoObservation.HighCardinalityCommandTags;
+import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandTags;
+
+import com.mongodb.ServerAddress;
+import com.mongodb.connection.ClusterId;
+import com.mongodb.connection.ConnectionDescription;
+import com.mongodb.connection.ServerId;
+import com.mongodb.event.CommandFailedEvent;
+import com.mongodb.event.CommandStartedEvent;
+import com.mongodb.event.CommandSucceededEvent;
+
+/**
+ * Series of test cases exercising {@link MongoObservationCommandListener}.
+ *
+ * @author Marcin Grzejszczak
+ * @author Greg Turnquist
+ * @since 4.0.0
+ */
+class MongoObservationCommandListenerTests {
+
+ ObservationRegistry observationRegistry;
+ MeterRegistry meterRegistry;
+
+ MongoObservationCommandListener listener;
+
+ @BeforeEach
+ void setup() {
+
+ this.meterRegistry = new SimpleMeterRegistry();
+ this.observationRegistry = ObservationRegistry.create();
+ this.observationRegistry.observationConfig().observationHandler(new TimerObservationHandler(meterRegistry));
+
+ this.listener = new MongoObservationCommandListener(observationRegistry);
+ }
+
+ @Test
+ void commandStartedShouldNotInstrumentWhenAdminDatabase() {
+
+ // when
+ listener.commandStarted(new CommandStartedEvent(null, 0, null, "admin", "", null));
+
+ // then
+ assertThat(meterRegistry).hasNoMetrics();
+ }
+
+ @Test
+ void commandStartedShouldNotInstrumentWhenNoRequestContext() {
+
+ // when
+ listener.commandStarted(new CommandStartedEvent(null, 0, null, "some name", "", null));
+
+ // then
+ assertThat(meterRegistry).hasNoMetrics();
+ }
+
+ @Test
+ void commandStartedShouldNotInstrumentWhenNoParentSampleInRequestContext() {
+
+ // when
+ listener.commandStarted(new CommandStartedEvent(new TestRequestContext(), 0, null, "some name", "", null));
+
+ // then
+ assertThat(meterRegistry).hasNoMetrics();
+ }
+
+ @Test
+ void successfullyCompletedCommandShouldCreateTimerWhenParentSampleInRequestContext() {
+
+ // given
+ Observation parent = Observation.start("name", observationRegistry);
+ TestRequestContext testRequestContext = TestRequestContext.withObservation(parent);
+
+ // when
+ listener.commandStarted(new CommandStartedEvent(testRequestContext, 0, //
+ new ConnectionDescription( //
+ new ServerId( //
+ new ClusterId("description"), //
+ new ServerAddress("localhost", 1234))),
+ "database", "insert", //
+ new BsonDocument("collection", new BsonString("user"))));
+ listener.commandSucceeded(new CommandSucceededEvent(testRequestContext, 0, null, "insert", null, 0));
+
+ // then
+ assertThatTimerRegisteredWithTags();
+ }
+
+ @Test
+ void successfullyCompletedCommandWithCollectionHavingCommandNameShouldCreateTimerWhenParentSampleInRequestContext() {
+
+ // given
+ Observation parent = Observation.start("name", observationRegistry);
+ TestRequestContext testRequestContext = TestRequestContext.withObservation(parent);
+
+ // when
+ listener.commandStarted(new CommandStartedEvent(testRequestContext, 0, //
+ new ConnectionDescription( //
+ new ServerId( //
+ new ClusterId("description"), //
+ new ServerAddress("localhost", 1234))), //
+ "database", "aggregate", //
+ new BsonDocument("aggregate", new BsonString("user"))));
+ listener.commandSucceeded(new CommandSucceededEvent(testRequestContext, 0, null, "aggregate", null, 0));
+
+ // then
+ assertThatTimerRegisteredWithTags();
+ }
+
+ @Test
+ void successfullyCompletedCommandWithoutClusterInformationShouldCreateTimerWhenParentSampleInRequestContext() {
+
+ // given
+ Observation parent = Observation.start("name", observationRegistry);
+ TestRequestContext testRequestContext = TestRequestContext.withObservation(parent);
+
+ // when
+ listener.commandStarted(new CommandStartedEvent(testRequestContext, 0, null, "database", "insert",
+ new BsonDocument("collection", new BsonString("user"))));
+ listener.commandSucceeded(new CommandSucceededEvent(testRequestContext, 0, null, "insert", null, 0));
+
+ // then
+ assertThat(meterRegistry).hasTimerWithNameAndTags(HighCardinalityCommandTags.MONGODB_COMMAND.getKey(),
+ Tags.of(LowCardinalityCommandTags.MONGODB_COLLECTION.of("user")));
+ }
+
+ @Test
+ void commandWithErrorShouldCreateTimerWhenParentSampleInRequestContext() {
+
+ // given
+ Observation parent = Observation.start("name", observationRegistry);
+ TestRequestContext testRequestContext = TestRequestContext.withObservation(parent);
+
+ // when
+ listener.commandStarted(new CommandStartedEvent(testRequestContext, 0, //
+ new ConnectionDescription( //
+ new ServerId( //
+ new ClusterId("description"), //
+ new ServerAddress("localhost", 1234))), //
+ "database", "insert", //
+ new BsonDocument("collection", new BsonString("user"))));
+ listener.commandFailed( //
+ new CommandFailedEvent(testRequestContext, 0, null, "insert", 0, new IllegalAccessException()));
+
+ // then
+ assertThatTimerRegisteredWithTags();
+ }
+
+ private void assertThatTimerRegisteredWithTags() {
+
+ assertThat(meterRegistry) //
+ .hasTimerWithNameAndTags(HighCardinalityCommandTags.MONGODB_COMMAND.getKey(),
+ Tags.of(LowCardinalityCommandTags.MONGODB_COLLECTION.getKey(), "user")) //
+ .hasTimerWithNameAndTagKeys(HighCardinalityCommandTags.MONGODB_COMMAND.getKey(),
+ LowCardinalityCommandTags.MONGODB_CLUSTER_ID.getKey());
+ }
+
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/TestRequestContext.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/TestRequestContext.java
new file mode 100644
index 000000000..6f82e5678
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/TestRequestContext.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2013-2022 the original author or authors.
+ *
+ * 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
+ *
+ * https://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 org.springframework.data.mongodb.observability;
+
+import io.micrometer.observation.Observation;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import com.mongodb.RequestContext;
+
+/**
+ * A {@link Map}-based {@link RequestContext}. (For test purposes only).
+ *
+ * @author Marcin Grzejszczak
+ * @author Greg Turnquist
+ * @since 4.0.0
+ */
+class TestRequestContext implements RequestContext {
+
+ private final Map