5 changed files with 517 additions and 166 deletions
@ -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(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue