Skip to content

Commit 1c2e11b

Browse files
toddbaertbeeme1mr
andauthored
feat!: context enrichment via contextEnricher, not from init (#991)
Signed-off-by: Todd Baert <[email protected]> Co-authored-by: Michael Beemer <[email protected]>
1 parent a701161 commit 1c2e11b

File tree

11 files changed

+1119
-993
lines changed

11 files changed

+1119
-993
lines changed

providers/flagd/README.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ FlagdProvider flagdProvider = new FlagdProvider(options);
9696
9797
### Configuration options
9898

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

180+
##### Context enrichment
181+
182+
The `contextEnricher` option is a function which provides a context to be added to each evaluation.
183+
This function runs on the initial provider connection and every reconnection, and is passed the [sync-metadata](#sync-metadata).
184+
By default, a simple implementation which uses the sync-metadata payload in its entirety is used.
185+
180186
### OpenTelemetry tracing (RPC only)
181187

182188
flagd provider support OpenTelemetry traces for gRPC-backed remote evaluations.

providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdOptions.java

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
package dev.openfeature.contrib.providers.flagd;
22

3+
import static dev.openfeature.contrib.providers.flagd.Config.fallBackToEnvOrDefault;
4+
import static dev.openfeature.contrib.providers.flagd.Config.fromValueProvider;
5+
6+
import java.util.function.Function;
7+
38
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.Connector;
9+
import dev.openfeature.sdk.EvaluationContext;
10+
import dev.openfeature.sdk.ImmutableContext;
11+
import dev.openfeature.sdk.Structure;
412
import io.opentelemetry.api.GlobalOpenTelemetry;
513
import io.opentelemetry.api.OpenTelemetry;
614
import lombok.Builder;
715
import lombok.Getter;
816

9-
import static dev.openfeature.contrib.providers.flagd.Config.fallBackToEnvOrDefault;
10-
import static dev.openfeature.contrib.providers.flagd.Config.fromValueProvider;
11-
1217
/**
1318
* FlagdOptions is a builder to build flagd provider options.
1419
*/
@@ -109,6 +114,19 @@ public class FlagdOptions {
109114
@Builder.Default
110115
private String offlineFlagSourcePath = fallBackToEnvOrDefault(Config.OFFLINE_SOURCE_PATH, null);
111116

117+
/**
118+
* Function providing an EvaluationContext to mix into every evaluations.
119+
* The sync-metadata response
120+
* (https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1#flagd.sync.v1.GetMetadataResponse),
121+
* represented as a {@link dev.openfeature.sdk.Structure}, is passed as an
122+
* argument.
123+
* This function runs every time the provider (re)connects, and its result is cached and used in every evaluation.
124+
* By default, the entire sync response (converted to a Structure) is used.
125+
*/
126+
@Builder.Default
127+
private Function<Structure, EvaluationContext> contextEnricher = (syncMetadata) -> new ImmutableContext(
128+
syncMetadata.asMap());
129+
112130
/**
113131
* Inject a Custom Connector for fetching flags.
114132
*/

providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java

+33-19
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package dev.openfeature.contrib.providers.flagd;
22

3+
import java.util.ArrayList;
34
import java.util.Collections;
4-
import java.util.Map;
5+
import java.util.List;
6+
import java.util.function.Function;
57

68
import dev.openfeature.contrib.providers.flagd.resolver.Resolver;
79
import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent;
@@ -10,9 +12,13 @@
1012
import dev.openfeature.contrib.providers.flagd.resolver.process.InProcessResolver;
1113
import dev.openfeature.sdk.EvaluationContext;
1214
import dev.openfeature.sdk.EventProvider;
15+
import dev.openfeature.sdk.Hook;
16+
import dev.openfeature.sdk.ImmutableContext;
17+
import dev.openfeature.sdk.ImmutableStructure;
1318
import dev.openfeature.sdk.Metadata;
1419
import dev.openfeature.sdk.ProviderEvaluation;
1520
import dev.openfeature.sdk.ProviderEventDetails;
21+
import dev.openfeature.sdk.Structure;
1622
import dev.openfeature.sdk.Value;
1723
import lombok.extern.slf4j.Slf4j;
1824

@@ -22,13 +28,14 @@
2228
@Slf4j
2329
@SuppressWarnings({ "PMD.TooManyStaticImports", "checkstyle:NoFinalizer" })
2430
public class FlagdProvider extends EventProvider {
31+
private Function<Structure, EvaluationContext> contextEnricher;
2532
private static final String FLAGD_PROVIDER = "flagd";
2633
private final Resolver flagResolver;
2734
private volatile boolean initialized = false;
2835
private volatile boolean connected = false;
29-
private volatile Map<String, Object> syncMetadata = Collections.emptyMap();
30-
31-
private EvaluationContext evaluationContext;
36+
private volatile Structure syncMetadata = new ImmutableStructure();
37+
private volatile EvaluationContext enrichedContext = new ImmutableContext();
38+
private final List<Hook> hooks = new ArrayList<>();
3239

3340
protected final void finalize() {
3441
// DO NOT REMOVE, spotbugs: CT_CONSTRUCTOR_THROW
@@ -62,6 +69,13 @@ public FlagdProvider(final FlagdOptions options) {
6269
throw new IllegalStateException(
6370
String.format("Requested unsupported resolver type of %s", options.getResolverType()));
6471
}
72+
hooks.add(new SyncMetadataHook(this::getEnrichedContext));
73+
contextEnricher = options.getContextEnricher();
74+
}
75+
76+
@Override
77+
public List<Hook> getProviderHooks() {
78+
return Collections.unmodifiableList(hooks);
6579
}
6680

6781
@Override
@@ -70,7 +84,6 @@ public synchronized void initialize(EvaluationContext evaluationContext) throws
7084
return;
7185
}
7286

73-
this.evaluationContext = evaluationContext;
7487
this.flagResolver.init();
7588
this.initialized = true;
7689
}
@@ -97,48 +110,48 @@ public Metadata getMetadata() {
97110

98111
@Override
99112
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
100-
return this.flagResolver.booleanEvaluation(key, defaultValue, mergeContext(ctx));
113+
return this.flagResolver.booleanEvaluation(key, defaultValue, ctx);
101114
}
102115

103116
@Override
104117
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
105-
return this.flagResolver.stringEvaluation(key, defaultValue, mergeContext(ctx));
118+
return this.flagResolver.stringEvaluation(key, defaultValue, ctx);
106119
}
107120

108121
@Override
109122
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
110-
return this.flagResolver.doubleEvaluation(key, defaultValue, mergeContext(ctx));
123+
return this.flagResolver.doubleEvaluation(key, defaultValue, ctx);
111124
}
112125

113126
@Override
114127
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
115-
return this.flagResolver.integerEvaluation(key, defaultValue, mergeContext(ctx));
128+
return this.flagResolver.integerEvaluation(key, defaultValue, ctx);
116129
}
117130

118131
@Override
119132
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
120-
return this.flagResolver.objectEvaluation(key, defaultValue, mergeContext(ctx));
133+
return this.flagResolver.objectEvaluation(key, defaultValue, ctx);
121134
}
122135

123136
/**
124-
* An unmodifiable view of an object map representing the latest result of the
137+
* An unmodifiable view of a Structure representing the latest result of the
125138
* SyncMetadata.
126139
* Set on initial connection and updated with every reconnection.
127140
* see:
128141
* https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1#flagd.sync.v1.FlagSyncService.GetMetadata
129142
*
130143
* @return Object map representing sync metadata
131144
*/
132-
protected Map<String, Object> getSyncMetadata() {
133-
return Collections.unmodifiableMap(syncMetadata);
145+
protected Structure getSyncMetadata() {
146+
return new ImmutableStructure(syncMetadata.asMap());
134147
}
135148

136-
private EvaluationContext mergeContext(final EvaluationContext clientCallCtx) {
137-
if (this.evaluationContext != null) {
138-
return evaluationContext.merge(clientCallCtx);
139-
}
140-
141-
return clientCallCtx;
149+
/**
150+
* The updated context mixed into all evaluations based on the sync-metadata.
151+
* @return context
152+
*/
153+
EvaluationContext getEnrichedContext() {
154+
return enrichedContext;
142155
}
143156

144157
private boolean isConnected() {
@@ -149,6 +162,7 @@ private void onConnectionEvent(ConnectionEvent connectionEvent) {
149162
boolean previous = connected;
150163
boolean current = connected = connectionEvent.isConnected();
151164
syncMetadata = connectionEvent.getSyncMetadata();
165+
enrichedContext = contextEnricher.apply(connectionEvent.getSyncMetadata());
152166

153167
// configuration changed
154168
if (initialized && previous && current) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package dev.openfeature.contrib.providers.flagd;
2+
3+
import java.util.Map;
4+
import java.util.Optional;
5+
import java.util.function.Supplier;
6+
7+
import dev.openfeature.sdk.EvaluationContext;
8+
import dev.openfeature.sdk.Hook;
9+
import dev.openfeature.sdk.HookContext;
10+
11+
class SyncMetadataHook implements Hook<Object> {
12+
13+
private Supplier<EvaluationContext> contextSupplier;
14+
15+
SyncMetadataHook(Supplier<EvaluationContext> contextSupplier) {
16+
this.contextSupplier = contextSupplier;
17+
}
18+
19+
/**
20+
* Return the context adapted from the sync-metadata provided by the supplier.
21+
*/
22+
@Override
23+
public Optional<EvaluationContext> before(HookContext<Object> ctx, Map<String, Object> hints) {
24+
return Optional.ofNullable(contextSupplier.get());
25+
}
26+
}

providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ConnectionEvent.java

+10-9
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
import java.util.Collections;
44
import java.util.List;
5-
import java.util.Map;
65

6+
import dev.openfeature.sdk.ImmutableStructure;
7+
import dev.openfeature.sdk.Structure;
78
import lombok.AllArgsConstructor;
89
import lombok.Getter;
910

@@ -17,15 +18,15 @@ public class ConnectionEvent {
1718
@Getter
1819
private final boolean connected;
1920
private final List<String> flagsChanged;
20-
private final Map<String, Object> syncMetadata;
21+
private final Structure syncMetadata;
2122

2223
/**
2324
* Construct a new ConnectionEvent.
2425
*
2526
* @param connected status of the connection
2627
*/
2728
public ConnectionEvent(boolean connected) {
28-
this(connected, Collections.emptyList(), Collections.emptyMap());
29+
this(connected, Collections.emptyList(), new ImmutableStructure());
2930
}
3031

3132
/**
@@ -35,7 +36,7 @@ public ConnectionEvent(boolean connected) {
3536
* @param flagsChanged list of flags changed
3637
*/
3738
public ConnectionEvent(boolean connected, List<String> flagsChanged) {
38-
this(connected, flagsChanged, Collections.emptyMap());
39+
this(connected, flagsChanged, new ImmutableStructure());
3940
}
4041

4142
/**
@@ -44,8 +45,8 @@ public ConnectionEvent(boolean connected, List<String> flagsChanged) {
4445
* @param connected status of the connection
4546
* @param syncMetadata sync.getMetadata
4647
*/
47-
public ConnectionEvent(boolean connected, Map<String, Object> syncMetadata) {
48-
this(connected, Collections.emptyList(), syncMetadata);
48+
public ConnectionEvent(boolean connected, Structure syncMetadata) {
49+
this(connected, Collections.emptyList(), new ImmutableStructure(syncMetadata.asMap()));
4950
}
5051

5152
/**
@@ -58,11 +59,11 @@ public List<String> getFlagsChanged() {
5859
}
5960

6061
/**
61-
* Get changed sync metadata.
62+
* Get changed sync metadata represented as SDK structure type.
6263
*
6364
* @return an unmodifiable view of the sync metadata
6465
*/
65-
public Map<String, Object> getSyncMetadata() {
66-
return Collections.unmodifiableMap(syncMetadata);
66+
public Structure getSyncMetadata() {
67+
return new ImmutableStructure(syncMetadata.asMap());
6768
}
6869
}

providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/Convert.java

+14
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import com.google.protobuf.Struct;
1313

1414
import dev.openfeature.sdk.EvaluationContext;
15+
import dev.openfeature.sdk.ImmutableContext;
1516
import dev.openfeature.sdk.MutableStructure;
1617
import dev.openfeature.sdk.Structure;
1718
import dev.openfeature.sdk.Value;
@@ -20,6 +21,19 @@
2021
* gRPC type conversion utils.
2122
*/
2223
public class Convert {
24+
25+
/**
26+
* Converts a protobuf struct to EvaluationContext.
27+
*
28+
* @param struct profobuf struct to convert
29+
* @return a context
30+
*/
31+
public static EvaluationContext convertProtobufStructToContext(final Struct struct) {
32+
final HashMap<String, Value> values = new HashMap<>();
33+
struct.getFieldsMap().forEach((key, value) -> values.put(key, convertAny(value)));
34+
return new ImmutableContext(values);
35+
}
36+
2337
/**
2438
* Recursively convert protobuf structure to openfeature value.
2539
*/

providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStore.java

+6-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

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

5-
import java.util.Collections;
65
import java.util.HashMap;
76
import java.util.List;
87
import java.util.Map;
@@ -19,6 +18,8 @@
1918
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.Connector;
2019
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload;
2120
import dev.openfeature.flagd.grpc.sync.Sync.GetMetadataResponse;
21+
import dev.openfeature.sdk.ImmutableStructure;
22+
import dev.openfeature.sdk.Structure;
2223
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
2324
import lombok.extern.slf4j.Slf4j;
2425

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

146-
private Map<String, Object> parseSyncMetadata(GetMetadataResponse metadataResponse) {
147+
private Structure parseSyncMetadata(GetMetadataResponse metadataResponse) {
147148
try {
148-
return convertProtobufMapToStructure(metadataResponse.getMetadata().getFieldsMap())
149-
.asObjectMap();
149+
return convertProtobufMapToStructure(metadataResponse.getMetadata().getFieldsMap());
150150
} catch (Exception exception) {
151151
log.error("Failed to parse metadataResponse, provider metadata may not be up-to-date");
152152
}
153-
return Collections.emptyMap();
153+
return new ImmutableStructure();
154154
}
155155

156156
private List<String> getChangedFlagsKeys(Map<String, FeatureFlag> newFlags) {

0 commit comments

Comments
 (0)