Browse Source

Align conventions with OpenTelemetry spec.

See: #4216
pull/4218/head
Mark Paluch 3 years ago
parent
commit
2d63d6006d
No known key found for this signature in database
GPG Key ID: 4406B84C1661DCD1
  1. 4
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/ContextProviderFactory.java
  2. 91
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java
  3. 31
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerContext.java
  4. 21
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsConfiguration.java
  5. 109
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservation.java
  6. 37
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservationCommandListener.java
  7. 12
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java
  8. 152
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerForTracingTests.java
  9. 18
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java
  10. 15
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ReactiveIntegrationTests.java
  11. 10
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/TestConfig.java

4
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/ContextProviderFactory.java

@ -31,7 +31,6 @@ import com.mongodb.reactivestreams.client.ReactiveContextProvider;
import io.micrometer.observation.Observation; import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
import reactor.core.CoreSubscriber; import reactor.core.CoreSubscriber;
/** /**
@ -105,9 +104,6 @@ public class ContextProviderFactory {
Map<Object, Object> map = cs.currentContext().stream() Map<Object, Object> map = cs.currentContext().stream()
.collect(Collectors.toConcurrentMap(Entry::getKey, Entry::getValue)); .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(map);
} }

91
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java

@ -15,39 +15,84 @@
*/ */
package org.springframework.data.mongodb.observability; package org.springframework.data.mongodb.observability;
import org.springframework.data.mongodb.observability.MongoObservation.HighCardinalityCommandKeyNames; import java.net.InetSocketAddress;
import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames; import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
import com.mongodb.ConnectionString;
import com.mongodb.ServerAddress;
import com.mongodb.connection.ConnectionDescription; import com.mongodb.connection.ConnectionDescription;
import com.mongodb.connection.ConnectionId; import com.mongodb.connection.ConnectionId;
import com.mongodb.event.CommandStartedEvent; import com.mongodb.event.CommandStartedEvent;
import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues; import io.micrometer.common.KeyValues;
/** /**
* Default {@link MongoHandlerObservationConvention} implementation. * Default {@link MongoHandlerObservationConvention} implementation.
* *
* @author Greg Turnquist * @author Greg Turnquist
* @since 4 * @author Mark Paluch
* @since 4.0
*/ */
class DefaultMongoHandlerObservationConvention implements MongoHandlerObservationConvention { class DefaultMongoHandlerObservationConvention implements MongoHandlerObservationConvention {
@Override @Override
public KeyValues getLowCardinalityKeyValues(MongoHandlerContext context) { public KeyValues getLowCardinalityKeyValues(MongoHandlerContext context) {
KeyValues keyValues = KeyValues.empty(); KeyValues keyValues = KeyValues.of(LowCardinalityCommandKeyNames.DB_SYSTEM.withValue("mongodb"),
LowCardinalityCommandKeyNames.MONGODB_COMMAND.withValue(context.getCommandName()));
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())) { if (!ObjectUtils.isEmpty(context.getCollectionName())) {
keyValues = keyValues keyValues = keyValues
.and(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue(context.getCollectionName())); .and(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue(context.getCollectionName()));
} }
KeyValue connectionTag = connectionTag(context.getCommandStartedEvent()); ConnectionDescription connectionDescription = context.getCommandStartedEvent().getConnectionDescription();
if (connectionTag != null) {
keyValues = keyValues.and(connectionTag); if (connectionDescription != null) {
ServerAddress serverAddress = connectionDescription.getServerAddress();
if (serverAddress != null) {
keyValues = keyValues.and(LowCardinalityCommandKeyNames.NET_TRANSPORT.withValue("IP.TCP"),
LowCardinalityCommandKeyNames.NET_PEER_NAME.withValue(serverAddress.getHost()),
LowCardinalityCommandKeyNames.NET_PEER_PORT.withValue("" + serverAddress.getPort()));
InetSocketAddress socketAddress = serverAddress.getSocketAddress();
if (socketAddress != null) {
keyValues = keyValues.and(
LowCardinalityCommandKeyNames.NET_SOCK_PEER_ADDR.withValue(socketAddress.getHostName()),
LowCardinalityCommandKeyNames.NET_SOCK_PEER_PORT.withValue("" + socketAddress.getPort()));
}
}
ConnectionId connectionId = connectionDescription.getConnectionId();
if (connectionId != null) {
keyValues = keyValues.and(LowCardinalityCommandKeyNames.MONGODB_CLUSTER_ID
.withValue(connectionId.getServerId().getClusterId().getValue()));
}
} }
return keyValues; return keyValues;
@ -55,36 +100,20 @@ class DefaultMongoHandlerObservationConvention implements MongoHandlerObservatio
@Override @Override
public KeyValues getHighCardinalityKeyValues(MongoHandlerContext context) { public KeyValues getHighCardinalityKeyValues(MongoHandlerContext context) {
return KeyValues.empty();
return KeyValues.of(
HighCardinalityCommandKeyNames.MONGODB_COMMAND.withValue(context.getCommandStartedEvent().getCommandName()));
} }
@Override @Override
public String getContextualName(MongoHandlerContext context) { public String getContextualName(MongoHandlerContext context) {
return context.getContextualName();
}
/** String collectionName = context.getCollectionName();
* Extract connection details for a MongoDB connection into a {@link KeyValue}. CommandStartedEvent commandStartedEvent = context.getCommandStartedEvent();
*
* @param event
* @return
*/
@Nullable
private static KeyValue connectionTag(CommandStartedEvent event) {
ConnectionDescription connectionDescription = event.getConnectionDescription();
if (connectionDescription != null) {
ConnectionId connectionId = connectionDescription.getConnectionId(); if (ObjectUtils.isEmpty(collectionName)) {
if (connectionId != null) { return commandStartedEvent.getCommandName();
return LowCardinalityCommandKeyNames.MONGODB_CLUSTER_ID
.withValue(connectionId.getServerId().getClusterId().getValue());
}
} }
return null; return collectionName + "." + commandStartedEvent.getCommandName();
} }
} }

31
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerContext.java

@ -23,6 +23,7 @@ import org.bson.BsonDocument;
import org.bson.BsonValue; import org.bson.BsonValue;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import com.mongodb.ConnectionString;
import com.mongodb.RequestContext; import com.mongodb.RequestContext;
import com.mongodb.event.CommandFailedEvent; import com.mongodb.event.CommandFailedEvent;
import com.mongodb.event.CommandStartedEvent; import com.mongodb.event.CommandStartedEvent;
@ -37,9 +38,10 @@ import io.micrometer.observation.transport.SenderContext;
* *
* @author Marcin Grzejszczak * @author Marcin Grzejszczak
* @author Greg Turnquist * @author Greg Turnquist
* @since 4.0.0 * @author Mark Paluch
* @since 4.0
*/ */
public class MongoHandlerContext extends SenderContext<Object> { class MongoHandlerContext extends SenderContext<Object> {
/** /**
* @see <a href= * @see <a href=
@ -51,6 +53,7 @@ public class MongoHandlerContext extends SenderContext<Object> {
"insert", "update", "collMod", "compact", "convertToCapped", "create", "createIndexes", "drop", "dropIndexes", "insert", "update", "collMod", "compact", "convertToCapped", "create", "createIndexes", "drop", "dropIndexes",
"killCursors", "listIndexes", "reIndex")); "killCursors", "listIndexes", "reIndex"));
private final @Nullable ConnectionString connectionString;
private final CommandStartedEvent commandStartedEvent; private final CommandStartedEvent commandStartedEvent;
private final RequestContext requestContext; private final RequestContext requestContext;
private final String collectionName; private final String collectionName;
@ -58,8 +61,11 @@ public class MongoHandlerContext extends SenderContext<Object> {
private CommandSucceededEvent commandSucceededEvent; private CommandSucceededEvent commandSucceededEvent;
private CommandFailedEvent commandFailedEvent; private CommandFailedEvent commandFailedEvent;
public MongoHandlerContext(CommandStartedEvent commandStartedEvent, RequestContext requestContext) { public MongoHandlerContext(@Nullable ConnectionString connectionString, CommandStartedEvent commandStartedEvent,
RequestContext requestContext) {
super((carrier, key, value) -> {}, Kind.CLIENT); super((carrier, key, value) -> {}, Kind.CLIENT);
this.connectionString = connectionString;
this.commandStartedEvent = commandStartedEvent; this.commandStartedEvent = commandStartedEvent;
this.requestContext = requestContext; this.requestContext = requestContext;
this.collectionName = getCollectionName(commandStartedEvent); this.collectionName = getCollectionName(commandStartedEvent);
@ -73,17 +79,21 @@ public class MongoHandlerContext extends SenderContext<Object> {
return this.requestContext; return this.requestContext;
} }
public String getDatabaseName() {
return commandStartedEvent.getDatabaseName();
}
public String getCollectionName() { public String getCollectionName() {
return this.collectionName; return this.collectionName;
} }
public String getContextualName() { public String getCommandName() {
return commandStartedEvent.getCommandName();
if (this.collectionName == null) {
return this.commandStartedEvent.getCommandName();
} }
return this.commandStartedEvent.getCommandName() + " " + this.collectionName; @Nullable
public ConnectionString getConnectionString() {
return connectionString;
} }
public void setCommandSucceededEvent(CommandSucceededEvent commandSucceededEvent) { public void setCommandSucceededEvent(CommandSucceededEvent commandSucceededEvent) {
@ -116,7 +126,7 @@ public class MongoHandlerContext extends SenderContext<Object> {
} }
// Some other commands, like getMore, have a field like {"collection": collectionName}. // Some other commands, like getMore, have a field like {"collection": collectionName}.
return getNonEmptyBsonString(command.get("collection")); return command == null ? "" : getNonEmptyBsonString(command.get("collection"));
} }
/** /**
@ -125,7 +135,7 @@ public class MongoHandlerContext extends SenderContext<Object> {
* @return trimmed string from {@code bsonValue} or null if the trimmed string was empty or the value wasn't a string * @return trimmed string from {@code bsonValue} or null if the trimmed string was empty or the value wasn't a string
*/ */
@Nullable @Nullable
private static String getNonEmptyBsonString(BsonValue bsonValue) { private static String getNonEmptyBsonString(@Nullable BsonValue bsonValue) {
if (bsonValue == null || !bsonValue.isString()) { if (bsonValue == null || !bsonValue.isString()) {
return null; return null;
@ -135,4 +145,5 @@ public class MongoHandlerContext extends SenderContext<Object> {
return stringValue.isEmpty() ? null : stringValue; return stringValue.isEmpty() ? null : stringValue;
} }
} }

21
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoMetricsConfiguration.java

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

109
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservation.java

@ -44,13 +44,9 @@ enum MongoObservation implements ObservationDocumentation {
@Override @Override
public KeyName[] getHighCardinalityKeyNames() { public KeyName[] getHighCardinalityKeyNames() {
return HighCardinalityCommandKeyNames.values(); return new KeyName[0];
} }
@Override
public String getPrefix() {
return "spring.data.mongodb";
}
}; };
/** /**
@ -59,30 +55,114 @@ enum MongoObservation implements ObservationDocumentation {
enum LowCardinalityCommandKeyNames implements KeyName { enum LowCardinalityCommandKeyNames implements KeyName {
/** /**
* MongoDB collection name. * MongoDB database system.
*/ */
MONGODB_COLLECTION { DB_SYSTEM {
@Override @Override
public String asString() { public String asString() {
return "spring.data.mongodb.collection"; return "db.system";
} }
}, },
/** /**
* MongoDB cluster identifier. * MongoDB connection string.
*/ */
MONGODB_CLUSTER_ID { DB_CONNECTION_STRING {
@Override @Override
public String asString() { public String asString() {
return "spring.data.mongodb.cluster_id"; 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";
} }
},
/** /**
* Enums related to high cardinality key names for MongoDB commands. * Logical remote port number.
*/ */
enum HighCardinalityCommandKeyNames implements KeyName { NET_PEER_PORT {
@Override
public String asString() {
return "net.peer.port";
}
},
/**
* Mongo peer address.
*/
NET_SOCK_PEER_ADDR {
@Override
public String asString() {
return "net.sock.peer.addr";
}
},
/**
* Mongo peer port.
*/
NET_SOCK_PEER_PORT {
@Override
public String asString() {
return "net.sock.peer.port";
}
},
/**
* 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 "db.mongodb.collection";
}
},
/**
* MongoDB cluster identifier.
*/
MONGODB_CLUSTER_ID {
@Override
public String asString() {
return "spring.data.mongodb.cluster_id";
}
},
/** /**
* MongoDB command value. * MongoDB command value.
@ -90,8 +170,9 @@ enum MongoObservation implements ObservationDocumentation {
MONGODB_COMMAND { MONGODB_COMMAND {
@Override @Override
public String asString() { public String asString() {
return "spring.data.mongodb.command"; return "db.operation";
} }
} }
} }
} }

37
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservationCommandListener.java

@ -20,6 +20,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import com.mongodb.ConnectionString;
import com.mongodb.RequestContext; import com.mongodb.RequestContext;
import com.mongodb.event.CommandFailedEvent; import com.mongodb.event.CommandFailedEvent;
import com.mongodb.event.CommandListener; import com.mongodb.event.CommandListener;
@ -42,6 +43,7 @@ public class MongoObservationCommandListener implements CommandListener {
private static final Log log = LogFactory.getLog(MongoObservationCommandListener.class); private static final Log log = LogFactory.getLog(MongoObservationCommandListener.class);
private final ObservationRegistry observationRegistry; private final ObservationRegistry observationRegistry;
private final @Nullable ConnectionString connectionString;
private final MongoHandlerObservationConvention observationConvention = new DefaultMongoHandlerObservationConvention(); private final MongoHandlerObservationConvention observationConvention = new DefaultMongoHandlerObservationConvention();
@ -55,6 +57,23 @@ public class MongoObservationCommandListener implements CommandListener {
Assert.notNull(observationRegistry, "ObservationRegistry must not be null"); Assert.notNull(observationRegistry, "ObservationRegistry must not be null");
this.observationRegistry = observationRegistry; this.observationRegistry = observationRegistry;
this.connectionString = null;
}
/**
* Create a new {@link MongoObservationCommandListener} to record {@link Observation}s. This constructor attaches the
* {@link ConnectionString} to every {@link Observation}.
*
* @param observationRegistry must not be {@literal null}
* @param connectionString must not be {@literal null}
*/
public MongoObservationCommandListener(ObservationRegistry observationRegistry, ConnectionString connectionString) {
Assert.notNull(observationRegistry, "ObservationRegistry must not be null");
Assert.notNull(connectionString, "ConnectionString must not be null");
this.observationRegistry = observationRegistry;
this.connectionString = connectionString;
} }
@Override @Override
@ -82,7 +101,7 @@ public class MongoObservationCommandListener implements CommandListener {
log.debug("Found the following observation passed from the mongo context [" + parent + "]"); 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"); observationContext.setRemoteServiceName("mongo");
Observation observation = MongoObservation.MONGODB_COMMAND_OBSERVATION Observation observation = MongoObservation.MONGODB_COMMAND_OBSERVATION
@ -107,16 +126,18 @@ public class MongoObservationCommandListener implements CommandListener {
@Override @Override
public void commandSucceeded(CommandSucceededEvent event) { public void commandSucceeded(CommandSucceededEvent event) {
if (event.getRequestContext() == null) { RequestContext requestContext = event.getRequestContext();
if (requestContext == null) {
return; return;
} }
Observation observation = event.getRequestContext().getOrDefault(Observation.class, null); Observation observation = requestContext.getOrDefault(Observation.class, null);
if (observation == null) { if (observation == null) {
return; return;
} }
MongoHandlerContext context = event.getRequestContext().get(MongoHandlerContext.class); MongoHandlerContext context = requestContext.get(MongoHandlerContext.class);
context.setCommandSucceededEvent(event); context.setCommandSucceededEvent(event);
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
@ -129,16 +150,18 @@ public class MongoObservationCommandListener implements CommandListener {
@Override @Override
public void commandFailed(CommandFailedEvent event) { public void commandFailed(CommandFailedEvent event) {
if (event.getRequestContext() == null) { RequestContext requestContext = event.getRequestContext();
if (requestContext == null) {
return; return;
} }
Observation observation = event.getRequestContext().getOrDefault(Observation.class, null); Observation observation = requestContext.getOrDefault(Observation.class, null);
if (observation == null) { if (observation == null) {
return; return;
} }
MongoHandlerContext context = event.getRequestContext().get(MongoHandlerContext.class); MongoHandlerContext context = requestContext.get(MongoHandlerContext.class);
context.setCommandFailedEvent(event); context.setCommandFailedEvent(event);
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {

12
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java

@ -29,6 +29,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.ObservationRegistry;
import io.micrometer.tracing.exporter.FinishedSpan;
import io.micrometer.tracing.test.SampleTestRunner; import io.micrometer.tracing.test.SampleTestRunner;
/** /**
@ -72,6 +73,17 @@ public class ImperativeIntegrationTests extends SampleTestRunner {
repository.deleteAll(); repository.deleteAll();
System.out.println(((SimpleMeterRegistry) meterRegistry).getMetersAsString()); System.out.println(((SimpleMeterRegistry) meterRegistry).getMetersAsString());
assertThat(tracer.getFinishedSpans()).hasSize(5).extracting(FinishedSpan::getName).contains("person.delete",
"person.update", "person.find");
for (FinishedSpan span : tracer.getFinishedSpans()) {
assertThat(span.getTags()).containsEntry("db.system", "mongodb").containsEntry("net.transport", "IP.TCP");
assertThat(span.getTags()).containsKeys("db.connection_string", "db.name", "db.operation",
"db.mongodb.collection", "net.peer.name", "net.peer.port", "net.sock.peer.addr", "net.sock.peer.port");
}
}; };
} }
} }

152
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerForTracingTests.java

@ -1,152 +0,0 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.observability;
import org.bson.BsonDocument;
import org.bson.BsonString;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.data.mongodb.observability.MongoObservation.HighCardinalityCommandKeyNames;
import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames;
import com.mongodb.RequestContext;
import com.mongodb.ServerAddress;
import com.mongodb.client.SynchronousContextProvider;
import com.mongodb.connection.ClusterId;
import com.mongodb.connection.ConnectionDescription;
import com.mongodb.connection.ServerId;
import com.mongodb.event.CommandFailedEvent;
import com.mongodb.event.CommandStartedEvent;
import com.mongodb.event.CommandSucceededEvent;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
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
*/
class MongoObservationCommandListenerForTracingTests {
SimpleTracer simpleTracer;
MeterRegistry meterRegistry;
ObservationRegistry observationRegistry;
MongoObservationCommandListener listener;
@BeforeEach
void setup() {
this.simpleTracer = new SimpleTracer();
this.meterRegistry = new SimpleMeterRegistry();
this.observationRegistry = ObservationRegistry.create();
this.observationRegistry.observationConfig().observationHandler(new DefaultMeterObservationHandler(meterRegistry));
this.listener = new MongoObservationCommandListener(observationRegistry);
}
@Test
void successfullyCompletedCommandShouldCreateSpanWhenParentSampleInRequestContext() {
// given
RequestContext traceRequestContext = createTestRequestContextWithParentObservationAndStartIt();
// when
commandStartedAndSucceeded(traceRequestContext);
// then
assertThatMongoSpanIsClientWithTags().hasIpThatIsBlank().hasPortThatIsNotSet();
}
@Test
void commandWithErrorShouldCreateTimerWhenParentSampleInRequestContext() {
// given
RequestContext traceRequestContext = createTestRequestContextWithParentObservationAndStartIt();
// when
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.commandFailed( //
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 MapRequestContext}.
*/
@NotNull
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 MapRequestContext} in order to inject some test data.
*
* @param traceRequestContext
*/
private void commandStartedAndSucceeded(RequestContext traceRequestContext) {
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(traceRequestContext, 0, null, "insert", null, 0));
}
/**
* Create a base MongoDB-based {@link SpanAssert} using Micrometer Tracing's fluent API. Other test methods can apply
* additional assertions.
*
* @return
*/
private SpanAssert assertThatMongoSpanIsClientWithTags() {
return TracerAssert.assertThat(simpleTracer).onlySpan() //
.hasNameEqualTo("insert user") //
.hasKindEqualTo(Span.Kind.CLIENT) //
.hasRemoteServiceNameEqualTo("mongodb-database") //
.hasTag(HighCardinalityCommandKeyNames.MONGODB_COMMAND.asString(), "insert") //
.hasTag(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.asString(), "user") //
.hasTagWithKey(LowCardinalityCommandKeyNames.MONGODB_CLUSTER_ID.asString());
}
}

18
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java

@ -21,7 +21,6 @@ import org.bson.BsonDocument;
import org.bson.BsonString; import org.bson.BsonString;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.data.mongodb.observability.MongoObservation.HighCardinalityCommandKeyNames;
import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames; import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames;
import com.mongodb.RequestContext; import com.mongodb.RequestContext;
@ -91,7 +90,7 @@ class MongoObservationCommandListenerTests {
listener.commandStarted(new CommandStartedEvent(new MapRequestContext(), 0, null, "some name", "", null)); listener.commandStarted(new CommandStartedEvent(new MapRequestContext(), 0, null, "some name", "", null));
// then // then
assertThat(meterRegistry).hasNoMetrics(); assertThat(meterRegistry).hasMeterWithName("spring.data.mongodb.command.active");
} }
@Test @Test
@ -136,7 +135,6 @@ class MongoObservationCommandListenerTests {
assertThatTimerRegisteredWithTags(); assertThatTimerRegisteredWithTags();
} }
@Test @Test
void successfullyCompletedCommandWithoutClusterInformationShouldCreateTimerWhenParentSampleInRequestContext() { void successfullyCompletedCommandWithoutClusterInformationShouldCreateTimerWhenParentSampleInRequestContext() {
@ -149,9 +147,11 @@ class MongoObservationCommandListenerTests {
new BsonDocument("collection", new BsonString("user")))); new BsonDocument("collection", new BsonString("user"))));
listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0)); listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0));
// then assertThat(meterRegistry).hasTimerWithNameAndTags(MongoObservation.MONGODB_COMMAND_OBSERVATION.getName(),
assertThat(meterRegistry).hasTimerWithNameAndTags(HighCardinalityCommandKeyNames.MONGODB_COMMAND.asString(), KeyValues.of(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue("user"),
KeyValues.of(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue("user"))); LowCardinalityCommandKeyNames.DB_NAME.withValue("database"),
LowCardinalityCommandKeyNames.MONGODB_COMMAND.withValue("insert"),
LowCardinalityCommandKeyNames.DB_SYSTEM.withValue("mongodb")).and("error", "none"));
} }
@Test @Test
@ -183,10 +183,8 @@ class MongoObservationCommandListenerTests {
private void assertThatTimerRegisteredWithTags() { private void assertThatTimerRegisteredWithTags() {
assertThat(meterRegistry) // assertThat(meterRegistry) //
.hasTimerWithNameAndTags(HighCardinalityCommandKeyNames.MONGODB_COMMAND.asString(), .hasTimerWithNameAndTags(MongoObservation.MONGODB_COMMAND_OBSERVATION.getName(),
KeyValues.of(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue("user"))) // KeyValues.of(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue("user")));
.hasTimerWithNameAndTagKeys(HighCardinalityCommandKeyNames.MONGODB_COMMAND.asString(),
LowCardinalityCommandKeyNames.MONGODB_CLUSTER_ID.asString());
} }
} }

15
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ReactiveIntegrationTests.java

@ -28,6 +28,8 @@ import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import io.micrometer.observation.Observation; import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
import io.micrometer.tracing.exporter.FinishedSpan;
import io.micrometer.tracing.test.SampleTestRunner; import io.micrometer.tracing.test.SampleTestRunner;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
import reactor.util.context.Context; import reactor.util.context.Context;
@ -64,17 +66,24 @@ public class ReactiveIntegrationTests extends SampleTestRunner {
Observation intermediate = Observation.start("intermediate", createObservationRegistry()); Observation intermediate = Observation.start("intermediate", createObservationRegistry());
repository.deleteAll().then(repository.save(new Person("Dave", "Matthews", 42))) repository.deleteAll() //
.contextWrite(Context.of(Observation.class, intermediate)).as(StepVerifier::create).expectNextCount(1) .then(repository.save(new Person("Dave", "Matthews", 42))) //
.contextWrite(Context.of(ObservationThreadLocalAccessor.KEY, intermediate)) //
.as(StepVerifier::create).expectNextCount(1)//
.verifyComplete(); .verifyComplete();
repository.findByLastname("Matthews").contextWrite(Context.of(Observation.class, intermediate)) repository.findByLastname("Matthews") //
.contextWrite(Context.of(Observation.class, intermediate)) //
.as(StepVerifier::create).assertNext(actual -> { .as(StepVerifier::create).assertNext(actual -> {
assertThat(actual).extracting("firstname", "lastname").containsExactly("Dave", "Matthews"); assertThat(actual).extracting("firstname", "lastname").containsExactly("Dave", "Matthews");
}).verifyComplete(); }).verifyComplete();
intermediate.stop();
System.out.println(((SimpleMeterRegistry) meterRegistry).getMetersAsString()); System.out.println(((SimpleMeterRegistry) meterRegistry).getMetersAsString());
assertThat(tracer.getFinishedSpans()).hasSize(5).extracting(FinishedSpan::getName).contains("person.delete",
"person.update", "person.find");
}; };
} }
} }

10
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)); OBSERVATION_REGISTRY.observationConfig().observationHandler(new DefaultMeterObservationHandler(METER_REGISTRY));
} }
@Bean
MongoObservationCommandListener mongoObservationCommandListener(ObservationRegistry registry) {
return new MongoObservationCommandListener(registry);
}
@Bean @Bean
MongoDatabaseFactory mongoDatabaseFactory(MongoClientSettings settings) { MongoDatabaseFactory mongoDatabaseFactory(MongoClientSettings settings) {
return new SimpleMongoClientDatabaseFactory(MongoClients.create(settings), "observable"); return new SimpleMongoClientDatabaseFactory(MongoClients.create(settings), "observable");
@ -82,14 +77,13 @@ class TestConfig {
} }
@Bean @Bean
MongoClientSettings mongoClientSettings(MongoObservationCommandListener commandListener, MongoClientSettings mongoClientSettings(ObservationRegistry observationRegistry) {
ObservationRegistry observationRegistry) {
ConnectionString connectionString = new ConnectionString( ConnectionString connectionString = new ConnectionString(
String.format("mongodb://%s:%s/?w=majority&uuidrepresentation=javaLegacy", "127.0.0.1", 27017)); String.format("mongodb://%s:%s/?w=majority&uuidrepresentation=javaLegacy", "127.0.0.1", 27017));
MongoClientSettings settings = MongoClientSettings.builder() // MongoClientSettings settings = MongoClientSettings.builder() //
.addCommandListener(commandListener) // .addCommandListener(new MongoObservationCommandListener(observationRegistry, connectionString)) //
.contextProvider(ContextProviderFactory.create(observationRegistry)) // .contextProvider(ContextProviderFactory.create(observationRegistry)) //
.applyConnectionString(connectionString) // .applyConnectionString(connectionString) //
.build(); .build();

Loading…
Cancel
Save