From d71954728de41c673fcb267f458a6775f6220301 Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov Date: Wed, 16 Jul 2025 18:56:23 -0700 Subject: [PATCH 1/2] Provide fallback for Observation KeyValues --- ...aultMongoHandlerObservationConvention.java | 65 ++++++++----------- .../observability/MongoObservation.java | 12 ++++ 2 files changed, 38 insertions(+), 39 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java index 4509fd7b2e..82ce364956 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java @@ -15,18 +15,16 @@ */ package org.springframework.data.mongodb.observability; -import io.micrometer.common.KeyValue; -import io.micrometer.common.KeyValues; - -import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames; -import org.springframework.util.Assert; -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.KeyValues; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import static org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames.*; /** * Default {@link MongoHandlerObservationConvention} implementation. @@ -41,54 +39,43 @@ class DefaultMongoHandlerObservationConvention implements MongoHandlerObservatio @Override public KeyValues getLowCardinalityKeyValues(MongoHandlerContext context) { - 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())); - } - - keyValues = keyValues.and(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue( - ObjectUtils.isEmpty(context.getCollectionName()) ? KeyValue.NONE_VALUE : context.getCollectionName())); - if (context.getCommandStartedEvent() == null) { throw new IllegalStateException("not command started event present"); } - ConnectionDescription connectionDescription = context.getCommandStartedEvent().getConnectionDescription(); + ConnectionString connectionString = context.getConnectionString(); + String connectionStringValue = connectionString != null ? connectionString.getConnectionString() : null; + String username = connectionString != null ? connectionString.getUsername() : null; + String transport = null, peerName = null, peerPort =null, clusterId = null; + 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())); + transport = "IP.TCP"; + peerName = serverAddress.getHost(); + peerPort = String.valueOf(serverAddress.getPort()); } ConnectionId connectionId = connectionDescription.getConnectionId(); if (connectionId != null) { - keyValues = keyValues.and(LowCardinalityCommandKeyNames.MONGODB_CLUSTER_ID - .withValue(connectionId.getServerId().getClusterId().getValue())); + clusterId = connectionId.getServerId().getClusterId().getValue(); } } - return keyValues; + return KeyValues.of( + DB_SYSTEM.withValue("mongodb"), + MONGODB_COMMAND.withValue(context.getCommandName()), + DB_CONNECTION_STRING.withOptionalValue(connectionStringValue), + DB_USER.withOptionalValue(username), + DB_NAME.withOptionalValue(context.getDatabaseName()), + MONGODB_COLLECTION.withOptionalValue(context.getCollectionName()), + NET_TRANSPORT.withOptionalValue(transport), + NET_PEER_NAME.withOptionalValue(peerName), + NET_PEER_PORT.withOptionalValue(peerPort), + MONGODB_CLUSTER_ID.withOptionalValue(clusterId) + ); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservation.java index 9dfc292521..6f8826a0a0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservation.java @@ -15,8 +15,11 @@ */ package org.springframework.data.mongodb.observability; +import io.micrometer.common.KeyValue; import io.micrometer.common.docs.KeyName; import io.micrometer.observation.docs.ObservationDocumentation; +import org.jspecify.annotations.Nullable; +import org.springframework.util.ObjectUtils; /** * A MongoDB-based {@link io.micrometer.observation.Observation}. @@ -172,6 +175,15 @@ public String asString() { public String asString() { return "db.operation"; } + }; + + /** + * Creates a key value for the given key name. + * @param value value for key, if value is null or empty {@link KeyValue.NONE_VALUE} will be used + * @return key value + */ + public KeyValue withOptionalValue(@Nullable String value) { + return withValue(ObjectUtils.isEmpty(value) ? KeyValue.NONE_VALUE : value); } } From 2b62a129f288503bf882cca5b4ce70d0f125efbe Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 7 Aug 2025 12:39:54 +0200 Subject: [PATCH 2/2] Introduce contextual Observer and improved KeyName utils. --- ...aultMongoHandlerObservationConvention.java | 47 +-- .../mongodb/observability/MongoKeyName.java | 178 +++++++++++ .../observability/MongoObservation.java | 166 ++++------- .../data/mongodb/observability/Observer.java | 281 ++++++++++++++++++ .../MongoObservationCommandListenerTests.java | 11 +- 5 files changed, 517 insertions(+), 166 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoKeyName.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/Observer.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java index 82ce364956..3d3742d577 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java @@ -15,16 +15,12 @@ */ package org.springframework.data.mongodb.observability; -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.KeyValues; + import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; -import static org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames.*; +import com.mongodb.event.CommandStartedEvent; /** * Default {@link MongoHandlerObservationConvention} implementation. @@ -43,44 +39,7 @@ public KeyValues getLowCardinalityKeyValues(MongoHandlerContext context) { throw new IllegalStateException("not command started event present"); } - ConnectionString connectionString = context.getConnectionString(); - String connectionStringValue = connectionString != null ? connectionString.getConnectionString() : null; - String username = connectionString != null ? connectionString.getUsername() : null; - - String transport = null, peerName = null, peerPort =null, clusterId = null; - ConnectionDescription connectionDescription = context.getCommandStartedEvent().getConnectionDescription(); - if (connectionDescription != null) { - ServerAddress serverAddress = connectionDescription.getServerAddress(); - - if (serverAddress != null) { - transport = "IP.TCP"; - peerName = serverAddress.getHost(); - peerPort = String.valueOf(serverAddress.getPort()); - } - - ConnectionId connectionId = connectionDescription.getConnectionId(); - if (connectionId != null) { - clusterId = connectionId.getServerId().getClusterId().getValue(); - } - } - - return KeyValues.of( - DB_SYSTEM.withValue("mongodb"), - MONGODB_COMMAND.withValue(context.getCommandName()), - DB_CONNECTION_STRING.withOptionalValue(connectionStringValue), - DB_USER.withOptionalValue(username), - DB_NAME.withOptionalValue(context.getDatabaseName()), - MONGODB_COLLECTION.withOptionalValue(context.getCollectionName()), - NET_TRANSPORT.withOptionalValue(transport), - NET_PEER_NAME.withOptionalValue(peerName), - NET_PEER_PORT.withOptionalValue(peerPort), - MONGODB_CLUSTER_ID.withOptionalValue(clusterId) - ); - } - - @Override - public KeyValues getHighCardinalityKeyValues(MongoHandlerContext context) { - return KeyValues.empty(); + return MongoObservation.LowCardinality.observe(context).toKeyValues(); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoKeyName.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoKeyName.java new file mode 100644 index 0000000000..b7db03fc3c --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoKeyName.java @@ -0,0 +1,178 @@ +/* + * Copyright 2025 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.KeyValue; +import io.micrometer.common.docs.KeyName; + +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.jspecify.annotations.Nullable; + +import org.springframework.util.StringUtils; + +/** + * Value object representing an observation key name for MongoDB operations. It allows easier transformation to + * {@link KeyValue} and {@link KeyName}. + * + * @author Mark Paluch + */ +record MongoKeyName(String name, boolean required, Function valueFunction) implements KeyName { + + /** + * Creates a required {@link MongoKeyName} along with a contextual value function to extract the value from the + * context. The value defaults to {@link KeyValue#NONE_VALUE} if the contextual value function returns + * {@literal null}. + * + * @param name + * @param valueFunction + * @return + * @param + */ + public static MongoKeyName required(String name, Function valueFunction) { + return required(name, valueFunction, Objects::nonNull); + } + + /** + * Creates a required {@link MongoKeyName} along with a contextual value function to extract the value from the + * context. The value defaults to {@link KeyValue#NONE_VALUE} if the contextual value function returns {@literal null} + * or an empty {@link String}. + * + * @param name + * @param valueFunction + * @return + * @param + */ + public static MongoKeyName requiredString(String name, Function valueFunction) { + return required(name, valueFunction, StringUtils::hasText); + } + + /** + * Creates a required {@link MongoKeyName} along with a contextual value function to extract the value from the + * context. The value defaults to {@link KeyValue#NONE_VALUE} if the contextual value function returns + * {@literal null}. + * + * @param name + * @param valueFunction + * @param hasValue predicate to determine if the value is present. + * @return + * @param + */ + public static MongoKeyName required(String name, Function valueFunction, + Predicate hasValue) { + return new MongoKeyName<>(name, true, c -> { + V value = valueFunction.apply(c); + return hasValue.test(value) ? value : null; + }); + } + + /** + * Creates a required {@link MongoKeyValue} with a constant value. + * + * @param name + * @param value + * @return + */ + public static MongoKeyValue just(String name, String value) { + return new MongoKeyName<>(name, false, it -> value).withValue(value); + } + + /** + * Create a new {@link MongoKeyValue} with a given value. + * + * @param value value for key + * @return + */ + @Override + public MongoKeyValue withValue(String value) { + return new MongoKeyValue(this, value); + } + + /** + * Create a new {@link MongoKeyValue} from the context. If the context is {@literal null}, the value will be + * {@link KeyValue#NONE_VALUE}. + * + * @param context + * @return + */ + public MongoKeyValue valueOf(@Nullable C context) { + + Object value = context != null ? valueFunction.apply(context) : null; + return new MongoKeyValue(this, value == null ? KeyValue.NONE_VALUE : value.toString()); + } + + /** + * Create a new absent {@link MongoKeyValue} with the {@link KeyValue#NONE_VALUE} as value. + * + * @return + */ + public MongoKeyValue absent() { + return new MongoKeyValue(this, KeyValue.NONE_VALUE); + } + + @Override + public boolean isRequired() { + return required; + } + + @Override + public String asString() { + return name; + } + + @Override + public String toString() { + return "Key: " + asString(); + } + + /** + * Value object representing an observation key and value for MongoDB operations. It allows easier transformation to + * {@link KeyValue} and {@link KeyName}. + */ + static class MongoKeyValue implements KeyName, KeyValue { + + private final KeyName keyName; + private final String value; + + MongoKeyValue(KeyName keyName, String value) { + this.keyName = keyName; + this.value = value; + } + + @Override + public String getKey() { + return keyName.asString(); + } + + @Override + public String getValue() { + return value; + } + + @Override + public String asString() { + return getKey(); + } + + @Override + public String toString() { + return getKey() + "=" + getValue(); + } + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservation.java index 6f8826a0a0..b898def63e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservation.java @@ -15,11 +15,19 @@ */ package org.springframework.data.mongodb.observability; -import io.micrometer.common.KeyValue; +import static org.springframework.data.mongodb.observability.MongoKeyName.*; + import io.micrometer.common.docs.KeyName; import io.micrometer.observation.docs.ObservationDocumentation; + import org.jspecify.annotations.Nullable; -import org.springframework.util.ObjectUtils; + +import org.springframework.util.StringUtils; + +import com.mongodb.ConnectionString; +import com.mongodb.ServerAddress; +import com.mongodb.connection.ConnectionDescription; +import com.mongodb.event.CommandEvent; /** * A MongoDB-based {@link io.micrometer.observation.Observation}. @@ -42,7 +50,7 @@ public String getName() { @Override public KeyName[] getLowCardinalityKeyNames() { - return LowCardinalityCommandKeyNames.values(); + return LowCardinality.getKeyNames(); } @Override @@ -53,137 +61,63 @@ public KeyName[] getHighCardinalityKeyNames() { }; /** - * Enums related to low cardinality key names for MongoDB commands. + * Contributors for low cardinality key names. */ - enum LowCardinalityCommandKeyNames implements KeyName { + static class LowCardinality { - /** - * MongoDB database system. - */ - DB_SYSTEM { - @Override - public String asString() { - return "db.system"; - } - }, + static MongoKeyValue DB_SYSTEM = just("db.system", "mongodb"); + static MongoKeyName MONGODB_COMMAND = MongoKeyName.requiredString("db.operation", + MongoHandlerContext::getCommandName); - /** - * MongoDB connection string. - */ - DB_CONNECTION_STRING { - @Override - public String asString() { - return "db.connection_string"; - } - }, + static MongoKeyName DB_NAME = MongoKeyName.requiredString("db.name", + MongoHandlerContext::getDatabaseName); - /** - * Network transport. - */ - NET_TRANSPORT { - @Override - public String asString() { - return "net.transport"; - } - }, + static MongoKeyName MONGODB_COLLECTION = MongoKeyName.requiredString("db.mongodb.collection", + MongoHandlerContext::getCollectionName); /** - * 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. + * MongoDB cluster identifier. */ - NET_SOCK_PEER_ADDR { - @Override - public String asString() { - return "net.sock.peer.addr"; - } - }, + static MongoKeyName MONGODB_CLUSTER_ID = MongoKeyName.required( + "spring.data.mongodb.cluster_id", it -> it.getConnectionId().getServerId().getClusterId().getValue(), + StringUtils::hasText); - /** - * Mongo peer port. - */ - NET_SOCK_PEER_PORT { - @Override - public String asString() { - return "net.sock.peer.port"; - } - }, + static MongoKeyValue NET_TRANSPORT_TCP_IP = just("net.transport", "IP.TCP"); + static MongoKeyName NET_PEER_NAME = MongoKeyName.required("net.peer.name", ServerAddress::getHost); + static MongoKeyName NET_PEER_PORT = MongoKeyName.required("net.peer.port", ServerAddress::getPort); - /** - * MongoDB user. - */ - DB_USER { - @Override - public String asString() { - return "db.user"; - } - }, + static MongoKeyName DB_CONNECTION_STRING = MongoKeyName.requiredString("db.connection_string", + Object::toString); + static MongoKeyName DB_USER = MongoKeyName.requiredString("db.user", + ConnectionString::getUsername); /** - * MongoDB database name. + * Observe low cardinality key values for the given {@link MongoHandlerContext}. + * + * @param context the context to contribute from, can be {@literal null} if no context is available. + * @return the key value contributor providing low cardinality key names. */ - DB_NAME { - @Override - public String asString() { - return "db.name"; - } - }, + public static Observer observe(@Nullable MongoHandlerContext context) { - /** - * MongoDB collection name. - */ - MONGODB_COLLECTION { - @Override - public String asString() { - return "db.mongodb.collection"; - } - }, + return Observer.fromContext(context, it -> { - /** - * MongoDB cluster identifier. - */ - MONGODB_CLUSTER_ID { - @Override - public String asString() { - return "spring.data.mongodb.cluster_id"; - } - }, + it.contribute(DB_SYSTEM).contribute(MONGODB_COMMAND, DB_NAME, MONGODB_COLLECTION); - /** - * MongoDB command value. - */ - MONGODB_COMMAND { - @Override - public String asString() { - return "db.operation"; - } - }; + it.nested(MongoHandlerContext::getConnectionString).contribute(DB_CONNECTION_STRING, DB_USER); + it.nested(MongoHandlerContext::getCommandStartedEvent) // + .nested(CommandEvent::getConnectionDescription).contribute(MONGODB_CLUSTER_ID) // + .nested(ConnectionDescription::getServerAddress) // + .contribute(NET_TRANSPORT_TCP_IP).contribute(NET_PEER_NAME, NET_PEER_PORT); + }); + } /** - * Creates a key value for the given key name. - * @param value value for key, if value is null or empty {@link KeyValue.NONE_VALUE} will be used - * @return key value + * Returns the key names for low cardinality keys. + * + * @return the key names for low cardinality keys. */ - public KeyValue withOptionalValue(@Nullable String value) { - return withValue(ObjectUtils.isEmpty(value) ? KeyValue.NONE_VALUE : value); + public static KeyName[] getKeyNames() { + return observe(null).toKeyNames(); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/Observer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/Observer.java new file mode 100644 index 0000000000..d0a414936c --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/Observer.java @@ -0,0 +1,281 @@ +/* + * Copyright 2025 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.KeyValues; +import io.micrometer.common.docs.KeyName; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.jspecify.annotations.Nullable; + +/** + * An observer abstraction that can observe a context and contribute {@literal KeyValue}s for propagation into + * observability systems. + * + * @author Mark Paluch + */ +class Observer { + + private final List keyValues = new ArrayList<>(); + + /** + * Create a new {@link Observer}. + * + * @return a new {@link Observer}. + */ + public static Observer create() { + return new Observer(); + } + + /** + * Create a new {@link Observer} given an optional context and a consumer that will contribute key-value tuples from + * the given context. + * + * @param context the context to observe, can be {@literal null}. + * @param consumer consumer for a functional declaration that supplies key-value tuples. + * @return the stateful {@link Observer}. + * @param context type. + */ + public static Observer fromContext(@Nullable C context, Consumer> consumer) { + + Observer contributor = create(); + + consumer.accept(contributor.contextual(context)); + + return contributor; + } + + /** + * Contribute a single {@link MongoKeyName.MongoKeyValue} to the observer. + * + * @param keyValue + * @return + */ + public Observer contribute(MongoKeyName.MongoKeyValue keyValue) { + + keyValues.add(keyValue); + + return this; + } + + /** + * Create a nested, contextual {@link ContextualObserver} that can contribute key-value tuples based on the given + * context. + * + * @param context the context to observe, can be {@literal null}. + * @return the nested contextual {@link ContextualObserver} that can contribute key-value tuples. + * @param + */ + public ContextualObserver contextual(@Nullable C context) { + + if (context == null) { + return new EmptyContextualObserver<>(keyValues); + } + + return new DefaultContextualObserver<>(context, keyValues); + } + + public ContextualObserver empty(Class targetType) { + return new EmptyContextualObserver<>(this.keyValues); + } + + public KeyValues toKeyValues() { + return KeyValues.of(keyValues); + } + + public KeyName[] toKeyNames() { + + KeyName[] keyNames = new KeyName[keyValues.size()]; + + for (int i = 0; i < keyValues.size(); i++) { + MongoKeyName.MongoKeyValue keyValue = keyValues.get(i); + keyNames[i] = keyValue; + } + + return keyNames; + } + + /** + * Contextual observer interface to contribute key-value tuples based on a context. The context can be transformed + * into a nested context using {@link #nested(Function)}. + * + * @param + */ + interface ContextualObserver { + + /** + * Create a nested {@link ContextualObserver} that can contribute key-value tuples based on the transformation of + * the current context. If the {@code mapper} function returns {@literal null}, the nested observer will operate + * without a context contributing {@literal MonKoKeyName.absent()} values simplifying nullability handling. + * + * @param mapper context mapper function that transforms the current context into a nested context. + * @return the nested contextual observer. + * @param nested context type. + */ + ContextualObserver nested(Function mapper); + + /** + * Functional-style contribution of a {@link ContextualObserver} callback. + * + * @param consumer the consumer that will be invoked with this {@link ContextualObserver}. + * @return {@code this} {@link ContextualObserver} for further chaining. + */ + default ContextualObserver contribute(Consumer> consumer) { + consumer.accept(this); + return this; + } + + /** + * Contribute a {@link MongoKeyName.MongoKeyValue} to the observer. + * + * @param keyValue + * @return {@code this} {@link ContextualObserver} for further chaining. + */ + ContextualObserver contribute(MongoKeyName.MongoKeyValue keyValue); + + /** + * Contribute a {@link MongoKeyName} to the observer. + * + * @param keyName + * @return {@code this} {@link ContextualObserver} for further chaining. + */ + default ContextualObserver contribute(MongoKeyName keyName) { + return contribute(List.of(keyName)); + } + + /** + * Contribute a collection of {@link MongoKeyName}s to the observer. + * + * @param keyName0 + * @param keyName1 + * @return {@code this} {@link ContextualObserver} for further chaining. + */ + default ContextualObserver contribute(MongoKeyName keyName0, MongoKeyName keyName1) { + return contribute(List.of(keyName0, keyName1)); + } + + /** + * Contribute a collection of {@link MongoKeyName}s to the observer. + * + * @param keyName0 + * @param keyName1 + * @param keyName2 + * @return {@code this} {@link ContextualObserver} for further chaining. + */ + default ContextualObserver contribute(MongoKeyName keyName0, MongoKeyName keyName1, + MongoKeyName keyName2) { + return contribute(List.of(keyName0, keyName1, keyName2)); + } + + /** + * Contribute a collection of {@link MongoKeyName}s to the observer. + * + * @param keyNames + * @return {@code this} {@link ContextualObserver} for further chaining. + */ + ContextualObserver contribute(Iterable> keyNames); + + } + + /** + * A default {@link ContextualObserver} that observes a target and contributes key-value tuples by providing the + * context to {@link MongoKeyName}. + * + * @param target + * @param keyValues + * @param + */ + private record DefaultContextualObserver(T target, + List keyValues) implements ContextualObserver { + + public ContextualObserver nested(Function mapper) { + + N nestedTarget = mapper.apply(target); + + if (nestedTarget == null) { + return new EmptyContextualObserver<>(keyValues); + } + + return new DefaultContextualObserver<>(nestedTarget, keyValues); + } + + @Override + public ContextualObserver contribute(MongoKeyName.MongoKeyValue keyValue) { + + keyValues.add(keyValue); + + return this; + } + + @Override + public ContextualObserver contribute(MongoKeyName keyName) { + + keyValues.add(keyName.valueOf(target)); + + return this; + } + + @Override + public ContextualObserver contribute(Iterable> keyNames) { + + for (MongoKeyName name : keyNames) { + keyValues.add(name.valueOf(target)); + } + + return this; + } + + } + + /** + * Empty {@link ContextualObserver} that is not associated with a context and therefore, it only contributes + * {@link MongoKeyName#absent()} values. + * + * @param keyValues + * @param + */ + private record EmptyContextualObserver( + List keyValues) implements ContextualObserver { + + public ContextualObserver nested(Function mapper) { + return new EmptyContextualObserver<>(keyValues); + } + + @Override + public ContextualObserver contribute(MongoKeyName.MongoKeyValue keyValue) { + + keyValues.add(keyValue); + + return this; + } + + @Override + public ContextualObserver contribute(Iterable> keyNames) { + + for (MongoKeyName name : keyNames) { + keyValues.add(name.absent()); + } + + return this; + } + + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java index 35536e3921..fe74a03bd6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java @@ -31,7 +31,6 @@ import org.bson.BsonString; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames; import com.mongodb.ConnectionString; import com.mongodb.RequestContext; @@ -167,10 +166,10 @@ void successfullyCompletedCommandWithoutClusterInformationShouldCreateTimerWhenP listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, 0, null, "insert", null, null, 0)); 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")); + KeyValues.of(MongoObservation.LowCardinality.MONGODB_COLLECTION.withValue("user"), + MongoObservation.LowCardinality.DB_NAME.withValue("database"), + MongoObservation.LowCardinality.MONGODB_COMMAND.withValue("insert"), + MongoObservation.LowCardinality.DB_SYSTEM.withValue("mongodb")).and("error", "none")); } @Test @@ -260,7 +259,7 @@ private void assertThatTimerRegisteredWithTags() { assertThat(meterRegistry) // .hasTimerWithNameAndTags(MongoObservation.MONGODB_COMMAND_OBSERVATION.getName(), - KeyValues.of(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue("user"))); + KeyValues.of(MongoObservation.LowCardinality.MONGODB_COLLECTION.withValue("user"))); } }