@@ -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<Object, Object> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -15,30 +15,32 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.observability;
|
||||
|
||||
import io.micrometer.common.KeyValue;
|
||||
import io.micrometer.common.KeyValues;
|
||||
|
||||
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.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();
|
||||
|
||||
if (context.getCollectionName() != null) {
|
||||
if (!ObjectUtils.isEmpty(context.getCollectionName())) {
|
||||
keyValues = keyValues
|
||||
.and(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue(context.getCollectionName()));
|
||||
}
|
||||
@@ -58,12 +60,18 @@ public class DefaultMongoHandlerObservationConvention implements MongoHandlerObs
|
||||
HighCardinalityCommandKeyNames.MONGODB_COMMAND.withValue(context.getCommandStartedEvent().getCommandName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContextualName(MongoHandlerContext context) {
|
||||
return context.getContextualName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract connection details for a MongoDB connection into a {@link KeyValue}.
|
||||
*
|
||||
* @param event
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
private static KeyValue connectionTag(CommandStartedEvent event) {
|
||||
|
||||
ConnectionDescription connectionDescription = event.getConnectionDescription();
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package org.springframework.data.mongodb.observability;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
/**
|
||||
* Annotation to active Spring Data MongoDB's usage of Micrometer's Observation API.
|
||||
*/
|
||||
@Inherited
|
||||
@Documented
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Import(MongoMetricsConfiguration.class)
|
||||
public @interface EnableMongoObservability {
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -28,15 +28,15 @@ import com.mongodb.RequestContext;
|
||||
* @author Greg Turnquist
|
||||
* @since 4.0.0
|
||||
*/
|
||||
class TraceRequestContext implements RequestContext {
|
||||
class MapRequestContext implements RequestContext {
|
||||
|
||||
private final Map<Object, Object> map;
|
||||
|
||||
public TraceRequestContext() {
|
||||
public MapRequestContext() {
|
||||
this(new HashMap<>());
|
||||
}
|
||||
|
||||
public TraceRequestContext(Map<Object, Object> context) {
|
||||
public MapRequestContext(Map<Object, Object> context) {
|
||||
this.map = context;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -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;
|
||||
@@ -30,6 +28,10 @@ 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.
|
||||
*
|
||||
@@ -37,10 +39,12 @@ import com.mongodb.event.CommandSucceededEvent;
|
||||
* @author Greg Turnquist
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class MongoHandlerContext extends Observation.Context {
|
||||
public class MongoHandlerContext extends SenderContext<Object> {
|
||||
|
||||
/**
|
||||
* @see https://docs.mongodb.com/manual/reference/command for the command reference
|
||||
* @see <a href=
|
||||
* "https://docs.mongodb.com/manual/reference/command">https://docs.mongodb.com/manual/reference/command</a> for
|
||||
* the command reference
|
||||
*/
|
||||
private static final Set<String> COMMANDS_WITH_COLLECTION_NAME = new LinkedHashSet<>(
|
||||
Arrays.asList("aggregate", "count", "distinct", "mapReduce", "geoSearch", "delete", "find", "findAndModify",
|
||||
@@ -55,7 +59,7 @@ public class MongoHandlerContext extends Observation.Context {
|
||||
private CommandFailedEvent commandFailedEvent;
|
||||
|
||||
public MongoHandlerContext(CommandStartedEvent commandStartedEvent, RequestContext requestContext) {
|
||||
|
||||
super((carrier, key, value) -> {}, Kind.CLIENT);
|
||||
this.commandStartedEvent = commandStartedEvent;
|
||||
this.requestContext = requestContext;
|
||||
this.collectionName = getCollectionName(commandStartedEvent);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -22,7 +22,7 @@ import io.micrometer.observation.ObservationConvention;
|
||||
* {@link ObservationConvention} for {@link MongoHandlerContext}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 4.0.0
|
||||
* @since 4
|
||||
*/
|
||||
public interface MongoHandlerObservationConvention extends ObservationConvention<MongoHandlerContext> {
|
||||
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
package org.springframework.data.mongodb.observability;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import io.micrometer.tracing.Tracer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
|
||||
/**
|
||||
* Class to configure needed beans for MongoDB + Micrometer.
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
@Configuration
|
||||
public class MongoMetricsConfiguration {
|
||||
|
||||
@Bean
|
||||
MongoObservationCommandListener mongoObservationCommandListener(ObservationRegistry registry) {
|
||||
return new MongoObservationCommandListener(registry);
|
||||
}
|
||||
@Bean
|
||||
public MongoObservationCommandListener mongoObservationCommandListener(ObservationRegistry registry) {
|
||||
return new MongoObservationCommandListener(registry);
|
||||
}
|
||||
|
||||
@Bean
|
||||
MongoTracingObservationHandler mongoTracingObservationHandler(Tracer tracer) {
|
||||
return new MongoTracingObservationHandler(tracer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package org.springframework.data.mongodb.observability;
|
||||
|
||||
import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import io.micrometer.tracing.Tracer;
|
||||
|
||||
import com.mongodb.client.SynchronousContextProvider;
|
||||
|
||||
/**
|
||||
* Helper functions to ease registration of Spring Data MongoDB's observability.
|
||||
*/
|
||||
public class MongoMetricsConfigurationHelper {
|
||||
|
||||
public static SynchronousContextProvider synchronousContextProvider(Tracer tracer, ObservationRegistry registry) {
|
||||
return () -> new SynchronousTraceRequestContext(tracer).withObservation(Observation.start("name", registry));
|
||||
}
|
||||
|
||||
public static void addObservationHandler(ObservationRegistry registry, Tracer tracer) {
|
||||
registry.observationConfig().observationHandler(new MongoTracingObservationHandler(tracer));
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package org.springframework.data.mongodb.observability;
|
||||
|
||||
import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import reactor.core.CoreSubscriber;
|
||||
import reactor.util.context.Context;
|
||||
|
||||
import com.mongodb.reactivestreams.client.ReactiveContextProvider;
|
||||
|
||||
/**
|
||||
* Helper functions to ease registration of Spring Data MongoDB's observability.
|
||||
*/
|
||||
public class MongoMetricsReactiveConfigurationHelper {
|
||||
|
||||
public static ReactiveContextProvider reactiveContextProvider(ObservationRegistry registry) {
|
||||
return subscriber -> {
|
||||
if (subscriber instanceof CoreSubscriber<?> coreSubscriber) {
|
||||
return new ReactiveTraceRequestContext(coreSubscriber.currentContext())
|
||||
.withObservation(Observation.start("name", registry));
|
||||
}
|
||||
return new ReactiveTraceRequestContext(Context.empty()).withObservation(Observation.start("name", registry));
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -23,7 +23,7 @@ import io.micrometer.observation.docs.ObservationDocumentation;
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @author Greg Turnquist
|
||||
* @since 4.0.0
|
||||
* @since 4.0
|
||||
*/
|
||||
enum MongoObservation implements ObservationDocumentation {
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -15,11 +15,10 @@
|
||||
*/
|
||||
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 org.springframework.util.Assert;
|
||||
|
||||
import com.mongodb.RequestContext;
|
||||
import com.mongodb.event.CommandFailedEvent;
|
||||
@@ -27,27 +26,35 @@ 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 MongoHandlerObservationConvention observationConvention;
|
||||
private final MongoHandlerObservationConvention observationConvention = new DefaultMongoHandlerObservationConvention();
|
||||
|
||||
/**
|
||||
* Create a new {@link MongoObservationCommandListener} to record {@link Observation}s.
|
||||
*
|
||||
* @param observationRegistry must not be {@literal null}
|
||||
*/
|
||||
public MongoObservationCommandListener(ObservationRegistry observationRegistry) {
|
||||
|
||||
Assert.notNull(observationRegistry, "ObservationRegistry must not be null");
|
||||
|
||||
this.observationRegistry = observationRegistry;
|
||||
this.observationConvention = new DefaultMongoHandlerObservationConvention();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -75,11 +82,26 @@ public final class MongoObservationCommandListener implements CommandListener {
|
||||
log.debug("Found the following observation passed from the mongo context [" + parent + "]");
|
||||
}
|
||||
|
||||
if (parent == null) {
|
||||
return;
|
||||
MongoHandlerContext observationContext = new MongoHandlerContext(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 +155,7 @@ public final class MongoObservationCommandListener implements CommandListener {
|
||||
* @param context
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
private static Observation observationFromContext(RequestContext context) {
|
||||
|
||||
Observation observation = context.getOrDefault(Observation.class, null);
|
||||
@@ -140,7 +163,7 @@ public final class MongoObservationCommandListener implements CommandListener {
|
||||
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 +174,4 @@ public final class MongoObservationCommandListener implements CommandListener {
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,122 +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.observation.ObservationRegistry;
|
||||
import io.micrometer.tracing.Span;
|
||||
import io.micrometer.tracing.Tracer;
|
||||
import io.micrometer.tracing.handler.TracingObservationHandler;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import com.mongodb.MongoSocketException;
|
||||
import com.mongodb.connection.ConnectionDescription;
|
||||
import com.mongodb.event.CommandStartedEvent;
|
||||
|
||||
/**
|
||||
* A {@link TracingObservationHandler} that handles {@link MongoHandlerContext}. It configures a span specific to Mongo
|
||||
* operations.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @author Greg Turnquist
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class MongoTracingObservationHandler implements TracingObservationHandler<MongoHandlerContext> {
|
||||
|
||||
private static final Log log = LogFactory.getLog(MongoTracingObservationHandler.class);
|
||||
|
||||
private final Tracer tracer;
|
||||
|
||||
private boolean setRemoteIpAndPortEnabled;
|
||||
|
||||
public MongoTracingObservationHandler(Tracer tracer) {
|
||||
this.tracer = tracer;
|
||||
}
|
||||
|
||||
public void register(ObservationRegistry observationRegistry) {
|
||||
observationRegistry.observationConfig().observationHandler(this);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package org.springframework.data.mongodb.observability;
|
||||
|
||||
import io.micrometer.observation.Observation;
|
||||
import reactor.util.context.ContextView;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
class ReactiveTraceRequestContext extends TraceRequestContext {
|
||||
|
||||
ReactiveTraceRequestContext withObservation(Observation value) {
|
||||
|
||||
put(Observation.class, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
ReactiveTraceRequestContext(ContextView context) {
|
||||
super(context.stream().collect(Collectors.toConcurrentMap(Map.Entry::getKey, Map.Entry::getValue)));
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package org.springframework.data.mongodb.observability;
|
||||
|
||||
import io.micrometer.observation.Observation;
|
||||
import io.micrometer.tracing.Span;
|
||||
import io.micrometer.tracing.TraceContext;
|
||||
import io.micrometer.tracing.Tracer;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
class SynchronousTraceRequestContext extends TraceRequestContext {
|
||||
|
||||
SynchronousTraceRequestContext(Tracer tracer) {
|
||||
super(context(tracer));
|
||||
}
|
||||
|
||||
SynchronousTraceRequestContext withObservation(Observation value) {
|
||||
|
||||
put(Observation.class, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
private static Map<Object, Object> context(Tracer tracer) {
|
||||
|
||||
Map<Object, Object> map = new ConcurrentHashMap<>();
|
||||
|
||||
Span currentSpan = tracer.currentSpan();
|
||||
|
||||
if (currentSpan == null) {
|
||||
return map;
|
||||
}
|
||||
|
||||
map.put(Span.class, currentSpan);
|
||||
map.put(TraceContext.class, currentSpan.context());
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Infrastructure to provide driver observability using Micrometer.
|
||||
*/
|
||||
@org.springframework.lang.NonNullApi
|
||||
package org.springframework.data.mongodb.observability;
|
||||
@@ -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<Person> people = repository.findByLastname("Matthews");
|
||||
|
||||
assertThat(people).hasSize(1);
|
||||
assertThat(people.get(0)).extracting("firstname", "lastname").containsExactly("Dave", "Matthews");
|
||||
|
||||
repository.deleteAll();
|
||||
|
||||
System.out.println(((SimpleMeterRegistry) meterRegistry).getMetersAsString());
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,24 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.observability;
|
||||
|
||||
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.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;
|
||||
import com.mongodb.event.CommandFailedEvent;
|
||||
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;
|
||||
@@ -25,35 +43,16 @@ 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.HighCardinalityCommandKeyNames;
|
||||
import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames;
|
||||
|
||||
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;
|
||||
|
||||
@@ -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,7 +74,7 @@ class MongoObservationCommandListenerForTracingTests {
|
||||
void successfullyCompletedCommandShouldCreateSpanWhenParentSampleInRequestContext() {
|
||||
|
||||
// given
|
||||
TraceRequestContext traceRequestContext = createTestRequestContextWithParentObservationAndStartIt();
|
||||
RequestContext traceRequestContext = createTestRequestContextWithParentObservationAndStartIt();
|
||||
|
||||
// when
|
||||
commandStartedAndSucceeded(traceRequestContext);
|
||||
@@ -86,25 +83,12 @@ class MongoObservationCommandListenerForTracingTests {
|
||||
assertThatMongoSpanIsClientWithTags().hasIpThatIsBlank().hasPortThatIsNotSet();
|
||||
}
|
||||
|
||||
@Test
|
||||
void successfullyCompletedCommandShouldCreateSpanWithAddressInfoWhenParentSampleInRequestContextAndHandlerAddressInfoEnabled() {
|
||||
|
||||
// given
|
||||
handler.setSetRemoteIpAndPortEnabled(true);
|
||||
TraceRequestContext traceRequestContext = createTestRequestContextWithParentObservationAndStartIt();
|
||||
|
||||
// when
|
||||
commandStartedAndSucceeded(traceRequestContext);
|
||||
|
||||
// then
|
||||
assertThatMongoSpanIsClientWithTags().hasIpThatIsNotBlank().hasPortThatIsSet();
|
||||
}
|
||||
|
||||
@Test
|
||||
void commandWithErrorShouldCreateTimerWhenParentSampleInRequestContext() {
|
||||
|
||||
// given
|
||||
TraceRequestContext traceRequestContext = createTestRequestContextWithParentObservationAndStartIt();
|
||||
RequestContext traceRequestContext = createTestRequestContextWithParentObservationAndStartIt();
|
||||
|
||||
// when
|
||||
listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, //
|
||||
@@ -122,23 +106,21 @@ class MongoObservationCommandListenerForTracingTests {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a parent {@link Observation} then wrap it inside a {@link TraceRequestContext}.
|
||||
* Create a parent {@link Observation} then wrap it inside a {@link MapRequestContext}.
|
||||
*/
|
||||
@NotNull
|
||||
private TraceRequestContext 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 TraceRequestContext} in order to inject some test data.
|
||||
* {@link MapRequestContext} in order to inject some test data.
|
||||
*
|
||||
* @param traceRequestContext
|
||||
*/
|
||||
private void commandStartedAndSucceeded(TraceRequestContext traceRequestContext) {
|
||||
private void commandStartedAndSucceeded(RequestContext traceRequestContext) {
|
||||
|
||||
listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, //
|
||||
new ConnectionDescription( //
|
||||
|
||||
@@ -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.junit.jupiter.api.Test;
|
||||
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.CommandFailedEvent;
|
||||
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 @@ class MongoObservationCommandListenerTests {
|
||||
void commandStartedShouldNotInstrumentWhenNoParentSampleInRequestContext() {
|
||||
|
||||
// when
|
||||
listener.commandStarted(new CommandStartedEvent(new TraceRequestContext(), 0, null, "some name", "", null));
|
||||
listener.commandStarted(new CommandStartedEvent(new MapRequestContext(), 0, null, "some name", "", null));
|
||||
|
||||
// then
|
||||
assertThat(meterRegistry).hasNoMetrics();
|
||||
@@ -98,7 +99,7 @@ class MongoObservationCommandListenerTests {
|
||||
|
||||
// given
|
||||
Observation parent = Observation.start("name", observationRegistry);
|
||||
TraceRequestContext traceRequestContext = TestRequestContext.withObservation(parent);
|
||||
RequestContext traceRequestContext = getContext();
|
||||
|
||||
// when
|
||||
listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, //
|
||||
@@ -119,7 +120,7 @@ class MongoObservationCommandListenerTests {
|
||||
|
||||
// given
|
||||
Observation parent = Observation.start("name", observationRegistry);
|
||||
TraceRequestContext traceRequestContext = TestRequestContext.withObservation(parent);
|
||||
RequestContext traceRequestContext = getContext();
|
||||
|
||||
// when
|
||||
listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, //
|
||||
@@ -135,12 +136,13 @@ class MongoObservationCommandListenerTests {
|
||||
assertThatTimerRegisteredWithTags();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void successfullyCompletedCommandWithoutClusterInformationShouldCreateTimerWhenParentSampleInRequestContext() {
|
||||
|
||||
// given
|
||||
Observation parent = Observation.start("name", observationRegistry);
|
||||
TraceRequestContext traceRequestContext = TestRequestContext.withObservation(parent);
|
||||
RequestContext traceRequestContext = getContext();
|
||||
|
||||
// when
|
||||
listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, null, "database", "insert",
|
||||
@@ -157,7 +159,7 @@ class MongoObservationCommandListenerTests {
|
||||
|
||||
// given
|
||||
Observation parent = Observation.start("name", observationRegistry);
|
||||
TraceRequestContext traceRequestContext = TestRequestContext.withObservation(parent);
|
||||
RequestContext traceRequestContext = getContext();
|
||||
|
||||
// when
|
||||
listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, //
|
||||
@@ -174,6 +176,10 @@ class MongoObservationCommandListenerTests {
|
||||
assertThatTimerRegisteredWithTags();
|
||||
}
|
||||
|
||||
private RequestContext getContext() {
|
||||
return ((SynchronousContextProvider) ContextProviderFactory.create(observationRegistry)).getContext();
|
||||
}
|
||||
|
||||
private void assertThatTimerRegisteredWithTags() {
|
||||
|
||||
assertThat(meterRegistry) //
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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();
|
||||
|
||||
System.out.println(((SimpleMeterRegistry) meterRegistry).getMetersAsString());
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* 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
|
||||
MongoObservationCommandListener mongoObservationCommandListener(ObservationRegistry registry) {
|
||||
return new MongoObservationCommandListener(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(MongoObservationCommandListener commandListener,
|
||||
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(commandListener) //
|
||||
.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<PersonRepository, Person, String> personRepositoryFactoryBean(MongoOperations operations,
|
||||
Properties namedQueriesProperties) {
|
||||
|
||||
MongoRepositoryFactoryBean<PersonRepository, Person, String> factoryBean = new MongoRepositoryFactoryBean<>(
|
||||
PersonRepository.class);
|
||||
factoryBean.setNamedQueries(new PropertiesBasedNamedQueries(namedQueriesProperties));
|
||||
factoryBean.setMongoOperations(operations);
|
||||
factoryBean.setCreateIndexesForQueryMethods(true);
|
||||
return factoryBean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveMongoRepositoryFactoryBean<ReactivePersonRepository, Person, String> reactivePersonRepositoryFactoryBean(
|
||||
ReactiveMongoOperations operations, Properties namedQueriesProperties) {
|
||||
|
||||
ReactiveMongoRepositoryFactoryBean<ReactivePersonRepository, Person, String> 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();
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package org.springframework.data.mongodb.observability;
|
||||
|
||||
import io.micrometer.observation.Observation;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
class TestRequestContext extends TraceRequestContext {
|
||||
|
||||
static TestRequestContext withObservation(Observation value) {
|
||||
return new TestRequestContext(value);
|
||||
}
|
||||
|
||||
private TestRequestContext(Observation value) {
|
||||
super(context(value));
|
||||
}
|
||||
|
||||
private static Map<Object, Object> context(Observation value) {
|
||||
|
||||
Map<Object, Object> map = new ConcurrentHashMap<>();
|
||||
|
||||
map.put(Observation.class, value);
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -1,221 +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.Tracer;
|
||||
import io.micrometer.tracing.test.SampleTestRunner;
|
||||
import io.micrometer.tracing.test.reporter.BuildingBlocks;
|
||||
import io.micrometer.tracing.test.simple.SimpleTracer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
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.ContextProvider;
|
||||
import com.mongodb.MongoClientSettings;
|
||||
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
|
||||
*/
|
||||
@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<BuildingBlocks, Deque<ObservationHandler<? extends Observation.Context>>> customizeObservationHandlers() {
|
||||
|
||||
return (buildingBlocks, observationHandlers) -> observationHandlers
|
||||
.addLast(new MongoTracingObservationHandler(buildingBlocks.getTracer()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TracingSetup[] getTracingSetup() {
|
||||
return new TracingSetup[] { TracingSetup.ZIPKIN_BRAVE };
|
||||
}
|
||||
|
||||
@Override
|
||||
public SampleTestRunnerConsumer yourCode() {
|
||||
|
||||
return (tracer, meterRegistry) -> {
|
||||
|
||||
repository.deleteAll();
|
||||
repository.save(new Person("Dave", "Matthews", 42));
|
||||
List<Person> people = repository.findByLastname("Matthews");
|
||||
|
||||
assertThat(people).hasSize(1);
|
||||
assertThat(people.get(0)).extracting("firstname", "lastname").containsExactly("Dave", "Matthews");
|
||||
|
||||
repository.deleteAll();
|
||||
|
||||
System.out.println(((SimpleMeterRegistry) meterRegistry).getMetersAsString());
|
||||
};
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableMongoRepositories
|
||||
static class TestConfig {
|
||||
|
||||
@Bean
|
||||
MongoObservationCommandListener mongoObservationCommandListener(ObservationRegistry registry) {
|
||||
return new MongoObservationCommandListener(registry);
|
||||
}
|
||||
|
||||
@Bean
|
||||
MongoDatabaseFactory mongoDatabaseFactory(MongoClientSettings settings) {
|
||||
return new SimpleMongoClientDatabaseFactory(MongoClients.create(settings), "observable");
|
||||
}
|
||||
|
||||
@Bean
|
||||
MongoClientSettings mongoClientSettings(MongoObservationCommandListener commandListener,
|
||||
ContextProvider contextProvider) {
|
||||
|
||||
ConnectionString connectionString = new ConnectionString(
|
||||
String.format("mongodb://%s:%s/?w=majority&uuidrepresentation=javaLegacy", "127.0.0.1", 27017));
|
||||
|
||||
MongoClientSettings settings = MongoClientSettings.builder() //
|
||||
.addCommandListener(commandListener) //
|
||||
.contextProvider(contextProvider) //
|
||||
.applyConnectionString(connectionString) //
|
||||
.build();
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SynchronousContextProvider contextProvider(ObservationRegistry registry) {
|
||||
return () -> TestRequestContext.withObservation(Observation.start("name", registry));
|
||||
}
|
||||
|
||||
@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);
|
||||
template.setWriteConcern(WriteConcern.JOURNALED);
|
||||
return template;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PropertiesFactoryBean namedQueriesProperties() {
|
||||
|
||||
PropertiesFactoryBean bean = new PropertiesFactoryBean();
|
||||
bean.setLocation(new ClassPathResource("META-INF/mongo-named-queries.properties"));
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
MongoRepositoryFactoryBean<PersonRepository, Person, String> repositoryFactoryBean(MongoOperations operations,
|
||||
PropertiesFactoryBean namedQueriesProperties) throws IOException {
|
||||
|
||||
MongoRepositoryFactoryBean<PersonRepository, Person, String> factoryBean = new MongoRepositoryFactoryBean<>(
|
||||
PersonRepository.class);
|
||||
factoryBean.setMongoOperations(operations);
|
||||
factoryBean.setNamedQueries(new PropertiesBasedNamedQueries(namedQueriesProperties.getObject()));
|
||||
factoryBean.setCreateIndexesForQueryMethods(true);
|
||||
return factoryBean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SampleEvaluationContextExtension contextExtension() {
|
||||
return new SampleEvaluationContextExtension();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ObservationRegistry registry() {
|
||||
return OBSERVATION_REGISTRY;
|
||||
}
|
||||
|
||||
@Bean
|
||||
Tracer tracer() {
|
||||
return new SimpleTracer();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user