diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/DefaultMongoCommandTagsProvider.java b/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/DefaultMongoCommandTagsProvider.java index 0ed99e6dc4..f30af00fdc 100644 --- a/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/DefaultMongoCommandTagsProvider.java +++ b/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/DefaultMongoCommandTagsProvider.java @@ -15,23 +15,16 @@ */ package io.micrometer.binder.mongodb; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - import com.mongodb.event.CommandEvent; import com.mongodb.event.CommandStartedEvent; import com.mongodb.event.CommandSucceededEvent; import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Tags; -import io.micrometer.core.instrument.util.StringUtils; import io.micrometer.core.util.internal.logging.WarnThenDebugLogger; -import org.bson.BsonDocument; -import org.bson.BsonString; -import org.bson.BsonValue; + +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; /** * Default implementation for {@link MongoCommandTagsProvider}. @@ -41,21 +34,17 @@ */ public class DefaultMongoCommandTagsProvider implements MongoCommandTagsProvider { - // See https://docs.mongodb.com/manual/reference/command for the command reference - private static final Set COMMANDS_WITH_COLLECTION_NAME = new HashSet<>(Arrays.asList( - "aggregate", "count", "distinct", "mapReduce", "geoSearch", "delete", "find", "findAndModify", - "insert", "update", "collMod", "compact", "convertToCapped", "create", "createIndexes", "drop", - "dropIndexes", "killCursors", "listIndexes", "reIndex")); - private static final WarnThenDebugLogger WARN_THEN_DEBUG_LOGGER = new WarnThenDebugLogger(DefaultMongoCommandTagsProvider.class); - private final ConcurrentMap inFlightCommandCollectionNames = new ConcurrentHashMap<>(); + private final ConcurrentMap inFlightCommandStartedEventTags = new ConcurrentHashMap<>(); @Override public Iterable commandTags(CommandEvent event) { + Optional mongoCommandStartedEventTags = Optional.ofNullable(inFlightCommandStartedEventTags.remove(event.getRequestId())); return Tags.of( Tag.of("command", event.getCommandName()), - Tag.of("collection", getAndRemoveCollectionNameForCommand(event)), + Tag.of("database", mongoCommandStartedEventTags.map(MongoCommandStartedEventTags::getDatabase).orElse("unknown")), + Tag.of("collection", mongoCommandStartedEventTags.map(MongoCommandStartedEventTags::getCollection).orElse("unknown")), Tag.of("cluster.id", event.getConnectionDescription().getConnectionId().getServerId().getClusterId().getValue()), Tag.of("server.address", event.getConnectionDescription().getServerAddress().toString()), Tag.of("status", (event instanceof CommandSucceededEvent) ? "SUCCESS" : "FAILED")); @@ -63,58 +52,16 @@ public Iterable commandTags(CommandEvent event) { @Override public void commandStarted(CommandStartedEvent event) { - determineCollectionName(event.getCommandName(), event.getCommand()) - .ifPresent(collectionName -> addCollectionNameForCommand(event, collectionName)); + MongoCommandStartedEventTags tags = new MongoCommandStartedEventTags(event); + addTagsForStartedCommandEvent(event, tags); } - private void addCollectionNameForCommand(CommandEvent event, String collectionName) { - if (inFlightCommandCollectionNames.size() < 1000) { - inFlightCommandCollectionNames.put(event.getRequestId(), collectionName); + private void addTagsForStartedCommandEvent(CommandEvent event, MongoCommandStartedEventTags tags) { + if (inFlightCommandStartedEventTags.size() < 1000) { + inFlightCommandStartedEventTags.put(event.getRequestId(), tags); return; } // Cache over capacity WARN_THEN_DEBUG_LOGGER.log("Collection names cache is full - Mongo is not calling listeners properly"); } - - private String getAndRemoveCollectionNameForCommand(CommandEvent event) { - String collectionName = inFlightCommandCollectionNames.remove(event.getRequestId()); - return collectionName != null ? collectionName : "unknown"; - } - - /** - * Attempts to determine the name of the collection a command is operating on. - * - *

Because some commands either do not have collection info or it is problematic to determine the collection info, - * there is an allow list of command names {@code COMMANDS_WITH_COLLECTION_NAME} used. If {@code commandName} is - * not in the allow list or there is no collection info in {@code command}, it will use the content of the - * {@code 'collection'} field on {@code command}, if it exists. - * - *

Taken from TraceMongoCommandListener.java in Brave - * - * @param commandName name of the mongo command - * @param command mongo command object - * @return optional collection name or empty if could not be determined or not in the allow list of command names - */ - protected Optional determineCollectionName(String commandName, BsonDocument command) { - if (COMMANDS_WITH_COLLECTION_NAME.contains(commandName)) { - Optional collectionName = getNonEmptyBsonString(command.get(commandName)); - if (collectionName.isPresent()) { - return collectionName; - } - } - // Some other commands, like getMore, have a field like {"collection": collectionName}. - return getNonEmptyBsonString(command.get("collection")); - } - - /** - * @return trimmed string from {@code bsonValue} in the Optional or empty Optional if value was not a non-empty string - */ - private Optional getNonEmptyBsonString(BsonValue bsonValue) { - return Optional.ofNullable(bsonValue) - .filter(BsonValue::isString) - .map(BsonValue::asString) - .map(BsonString::getValue) - .map(String::trim) - .filter(StringUtils::isNotEmpty); - } } diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/MongoCommandStartedEventTags.java b/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/MongoCommandStartedEventTags.java new file mode 100644 index 0000000000..69e4a291fb --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/MongoCommandStartedEventTags.java @@ -0,0 +1,93 @@ +/* + * Copyright 2021 VMware, Inc. + * + * 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 io.micrometer.binder.mongodb; + +import com.mongodb.event.CommandStartedEvent; +import io.micrometer.core.instrument.util.StringUtils; +import org.bson.BsonDocument; +import org.bson.BsonString; +import org.bson.BsonValue; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +class MongoCommandStartedEventTags { + + // See https://docs.mongodb.com/manual/reference/command for the command reference + private static final Set COMMANDS_WITH_COLLECTION_NAME = new HashSet<>(Arrays.asList( + "aggregate", "count", "distinct", "mapReduce", "geoSearch", "delete", "find", "findAndModify", + "insert", "update", "collMod", "compact", "convertToCapped", "create", "createIndexes", "drop", + "dropIndexes", "killCursors", "listIndexes", "reIndex")); + public static final String UNKNOWN = "unknown"; + + public MongoCommandStartedEventTags(CommandStartedEvent event) { + this.database = event.getDatabaseName(); + this.collection = this.determineCollectionName(event.getCommandName(), event.getCommand()) + .orElse(UNKNOWN); + } + + private final String collection; + private final String database; + + public String getDatabase() { + return database; + } + + public String getCollection() { + return collection; + } + + /** + * Attempts to determine the name of the collection a command is operating on. + * + *

Because some commands either do not have collection info or it is problematic to determine the collection info, + * there is an allow list of command names {@code COMMANDS_WITH_COLLECTION_NAME} used. If {@code commandName} is + * not in the allow list or there is no collection info in {@code command}, it will use the content of the + * {@code 'collection'} field on {@code command}, if it exists. + * + *

Taken from TraceMongoCommandListener.java in Brave + * + * @param commandName name of the mongo command + * @param command mongo command object + * @return optional collection name or empty if could not be determined or not in the allow list of command names + */ + private Optional determineCollectionName(String commandName, BsonDocument command) { + Optional collectionName = Optional.ofNullable(commandName) + .filter(COMMANDS_WITH_COLLECTION_NAME::contains) + .map(command::get) + .flatMap(this::getNonEmptyBsonString); + + if (collectionName.isPresent()) { + return collectionName; + } + + return getNonEmptyBsonString(command.get("collection")); + } + + /** + * @return trimmed string from {@code bsonValue} in the Optional or empty Optional if value was not a non-empty string + */ + private Optional getNonEmptyBsonString(BsonValue bsonValue) { + return Optional.ofNullable(bsonValue) + .filter(BsonValue::isString) + .map(BsonValue::asString) + .map(BsonString::getValue) + .map(String::trim) + .filter(StringUtils::isNotEmpty); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/mongodb/DefaultMongoCommandTagsProviderTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/mongodb/DefaultMongoCommandTagsProviderTest.java index f1f6141ff8..cd44759714 100644 --- a/micrometer-binders/src/test/java/io/micrometer/binder/mongodb/DefaultMongoCommandTagsProviderTest.java +++ b/micrometer-binders/src/test/java/io/micrometer/binder/mongodb/DefaultMongoCommandTagsProviderTest.java @@ -15,8 +15,6 @@ */ package io.micrometer.binder.mongodb; -import java.util.Arrays; - import com.mongodb.ServerAddress; import com.mongodb.connection.ClusterId; import com.mongodb.connection.ConnectionDescription; @@ -24,11 +22,8 @@ import com.mongodb.event.CommandStartedEvent; import com.mongodb.event.CommandSucceededEvent; import io.micrometer.core.instrument.Tag; -import org.bson.BsonBoolean; import org.bson.BsonDocument; -import org.bson.BsonElement; import org.bson.BsonString; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -40,6 +35,7 @@ */ class DefaultMongoCommandTagsProviderTest { + private final ConnectionDescription connectionDesc = new ConnectionDescription( new ServerId(new ClusterId("cluster1"), new ServerAddress("localhost", 5150))); @@ -51,6 +47,7 @@ void defaultCommandTags() { Iterable tags = tagsProvider.commandTags(event); assertThat(tags).containsExactlyInAnyOrder( Tag.of("command", "find"), + Tag.of("database", "unknown"), Tag.of("collection", "unknown"), Tag.of("cluster.id", connectionDesc.getConnectionId().getServerId().getClusterId().getValue()), Tag.of("server.address", "localhost:5150"), @@ -68,11 +65,11 @@ void handlesCommandsOverLimitGracefully() { // 1001 will not be added to state map and therefore will use 'unknown' tagsProvider.commandStarted(commandStartedEvent(1001)); Iterable tags = tagsProvider.commandTags(commandSucceededEvent(1001)); - assertThat(tags).contains(Tag.of("collection", "unknown")); + assertThat(tags).contains(Tag.of("database", "unknown"), Tag.of("collection", "unknown")); // Complete 1000 - which will remove previously added entry from state map tags = tagsProvider.commandTags(commandSucceededEvent(1000)); - assertThat(tags).contains(Tag.of("collection", "collection-1000")); + assertThat(tags).contains(Tag.of("database", "db1"), Tag.of("collection", "collection-1000")); // 1001 will now be put in state map (since 1000 removed and made room for it) tagsProvider.commandStarted(commandStartedEvent(1001)); @@ -81,10 +78,10 @@ void handlesCommandsOverLimitGracefully() { tagsProvider.commandStarted(commandStartedEvent(1002)); tags = tagsProvider.commandTags(commandSucceededEvent(1001)); - assertThat(tags).contains(Tag.of("collection", "collection-1001")); + assertThat(tags).contains(Tag.of("database", "db1"), Tag.of("collection", "collection-1001")); tags = tagsProvider.commandTags(commandSucceededEvent(1002)); - assertThat(tags).contains(Tag.of("collection", "unknown")); + assertThat(tags).contains(Tag.of("database", "unknown"), Tag.of("collection", "unknown")); } private CommandStartedEvent commandStartedEvent(int requestId) { @@ -104,57 +101,4 @@ private CommandSucceededEvent commandSucceededEvent(int requestId) { new BsonDocument(), 1200L); } - - @Nested - class DetermineCollectionName { - - @Test - void withNameInAllowList() { - assertThat(tagsProvider.determineCollectionName("find", new BsonDocument("find", new BsonString(" bar ")))).hasValue("bar"); - } - - @Test - void withNameNotInAllowList() { - assertThat(tagsProvider.determineCollectionName("cmd", new BsonDocument("cmd", new BsonString(" bar ")))).isEmpty(); - } - - @Test - void withNameNotInCommand() { - assertThat(tagsProvider.determineCollectionName("find", new BsonDocument())).isEmpty(); - } - - @Test - void withNonStringCommand() { - assertThat(tagsProvider.determineCollectionName("find", new BsonDocument("find", BsonBoolean.TRUE))).isEmpty(); - } - - @Test - void withEmptyStringCommand() { - assertThat(tagsProvider.determineCollectionName("find", new BsonDocument("find", new BsonString(" ")))).isEmpty(); - } - - @Test - void withCollectionFieldOnly() { - assertThat(tagsProvider.determineCollectionName("find", new BsonDocument("collection", new BsonString(" bar ")))).hasValue("bar"); - } - - @Test - void withCollectionFieldAndAllowListedCommand() { - BsonDocument command = new BsonDocument(Arrays.asList( - new BsonElement("collection", new BsonString("coll")), - new BsonElement("find", new BsonString("bar")) - )); - assertThat(tagsProvider.determineCollectionName("find", command)).hasValue("bar"); - } - - @Test - void withCollectionFieldAndNotAllowListedCommand() { - BsonDocument command = new BsonDocument(Arrays.asList( - new BsonElement("collection", new BsonString("coll")), - new BsonElement("cmd", new BsonString("bar")) - )); - assertThat(tagsProvider.determineCollectionName("find", command)).hasValue("coll"); - } - } - } diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/mongodb/MongoCommandStartedEventTagsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/mongodb/MongoCommandStartedEventTagsTest.java new file mode 100644 index 0000000000..a47d350edc --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/mongodb/MongoCommandStartedEventTagsTest.java @@ -0,0 +1,115 @@ +/* + * Copyright 2021 VMware, Inc. + * + * 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 io.micrometer.binder.mongodb; + +import com.mongodb.ServerAddress; +import com.mongodb.connection.ClusterId; +import com.mongodb.connection.ConnectionDescription; +import com.mongodb.connection.ServerId; +import com.mongodb.event.CommandStartedEvent; +import org.bson.BsonBoolean; +import org.bson.BsonDocument; +import org.bson.BsonElement; +import org.bson.BsonString; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +class MongoCommandStartedEventTagsTest { + + private final ConnectionDescription connectionDesc = new ConnectionDescription( + new ServerId(new ClusterId("cluster1"), new ServerAddress("localhost", 5150))); + + @Test + void withNameInAllowList() { + CommandStartedEvent event = new CommandStartedEvent(1000, connectionDesc, "db1", "find", new BsonDocument("find", new BsonString(" bar "))); + MongoCommandStartedEventTags tags = new MongoCommandStartedEventTags(event); + assertThat(tags.getDatabase()).isEqualTo("db1"); + assertThat(tags.getCollection()).isEqualTo("bar"); + } + + @Test + void withNameNotInAllowList() { + CommandStartedEvent event = new CommandStartedEvent(1000, connectionDesc, "db1", "cmd", new BsonDocument("cmd", new BsonString(" bar "))); + MongoCommandStartedEventTags tags = new MongoCommandStartedEventTags(event); + assertThat(tags.getDatabase()).isEqualTo("db1"); + assertThat(tags.getCollection()).isEqualTo("unknown"); + } + + @Test + void withNameNotInCommand() { + CommandStartedEvent event = new CommandStartedEvent(1000, connectionDesc, "db1", "find", new BsonDocument()); + MongoCommandStartedEventTags tags = new MongoCommandStartedEventTags(event); + + assertThat(tags.getDatabase()).isEqualTo("db1"); + assertThat(tags.getCollection()).isEqualTo("unknown"); + } + + @Test + void withNonStringCommand() { + CommandStartedEvent event = new CommandStartedEvent(1000, connectionDesc, "db1", "find", new BsonDocument("find", BsonBoolean.TRUE)); + MongoCommandStartedEventTags tags = new MongoCommandStartedEventTags(event); + + assertThat(tags.getDatabase()).isEqualTo("db1"); + assertThat(tags.getCollection()).isEqualTo("unknown"); + } + + @Test + void withEmptyStringCommand() { + CommandStartedEvent event = new CommandStartedEvent(1000, connectionDesc, "db1", "find", new BsonDocument("find", new BsonString(" "))); + MongoCommandStartedEventTags tags = new MongoCommandStartedEventTags(event); + + assertThat(tags.getDatabase()).isEqualTo("db1"); + assertThat(tags.getCollection()).isEqualTo("unknown"); + } + + @Test + void withCollectionFieldOnly() { + CommandStartedEvent event = new CommandStartedEvent(1000, connectionDesc, "db1", "find", new BsonDocument("collection", new BsonString(" bar "))); + MongoCommandStartedEventTags tags = new MongoCommandStartedEventTags(event); + + assertThat(tags.getDatabase()).isEqualTo("db1"); + assertThat(tags.getCollection()).isEqualTo("bar"); + } + + @Test + void withCollectionFieldAndAllowListedCommand() { + BsonDocument command = new BsonDocument(Arrays.asList( + new BsonElement("collection", new BsonString("coll")), + new BsonElement("find", new BsonString("bar")) + )); + CommandStartedEvent event = new CommandStartedEvent(1000, connectionDesc, "db1", "find", command); + MongoCommandStartedEventTags tags = new MongoCommandStartedEventTags(event); + + assertThat(tags.getDatabase()).isEqualTo("db1"); + assertThat(tags.getCollection()).isEqualTo("bar"); + } + + @Test + void withCollectionFieldAndNotAllowListedCommand() { + BsonDocument command = new BsonDocument(Arrays.asList( + new BsonElement("collection", new BsonString("coll")), + new BsonElement("cmd", new BsonString("bar")) + )); + CommandStartedEvent event = new CommandStartedEvent(1000, connectionDesc, "db1", "find", command); + MongoCommandStartedEventTags tags = new MongoCommandStartedEventTags(event); + + assertThat(tags.getDatabase()).isEqualTo("db1"); + assertThat(tags.getCollection()).isEqualTo("coll"); + } +}