Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: context enrichment via contextEnricher, not from init #991

Merged
merged 6 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion providers/flagd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ FlagdProvider flagdProvider = new FlagdProvider(options);

### Configuration options

Options can be defined in the constructor or as environment variables, with constructor options having the highest
Most options can be defined in the constructor or as environment variables, with constructor options having the highest
precedence.
Default options can be overridden through a `FlagdOptions` based constructor or set to be picked up from the environment
variables.
Expand Down Expand Up @@ -177,6 +177,12 @@ By default, the provider is configured to
use [least recently used (lru)](https://commons.apache.org/proper/commons-collections/apidocs/org/apache/commons/collections4/map/LRUMap.html)
caching with up to 1000 entries.

##### Context enrichment
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be used to select a subset of fields from the getMetadata response. By default, all of it is injected into the context.


The `contextEnricher` option is a function which provides a context to be added to each evaluation.
This function runs on the initial provider connection and every reconnection, and is passed the [sync-metadata](#sync-metadata).
By default, a simple implementation which uses the sync-metadata payload in its entirety is used.

### OpenTelemetry tracing (RPC only)

flagd provider support OpenTelemetry traces for gRPC-backed remote evaluations.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package dev.openfeature.contrib.providers.flagd;

import static dev.openfeature.contrib.providers.flagd.Config.fallBackToEnvOrDefault;
import static dev.openfeature.contrib.providers.flagd.Config.fromValueProvider;

import java.util.function.Function;

import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.Connector;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.ImmutableContext;
import dev.openfeature.sdk.Structure;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import lombok.Builder;
import lombok.Getter;

import static dev.openfeature.contrib.providers.flagd.Config.fallBackToEnvOrDefault;
import static dev.openfeature.contrib.providers.flagd.Config.fromValueProvider;

/**
* FlagdOptions is a builder to build flagd provider options.
*/
Expand Down Expand Up @@ -109,6 +114,19 @@ public class FlagdOptions {
@Builder.Default
private String offlineFlagSourcePath = fallBackToEnvOrDefault(Config.OFFLINE_SOURCE_PATH, null);

/**
* Function providing an EvaluationContext to mix into every evaluations.
* The sync-metadata response
* (https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1#flagd.sync.v1.GetMetadataResponse),
* represented as a {@link dev.openfeature.sdk.Structure}, is passed as an
* argument.
* This function runs every time the provider (re)connects, and its result is cached and used in every evaluation.
* By default, the entire sync response (converted to a Structure) is used.
*/
@Builder.Default
private Function<Structure, EvaluationContext> contextEnricher = (syncMetadata) -> new ImmutableContext(
syncMetadata.asMap());

/**
* Inject a Custom Connector for fetching flags.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package dev.openfeature.contrib.providers.flagd;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import java.util.List;
import java.util.function.Function;

import dev.openfeature.contrib.providers.flagd.resolver.Resolver;
import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent;
Expand All @@ -10,9 +12,13 @@
import dev.openfeature.contrib.providers.flagd.resolver.process.InProcessResolver;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.EventProvider;
import dev.openfeature.sdk.Hook;
import dev.openfeature.sdk.ImmutableContext;
import dev.openfeature.sdk.ImmutableStructure;
import dev.openfeature.sdk.Metadata;
import dev.openfeature.sdk.ProviderEvaluation;
import dev.openfeature.sdk.ProviderEventDetails;
import dev.openfeature.sdk.Structure;
import dev.openfeature.sdk.Value;
import lombok.extern.slf4j.Slf4j;

Expand All @@ -22,13 +28,14 @@
@Slf4j
@SuppressWarnings({ "PMD.TooManyStaticImports", "checkstyle:NoFinalizer" })
public class FlagdProvider extends EventProvider {
private Function<Structure, EvaluationContext> contextEnricher;
private static final String FLAGD_PROVIDER = "flagd";
private final Resolver flagResolver;
private volatile boolean initialized = false;
private volatile boolean connected = false;
private volatile Map<String, Object> syncMetadata = Collections.emptyMap();

private EvaluationContext evaluationContext;
private volatile Structure syncMetadata = new ImmutableStructure();
private volatile EvaluationContext enrichedContext = new ImmutableContext();
private final List<Hook> hooks = new ArrayList<>();

protected final void finalize() {
// DO NOT REMOVE, spotbugs: CT_CONSTRUCTOR_THROW
Expand Down Expand Up @@ -62,6 +69,13 @@ public FlagdProvider(final FlagdOptions options) {
throw new IllegalStateException(
String.format("Requested unsupported resolver type of %s", options.getResolverType()));
}
hooks.add(new SyncMetadataHook(this::getEnrichedContext));
contextEnricher = options.getContextEnricher();
}

@Override
public List<Hook> getProviderHooks() {
return Collections.unmodifiableList(hooks);
}

@Override
Expand All @@ -70,7 +84,6 @@ public synchronized void initialize(EvaluationContext evaluationContext) throws
return;
}

this.evaluationContext = evaluationContext;
this.flagResolver.init();
this.initialized = true;
}
Expand All @@ -97,48 +110,48 @@ public Metadata getMetadata() {

@Override
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
return this.flagResolver.booleanEvaluation(key, defaultValue, mergeContext(ctx));
return this.flagResolver.booleanEvaluation(key, defaultValue, ctx);
}

@Override
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
return this.flagResolver.stringEvaluation(key, defaultValue, mergeContext(ctx));
return this.flagResolver.stringEvaluation(key, defaultValue, ctx);
}

@Override
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
return this.flagResolver.doubleEvaluation(key, defaultValue, mergeContext(ctx));
return this.flagResolver.doubleEvaluation(key, defaultValue, ctx);
}

@Override
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
return this.flagResolver.integerEvaluation(key, defaultValue, mergeContext(ctx));
return this.flagResolver.integerEvaluation(key, defaultValue, ctx);
}

@Override
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
return this.flagResolver.objectEvaluation(key, defaultValue, mergeContext(ctx));
return this.flagResolver.objectEvaluation(key, defaultValue, ctx);
}

/**
* An unmodifiable view of an object map representing the latest result of the
* An unmodifiable view of a Structure representing the latest result of the
* SyncMetadata.
* Set on initial connection and updated with every reconnection.
* see:
* https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1#flagd.sync.v1.FlagSyncService.GetMetadata
*
* @return Object map representing sync metadata
*/
protected Map<String, Object> getSyncMetadata() {
return Collections.unmodifiableMap(syncMetadata);
protected Structure getSyncMetadata() {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a much safer type which we already use for this sort of thing.

return new ImmutableStructure(syncMetadata.asMap());
}

private EvaluationContext mergeContext(final EvaluationContext clientCallCtx) {
if (this.evaluationContext != null) {
return evaluationContext.merge(clientCallCtx);
}

return clientCallCtx;
/**
* The updated context mixed into all evaluations based on the sync-metadata.
* @return context
*/
EvaluationContext getEnrichedContext() {
return enrichedContext;
}

private boolean isConnected() {
Expand All @@ -149,6 +162,7 @@ private void onConnectionEvent(ConnectionEvent connectionEvent) {
boolean previous = connected;
boolean current = connected = connectionEvent.isConnected();
syncMetadata = connectionEvent.getSyncMetadata();
enrichedContext = contextEnricher.apply(connectionEvent.getSyncMetadata());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every time the resolver connects, we update the cached context by re-running the passed enricher.


// configuration changed
if (initialized && previous && current) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package dev.openfeature.contrib.providers.flagd;

import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;

import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.Hook;
import dev.openfeature.sdk.HookContext;

class SyncMetadataHook implements Hook<Object> {

private Supplier<EvaluationContext> contextSupplier;

SyncMetadataHook(Supplier<EvaluationContext> contextSupplier) {
this.contextSupplier = contextSupplier;
}

/**
* Return the context adapted from the sync-metadata provided by the supplier.
*/
@Override
public Optional<EvaluationContext> before(HookContext<Object> ctx, Map<String, Object> hints) {
return Optional.ofNullable(contextSupplier.get());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

import java.util.Collections;
import java.util.List;
import java.util.Map;

import dev.openfeature.sdk.ImmutableStructure;
import dev.openfeature.sdk.Structure;
import lombok.AllArgsConstructor;
import lombok.Getter;

Expand All @@ -17,15 +18,15 @@ public class ConnectionEvent {
@Getter
private final boolean connected;
private final List<String> flagsChanged;
private final Map<String, Object> syncMetadata;
private final Structure syncMetadata;

/**
* Construct a new ConnectionEvent.
*
* @param connected status of the connection
*/
public ConnectionEvent(boolean connected) {
this(connected, Collections.emptyList(), Collections.emptyMap());
this(connected, Collections.emptyList(), new ImmutableStructure());
}

/**
Expand All @@ -35,7 +36,7 @@ public ConnectionEvent(boolean connected) {
* @param flagsChanged list of flags changed
*/
public ConnectionEvent(boolean connected, List<String> flagsChanged) {
this(connected, flagsChanged, Collections.emptyMap());
this(connected, flagsChanged, new ImmutableStructure());
}

/**
Expand All @@ -44,8 +45,8 @@ public ConnectionEvent(boolean connected, List<String> flagsChanged) {
* @param connected status of the connection
* @param syncMetadata sync.getMetadata
*/
public ConnectionEvent(boolean connected, Map<String, Object> syncMetadata) {
this(connected, Collections.emptyList(), syncMetadata);
public ConnectionEvent(boolean connected, Structure syncMetadata) {
this(connected, Collections.emptyList(), new ImmutableStructure(syncMetadata.asMap()));
}

/**
Expand All @@ -58,11 +59,11 @@ public List<String> getFlagsChanged() {
}

/**
* Get changed sync metadata.
* Get changed sync metadata represented as SDK structure type.
*
* @return an unmodifiable view of the sync metadata
*/
public Map<String, Object> getSyncMetadata() {
return Collections.unmodifiableMap(syncMetadata);
public Structure getSyncMetadata() {
return new ImmutableStructure(syncMetadata.asMap());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.google.protobuf.Struct;

import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.ImmutableContext;
import dev.openfeature.sdk.MutableStructure;
import dev.openfeature.sdk.Structure;
import dev.openfeature.sdk.Value;
Expand All @@ -20,6 +21,19 @@
* gRPC type conversion utils.
*/
public class Convert {

/**
* Converts a protobuf struct to EvaluationContext.
*
* @param struct profobuf struct to convert
* @return a context
*/
public static EvaluationContext convertProtobufStructToContext(final Struct struct) {
final HashMap<String, Value> values = new HashMap<>();
struct.getFieldsMap().forEach((key, value) -> values.put(key, convertAny(value)));
return new ImmutableContext(values);
}

/**
* Recursively convert protobuf structure to openfeature value.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import static dev.openfeature.contrib.providers.flagd.resolver.common.Convert.convertProtobufMapToStructure;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -19,6 +18,8 @@
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.Connector;
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload;
import dev.openfeature.flagd.grpc.sync.Sync.GetMetadataResponse;
import dev.openfeature.sdk.ImmutableStructure;
import dev.openfeature.sdk.Structure;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import lombok.extern.slf4j.Slf4j;

Expand Down Expand Up @@ -109,7 +110,7 @@ private void streamerListener(final Connector connector) throws InterruptedExcep
List<String> changedFlagsKeys;
Map<String, FeatureFlag> flagMap = FlagParser.parseString(payload.getFlagData(),
throwIfInvalid);
Map<String, Object> metadata = parseSyncMetadata(payload.getMetadataResponse());
Structure metadata = parseSyncMetadata(payload.getMetadataResponse());
writeLock.lock();
try {
changedFlagsKeys = getChangedFlagsKeys(flagMap);
Expand Down Expand Up @@ -143,14 +144,13 @@ private void streamerListener(final Connector connector) throws InterruptedExcep
log.info("Shutting down store stream listener");
}

private Map<String, Object> parseSyncMetadata(GetMetadataResponse metadataResponse) {
private Structure parseSyncMetadata(GetMetadataResponse metadataResponse) {
try {
return convertProtobufMapToStructure(metadataResponse.getMetadata().getFieldsMap())
.asObjectMap();
return convertProtobufMapToStructure(metadataResponse.getMetadata().getFieldsMap());
} catch (Exception exception) {
log.error("Failed to parse metadataResponse, provider metadata may not be up-to-date");
}
return Collections.emptyMap();
return new ImmutableStructure();
}

private List<String> getChangedFlagsKeys(Map<String, FeatureFlag> newFlags) {
Expand Down
Loading