Skip to content

Commit 13dc0e4

Browse files
feat: Add experimental plugin support (#76)
**Requirements** - [X] I have added test coverage for new or changed functionality - [X] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) - [X] I have validated my changes against all supported platform versions **Describe the solution you've provided** Adds experimental plugin support
1 parent 700d0f4 commit 13dc0e4

File tree

14 files changed

+619
-3
lines changed

14 files changed

+619
-3
lines changed

lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Components.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.launchdarkly.sdk.server.ComponentsImpl.LoggingConfigurationBuilderImpl;
1111
import com.launchdarkly.sdk.server.ComponentsImpl.NullDataSourceFactory;
1212
import com.launchdarkly.sdk.server.ComponentsImpl.PersistentDataStoreBuilderImpl;
13+
import com.launchdarkly.sdk.server.ComponentsImpl.PluginsConfigurationBuilderImpl;
1314
import com.launchdarkly.sdk.server.ComponentsImpl.PollingDataSourceBuilderImpl;
1415
import com.launchdarkly.sdk.server.ComponentsImpl.ServiceEndpointsBuilderImpl;
1516
import com.launchdarkly.sdk.server.ComponentsImpl.StreamingDataSourceBuilderImpl;
@@ -21,6 +22,7 @@
2122
import com.launchdarkly.sdk.server.integrations.HttpConfigurationBuilder;
2223
import com.launchdarkly.sdk.server.integrations.LoggingConfigurationBuilder;
2324
import com.launchdarkly.sdk.server.integrations.PersistentDataStoreBuilder;
25+
import com.launchdarkly.sdk.server.integrations.PluginsConfigurationBuilder;
2426
import com.launchdarkly.sdk.server.integrations.PollingDataSourceBuilder;
2527
import com.launchdarkly.sdk.server.integrations.ServiceEndpointsBuilder;
2628
import com.launchdarkly.sdk.server.integrations.StreamingDataSourceBuilder;
@@ -445,6 +447,26 @@ public static HooksConfigurationBuilder hooks() {
445447
return new HooksConfigurationBuilderImpl();
446448
}
447449

450+
/**
451+
* Returns a builder for configuring plugins.
452+
*
453+
* Passing this to {@link LDConfig.Builder#plugins(com.launchdarkly.sdk.server.integrations.PluginsConfigurationBuilder)},
454+
* after setting any desired plugins on the builder, applies this configuration to the SDK.
455+
* <pre><code>
456+
* List plugins = myCreatePluginsFunc();
457+
* LDConfig config = new LDConfig.Builder()
458+
* .plugins(
459+
* Components.plugins()
460+
* .setPlugins(plugins)
461+
* )
462+
* .build();
463+
* </code></pre>
464+
* @return a {@link PluginsConfigurationBuilder} that can be used for customization
465+
*/
466+
public static PluginsConfigurationBuilder plugins() {
467+
return new PluginsConfigurationBuilderImpl();
468+
}
469+
448470
/**
449471
* Returns a wrapper information builder.
450472
* <p>

lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import com.launchdarkly.sdk.server.integrations.HttpConfigurationBuilder;
1818
import com.launchdarkly.sdk.server.integrations.LoggingConfigurationBuilder;
1919
import com.launchdarkly.sdk.server.integrations.PersistentDataStoreBuilder;
20+
import com.launchdarkly.sdk.server.integrations.PluginsConfigurationBuilder;
2021
import com.launchdarkly.sdk.server.integrations.PollingDataSourceBuilder;
2122
import com.launchdarkly.sdk.server.integrations.ServiceEndpointsBuilder;
2223
import com.launchdarkly.sdk.server.integrations.StreamingDataSourceBuilder;
@@ -35,6 +36,8 @@
3536
import com.launchdarkly.sdk.server.subsystems.HttpConfiguration;
3637
import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration;
3738
import com.launchdarkly.sdk.server.subsystems.PersistentDataStore;
39+
import com.launchdarkly.sdk.server.subsystems.PluginsConfiguration;
40+
3841
import okhttp3.Credentials;
3942

4043
import java.io.IOException;
@@ -478,6 +481,19 @@ public HookConfiguration build() {
478481
}
479482
}
480483

484+
static final class PluginsConfigurationBuilderImpl extends PluginsConfigurationBuilder {
485+
public static PluginsConfigurationBuilderImpl fromPluginsConfiguration(PluginsConfiguration pluginsConfiguration) {
486+
PluginsConfigurationBuilderImpl builder = new PluginsConfigurationBuilderImpl();
487+
builder.setPlugins(pluginsConfiguration.getPlugins());
488+
return builder;
489+
}
490+
491+
@Override
492+
public PluginsConfiguration build() {
493+
return new PluginsConfiguration(plugins);
494+
}
495+
}
496+
481497
static final class WrapperInfoBuilderImpl extends WrapperInfoBuilder {
482498
public WrapperInfoBuilderImpl() {
483499
this(null, null);

lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDClient.java

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
import com.launchdarkly.sdk.LDValue;
1010
import com.launchdarkly.sdk.LDValueType;
1111
import com.launchdarkly.sdk.internal.http.HttpHelpers;
12+
import com.launchdarkly.sdk.server.integrations.EnvironmentMetadata;
13+
import com.launchdarkly.sdk.server.integrations.Hook;
14+
import com.launchdarkly.sdk.server.integrations.Plugin;
15+
import com.launchdarkly.sdk.server.integrations.SdkMetadata;
1216
import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreStatusProvider;
1317
import com.launchdarkly.sdk.server.interfaces.BigSegmentsConfiguration;
1418
import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider;
@@ -30,6 +34,9 @@
3034
import java.io.UnsupportedEncodingException;
3135
import java.security.InvalidKeyException;
3236
import java.security.NoSuchAlgorithmException;
37+
import java.util.ArrayList;
38+
import java.util.Collections;
39+
import java.util.List;
3340
import java.util.concurrent.Executors;
3441
import java.util.concurrent.Future;
3542
import java.util.concurrent.ScheduledExecutorService;
@@ -206,13 +213,33 @@ public LDClient(String sdkKey, LDConfig config) {
206213

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

216+
// build environment metadata for plugins
217+
SdkMetadata sdkMetadata;
218+
if (config.wrapperInfo == null) {
219+
sdkMetadata = new SdkMetadata("JavaClient", Version.SDK_VERSION);
220+
} else {
221+
sdkMetadata = new SdkMetadata("JavaClient", Version.SDK_VERSION, config.wrapperInfo.getWrapperName(), config.wrapperInfo.getWrapperVersion());
222+
}
223+
EnvironmentMetadata environmentMetadata = new EnvironmentMetadata(config.applicationInfo, sdkMetadata, sdkKey);
224+
225+
// add plugin hooks
226+
List<Hook> allHooks = new ArrayList<>(config.hooks.getHooks());
227+
for (Plugin plugin : config.plugins.getPlugins()) {
228+
try {
229+
allHooks.addAll(plugin.getHooks(environmentMetadata));
230+
} catch (Exception e) {
231+
baseLogger.error("Exception thrown getting hooks for plugin " + plugin.getMetadata().getName() + ". Unable to get hooks, plugin will not be registered.");
232+
}
233+
}
234+
allHooks = Collections.unmodifiableList(allHooks);
235+
209236
// decorate evaluator with hooks if hooks were provided
210-
if (config.hooks.getHooks().isEmpty()) {
237+
if (allHooks.isEmpty()) {
211238
this.evaluator = evaluator;
212239
this.migrationEvaluator = new MigrationStageEnforcingEvaluator(evaluator, evaluationLogger);
213240
} else {
214-
this.evaluator = new EvaluatorWithHooks(evaluator, config.hooks.getHooks(), this.baseLogger.subLogger(Loggers.HOOKS_LOGGER_NAME));
215-
this.migrationEvaluator = new EvaluatorWithHooks(new MigrationStageEnforcingEvaluator(evaluator, evaluationLogger), config.hooks.getHooks(), this.baseLogger.subLogger(Loggers.HOOKS_LOGGER_NAME));
241+
this.evaluator = new EvaluatorWithHooks(evaluator, allHooks, this.baseLogger.subLogger(Loggers.HOOKS_LOGGER_NAME));
242+
this.migrationEvaluator = new EvaluatorWithHooks(new MigrationStageEnforcingEvaluator(evaluator, evaluationLogger), allHooks, this.baseLogger.subLogger(Loggers.HOOKS_LOGGER_NAME));
216243
}
217244

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

266+
// register plugins as soon as possible after client is valid
267+
for (Plugin plugin : config.plugins.getPlugins()) {
268+
try {
269+
plugin.register(this, environmentMetadata);
270+
} catch (Exception e) {
271+
baseLogger.error("Exception thrown registering plugin " + plugin.getMetadata().getName() + ". Plugin will not be registered.");
272+
}
273+
}
274+
239275
Future<Void> startFuture = dataSource.start();
240276
if (!config.startWait.isZero() && !config.startWait.isNegative()) {
241277
if (!(dataSource instanceof ComponentsImpl.NullDataSource)) {

lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDConfig.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.launchdarkly.sdk.EvaluationReason.BigSegmentsStatus;
55
import com.launchdarkly.sdk.server.integrations.ApplicationInfoBuilder;
66
import com.launchdarkly.sdk.server.integrations.HooksConfigurationBuilder;
7+
import com.launchdarkly.sdk.server.integrations.PluginsConfigurationBuilder;
78
import com.launchdarkly.sdk.server.integrations.ServiceEndpointsBuilder;
89
import com.launchdarkly.sdk.server.integrations.WrapperInfoBuilder;
910
import com.launchdarkly.sdk.server.interfaces.ApplicationInfo;
@@ -17,6 +18,7 @@
1718
import com.launchdarkly.sdk.server.subsystems.HookConfiguration;
1819
import com.launchdarkly.sdk.server.subsystems.HttpConfiguration;
1920
import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration;
21+
import com.launchdarkly.sdk.server.subsystems.PluginsConfiguration;
2022

2123
import java.time.Duration;
2224

@@ -38,6 +40,7 @@ public final class LDConfig {
3840
final boolean diagnosticOptOut;
3941
final ComponentConfigurer<EventProcessor> events;
4042
final HookConfiguration hooks;
43+
final PluginsConfiguration plugins;
4144
final ComponentConfigurer<HttpConfiguration> http;
4245
final ComponentConfigurer<LoggingConfiguration> logging;
4346
final ServiceEndpoints serviceEndpoints;
@@ -61,6 +64,7 @@ protected LDConfig(Builder builder) {
6164
this.dataStore = builder.dataStore == null ? Components.inMemoryDataStore() : builder.dataStore;
6265
this.diagnosticOptOut = builder.diagnosticOptOut;
6366
this.hooks = (builder.hooksConfigurationBuilder == null ? Components.hooks() : builder.hooksConfigurationBuilder).build();
67+
this.plugins = (builder.pluginsConfigurationBuilder == null ? Components.plugins() : builder.pluginsConfigurationBuilder).build();
6468
this.http = builder.http == null ? Components.httpConfiguration() : builder.http;
6569
this.logging = builder.logging == null ? Components.logging() : builder.logging;
6670
this.offline = builder.offline;
@@ -91,6 +95,7 @@ public static class Builder {
9195
private boolean diagnosticOptOut = false;
9296
private ComponentConfigurer<EventProcessor> events = null;
9397
private HooksConfigurationBuilder hooksConfigurationBuilder = null;
98+
private PluginsConfigurationBuilder pluginsConfigurationBuilder = null;
9499
private ComponentConfigurer<HttpConfiguration> http = null;
95100
private ComponentConfigurer<LoggingConfiguration> logging = null;
96101
private ServiceEndpointsBuilder serviceEndpointsBuilder = null;
@@ -120,6 +125,7 @@ public static Builder fromConfig(LDConfig config) {
120125
newBuilder.diagnosticOptOut = config.diagnosticOptOut;
121126
newBuilder.events = config.events;
122127
newBuilder.hooksConfigurationBuilder = ComponentsImpl.HooksConfigurationBuilderImpl.fromHooksConfiguration(config.hooks);
128+
newBuilder.pluginsConfigurationBuilder = ComponentsImpl.PluginsConfigurationBuilderImpl.fromPluginsConfiguration(config.plugins);
123129
newBuilder.http = config.http;
124130
newBuilder.logging = config.logging;
125131

@@ -270,6 +276,22 @@ public Builder hooks(HooksConfigurationBuilder hooksConfiguration) {
270276
return this;
271277
}
272278

279+
/**
280+
* Sets the SDK's plugins configuration, using a builder. This is normally a obtained from
281+
* {@link Components#plugins()} ()}, which has methods for setting individual other plugin
282+
* related properties.
283+
* <p>
284+
* Plugin support is currently experimental and subject to change.
285+
*
286+
* @param pluginsConfiguration the plugins configuration builder
287+
* @return the main configuration builder
288+
* @see Components#plugins()
289+
*/
290+
public Builder plugins(PluginsConfigurationBuilder pluginsConfiguration) {
291+
this.pluginsConfigurationBuilder = pluginsConfiguration;
292+
return this;
293+
}
294+
273295
/**
274296
* Sets the SDK's networking configuration, using a configuration builder. This builder is
275297
* obtained from {@link Components#httpConfiguration()}, and has methods for setting individual
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.launchdarkly.sdk.server.integrations;
2+
3+
import com.launchdarkly.sdk.server.interfaces.ApplicationInfo;
4+
5+
/**
6+
* Metadata about the environment that flag evaluations or other functionalities are being performed in.
7+
*/
8+
public final class EnvironmentMetadata {
9+
private final ApplicationInfo applicationInfo;
10+
private final SdkMetadata sdkMetadata;
11+
private final String sdkKey;
12+
13+
/**
14+
* @param applicationInfo for the application this SDK is used in
15+
* @param sdkMetadata for the LaunchDarkly SDK
16+
* @param sdkKey for the key used to initialize the SDK client
17+
*/
18+
public EnvironmentMetadata(ApplicationInfo applicationInfo, SdkMetadata sdkMetadata, String sdkKey) {
19+
this.applicationInfo = applicationInfo;
20+
this.sdkMetadata = sdkMetadata;
21+
this.sdkKey = sdkKey;
22+
}
23+
24+
/**
25+
* @return the {@link ApplicationInfo} for the application this SDK is used in.
26+
*/
27+
public ApplicationInfo getApplicationInfo() {
28+
return applicationInfo;
29+
}
30+
31+
/**
32+
* @return the {@link SdkMetadata} for the LaunchDarkly SDK.
33+
*/
34+
public SdkMetadata getSdkMetadata() {
35+
return sdkMetadata;
36+
}
37+
38+
/**
39+
* @return the key used to initialize the SDK client
40+
*/
41+
public String getSdkKey() {
42+
return sdkKey;
43+
}
44+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.launchdarkly.sdk.server.integrations;
2+
3+
import com.launchdarkly.sdk.server.LDClient;
4+
5+
import java.util.Collections;
6+
import java.util.List;
7+
8+
/**
9+
* Abstract class that you can extend to create a plugin to the LaunchDarkly SDK.
10+
*/
11+
public abstract class Plugin {
12+
/**
13+
* @return the {@link PluginMetadata} that gives details about the plugin.
14+
*/
15+
public abstract PluginMetadata getMetadata();
16+
17+
/**
18+
* Registers the plugin with the SDK. Called once during SDK initialization.
19+
* The SDK initialization will typically not have been completed at this point, so the plugin should take appropriate
20+
* actions to ensure the SDK is ready before sending track events or evaluating flags.
21+
*
22+
* @param client for the plugin to use
23+
* @param metadata metadata about the environment where the plugin is running.
24+
*/
25+
public abstract void register(LDClient client, EnvironmentMetadata metadata);
26+
27+
/**
28+
* Gets a list of hooks that the plugin wants to register.
29+
* This method will be called once during SDK initialization before the register method is called.
30+
* If the plugin does not need to register any hooks, this method doesn't need to be implemented.
31+
*
32+
* @param metadata metadata about the environment where the plugin is running.
33+
* @return a list of hooks that the plugin wants to register.
34+
*/
35+
public List<Hook> getHooks(EnvironmentMetadata metadata) {
36+
return Collections.emptyList();
37+
}
38+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.launchdarkly.sdk.server.integrations;
2+
3+
/**
4+
* PluginMetadata contains information about a specific plugin implementation
5+
*/
6+
public abstract class PluginMetadata {
7+
/**
8+
* @return the name of the plugin implementation
9+
*/
10+
public abstract String getName();
11+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.launchdarkly.sdk.server.integrations;
2+
3+
import com.launchdarkly.sdk.server.Components;
4+
import com.launchdarkly.sdk.server.subsystems.PluginsConfiguration;
5+
6+
import java.util.ArrayList;
7+
import java.util.Collections;
8+
import java.util.List;
9+
10+
/**
11+
* Contains methods for configuring the SDK's 'plugins'.
12+
* <p>
13+
* If you want to add plugins, use {@link Components#plugins()}, configure accordingly, and pass it
14+
* to {@link com.launchdarkly.sdk.server.LDConfig.Builder#plugins(PluginsConfigurationBuilder)}.
15+
*
16+
* <pre><code>
17+
* List plugins = getPluginsFunc();
18+
* LDConfig config = new LDConfig.Builder()
19+
* .plugins(
20+
* Components.plugins()
21+
* .setPlugins(plugins)
22+
* )
23+
* .build();
24+
* </code></pre>
25+
* <p>
26+
* Note that this class is abstract; the actual implementation is created by calling {@link Components#plugins()}.
27+
*/
28+
public abstract class PluginsConfigurationBuilder {
29+
/**
30+
* The current set of plugins the builder has.
31+
*/
32+
protected List<Plugin> plugins = Collections.emptyList();
33+
34+
/**
35+
* Sets the provided list of plugins on the configuration. Note that the order of plugins is important and controls
36+
* the order in which they will be registered. See {@link Plugin} for more details.
37+
*
38+
* @param plugins to be set on the configuration
39+
* @return the builder
40+
*/
41+
public PluginsConfigurationBuilder setPlugins(List<Plugin> plugins) {
42+
// copy to avoid list manipulations impacting the SDK
43+
this.plugins = Collections.unmodifiableList(new ArrayList<>(plugins));
44+
return this;
45+
}
46+
47+
/**
48+
* @return the plugins configuration
49+
*/
50+
public abstract PluginsConfiguration build();
51+
}

0 commit comments

Comments
 (0)