Skip to content

feat: Add experimental plugin support #76

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

Merged
merged 2 commits into from
Jun 27, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.launchdarkly.sdk.server.ComponentsImpl.LoggingConfigurationBuilderImpl;
import com.launchdarkly.sdk.server.ComponentsImpl.NullDataSourceFactory;
import com.launchdarkly.sdk.server.ComponentsImpl.PersistentDataStoreBuilderImpl;
import com.launchdarkly.sdk.server.ComponentsImpl.PluginsConfigurationBuilderImpl;
import com.launchdarkly.sdk.server.ComponentsImpl.PollingDataSourceBuilderImpl;
import com.launchdarkly.sdk.server.ComponentsImpl.ServiceEndpointsBuilderImpl;
import com.launchdarkly.sdk.server.ComponentsImpl.StreamingDataSourceBuilderImpl;
Expand All @@ -21,6 +22,7 @@
import com.launchdarkly.sdk.server.integrations.HttpConfigurationBuilder;
import com.launchdarkly.sdk.server.integrations.LoggingConfigurationBuilder;
import com.launchdarkly.sdk.server.integrations.PersistentDataStoreBuilder;
import com.launchdarkly.sdk.server.integrations.PluginsConfigurationBuilder;
import com.launchdarkly.sdk.server.integrations.PollingDataSourceBuilder;
import com.launchdarkly.sdk.server.integrations.ServiceEndpointsBuilder;
import com.launchdarkly.sdk.server.integrations.StreamingDataSourceBuilder;
Expand Down Expand Up @@ -445,6 +447,26 @@ public static HooksConfigurationBuilder hooks() {
return new HooksConfigurationBuilderImpl();
}

/**
* Returns a builder for configuring plugins.
*
* Passing this to {@link LDConfig.Builder#plugins(com.launchdarkly.sdk.server.integrations.PluginsConfigurationBuilder)},
* after setting any desired plugins on the builder, applies this configuration to the SDK.
* <pre><code>
* List plugins = myCreatePluginsFunc();
* LDConfig config = new LDConfig.Builder()
* .plugins(
* Components.plugins()
* .setPlugins(plugins)
* )
* .build();
* </code></pre>
* @return a {@link PluginsConfigurationBuilder} that can be used for customization
*/
public static PluginsConfigurationBuilder plugins() {
return new PluginsConfigurationBuilderImpl();
}

/**
* Returns a wrapper information builder.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.launchdarkly.sdk.server.integrations.HttpConfigurationBuilder;
import com.launchdarkly.sdk.server.integrations.LoggingConfigurationBuilder;
import com.launchdarkly.sdk.server.integrations.PersistentDataStoreBuilder;
import com.launchdarkly.sdk.server.integrations.PluginsConfigurationBuilder;
import com.launchdarkly.sdk.server.integrations.PollingDataSourceBuilder;
import com.launchdarkly.sdk.server.integrations.ServiceEndpointsBuilder;
import com.launchdarkly.sdk.server.integrations.StreamingDataSourceBuilder;
Expand All @@ -35,6 +36,8 @@
import com.launchdarkly.sdk.server.subsystems.HttpConfiguration;
import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration;
import com.launchdarkly.sdk.server.subsystems.PersistentDataStore;
import com.launchdarkly.sdk.server.subsystems.PluginsConfiguration;

import okhttp3.Credentials;

import java.io.IOException;
Expand Down Expand Up @@ -478,6 +481,19 @@ public HookConfiguration build() {
}
}

static final class PluginsConfigurationBuilderImpl extends PluginsConfigurationBuilder {
public static PluginsConfigurationBuilderImpl fromPluginsConfiguration(PluginsConfiguration pluginsConfiguration) {
PluginsConfigurationBuilderImpl builder = new PluginsConfigurationBuilderImpl();
builder.setPlugins(pluginsConfiguration.getPlugins());
return builder;
}

@Override
public PluginsConfiguration build() {
return new PluginsConfiguration(plugins);
}
}

static final class WrapperInfoBuilderImpl extends WrapperInfoBuilder {
public WrapperInfoBuilderImpl() {
this(null, null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
import com.launchdarkly.sdk.LDValue;
import com.launchdarkly.sdk.LDValueType;
import com.launchdarkly.sdk.internal.http.HttpHelpers;
import com.launchdarkly.sdk.server.integrations.EnvironmentMetadata;
import com.launchdarkly.sdk.server.integrations.Hook;
import com.launchdarkly.sdk.server.integrations.Plugin;
import com.launchdarkly.sdk.server.integrations.SdkMetadata;
import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreStatusProvider;
import com.launchdarkly.sdk.server.interfaces.BigSegmentsConfiguration;
import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider;
Expand All @@ -30,6 +34,9 @@
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
Expand Down Expand Up @@ -206,13 +213,33 @@ public LDClient(String sdkKey, LDConfig config) {

EvaluatorInterface evaluator = new InputValidatingEvaluator(dataStore, bigSegmentStoreWrapper, eventProcessor, evaluationLogger);

// build environment metadata for plugins
SdkMetadata sdkMetadata;
if (config.wrapperInfo == null) {
sdkMetadata = new SdkMetadata("JavaClient", Version.SDK_VERSION);
} else {
sdkMetadata = new SdkMetadata("JavaClient", Version.SDK_VERSION, config.wrapperInfo.getWrapperName(), config.wrapperInfo.getWrapperVersion());
}
EnvironmentMetadata environmentMetadata = new EnvironmentMetadata(config.applicationInfo, sdkMetadata, sdkKey);

// add plugin hooks
List<Hook> allHooks = new ArrayList<>(config.hooks.getHooks());
for (Plugin plugin : config.plugins.getPlugins()) {
try {
allHooks.addAll(plugin.getHooks(environmentMetadata));
} catch (Exception e) {
baseLogger.error("Exception thrown getting hooks for plugin " + plugin.getMetadata().getName() + ". Unable to get hooks, plugin will not be registered.");
}
}
allHooks = Collections.unmodifiableList(allHooks);

// decorate evaluator with hooks if hooks were provided
if (config.hooks.getHooks().isEmpty()) {
if (allHooks.isEmpty()) {
this.evaluator = evaluator;
this.migrationEvaluator = new MigrationStageEnforcingEvaluator(evaluator, evaluationLogger);
} else {
this.evaluator = new EvaluatorWithHooks(evaluator, config.hooks.getHooks(), this.baseLogger.subLogger(Loggers.HOOKS_LOGGER_NAME));
this.migrationEvaluator = new EvaluatorWithHooks(new MigrationStageEnforcingEvaluator(evaluator, evaluationLogger), config.hooks.getHooks(), this.baseLogger.subLogger(Loggers.HOOKS_LOGGER_NAME));
this.evaluator = new EvaluatorWithHooks(evaluator, allHooks, this.baseLogger.subLogger(Loggers.HOOKS_LOGGER_NAME));
this.migrationEvaluator = new EvaluatorWithHooks(new MigrationStageEnforcingEvaluator(evaluator, evaluationLogger), allHooks, this.baseLogger.subLogger(Loggers.HOOKS_LOGGER_NAME));
}

this.flagChangeBroadcaster = EventBroadcasterImpl.forFlagChangeEvents(sharedExecutor, baseLogger);
Expand All @@ -236,6 +263,15 @@ public LDClient(String sdkKey, LDConfig config) {
this.dataSource = config.dataSource.build(context.withDataSourceUpdateSink(dataSourceUpdates));
this.dataSourceStatusProvider = new DataSourceStatusProviderImpl(dataSourceStatusNotifier, dataSourceUpdates);

// register plugins as soon as possible after client is valid
for (Plugin plugin : config.plugins.getPlugins()) {
try {
plugin.register(this, environmentMetadata);
} catch (Exception e) {
baseLogger.error("Exception thrown registering plugin " + plugin.getMetadata().getName() + ". Plugin will not be registered.");
}
}

Future<Void> startFuture = dataSource.start();
if (!config.startWait.isZero() && !config.startWait.isNegative()) {
if (!(dataSource instanceof ComponentsImpl.NullDataSource)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.launchdarkly.sdk.EvaluationReason.BigSegmentsStatus;
import com.launchdarkly.sdk.server.integrations.ApplicationInfoBuilder;
import com.launchdarkly.sdk.server.integrations.HooksConfigurationBuilder;
import com.launchdarkly.sdk.server.integrations.PluginsConfigurationBuilder;
import com.launchdarkly.sdk.server.integrations.ServiceEndpointsBuilder;
import com.launchdarkly.sdk.server.integrations.WrapperInfoBuilder;
import com.launchdarkly.sdk.server.interfaces.ApplicationInfo;
Expand All @@ -17,6 +18,7 @@
import com.launchdarkly.sdk.server.subsystems.HookConfiguration;
import com.launchdarkly.sdk.server.subsystems.HttpConfiguration;
import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration;
import com.launchdarkly.sdk.server.subsystems.PluginsConfiguration;

import java.time.Duration;

Expand All @@ -38,6 +40,7 @@ public final class LDConfig {
final boolean diagnosticOptOut;
final ComponentConfigurer<EventProcessor> events;
final HookConfiguration hooks;
final PluginsConfiguration plugins;
final ComponentConfigurer<HttpConfiguration> http;
final ComponentConfigurer<LoggingConfiguration> logging;
final ServiceEndpoints serviceEndpoints;
Expand All @@ -61,6 +64,7 @@ protected LDConfig(Builder builder) {
this.dataStore = builder.dataStore == null ? Components.inMemoryDataStore() : builder.dataStore;
this.diagnosticOptOut = builder.diagnosticOptOut;
this.hooks = (builder.hooksConfigurationBuilder == null ? Components.hooks() : builder.hooksConfigurationBuilder).build();
this.plugins = (builder.pluginsConfigurationBuilder == null ? Components.plugins() : builder.pluginsConfigurationBuilder).build();
this.http = builder.http == null ? Components.httpConfiguration() : builder.http;
this.logging = builder.logging == null ? Components.logging() : builder.logging;
this.offline = builder.offline;
Expand Down Expand Up @@ -91,6 +95,7 @@ public static class Builder {
private boolean diagnosticOptOut = false;
private ComponentConfigurer<EventProcessor> events = null;
private HooksConfigurationBuilder hooksConfigurationBuilder = null;
private PluginsConfigurationBuilder pluginsConfigurationBuilder = null;
private ComponentConfigurer<HttpConfiguration> http = null;
private ComponentConfigurer<LoggingConfiguration> logging = null;
private ServiceEndpointsBuilder serviceEndpointsBuilder = null;
Expand Down Expand Up @@ -120,6 +125,7 @@ public static Builder fromConfig(LDConfig config) {
newBuilder.diagnosticOptOut = config.diagnosticOptOut;
newBuilder.events = config.events;
newBuilder.hooksConfigurationBuilder = ComponentsImpl.HooksConfigurationBuilderImpl.fromHooksConfiguration(config.hooks);
newBuilder.pluginsConfigurationBuilder = ComponentsImpl.PluginsConfigurationBuilderImpl.fromPluginsConfiguration(config.plugins);
newBuilder.http = config.http;
newBuilder.logging = config.logging;

Expand Down Expand Up @@ -270,6 +276,22 @@ public Builder hooks(HooksConfigurationBuilder hooksConfiguration) {
return this;
}

/**
* Sets the SDK's plugins configuration, using a builder. This is normally a obtained from
* {@link Components#plugins()} ()}, which has methods for setting individual other plugin
* related properties.
* <p>
* Plugin support is currently experimental and subject to change.
*
* @param pluginsConfiguration the plugins configuration builder
* @return the main configuration builder
* @see Components#plugins()
*/
public Builder plugins(PluginsConfigurationBuilder pluginsConfiguration) {
this.pluginsConfigurationBuilder = pluginsConfiguration;
return this;
}

/**
* Sets the SDK's networking configuration, using a configuration builder. This builder is
* obtained from {@link Components#httpConfiguration()}, and has methods for setting individual
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.launchdarkly.sdk.server.integrations;

import com.launchdarkly.sdk.server.interfaces.ApplicationInfo;

/**
* Metadata about the environment that flag evaluations or other functionalities are being performed in.
*/
public final class EnvironmentMetadata {
private final ApplicationInfo applicationInfo;
private final SdkMetadata sdkMetadata;
private final String sdkKey;

/**
* @param applicationInfo for the application this SDK is used in
* @param sdkMetadata for the LaunchDarkly SDK
* @param sdkKey for the key used to initialize the SDK client
*/
public EnvironmentMetadata(ApplicationInfo applicationInfo, SdkMetadata sdkMetadata, String sdkKey) {
this.applicationInfo = applicationInfo;
this.sdkMetadata = sdkMetadata;
this.sdkKey = sdkKey;
}

/**
* @return the {@link ApplicationInfo} for the application this SDK is used in.
*/
public ApplicationInfo getApplicationInfo() {
return applicationInfo;
}

/**
* @return the {@link SdkMetadata} for the LaunchDarkly SDK.
*/
public SdkMetadata getSdkMetadata() {
return sdkMetadata;
}

/**
* @return the key used to initialize the SDK client
*/
public String getSdkKey() {
return sdkKey;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.launchdarkly.sdk.server.integrations;

import com.launchdarkly.sdk.server.LDClient;

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

/**
* Abstract class that you can extend to create a plugin to the LaunchDarkly SDK.
*/
public abstract class Plugin {
/**
* @return the {@link PluginMetadata} that gives details about the plugin.
*/
public abstract PluginMetadata getMetadata();

/**
* Registers the plugin with the SDK. Called once during SDK initialization.
* The SDK initialization will typically not have been completed at this point, so the plugin should take appropriate
* actions to ensure the SDK is ready before sending track events or evaluating flags.
*
* @param client for the plugin to use
* @param metadata metadata about the environment where the plugin is running.
*/
public abstract void register(LDClient client, EnvironmentMetadata metadata);

/**
* Gets a list of hooks that the plugin wants to register.
* This method will be called once during SDK initialization before the register method is called.
* If the plugin does not need to register any hooks, this method doesn't need to be implemented.
*
* @param metadata metadata about the environment where the plugin is running.
* @return a list of hooks that the plugin wants to register.
*/
public List<Hook> getHooks(EnvironmentMetadata metadata) {
return Collections.emptyList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.launchdarkly.sdk.server.integrations;

/**
* PluginMetadata contains information about a specific plugin implementation
*/
public abstract class PluginMetadata {
/**
* @return the name of the plugin implementation
*/
public abstract String getName();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.launchdarkly.sdk.server.integrations;

import com.launchdarkly.sdk.server.Components;
import com.launchdarkly.sdk.server.subsystems.PluginsConfiguration;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
* Contains methods for configuring the SDK's 'plugins'.
* <p>
* If you want to add plugins, use {@link Components#plugins()}, configure accordingly, and pass it
* to {@link com.launchdarkly.sdk.server.LDConfig.Builder#plugins(PluginsConfigurationBuilder)}.
*
* <pre><code>
* List plugins = getPluginsFunc();
* LDConfig config = new LDConfig.Builder()
* .plugins(
* Components.plugins()
* .setPlugins(plugins)
* )
* .build();
* </code></pre>
* <p>
* Note that this class is abstract; the actual implementation is created by calling {@link Components#plugins()}.
*/
public abstract class PluginsConfigurationBuilder {
/**
* The current set of plugins the builder has.
*/
protected List<Plugin> plugins = Collections.emptyList();

/**
* Sets the provided list of plugins on the configuration. Note that the order of plugins is important and controls
* the order in which they will be registered. See {@link Plugin} for more details.
*
* @param plugins to be set on the configuration
* @return the builder
*/
public PluginsConfigurationBuilder setPlugins(List<Plugin> plugins) {
// copy to avoid list manipulations impacting the SDK
this.plugins = Collections.unmodifiableList(new ArrayList<>(plugins));
return this;
}

/**
* @return the plugins configuration
*/
public abstract PluginsConfiguration build();
}
Loading