Browse Source

Introduce contextual Observer and improved KeyName utils.

Original Pull Request: #5020
pull/5044/head
Mark Paluch 4 months ago committed by Christoph Strobl
parent
commit
75f07cd651
No known key found for this signature in database
GPG Key ID: E6054036D0C37A4B
  1. 47
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java
  2. 178
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoKeyName.java
  3. 166
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservation.java
  4. 281
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/Observer.java
  5. 11
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java

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

@ -15,16 +15,12 @@ @@ -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 @@ class DefaultMongoHandlerObservationConvention implements MongoHandlerObservatio @@ -43,44 +39,7 @@ class DefaultMongoHandlerObservationConvention implements MongoHandlerObservatio
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

178
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoKeyName.java

@ -0,0 +1,178 @@ @@ -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<C>(String name, boolean required, Function<C, @Nullable Object> 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 <C>
*/
public static <C> MongoKeyName<C> required(String name, Function<C, @Nullable Object> 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 <C>
*/
public static <C> MongoKeyName<C> requiredString(String name, Function<C, @Nullable String> 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 <C>
*/
public static <C, V extends @Nullable Object> MongoKeyName<C> required(String name, Function<C, V> valueFunction,
Predicate<V> 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();
}
}
}

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

@ -15,11 +15,19 @@ @@ -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 @@ enum MongoObservation implements ObservationDocumentation { @@ -42,7 +50,7 @@ enum MongoObservation implements ObservationDocumentation {
@Override
public KeyName[] getLowCardinalityKeyNames() {
return LowCardinalityCommandKeyNames.values();
return LowCardinality.getKeyNames();
}
@Override
@ -53,137 +61,63 @@ enum MongoObservation implements ObservationDocumentation { @@ -53,137 +61,63 @@ enum MongoObservation implements ObservationDocumentation {
};
/**
* 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<MongoHandlerContext> MONGODB_COMMAND = MongoKeyName.requiredString("db.operation",
MongoHandlerContext::getCommandName);
/**
* MongoDB connection string.
*/
DB_CONNECTION_STRING {
@Override
public String asString() {
return "db.connection_string";
}
},
static MongoKeyName<MongoHandlerContext> DB_NAME = MongoKeyName.requiredString("db.name",
MongoHandlerContext::getDatabaseName);
/**
* Network transport.
*/
NET_TRANSPORT {
@Override
public String asString() {
return "net.transport";
}
},
static MongoKeyName<MongoHandlerContext> 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<ConnectionDescription> 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<ServerAddress> NET_PEER_NAME = MongoKeyName.required("net.peer.name", ServerAddress::getHost);
static MongoKeyName<ServerAddress> NET_PEER_PORT = MongoKeyName.required("net.peer.port", ServerAddress::getPort);
/**
* MongoDB user.
*/
DB_USER {
@Override
public String asString() {
return "db.user";
}
},
static MongoKeyName<ConnectionString> DB_CONNECTION_STRING = MongoKeyName.requiredString("db.connection_string",
Object::toString);
static MongoKeyName<ConnectionString> 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();
}
}

281
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/Observer.java

@ -0,0 +1,281 @@ @@ -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<MongoKeyName.MongoKeyValue> 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 <C> context type.
*/
public static <C> Observer fromContext(@Nullable C context, Consumer<? super ContextualObserver<C>> 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 <C>
*/
public <C> ContextualObserver<C> contextual(@Nullable C context) {
if (context == null) {
return new EmptyContextualObserver<>(keyValues);
}
return new DefaultContextualObserver<>(context, keyValues);
}
public <T> ContextualObserver<T> empty(Class<T> 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 <T>
*/
interface ContextualObserver<T> {
/**
* 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 <N> nested context type.
*/
<N> ContextualObserver<N> nested(Function<? super T, ? extends @Nullable N> 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<T> contribute(Consumer<? super ContextualObserver<T>> 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<T> contribute(MongoKeyName.MongoKeyValue keyValue);
/**
* Contribute a {@link MongoKeyName} to the observer.
*
* @param keyName
* @return {@code this} {@link ContextualObserver} for further chaining.
*/
default ContextualObserver<T> contribute(MongoKeyName<T> 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<T> contribute(MongoKeyName<T> keyName0, MongoKeyName<T> 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<T> contribute(MongoKeyName<T> keyName0, MongoKeyName<T> keyName1,
MongoKeyName<T> 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<T> contribute(Iterable<MongoKeyName<T>> 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 <T>
*/
private record DefaultContextualObserver<T>(T target,
List<MongoKeyName.MongoKeyValue> keyValues) implements ContextualObserver<T> {
public <N> ContextualObserver<N> nested(Function<? super T, ? extends @Nullable N> mapper) {
N nestedTarget = mapper.apply(target);
if (nestedTarget == null) {
return new EmptyContextualObserver<>(keyValues);
}
return new DefaultContextualObserver<>(nestedTarget, keyValues);
}
@Override
public ContextualObserver<T> contribute(MongoKeyName.MongoKeyValue keyValue) {
keyValues.add(keyValue);
return this;
}
@Override
public ContextualObserver<T> contribute(MongoKeyName<T> keyName) {
keyValues.add(keyName.valueOf(target));
return this;
}
@Override
public ContextualObserver<T> contribute(Iterable<MongoKeyName<T>> keyNames) {
for (MongoKeyName<T> 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 <T>
*/
private record EmptyContextualObserver<T>(
List<MongoKeyName.MongoKeyValue> keyValues) implements ContextualObserver<T> {
public <N> ContextualObserver<N> nested(Function<? super T, ? extends @Nullable N> mapper) {
return new EmptyContextualObserver<>(keyValues);
}
@Override
public ContextualObserver<T> contribute(MongoKeyName.MongoKeyValue keyValue) {
keyValues.add(keyValue);
return this;
}
@Override
public ContextualObserver<T> contribute(Iterable<MongoKeyName<T>> keyNames) {
for (MongoKeyName<T> name : keyNames) {
keyValues.add(name.absent());
}
return this;
}
}
}

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

@ -31,7 +31,6 @@ import org.bson.BsonDocument; @@ -31,7 +31,6 @@ import org.bson.BsonDocument;
import org.bson.BsonString;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames;
import com.mongodb.ConnectionString;
import com.mongodb.RequestContext;
@ -167,10 +166,10 @@ class MongoObservationCommandListenerTests { @@ -167,10 +166,10 @@ class MongoObservationCommandListenerTests {
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 @@ class MongoObservationCommandListenerTests { @@ -260,7 +259,7 @@ class MongoObservationCommandListenerTests {
assertThat(meterRegistry) //
.hasTimerWithNameAndTags(MongoObservation.MONGODB_COMMAND_OBSERVATION.getName(),
KeyValues.of(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue("user")));
KeyValues.of(MongoObservation.LowCardinality.MONGODB_COLLECTION.withValue("user")));
}
}

Loading…
Cancel
Save