diff --git a/pom.xml b/pom.xml index ed0dce4796..321477a286 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 4.0.0-SNAPSHOT + 4.0.0-mongo-micrometer-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index c28a240d2c..b61040e965 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 4.0.0-SNAPSHOT + 4.0.0-mongo-micrometer-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 0412911f82..7d91980c2c 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 4.0.0-SNAPSHOT + 4.0.0-mongo-micrometer-SNAPSHOT ../pom.xml @@ -41,7 +41,7 @@ generate-metrics-metadata - prepare-package + generate-resources java @@ -52,7 +52,7 @@ generate-tracing-metadata - prepare-package + generate-resources java diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 1f8c5b28a8..9553be436c 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 4.0.0-SNAPSHOT + 4.0.0-mongo-micrometer-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/ContextProviderFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/ContextProviderFactory.java new file mode 100644 index 0000000000..2357b334a2 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/ContextProviderFactory.java @@ -0,0 +1,139 @@ +/* + * Copyright 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 java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +import org.reactivestreams.Subscriber; +import org.springframework.data.repository.util.ReactiveWrappers; +import org.springframework.data.repository.util.ReactiveWrappers.ReactiveLibrary; +import org.springframework.util.ClassUtils; + +import com.mongodb.ContextProvider; +import com.mongodb.RequestContext; +import com.mongodb.client.SynchronousContextProvider; +import com.mongodb.reactivestreams.client.ReactiveContextProvider; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; +import reactor.core.CoreSubscriber; + +/** + * Factory to create a {@link ContextProvider} to propagate the request context across tasks. Requires either + * {@link SynchronousContextProvider} or {@link ReactiveContextProvider} to be present. + * + * @author Mark Paluch + * @since 3.0 + */ +public class ContextProviderFactory { + + private static final boolean SYNCHRONOUS_PRESENT = ClassUtils + .isPresent("com.mongodb.client.SynchronousContextProvider", ContextProviderFactory.class.getClassLoader()); + + private static final boolean REACTIVE_PRESENT = ClassUtils.isPresent( + "com.mongodb.reactivestreams.client.ReactiveContextProvider", ContextProviderFactory.class.getClassLoader()) + && ReactiveWrappers.isAvailable(ReactiveLibrary.PROJECT_REACTOR); + + /** + * Create a {@link ContextProvider} given {@link ObservationRegistry}. The factory method attempts to create a + * {@link ContextProvider} that is capable to propagate request contexts across imperative or reactive usage, + * depending on their class path presence. + * + * @param observationRegistry must not be {@literal null}. + * @return + */ + public static ContextProvider create(ObservationRegistry observationRegistry) { + + if (SYNCHRONOUS_PRESENT && REACTIVE_PRESENT) { + return new CompositeContextProvider(observationRegistry); + } + + if (SYNCHRONOUS_PRESENT) { + return new DefaultSynchronousContextProvider(observationRegistry); + } + + if (REACTIVE_PRESENT) { + return DefaultReactiveContextProvider.INSTANCE; + } + + throw new IllegalStateException( + "Cannot create ContextProvider. Neither SynchronousContextProvider nor ReactiveContextProvider is on the class path."); + } + + record DefaultSynchronousContextProvider( + ObservationRegistry observationRegistry) implements SynchronousContextProvider { + + @Override + public RequestContext getContext() { + + MapRequestContext requestContext = new MapRequestContext(); + + Observation currentObservation = observationRegistry.getCurrentObservation(); + if (currentObservation != null) { + requestContext.put(Observation.class, currentObservation); + } + + return requestContext; + } + + } + + enum DefaultReactiveContextProvider implements ReactiveContextProvider { + + INSTANCE; + + @Override + public RequestContext getContext(Subscriber subscriber) { + + if (subscriber instanceof CoreSubscriber cs) { + + Map map = cs.currentContext().stream() + .collect(Collectors.toConcurrentMap(Entry::getKey, Entry::getValue)); + if (map.containsKey(ObservationThreadLocalAccessor.KEY)) { + map.put(Observation.class, map.get(ObservationThreadLocalAccessor.KEY)); + } + + return new MapRequestContext(map); + } + + return new MapRequestContext(); + } + } + + record CompositeContextProvider(DefaultSynchronousContextProvider synchronousContextProvider) + implements + SynchronousContextProvider, + ReactiveContextProvider { + + CompositeContextProvider(ObservationRegistry observationRegistry) { + this(new DefaultSynchronousContextProvider(observationRegistry)); + } + + @Override + public RequestContext getContext() { + return synchronousContextProvider.getContext(); + } + + @Override + public RequestContext getContext(Subscriber subscriber) { + return DefaultReactiveContextProvider.INSTANCE.getContext(subscriber); + } + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java index 69b4152157..45dcc3db95 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java @@ -15,34 +15,75 @@ */ package org.springframework.data.mongodb.observability; -import io.micrometer.common.KeyValue; -import io.micrometer.common.KeyValues; +import java.net.InetSocketAddress; import org.springframework.data.mongodb.observability.MongoObservation.HighCardinalityCommandKeyNames; import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; +import com.mongodb.ConnectionString; +import com.mongodb.ServerAddress; import com.mongodb.connection.ConnectionDescription; import com.mongodb.connection.ConnectionId; import com.mongodb.event.CommandStartedEvent; +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; + /** * Default {@link MongoHandlerObservationConvention} implementation. * * @author Greg Turnquist - * @since 4.0.0 + * @since 4 */ -public class DefaultMongoHandlerObservationConvention implements MongoHandlerObservationConvention { +class DefaultMongoHandlerObservationConvention implements MongoHandlerObservationConvention { @Override public KeyValues getLowCardinalityKeyValues(MongoHandlerContext context) { - KeyValues keyValues = KeyValues.empty(); + KeyValues keyValues = KeyValues.of(LowCardinalityCommandKeyNames.DB_SYSTEM.withValue("mongodb")); + + ConnectionString connectionString = context.getConnectionString(); + if (connectionString != null) { - if (context.getCollectionName() != null) { + keyValues = keyValues + .and(LowCardinalityCommandKeyNames.DB_CONNECTION_STRING.withValue(connectionString.getConnectionString())); + + String user = connectionString.getUsername(); + + if (!ObjectUtils.isEmpty(user)) { + keyValues = keyValues.and(LowCardinalityCommandKeyNames.DB_USER.withValue(user)); + } + + } + + if (!ObjectUtils.isEmpty(context.getDatabaseName())) { + keyValues = keyValues.and(LowCardinalityCommandKeyNames.DB_NAME.withValue(context.getDatabaseName())); + } + + if (!ObjectUtils.isEmpty(context.getCollectionName())) { keyValues = keyValues .and(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue(context.getCollectionName())); } + ServerAddress serverAddress = context.getCommandStartedEvent().getConnectionDescription().getServerAddress(); + + if (serverAddress != null) { + + keyValues = keyValues.and(LowCardinalityCommandKeyNames.NET_TRANSPORT.withValue("IP.TCP"), + LowCardinalityCommandKeyNames.NET_PEER_ADDR.withValue(serverAddress.getHost())); + + InetSocketAddress socketAddress = serverAddress.getSocketAddress(); + + if (socketAddress != null) { + + keyValues = keyValues.and(LowCardinalityCommandKeyNames.NET_PEER_NAME.withValue(socketAddress.getHostName()), + LowCardinalityCommandKeyNames.NET_PEER_PORT.withValue("" + socketAddress.getPort())); + } + + } + KeyValue connectionTag = connectionTag(context.getCommandStartedEvent()); if (connectionTag != null) { keyValues = keyValues.and(connectionTag); @@ -54,8 +95,12 @@ public KeyValues getLowCardinalityKeyValues(MongoHandlerContext context) { @Override public KeyValues getHighCardinalityKeyValues(MongoHandlerContext context) { - return KeyValues.of( - HighCardinalityCommandKeyNames.MONGODB_COMMAND.withValue(context.getCommandStartedEvent().getCommandName())); + return KeyValues.of(HighCardinalityCommandKeyNames.MONGODB_COMMAND.withValue(context.getCommandName())); + } + + @Override + public String getContextualName(MongoHandlerContext context) { + return context.getContextualName(); } /** @@ -64,6 +109,7 @@ public KeyValues getHighCardinalityKeyValues(MongoHandlerContext context) { * @param event * @return */ + @Nullable private static KeyValue connectionTag(CommandStartedEvent event) { ConnectionDescription connectionDescription = event.getConnectionDescription(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/TestRequestContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MapRequestContext.java similarity index 76% rename from spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/TestRequestContext.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MapRequestContext.java index 6f82e5678f..aa12ca1ddb 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/TestRequestContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MapRequestContext.java @@ -15,8 +15,6 @@ */ package org.springframework.data.mongodb.observability; -import io.micrometer.observation.Observation; - import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; @@ -24,15 +22,23 @@ import com.mongodb.RequestContext; /** - * A {@link Map}-based {@link RequestContext}. (For test purposes only). + * A {@link Map}-based {@link RequestContext}. * * @author Marcin Grzejszczak * @author Greg Turnquist * @since 4.0.0 */ -class TestRequestContext implements RequestContext { +class MapRequestContext implements RequestContext { + + private final Map map; + + public MapRequestContext() { + this(new HashMap<>()); + } - private final Map map = new HashMap<>(); + public MapRequestContext(Map context) { + this.map = context; + } @Override public T get(Object key) { @@ -68,11 +74,4 @@ public int size() { 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/main/java/org/springframework/data/mongodb/observability/MongoHandlerContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerContext.java index 7fece69fe5..760c02cab9 100644 --- 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 @@ -15,8 +15,6 @@ */ package org.springframework.data.mongodb.observability; -import io.micrometer.observation.Observation; - import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Set; @@ -25,28 +23,37 @@ import org.bson.BsonValue; import org.springframework.lang.Nullable; +import com.mongodb.ConnectionString; import com.mongodb.RequestContext; import com.mongodb.event.CommandFailedEvent; import com.mongodb.event.CommandStartedEvent; import com.mongodb.event.CommandSucceededEvent; +import io.micrometer.observation.Observation; +import io.micrometer.observation.transport.Kind; +import io.micrometer.observation.transport.SenderContext; + /** * A {@link Observation.Context} that contains MongoDB events. * * @author Marcin Grzejszczak * @author Greg Turnquist - * @since 4.0.0 + * @author Mark Paluch + * @since 4.0 */ -public class MongoHandlerContext extends Observation.Context { +class MongoHandlerContext extends SenderContext { /** - * @see https://docs.mongodb.com/manual/reference/command for the command reference + * @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 @Nullable ConnectionString connectionString; private final CommandStartedEvent commandStartedEvent; private final RequestContext requestContext; private final String collectionName; @@ -54,8 +61,11 @@ public class MongoHandlerContext extends Observation.Context { private CommandSucceededEvent commandSucceededEvent; private CommandFailedEvent commandFailedEvent; - public MongoHandlerContext(CommandStartedEvent commandStartedEvent, RequestContext requestContext) { + public MongoHandlerContext(@Nullable ConnectionString connectionString, CommandStartedEvent commandStartedEvent, + RequestContext requestContext) { + super((carrier, key, value) -> {}, Kind.CLIENT); + this.connectionString = connectionString; this.commandStartedEvent = commandStartedEvent; this.requestContext = requestContext; this.collectionName = getCollectionName(commandStartedEvent); @@ -69,17 +79,30 @@ public RequestContext getRequestContext() { return this.requestContext; } + public String getDatabaseName() { + return commandStartedEvent.getDatabaseName(); + } + public String getCollectionName() { return this.collectionName; } + public String getCommandName() { + return commandStartedEvent.getCommandName(); + } + + @Nullable + public ConnectionString getConnectionString() { + return connectionString; + } + public String getContextualName() { if (this.collectionName == null) { return this.commandStartedEvent.getCommandName(); } - return this.commandStartedEvent.getCommandName() + " " + this.collectionName; + return this.collectionName + "." + this.commandStartedEvent.getCommandName(); } public void setCommandSucceededEvent(CommandSucceededEvent commandSucceededEvent) { @@ -131,4 +154,5 @@ private static String getNonEmptyBsonString(BsonValue bsonValue) { return stringValue.isEmpty() ? null : stringValue; } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerObservationConvention.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerObservationConvention.java index e7b5a8e1be..f8ee860cc4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerObservationConvention.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerObservationConvention.java @@ -22,7 +22,7 @@ * {@link ObservationConvention} for {@link MongoHandlerContext}. * * @author Greg Turnquist - * @since 4.0.0 + * @since 4 */ public interface MongoHandlerObservationConvention extends ObservationConvention { 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 index 298dfd8d04..2389d6e895 100644 --- 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 @@ -47,10 +47,6 @@ public KeyName[] getHighCardinalityKeyNames() { return HighCardinalityCommandKeyNames.values(); } - @Override - public String getPrefix() { - return "spring.data.mongodb"; - } }; /** @@ -58,13 +54,93 @@ public String getPrefix() { */ enum LowCardinalityCommandKeyNames implements KeyName { + /** + * MongoDB database system. + */ + DB_SYSTEM { + @Override + public String asString() { + return "db.system"; + } + }, + + /** + * MongoDB connection string. + */ + DB_CONNECTION_STRING { + @Override + public String asString() { + return "db.connection_string"; + } + }, + + /** + * Network transport. + */ + NET_TRANSPORT { + @Override + public String asString() { + return "net.transport"; + } + }, + + /** + * Name of the database host. + */ + NET_PEER_NAME { + @Override + public String asString() { + return "net.peer.name"; + } + }, + + /** + * Logical remote port number. + */ + NET_PEER_PORT { + @Override + public String asString() { + return "net.peer.port"; + } + }, + + /** + * Redis peer address. + */ + NET_PEER_ADDR { + @Override + public String asString() { + return "net.sock.peer.addr"; + } + }, + + /** + * MongoDB user. + */ + DB_USER { + @Override + public String asString() { + return "db.user"; + } + }, + + /** + * MongoDB database name. + */ + DB_NAME { + @Override + public String asString() { + return "db.name"; + } + }, + /** * MongoDB collection name. */ MONGODB_COLLECTION { @Override public String asString() { - return "spring.data.mongodb.collection"; + return "db.mongodb.collection"; } }, @@ -90,7 +166,7 @@ enum HighCardinalityCommandKeyNames implements KeyName { MONGODB_COMMAND { @Override public String asString() { - return "spring.data.mongodb.command"; + return "db.operation"; } } } 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 index 34d921efc9..2dcdca6e05 100644 --- 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 @@ -15,39 +15,45 @@ */ 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 org.springframework.lang.Nullable; +import com.mongodb.ConnectionString; import com.mongodb.RequestContext; import com.mongodb.event.CommandFailedEvent; import com.mongodb.event.CommandListener; import com.mongodb.event.CommandStartedEvent; import com.mongodb.event.CommandSucceededEvent; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; + /** * 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 + * @since 4.0 */ -public final class MongoObservationCommandListener implements CommandListener { +public class MongoObservationCommandListener implements CommandListener { private static final Log log = LogFactory.getLog(MongoObservationCommandListener.class); private final ObservationRegistry observationRegistry; + private final @Nullable ConnectionString connectionString; - private MongoHandlerObservationConvention observationConvention; + private final MongoHandlerObservationConvention observationConvention = new DefaultMongoHandlerObservationConvention(); public MongoObservationCommandListener(ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + this.connectionString = null; + } + public MongoObservationCommandListener(ObservationRegistry observationRegistry, ConnectionString connectionString) { this.observationRegistry = observationRegistry; - this.observationConvention = new DefaultMongoHandlerObservationConvention(); + this.connectionString = connectionString; } @Override @@ -75,11 +81,26 @@ public void commandStarted(CommandStartedEvent event) { log.debug("Found the following observation passed from the mongo context [" + parent + "]"); } - if (parent == null) { - return; + MongoHandlerContext observationContext = new MongoHandlerContext(connectionString, event, requestContext); + observationContext.setRemoteServiceName("mongo"); + + Observation observation = MongoObservation.MONGODB_COMMAND_OBSERVATION + .observation(this.observationRegistry, () -> observationContext) // + .observationConvention(this.observationConvention); + + if (parent != null) { + observation.parentObservation(parent); } - setupObservability(event, requestContext); + observation.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 @@ -133,6 +154,7 @@ public void commandFailed(CommandFailedEvent event) { * @param context * @return */ + @Nullable private static Observation observationFromContext(RequestContext context) { Observation observation = context.getOrDefault(Observation.class, null); @@ -140,7 +162,7 @@ private static Observation observationFromContext(RequestContext context) { if (observation != null) { if (log.isDebugEnabled()) { - log.debug("Found a observation in mongo context [" + observation + "]"); + log.debug("Found a observation in Mongo context [" + observation + "]"); } return observation; } @@ -151,23 +173,4 @@ private static Observation observationFromContext(RequestContext context) { 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()) // - .observationConvention(this.observationConvention) // - .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"); - } - } } 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 deleted file mode 100644 index aae9d96241..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoTracingObservationHandler.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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/main/java/org/springframework/data/mongodb/observability/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/package-info.java new file mode 100644 index 0000000000..d240e12f9e --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/package-info.java @@ -0,0 +1,5 @@ +/** + * Infrastructure to provide driver observability using Micrometer. + */ +@org.springframework.lang.NonNullApi +package org.springframework.data.mongodb.observability; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java new file mode 100644 index 0000000000..b0335e74a7 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java @@ -0,0 +1,77 @@ +/* + * 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 java.util.List; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mongodb.repository.Person; +import org.springframework.data.mongodb.repository.PersonRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.test.SampleTestRunner; + +/** + * Collection of tests that log metrics and tracing with an external tracing tool. + * + * @author Greg Turnquist + * @author Mark Paluch + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = TestConfig.class) +public class ImperativeIntegrationTests extends SampleTestRunner { + + @Autowired PersonRepository repository; + + ImperativeIntegrationTests() { + super(SampleRunnerConfig.builder().build()); + } + + @Override + protected MeterRegistry createMeterRegistry() { + return TestConfig.METER_REGISTRY; + } + + @Override + protected ObservationRegistry createObservationRegistry() { + return TestConfig.OBSERVATION_REGISTRY; + } + + @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()); + }; + } +} 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 index 1705281fd0..3b41fe80c4 100644 --- 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 @@ -15,16 +15,6 @@ */ package org.springframework.data.mongodb.observability; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; -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; @@ -33,7 +23,9 @@ import org.springframework.data.mongodb.observability.MongoObservation.HighCardinalityCommandKeyNames; import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames; +import com.mongodb.RequestContext; import com.mongodb.ServerAddress; +import com.mongodb.client.SynchronousContextProvider; import com.mongodb.connection.ClusterId; import com.mongodb.connection.ConnectionDescription; import com.mongodb.connection.ServerId; @@ -41,19 +33,26 @@ import com.mongodb.event.CommandStartedEvent; import com.mongodb.event.CommandSucceededEvent; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; +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; + /** * 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; @@ -63,12 +62,10 @@ class MongoObservationCommandListenerForTracingTests { void setup() { this.simpleTracer = new SimpleTracer(); - this.handler = new MongoTracingObservationHandler(simpleTracer); this.meterRegistry = new SimpleMeterRegistry(); this.observationRegistry = ObservationRegistry.create(); this.observationRegistry.observationConfig().observationHandler(new DefaultMeterObservationHandler(meterRegistry)); - this.observationRegistry.observationConfig().observationHandler(handler); this.listener = new MongoObservationCommandListener(observationRegistry); } @@ -77,37 +74,24 @@ void setup() { void successfullyCompletedCommandShouldCreateSpanWhenParentSampleInRequestContext() { // given - TestRequestContext testRequestContext = createTestRequestContextWithParentObservationAndStartIt(); + RequestContext traceRequestContext = createTestRequestContextWithParentObservationAndStartIt(); // when - commandStartedAndSucceeded(testRequestContext); + commandStartedAndSucceeded(traceRequestContext); // 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(); + RequestContext traceRequestContext = createTestRequestContextWithParentObservationAndStartIt(); // when - listener.commandStarted(new CommandStartedEvent(testRequestContext, 0, // + listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, // new ConnectionDescription( // new ServerId( // new ClusterId("description"), // @@ -115,32 +99,30 @@ void commandWithErrorShouldCreateTimerWhenParentSampleInRequestContext() { "database", "insert", // new BsonDocument("collection", new BsonString("user")))); listener.commandFailed( // - new CommandFailedEvent(testRequestContext, 0, null, "insert", 0, new IllegalAccessException())); + new CommandFailedEvent(traceRequestContext, 0, null, "insert", 0, new IllegalAccessException())); // then assertThatMongoSpanIsClientWithTags().assertThatThrowable().isInstanceOf(IllegalAccessException.class); } /** - * Create a parent {@link Observation} then wrap it inside a {@link TestRequestContext}. + * Create a parent {@link Observation} then wrap it inside a {@link MapRequestContext}. */ @NotNull - private TestRequestContext createTestRequestContextWithParentObservationAndStartIt() { - - Observation parent = Observation.start("name", observationRegistry); - return TestRequestContext.withObservation(parent); + private RequestContext createTestRequestContextWithParentObservationAndStartIt() { + return ((SynchronousContextProvider) ContextProviderFactory.create(observationRegistry)).getContext(); } /** * 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. + * {@link MapRequestContext} in order to inject some test data. * - * @param testRequestContext + * @param traceRequestContext */ - private void commandStartedAndSucceeded(TestRequestContext testRequestContext) { + private void commandStartedAndSucceeded(RequestContext traceRequestContext) { - listener.commandStarted(new CommandStartedEvent(testRequestContext, 0, // + listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, // new ConnectionDescription( // new ServerId( // new ClusterId("description"), // @@ -148,7 +130,7 @@ private void commandStartedAndSucceeded(TestRequestContext testRequestContext) { "database", "insert", // new BsonDocument("collection", new BsonString("user")))); - listener.commandSucceeded(new CommandSucceededEvent(testRequestContext, 0, null, "insert", null, 0)); + listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0)); } /** 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 index ab47f1ca36..09818da7d2 100644 --- 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 @@ -15,14 +15,7 @@ */ package org.springframework.data.mongodb.observability; -import static io.micrometer.core.tck.MeterRegistryAssert.assertThat; - -import io.micrometer.common.KeyValues; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import io.micrometer.observation.Observation; -import io.micrometer.observation.ObservationRegistry; +import static io.micrometer.core.tck.MeterRegistryAssert.*; import org.bson.BsonDocument; import org.bson.BsonString; @@ -31,7 +24,9 @@ import org.springframework.data.mongodb.observability.MongoObservation.HighCardinalityCommandKeyNames; import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames; +import com.mongodb.RequestContext; import com.mongodb.ServerAddress; +import com.mongodb.client.SynchronousContextProvider; import com.mongodb.connection.ClusterId; import com.mongodb.connection.ConnectionDescription; import com.mongodb.connection.ServerId; @@ -39,12 +34,18 @@ import com.mongodb.event.CommandStartedEvent; import com.mongodb.event.CommandSucceededEvent; +import io.micrometer.common.KeyValues; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; + /** * Series of test cases exercising {@link MongoObservationCommandListener}. * * @author Marcin Grzejszczak * @author Greg Turnquist - * @since 4.0.0 */ class MongoObservationCommandListenerTests { @@ -87,7 +88,7 @@ void commandStartedShouldNotInstrumentWhenNoRequestContext() { void commandStartedShouldNotInstrumentWhenNoParentSampleInRequestContext() { // when - listener.commandStarted(new CommandStartedEvent(new TestRequestContext(), 0, null, "some name", "", null)); + listener.commandStarted(new CommandStartedEvent(new MapRequestContext(), 0, null, "some name", "", null)); // then assertThat(meterRegistry).hasNoMetrics(); @@ -98,17 +99,17 @@ void successfullyCompletedCommandShouldCreateTimerWhenParentSampleInRequestConte // given Observation parent = Observation.start("name", observationRegistry); - TestRequestContext testRequestContext = TestRequestContext.withObservation(parent); + RequestContext traceRequestContext = getContext(); // when - listener.commandStarted(new CommandStartedEvent(testRequestContext, 0, // + listener.commandStarted(new CommandStartedEvent(traceRequestContext, 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)); + listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0)); // then assertThatTimerRegisteredWithTags(); @@ -119,33 +120,34 @@ void successfullyCompletedCommandWithCollectionHavingCommandNameShouldCreateTime // given Observation parent = Observation.start("name", observationRegistry); - TestRequestContext testRequestContext = TestRequestContext.withObservation(parent); + RequestContext traceRequestContext = getContext(); // when - listener.commandStarted(new CommandStartedEvent(testRequestContext, 0, // + listener.commandStarted(new CommandStartedEvent(traceRequestContext, 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)); + listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "aggregate", null, 0)); // then assertThatTimerRegisteredWithTags(); } + @Test void successfullyCompletedCommandWithoutClusterInformationShouldCreateTimerWhenParentSampleInRequestContext() { // given Observation parent = Observation.start("name", observationRegistry); - TestRequestContext testRequestContext = TestRequestContext.withObservation(parent); + RequestContext traceRequestContext = getContext(); // when - listener.commandStarted(new CommandStartedEvent(testRequestContext, 0, null, "database", "insert", + listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, null, "database", "insert", new BsonDocument("collection", new BsonString("user")))); - listener.commandSucceeded(new CommandSucceededEvent(testRequestContext, 0, null, "insert", null, 0)); + listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0)); // then assertThat(meterRegistry).hasTimerWithNameAndTags(HighCardinalityCommandKeyNames.MONGODB_COMMAND.asString(), @@ -157,10 +159,10 @@ void commandWithErrorShouldCreateTimerWhenParentSampleInRequestContext() { // given Observation parent = Observation.start("name", observationRegistry); - TestRequestContext testRequestContext = TestRequestContext.withObservation(parent); + RequestContext traceRequestContext = getContext(); // when - listener.commandStarted(new CommandStartedEvent(testRequestContext, 0, // + listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, // new ConnectionDescription( // new ServerId( // new ClusterId("description"), // @@ -168,12 +170,16 @@ void commandWithErrorShouldCreateTimerWhenParentSampleInRequestContext() { "database", "insert", // new BsonDocument("collection", new BsonString("user")))); listener.commandFailed( // - new CommandFailedEvent(testRequestContext, 0, null, "insert", 0, new IllegalAccessException())); + new CommandFailedEvent(traceRequestContext, 0, null, "insert", 0, new IllegalAccessException())); // then assertThatTimerRegisteredWithTags(); } + private RequestContext getContext() { + return ((SynchronousContextProvider) ContextProviderFactory.create(observationRegistry)).getContext(); + } + private void assertThatTimerRegisteredWithTags() { assertThat(meterRegistry) // diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ReactiveIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ReactiveIntegrationTests.java new file mode 100644 index 0000000000..82bf460089 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ReactiveIntegrationTests.java @@ -0,0 +1,84 @@ +/* + * 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 org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mongodb.repository.Person; +import org.springframework.data.mongodb.repository.ReactivePersonRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.test.SampleTestRunner; +import reactor.test.StepVerifier; +import reactor.util.context.Context; + +/** + * Collection of tests that log metrics and tracing with an external tracing tool. + * + * @author Mark Paluch + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = TestConfig.class) +public class ReactiveIntegrationTests extends SampleTestRunner { + + @Autowired ReactivePersonRepository repository; + + ReactiveIntegrationTests() { + super(SampleRunnerConfig.builder().build()); + } + + @Override + protected MeterRegistry createMeterRegistry() { + return TestConfig.METER_REGISTRY; + } + + @Override + protected ObservationRegistry createObservationRegistry() { + return TestConfig.OBSERVATION_REGISTRY; + } + + @Override + public SampleTestRunnerConsumer yourCode() { + + return (tracer, meterRegistry) -> { + + Observation intermediate = Observation.start("intermediate", createObservationRegistry()); + + repository.deleteAll() // + .then(repository.save(new Person("Dave", "Matthews", 42))) // + .contextWrite(Context.of(Observation.class, intermediate)) // + .as(StepVerifier::create).expectNextCount(1)// + .verifyComplete(); + + repository.findByLastname("Matthews") // + .contextWrite(Context.of(Observation.class, intermediate)) // + .as(StepVerifier::create).assertNext(actual -> { + + assertThat(actual).extracting("firstname", "lastname").containsExactly("Dave", "Matthews"); + }).verifyComplete(); + + intermediate.stop(); + System.out.println(((SimpleMeterRegistry) meterRegistry).getMetersAsString()); + }; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/TestConfig.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/TestConfig.java new file mode 100644 index 0000000000..76d3ee99ce --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/TestConfig.java @@ -0,0 +1,165 @@ +/* + * Copyright 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 java.util.Properties; + +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.ReactiveMongoDatabaseFactory; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory; +import org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory; +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.ReactivePersonRepository; +import org.springframework.data.mongodb.repository.SampleEvaluationContextExtension; +import org.springframework.data.mongodb.repository.support.MongoRepositoryFactoryBean; +import org.springframework.data.mongodb.repository.support.ReactiveMongoRepositoryFactoryBean; +import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClients; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.test.simple.SimpleTracer; + +/** + * @author Mark Paluch + */ +@Configuration +class TestConfig { + + static final MeterRegistry METER_REGISTRY = new SimpleMeterRegistry(); + static final ObservationRegistry OBSERVATION_REGISTRY = ObservationRegistry.create(); + + static { + OBSERVATION_REGISTRY.observationConfig().observationHandler(new DefaultMeterObservationHandler(METER_REGISTRY)); + } + + @Bean + MongoDatabaseFactory mongoDatabaseFactory(MongoClientSettings settings) { + return new SimpleMongoClientDatabaseFactory(MongoClients.create(settings), "observable"); + } + + @Bean + ReactiveMongoDatabaseFactory reactiveMongoDatabaseFactory(MongoClientSettings settings) { + return new SimpleReactiveMongoDatabaseFactory(com.mongodb.reactivestreams.client.MongoClients.create(settings), + "observable"); + } + + @Bean + MongoClientSettings mongoClientSettings(ObservationRegistry observationRegistry) { + + ConnectionString connectionString = new ConnectionString( + String.format("mongodb://%s:%s/?w=majority&uuidrepresentation=javaLegacy", "127.0.0.1", 27017)); + + MongoClientSettings settings = MongoClientSettings.builder() // + .addCommandListener(new MongoObservationCommandListener(observationRegistry, connectionString)) // + .contextProvider(ContextProviderFactory.create(observationRegistry)) // + .applyConnectionString(connectionString) // + .build(); + + return settings; + } + + @Bean + MappingMongoConverter mongoConverter(MongoMappingContext mappingContext, MongoDatabaseFactory factory) { + return new MappingMongoConverter(new DefaultDbRefResolver(factory), mappingContext); + } + + @Bean + MongoMappingContext mappingContext() { + return new MongoMappingContext(); + } + + @Bean + MongoTemplate mongoTemplate(MongoDatabaseFactory mongoDatabaseFactory, MongoConverter mongoConverter) { + + MongoTemplate template = new MongoTemplate(mongoDatabaseFactory, mongoConverter); + return template; + } + + @Bean + ReactiveMongoTemplate reactiveMongoTemplate(ReactiveMongoDatabaseFactory mongoDatabaseFactory, + MongoConverter mongoConverter) { + + ReactiveMongoTemplate template = new ReactiveMongoTemplate(mongoDatabaseFactory, mongoConverter); + return template; + } + + @Bean + public PropertiesFactoryBean namedQueriesProperties() { + + PropertiesFactoryBean bean = new PropertiesFactoryBean(); + bean.setLocation(new ClassPathResource("META-INF/mongo-named-queries.properties")); + return bean; + } + + @Bean + MongoRepositoryFactoryBean personRepositoryFactoryBean(MongoOperations operations, + Properties namedQueriesProperties) { + + MongoRepositoryFactoryBean factoryBean = new MongoRepositoryFactoryBean<>( + PersonRepository.class); + factoryBean.setNamedQueries(new PropertiesBasedNamedQueries(namedQueriesProperties)); + factoryBean.setMongoOperations(operations); + factoryBean.setCreateIndexesForQueryMethods(true); + return factoryBean; + } + + @Bean + ReactiveMongoRepositoryFactoryBean reactivePersonRepositoryFactoryBean( + ReactiveMongoOperations operations, Properties namedQueriesProperties) { + + ReactiveMongoRepositoryFactoryBean factoryBean = new ReactiveMongoRepositoryFactoryBean<>( + ReactivePersonRepository.class); + factoryBean.setNamedQueries(new PropertiesBasedNamedQueries(namedQueriesProperties)); + factoryBean.setReactiveMongoOperations(operations); + factoryBean.setCreateIndexesForQueryMethods(true); + return factoryBean; + } + + @Bean + SampleEvaluationContextExtension contextExtension() { + return new SampleEvaluationContextExtension(); + } + + @Bean + ObservationRegistry registry() { + return OBSERVATION_REGISTRY; + } + + @Bean + Tracer tracer() { + return new SimpleTracer(); + } +} 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 deleted file mode 100644 index 308e880a02..0000000000 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ZipkinIntegrationTests.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * 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.assertThat; - -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; -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 DefaultMeterObservationHandler(METER_REGISTRY)); - } - - @Autowired PersonRepository repository; - - ZipkinIntegrationTests() { - super(SampleRunnerConfig.builder().build()); - } - - @Override - protected MeterRegistry createMeterRegistry() { - return METER_REGISTRY; - } - - @Override - protected ObservationRegistry createObservationRegistry() { - return OBSERVATION_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/reference/observability.adoc b/src/main/asciidoc/reference/observability.adoc index ebdbb60501..d6501a93ea 100644 --- a/src/main/asciidoc/reference/observability.adoc +++ b/src/main/asciidoc/reference/observability.adoc @@ -8,3 +8,44 @@ include::{root-target}_conventions.adoc[] include::{root-target}_metrics.adoc[] include::{root-target}_spans.adoc[] + +[[observability.registration]] +== Observability Registration + +Spring Data MongoDB currently has the most up-to-date code to support Observability in your MongoDB application. +These changes, however, haven't been picked up by Spring Boot (yet). +Until those changes are applied, if you wish to use Spring Data MongoDB's flavor of Observability, you must carry out the following steps. + +. First of all, you must opt into Spring Data MongoDB's configuration settings by adding the `@EnableMongoObservability` to either your `@SpringBootApplication` class or one of your configuration classes. +. Your project must include *Spring Boot Actuator*. +. Next you must add the customizer bean based: ++ +.Registering MongoDB Micrometer setup +==== +[source,java] +---- +@Bean +MongoClientSettingsBuilderCustomizer mongoMetricsSynchronousContextProvider(ObservationRegistry registry) { + return (clientSettingsBuilder) -> { + clientSettingsBuilder.contextProvider(ContextProviderFactory.create(registry)) + .addCommandListener(new MongoObservationCommandListener(registry)); + }; +} +---- +==== ++ +.Disable Spring Boot's autoconfigured MongoDB command listener and enable tracing manually by adding the following properties to your `application.properties` ++ +.Custom settings to apply +==== +[source] +---- +# Disable Spring Boot's autoconfigured tracing +management.metrics.mongo.command.enabled=false +# Enable it manually +management.tracing.enabled=true +---- +Be sure to add any other relevant settings needed to configure the tracer you are using based upon Micrometer's reference documentation. +==== + +This should do it! You are now running with Spring Data MongoDB's usage of Spring Observability's `Observation` API.