Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
22 changes: 22 additions & 0 deletions pkgs/sdk/server/src/Components.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using LaunchDarkly.Sdk.Server.Integrations;
using LaunchDarkly.Sdk.Server.Interfaces;
using LaunchDarkly.Sdk.Server.Internal;
using LaunchDarkly.Sdk.Server.Plugins;
using LaunchDarkly.Sdk.Server.Subsystems;

namespace LaunchDarkly.Sdk.Server
Expand Down Expand Up @@ -218,6 +219,27 @@ public static LoggingConfigurationBuilder Logging(ILogAdapter adapter) =>
/// <returns>a configuration builder</returns>
public static HookConfigurationBuilder Hooks(IEnumerable<Hook> hooks) => new HookConfigurationBuilder(hooks);

/// <summary>
/// Returns a configuration builder for the SDK's plugin configuration.
/// </summary>
/// <example>
/// <code>
/// var config = Configuration.Builder(sdkKey)
/// .Add(new MyPlugin(...))
/// .Add(new MyOtherPlugin(...))
/// ).Build();
/// </code>
/// </example>
/// <returns>a configuration builder</returns>
public static PluginConfigurationBuilder Plugins() => new PluginConfigurationBuilder();

/// <summary>
/// Returns a configuration builder for the SDK's plugin configuration, with an initial set of plugins.
/// </summary>
/// <param name="plugins">a collection of plugins</param>
/// <returns>a configuration builder</returns>
public static PluginConfigurationBuilder Plugins(IEnumerable<Plugin> plugins) => new PluginConfigurationBuilder(plugins);

/// <summary>
/// Returns a configuration object that disables analytics events.
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions pkgs/sdk/server/src/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ public class Configuration
/// </summary>
public HookConfigurationBuilder Hooks { get; }

/// <summary>
/// Contains methods for configuring the SDK's 'plugins' to extend or customize SDK behavior.
/// </summary>
public PluginConfigurationBuilder Plugins { get; }

#endregion

#region Public methods
Expand Down Expand Up @@ -189,6 +194,7 @@ internal Configuration(ConfigurationBuilder builder)
ApplicationInfo = builder._applicationInfo;
WrapperInfo = builder._wrapperInfo;
Hooks = builder._hooks;
Plugins = builder._plugins;
}

#endregion
Expand Down
13 changes: 13 additions & 0 deletions pkgs/sdk/server/src/ConfigurationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public sealed class ConfigurationBuilder
internal bool _diagnosticOptOut = false;
internal IComponentConfigurer<IEventProcessor> _events = null;
internal HookConfigurationBuilder _hooks = null;
internal PluginConfigurationBuilder _plugins = null;
internal IComponentConfigurer<HttpConfiguration> _http = null;
internal IComponentConfigurer<LoggingConfiguration> _logging = null;
internal bool _offline = false;
Expand All @@ -65,6 +66,7 @@ internal ConfigurationBuilder(Configuration copyFrom)
_diagnosticOptOut = copyFrom.DiagnosticOptOut;
_events = copyFrom.Events;
_hooks = copyFrom.Hooks;
_plugins = copyFrom.Plugins;
_http = copyFrom.Http;
_logging = copyFrom.Logging;
_offline = copyFrom.Offline;
Expand Down Expand Up @@ -291,6 +293,17 @@ public ConfigurationBuilder Hooks(HookConfigurationBuilder hooksConfig)
return this;
}

/// <summary>
/// Configures the SDK's plugins.
/// </summary>
/// <param name="pluginsConfig">the plugin configuration</param>
/// <returns>the same builder</returns>
public ConfigurationBuilder Plugins(PluginConfigurationBuilder pluginsConfig)
{
_plugins = pluginsConfig;
return this;
}

/// <summary>
/// Sets whether or not this client is offline. If true, no calls to Launchdarkly will be made.
/// </summary>
Expand Down
49 changes: 49 additions & 0 deletions pkgs/sdk/server/src/Integrations/PluginConfigurationBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.Collections.Generic;
using System.Linq;
using LaunchDarkly.Sdk.Server.Plugins;
using LaunchDarkly.Sdk.Server.Subsystems;

namespace LaunchDarkly.Sdk.Server.Integrations
{
/// <summary>
/// PluginConfigurationBuilder is a builder for the SDK's plugin configuration.
/// </summary>
public sealed class PluginConfigurationBuilder
{
private readonly List<Plugin> _plugins;

/// <summary>
/// Constructs a configuration representing no plugins by default.
/// </summary>
public PluginConfigurationBuilder() : this(new List<Plugin>())
{
}

/// <summary>
/// Constructs a configuration from an existing collection of plugins.
/// </summary>
public PluginConfigurationBuilder(IEnumerable<Plugin> plugins)
{
_plugins = plugins.ToList();
}

/// <summary>
/// Adds a plugin to the configuration.
/// </summary>
/// <returns>the builder</returns>
public PluginConfigurationBuilder Add(Plugin plugin)
{
_plugins.Add(plugin);
return this;
}

/// <summary>
/// Builds the configuration.
/// </summary>
/// <returns>the built configuration</returns>
public PluginConfiguration Build()
{
return new PluginConfiguration(_plugins);
}
}
}
155 changes: 124 additions & 31 deletions pkgs/sdk/server/src/LdClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using LaunchDarkly.Sdk.Server.Hooks;
using LaunchDarkly.Sdk.Server.Interfaces;
using LaunchDarkly.Sdk.Server.Internal;
using LaunchDarkly.Sdk.Server.Plugins;
using LaunchDarkly.Sdk.Server.Internal.BigSegments;
using LaunchDarkly.Sdk.Server.Internal.DataSources;
using LaunchDarkly.Sdk.Server.Internal.DataStores;
Expand Down Expand Up @@ -130,7 +131,7 @@ public LdClient(Configuration config)
{
_configuration = config;

var logConfig = (config.Logging ?? Components.Logging()).Build(new LdClientContext(config.SdkKey));
var logConfig = (_configuration.Logging ?? Components.Logging()).Build(new LdClientContext(_configuration.SdkKey));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The use of config and _configuration was mixed throughout this method. Unifying it to use the instance property once it is set.


_log = logConfig.LogAdapter.Logger(logConfig.BaseLoggerName ?? LogNames.DefaultBase);
_log.Info("Starting LaunchDarkly client {0}",
Expand All @@ -140,20 +141,20 @@ public LdClient(Configuration config)
var taskExecutor = new TaskExecutor(this, _log);

var clientContext = new LdClientContext(
config.SdkKey,
_configuration.SdkKey,
null,
null,
null,
_log,
config.Offline,
config.ServiceEndpoints,
_configuration.Offline,
_configuration.ServiceEndpoints,
null,
taskExecutor,
config.ApplicationInfo?.Build() ?? new ApplicationInfo(),
config.WrapperInfo?.Build()
_configuration.ApplicationInfo?.Build() ?? new ApplicationInfo(),
_configuration.WrapperInfo?.Build()
);

var httpConfig = (config.Http ?? Components.HttpConfiguration()).Build(clientContext);
var httpConfig = (_configuration.Http ?? Components.HttpConfiguration()).Build(clientContext);
clientContext = clientContext.WithHttp(httpConfig);

var diagnosticStore = _configuration.DiagnosticOptOut ? null :
Expand Down Expand Up @@ -185,25 +186,34 @@ public LdClient(Configuration config)
);

var eventProcessorFactory =
config.Offline ? Components.NoEvents :
(_configuration.Events?? Components.SendEvents());
_configuration.Offline ? Components.NoEvents :
(_configuration.Events ?? Components.SendEvents());
_eventProcessor = eventProcessorFactory.Build(clientContext);

var dataSourceUpdates = new DataSourceUpdatesImpl(_dataStore, _dataStoreStatusProvider,
taskExecutor, _log, logConfig.LogDataSourceOutageAsErrorAfter);
IComponentConfigurer<IDataSource> dataSourceFactory =
config.Offline ? Components.ExternalUpdatesOnly :
_configuration.Offline ? Components.ExternalUpdatesOnly :
(_configuration.DataSource ?? Components.StreamingDataSource());
_dataSource = dataSourceFactory.Build(clientContext.WithDataSourceUpdates(dataSourceUpdates));
_dataSourceStatusProvider = new DataSourceStatusProviderImpl(dataSourceUpdates);
_flagTracker = new FlagTrackerImpl(dataSourceUpdates,
(string key, Context context) => JsonVariation(key, context, LdValue.Null));

var hookConfig = (config.Hooks ?? Components.Hooks()).Build();
_hookExecutor = hookConfig.Hooks.Any() ?
(IHookExecutor) new Executor(_log.SubLogger(LogNames.HooksSubLog), hookConfig.Hooks)
var allHooks = new List<Hook>();

var hookConfig = (_configuration.Hooks ?? Components.Hooks()).Build();
allHooks.AddRange(hookConfig.Hooks);

var pluginConfig = (_configuration.Plugins ?? Components.Plugins()).Build();
EnvironmentMetadata environmentMetadata = CreateEnvironmentMetadata(clientContext);
allHooks.AddRange(GetPluginHooks(pluginConfig, environmentMetadata));

_hookExecutor = allHooks.Any() ?
(IHookExecutor)new Executor(_log.SubLogger(LogNames.HooksSubLog), allHooks)
: new NoopExecutor();

RegisterPlugins(pluginConfig, environmentMetadata);

var initTask = _dataSource.Start();

Expand Down Expand Up @@ -323,7 +333,7 @@ public MigrationVariation MigrationVariation(string key, Context context, Migrat
var (detail, flag) = EvaluateWithHooks(Method.MigrationVariation, key, context, LdValue.Of(defaultStage.ToDataModelString()),
LdValue.Convert.String, true, EventFactory.Default);

var nullableStage = MigrationStageExtensions.FromDataModelString(detail.Value);
var nullableStage = MigrationStageExtensions.FromDataModelString(detail.Value);
var stage = nullableStage ?? defaultStage;
if (nullableStage == null)
{
Expand Down Expand Up @@ -648,24 +658,34 @@ public Logger GetLogger()

#region Private methods

private FeatureFlag GetFlag(string key)
/// <summary>
/// Creates metadata about the environment, including SDK and application information.
/// </summary>
/// <param name="clientContext">The client context containing application and wrapper information.</param>
/// <returns>An <see cref="EnvironmentMetadata"/> instance containing the environment metadata.</returns>
/// <remarks>
/// This method constructs the environment metadata using the SDK key, application ID, and version,
/// along with any wrapper information if available. It is used to provide context for plugins and
/// hooks that may need to interact with the environment.
/// </remarks>
private EnvironmentMetadata CreateEnvironmentMetadata(LdClientContext clientContext)
{
var maybeItem = _dataStore.Get(DataModel.Features, key);
if (maybeItem.HasValue && maybeItem.Value.Item != null && maybeItem.Value.Item is FeatureFlag f)
{
return f;
}
return null;
}

private Segment GetSegment(string key)
{
var maybeItem = _dataStore.Get(DataModel.Segments, key);
if (maybeItem.HasValue && maybeItem.Value.Item != null && maybeItem.Value.Item is Segment s)
{
return s;
}
return null;
var applicationInfo = clientContext.ApplicationInfo;
var wrapperInfo = _configuration.WrapperInfo?.Build();

var sdkMetadata = new SdkMetadata(
"dotnet-server-sdk",
AssemblyVersions.GetAssemblyVersionStringForType(typeof(LdClient)),
wrapperInfo?.Name,
wrapperInfo?.Version
);

var applicationMetadata = new ApplicationMetadata(
applicationInfo.ApplicationId,
applicationInfo.ApplicationVersion
);

return new EnvironmentMetadata(sdkMetadata, _configuration.SdkKey, applicationMetadata);
}

private void Dispose(bool disposing)
Expand Down Expand Up @@ -694,6 +714,79 @@ private string GetEnvironmentId()
return null;
}

private FeatureFlag GetFlag(string key)
{
var maybeItem = _dataStore.Get(DataModel.Features, key);
if (maybeItem.HasValue && maybeItem.Value.Item != null && maybeItem.Value.Item is FeatureFlag f)
{
return f;
}
return null;
}

/// <summary>
/// Retrieves all hooks from the plugins defined in the plugin configuration.
/// </summary>
/// <param name="pluginConfig">The plugin configuration containing the list of plugins.</param>
/// <param name="environmentMetadata">Metadata about the environment to pass to the plugins.</param>
/// <returns>A list of hooks retrieved from the plugins.</returns>
private List<Hook> GetPluginHooks(PluginConfiguration pluginConfig, EnvironmentMetadata environmentMetadata)
{
var allHooks = new List<Hook>();
foreach (var plugin in pluginConfig.Plugins)
{
try
{
var pluginHooks = plugin.GetHooks(environmentMetadata);
if (pluginHooks != null)
{
allHooks.AddRange(pluginHooks);
}
}
catch (Exception ex)
{
_log.Error("Error getting hooks from plugin {0}: {1}", plugin.GetMetadata()?.Name ?? "unknown", ex);
}
}

return allHooks;
}

private Segment GetSegment(string key)
{
var maybeItem = _dataStore.Get(DataModel.Segments, key);
if (maybeItem.HasValue && maybeItem.Value.Item != null && maybeItem.Value.Item is Segment s)
{
return s;
}
return null;
}

/// <summary>
/// Registers all plugins with the client and environment metadata.
/// </summary>
/// <param name="pluginConfig">The plugin configuration containing the list of plugins.</param>
/// <param name="environmentMetadata">Metadata about the environment to pass to the plugins.</param>
/// <remarks>
/// This method iterates through each plugin in the configuration and calls its `Register` method
/// to initialize it with the client and environment metadata. It logs any exceptions that occur during
/// the registration process, allowing the client to continue functioning even if some plugins fail to register.
/// </remarks>
private void RegisterPlugins(PluginConfiguration pluginConfig, EnvironmentMetadata environmentMetadata)
{
foreach (var plugin in pluginConfig.Plugins)
{
try
{
plugin.Register(this, environmentMetadata);
}
catch (Exception ex)
{
_log.Error("Error registering plugin {0}: {1}", plugin.GetMetadata()?.Name ?? "unknown", ex);
}
}
}

#endregion
}
}
Loading
Loading