diff --git a/openfeature-provider-local/README.md b/openfeature-provider-local/README.md
new file mode 100644
index 00000000..5b136c9d
--- /dev/null
+++ b/openfeature-provider-local/README.md
@@ -0,0 +1,95 @@
+# Confidence OpenFeature Local Provider
+
+
+
+A high-performance OpenFeature provider for [Confidence](https://confidence.spotify.com/) feature flags that evaluates flags locally for minimal latency.
+
+## Features
+
+- **Local Resolution**: Evaluates feature flags locally using WebAssembly (WASM) or pure Java
+- **Low Latency**: No network calls during flag evaluation
+- **Automatic Sync**: Periodically syncs flag configurations from Confidence
+- **Exposure Logging**: Fully supported exposure logging (and other resolve analytics)
+- **OpenFeature Compatible**: Works with the standard OpenFeature SDK
+
+## Installation
+
+Add this dependency to your `pom.xml`:
+
+```xml
+
+ com.spotify.confidence
+ openfeature-provider-local
+ 0.2.4
+
+```
+
+## Quick Start
+
+```java
+import com.spotify.confidence.ApiSecret;
+import com.spotify.confidence.OpenFeatureLocalResolveProvider;
+import dev.openfeature.sdk.OpenFeatureAPI;
+import dev.openfeature.sdk.Client;
+
+// Create API credentials
+ApiSecret apiSecret = new ApiSecret("your-client-id", "your-client-secret");
+String clientSecret = "your-application-client-secret";
+
+// Create and register the provider
+OpenFeatureLocalResolveProvider provider =
+ new OpenFeatureLocalResolveProvider(apiSecret, clientSecret);
+OpenFeatureAPI.getInstance().setProvider(provider);
+
+// Use OpenFeature client
+Client client = OpenFeatureAPI.getInstance().getClient();
+String value = client.getStringValue("my-flag", "default-value");
+```
+
+## Configuration
+
+### Resolution Modes
+
+The provider supports two resolution modes:
+
+- **WASM mode** (default): Uses WebAssembly resolver
+- **Java mode**: Pure Java implementation of the resolver
+
+Control the mode with the `LOCAL_RESOLVE_MODE` environment variable:
+
+```bash
+# Force WASM mode
+export LOCAL_RESOLVE_MODE=WASM
+
+# Force Java mode
+export LOCAL_RESOLVE_MODE=JAVA
+```
+
+### Exposure Logging
+
+Enable or disable exposure logging:
+
+```java
+// Enable exposure logging (default)
+new OpenFeatureLocalResolveProvider(apiSecret, clientSecret, true);
+
+// Disable exposure logging
+new OpenFeatureLocalResolveProvider(apiSecret, clientSecret, false);
+```
+
+## Credentials
+
+You need two types of credentials:
+
+1. **API Secret** (`ApiSecret`): For authenticating with the Confidence API
+ - Contains `clientId` and `clientSecret` for your Confidence application
+
+2. **Client Secret** (`String`): For flag resolution authentication
+ - Application-specific secret for flag evaluation
+
+Both can be obtained from your Confidence dashboard.
+
+## Requirements
+
+- Java 17+
+- OpenFeature SDK 1.6.1+
\ No newline at end of file
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/ApiSecret.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/ApiSecret.java
index f30fdb5d..79214fd1 100644
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/ApiSecret.java
+++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/ApiSecret.java
@@ -1,3 +1,13 @@
package com.spotify.confidence;
+/**
+ * API credentials for authenticating with the Confidence service.
+ *
+ *
This record holds the client ID and client secret used to authenticate with the Confidence API
+ * for administrative operations like fetching flag configurations and logging exposure events.
+ *
+ * @param clientId the client ID for your Confidence application
+ * @param clientSecret the client secret for your Confidence application
+ * @since 0.2.4
+ */
public record ApiSecret(String clientId, String clientSecret) {}
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/LocalResolverServiceFactory.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/LocalResolverServiceFactory.java
index ef5a99ed..863411ac 100644
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/LocalResolverServiceFactory.java
+++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/LocalResolverServiceFactory.java
@@ -37,6 +37,7 @@ class LocalResolverServiceFactory implements ResolverServiceFactory {
private final Supplier resolveIdSupplier;
private final ResolveLogger resolveLogger;
private final AssignLogger assignLogger;
+ private final boolean enableExposureLogs;
private static final MetricRegistry metricRegistry = new MetricRegistry();
private static final String CONFIDENCE_DOMAIN = "edge-grpc.spotify.com";
private static final Duration ASSIGN_LOG_INTERVAL = Duration.ofSeconds(10);
@@ -59,11 +60,16 @@ private static ManagedChannel createConfidenceChannel() {
}
static FlagResolverService from(ApiSecret apiSecret, String clientSecret, boolean isWasm) {
- return createFlagResolverService(apiSecret, clientSecret, isWasm);
+ return createFlagResolverService(apiSecret, clientSecret, isWasm, true);
+ }
+
+ static FlagResolverService from(
+ ApiSecret apiSecret, String clientSecret, boolean isWasm, boolean enableExposureLogs) {
+ return createFlagResolverService(apiSecret, clientSecret, isWasm, enableExposureLogs);
}
private static FlagResolverService createFlagResolverService(
- ApiSecret apiSecret, String clientSecret, boolean isWasm) {
+ ApiSecret apiSecret, String clientSecret, boolean isWasm, boolean enableExposureLogs) {
final var channel = createConfidenceChannel();
final AuthServiceGrpc.AuthServiceBlockingStub authService =
AuthServiceGrpc.newBlockingStub(channel);
@@ -97,7 +103,7 @@ private static FlagResolverService createFlagResolverService(
flagLoggerStub, ASSIGN_LOG_INTERVAL, metricRegistry, assignLogCapacity);
final ResolveLogger resolveLogger =
ResolveLogger.createStarted(() -> flagsAdminStub, RESOLVE_INFO_LOG_INTERVAL);
- final var flagLogger = getFlagLogger(resolveLogger, assignLogger);
+ final var flagLogger = getFlagLogger(resolveLogger, assignLogger, enableExposureLogs);
if (isWasm) {
final SwapWasmResolverApi wasmResolverApi =
new SwapWasmResolverApi(
@@ -116,7 +122,8 @@ private static FlagResolverService createFlagResolverService(
sidecarFlagsAdminFetcher.stateHolder(),
resolveTokenConverter,
resolveLogger,
- assignLogger)
+ assignLogger,
+ enableExposureLogs)
.create(clientSecret);
} else {
flagsFetcherExecutor.scheduleWithFixedDelay(
@@ -128,11 +135,30 @@ private static FlagResolverService createFlagResolverService(
sidecarFlagsAdminFetcher.stateHolder(),
resolveTokenConverter,
resolveLogger,
- assignLogger)
+ assignLogger,
+ enableExposureLogs)
.create(clientSecret);
}
}
+ LocalResolverServiceFactory(
+ SwapWasmResolverApi wasmResolveApi,
+ AtomicReference resolverStateHolder,
+ ResolveTokenConverter resolveTokenConverter,
+ ResolveLogger resolveLogger,
+ AssignLogger assignLogger,
+ boolean enableExposureLogs) {
+ this(
+ wasmResolveApi,
+ resolverStateHolder,
+ resolveTokenConverter,
+ Instant::now,
+ () -> RandomStringUtils.randomAlphanumeric(32),
+ resolveLogger,
+ assignLogger,
+ enableExposureLogs);
+ }
+
LocalResolverServiceFactory(
SwapWasmResolverApi wasmResolveApi,
AtomicReference resolverStateHolder,
@@ -146,7 +172,25 @@ private static FlagResolverService createFlagResolverService(
Instant::now,
() -> RandomStringUtils.randomAlphanumeric(32),
resolveLogger,
- assignLogger);
+ assignLogger,
+ true);
+ }
+
+ LocalResolverServiceFactory(
+ AtomicReference resolverStateHolder,
+ ResolveTokenConverter resolveTokenConverter,
+ ResolveLogger resolveLogger,
+ AssignLogger assignLogger,
+ boolean enableExposureLogs) {
+ this(
+ null,
+ resolverStateHolder,
+ resolveTokenConverter,
+ Instant::now,
+ () -> RandomStringUtils.randomAlphanumeric(32),
+ resolveLogger,
+ assignLogger,
+ enableExposureLogs);
}
LocalResolverServiceFactory(
@@ -161,7 +205,8 @@ private static FlagResolverService createFlagResolverService(
Instant::now,
() -> RandomStringUtils.randomAlphanumeric(32),
resolveLogger,
- assignLogger);
+ assignLogger,
+ true);
}
LocalResolverServiceFactory(
@@ -171,7 +216,8 @@ private static FlagResolverService createFlagResolverService(
Supplier timeSupplier,
Supplier resolveIdSupplier,
ResolveLogger resolveLogger,
- AssignLogger assignLogger) {
+ AssignLogger assignLogger,
+ boolean enableExposureLogs) {
this.wasmResolveApi = wasmResolveApi;
this.resolverStateHolder = resolverStateHolder;
this.resolveTokenConverter = resolveTokenConverter;
@@ -179,6 +225,7 @@ private static FlagResolverService createFlagResolverService(
this.resolveIdSupplier = resolveIdSupplier;
this.resolveLogger = resolveLogger;
this.assignLogger = assignLogger;
+ this.enableExposureLogs = enableExposureLogs;
}
@VisibleForTesting
@@ -207,7 +254,7 @@ private FlagResolverService createJavaFlagResolverService(
}
final AccountState accountState = state.accountStates().get(accountClient.accountName());
- final var flagLogger = getFlagLogger(resolveLogger, assignLogger);
+ final var flagLogger = getFlagLogger(resolveLogger, assignLogger, enableExposureLogs);
return new JavaFlagResolverService(
accountState,
@@ -218,7 +265,31 @@ private FlagResolverService createJavaFlagResolverService(
resolveIdSupplier);
}
- private static FlagLogger getFlagLogger(ResolveLogger resolveLogger, AssignLogger assignLogger) {
+ private static FlagLogger getFlagLogger(
+ ResolveLogger resolveLogger, AssignLogger assignLogger, boolean enableExposureLogs) {
+ if (!enableExposureLogs) {
+ return new FlagLogger() {
+ @Override
+ public void logResolve(
+ String resolveId,
+ Struct evaluationContext,
+ Sdk sdk,
+ AccountClient accountClient,
+ List values) {
+ // Logging disabled - no-op
+ }
+
+ @Override
+ public void logAssigns(
+ String resolveId,
+ Sdk sdk,
+ List flagsToApply,
+ AccountClient accountClient) {
+ // Logging disabled - no-op
+ }
+ };
+ }
+
return new FlagLogger() {
@Override
public void logResolve(
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/OpenFeatureLocalResolveProvider.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/OpenFeatureLocalResolveProvider.java
index 1c8deb15..295e7d63 100644
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/OpenFeatureLocalResolveProvider.java
+++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/OpenFeatureLocalResolveProvider.java
@@ -18,6 +18,49 @@
import java.util.function.Function;
import org.slf4j.Logger;
+/**
+ * OpenFeature provider for Confidence feature flags using local resolution.
+ *
+ * This provider evaluates feature flags locally using either a WebAssembly (WASM) resolver or a
+ * pure Java implementation. It periodically syncs flag configurations from the Confidence service
+ * and caches them locally for fast, low-latency flag evaluation.
+ *
+ *
The provider supports two resolution modes:
+ *
+ *
+ * - WASM mode (default): Uses a WebAssembly resolver
+ *
- Java mode: Uses a pure Java resolver
+ *
+ *
+ * Resolution mode can be controlled via the {@code LOCAL_RESOLVE_MODE} environment variable:
+ *
+ *
+ * - {@code LOCAL_RESOLVE_MODE=WASM} - Forces WASM mode
+ *
- {@code LOCAL_RESOLVE_MODE=JAVA} - Forces Java mode
+ *
- Not set - Defaults to WASM mode
+ *
+ *
+ * Usage Example:
+ *
+ *
{@code
+ * // Create API credentials
+ * ApiSecret apiSecret = new ApiSecret("your-client-id", "your-client-secret");
+ * String clientSecret = "your-application-client-secret";
+ *
+ * // Create provider with default settings (exposure logs enabled)
+ * OpenFeatureLocalResolveProvider provider =
+ * new OpenFeatureLocalResolveProvider(apiSecret, clientSecret);
+ *
+ * // Register with OpenFeature
+ * OpenFeatureAPI.getInstance().setProvider(provider);
+ *
+ * // Use with OpenFeature client
+ * Client client = OpenFeatureAPI.getInstance().getClient();
+ * String flagValue = client.getStringValue("my-flag", "default-value");
+ * }
+ *
+ * @since 0.2.4
+ */
@Experimental
public class OpenFeatureLocalResolveProvider implements FeatureProvider {
private final String clientSecret;
@@ -25,12 +68,55 @@ public class OpenFeatureLocalResolveProvider implements FeatureProvider {
org.slf4j.LoggerFactory.getLogger(OpenFeatureLocalResolveProvider.class);
private final FlagResolverService flagResolverService;
+ /**
+ * Creates a new OpenFeature provider for local flag resolution with exposure logging enabled.
+ *
+ * This constructor enables exposure logging by default, which means flag evaluations will be
+ * logged to the Confidence service for populating exposure data and other analytics.
+ *
+ *
This is equivalent to calling {@code new OpenFeatureLocalResolveProvider(apiSecret,
+ * clientSecret, true)}.
+ *
+ * @param apiSecret the API credentials for authenticating with the Confidence service
+ * @param clientSecret the client secret for your application
+ * @see #OpenFeatureLocalResolveProvider(ApiSecret, String, boolean) for detailed parameter
+ * documentation
+ */
public OpenFeatureLocalResolveProvider(ApiSecret apiSecret, String clientSecret) {
+ this(apiSecret, clientSecret, true);
+ }
+
+ /**
+ * Creates a new OpenFeature provider for local flag resolution with configurable exposure
+ * logging.
+ *
+ *
This is the primary constructor that allows full control over the provider configuration.
+ * The provider will automatically determine the resolution mode (WASM or Java) based on the
+ * {@code LOCAL_RESOLVE_MODE} environment variable, defaulting to WASM mode.
+ *
+ * @param apiSecret the API credentials containing client ID and client secret for authenticating
+ * with the Confidence service. Create using {@code new ApiSecret("client-id",
+ * "client-secret")}
+ * @param clientSecret the client secret for your application, used for flag resolution
+ * authentication. This is different from the API secret and is specific to your application
+ * configuration
+ * @param enableExposureLogs whether to enable exposure logging. When {@code true}, flag
+ * evaluations are logged to Confidence. When {@code false}, evaluations are not logged,
+ * useful when debugging.
+ * @since 0.2.4
+ */
+ public OpenFeatureLocalResolveProvider(
+ ApiSecret apiSecret, String clientSecret, boolean enableExposureLogs) {
final var env = System.getenv("LOCAL_RESOLVE_MODE");
if (env != null && env.equals("WASM")) {
- this.flagResolverService = LocalResolverServiceFactory.from(apiSecret, clientSecret, true);
+ this.flagResolverService =
+ LocalResolverServiceFactory.from(apiSecret, clientSecret, true, enableExposureLogs);
+ } else if (env != null && env.equals("JAVA")) {
+ this.flagResolverService =
+ LocalResolverServiceFactory.from(apiSecret, clientSecret, false, enableExposureLogs);
} else {
- this.flagResolverService = LocalResolverServiceFactory.from(apiSecret, clientSecret, false);
+ this.flagResolverService =
+ LocalResolverServiceFactory.from(apiSecret, clientSecret, true, enableExposureLogs);
}
this.clientSecret = clientSecret;
}
diff --git a/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java b/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java
index e4868390..88263eb4 100644
--- a/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java
+++ b/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java
@@ -67,8 +67,7 @@ public void logAssigns(
wasmResolverApi, resolverState, resolveTokenConverter, mock(), mock());
} else {
resolverServiceFactory =
- new LocalResolverServiceFactory(
- null, resolverState, resolveTokenConverter, mock(), mock());
+ new LocalResolverServiceFactory(resolverState, resolveTokenConverter, mock(), mock());
}
}