5 changed files with 515 additions and 166 deletions
@ -0,0 +1,177 @@
@@ -0,0 +1,177 @@
|
||||
/* |
||||
* 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.springframework.lang.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, 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, 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, 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 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(); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -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.springframework.lang.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 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 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 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; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue