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 map = new HashMap<>(); + + @Override + public 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> stream() { + return map.entrySet().stream(); + } + + static TestRequestContext withObservation(Observation value) { + + TestRequestContext testRequestContext = new TestRequestContext(); + testRequestContext.put(Observation.class, value); + return testRequestContext; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ZipkinIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ZipkinIntegrationTests.java new file mode 100644 index 000000000..41b9174ec --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ZipkinIntegrationTests.java @@ -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> 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 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 repositoryFactoryBean(MongoOperations operations, + PropertiesFactoryBean namedQueriesProperties) throws IOException { + + MongoRepositoryFactoryBean 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; + } + } +} diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index 27e52a05c..684321ae2 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -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] diff --git a/src/main/asciidoc/reference/observability.adoc b/src/main/asciidoc/reference/observability.adoc new file mode 100644 index 000000000..f31372d35 --- /dev/null +++ b/src/main/asciidoc/reference/observability.adoc @@ -0,0 +1,8 @@ +:root-target: ../../../../target/ + +[[observability]] +== Observability metadata + +include::{root-target}_metrics.adoc[] + +include::{root-target}_spans.adoc[]