15 changed files with 1790 additions and 401 deletions
@ -0,0 +1,78 @@ |
|||||||
|
/* |
||||||
|
* 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.common.Tag; |
||||||
|
import io.micrometer.common.Tags; |
||||||
|
|
||||||
|
import com.mongodb.connection.ConnectionDescription; |
||||||
|
import com.mongodb.connection.ConnectionId; |
||||||
|
import com.mongodb.event.CommandStartedEvent; |
||||||
|
|
||||||
|
/** |
||||||
|
* Default {@link MongoHandlerTagsProvider} implementation. |
||||||
|
* |
||||||
|
* @author Greg Turnquist |
||||||
|
* @since 4.0.0 |
||||||
|
*/ |
||||||
|
public class DefaultMongoHandlerTagsProvider implements MongoHandlerTagsProvider { |
||||||
|
|
||||||
|
@Override |
||||||
|
public Tags getLowCardinalityTags(MongoHandlerContext context) { |
||||||
|
|
||||||
|
Tags tags = Tags.empty(); |
||||||
|
|
||||||
|
if (context.getCollectionName() != null) { |
||||||
|
tags = tags.and(MongoObservation.LowCardinalityCommandTags.MONGODB_COLLECTION.of(context.getCollectionName())); |
||||||
|
} |
||||||
|
|
||||||
|
Tag connectionTag = connectionTag(context.getCommandStartedEvent()); |
||||||
|
if (connectionTag != null) { |
||||||
|
tags = tags.and(connectionTag); |
||||||
|
} |
||||||
|
|
||||||
|
return tags; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Tags getHighCardinalityTags(MongoHandlerContext context) { |
||||||
|
|
||||||
|
return Tags.of(MongoObservation.HighCardinalityCommandTags.MONGODB_COMMAND |
||||||
|
.of(context.getCommandStartedEvent().getCommandName())); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Extract connection details for a MongoDB connection into a {@link Tag}. |
||||||
|
* |
||||||
|
* @param event |
||||||
|
* @return |
||||||
|
*/ |
||||||
|
private static Tag connectionTag(CommandStartedEvent event) { |
||||||
|
|
||||||
|
ConnectionDescription connectionDescription = event.getConnectionDescription(); |
||||||
|
|
||||||
|
if (connectionDescription != null) { |
||||||
|
|
||||||
|
ConnectionId connectionId = connectionDescription.getConnectionId(); |
||||||
|
if (connectionId != null) { |
||||||
|
return MongoObservation.LowCardinalityCommandTags.MONGODB_CLUSTER_ID |
||||||
|
.of(connectionId.getServerId().getClusterId().getValue()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,134 @@ |
|||||||
|
/* |
||||||
|
* 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.Arrays; |
||||||
|
import java.util.LinkedHashSet; |
||||||
|
import java.util.Set; |
||||||
|
|
||||||
|
import org.bson.BsonDocument; |
||||||
|
import org.bson.BsonValue; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
|
||||||
|
import com.mongodb.RequestContext; |
||||||
|
import com.mongodb.event.CommandFailedEvent; |
||||||
|
import com.mongodb.event.CommandStartedEvent; |
||||||
|
import com.mongodb.event.CommandSucceededEvent; |
||||||
|
|
||||||
|
/** |
||||||
|
* A {@link Observation.Context} that contains MongoDB events. |
||||||
|
* |
||||||
|
* @author Marcin Grzejszczak |
||||||
|
* @author Greg Turnquist |
||||||
|
* @since 4.0.0 |
||||||
|
*/ |
||||||
|
public class MongoHandlerContext extends Observation.Context { |
||||||
|
|
||||||
|
/** |
||||||
|
* @see https://docs.mongodb.com/manual/reference/command for the command reference
|
||||||
|
*/ |
||||||
|
private static final Set<String> COMMANDS_WITH_COLLECTION_NAME = new LinkedHashSet<>( |
||||||
|
Arrays.asList("aggregate", "count", "distinct", "mapReduce", "geoSearch", "delete", "find", "findAndModify", |
||||||
|
"insert", "update", "collMod", "compact", "convertToCapped", "create", "createIndexes", "drop", "dropIndexes", |
||||||
|
"killCursors", "listIndexes", "reIndex")); |
||||||
|
|
||||||
|
private final CommandStartedEvent commandStartedEvent; |
||||||
|
private final RequestContext requestContext; |
||||||
|
private final String collectionName; |
||||||
|
|
||||||
|
private CommandSucceededEvent commandSucceededEvent; |
||||||
|
private CommandFailedEvent commandFailedEvent; |
||||||
|
|
||||||
|
public MongoHandlerContext(CommandStartedEvent commandStartedEvent, RequestContext requestContext) { |
||||||
|
|
||||||
|
this.commandStartedEvent = commandStartedEvent; |
||||||
|
this.requestContext = requestContext; |
||||||
|
this.collectionName = getCollectionName(commandStartedEvent); |
||||||
|
} |
||||||
|
|
||||||
|
public CommandStartedEvent getCommandStartedEvent() { |
||||||
|
return this.commandStartedEvent; |
||||||
|
} |
||||||
|
|
||||||
|
public RequestContext getRequestContext() { |
||||||
|
return this.requestContext; |
||||||
|
} |
||||||
|
|
||||||
|
public String getCollectionName() { |
||||||
|
return this.collectionName; |
||||||
|
} |
||||||
|
|
||||||
|
public String getContextualName() { |
||||||
|
|
||||||
|
if (this.collectionName == null) { |
||||||
|
return this.commandStartedEvent.getCommandName(); |
||||||
|
} |
||||||
|
|
||||||
|
return this.commandStartedEvent.getCommandName() + " " + this.collectionName; |
||||||
|
} |
||||||
|
|
||||||
|
public void setCommandSucceededEvent(CommandSucceededEvent commandSucceededEvent) { |
||||||
|
this.commandSucceededEvent = commandSucceededEvent; |
||||||
|
} |
||||||
|
|
||||||
|
public void setCommandFailedEvent(CommandFailedEvent commandFailedEvent) { |
||||||
|
this.commandFailedEvent = commandFailedEvent; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Transform the command name into a collection name; |
||||||
|
* |
||||||
|
* @param event the {@link CommandStartedEvent} |
||||||
|
* @return the name of the collection based on the command |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
private static String getCollectionName(CommandStartedEvent event) { |
||||||
|
|
||||||
|
String commandName = event.getCommandName(); |
||||||
|
BsonDocument command = event.getCommand(); |
||||||
|
|
||||||
|
if (COMMANDS_WITH_COLLECTION_NAME.contains(commandName)) { |
||||||
|
|
||||||
|
String collectionName = getNonEmptyBsonString(command.get(commandName)); |
||||||
|
|
||||||
|
if (collectionName != null) { |
||||||
|
return collectionName; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Some other commands, like getMore, have a field like {"collection": collectionName}.
|
||||||
|
return getNonEmptyBsonString(command.get("collection")); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Utility method to convert {@link BsonValue} into a plain string. |
||||||
|
* |
||||||
|
* @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) { |
||||||
|
|
||||||
|
if (bsonValue == null || !bsonValue.isString()) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
String stringValue = bsonValue.asString().getValue().trim(); |
||||||
|
|
||||||
|
return stringValue.isEmpty() ? null : stringValue; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,32 @@ |
|||||||
|
/* |
||||||
|
* 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; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link Observation.TagsProvider} for {@link MongoHandlerContext}. |
||||||
|
* |
||||||
|
* @author Greg Turnquist |
||||||
|
* @since 4.0.0 |
||||||
|
*/ |
||||||
|
public interface MongoHandlerTagsProvider extends Observation.TagsProvider<MongoHandlerContext> { |
||||||
|
|
||||||
|
@Override |
||||||
|
default boolean supportsContext(Observation.Context context) { |
||||||
|
return context instanceof MongoHandlerContext; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,97 @@ |
|||||||
|
/* |
||||||
|
* 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.common.docs.TagKey; |
||||||
|
import io.micrometer.observation.docs.DocumentedObservation; |
||||||
|
|
||||||
|
/** |
||||||
|
* A MongoDB-based {@link io.micrometer.observation.Observation}. |
||||||
|
* |
||||||
|
* @author Marcin Grzejszczak |
||||||
|
* @author Greg Turnquist |
||||||
|
* @since 1.0.0 |
||||||
|
*/ |
||||||
|
enum MongoObservation implements DocumentedObservation { |
||||||
|
|
||||||
|
/** |
||||||
|
* Timer created around a MongoDB command execution. |
||||||
|
*/ |
||||||
|
MONGODB_COMMAND_OBSERVATION { |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getName() { |
||||||
|
return "spring.data.mongodb.command"; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public TagKey[] getLowCardinalityTagKeys() { |
||||||
|
return LowCardinalityCommandTags.values(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public TagKey[] getHighCardinalityTagKeys() { |
||||||
|
return HighCardinalityCommandTags.values(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getPrefix() { |
||||||
|
return "spring.data.mongodb"; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Enums related to low cardinality tags for MongoDB commands. |
||||||
|
*/ |
||||||
|
enum LowCardinalityCommandTags implements TagKey { |
||||||
|
|
||||||
|
/** |
||||||
|
* MongoDB collection name. |
||||||
|
*/ |
||||||
|
MONGODB_COLLECTION { |
||||||
|
@Override |
||||||
|
public String getKey() { |
||||||
|
return "spring.data.mongodb.collection"; |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* MongoDB cluster identifier. |
||||||
|
*/ |
||||||
|
MONGODB_CLUSTER_ID { |
||||||
|
@Override |
||||||
|
public String getKey() { |
||||||
|
return "spring.data.mongodb.cluster_id"; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Enums related to high cardinality tags for MongoDB commands. |
||||||
|
*/ |
||||||
|
enum HighCardinalityCommandTags implements TagKey { |
||||||
|
|
||||||
|
/** |
||||||
|
* MongoDB command value. |
||||||
|
*/ |
||||||
|
MONGODB_COMMAND { |
||||||
|
@Override |
||||||
|
public String getKey() { |
||||||
|
return "spring.data.mongodb.command"; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,179 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2013-2022 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.data.mongodb.observability; |
||||||
|
|
||||||
|
import io.micrometer.observation.Observation; |
||||||
|
import io.micrometer.observation.ObservationRegistry; |
||||||
|
|
||||||
|
import org.apache.commons.logging.Log; |
||||||
|
import org.apache.commons.logging.LogFactory; |
||||||
|
|
||||||
|
import com.mongodb.RequestContext; |
||||||
|
import com.mongodb.event.CommandFailedEvent; |
||||||
|
import com.mongodb.event.CommandListener; |
||||||
|
import com.mongodb.event.CommandStartedEvent; |
||||||
|
import com.mongodb.event.CommandSucceededEvent; |
||||||
|
|
||||||
|
/** |
||||||
|
* Implement MongoDB's {@link CommandListener} using Micrometer's {@link Observation} API. |
||||||
|
* |
||||||
|
* @see https://github.com/openzipkin/brave/blob/release-5.13.0/instrumentation/mongodb/src/main/java/brave/mongodb/TraceMongoCommandListener.java
|
||||||
|
* @author OpenZipkin Brave Authors |
||||||
|
* @author Marcin Grzejszczak |
||||||
|
* @author Greg Turnquist |
||||||
|
* @since 4.0.0 |
||||||
|
*/ |
||||||
|
public final class MongoObservationCommandListener |
||||||
|
implements CommandListener, Observation.TagsProviderAware<MongoHandlerTagsProvider> { |
||||||
|
|
||||||
|
private static final Log log = LogFactory.getLog(MongoObservationCommandListener.class); |
||||||
|
|
||||||
|
private final ObservationRegistry observationRegistry; |
||||||
|
|
||||||
|
private MongoHandlerTagsProvider tagsProvider; |
||||||
|
|
||||||
|
public MongoObservationCommandListener(ObservationRegistry observationRegistry) { |
||||||
|
|
||||||
|
this.observationRegistry = observationRegistry; |
||||||
|
this.tagsProvider = new DefaultMongoHandlerTagsProvider(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void commandStarted(CommandStartedEvent event) { |
||||||
|
|
||||||
|
if (log.isDebugEnabled()) { |
||||||
|
log.debug("Instrumenting the command started event"); |
||||||
|
} |
||||||
|
|
||||||
|
String databaseName = event.getDatabaseName(); |
||||||
|
|
||||||
|
if ("admin".equals(databaseName)) { |
||||||
|
return; // don't instrument commands like "endSessions"
|
||||||
|
} |
||||||
|
|
||||||
|
RequestContext requestContext = event.getRequestContext(); |
||||||
|
|
||||||
|
if (requestContext == null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
Observation parent = observationFromContext(requestContext); |
||||||
|
|
||||||
|
if (log.isDebugEnabled()) { |
||||||
|
log.debug("Found the following observation passed from the mongo context [" + parent + "]"); |
||||||
|
} |
||||||
|
|
||||||
|
if (parent == null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
setupObservability(event, requestContext); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void commandSucceeded(CommandSucceededEvent event) { |
||||||
|
|
||||||
|
if (event.getRequestContext() == null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
Observation observation = event.getRequestContext().getOrDefault(Observation.class, null); |
||||||
|
if (observation == null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
MongoHandlerContext context = event.getRequestContext().get(MongoHandlerContext.class); |
||||||
|
context.setCommandSucceededEvent(event); |
||||||
|
|
||||||
|
if (log.isDebugEnabled()) { |
||||||
|
log.debug("Command succeeded - will stop observation [" + observation + "]"); |
||||||
|
} |
||||||
|
|
||||||
|
observation.stop(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void commandFailed(CommandFailedEvent event) { |
||||||
|
|
||||||
|
if (event.getRequestContext() == null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
Observation observation = event.getRequestContext().getOrDefault(Observation.class, null); |
||||||
|
if (observation == null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
MongoHandlerContext context = event.getRequestContext().get(MongoHandlerContext.class); |
||||||
|
context.setCommandFailedEvent(event); |
||||||
|
|
||||||
|
if (log.isDebugEnabled()) { |
||||||
|
log.debug("Command failed - will stop observation [" + observation + "]"); |
||||||
|
} |
||||||
|
|
||||||
|
observation.error(event.getThrowable()); |
||||||
|
observation.stop(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Extract the {@link Observation} from MongoDB's {@link RequestContext}. |
||||||
|
* |
||||||
|
* @param context |
||||||
|
* @return |
||||||
|
*/ |
||||||
|
private static Observation observationFromContext(RequestContext context) { |
||||||
|
|
||||||
|
Observation observation = context.getOrDefault(Observation.class, null); |
||||||
|
|
||||||
|
if (observation != null) { |
||||||
|
|
||||||
|
if (log.isDebugEnabled()) { |
||||||
|
log.debug("Found a observation in mongo context [" + observation + "]"); |
||||||
|
} |
||||||
|
return observation; |
||||||
|
} |
||||||
|
|
||||||
|
if (log.isDebugEnabled()) { |
||||||
|
log.debug("No observation was found - will not create any child spans"); |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
private void setupObservability(CommandStartedEvent event, RequestContext requestContext) { |
||||||
|
|
||||||
|
MongoHandlerContext observationContext = new MongoHandlerContext(event, requestContext); |
||||||
|
|
||||||
|
Observation observation = MongoObservation.MONGODB_COMMAND_OBSERVATION |
||||||
|
.observation(this.observationRegistry, observationContext) //
|
||||||
|
.contextualName(observationContext.getContextualName()) //
|
||||||
|
.tagsProvider(this.tagsProvider) //
|
||||||
|
.start(); |
||||||
|
|
||||||
|
requestContext.put(Observation.class, observation); |
||||||
|
requestContext.put(MongoHandlerContext.class, observationContext); |
||||||
|
|
||||||
|
if (log.isDebugEnabled()) { |
||||||
|
log.debug( |
||||||
|
"Created a child observation [" + observation + "] for mongo instrumentation and put it in mongo context"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void setTagsProvider(MongoHandlerTagsProvider mongoHandlerTagsProvider) { |
||||||
|
this.tagsProvider = mongoHandlerTagsProvider; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,117 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2013-2022 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.data.mongodb.observability; |
||||||
|
|
||||||
|
import io.micrometer.observation.Observation; |
||||||
|
import io.micrometer.tracing.Span; |
||||||
|
import io.micrometer.tracing.Tracer; |
||||||
|
import io.micrometer.tracing.handler.TracingObservationHandler; |
||||||
|
|
||||||
|
import java.net.InetSocketAddress; |
||||||
|
|
||||||
|
import org.apache.commons.logging.Log; |
||||||
|
import org.apache.commons.logging.LogFactory; |
||||||
|
|
||||||
|
import com.mongodb.MongoSocketException; |
||||||
|
import com.mongodb.connection.ConnectionDescription; |
||||||
|
import com.mongodb.event.CommandStartedEvent; |
||||||
|
|
||||||
|
/** |
||||||
|
* A {@link TracingObservationHandler} that handles {@link MongoHandlerContext}. It configures a span specific to Mongo |
||||||
|
* operations. |
||||||
|
* |
||||||
|
* @author Marcin Grzejszczak |
||||||
|
* @author Greg Turnquist |
||||||
|
* @since 4.0.0 |
||||||
|
*/ |
||||||
|
public class MongoTracingObservationHandler implements TracingObservationHandler<MongoHandlerContext> { |
||||||
|
|
||||||
|
private static final Log log = LogFactory.getLog(MongoTracingObservationHandler.class); |
||||||
|
|
||||||
|
private final Tracer tracer; |
||||||
|
|
||||||
|
private boolean setRemoteIpAndPortEnabled; |
||||||
|
|
||||||
|
public MongoTracingObservationHandler(Tracer tracer) { |
||||||
|
this.tracer = tracer; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Tracer getTracer() { |
||||||
|
return this.tracer; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onStart(MongoHandlerContext context) { |
||||||
|
|
||||||
|
CommandStartedEvent event = context.getCommandStartedEvent(); |
||||||
|
|
||||||
|
Span.Builder builder = this.tracer.spanBuilder() //
|
||||||
|
.name(context.getContextualName()) //
|
||||||
|
.kind(Span.Kind.CLIENT) //
|
||||||
|
.remoteServiceName("mongodb-" + event.getDatabaseName()); |
||||||
|
|
||||||
|
if (this.setRemoteIpAndPortEnabled) { |
||||||
|
|
||||||
|
ConnectionDescription connectionDescription = event.getConnectionDescription(); |
||||||
|
|
||||||
|
if (connectionDescription != null) { |
||||||
|
|
||||||
|
try { |
||||||
|
|
||||||
|
InetSocketAddress socketAddress = connectionDescription.getServerAddress().getSocketAddress(); |
||||||
|
builder.remoteIpAndPort(socketAddress.getAddress().getHostAddress(), socketAddress.getPort()); |
||||||
|
} catch (MongoSocketException e) { |
||||||
|
if (log.isDebugEnabled()) { |
||||||
|
log.debug("Ignored exception when setting remote ip and port", e); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
getTracingContext(context).setSpan(builder.start()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onStop(MongoHandlerContext context) { |
||||||
|
|
||||||
|
Span span = getRequiredSpan(context); |
||||||
|
tagSpan(context, span); |
||||||
|
|
||||||
|
context.getRequestContext().delete(Observation.class); |
||||||
|
context.getRequestContext().delete(MongoHandlerContext.class); |
||||||
|
|
||||||
|
span.end(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean supportsContext(Observation.Context context) { |
||||||
|
return context instanceof MongoHandlerContext; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Should remote ip and port be set on the span. |
||||||
|
* |
||||||
|
* @return {@code true} when the remote ip and port should be set |
||||||
|
*/ |
||||||
|
public boolean isSetRemoteIpAndPortEnabled() { |
||||||
|
return this.setRemoteIpAndPortEnabled; |
||||||
|
} |
||||||
|
|
||||||
|
public void setSetRemoteIpAndPortEnabled(boolean setRemoteIpAndPortEnabled) { |
||||||
|
this.setRemoteIpAndPortEnabled = setRemoteIpAndPortEnabled; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,170 @@ |
|||||||
|
/* |
||||||
|
* 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 io.micrometer.core.instrument.MeterRegistry; |
||||||
|
import io.micrometer.core.instrument.observation.TimerObservationHandler; |
||||||
|
import io.micrometer.core.instrument.simple.SimpleMeterRegistry; |
||||||
|
import io.micrometer.observation.Observation; |
||||||
|
import io.micrometer.observation.ObservationRegistry; |
||||||
|
import io.micrometer.tracing.Span; |
||||||
|
import io.micrometer.tracing.test.simple.SimpleTracer; |
||||||
|
import io.micrometer.tracing.test.simple.SpanAssert; |
||||||
|
import io.micrometer.tracing.test.simple.TracerAssert; |
||||||
|
|
||||||
|
import org.bson.BsonDocument; |
||||||
|
import org.bson.BsonString; |
||||||
|
import org.jetbrains.annotations.NotNull; |
||||||
|
import org.junit.jupiter.api.BeforeEach; |
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
import org.springframework.data.mongodb.observability.MongoObservation.HighCardinalityCommandTags; |
||||||
|
import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandTags; |
||||||
|
|
||||||
|
import com.mongodb.ServerAddress; |
||||||
|
import com.mongodb.connection.ClusterId; |
||||||
|
import com.mongodb.connection.ConnectionDescription; |
||||||
|
import com.mongodb.connection.ServerId; |
||||||
|
import com.mongodb.event.CommandFailedEvent; |
||||||
|
import com.mongodb.event.CommandStartedEvent; |
||||||
|
import com.mongodb.event.CommandSucceededEvent; |
||||||
|
|
||||||
|
/** |
||||||
|
* Series of test cases exercising {@link MongoObservationCommandListener} to ensure proper creation of {@link Span}s. |
||||||
|
* |
||||||
|
* @author Marcin Grzejszczak |
||||||
|
* @author Greg Turnquist |
||||||
|
* @since 4.0.0 |
||||||
|
*/ |
||||||
|
class MongoObservationCommandListenerForTracingTests { |
||||||
|
|
||||||
|
SimpleTracer simpleTracer; |
||||||
|
|
||||||
|
MongoTracingObservationHandler handler; |
||||||
|
|
||||||
|
MeterRegistry meterRegistry; |
||||||
|
ObservationRegistry observationRegistry; |
||||||
|
|
||||||
|
MongoObservationCommandListener listener; |
||||||
|
|
||||||
|
@BeforeEach |
||||||
|
void setup() { |
||||||
|
|
||||||
|
this.simpleTracer = new SimpleTracer(); |
||||||
|
this.handler = new MongoTracingObservationHandler(simpleTracer); |
||||||
|
|
||||||
|
this.meterRegistry = new SimpleMeterRegistry(); |
||||||
|
this.observationRegistry = ObservationRegistry.create(); |
||||||
|
this.observationRegistry.observationConfig().observationHandler(new TimerObservationHandler(meterRegistry)); |
||||||
|
this.observationRegistry.observationConfig().observationHandler(handler); |
||||||
|
|
||||||
|
this.listener = new MongoObservationCommandListener(observationRegistry); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void successfullyCompletedCommandShouldCreateSpanWhenParentSampleInRequestContext() { |
||||||
|
|
||||||
|
// given
|
||||||
|
TestRequestContext testRequestContext = createTestRequestContextWithParentObservationAndStartIt(); |
||||||
|
|
||||||
|
// when
|
||||||
|
commandStartedAndSucceeded(testRequestContext); |
||||||
|
|
||||||
|
// then
|
||||||
|
assertThatMongoSpanIsClientWithTags().hasIpThatIsBlank().hasPortThatIsNotSet(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void successfullyCompletedCommandShouldCreateSpanWithAddressInfoWhenParentSampleInRequestContextAndHandlerAddressInfoEnabled() { |
||||||
|
|
||||||
|
// given
|
||||||
|
handler.setSetRemoteIpAndPortEnabled(true); |
||||||
|
TestRequestContext testRequestContext = createTestRequestContextWithParentObservationAndStartIt(); |
||||||
|
|
||||||
|
// when
|
||||||
|
commandStartedAndSucceeded(testRequestContext); |
||||||
|
|
||||||
|
// then
|
||||||
|
assertThatMongoSpanIsClientWithTags().hasIpThatIsNotBlank().hasPortThatIsSet(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void commandWithErrorShouldCreateTimerWhenParentSampleInRequestContext() { |
||||||
|
|
||||||
|
// given
|
||||||
|
TestRequestContext testRequestContext = createTestRequestContextWithParentObservationAndStartIt(); |
||||||
|
|
||||||
|
// when
|
||||||
|
listener.commandStarted(new CommandStartedEvent(testRequestContext, 0, //
|
||||||
|
new ConnectionDescription( //
|
||||||
|
new ServerId( //
|
||||||
|
new ClusterId("description"), //
|
||||||
|
new ServerAddress("localhost", 1234))), //
|
||||||
|
"database", "insert", //
|
||||||
|
new BsonDocument("collection", new BsonString("user")))); |
||||||
|
listener.commandFailed( //
|
||||||
|
new CommandFailedEvent(testRequestContext, 0, null, "insert", 0, new IllegalAccessException())); |
||||||
|
|
||||||
|
// then
|
||||||
|
assertThatMongoSpanIsClientWithTags().assertThatThrowable().isInstanceOf(IllegalAccessException.class); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a parent {@link Observation} then wrap it inside a {@link TestRequestContext}. |
||||||
|
*/ |
||||||
|
@NotNull |
||||||
|
private TestRequestContext createTestRequestContextWithParentObservationAndStartIt() { |
||||||
|
|
||||||
|
Observation parent = Observation.start("name", observationRegistry); |
||||||
|
return TestRequestContext.withObservation(parent); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 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. |
||||||
|
* |
||||||
|
* @param testRequestContext |
||||||
|
*/ |
||||||
|
private void commandStartedAndSucceeded(TestRequestContext testRequestContext) { |
||||||
|
|
||||||
|
listener.commandStarted(new CommandStartedEvent(testRequestContext, 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)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 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(HighCardinalityCommandTags.MONGODB_COMMAND.getKey(), "insert") //
|
||||||
|
.hasTag(LowCardinalityCommandTags.MONGODB_COLLECTION.getKey(), "user") //
|
||||||
|
.hasTagWithKey(LowCardinalityCommandTags.MONGODB_CLUSTER_ID.getKey()); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,186 @@ |
|||||||
|
/* |
||||||
|
* 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 static io.micrometer.core.tck.MeterRegistryAssert.*; |
||||||
|
|
||||||
|
import io.micrometer.common.Tags; |
||||||
|
import io.micrometer.core.instrument.MeterRegistry; |
||||||
|
import io.micrometer.core.instrument.observation.TimerObservationHandler; |
||||||
|
import io.micrometer.core.instrument.simple.SimpleMeterRegistry; |
||||||
|
import io.micrometer.observation.Observation; |
||||||
|
import io.micrometer.observation.ObservationRegistry; |
||||||
|
|
||||||
|
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.HighCardinalityCommandTags; |
||||||
|
import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandTags; |
||||||
|
|
||||||
|
import com.mongodb.ServerAddress; |
||||||
|
import com.mongodb.connection.ClusterId; |
||||||
|
import com.mongodb.connection.ConnectionDescription; |
||||||
|
import com.mongodb.connection.ServerId; |
||||||
|
import com.mongodb.event.CommandFailedEvent; |
||||||
|
import com.mongodb.event.CommandStartedEvent; |
||||||
|
import com.mongodb.event.CommandSucceededEvent; |
||||||
|
|
||||||
|
/** |
||||||
|
* Series of test cases exercising {@link MongoObservationCommandListener}. |
||||||
|
* |
||||||
|
* @author Marcin Grzejszczak |
||||||
|
* @author Greg Turnquist |
||||||
|
* @since 4.0.0 |
||||||
|
*/ |
||||||
|
class MongoObservationCommandListenerTests { |
||||||
|
|
||||||
|
ObservationRegistry observationRegistry; |
||||||
|
MeterRegistry meterRegistry; |
||||||
|
|
||||||
|
MongoObservationCommandListener listener; |
||||||
|
|
||||||
|
@BeforeEach |
||||||
|
void setup() { |
||||||
|
|
||||||
|
this.meterRegistry = new SimpleMeterRegistry(); |
||||||
|
this.observationRegistry = ObservationRegistry.create(); |
||||||
|
this.observationRegistry.observationConfig().observationHandler(new TimerObservationHandler(meterRegistry)); |
||||||
|
|
||||||
|
this.listener = new MongoObservationCommandListener(observationRegistry); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void commandStartedShouldNotInstrumentWhenAdminDatabase() { |
||||||
|
|
||||||
|
// when
|
||||||
|
listener.commandStarted(new CommandStartedEvent(null, 0, null, "admin", "", null)); |
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(meterRegistry).hasNoMetrics(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void commandStartedShouldNotInstrumentWhenNoRequestContext() { |
||||||
|
|
||||||
|
// when
|
||||||
|
listener.commandStarted(new CommandStartedEvent(null, 0, null, "some name", "", null)); |
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(meterRegistry).hasNoMetrics(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void commandStartedShouldNotInstrumentWhenNoParentSampleInRequestContext() { |
||||||
|
|
||||||
|
// when
|
||||||
|
listener.commandStarted(new CommandStartedEvent(new TestRequestContext(), 0, null, "some name", "", null)); |
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(meterRegistry).hasNoMetrics(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void successfullyCompletedCommandShouldCreateTimerWhenParentSampleInRequestContext() { |
||||||
|
|
||||||
|
// given
|
||||||
|
Observation parent = Observation.start("name", observationRegistry); |
||||||
|
TestRequestContext testRequestContext = TestRequestContext.withObservation(parent); |
||||||
|
|
||||||
|
// when
|
||||||
|
listener.commandStarted(new CommandStartedEvent(testRequestContext, 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)); |
||||||
|
|
||||||
|
// then
|
||||||
|
assertThatTimerRegisteredWithTags(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void successfullyCompletedCommandWithCollectionHavingCommandNameShouldCreateTimerWhenParentSampleInRequestContext() { |
||||||
|
|
||||||
|
// given
|
||||||
|
Observation parent = Observation.start("name", observationRegistry); |
||||||
|
TestRequestContext testRequestContext = TestRequestContext.withObservation(parent); |
||||||
|
|
||||||
|
// when
|
||||||
|
listener.commandStarted(new CommandStartedEvent(testRequestContext, 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)); |
||||||
|
|
||||||
|
// then
|
||||||
|
assertThatTimerRegisteredWithTags(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void successfullyCompletedCommandWithoutClusterInformationShouldCreateTimerWhenParentSampleInRequestContext() { |
||||||
|
|
||||||
|
// given
|
||||||
|
Observation parent = Observation.start("name", observationRegistry); |
||||||
|
TestRequestContext testRequestContext = TestRequestContext.withObservation(parent); |
||||||
|
|
||||||
|
// when
|
||||||
|
listener.commandStarted(new CommandStartedEvent(testRequestContext, 0, null, "database", "insert", |
||||||
|
new BsonDocument("collection", new BsonString("user")))); |
||||||
|
listener.commandSucceeded(new CommandSucceededEvent(testRequestContext, 0, null, "insert", null, 0)); |
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(meterRegistry).hasTimerWithNameAndTags(HighCardinalityCommandTags.MONGODB_COMMAND.getKey(), |
||||||
|
Tags.of(LowCardinalityCommandTags.MONGODB_COLLECTION.of("user"))); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void commandWithErrorShouldCreateTimerWhenParentSampleInRequestContext() { |
||||||
|
|
||||||
|
// given
|
||||||
|
Observation parent = Observation.start("name", observationRegistry); |
||||||
|
TestRequestContext testRequestContext = TestRequestContext.withObservation(parent); |
||||||
|
|
||||||
|
// when
|
||||||
|
listener.commandStarted(new CommandStartedEvent(testRequestContext, 0, //
|
||||||
|
new ConnectionDescription( //
|
||||||
|
new ServerId( //
|
||||||
|
new ClusterId("description"), //
|
||||||
|
new ServerAddress("localhost", 1234))), //
|
||||||
|
"database", "insert", //
|
||||||
|
new BsonDocument("collection", new BsonString("user")))); |
||||||
|
listener.commandFailed( //
|
||||||
|
new CommandFailedEvent(testRequestContext, 0, null, "insert", 0, new IllegalAccessException())); |
||||||
|
|
||||||
|
// then
|
||||||
|
assertThatTimerRegisteredWithTags(); |
||||||
|
} |
||||||
|
|
||||||
|
private void assertThatTimerRegisteredWithTags() { |
||||||
|
|
||||||
|
assertThat(meterRegistry) //
|
||||||
|
.hasTimerWithNameAndTags(HighCardinalityCommandTags.MONGODB_COMMAND.getKey(), |
||||||
|
Tags.of(LowCardinalityCommandTags.MONGODB_COLLECTION.getKey(), "user")) //
|
||||||
|
.hasTimerWithNameAndTagKeys(HighCardinalityCommandTags.MONGODB_COMMAND.getKey(), |
||||||
|
LowCardinalityCommandTags.MONGODB_CLUSTER_ID.getKey()); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,78 @@ |
|||||||
|
/* |
||||||
|
* 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 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(); |
||||||
|
} |
||||||
|
|
||||||
|
static TestRequestContext withObservation(Observation value) { |
||||||
|
|
||||||
|
TestRequestContext testRequestContext = new TestRequestContext(); |
||||||
|
testRequestContext.put(Observation.class, value); |
||||||
|
return testRequestContext; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,198 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2013-2022 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.data.mongodb.observability; |
||||||
|
|
||||||
|
import static org.springframework.data.mongodb.test.util.Assertions.*; |
||||||
|
|
||||||
|
import io.micrometer.core.instrument.MeterRegistry; |
||||||
|
import io.micrometer.core.instrument.observation.TimerObservationHandler; |
||||||
|
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.test.SampleTestRunner; |
||||||
|
import io.micrometer.tracing.test.reporter.BuildingBlocks; |
||||||
|
|
||||||
|
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; |
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
import org.springframework.core.io.ClassPathResource; |
||||||
|
import org.springframework.data.mongodb.MongoDatabaseFactory; |
||||||
|
import org.springframework.data.mongodb.core.MongoOperations; |
||||||
|
import org.springframework.data.mongodb.core.MongoTemplate; |
||||||
|
import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory; |
||||||
|
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; |
||||||
|
import org.springframework.data.mongodb.core.convert.MappingMongoConverter; |
||||||
|
import org.springframework.data.mongodb.core.convert.MongoConverter; |
||||||
|
import org.springframework.data.mongodb.core.mapping.MongoMappingContext; |
||||||
|
import org.springframework.data.mongodb.repository.Person; |
||||||
|
import org.springframework.data.mongodb.repository.PersonRepository; |
||||||
|
import org.springframework.data.mongodb.repository.SampleEvaluationContextExtension; |
||||||
|
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; |
||||||
|
import org.springframework.data.mongodb.repository.support.MongoRepositoryFactoryBean; |
||||||
|
import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; |
||||||
|
import org.springframework.test.context.ContextConfiguration; |
||||||
|
import org.springframework.test.context.junit.jupiter.SpringExtension; |
||||||
|
|
||||||
|
import com.mongodb.ConnectionString; |
||||||
|
import com.mongodb.MongoClientSettings; |
||||||
|
import com.mongodb.RequestContext; |
||||||
|
import com.mongodb.WriteConcern; |
||||||
|
import com.mongodb.client.MongoClients; |
||||||
|
import com.mongodb.client.SynchronousContextProvider; |
||||||
|
|
||||||
|
/** |
||||||
|
* Collection of tests that log metrics and tracing with an external tracing tool. Since this external tool must be up |
||||||
|
* and running after the test is completed, this test is ONLY run manually. Needed: |
||||||
|
* {@code docker run -p 9411:9411 openzipkin/zipkin} and {@code docker run -p 27017:27017 mongo:latest} (either from |
||||||
|
* Docker Desktop or within separate shells). |
||||||
|
* |
||||||
|
* @author Greg Turnquist |
||||||
|
* @since 4.0.0 |
||||||
|
*/ |
||||||
|
@Disabled("Run this manually to visually test spans in Zipkin") |
||||||
|
@ExtendWith(SpringExtension.class) |
||||||
|
@ContextConfiguration |
||||||
|
public class ZipkinIntegrationTests extends SampleTestRunner { |
||||||
|
|
||||||
|
private static final MeterRegistry METER_REGISTRY = new SimpleMeterRegistry(); |
||||||
|
private static final ObservationRegistry OBSERVATION_REGISTRY = ObservationRegistry.create(); |
||||||
|
|
||||||
|
static { |
||||||
|
OBSERVATION_REGISTRY.observationConfig().observationHandler(new TimerObservationHandler(METER_REGISTRY)); |
||||||
|
} |
||||||
|
|
||||||
|
@Autowired PersonRepository repository; |
||||||
|
|
||||||
|
ZipkinIntegrationTests() { |
||||||
|
super(SampleRunnerConfig.builder().build(), OBSERVATION_REGISTRY, METER_REGISTRY); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public BiConsumer<BuildingBlocks, Deque<ObservationHandler>> customizeObservationHandlers() { |
||||||
|
|
||||||
|
return (buildingBlocks, observationHandlers) -> { |
||||||
|
observationHandlers.addLast(new MongoTracingObservationHandler(buildingBlocks.getTracer())); |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public TracingSetup[] getTracingSetup() { |
||||||
|
return new TracingSetup[] { TracingSetup.ZIPKIN_BRAVE }; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public SampleTestRunnerConsumer yourCode() { |
||||||
|
|
||||||
|
return (tracer, meterRegistry) -> { |
||||||
|
|
||||||
|
repository.deleteAll(); |
||||||
|
repository.save(new Person("Dave", "Matthews", 42)); |
||||||
|
List<Person> people = repository.findByLastname("Matthews"); |
||||||
|
|
||||||
|
assertThat(people).hasSize(1); |
||||||
|
assertThat(people.get(0)).extracting("firstname", "lastname").containsExactly("Dave", "Matthews"); |
||||||
|
|
||||||
|
repository.deleteAll(); |
||||||
|
|
||||||
|
System.out.println(((SimpleMeterRegistry) meterRegistry).getMetersAsString()); |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
@Configuration |
||||||
|
@EnableMongoRepositories |
||||||
|
static class TestConfig { |
||||||
|
|
||||||
|
@Bean |
||||||
|
MongoObservationCommandListener mongoObservationCommandListener(ObservationRegistry registry) { |
||||||
|
return new MongoObservationCommandListener(registry); |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
MongoDatabaseFactory mongoDatabaseFactory(MongoObservationCommandListener commandListener, |
||||||
|
ObservationRegistry registry) { |
||||||
|
|
||||||
|
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"); |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
MappingMongoConverter mongoConverter(MongoDatabaseFactory factory) { |
||||||
|
|
||||||
|
MongoMappingContext mappingContext = new MongoMappingContext(); |
||||||
|
mappingContext.afterPropertiesSet(); |
||||||
|
|
||||||
|
return new MappingMongoConverter(new DefaultDbRefResolver(factory), mappingContext); |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
MongoTemplate mongoTemplate(MongoDatabaseFactory mongoDatabaseFactory, MongoConverter mongoConverter) { |
||||||
|
|
||||||
|
MongoTemplate template = new MongoTemplate(mongoDatabaseFactory, mongoConverter); |
||||||
|
template.setWriteConcern(WriteConcern.JOURNALED); |
||||||
|
return template; |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
public PropertiesFactoryBean namedQueriesProperties() { |
||||||
|
|
||||||
|
PropertiesFactoryBean bean = new PropertiesFactoryBean(); |
||||||
|
bean.setLocation(new ClassPathResource("META-INF/mongo-named-queries.properties")); |
||||||
|
return bean; |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
MongoRepositoryFactoryBean<PersonRepository, Person, String> repositoryFactoryBean(MongoOperations operations, |
||||||
|
PropertiesFactoryBean namedQueriesProperties) throws IOException { |
||||||
|
|
||||||
|
MongoRepositoryFactoryBean<PersonRepository, Person, String> factoryBean = new MongoRepositoryFactoryBean<>( |
||||||
|
PersonRepository.class); |
||||||
|
factoryBean.setMongoOperations(operations); |
||||||
|
factoryBean.setNamedQueries(new PropertiesBasedNamedQueries(namedQueriesProperties.getObject())); |
||||||
|
factoryBean.setCreateIndexesForQueryMethods(true); |
||||||
|
return factoryBean; |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
SampleEvaluationContextExtension contextExtension() { |
||||||
|
return new SampleEvaluationContextExtension(); |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
ObservationRegistry registry() { |
||||||
|
return OBSERVATION_REGISTRY; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue