Browse Source

Introduce Observability with Micrometer and Micrometer Tracing.

See #3942.
pull/4015/head
Christoph Strobl 4 years ago committed by Greg L. Turnquist
parent
commit
4c77763cd3
No known key found for this signature in database
GPG Key ID: CB2FA4D512B5C413
  1. 3
      pom.xml
  2. 162
      spring-data-mongodb-distribution/pom.xml
  3. 748
      spring-data-mongodb/pom.xml
  4. 78
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerTagsProvider.java
  5. 134
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerContext.java
  6. 32
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerTagsProvider.java
  7. 97
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservation.java
  8. 179
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservationCommandListener.java
  9. 117
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoTracingObservationHandler.java
  10. 170
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerForTracingTests.java
  11. 186
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java
  12. 78
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/TestRequestContext.java
  13. 198
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ZipkinIntegrationTests.java
  14. 1
      src/main/asciidoc/index.adoc
  15. 8
      src/main/asciidoc/reference/observability.adoc

3
pom.xml

@ -137,6 +137,9 @@ @@ -137,6 +137,9 @@
<repository>
<id>spring-libs-snapshot</id>
<url>https://repo.spring.io/libs-snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>sonatype-libs-snapshot</id>

162
spring-data-mongodb-distribution/pom.xml

@ -1,46 +1,124 @@ @@ -1,46 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-data-mongodb-distribution</artifactId>
<packaging>pom</packaging>
<name>Spring Data MongoDB - Distribution</name>
<description>Distribution build for Spring Data MongoDB</description>
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<properties>
<project.root>${basedir}/..</project.root>
<dist.key>SDMONGO</dist.key>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<configuration>
<attributes>
<mongo-reactivestreams>${mongo.reactivestreams}</mongo-reactivestreams>
<reactor>${reactor}</reactor>
</attributes>
</configuration>
</plugin>
</plugins>
</build>
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-data-mongodb-distribution</artifactId>
<packaging>pom</packaging>
<name>Spring Data MongoDB - Distribution</name>
<description>Distribution build for Spring Data MongoDB</description>
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<properties>
<project.root>${basedir}/..</project.root>
<dist.key>SDMONGO</dist.key>
<!-- Observability -->
<micrometer-docs-generator.version>1.0.0-SNAPSHOT</micrometer-docs-generator.version>
<micrometer-docs-generator.inputPath>${maven.multiModuleProjectDirectory}/spring-data-mongodb/
</micrometer-docs-generator.inputPath>
<micrometer-docs-generator.inclusionPattern>.*</micrometer-docs-generator.inclusionPattern>
<micrometer-docs-generator.outputPath>${maven.multiModuleProjectDirectory}/target/
</micrometer-docs-generator.outputPath>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<executions>
<execution>
<id>generate-metrics-metadata</id>
<phase>prepare-package</phase>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>io.micrometer.docs.metrics.DocsFromSources</mainClass>
</configuration>
</execution>
<execution>
<id>generate-tracing-metadata</id>
<phase>prepare-package</phase>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>io.micrometer.docs.spans.DocsFromSources</mainClass>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-docs-generator-spans</artifactId>
<version>${micrometer-docs-generator.version}</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-docs-generator-metrics</artifactId>
<version>${micrometer-docs-generator.version}</version>
<type>jar</type>
</dependency>
</dependencies>
<configuration>
<includePluginDependencies>true</includePluginDependencies>
<arguments>
<argument>${micrometer-docs-generator.inputPath}</argument>
<argument>${micrometer-docs-generator.inclusionPattern}</argument>
<argument>${micrometer-docs-generator.outputPath}</argument>
</arguments>
</configuration>
</plugin>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<configuration>
<attributes>
<mongo-reactivestreams>${mongo.reactivestreams}</mongo-reactivestreams>
<reactor>${reactor}</reactor>
</attributes>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url> <!-- For Snapshots -->
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url> <!-- For Milestones -->
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>

748
spring-data-mongodb/pom.xml

@ -1,361 +1,391 @@ @@ -1,361 +1,391 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-data-mongodb</artifactId>
<name>Spring Data MongoDB - Core</name>
<description>MongoDB support for Spring Data</description>
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<properties>
<objenesis>1.3</objenesis>
<equalsverifier>1.7.8</equalsverifier>
<java-module-name>spring.data.mongodb</java-module-name>
<project.root>${basedir}/..</project.root>
<multithreadedtc>1.01</multithreadedtc>
</properties>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
</dependency>
<!-- Spring Data -->
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-data-commons</artifactId>
<version>${springdata.commons}</version>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-mongodb</artifactId>
<version>${querydsl}</version>
<optional>true</optional>
<exclusions>
<exclusion>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>jsr250-api</artifactId>
<version>1.0</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>3.0.2</version>
<optional>true</optional>
</dependency>
<!-- reactive -->
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId>
<version>${mongo}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-reactivestreams</artifactId>
<version>${mongo.reactivestreams}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.reactivex.rxjava3</groupId>
<artifactId>rxjava</artifactId>
<version>${rxjava3}</version>
<optional>true</optional>
</dependency>
<!-- CDI -->
<!-- Dependency order required to build against CDI 1.0 and test with CDI 2.0 -->
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
<version>1.2.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.enterprise</groupId>
<artifactId>jakarta.enterprise.cdi-api</artifactId>
<version>${cdi}</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>${jakarta-annotation-api}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.openwebbeans</groupId>
<artifactId>openwebbeans-se</artifactId>
<classifier>jakarta</classifier>
<version>${webbeans}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.openwebbeans</groupId>
<artifactId>openwebbeans-spi</artifactId>
<classifier>jakarta</classifier>
<version>${webbeans}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.openwebbeans</groupId>
<artifactId>openwebbeans-impl</artifactId>
<classifier>jakarta</classifier>
<version>${webbeans}</version>
<scope>test</scope>
</dependency>
<!-- JSR 303 Validation -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>${validation}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId>
<version>${objenesis}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>7.0.1.Final</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.el</groupId>
<artifactId>jakarta.el-api</artifactId>
<version>4.0.0</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>
<version>4.0.2</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>nl.jqno.equalsverifier</groupId>
<artifactId>equalsverifier</artifactId>
<version>${equalsverifier}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>de.schauderhaft.degraph</groupId>
<artifactId>degraph-check</artifactId>
<version>0.1.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>edu.umd.cs.mtc</groupId>
<artifactId>multithreadedtc</artifactId>
<version>${multithreadedtc}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit-pioneer</groupId>
<artifactId>junit-pioneer</artifactId>
<version>0.5.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.transaction</groupId>
<artifactId>jakarta.transaction-api</artifactId>
<version>2.0.0</version>
<scope>test</scope>
</dependency>
<!-- Kotlin extension -->
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-reactor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk</artifactId>
<version>${mockk}</version>
<scope>test</scope>
</dependency>
<!-- jMolecules -->
<dependency>
<groupId>org.jmolecules</groupId>
<artifactId>jmolecules-ddd</artifactId>
<version>${jmolecules}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>${apt}</version>
<dependencies>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl}</version>
</dependency>
</dependencies>
<executions>
<execution>
<phase>generate-test-sources</phase>
<goals>
<goal>test-process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-test-sources</outputDirectory>
<processor>org.springframework.data.mongodb.repository.support.MongoAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<useSystemClassLoader>false</useSystemClassLoader>
<useFile>false</useFile>
<includes>
<include>**/*Tests.java</include>
</includes>
<excludes>
<exclude>**/PerformanceTests.java</exclude>
<exclude>**/ReactivePerformanceTests.java</exclude>
</excludes>
<systemPropertyVariables>
<java.util.logging.config.file>src/test/resources/logging.properties</java.util.logging.config.file>
<reactor.trace.cancel>true</reactor.trace.cancel>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-data-mongodb</artifactId>
<name>Spring Data MongoDB - Core</name>
<description>MongoDB support for Spring Data</description>
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<properties>
<objenesis>1.3</objenesis>
<equalsverifier>1.7.8</equalsverifier>
<java-module-name>spring.data.mongodb</java-module-name>
<project.root>${basedir}/..</project.root>
<multithreadedtc>1.01</multithreadedtc>
</properties>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
</dependency>
<!-- Spring Data -->
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-data-commons</artifactId>
<version>${springdata.commons}</version>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-mongodb</artifactId>
<version>${querydsl}</version>
<optional>true</optional>
<exclusions>
<exclusion>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>jsr250-api</artifactId>
<version>1.0</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>3.0.2</version>
<optional>true</optional>
</dependency>
<!-- reactive -->
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId>
<version>${mongo}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-reactivestreams</artifactId>
<version>${mongo.reactivestreams}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.reactivex.rxjava3</groupId>
<artifactId>rxjava</artifactId>
<version>${rxjava3}</version>
<optional>true</optional>
</dependency>
<!-- CDI -->
<!-- Dependency order required to build against CDI 1.0 and test with CDI 2.0 -->
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
<version>1.2.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.enterprise</groupId>
<artifactId>jakarta.enterprise.cdi-api</artifactId>
<version>${cdi}</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>${jakarta-annotation-api}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.openwebbeans</groupId>
<artifactId>openwebbeans-se</artifactId>
<classifier>jakarta</classifier>
<version>${webbeans}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.openwebbeans</groupId>
<artifactId>openwebbeans-spi</artifactId>
<classifier>jakarta</classifier>
<version>${webbeans}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.openwebbeans</groupId>
<artifactId>openwebbeans-impl</artifactId>
<classifier>jakarta</classifier>
<version>${webbeans}</version>
<scope>test</scope>
</dependency>
<!-- JSR 303 Validation -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>${validation}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId>
<version>${objenesis}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-api</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>7.0.1.Final</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.el</groupId>
<artifactId>jakarta.el-api</artifactId>
<version>4.0.0</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>
<version>4.0.2</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>nl.jqno.equalsverifier</groupId>
<artifactId>equalsverifier</artifactId>
<version>${equalsverifier}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>de.schauderhaft.degraph</groupId>
<artifactId>degraph-check</artifactId>
<version>0.1.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>edu.umd.cs.mtc</groupId>
<artifactId>multithreadedtc</artifactId>
<version>${multithreadedtc}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit-pioneer</groupId>
<artifactId>junit-pioneer</artifactId>
<version>0.5.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.transaction</groupId>
<artifactId>jakarta.transaction-api</artifactId>
<version>2.0.0</version>
<scope>test</scope>
</dependency>
<!-- Kotlin extension -->
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-reactor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk</artifactId>
<version>${mockk}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-integration-test</artifactId>
<scope>test</scope>
</dependency>
<!-- jMolecules -->
<dependency>
<groupId>org.jmolecules</groupId>
<artifactId>jmolecules-ddd</artifactId>
<version>${jmolecules}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>${apt}</version>
<dependencies>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl}</version>
</dependency>
</dependencies>
<executions>
<execution>
<phase>generate-test-sources</phase>
<goals>
<goal>test-process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-test-sources</outputDirectory>
<processor>org.springframework.data.mongodb.repository.support.MongoAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<useSystemClassLoader>false</useSystemClassLoader>
<useFile>false</useFile>
<includes>
<include>**/*Tests.java</include>
</includes>
<excludes>
<exclude>**/PerformanceTests.java</exclude>
<exclude>**/ReactivePerformanceTests.java</exclude>
</excludes>
<systemPropertyVariables>
<java.util.logging.config.file>src/test/resources/logging.properties</java.util.logging.config.file>
<reactor.trace.cancel>true</reactor.trace.cancel>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</project>

78
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerTagsProvider.java

@ -0,0 +1,78 @@ @@ -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;
}
}

134
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerContext.java

@ -0,0 +1,134 @@ @@ -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<String> 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;
}
}

32
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerTagsProvider.java

@ -0,0 +1,32 @@ @@ -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<MongoHandlerContext> {
@Override
default boolean supportsContext(Observation.Context context) {
return context instanceof MongoHandlerContext;
}
}

97
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservation.java

@ -0,0 +1,97 @@ @@ -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";
}
}
}
}

179
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservationCommandListener.java

@ -0,0 +1,179 @@ @@ -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<MongoHandlerTagsProvider> {
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;
}
}

117
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoTracingObservationHandler.java

@ -0,0 +1,117 @@ @@ -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<MongoHandlerContext> {
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;
}
}

170
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerForTracingTests.java

@ -0,0 +1,170 @@ @@ -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());
}
}

186
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java

@ -0,0 +1,186 @@ @@ -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());
}
}

78
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/TestRequestContext.java

@ -0,0 +1,78 @@ @@ -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<Object, Object> map = new HashMap<>();
@Override
public <T> T get(Object key) {
return (T) map.get(key);
}
@Override
public boolean hasKey(Object key) {
return map.containsKey(key);
}
@Override
public boolean isEmpty() {
return map.isEmpty();
}
@Override
public void put(Object key, Object value) {
map.put(key, value);
}
@Override
public void delete(Object key) {
map.remove(key);
}
@Override
public int size() {
return map.size();
}
@Override
public Stream<Map.Entry<Object, Object>> stream() {
return map.entrySet().stream();
}
static TestRequestContext withObservation(Observation value) {
TestRequestContext testRequestContext = new TestRequestContext();
testRequestContext.put(Observation.class, value);
return testRequestContext;
}
}

198
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ZipkinIntegrationTests.java

@ -0,0 +1,198 @@ @@ -0,0 +1,198 @@
/*
* 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 static org.springframework.data.mongodb.test.util.Assertions.*;
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.ObservationHandler;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.tracing.test.SampleTestRunner;
import io.micrometer.tracing.test.reporter.BuildingBlocks;
import java.io.IOException;
import java.util.Deque;
import java.util.List;
import java.util.function.BiConsumer;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.repository.Person;
import org.springframework.data.mongodb.repository.PersonRepository;
import org.springframework.data.mongodb.repository.SampleEvaluationContextExtension;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import org.springframework.data.mongodb.repository.support.MongoRepositoryFactoryBean;
import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.RequestContext;
import com.mongodb.WriteConcern;
import com.mongodb.client.MongoClients;
import com.mongodb.client.SynchronousContextProvider;
/**
* Collection of tests that log metrics and tracing with an external tracing tool. Since this external tool must be up
* and running after the test is completed, this test is ONLY run manually. Needed:
* {@code docker run -p 9411:9411 openzipkin/zipkin} and {@code docker run -p 27017:27017 mongo:latest} (either from
* Docker Desktop or within separate shells).
*
* @author Greg Turnquist
* @since 4.0.0
*/
@Disabled("Run this manually to visually test spans in Zipkin")
@ExtendWith(SpringExtension.class)
@ContextConfiguration
public class ZipkinIntegrationTests extends SampleTestRunner {
private static final MeterRegistry METER_REGISTRY = new SimpleMeterRegistry();
private static final ObservationRegistry OBSERVATION_REGISTRY = ObservationRegistry.create();
static {
OBSERVATION_REGISTRY.observationConfig().observationHandler(new TimerObservationHandler(METER_REGISTRY));
}
@Autowired PersonRepository repository;
ZipkinIntegrationTests() {
super(SampleRunnerConfig.builder().build(), OBSERVATION_REGISTRY, METER_REGISTRY);
}
@Override
public BiConsumer<BuildingBlocks, Deque<ObservationHandler>> customizeObservationHandlers() {
return (buildingBlocks, observationHandlers) -> {
observationHandlers.addLast(new MongoTracingObservationHandler(buildingBlocks.getTracer()));
};
}
@Override
public TracingSetup[] getTracingSetup() {
return new TracingSetup[] { TracingSetup.ZIPKIN_BRAVE };
}
@Override
public SampleTestRunnerConsumer yourCode() {
return (tracer, meterRegistry) -> {
repository.deleteAll();
repository.save(new Person("Dave", "Matthews", 42));
List<Person> people = repository.findByLastname("Matthews");
assertThat(people).hasSize(1);
assertThat(people.get(0)).extracting("firstname", "lastname").containsExactly("Dave", "Matthews");
repository.deleteAll();
System.out.println(((SimpleMeterRegistry) meterRegistry).getMetersAsString());
};
}
@Configuration
@EnableMongoRepositories
static class TestConfig {
@Bean
MongoObservationCommandListener mongoObservationCommandListener(ObservationRegistry registry) {
return new MongoObservationCommandListener(registry);
}
@Bean
MongoDatabaseFactory mongoDatabaseFactory(MongoObservationCommandListener commandListener,
ObservationRegistry registry) {
ConnectionString connectionString = new ConnectionString(
String.format("mongodb://%s:%s/?w=majority&uuidrepresentation=javaLegacy", "127.0.0.1", 27017));
RequestContext requestContext = TestRequestContext.withObservation(Observation.start("name", registry));
SynchronousContextProvider contextProvider = () -> requestContext;
MongoClientSettings settings = MongoClientSettings.builder() //
.addCommandListener(commandListener) //
.contextProvider(contextProvider) //
.applyConnectionString(connectionString) //
.build();
return new SimpleMongoClientDatabaseFactory(MongoClients.create(settings), "observable");
}
@Bean
MappingMongoConverter mongoConverter(MongoDatabaseFactory factory) {
MongoMappingContext mappingContext = new MongoMappingContext();
mappingContext.afterPropertiesSet();
return new MappingMongoConverter(new DefaultDbRefResolver(factory), mappingContext);
}
@Bean
MongoTemplate mongoTemplate(MongoDatabaseFactory mongoDatabaseFactory, MongoConverter mongoConverter) {
MongoTemplate template = new MongoTemplate(mongoDatabaseFactory, mongoConverter);
template.setWriteConcern(WriteConcern.JOURNALED);
return template;
}
@Bean
public PropertiesFactoryBean namedQueriesProperties() {
PropertiesFactoryBean bean = new PropertiesFactoryBean();
bean.setLocation(new ClassPathResource("META-INF/mongo-named-queries.properties"));
return bean;
}
@Bean
MongoRepositoryFactoryBean<PersonRepository, Person, String> repositoryFactoryBean(MongoOperations operations,
PropertiesFactoryBean namedQueriesProperties) throws IOException {
MongoRepositoryFactoryBean<PersonRepository, Person, String> factoryBean = new MongoRepositoryFactoryBean<>(
PersonRepository.class);
factoryBean.setMongoOperations(operations);
factoryBean.setNamedQueries(new PropertiesBasedNamedQueries(namedQueriesProperties.getObject()));
factoryBean.setCreateIndexesForQueryMethods(true);
return factoryBean;
}
@Bean
SampleEvaluationContextExtension contextExtension() {
return new SampleEvaluationContextExtension();
}
@Bean
ObservationRegistry registry() {
return OBSERVATION_REGISTRY;
}
}
}

1
src/main/asciidoc/index.adoc

@ -42,3 +42,4 @@ include::{spring-data-commons-docs}/repository-namespace-reference.adoc[leveloff @@ -42,3 +42,4 @@ include::{spring-data-commons-docs}/repository-namespace-reference.adoc[leveloff
include::{spring-data-commons-docs}/repository-populator-namespace-reference.adoc[leveloffset=+1]
include::{spring-data-commons-docs}/repository-query-keywords-reference.adoc[leveloffset=+1]
include::{spring-data-commons-docs}/repository-query-return-types-reference.adoc[leveloffset=+1]
include::reference/observability.adoc[leveloffset=+1]

8
src/main/asciidoc/reference/observability.adoc

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
:root-target: ../../../../target/
[[observability]]
== Observability metadata
include::{root-target}_metrics.adoc[]
include::{root-target}_spans.adoc[]
Loading…
Cancel
Save