diff --git a/sdk/@launchdarkly/observability-dotnet/AspSampleApp/AspSampleApp.csproj b/sdk/@launchdarkly/observability-dotnet/AspSampleApp/AspSampleApp.csproj index bd1fbb1ed..85a10af52 100644 --- a/sdk/@launchdarkly/observability-dotnet/AspSampleApp/AspSampleApp.csproj +++ b/sdk/@launchdarkly/observability-dotnet/AspSampleApp/AspSampleApp.csproj @@ -7,6 +7,7 @@ + diff --git a/sdk/@launchdarkly/observability-dotnet/AspSampleApp/Program.cs b/sdk/@launchdarkly/observability-dotnet/AspSampleApp/Program.cs index de3580e5a..273227309 100644 --- a/sdk/@launchdarkly/observability-dotnet/AspSampleApp/Program.cs +++ b/sdk/@launchdarkly/observability-dotnet/AspSampleApp/Program.cs @@ -1,20 +1,24 @@ using LaunchDarkly.Observability; +using LaunchDarkly.Sdk; +using LaunchDarkly.Sdk.Server; +using LaunchDarkly.Sdk.Server.Integrations; var builder = WebApplication.CreateBuilder(args); -// Add services to the container. -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); -builder.Services.AddLaunchDarklyObservability( - Environment.GetEnvironmentVariable("LAUNCHDARKLY_SDK_KEY"), - ldBuilder => - { - ldBuilder.WithServiceName("ryan-test-service"); - } -); +var config = Configuration.Builder(Environment.GetEnvironmentVariable("LAUNCHDARKLY_SDK_KEY")) + .Plugins(new PluginConfigurationBuilder() + .Add(ObservabilityPlugin.Builder(builder.Services) + .WithServiceName("ryan-test-service") + .WithServiceVersion("0.0.0") + .Build())).Build(); + +// Building the LdClient with the Observability plugin. This line will add services to the web application. +var client = new LdClient(config); +// Client must be built before this line. var app = builder.Build(); // Configure the HTTP request pipeline. @@ -33,11 +37,13 @@ app.MapGet("/weatherforecast", () => { + var isMercury = + client.BoolVariation("isMercury", Context.New(ContextKind.Of("request"), Guid.NewGuid().ToString())); var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast ( DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - Random.Shared.Next(-20, 55), + Random.Shared.Next(isMercury ? -170 : -20, isMercury ? 400 : 55), summaries[Random.Shared.Next(summaries.Length)] )) .ToArray(); diff --git a/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/BaseBuilder.cs b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/BaseBuilder.cs new file mode 100644 index 000000000..b5f6655ec --- /dev/null +++ b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/BaseBuilder.cs @@ -0,0 +1,116 @@ +using System; + +namespace LaunchDarkly.Observability +{ + /// + /// Base builder which allows for methods to be shared between building a config directly and building a plugin. + /// + /// This uses the CRTP pattern to allow the individual builder methods to return instances of the derived builder + /// type. + /// + /// + public class BaseBuilder where TBuilder : BaseBuilder + { + private const string DefaultOtlpEndpoint = "https://otel.observability.app.launchdarkly.com:4318"; + private const string DefaultBackendUrl = "https://pub.observability.app.launchdarkly.com"; + private string _otlpEndpoint = DefaultOtlpEndpoint; + private string _backendUrl = DefaultBackendUrl; + private string _serviceName = string.Empty; + private string _environment = string.Empty; + private string _serviceVersion = string.Empty; + + protected BaseBuilder() + { + } + + /// + /// Set the OTLP endpoint. + /// + /// For most configurations, the OTLP endpoint will not need to be set. + /// + /// + /// Setting the endpoint to null will reset the builder value to the default. + /// + /// + /// The OTLP exporter endpoint URL. + /// A reference to this builder. + public TBuilder WithOtlpEndpoint(string otlpEndpoint) + { + _otlpEndpoint = otlpEndpoint ?? DefaultOtlpEndpoint; + return (TBuilder)this; + } + + /// + /// Set the back-end URL for non-telemetry operations. + /// + /// For most configurations, the backend url will not need to be set. + /// + /// + /// Setting the url to null will reset the builder value to the default. + /// + /// + /// The back-end URL used for non-telemetry operations. + /// A reference to this builder. + public TBuilder WithBackendUrl(string backendUrl) + { + _backendUrl = backendUrl ?? DefaultBackendUrl; + return (TBuilder)this; + } + + /// + /// Set the service name. + /// + /// The logical service name used in telemetry resource attributes. + /// A reference to this builder. + public TBuilder WithServiceName(string serviceName) + { + _serviceName = serviceName ?? string.Empty; + return (TBuilder)this; + } + + /// + /// Set the service version. + /// + /// + /// The version of the service that will be added to resource attributes when a service name is provided. + /// + /// A reference to this builder. + public TBuilder WithServiceVersion(string serviceVersion) + { + _serviceVersion = serviceVersion ?? string.Empty; + return (TBuilder)this; + } + + /// + /// Set the environment name. + /// + /// The environment name (for example, "prod" or "staging"). + /// A reference to this builder. + public TBuilder WithEnvironment(string environment) + { + _environment = environment ?? string.Empty; + return (TBuilder)this; + } + + /// + /// Build an immutable instance. + /// + /// The constructed . + internal ObservabilityConfig BuildConfig(string sdkKey) + { + if (sdkKey == null) + { + throw new ArgumentNullException(nameof(sdkKey), + "SDK key cannot be null when creating an ObservabilityConfig builder."); + } + + return new ObservabilityConfig( + _otlpEndpoint, + _backendUrl, + _serviceName, + _environment, + _serviceVersion, + sdkKey); + } + } +} diff --git a/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/Class1.cs b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/Class1.cs deleted file mode 100644 index bedb65f37..000000000 --- a/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/Class1.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace LaunchDarkly.Observability -{ - public class Class1 - { - - } -} diff --git a/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/LaunchDarkly.Observability.csproj b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/LaunchDarkly.Observability.csproj index 571a94c90..7a695c14f 100644 --- a/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/LaunchDarkly.Observability.csproj +++ b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/LaunchDarkly.Observability.csproj @@ -54,7 +54,7 @@ - + diff --git a/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/ObservabilityConfig.cs b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/ObservabilityConfig.cs index a031cd3af..81f094e1d 100644 --- a/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/ObservabilityConfig.cs +++ b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/ObservabilityConfig.cs @@ -8,7 +8,7 @@ public struct ObservabilityConfig /// The configured OTLP endpoint. /// public string OtlpEndpoint { get; } - + /// /// The configured back-end URL. /// @@ -16,6 +16,7 @@ public struct ObservabilityConfig /// /// public string BackendUrl { get; } + /// /// The name of the service. /// @@ -24,20 +25,23 @@ public struct ObservabilityConfig /// /// public string ServiceName { get; } + /// /// The version of the service. /// public string ServiceVersion { get; } + /// /// The environment for the service. /// public string Environment { get; } + /// /// The LaunchDarkly SDK key. /// public string SdkKey { get; } - private ObservabilityConfig( + internal ObservabilityConfig( string otlpEndpoint, string backendUrl, string serviceName, @@ -56,111 +60,21 @@ private ObservabilityConfig( /// /// Create a new builder for . /// - /// The LaunchDarkly SDK key used for authentication and resource attributes. - /// A new instance for configuring observability. - internal static Builder CreateBuilder(string sdkKey) => new Builder(sdkKey); + /// A new instance for configuring observability. + internal static ObservabilityConfigBuilder Builder() => new ObservabilityConfigBuilder(); /// - /// Fluent builder for . + /// Builder for building an observability configuration. /// - public sealed class Builder + public class ObservabilityConfigBuilder : BaseBuilder { - private const string DefaultOtlpEndpoint = "https://otel.observability.app.launchdarkly.com:4318"; - private const string DefaultBackendUrl = "https://pub.observability.app.launchdarkly.com"; - private string _otlpEndpoint = DefaultOtlpEndpoint; - private string _backendUrl = DefaultBackendUrl; - private string _serviceName = string.Empty; - private string _environment = string.Empty; - private string _serviceVersion = string.Empty; - private readonly string _sdkKey; - - internal Builder(string sdkKey) - { - _sdkKey = sdkKey ?? throw new ArgumentNullException(nameof(sdkKey), "SDK key cannot be null when creating an ObservabilityConfig builder."); - } - - /// - /// Set the OTLP endpoint. - /// - /// For most configurations, the OTLP endpoint will not need to be set. - /// - /// - /// Setting the endpoint to null will reset the builder value to the default. - /// - /// - /// The OTLP exporter endpoint URL. - /// A reference to this builder. - public Builder WithOtlpEndpoint(string otlpEndpoint) - { - _otlpEndpoint = otlpEndpoint ?? DefaultOtlpEndpoint; - return this; - } - - /// - /// Set the back-end URL for non-telemetry operations. - /// - /// For most configurations, the backend url will not need to be set. - /// - /// - /// Setting the url to null will reset the builder value to the default. - /// - /// - /// The back-end URL used for non-telemetry operations. - /// A reference to this builder. - public Builder WithBackendUrl(string backendUrl) - { - _backendUrl = backendUrl ?? DefaultBackendUrl; - return this; - } - - /// - /// Set the service name. - /// - /// The logical service name used in telemetry resource attributes. - /// A reference to this builder. - public Builder WithServiceName(string serviceName) - { - _serviceName = serviceName ?? string.Empty; - return this; - } - - /// - /// Set the service version. - /// - /// - /// The version of the service that will be added to resource attributes when a service name is provided. - /// - /// A reference to this builder. - public Builder WithServiceVersion(string serviceVersion) - { - _serviceVersion = serviceVersion ?? string.Empty; - return this; - } - - /// - /// Set the environment name. - /// - /// The environment name (for example, "prod" or "staging"). - /// A reference to this builder. - public Builder WithEnvironment(string environment) + internal ObservabilityConfigBuilder() { - _environment = environment ?? string.Empty; - return this; } - /// - /// Build an immutable instance. - /// - /// The constructed . - public ObservabilityConfig Build() + internal ObservabilityConfig Build(string sdkKey) { - return new ObservabilityConfig( - _otlpEndpoint, - _backendUrl, - _serviceName, - _environment, - _serviceVersion, - _sdkKey); + return BuildConfig(sdkKey); } } } diff --git a/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/ObservabilityExtensions.cs b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/ObservabilityExtensions.cs index 156c41f1d..9061a12da 100644 --- a/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/ObservabilityExtensions.cs +++ b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/ObservabilityExtensions.cs @@ -8,8 +8,8 @@ using OpenTelemetry.Logs; using OpenTelemetry.Metrics; -namespace LaunchDarkly.Observability { - +namespace LaunchDarkly.Observability +{ /// /// Static class containing extension methods for configuring observability /// @@ -25,38 +25,24 @@ public static class ObservabilityExtensions private const string TracesPath = "/v1/traces"; private const string LogsPath = "/v1/logs"; private const string MetricsPath = "/v1/metrics"; - private static IEnumerable> GetResourceAttributes(ObservabilityConfig config) + + private static IEnumerable> GetResourceAttributes(ObservabilityConfig config) { - var attrs = new List>(); + var attrs = new List>(); - if (!string.IsNullOrWhiteSpace(config.Environment)) - { - attrs.Add(new KeyValuePair("deployment.environment.name", config.Environment)); - } + if (!string.IsNullOrWhiteSpace(config.Environment)) + { + attrs.Add(new KeyValuePair("deployment.environment.name", config.Environment)); + } - attrs.Add(new KeyValuePair("highlight.project_id", config.SdkKey)); + attrs.Add(new KeyValuePair("highlight.project_id", config.SdkKey)); - return attrs; + return attrs; } - /// - /// Add the LaunchDarkly Observability services. This function would typically be called by the LaunchDarkly - /// Observability plugin. This should only be called by the end user if the Observability plugin needs to be - /// initialized earlier than the LaunchDarkly client. - /// - /// The service collection - /// The LaunchDarkly SDK - /// A method to configure the services - /// The service collection - public static IServiceCollection AddLaunchDarklyObservability( - this IServiceCollection services, - string sdkKey, - Action configure) + internal static void AddLaunchDarklyObservabilityWithConfig(this IServiceCollection services, + ObservabilityConfig config) { - var builder = ObservabilityConfig.CreateBuilder(sdkKey); - configure(builder); - - var config = builder.Build(); var resourceAttributes = GetResourceAttributes(config); var resourceBuilder = ResourceBuilder.CreateDefault(); @@ -68,20 +54,13 @@ public static IServiceCollection AddLaunchDarklyObservability( services.AddOpenTelemetry().WithTracing(tracing => { - tracing.SetResourceBuilder(resourceBuilder) .AddHttpClientInstrumentation() .AddGrpcClientInstrumentation() .AddWcfInstrumentation() .AddQuartzInstrumentation() - .AddAspNetCoreInstrumentation(options => - { - options.RecordException = true; - }) - .AddSqlClientInstrumentation(options => - { - options.SetDbStatementForText = true; - }) + .AddAspNetCoreInstrumentation(options => { options.RecordException = true; }) + .AddSqlClientInstrumentation(options => { options.SetDbStatementForText = true; }) .AddOtlpExporter(options => { options.Endpoint = new Uri(config.OtlpEndpoint + TracesPath); @@ -116,6 +95,27 @@ public static IServiceCollection AddLaunchDarklyObservability( Protocol = ExportProtocol }))); }); + } + + /// + /// Add the LaunchDarkly Observability services. This function would typically be called by the LaunchDarkly + /// Observability plugin. This should only be called by the end user if the Observability plugin needs to be + /// initialized earlier than the LaunchDarkly client. + /// + /// The service collection + /// The LaunchDarkly SDK + /// A method to configure the services + /// The service collection + public static IServiceCollection AddLaunchDarklyObservability( + this IServiceCollection services, + string sdkKey, + Action configure) + { + var builder = ObservabilityConfig.Builder(); + configure(builder); + + var config = builder.Build(sdkKey); + AddLaunchDarklyObservabilityWithConfig(services, config); return services; } } diff --git a/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/ObservabilityPlugin.cs b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/ObservabilityPlugin.cs new file mode 100644 index 000000000..cf010bcbd --- /dev/null +++ b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/ObservabilityPlugin.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using LaunchDarkly.Sdk.Integrations.Plugins; +using LaunchDarkly.Sdk.Server.Hooks; +using LaunchDarkly.Sdk.Server.Interfaces; +using LaunchDarkly.Sdk.Server.Plugins; +using LaunchDarkly.Sdk.Server.Telemetry; +using Microsoft.Extensions.DependencyInjection; + +namespace LaunchDarkly.Observability +{ + public class ObservabilityPlugin : Plugin + { + private readonly ObservabilityPluginBuilder _config; + private readonly IServiceCollection _services; + + /// + /// Construct a plugin which is intended to be used with already configured observability services. + /// + /// In a typical configuration, this method will not need to be used. + /// + /// + /// This method only needs to be used when observability related functionality must be intialized before it + /// is possible to initialize the LaunchDarkly SDK. + /// + /// + /// an observability plugin instance + public static ObservabilityPlugin ForExistingServices() => new ObservabilityPlugin(); + + /// + /// Create a new builder for . + /// + /// When using this builder, LaunchDarkly client must be constructed before your application is built. + /// For example: + /// + /// var builder = WebApplication.CreateBuilder(args); + /// + /// + /// var config = Configuration.Builder(Environment.GetEnvironmentVariable("your-sdk-key") + /// .Plugins(new PluginConfigurationBuilder() + /// .Add(ObservabilityPlugin.Builder(builder.Services) + /// .WithServiceName("ryan-test-service") + /// .WithServiceVersion("0.0.0") + /// .Build())).Build(); + /// // Building the LdClient with the Observability plugin. This line will add services to the web application. + /// var client = new LdClient(config); + /// + /// // Client must be built before this line. + /// var app = builder.Build(); + /// + /// + /// + /// The service collection for dependency injection. + /// A new instance for configuring the observability plugin. + public static ObservabilityPluginBuilder Builder(IServiceCollection services) => + new ObservabilityPluginBuilder(services); + + internal ObservabilityPlugin(IServiceCollection services, ObservabilityPluginBuilder config) : base( + "LaunchDarkly.Observability") + { + _config = config; + _services = services; + } + + internal ObservabilityPlugin() : base("LaunchDarkly.Observability") + { + _services = null; + _config = null; + } + + /// + public override void Register(ILdClient client, EnvironmentMetadata metadata) + { + if (_services == null || _config == null) return; + var config = _config.BuildConfig(metadata.Credential); + _services.AddLaunchDarklyObservabilityWithConfig(config); + } + + /// + public override IList GetHooks(EnvironmentMetadata metadata) + { + return new List + { + TracingHook.Builder().IncludeValue().Build() + }; + } + + /// + /// Used to build an instance of the Observability Plugin. + /// + public sealed class ObservabilityPluginBuilder : BaseBuilder + { + private readonly IServiceCollection _services; + + internal ObservabilityPluginBuilder(IServiceCollection services) : base() + { + _services = services ?? throw new ArgumentNullException(nameof(services), + "Service collection cannot be null when creating an ObservabilityPlugin builder."); + } + + /// + /// Build an instance with the configured settings. + /// + /// The constructed . + public ObservabilityPlugin Build() + { + return new ObservabilityPlugin(_services, this); + } + } + } +} diff --git a/sdk/@launchdarkly/observability-dotnet/test/LaunchDarkly.Observability.Tests/ObservabilityConfigBuilderTests.cs b/sdk/@launchdarkly/observability-dotnet/test/LaunchDarkly.Observability.Tests/ObservabilityConfigBuilderTests.cs index aa0f61a60..cc80fbc1b 100644 --- a/sdk/@launchdarkly/observability-dotnet/test/LaunchDarkly.Observability.Tests/ObservabilityConfigBuilderTests.cs +++ b/sdk/@launchdarkly/observability-dotnet/test/LaunchDarkly.Observability.Tests/ObservabilityConfigBuilderTests.cs @@ -1,6 +1,5 @@ using LaunchDarkly.Observability; using NUnit.Framework; -using System; namespace LaunchDarkly.Observability.Test { @@ -10,13 +9,13 @@ public class ObservabilityConfigBuilderTests [Test] public void Build_WithAllFields_SetsValues() { - var config = ObservabilityConfig.CreateBuilder("sdk-123") + var config = ObservabilityConfig.Builder() .WithOtlpEndpoint("https://otlp.example.com") .WithBackendUrl("https://backend.example.com") .WithServiceName("service-a") .WithServiceVersion("1.0.0") .WithEnvironment("prod") - .Build(); + .Build("sdk-123"); Assert.Multiple(() => { @@ -32,7 +31,7 @@ public void Build_WithAllFields_SetsValues() [Test] public void Build_WithoutSettingFields_UsesDefaults() { - var config = ObservabilityConfig.CreateBuilder("sdk-xyz").Build(); + var config = ObservabilityConfig.Builder().Build("sdk-xyz"); Assert.Multiple(() => { @@ -48,13 +47,13 @@ public void Build_WithoutSettingFields_UsesDefaults() [Test] public void WithMethods_HandleNullValues_ResetsToDefaults() { - var config = ObservabilityConfig.CreateBuilder("sdk-null") + var config = ObservabilityConfig.Builder() .WithOtlpEndpoint(null) .WithBackendUrl(null) .WithServiceName(null) .WithServiceVersion(null) .WithEnvironment(null) - .Build(); + .Build("my-sdk-key"); Assert.Multiple(() => { @@ -63,21 +62,21 @@ public void WithMethods_HandleNullValues_ResetsToDefaults() Assert.That(config.ServiceName, Is.EqualTo(string.Empty)); Assert.That(config.ServiceVersion, Is.EqualTo(string.Empty)); Assert.That(config.Environment, Is.EqualTo(string.Empty)); - Assert.That(config.SdkKey, Is.EqualTo("sdk-null")); + Assert.That(config.SdkKey, Is.EqualTo("my-sdk-key")); }); } [Test] public void Build_ProducesImmutableConfig() { - var builder = ObservabilityConfig.CreateBuilder("sdk-immutable") + var builder = ObservabilityConfig.Builder() .WithOtlpEndpoint("e1") .WithBackendUrl("b1") .WithServiceName("s1") .WithServiceVersion("v1") .WithEnvironment("env1"); - var first = builder.Build(); + var first = builder.Build("my-sdk-key"); // Change builder afterward builder @@ -95,16 +94,8 @@ public void Build_ProducesImmutableConfig() Assert.That(first.ServiceName, Is.EqualTo("s1")); Assert.That(first.ServiceVersion, Is.EqualTo("v1")); Assert.That(first.Environment, Is.EqualTo("env1")); - Assert.That(first.SdkKey, Is.EqualTo("sdk-immutable")); + Assert.That(first.SdkKey, Is.EqualTo("my-sdk-key")); }); } - - [Test] - public void CreateBuilder_WithNullSdkKey_ThrowsArgumentNullException() - { - var exception = Assert.Throws(() => ObservabilityConfig.CreateBuilder(null)); - Assert.That(exception.ParamName, Is.EqualTo("sdkKey")); - Assert.That(exception.Message, Does.Contain("SDK key cannot be null when creating an ObservabilityConfig builder")); - } } } diff --git a/sdk/@launchdarkly/observability-dotnet/test/LaunchDarkly.Observability.Tests/ObservabilityPluginBuilderTests.cs b/sdk/@launchdarkly/observability-dotnet/test/LaunchDarkly.Observability.Tests/ObservabilityPluginBuilderTests.cs new file mode 100644 index 000000000..3623679ea --- /dev/null +++ b/sdk/@launchdarkly/observability-dotnet/test/LaunchDarkly.Observability.Tests/ObservabilityPluginBuilderTests.cs @@ -0,0 +1,69 @@ +using NUnit.Framework; +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace LaunchDarkly.Observability.Test +{ + [TestFixture] + public class ObservabilityPluginBuilderTests + { + private IServiceCollection _services; + + [SetUp] + public void SetUp() + { + _services = new ServiceCollection(); + } + + [Test] + public void CreateBuilder_WithValidParameters_CreatesBuilder() + { + var builder = ObservabilityPlugin.Builder(_services); + + Assert.That(builder, Is.Not.Null); + Assert.That(builder, Is.InstanceOf()); + } + + [Test] + public void CreateBuilder_WithNullServices_ThrowsArgumentNullException() + { + var exception = Assert.Throws(() => + ObservabilityPlugin.Builder(null)); + Assert.Multiple(() => + { + Assert.That(exception, Is.Not.Null); + Assert.That(exception.ParamName, Is.EqualTo("services")); + Assert.That(exception.Message, + Does.Contain("Service collection cannot be null when creating an ObservabilityPlugin builder")); + }); + } + + [Test] + public void Build_WithAllFields_CreatesPluginWithConfiguration() + { + var plugin = ObservabilityPlugin.Builder(_services) + .WithOtlpEndpoint("https://otlp.example.com") + .WithBackendUrl("https://backend.example.com") + .WithServiceName("service-a") + .WithServiceVersion("1.0.0") + .WithEnvironment("prod") + .Build(); + + Assert.That(plugin, Is.InstanceOf()); + } + + [Test] + public void Build_WithNullValues_HandlesNullsCorrectly() + { + var plugin = ObservabilityPlugin.Builder(_services) + .WithOtlpEndpoint(null) + .WithBackendUrl(null) + .WithServiceName(null) + .WithServiceVersion(null) + .WithEnvironment(null) + .Build(); + + Assert.That(plugin, Is.InstanceOf()); + } + } +} diff --git a/sdk/@launchdarkly/observability-dotnet/test/LaunchDarkly.Observability.Tests/UnitTest1.cs b/sdk/@launchdarkly/observability-dotnet/test/LaunchDarkly.Observability.Tests/UnitTest1.cs deleted file mode 100644 index 457ea3606..000000000 --- a/sdk/@launchdarkly/observability-dotnet/test/LaunchDarkly.Observability.Tests/UnitTest1.cs +++ /dev/null @@ -1,18 +0,0 @@ -using NUnit.Framework; - -namespace LaunchDarkly.Observability.Test -{ - public class Tests - { - [SetUp] - public void Setup() - { - } - - [Test] - public void Test1() - { - Assert.Pass(); - } - } -} \ No newline at end of file