From 6276caf75d4062fda796466af6193f5f464ffd62 Mon Sep 17 00:00:00 2001 From: "Greg L. Turnquist" Date: Wed, 19 Oct 2022 10:46:40 -0500 Subject: [PATCH 1/5] mongo-micrometer - Prepare branch --- pom.xml | 2 +- spring-data-mongodb-benchmarks/pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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..0eaf61eb3b 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 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 From 1560484b04be3c4368e7e724a03f268ec95d283d Mon Sep 17 00:00:00 2001 From: "Greg L. Turnquist" Date: Wed, 19 Oct 2022 10:46:54 -0500 Subject: [PATCH 2/5] Introduce missing configuration settings for Spring Data MongoDB + Micrometer. --- .../EnableMongoObservability.java | 16 ++++ .../MongoMetricsConfiguration.java | 21 +++++ .../MongoMetricsConfigurationHelper.java | 21 +++++ ...ngoMetricsReactiveConfigurationHelper.java | 24 ++++++ .../MongoTracingObservationHandler.java | 5 ++ .../ReactiveTraceRequestContext.java | 20 +++++ .../SynchronousTraceRequestContext.java | 38 +++++++++ .../observability/TraceRequestContext.java | 77 +++++++++++++++++ ...rvationCommandListenerForTracingTests.java | 28 +++---- .../MongoObservationCommandListenerTests.java | 26 +++--- .../observability/TestRequestContext.java | 72 +++------------- .../observability/ZipkinIntegrationTests.java | 40 ++++++--- .../asciidoc/reference/observability.adoc | 84 +++++++++++++++++++ 13 files changed, 370 insertions(+), 102 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/EnableMongoObservability.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsConfiguration.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsConfigurationHelper.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsReactiveConfigurationHelper.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/ReactiveTraceRequestContext.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/SynchronousTraceRequestContext.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/TraceRequestContext.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/EnableMongoObservability.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/EnableMongoObservability.java new file mode 100644 index 0000000000..6d6b8dde2d --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/EnableMongoObservability.java @@ -0,0 +1,16 @@ +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 { +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsConfiguration.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsConfiguration.java new file mode 100644 index 0000000000..75e41fd3d7 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsConfiguration.java @@ -0,0 +1,21 @@ +package org.springframework.data.mongodb.observability; + +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.Tracer; +import org.springframework.context.annotation.Bean; + +/** + * Class to configure needed beans for MongoDB + Micrometer. + */ +public class MongoMetricsConfiguration { + + @Bean + MongoObservationCommandListener mongoObservationCommandListener(ObservationRegistry registry) { + return new MongoObservationCommandListener(registry); + } + + @Bean + MongoTracingObservationHandler mongoTracingObservationHandler(Tracer tracer) { + return new MongoTracingObservationHandler(tracer); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsConfigurationHelper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsConfigurationHelper.java new file mode 100644 index 0000000000..3ec2032077 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsConfigurationHelper.java @@ -0,0 +1,21 @@ +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)); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsReactiveConfigurationHelper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsReactiveConfigurationHelper.java new file mode 100644 index 0000000000..6e690b405e --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsReactiveConfigurationHelper.java @@ -0,0 +1,24 @@ +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)); + }; + } +} 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 index aae9d96241..d3ef97d051 100644 --- 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 @@ -16,6 +16,7 @@ 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; @@ -49,6 +50,10 @@ public MongoTracingObservationHandler(Tracer tracer) { this.tracer = tracer; } + public void register(ObservationRegistry observationRegistry) { + observationRegistry.observationConfig().observationHandler(this); + } + @Override public Tracer getTracer() { return this.tracer; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/ReactiveTraceRequestContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/ReactiveTraceRequestContext.java new file mode 100644 index 0000000000..b53ea0c56b --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/ReactiveTraceRequestContext.java @@ -0,0 +1,20 @@ +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))); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/SynchronousTraceRequestContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/SynchronousTraceRequestContext.java new file mode 100644 index 0000000000..d061755343 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/SynchronousTraceRequestContext.java @@ -0,0 +1,38 @@ +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 context(Tracer tracer) { + + Map 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; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/TraceRequestContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/TraceRequestContext.java new file mode 100644 index 0000000000..7c35764ca6 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/TraceRequestContext.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 java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +import com.mongodb.RequestContext; + +/** + * A {@link Map}-based {@link RequestContext}. + * + * @author Marcin Grzejszczak + * @author Greg Turnquist + * @since 4.0.0 + */ +class TraceRequestContext implements RequestContext { + + private final Map map; + + public TraceRequestContext() { + this(new HashMap<>()); + } + + public TraceRequestContext(Map context) { + this.map = context; + } + + @Override + public T get(Object key) { + return (T) map.get(key); + } + + @Override + public boolean hasKey(Object key) { + return map.containsKey(key); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public void put(Object key, Object value) { + map.put(key, value); + } + + @Override + public void delete(Object key) { + map.remove(key); + } + + @Override + public int size() { + return map.size(); + } + + @Override + public Stream> stream() { + return map.entrySet().stream(); + } +} 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..5a9133e40f 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 @@ -77,10 +77,10 @@ void setup() { void successfullyCompletedCommandShouldCreateSpanWhenParentSampleInRequestContext() { // given - TestRequestContext testRequestContext = createTestRequestContextWithParentObservationAndStartIt(); + TraceRequestContext traceRequestContext = createTestRequestContextWithParentObservationAndStartIt(); // when - commandStartedAndSucceeded(testRequestContext); + commandStartedAndSucceeded(traceRequestContext); // then assertThatMongoSpanIsClientWithTags().hasIpThatIsBlank().hasPortThatIsNotSet(); @@ -91,10 +91,10 @@ void successfullyCompletedCommandShouldCreateSpanWithAddressInfoWhenParentSample // given handler.setSetRemoteIpAndPortEnabled(true); - TestRequestContext testRequestContext = createTestRequestContextWithParentObservationAndStartIt(); + TraceRequestContext traceRequestContext = createTestRequestContextWithParentObservationAndStartIt(); // when - commandStartedAndSucceeded(testRequestContext); + commandStartedAndSucceeded(traceRequestContext); // then assertThatMongoSpanIsClientWithTags().hasIpThatIsNotBlank().hasPortThatIsSet(); @@ -104,10 +104,10 @@ void successfullyCompletedCommandShouldCreateSpanWithAddressInfoWhenParentSample void commandWithErrorShouldCreateTimerWhenParentSampleInRequestContext() { // given - TestRequestContext testRequestContext = createTestRequestContextWithParentObservationAndStartIt(); + TraceRequestContext traceRequestContext = createTestRequestContextWithParentObservationAndStartIt(); // when - listener.commandStarted(new CommandStartedEvent(testRequestContext, 0, // + listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, // new ConnectionDescription( // new ServerId( // new ClusterId("description"), // @@ -115,17 +115,17 @@ 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 TraceRequestContext}. */ @NotNull - private TestRequestContext createTestRequestContextWithParentObservationAndStartIt() { + private TraceRequestContext createTestRequestContextWithParentObservationAndStartIt() { Observation parent = Observation.start("name", observationRegistry); return TestRequestContext.withObservation(parent); @@ -134,13 +134,13 @@ private TestRequestContext createTestRequestContextWithParentObservationAndStart /** * 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 TraceRequestContext} in order to inject some test data. * - * @param testRequestContext + * @param traceRequestContext */ - private void commandStartedAndSucceeded(TestRequestContext testRequestContext) { + private void commandStartedAndSucceeded(TraceRequestContext traceRequestContext) { - listener.commandStarted(new CommandStartedEvent(testRequestContext, 0, // + listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, // new ConnectionDescription( // new ServerId( // new ClusterId("description"), // @@ -148,7 +148,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..94b454c79f 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 @@ -87,7 +87,7 @@ void commandStartedShouldNotInstrumentWhenNoRequestContext() { void commandStartedShouldNotInstrumentWhenNoParentSampleInRequestContext() { // when - listener.commandStarted(new CommandStartedEvent(new TestRequestContext(), 0, null, "some name", "", null)); + listener.commandStarted(new CommandStartedEvent(new TraceRequestContext(), 0, null, "some name", "", null)); // then assertThat(meterRegistry).hasNoMetrics(); @@ -98,17 +98,17 @@ void successfullyCompletedCommandShouldCreateTimerWhenParentSampleInRequestConte // given Observation parent = Observation.start("name", observationRegistry); - TestRequestContext testRequestContext = TestRequestContext.withObservation(parent); + TraceRequestContext traceRequestContext = TestRequestContext.withObservation(parent); // 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,17 +119,17 @@ void successfullyCompletedCommandWithCollectionHavingCommandNameShouldCreateTime // given Observation parent = Observation.start("name", observationRegistry); - TestRequestContext testRequestContext = TestRequestContext.withObservation(parent); + TraceRequestContext traceRequestContext = TestRequestContext.withObservation(parent); // 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(); @@ -140,12 +140,12 @@ void successfullyCompletedCommandWithoutClusterInformationShouldCreateTimerWhenP // given Observation parent = Observation.start("name", observationRegistry); - TestRequestContext testRequestContext = TestRequestContext.withObservation(parent); + TraceRequestContext traceRequestContext = TestRequestContext.withObservation(parent); // 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 +157,10 @@ void commandWithErrorShouldCreateTimerWhenParentSampleInRequestContext() { // given Observation parent = Observation.start("name", observationRegistry); - TestRequestContext testRequestContext = TestRequestContext.withObservation(parent); + TraceRequestContext traceRequestContext = TestRequestContext.withObservation(parent); // when - listener.commandStarted(new CommandStartedEvent(testRequestContext, 0, // + listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, // new ConnectionDescription( // new ServerId( // new ClusterId("description"), // @@ -168,7 +168,7 @@ 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(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/TestRequestContext.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/TestRequestContext.java index 6f82e5678f..4ff0721d3a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/TestRequestContext.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/TestRequestContext.java @@ -1,78 +1,26 @@ -/* - * Copyright 2013-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package org.springframework.data.mongodb.observability; import io.micrometer.observation.Observation; -import java.util.HashMap; import java.util.Map; -import java.util.stream.Stream; +import java.util.concurrent.ConcurrentHashMap; -import com.mongodb.RequestContext; +class TestRequestContext extends TraceRequestContext { -/** - * A {@link Map}-based {@link RequestContext}. (For test purposes only). - * - * @author Marcin Grzejszczak - * @author Greg Turnquist - * @since 4.0.0 - */ -class TestRequestContext implements RequestContext { - - private final Map map = new HashMap<>(); - - @Override - public T get(Object key) { - return (T) map.get(key); - } - - @Override - public boolean hasKey(Object key) { - return map.containsKey(key); - } - - @Override - public boolean isEmpty() { - return map.isEmpty(); - } - - @Override - public void put(Object key, Object value) { - map.put(key, value); + static TestRequestContext withObservation(Observation value) { + return new TestRequestContext(value); } - @Override - public void delete(Object key) { - map.remove(key); + private TestRequestContext(Observation value) { + super(context(value)); } - @Override - public int size() { - return map.size(); - } + private static Map context(Observation value) { - @Override - public Stream> stream() { - return map.entrySet().stream(); - } + Map map = new ConcurrentHashMap<>(); - static TestRequestContext withObservation(Observation value) { + map.put(Observation.class, value); - TestRequestContext testRequestContext = new TestRequestContext(); - testRequestContext.put(Observation.class, value); - return testRequestContext; + return map; } } 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 index 308e880a02..7e66a0908e 100644 --- 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 @@ -23,15 +23,16 @@ 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.Disabled; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.PropertiesFactoryBean; @@ -56,8 +57,8 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import com.mongodb.ConnectionString; +import com.mongodb.ContextProvider; import com.mongodb.MongoClientSettings; -import com.mongodb.RequestContext; import com.mongodb.WriteConcern; import com.mongodb.client.MongoClients; import com.mongodb.client.SynchronousContextProvider; @@ -71,7 +72,6 @@ * @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 { @@ -139,33 +139,41 @@ MongoObservationCommandListener mongoObservationCommandListener(ObservationRegis } @Bean - MongoDatabaseFactory mongoDatabaseFactory(MongoObservationCommandListener commandListener, - ObservationRegistry registry) { + 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)); - 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"); + return settings; } @Bean - MappingMongoConverter mongoConverter(MongoDatabaseFactory factory) { - - MongoMappingContext mappingContext = new MongoMappingContext(); - mappingContext.afterPropertiesSet(); + 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) { @@ -203,5 +211,11 @@ SampleEvaluationContextExtension contextExtension() { ObservationRegistry registry() { return OBSERVATION_REGISTRY; } + + @Bean + Tracer tracer() { + return new SimpleTracer(); + } + } } diff --git a/src/main/asciidoc/reference/observability.adoc b/src/main/asciidoc/reference/observability.adoc index ebdbb60501..37d4fdbbd1 100644 --- a/src/main/asciidoc/reference/observability.adoc +++ b/src/main/asciidoc/reference/observability.adoc @@ -8,3 +8,87 @@ 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 one of the following bean definitions based on whether you're using non-reactive or reactive Spring Data MongoDB. ++ +.Registering a synchronous (non-reactive) MongoDB Micrometer setup +==== +[source,java] +---- +@Bean +MongoClientSettingsBuilderCustomizer mongoMetricsSynchronousContextProvider(Tracer tracer, + ObservationRegistry registry) { + return (clientSettingsBuilder) -> { + clientSettingsBuilder.contextProvider( // + MongoMetricsConfigurationHelper.synchronousContextProvider(tracer, registry)); + }; +} +---- +==== ++ +.Registering a reactive MongoDB Micrometer setup +==== +[source,java] +---- +@Bean +MongoClientSettingsBuilderCustomizer mongoMetricsReactiveContextProvider(ObservationRegistry registry) { + return (clientSettingsBuilder) -> { + clientSettingsBuilder.contextProvider( // + MongoMetricsReactiveConfigurationHelper.reactiveContextProvider(registry)); + }; +} +---- +==== ++ +IMPORTANT: ONLY add one of these two bean definitions! +. Add the following bean definition to listen for MongoDB command events and record them with Micrometer. ++ +.Registering to listen for MongoDB commands. +==== +[source,java] +---- +@Bean +MongoClientSettingsBuilderCustomizer mongoObservationCommandListenerCustomizer(MongoDBContainer mongoDBContainer, + MongoObservationCommandListener commandListener) { + return (clientSettingsBuilder) -> clientSettingsBuilder // + .addCommandListener(commandListener); +} +---- +==== +. Add the following bean definition to register Spring Data MongoDB's trace observation handler ++ +.Registering +==== +[source,java] +---- +@Bean +ObservationRegistryCustomizer mongoTracingHandlerCustomizer( + MongoTracingObservationHandler handler) { + return handler::register; +} +---- +==== +. 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. From e0ac4a2714094783792e5c03e6a0351266284971 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 20 Oct 2022 16:39:05 +0200 Subject: [PATCH 3/5] Polishing. --- .../observability/ContextProviderFactory.java | 139 +++++++++++ ...aultMongoHandlerObservationConvention.java | 20 +- .../EnableMongoObservability.java | 16 -- ...estContext.java => MapRequestContext.java} | 6 +- .../observability/MongoHandlerContext.java | 14 +- .../MongoHandlerObservationConvention.java | 2 +- .../MongoMetricsConfiguration.java | 20 +- .../MongoMetricsConfigurationHelper.java | 21 -- ...ngoMetricsReactiveConfigurationHelper.java | 24 -- .../MongoObservationCommandListener.java | 59 +++-- .../MongoTracingObservationHandler.java | 122 ---------- .../ReactiveTraceRequestContext.java | 20 -- .../SynchronousTraceRequestContext.java | 38 --- .../mongodb/observability/package-info.java | 5 + .../ImperativeIntegrationTests.java | 77 ++++++ ...rvationCommandListenerForTracingTests.java | 56 ++--- .../MongoObservationCommandListenerTests.java | 34 +-- .../ReactiveIntegrationTests.java | 80 +++++++ .../mongodb/observability/TestConfig.java | 171 ++++++++++++++ .../observability/TestRequestContext.java | 26 --- .../observability/ZipkinIntegrationTests.java | 221 ------------------ 21 files changed, 575 insertions(+), 596 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/ContextProviderFactory.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/EnableMongoObservability.java rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/{TraceRequestContext.java => MapRequestContext.java} (91%) delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsConfigurationHelper.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsReactiveConfigurationHelper.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoTracingObservationHandler.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/ReactiveTraceRequestContext.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/SynchronousTraceRequestContext.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/package-info.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ReactiveIntegrationTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/TestConfig.java delete mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/TestRequestContext.java delete mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ZipkinIntegrationTests.java 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..af278ec028 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,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 KeyValues getHighCardinalityKeyValues(MongoHandlerContext context) { 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(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/EnableMongoObservability.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/EnableMongoObservability.java deleted file mode 100644 index 6d6b8dde2d..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/EnableMongoObservability.java +++ /dev/null @@ -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 { -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/TraceRequestContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MapRequestContext.java similarity index 91% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/TraceRequestContext.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MapRequestContext.java index 7c35764ca6..aa12ca1ddb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/TraceRequestContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MapRequestContext.java @@ -28,15 +28,15 @@ * @author Greg Turnquist * @since 4.0.0 */ -class TraceRequestContext implements RequestContext { +class MapRequestContext implements RequestContext { private final Map map; - public TraceRequestContext() { + public MapRequestContext() { this(new HashMap<>()); } - public TraceRequestContext(Map context) { + public MapRequestContext(Map context) { this.map = context; } 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..0574c7c665 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; @@ -30,6 +28,10 @@ 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 @@ * @author Greg Turnquist * @since 4.0.0 */ -public class MongoHandlerContext extends Observation.Context { +public 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", @@ -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); 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/MongoMetricsConfiguration.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsConfiguration.java index 75e41fd3d7..5917fdb864 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsConfiguration.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsConfiguration.java @@ -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); - } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsConfigurationHelper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsConfigurationHelper.java deleted file mode 100644 index 3ec2032077..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsConfigurationHelper.java +++ /dev/null @@ -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)); - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsReactiveConfigurationHelper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsReactiveConfigurationHelper.java deleted file mode 100644 index 6e690b405e..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsReactiveConfigurationHelper.java +++ /dev/null @@ -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)); - }; - } -} 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..4e1162c505 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,11 +15,9 @@ */ 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.RequestContext; import com.mongodb.event.CommandFailedEvent; @@ -27,27 +25,27 @@ 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(); public MongoObservationCommandListener(ObservationRegistry observationRegistry) { - this.observationRegistry = observationRegistry; - this.observationConvention = new DefaultMongoHandlerObservationConvention(); } @Override @@ -75,11 +73,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(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 +146,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 +154,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 +165,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 d3ef97d051..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoTracingObservationHandler.java +++ /dev/null @@ -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 { - - 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; - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/ReactiveTraceRequestContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/ReactiveTraceRequestContext.java deleted file mode 100644 index b53ea0c56b..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/ReactiveTraceRequestContext.java +++ /dev/null @@ -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))); - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/SynchronousTraceRequestContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/SynchronousTraceRequestContext.java deleted file mode 100644 index d061755343..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/SynchronousTraceRequestContext.java +++ /dev/null @@ -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 context(Tracer tracer) { - - Map 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; - } -} 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 5a9133e40f..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,7 +74,7 @@ void setup() { void successfullyCompletedCommandShouldCreateSpanWhenParentSampleInRequestContext() { // given - TraceRequestContext traceRequestContext = createTestRequestContextWithParentObservationAndStartIt(); + RequestContext traceRequestContext = createTestRequestContextWithParentObservationAndStartIt(); // when commandStartedAndSucceeded(traceRequestContext); @@ -86,25 +83,12 @@ void successfullyCompletedCommandShouldCreateSpanWhenParentSampleInRequestContex 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 @@ void commandWithErrorShouldCreateTimerWhenParentSampleInRequestContext() { } /** - * 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( // 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 94b454c79f..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 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 @@ void successfullyCompletedCommandShouldCreateTimerWhenParentSampleInRequestConte // 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 @@ void successfullyCompletedCommandWithCollectionHavingCommandNameShouldCreateTime // 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 @@ void successfullyCompletedCommandWithCollectionHavingCommandNameShouldCreateTime 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 @@ void commandWithErrorShouldCreateTimerWhenParentSampleInRequestContext() { // 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 @@ void commandWithErrorShouldCreateTimerWhenParentSampleInRequestContext() { 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..c969458a87 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ReactiveIntegrationTests.java @@ -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()); + }; + } +} 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..13500b5d23 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/TestConfig.java @@ -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 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/TestRequestContext.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/TestRequestContext.java deleted file mode 100644 index 4ff0721d3a..0000000000 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/TestRequestContext.java +++ /dev/null @@ -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 context(Observation value) { - - Map map = new ConcurrentHashMap<>(); - - map.put(Observation.class, value); - - return map; - } -} 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 7e66a0908e..0000000000 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ZipkinIntegrationTests.java +++ /dev/null @@ -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>> 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(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 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; - } - - @Bean - Tracer tracer() { - return new SimpleTracer(); - } - - } -} From 9059f3301e9a4e62a0e058ee159565cfadd9bbfe Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 20 Oct 2022 16:42:17 +0200 Subject: [PATCH 4/5] Update documentation. --- spring-data-mongodb-distribution/pom.xml | 4 +- .../asciidoc/reference/observability.adoc | 55 ++----------------- 2 files changed, 8 insertions(+), 51 deletions(-) diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 0eaf61eb3b..7d91980c2c 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/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/src/main/asciidoc/reference/observability.adoc b/src/main/asciidoc/reference/observability.adoc index 37d4fdbbd1..d6501a93ea 100644 --- a/src/main/asciidoc/reference/observability.adoc +++ b/src/main/asciidoc/reference/observability.adoc @@ -18,66 +18,23 @@ Until those changes are applied, if you wish to use Spring Data MongoDB's flavor . 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 one of the following bean definitions based on whether you're using non-reactive or reactive Spring Data MongoDB. +. Next you must add the customizer bean based: + -.Registering a synchronous (non-reactive) MongoDB Micrometer setup +.Registering MongoDB Micrometer setup ==== [source,java] ---- @Bean -MongoClientSettingsBuilderCustomizer mongoMetricsSynchronousContextProvider(Tracer tracer, - ObservationRegistry registry) { +MongoClientSettingsBuilderCustomizer mongoMetricsSynchronousContextProvider(ObservationRegistry registry) { return (clientSettingsBuilder) -> { - clientSettingsBuilder.contextProvider( // - MongoMetricsConfigurationHelper.synchronousContextProvider(tracer, registry)); + clientSettingsBuilder.contextProvider(ContextProviderFactory.create(registry)) + .addCommandListener(new MongoObservationCommandListener(registry)); }; } ---- ==== + -.Registering a reactive MongoDB Micrometer setup -==== -[source,java] ----- -@Bean -MongoClientSettingsBuilderCustomizer mongoMetricsReactiveContextProvider(ObservationRegistry registry) { - return (clientSettingsBuilder) -> { - clientSettingsBuilder.contextProvider( // - MongoMetricsReactiveConfigurationHelper.reactiveContextProvider(registry)); - }; -} ----- -==== -+ -IMPORTANT: ONLY add one of these two bean definitions! -. Add the following bean definition to listen for MongoDB command events and record them with Micrometer. -+ -.Registering to listen for MongoDB commands. -==== -[source,java] ----- -@Bean -MongoClientSettingsBuilderCustomizer mongoObservationCommandListenerCustomizer(MongoDBContainer mongoDBContainer, - MongoObservationCommandListener commandListener) { - return (clientSettingsBuilder) -> clientSettingsBuilder // - .addCommandListener(commandListener); -} ----- -==== -. Add the following bean definition to register Spring Data MongoDB's trace observation handler -+ -.Registering -==== -[source,java] ----- -@Bean -ObservationRegistryCustomizer mongoTracingHandlerCustomizer( - MongoTracingObservationHandler handler) { - return handler::register; -} ----- -==== -. Disable Spring Boot's autoconfigured MongoDB command listener and enable tracing manually by adding the following properties to your `application.properties` +.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 ==== From bf1d9a21ddd5aff4ca18d8010075e2e14764abc7 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 21 Oct 2022 09:00:47 +0200 Subject: [PATCH 5/5] Align conventions with OpenTelemetry spec. --- ...aultMongoHandlerObservationConvention.java | 44 +++++++++- .../observability/MongoHandlerContext.java | 28 +++++- .../MongoMetricsConfiguration.java | 21 ----- .../observability/MongoObservation.java | 88 +++++++++++++++++-- .../MongoObservationCommandListener.java | 10 ++- .../ReactiveIntegrationTests.java | 10 ++- .../mongodb/observability/TestConfig.java | 10 +-- 7 files changed, 165 insertions(+), 46 deletions(-) delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsConfiguration.java 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 af278ec028..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,11 +15,15 @@ */ package org.springframework.data.mongodb.observability; +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; @@ -38,13 +42,48 @@ class DefaultMongoHandlerObservationConvention implements MongoHandlerObservatio @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) { + + 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); @@ -56,8 +95,7 @@ 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 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 0574c7c665..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 @@ -23,6 +23,7 @@ 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; @@ -37,9 +38,10 @@ * * @author Marcin Grzejszczak * @author Greg Turnquist - * @since 4.0.0 + * @author Mark Paluch + * @since 4.0 */ -public class MongoHandlerContext extends SenderContext { +class MongoHandlerContext extends SenderContext { /** * @see { "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; @@ -58,8 +61,11 @@ public class MongoHandlerContext extends SenderContext { 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); @@ -73,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) { @@ -135,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/MongoMetricsConfiguration.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsConfiguration.java deleted file mode 100644 index 5917fdb864..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsConfiguration.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.springframework.data.mongodb.observability; - -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 - public MongoObservationCommandListener mongoObservationCommandListener(ObservationRegistry registry) { - return new MongoObservationCommandListener(registry); - } - -} 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 4e1162c505..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 @@ -19,6 +19,7 @@ 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; @@ -41,11 +42,18 @@ public class MongoObservationCommandListener implements CommandListener { private static final Log log = LogFactory.getLog(MongoObservationCommandListener.class); private final ObservationRegistry observationRegistry; + private final @Nullable ConnectionString connectionString; 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.connectionString = connectionString; } @Override @@ -73,7 +81,7 @@ public void commandStarted(CommandStartedEvent event) { log.debug("Found the following observation passed from the mongo context [" + parent + "]"); } - MongoHandlerContext observationContext = new MongoHandlerContext(event, requestContext); + MongoHandlerContext observationContext = new MongoHandlerContext(connectionString, event, requestContext); observationContext.setRemoteServiceName("mongo"); Observation observation = MongoObservation.MONGODB_COMMAND_OBSERVATION 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 index c969458a87..82bf460089 100644 --- 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 @@ -64,16 +64,20 @@ public SampleTestRunnerConsumer yourCode() { 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) + 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)) + 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 index 13500b5d23..76d3ee99ce 100644 --- 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 @@ -65,11 +65,6 @@ class TestConfig { 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"); @@ -82,14 +77,13 @@ ReactiveMongoDatabaseFactory reactiveMongoDatabaseFactory(MongoClientSettings se } @Bean - MongoClientSettings mongoClientSettings(MongoObservationCommandListener commandListener, - ObservationRegistry observationRegistry) { + 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(commandListener) // + .addCommandListener(new MongoObservationCommandListener(observationRegistry, connectionString)) // .contextProvider(ContextProviderFactory.create(observationRegistry)) // .applyConnectionString(connectionString) // .build();