Improve configuration support for Observability integration.

Closes: #4216
This commit is contained in:
Greg L. Turnquist
2022-10-19 10:46:54 -05:00
committed by Mark Paluch
parent daef8b6e8e
commit e9ac77c058
13 changed files with 375 additions and 107 deletions

View File

@@ -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 {
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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));
};
}
}

View File

@@ -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 class MongoTracingObservationHandler implements TracingObservationHandler
this.tracer = tracer;
}
public void register(ObservationRegistry observationRegistry) {
observationRegistry.observationConfig().observationHandler(this);
}
@Override
public Tracer getTracer() {
return this.tracer;

View File

@@ -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)));
}
}

View File

@@ -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<Object, Object> context(Tracer tracer) {
Map<Object, Object> map = new ConcurrentHashMap<>();
Span currentSpan = tracer.currentSpan();
if (currentSpan == null) {
return map;
}
map.put(Span.class, currentSpan);
map.put(TraceContext.class, currentSpan.context());
return map;
}
}

View File

@@ -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<Object, Object> map;
public TraceRequestContext() {
this(new HashMap<>());
}
public TraceRequestContext(Map<Object, Object> context) {
this.map = context;
}
@Override
public <T> T get(Object key) {
return (T) map.get(key);
}
@Override
public boolean hasKey(Object key) {
return map.containsKey(key);
}
@Override
public boolean isEmpty() {
return map.isEmpty();
}
@Override
public void put(Object key, Object value) {
map.put(key, value);
}
@Override
public void delete(Object key) {
map.remove(key);
}
@Override
public int size() {
return map.size();
}
@Override
public Stream<Map.Entry<Object, Object>> stream() {
return map.entrySet().stream();
}
}

View File

@@ -77,10 +77,10 @@ class MongoObservationCommandListenerForTracingTests {
void successfullyCompletedCommandShouldCreateSpanWhenParentSampleInRequestContext() {
// given
TestRequestContext testRequestContext = createTestRequestContextWithParentObservationAndStartIt();
TraceRequestContext traceRequestContext = createTestRequestContextWithParentObservationAndStartIt();
// when
commandStartedAndSucceeded(testRequestContext);
commandStartedAndSucceeded(traceRequestContext);
// then
assertThatMongoSpanIsClientWithTags().hasIpThatIsBlank().hasPortThatIsNotSet();
@@ -91,10 +91,10 @@ class MongoObservationCommandListenerForTracingTests {
// given
handler.setSetRemoteIpAndPortEnabled(true);
TestRequestContext testRequestContext = createTestRequestContextWithParentObservationAndStartIt();
TraceRequestContext traceRequestContext = createTestRequestContextWithParentObservationAndStartIt();
// when
commandStartedAndSucceeded(testRequestContext);
commandStartedAndSucceeded(traceRequestContext);
// then
assertThatMongoSpanIsClientWithTags().hasIpThatIsNotBlank().hasPortThatIsSet();
@@ -104,10 +104,10 @@ class MongoObservationCommandListenerForTracingTests {
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 @@ class MongoObservationCommandListenerForTracingTests {
"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 @@ class MongoObservationCommandListenerForTracingTests {
/**
* 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 @@ class MongoObservationCommandListenerForTracingTests {
"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));
}
/**

View File

@@ -87,7 +87,7 @@ class MongoObservationCommandListenerTests {
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 @@ class MongoObservationCommandListenerTests {
// 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 @@ class MongoObservationCommandListenerTests {
// 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 @@ class MongoObservationCommandListenerTests {
// 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 @@ class MongoObservationCommandListenerTests {
// 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 @@ class MongoObservationCommandListenerTests {
"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();

View File

@@ -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;
/**
* A {@link Map}-based {@link RequestContext}. (For test purposes only).
*
* @author Marcin Grzejszczak
* @author Greg Turnquist
* @since 4.0.0
*/
class TestRequestContext implements RequestContext {
private final Map<Object, Object> map = new HashMap<>();
@Override
public <T> T get(Object key) {
return (T) map.get(key);
}
@Override
public boolean hasKey(Object key) {
return map.containsKey(key);
}
@Override
public boolean isEmpty() {
return map.isEmpty();
}
@Override
public void put(Object key, Object value) {
map.put(key, value);
}
@Override
public void delete(Object key) {
map.remove(key);
}
@Override
public int size() {
return map.size();
}
@Override
public Stream<Map.Entry<Object, Object>> stream() {
return map.entrySet().stream();
}
class TestRequestContext extends TraceRequestContext {
static TestRequestContext withObservation(Observation value) {
return new TestRequestContext(value);
}
TestRequestContext testRequestContext = new TestRequestContext();
testRequestContext.put(Observation.class, value);
return testRequestContext;
private TestRequestContext(Observation value) {
super(context(value));
}
private static Map<Object, Object> context(Observation value) {
Map<Object, Object> map = new ConcurrentHashMap<>();
map.put(Observation.class, value);
return map;
}
}

View File

@@ -23,15 +23,16 @@ 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.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.ContextConfiguration;
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 @@ import com.mongodb.client.SynchronousContextProvider;
* @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 @@ public class ZipkinIntegrationTests extends SampleTestRunner {
}
@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 @@ public class ZipkinIntegrationTests extends SampleTestRunner {
ObservationRegistry registry() {
return OBSERVATION_REGISTRY;
}
@Bean
Tracer tracer() {
return new SimpleTracer();
}
}
}

View File

@@ -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<ObservationRegistry> 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.