@@ -31,7 +31,6 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -105,9 +104,6 @@ public class ContextProviderFactory {
|
||||
|
||||
Map<Object, Object> map = cs.currentContext().stream()
|
||||
.collect(Collectors.toConcurrentMap(Entry::getKey, Entry::getValue));
|
||||
if (map.containsKey(ObservationThreadLocalAccessor.KEY)) {
|
||||
map.put(Observation.class, map.get(ObservationThreadLocalAccessor.KEY));
|
||||
}
|
||||
|
||||
return new MapRequestContext(map);
|
||||
}
|
||||
|
||||
@@ -15,39 +15,84 @@
|
||||
*/
|
||||
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.lang.Nullable;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
import com.mongodb.ConnectionString;
|
||||
import com.mongodb.ServerAddress;
|
||||
import com.mongodb.connection.ConnectionDescription;
|
||||
import com.mongodb.connection.ConnectionId;
|
||||
import com.mongodb.event.CommandStartedEvent;
|
||||
|
||||
import io.micrometer.common.KeyValue;
|
||||
import io.micrometer.common.KeyValues;
|
||||
|
||||
/**
|
||||
* Default {@link MongoHandlerObservationConvention} implementation.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 4
|
||||
* @author Mark Paluch
|
||||
* @since 4.0
|
||||
*/
|
||||
class DefaultMongoHandlerObservationConvention implements MongoHandlerObservationConvention {
|
||||
|
||||
@Override
|
||||
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())) {
|
||||
keyValues = keyValues
|
||||
.and(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue(context.getCollectionName()));
|
||||
}
|
||||
|
||||
KeyValue connectionTag = connectionTag(context.getCommandStartedEvent());
|
||||
if (connectionTag != null) {
|
||||
keyValues = keyValues.and(connectionTag);
|
||||
ConnectionDescription connectionDescription = context.getCommandStartedEvent().getConnectionDescription();
|
||||
|
||||
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;
|
||||
@@ -55,36 +100,20 @@ class DefaultMongoHandlerObservationConvention implements MongoHandlerObservatio
|
||||
|
||||
@Override
|
||||
public KeyValues getHighCardinalityKeyValues(MongoHandlerContext context) {
|
||||
|
||||
return KeyValues.of(
|
||||
HighCardinalityCommandKeyNames.MONGODB_COMMAND.withValue(context.getCommandStartedEvent().getCommandName()));
|
||||
return KeyValues.empty();
|
||||
}
|
||||
|
||||
@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) {
|
||||
String collectionName = context.getCollectionName();
|
||||
CommandStartedEvent commandStartedEvent = context.getCommandStartedEvent();
|
||||
|
||||
ConnectionDescription connectionDescription = event.getConnectionDescription();
|
||||
|
||||
if (connectionDescription != null) {
|
||||
|
||||
ConnectionId connectionId = connectionDescription.getConnectionId();
|
||||
if (connectionId != null) {
|
||||
return LowCardinalityCommandKeyNames.MONGODB_CLUSTER_ID
|
||||
.withValue(connectionId.getServerId().getClusterId().getValue());
|
||||
}
|
||||
if (ObjectUtils.isEmpty(collectionName)) {
|
||||
return commandStartedEvent.getCommandName();
|
||||
}
|
||||
|
||||
return null;
|
||||
return collectionName + "." + commandStartedEvent.getCommandName();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.bson.BsonDocument;
|
||||
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 @@ import io.micrometer.observation.transport.SenderContext;
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @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=
|
||||
@@ -51,6 +53,7 @@ public class MongoHandlerContext extends SenderContext<Object> {
|
||||
"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<Object> {
|
||||
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,21 @@ public class MongoHandlerContext extends SenderContext<Object> {
|
||||
return this.requestContext;
|
||||
}
|
||||
|
||||
public String getDatabaseName() {
|
||||
return commandStartedEvent.getDatabaseName();
|
||||
}
|
||||
|
||||
public String getCollectionName() {
|
||||
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) {
|
||||
@@ -116,7 +126,7 @@ public class MongoHandlerContext extends SenderContext<Object> {
|
||||
}
|
||||
|
||||
// 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
|
||||
*/
|
||||
@Nullable
|
||||
private static String getNonEmptyBsonString(BsonValue bsonValue) {
|
||||
private static String getNonEmptyBsonString(@Nullable BsonValue bsonValue) {
|
||||
|
||||
if (bsonValue == null || !bsonValue.isString()) {
|
||||
return null;
|
||||
@@ -135,4 +145,5 @@ public class MongoHandlerContext extends SenderContext<Object> {
|
||||
|
||||
return stringValue.isEmpty() ? null : stringValue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -44,13 +44,9 @@ enum MongoObservation implements ObservationDocumentation {
|
||||
|
||||
@Override
|
||||
public KeyName[] getHighCardinalityKeyNames() {
|
||||
return HighCardinalityCommandKeyNames.values();
|
||||
return new KeyName[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPrefix() {
|
||||
return "spring.data.mongodb";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -58,13 +54,103 @@ enum MongoObservation implements ObservationDocumentation {
|
||||
*/
|
||||
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";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 "spring.data.mongodb.collection";
|
||||
return "db.mongodb.collection";
|
||||
}
|
||||
},
|
||||
|
||||
@@ -76,13 +162,7 @@ enum MongoObservation implements ObservationDocumentation {
|
||||
public String asString() {
|
||||
return "spring.data.mongodb.cluster_id";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enums related to high cardinality key names for MongoDB commands.
|
||||
*/
|
||||
enum HighCardinalityCommandKeyNames implements KeyName {
|
||||
},
|
||||
|
||||
/**
|
||||
* MongoDB command value.
|
||||
@@ -90,8 +170,9 @@ enum MongoObservation implements ObservationDocumentation {
|
||||
MONGODB_COMMAND {
|
||||
@Override
|
||||
public String asString() {
|
||||
return "spring.data.mongodb.command";
|
||||
return "db.operation";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.mongodb.ConnectionString;
|
||||
import com.mongodb.RequestContext;
|
||||
import com.mongodb.event.CommandFailedEvent;
|
||||
import com.mongodb.event.CommandListener;
|
||||
@@ -42,6 +43,7 @@ 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();
|
||||
|
||||
@@ -55,6 +57,23 @@ public class MongoObservationCommandListener implements CommandListener {
|
||||
Assert.notNull(observationRegistry, "ObservationRegistry must not be null");
|
||||
|
||||
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
|
||||
@@ -82,7 +101,7 @@ public class MongoObservationCommandListener implements CommandListener {
|
||||
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
|
||||
@@ -107,16 +126,18 @@ public class MongoObservationCommandListener implements CommandListener {
|
||||
@Override
|
||||
public void commandSucceeded(CommandSucceededEvent event) {
|
||||
|
||||
if (event.getRequestContext() == null) {
|
||||
RequestContext requestContext = event.getRequestContext();
|
||||
|
||||
if (requestContext == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Observation observation = event.getRequestContext().getOrDefault(Observation.class, null);
|
||||
Observation observation = requestContext.getOrDefault(Observation.class, null);
|
||||
if (observation == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
MongoHandlerContext context = event.getRequestContext().get(MongoHandlerContext.class);
|
||||
MongoHandlerContext context = requestContext.get(MongoHandlerContext.class);
|
||||
context.setCommandSucceededEvent(event);
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
@@ -129,16 +150,18 @@ public class MongoObservationCommandListener implements CommandListener {
|
||||
@Override
|
||||
public void commandFailed(CommandFailedEvent event) {
|
||||
|
||||
if (event.getRequestContext() == null) {
|
||||
RequestContext requestContext = event.getRequestContext();
|
||||
|
||||
if (requestContext == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Observation observation = event.getRequestContext().getOrDefault(Observation.class, null);
|
||||
Observation observation = requestContext.getOrDefault(Observation.class, null);
|
||||
if (observation == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
MongoHandlerContext context = event.getRequestContext().get(MongoHandlerContext.class);
|
||||
MongoHandlerContext context = requestContext.get(MongoHandlerContext.class);
|
||||
context.setCommandFailedEvent(event);
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
|
||||
@@ -29,6 +29,7 @@ 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.exporter.FinishedSpan;
|
||||
import io.micrometer.tracing.test.SampleTestRunner;
|
||||
|
||||
/**
|
||||
@@ -72,6 +73,17 @@ public class ImperativeIntegrationTests extends SampleTestRunner {
|
||||
repository.deleteAll();
|
||||
|
||||
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");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import org.bson.BsonDocument;
|
||||
import org.bson.BsonString;
|
||||
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;
|
||||
@@ -91,7 +90,7 @@ class MongoObservationCommandListenerTests {
|
||||
listener.commandStarted(new CommandStartedEvent(new MapRequestContext(), 0, null, "some name", "", null));
|
||||
|
||||
// then
|
||||
assertThat(meterRegistry).hasNoMetrics();
|
||||
assertThat(meterRegistry).hasMeterWithName("spring.data.mongodb.command.active");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -136,7 +135,6 @@ class MongoObservationCommandListenerTests {
|
||||
assertThatTimerRegisteredWithTags();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void successfullyCompletedCommandWithoutClusterInformationShouldCreateTimerWhenParentSampleInRequestContext() {
|
||||
|
||||
@@ -149,9 +147,11 @@ class MongoObservationCommandListenerTests {
|
||||
new BsonDocument("collection", new BsonString("user"))));
|
||||
listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0));
|
||||
|
||||
// then
|
||||
assertThat(meterRegistry).hasTimerWithNameAndTags(HighCardinalityCommandKeyNames.MONGODB_COMMAND.asString(),
|
||||
KeyValues.of(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue("user")));
|
||||
assertThat(meterRegistry).hasTimerWithNameAndTags(MongoObservation.MONGODB_COMMAND_OBSERVATION.getName(),
|
||||
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
|
||||
@@ -183,10 +183,8 @@ class MongoObservationCommandListenerTests {
|
||||
private void assertThatTimerRegisteredWithTags() {
|
||||
|
||||
assertThat(meterRegistry) //
|
||||
.hasTimerWithNameAndTags(HighCardinalityCommandKeyNames.MONGODB_COMMAND.asString(),
|
||||
KeyValues.of(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue("user"))) //
|
||||
.hasTimerWithNameAndTagKeys(HighCardinalityCommandKeyNames.MONGODB_COMMAND.asString(),
|
||||
LowCardinalityCommandKeyNames.MONGODB_CLUSTER_ID.asString());
|
||||
.hasTimerWithNameAndTags(MongoObservation.MONGODB_COMMAND_OBSERVATION.getName(),
|
||||
KeyValues.of(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue("user")));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ 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.observation.contextpropagation.ObservationThreadLocalAccessor;
|
||||
import io.micrometer.tracing.exporter.FinishedSpan;
|
||||
import io.micrometer.tracing.test.SampleTestRunner;
|
||||
import reactor.test.StepVerifier;
|
||||
import reactor.util.context.Context;
|
||||
@@ -64,17 +66,24 @@ public class ReactiveIntegrationTests extends SampleTestRunner {
|
||||
|
||||
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(ObservationThreadLocalAccessor.KEY, 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());
|
||||
|
||||
assertThat(tracer.getFinishedSpans()).hasSize(5).extracting(FinishedSpan::getName).contains("person.delete",
|
||||
"person.update", "person.find");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 @@ class TestConfig {
|
||||
}
|
||||
|
||||
@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();
|
||||
|
||||
Reference in New Issue
Block a user