diff --git a/sdk/@launchdarkly/observability-dotnet/.gitignore b/sdk/@launchdarkly/observability-dotnet/.gitignore
index 858f059cc..69b1c92ce 100644
--- a/sdk/@launchdarkly/observability-dotnet/.gitignore
+++ b/sdk/@launchdarkly/observability-dotnet/.gitignore
@@ -2,6 +2,8 @@
**/obj
LaunchDarkly.Observability.sln.DotSettings.user
**/launchSettings.json
+**/.vs/**
+packages/
docs/
api/
nupkgs/
diff --git a/sdk/@launchdarkly/observability-dotnet/README.md b/sdk/@launchdarkly/observability-dotnet/README.md
index 25b0fe63e..3e723e9e2 100644
--- a/sdk/@launchdarkly/observability-dotnet/README.md
+++ b/sdk/@launchdarkly/observability-dotnet/README.md
@@ -9,7 +9,7 @@ LaunchDarkly Observability Plugin for .Net
**NB: APIs are subject to change until a 1.x version is released.**
-## Install
+## Install for ASP.Net Core
```shell
dotnet add package LaunchDarkly.Observability
@@ -18,7 +18,54 @@ dotnet add package LaunchDarkly.Observability
Install the plugin when configuring your LaunchDarkly SDK.
```csharp
- // TODO: Add example.
+var builder = WebApplication.CreateBuilder(args);
+
+
+var config = Configuration.Builder("your-sdk-key")
+ .Plugins(new PluginConfigurationBuilder()
+ .Add(ObservabilityPlugin.Builder(builder.Services)
+ .WithServiceName("your-service-name")
+ .WithServiceVersion("example-sha")
+ .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();
+```
+
+## Install for an ASP.Net application with .Net Framework
+
+```csharp
+// In Global.asax.cs
+protected void Application_Start()
+ {
+ // Other application specific code.
+ var client = new LdClient(Configuration.Builder("your-sdk-key")
+ .Plugins(new PluginConfigurationBuilder().Add(ObservabilityPlugin.Builder()
+ .WithServiceName("your-service-name")
+ .WithServiceVersion("example-sha")
+ .Build()))
+ .Build());
+ }
+
+ protected void Application_End() {
+ Observe.Shutdown();
+ }
+```
+
+```xml
+
+
+ // Any existing content should remain and the following should be added.
+
+
+
+
```
LaunchDarkly overview
@@ -49,4 +96,4 @@ We encourage pull requests and other contributions from the community. Check out
[dotnetplugin-sdk-ci]: https://github.com/launchdarkly/observability-sdk/actions/workflows/dotnet-plugin.yml
[o11y-docs-link]: https://launchdarkly.github.io/observability-sdk/sdk/@launchdarkly/observability-dotnet/
[dotnetplugin-nuget-badge]: https://img.shields.io/nuget/v/LaunchDarkly.Observability.svg?style=flat-square
-[dotnetplugin-nuget-link]: https://www.nuget.org/packages/LaunchDarkly.Observability/
\ No newline at end of file
+[dotnetplugin-nuget-link]: https://www.nuget.org/packages/LaunchDarkly.Observability/
diff --git a/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/Asp/Core/ObservabilityExtensions.cs b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/Asp/Core/ObservabilityExtensions.cs
new file mode 100644
index 000000000..87e89eded
--- /dev/null
+++ b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/Asp/Core/ObservabilityExtensions.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using LaunchDarkly.Observability.Otel;
+using LaunchDarkly.Logging;
+using LaunchDarkly.Observability.Logging;
+using LaunchDarkly.Observability.Sampling;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using OpenTelemetry;
+using OpenTelemetry.Resources;
+using OpenTelemetry.Trace;
+using OpenTelemetry.Exporter;
+using OpenTelemetry.Logs;
+using OpenTelemetry.Metrics;
+
+// ReSharper disable once CheckNamespace
+namespace LaunchDarkly.Observability
+{
+ ///
+ /// Static class containing extension methods for configuring observability
+ ///
+ public static class ObservabilityExtensions
+ {
+ private class LdObservabilityHostedService : IHostedService
+ {
+ private readonly ObservabilityConfig _config;
+ private readonly ILoggerProvider _loggerProvider;
+
+ public LdObservabilityHostedService(ObservabilityConfig config, IServiceProvider provider)
+ {
+ _loggerProvider = provider.GetService();
+ _config = config;
+ }
+
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ Observe.Initialize(_config, _loggerProvider);
+ return Task.CompletedTask;
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+ }
+
+ internal static void AddLaunchDarklyObservabilityWithConfig(this IServiceCollection services,
+ ObservabilityConfig config, Logger logger = null)
+ {
+ DebugLogger.SetLogger(logger);
+
+ var sampler = CommonOtelOptions.GetSampler(config);
+
+ var resourceBuilder = CommonOtelOptions.GetResourceBuilder(config);
+
+ services.AddOpenTelemetry().WithTracing(tracing =>
+ {
+ tracing
+ .WithCommonLaunchDarklyConfig(config, resourceBuilder, sampler)
+ .AddAspNetCoreInstrumentation(options => { options.RecordException = true; });
+ }).WithLogging(logging =>
+ {
+ logging.SetResourceBuilder(resourceBuilder)
+ .AddProcessor(new SamplingLogProcessor(sampler))
+ .AddOtlpExporter(options =>
+ {
+ options.WithCommonLaunchDarklyLoggingExport(config);
+ });
+ config.ExtendedLoggerConfiguration?.Invoke(logging);
+ }).WithMetrics(metrics =>
+ {
+ metrics
+ .WithCommonLaunchDarklyConfig(config, resourceBuilder)
+ .AddAspNetCoreInstrumentation();
+ });
+
+ // Attach a hosted service which will allow us to get a logger provider instance from the built
+ // service collection.
+ services.AddHostedService((serviceProvider) =>
+ new LdObservabilityHostedService(config, serviceProvider));
+ }
+
+ ///
+ /// 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/Asp/Core/ObservabilityPlugin.cs
similarity index 94%
rename from sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/ObservabilityPlugin.cs
rename to sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/Asp/Core/ObservabilityPlugin.cs
index 0859fe116..9bead039e 100644
--- a/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/ObservabilityPlugin.cs
+++ b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/Asp/Core/ObservabilityPlugin.cs
@@ -6,7 +6,9 @@
using LaunchDarkly.Sdk.Server.Plugins;
using LaunchDarkly.Sdk.Server.Telemetry;
using Microsoft.Extensions.DependencyInjection;
+using OpenTelemetry.Logs;
+// ReSharper disable once CheckNamespace
namespace LaunchDarkly.Observability
{
public class ObservabilityPlugin : Plugin
@@ -20,7 +22,7 @@ public class ObservabilityPlugin : Plugin
/// 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
+ /// This method only needs to be used when observability related functionality must be initialized before it
/// is possible to initialize the LaunchDarkly SDK.
///
///
@@ -32,15 +34,16 @@ public class ObservabilityPlugin : Plugin
///
/// 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")
+ /// var config = Configuration.Builder("your-sdk-key")
/// .Plugins(new PluginConfigurationBuilder()
/// .Add(ObservabilityPlugin.Builder(builder.Services)
/// .WithServiceName("ryan-test-service")
- /// .WithServiceVersion("0.0.0")
+ /// .WithServiceVersion("example-sha")
/// .Build())).Build();
/// // Building the LdClient with the Observability plugin. This line will add services to the web application.
/// var client = new LdClient(config);
diff --git a/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/Asp/Legacy/ObservabilityPlugin.cs b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/Asp/Legacy/ObservabilityPlugin.cs
new file mode 100644
index 000000000..3eb157449
--- /dev/null
+++ b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/Asp/Legacy/ObservabilityPlugin.cs
@@ -0,0 +1,113 @@
+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;
+
+// ReSharper disable once CheckNamespace
+namespace LaunchDarkly.Observability
+{
+ public class ObservabilityPlugin : Plugin
+ {
+ private readonly ObservabilityPluginBuilder _config;
+
+ ///
+ /// Create a new builder for .
+ ///
+ /// When using this builder, LaunchDarkly client must be constructed during Application_Start of your
+ /// web application in Global.asax.cs.
+ /// For example:
+ ///
+ ///
+ /// protected void Application_Start()
+ /// {
+ /// // Other application specific code.
+ /// var client = new LdClient(Configuration.Builder("your-sdk-key")
+ /// .Plugins(new PluginConfigurationBuilder().Add(ObservabilityPlugin.Builder()
+ /// .WithServiceName("classic-asp-application")
+ /// .WithServiceVersion("example-sha")
+ /// .Build()))
+ /// .Build());
+ /// }
+ ///
+ /// protected void Application_End() {
+ /// Observe.Shutdown();
+ /// }
+ ///
+ ///
+ /// Additionally the Web.config must include the following.
+ ///
+ ///
+ ///
+ /// // Any existing content should remain and the following should be added.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// A new instance for configuring the observability plugin.
+ public static ObservabilityPluginBuilder Builder() =>
+ new ObservabilityPluginBuilder();
+
+ ///
+ /// 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 initialized before it
+ /// is possible to initialize the LaunchDarkly SDK.
+ ///
+ ///
+ /// an observability plugin instance
+ public static ObservabilityPlugin ForExistingServices() => new ObservabilityPlugin();
+
+ internal ObservabilityPlugin(ObservabilityPluginBuilder config) : base(
+ "LaunchDarkly.Observability")
+ {
+ _config = config;
+ }
+
+ internal ObservabilityPlugin() : base("LaunchDarkly.Observability")
+ {
+ _config = null;
+ }
+
+ ///
+ public override void Register(ILdClient client, EnvironmentMetadata metadata)
+ {
+ if (_config == null) return;
+ var config = _config.BuildConfig(metadata.Credential);
+ OpenTelemetry.Register(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
+ {
+ ///
+ /// Build an instance with the configured settings.
+ ///
+ /// The constructed .
+ public ObservabilityPlugin Build()
+ {
+ return new ObservabilityPlugin(this);
+ }
+ }
+ }
+}
diff --git a/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/Asp/Legacy/OpenTelemetryConfig.cs b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/Asp/Legacy/OpenTelemetryConfig.cs
new file mode 100644
index 000000000..c98b3cef4
--- /dev/null
+++ b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/Asp/Legacy/OpenTelemetryConfig.cs
@@ -0,0 +1,100 @@
+using LaunchDarkly.Logging;
+using LaunchDarkly.Observability.Otel;
+using Microsoft.Extensions.Logging;
+using OpenTelemetry;
+using OpenTelemetry.Logs;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
+
+// ReSharper disable once CheckNamespace
+namespace LaunchDarkly.Observability
+{
+ public static class OpenTelemetry
+ {
+ private static TracerProvider _tracerProvider;
+ private static MeterProvider _meterProvider;
+ private static readonly object ProviderLock = new object();
+ private static ILoggerFactory _loggerFactory;
+
+ ///
+ /// Extension method which adds LaunchDarkly logging to a logging factory.
+ ///
+ ///
+ /// using (var factory = LoggerFactory.Create(builder => { builder.AddLaunchDarklyLogging(config); })) {
+ /// // Use factory to get logger which uses LaunchDarkly loging.
+ /// }
+ ///
+ ///
+ /// the logging builder for the factory
+ /// the LaunchDarkly observability configuration
+ public static void AddLaunchDarklyLogging(this ILoggingBuilder loggingBuilder, ObservabilityConfig config)
+ {
+ loggingBuilder.AddOpenTelemetry(options =>
+ {
+ options.SetResourceBuilder(CommonOtelOptions.GetResourceBuilder(config))
+ .AddProcessor(new SamplingLogProcessor(CommonOtelOptions.GetSampler(config)))
+ .AddOtlpExporter(exportOptions => { exportOptions.WithCommonLaunchDarklyLoggingExport(config); });
+
+ config.ExtendedLoggerConfiguration?.Invoke(options);
+ });
+ }
+
+ ///
+ /// Configure LaunchDarkly observability. In typical usage, the ObservabilityPlugin should be used instead.
+ ///
+ /// This method is for advanced use-cases where the LaunchDarkly client needs to be initialized later than the
+ /// telemetry implementation.
+ ///
+ ///
+ /// If this method is used, then the should be instantiated using
+ /// method.
+ ///
+ ///
+ /// configuration for LaunchDarkly Observability
+ /// an optional debug logger
+ public static void Register(ObservabilityConfig config, Logger debugLogger = null)
+ {
+ lock (ProviderLock)
+ {
+ // If the providers are set, then the implementation has already been configured.
+ if (_tracerProvider != null)
+ {
+ return;
+ }
+
+ var resourceBuilder = CommonOtelOptions.GetResourceBuilder(config);
+ var sampler = CommonOtelOptions.GetSampler(config);
+
+ _tracerProvider = global::OpenTelemetry.Sdk.CreateTracerProviderBuilder()
+ .WithCommonLaunchDarklyConfig(config, resourceBuilder, sampler)
+ .AddAspNetInstrumentation()
+ .Build();
+
+ _meterProvider = global::OpenTelemetry.Sdk.CreateMeterProviderBuilder()
+ .WithCommonLaunchDarklyConfig(config, resourceBuilder)
+ .AddAspNetInstrumentation()
+ .Build();
+
+ _loggerFactory = LoggerFactory.Create(builder => { builder.AddLaunchDarklyLogging(config); });
+ var logger = _loggerFactory.CreateLogger();
+ Observe.Initialize(config, logger);
+ }
+ }
+
+ ///
+ /// Shutdown the underlying telemetry.
+ ///
+ public static void Shutdown()
+ {
+ lock (ProviderLock)
+ {
+ _tracerProvider?.Dispose();
+ _meterProvider?.Dispose();
+ _loggerFactory?.Dispose();
+ _tracerProvider = null;
+ _meterProvider = null;
+ _loggerFactory = null;
+ }
+ }
+ }
+}
diff --git a/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/BaseBuilder.cs b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/BaseBuilder.cs
index d24b3fd19..e7f9a283e 100644
--- a/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/BaseBuilder.cs
+++ b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/BaseBuilder.cs
@@ -6,6 +6,12 @@
namespace LaunchDarkly.Observability
{
+#if NETFRAMEWORK
+ using LoggerBuilderType = OpenTelemetryLoggerOptions;
+#else
+ using LoggerBuilderType = LoggerProviderBuilder;
+#endif
+
///
/// Base builder which allows for methods to be shared between building a config directly and building a plugin.
///
@@ -23,7 +29,7 @@ public class BaseBuilder where TBuilder : BaseBuilder
private string _environment = string.Empty;
private string _serviceVersion = string.Empty;
private Action _extendedTracerConfiguration;
- private Action _extendedLoggerConfiguration;
+ private Action _extendedLoggerConfiguration;
private Action _extendedMeterConfiguration;
protected BaseBuilder()
@@ -171,7 +177,7 @@ public TBuilder WithExtendedTracingConfig(Action extended
///
/// A function used to extend the logging configuration.
/// A reference to this builder.
- public TBuilder WithExtendedLoggerConfiguration(Action extendedLoggerConfiguration)
+ public TBuilder WithExtendedLoggerConfiguration(Action extendedLoggerConfiguration)
{
_extendedLoggerConfiguration = extendedLoggerConfiguration;
return (TBuilder)this;
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 9b0e9e7bd..9156aa90e 100644
--- a/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/LaunchDarkly.Observability.csproj
+++ b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/LaunchDarkly.Observability.csproj
@@ -1,71 +1,97 @@
-
-
-
- 0.1.0
-
- disable
-
- netstandard2.0;net471;net8.0
- $(BUILDFRAMEWORKS)
- 7.3
-
- LaunchDarkly Observability for Server-Side .NET SDK
- LaunchDarkly
- LaunchDarkly
- LaunchDarkly
- Copyright 2025 Catamorphic, Co
- Apache-2.0
- https://github.com/launchdarkly/observability-sdk
- https://github.com/launchdarkly/observability-sdk
-
-
- 1570,1571,1572,1573,1574,1580,1581,1584,1591,1710,1711,1712
-
-
-
-
-
-
-
-
-
-
- ../../../../../LaunchDarkly.snk
- true
-
-
-
- true
- snupkg
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ 0.1.0
+
+ disable
+
+ netstandard2.0;net471;net8.0
+ $(BUILDFRAMEWORKS)
+ 7.3
+
+ LaunchDarkly Observability for Server-Side .NET SDK
+ LaunchDarkly
+ LaunchDarkly
+ LaunchDarkly
+ Copyright 2025 Catamorphic, Co
+ Apache-2.0
+ https://github.com/launchdarkly/observability-sdk
+ https://github.com/launchdarkly/observability-sdk
+
+ false
+
+
+ 1570,1571,1572,1573,1574,1580,1581,1584,1591,1710,1711,1712
+
+
+
+
+
+
+
+
+
+
+ ../../../../../LaunchDarkly.snk
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ snupkg
+
+
+
+
+
+
+
diff --git a/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/ObservabilityConfig.cs b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/ObservabilityConfig.cs
index 3da379deb..16faee42a 100644
--- a/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/ObservabilityConfig.cs
+++ b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/ObservabilityConfig.cs
@@ -5,7 +5,13 @@
namespace LaunchDarkly.Observability
{
- public struct ObservabilityConfig
+#if NETFRAMEWORK
+ using LoggerBuilderType = OpenTelemetryLoggerOptions;
+#else
+ using LoggerBuilderType = LoggerProviderBuilder;
+#endif
+
+ public class ObservabilityConfig
{
///
/// The configured OTLP endpoint.
@@ -52,7 +58,7 @@ public struct ObservabilityConfig
///
/// Function which extends the configuration of the logger provider.
///
- public Action ExtendedLoggerConfiguration { get; }
+ public Action ExtendedLoggerConfiguration { get; }
///
/// Function which extends the configuration of the meter provider.
@@ -67,9 +73,9 @@ internal ObservabilityConfig(
string serviceVersion,
string sdkKey,
Action extendedTracerConfiguration,
- Action extendedLoggerConfiguration,
+ Action extendedLoggerConfiguration,
Action extendedMeterConfiguration
- )
+ )
{
OtlpEndpoint = otlpEndpoint;
BackendUrl = backendUrl;
diff --git a/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/ObservabilityExtensions.cs b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/ObservabilityExtensions.cs
deleted file mode 100644
index a97b7dfd1..000000000
--- a/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/ObservabilityExtensions.cs
+++ /dev/null
@@ -1,186 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using LaunchDarkly.Observability.Otel;
-using LaunchDarkly.Logging;
-using LaunchDarkly.Observability.Logging;
-using LaunchDarkly.Observability.Sampling;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.Logging;
-using OpenTelemetry;
-using OpenTelemetry.Resources;
-using OpenTelemetry.Trace;
-using OpenTelemetry.Exporter;
-using OpenTelemetry.Logs;
-using OpenTelemetry.Metrics;
-
-namespace LaunchDarkly.Observability
-{
- ///
- /// Static class containing extension methods for configuring observability
- ///
- public static class ObservabilityExtensions
- {
- private const OtlpExportProtocol ExportProtocol = OtlpExportProtocol.HttpProtobuf;
- private const int FlushIntervalMs = 5 * 1000;
- private const int MaxExportBatchSize = 10000;
- private const int MaxQueueSize = 10000;
- private const int ExportTimeoutMs = 30000;
-
- private const string TracesPath = "/v1/traces";
- private const string LogsPath = "/v1/logs";
- private const string MetricsPath = "/v1/metrics";
-
-
- private class LdObservabilityHostedService : IHostedService
- {
- private readonly ObservabilityConfig _config;
- private readonly ILoggerProvider _loggerProvider;
-
- public LdObservabilityHostedService(ObservabilityConfig config, IServiceProvider provider)
- {
- _loggerProvider = provider.GetService();
- _config = config;
- }
-
- public Task StartAsync(CancellationToken cancellationToken)
- {
- Observe.Initialize(_config, _loggerProvider);
- return Task.CompletedTask;
- }
-
- public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
- }
-
- private static async Task GetSamplingConfigAsync(CustomSampler sampler, ObservabilityConfig config)
- {
- using (var samplingClient = new SamplingConfigClient(config.BackendUrl))
- {
- try
- {
- var res = await samplingClient.GetSamplingConfigAsync(config.SdkKey).ConfigureAwait(false);
- if (res == null) return;
- sampler.SetConfig(res);
- }
- catch (Exception ex)
- {
- DebugLogger.DebugLog($"Exception while getting sampling config: {ex}");
- }
- }
- }
-
- private static IEnumerable> GetResourceAttributes(ObservabilityConfig config)
- {
- var attrs = new List>();
-
- if (!string.IsNullOrWhiteSpace(config.Environment))
- {
- attrs.Add(
- new KeyValuePair(AttributeNames.DeploymentEnvironment, config.Environment));
- }
-
- attrs.Add(new KeyValuePair(AttributeNames.ProjectId, config.SdkKey));
-
- return attrs;
- }
-
- internal static void AddLaunchDarklyObservabilityWithConfig(this IServiceCollection services,
- ObservabilityConfig config, Logger logger = null)
- {
- DebugLogger.SetLogger(logger);
- var resourceAttributes = GetResourceAttributes(config);
-
- var sampler = new CustomSampler();
-
- // Asynchronously get sampling config.
- _ = Task.Run(() => GetSamplingConfigAsync(sampler, config));
-
- var resourceBuilder = ResourceBuilder.CreateDefault();
- if (!string.IsNullOrWhiteSpace(config.ServiceName))
- {
- resourceBuilder.AddService(config.ServiceName, serviceVersion: config.ServiceVersion);
- resourceBuilder.AddAttributes(resourceAttributes);
- }
-
- services.AddOpenTelemetry().WithTracing(tracing =>
- {
- tracing.SetResourceBuilder(resourceBuilder)
- .AddHttpClientInstrumentation()
- .AddGrpcClientInstrumentation()
- .AddWcfInstrumentation()
- .AddQuartzInstrumentation()
- .AddAspNetCoreInstrumentation(options => { options.RecordException = true; })
- .AddSqlClientInstrumentation(options => { options.SetDbStatementForText = true; })
- .AddSource(DefaultNames.ActivitySourceNameOrDefault(config.ServiceName));
-
- // Always use sampling exporter for traces
- var samplingTraceExporter = new SamplingTraceExporter(sampler, new OtlpExporterOptions
- {
- Endpoint = new Uri(config.OtlpEndpoint + TracesPath),
- Protocol = OtlpExportProtocol.HttpProtobuf,
- });
-
- tracing.AddProcessor(new BatchActivityExportProcessor(samplingTraceExporter, MaxQueueSize,
- FlushIntervalMs, ExportTimeoutMs, MaxExportBatchSize));
- config.ExtendedTracerConfiguration?.Invoke(tracing);
- }).WithLogging(logging =>
- {
- logging.SetResourceBuilder(resourceBuilder)
- .AddProcessor(new SamplingLogProcessor(sampler))
- .AddOtlpExporter(options =>
- {
- options.Endpoint = new Uri(config.OtlpEndpoint + LogsPath);
- options.Protocol = ExportProtocol;
- options.BatchExportProcessorOptions.MaxExportBatchSize = MaxExportBatchSize;
- options.BatchExportProcessorOptions.MaxQueueSize = MaxQueueSize;
- options.BatchExportProcessorOptions.ScheduledDelayMilliseconds = FlushIntervalMs;
- });
- config.ExtendedLoggerConfiguration?.Invoke(logging);
- }).WithMetrics(metrics =>
- {
- metrics.SetResourceBuilder(resourceBuilder)
- .AddMeter(DefaultNames.MeterNameOrDefault(config.ServiceName))
- .AddRuntimeInstrumentation()
- .AddProcessInstrumentation()
- .AddHttpClientInstrumentation()
- .AddAspNetCoreInstrumentation()
- .AddSqlClientInstrumentation()
- .AddReader(new PeriodicExportingMetricReader(new OtlpMetricExporter(new OtlpExporterOptions
- {
- Endpoint = new Uri(config.OtlpEndpoint + MetricsPath),
- Protocol = ExportProtocol
- })));
- config.ExtendedMeterConfiguration?.Invoke(metrics);
- });
-
- // Attach a hosted service which will allow us to get a logger provider instance from the built
- // service collection.
- services.AddHostedService((serviceProvider) =>
- new LdObservabilityHostedService(config, serviceProvider));
- }
-
- ///
- /// 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/Observe.cs b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/Observe.cs
index 0f3e03f9b..5c1c7019c 100644
--- a/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/Observe.cs
+++ b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/Observe.cs
@@ -31,6 +31,7 @@ private class Instance
public readonly ConcurrentDictionary> UpDownCounters =
new ConcurrentDictionary>();
+#if NETSTANDARD2_0 || NET5_0_OR_GREATER
internal Instance(ObservabilityConfig config, ILoggerProvider loggerProvider)
{
Meter = new Meter(DefaultNames.MeterNameOrDefault(config.ServiceName),
@@ -42,14 +43,32 @@ internal Instance(ObservabilityConfig config, ILoggerProvider loggerProvider)
Logger = loggerProvider.CreateLogger(DefaultNames.LoggerNameOrDefault(config.ServiceName));
}
}
+#endif
+
+#if NETFRAMEWORK
+ internal Instance(ObservabilityConfig config, ILogger logger) {
+ Meter = new Meter(DefaultNames.MeterNameOrDefault(config.ServiceName), config.ServiceVersion);
+ ActivitySource = new ActivitySource(DefaultNames.ActivitySourceNameOrDefault(config.ServiceName), config.ServiceVersion);
+ Logger = logger;
+ }
+#endif
}
private static Instance _instance;
+#if NETSTANDARD2_0 || NET5_0_OR_GREATER
internal static void Initialize(ObservabilityConfig config, ILoggerProvider loggerProvider)
{
Volatile.Write(ref _instance, new Instance(config, loggerProvider));
}
+#endif
+
+#if NETFRAMEWORK
+ internal static void Initialize(ObservabilityConfig config, ILogger logger)
+ {
+ Volatile.Write(ref _instance, new Instance(config, logger));
+ }
+#endif
private static Instance GetInstance()
{
@@ -130,6 +149,16 @@ public static void RecordException(Exception exception, IDictionary
+ /// Record a metric gauge value.
+ ///
+ /// Records a measurement value for a gauge metric with the specified name and optional attributes.
+ ///
+ ///
+ /// the name of the metric
+ /// the value to record
+ /// any additional attributes to add to the metric
+ /// thrown when name is null
public static void RecordMetric(string name, double value, IDictionary attributes = null)
{
if (name == null) throw new ArgumentNullException(nameof(name), "Metric name cannot be null.");
@@ -149,6 +178,17 @@ public static void RecordMetric(string name, double value, IDictionary
+ /// Record a counter value.
+ ///
+ /// Records a count value for a counter metric with the specified name and optional attributes.
+ /// Counter values should be monotonically increasing.
+ ///
+ ///
+ /// the name of the counter
+ /// the value to add to the counter
+ /// any additional attributes to add to the counter
+ /// thrown when name is null
public static void RecordCount(string name, long value, IDictionary attributes = null)
{
if (name == null) throw new ArgumentNullException(nameof(name), "Count name cannot be null.");
@@ -168,11 +208,30 @@ public static void RecordCount(string name, long value, IDictionary
+ /// Increment a counter by 1.
+ ///
+ /// This is a convenience method equivalent to calling RecordCount with a value of 1.
+ ///
+ ///
+ /// the name of the counter
+ /// any additional attributes to add to the counter
public static void RecordIncr(string name, IDictionary attributes = null)
{
RecordCount(name, 1, attributes);
}
+ ///
+ /// Record a histogram value.
+ ///
+ /// Records a measurement value for a histogram metric with the specified name and optional attributes.
+ /// Histograms are used to record distributions of values.
+ ///
+ ///
+ /// the name of the histogram
+ /// the value to record
+ /// any additional attributes to add to the histogram
+ /// thrown when name is null
public static void RecordHistogram(string name, double value, IDictionary attributes = null)
{
if (name == null) throw new ArgumentNullException(nameof(name), "Histogram name cannot be null.");
@@ -193,6 +252,17 @@ public static void RecordHistogram(string name, double value, IDictionary
+ /// Record an up-down counter value.
+ ///
+ /// Records a delta value for an up-down counter metric with the specified name and optional attributes.
+ /// Up-down counters can increase or decrease and are useful for tracking values like queue size.
+ ///
+ ///
+ /// the name of the up-down counter
+ /// the delta value to add (can be positive or negative)
+ /// any additional attributes to add to the counter
+ /// thrown when name is null
public static void RecordUpDownCounter(string name, long delta, IDictionary attributes = null)
{
if (name == null) throw new ArgumentNullException(nameof(name), "UpDownCounter name cannot be null.");
@@ -214,6 +284,16 @@ public static void RecordUpDownCounter(string name, long delta, IDictionary
+ /// Record a log message.
+ ///
+ /// Records a log message with the specified level and optional attributes using the configured logger.
+ /// If no logger is configured, the log message will be ignored.
+ ///
+ ///
+ /// the log message
+ /// the log level
+ /// any additional attributes to add to the log entry
public static void RecordLog(string message, LogLevel level, IDictionary attributes)
{
WithInstance(instance =>
@@ -228,6 +308,17 @@ public static void RecordLog(string message, LogLevel level, IDictionary
+ /// Start a new activity (span) with the specified name and kind.
+ ///
+ /// Creates and starts a new activity using the configured ActivitySource. The activity
+ /// should be disposed when the operation is complete to properly end the span.
+ ///
+ ///
+ /// the name of the activity
+ /// the kind of activity (defaults to Internal)
+ /// any initial attributes to add to the activity
+ /// the started activity, or null if the observability system is not initialized
public static Activity StartActivity(string name, ActivityKind kind = ActivityKind.Internal,
IDictionary attributes = null)
{
@@ -244,5 +335,19 @@ public static Activity StartActivity(string name, ActivityKind kind = ActivityKi
return activity;
}
+
+#if NETFRAMEWORK
+ ///
+ /// Shutdown LaunchDarkly Observability.
+ ///
+ /// Properly shuts down the OpenTelemetry system and flushes any pending telemetry data.
+ /// This method is only available on .NET Framework.
+ ///
+ ///
+ public static void Shutdown()
+ {
+ OpenTelemetry.Shutdown();
+ }
+#endif
}
}
diff --git a/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/Otel/CommonOtelOptions.cs b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/Otel/CommonOtelOptions.cs
new file mode 100644
index 000000000..3f765ada2
--- /dev/null
+++ b/sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/Otel/CommonOtelOptions.cs
@@ -0,0 +1,141 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using LaunchDarkly.Observability.Logging;
+using LaunchDarkly.Observability.Sampling;
+using LaunchDarkly.Sdk.Internal.Concurrent;
+using OpenTelemetry;
+using OpenTelemetry.Exporter;
+using OpenTelemetry.Logs;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Resources;
+using OpenTelemetry.Trace;
+
+namespace LaunchDarkly.Observability.Otel
+{
+ internal static class CommonOtelOptions
+ {
+ private const OtlpExportProtocol ExportProtocol = OtlpExportProtocol.HttpProtobuf;
+ private const int FlushIntervalMs = 5 * 1000;
+ private const int MaxExportBatchSize = 10000;
+ private const int MaxQueueSize = 10000;
+ private const int ExportTimeoutMs = 30000;
+
+ private const string TracesPath = "/v1/traces";
+ private const string LogsPath = "/v1/logs";
+ private const string MetricsPath = "/v1/metrics";
+
+ private static readonly ConcurrentDictionary Samplers =
+ new ConcurrentDictionary();
+
+ private static async Task GetSamplingConfigAsync(CustomSampler sampler, ObservabilityConfig config)
+ {
+ using (var samplingClient = new SamplingConfigClient(config.BackendUrl))
+ {
+ try
+ {
+ var res = await samplingClient.GetSamplingConfigAsync(config.SdkKey).ConfigureAwait(false);
+ if (res == null) return;
+ sampler.SetConfig(res);
+ }
+ catch (Exception ex)
+ {
+ DebugLogger.DebugLog($"Exception while getting sampling config: {ex}");
+ }
+ }
+ }
+
+ private static IEnumerable> GetResourceAttributes(ObservabilityConfig config)
+ {
+ var attrs = new List>();
+
+ if (!string.IsNullOrWhiteSpace(config.Environment))
+ {
+ attrs.Add(
+ new KeyValuePair(AttributeNames.DeploymentEnvironment, config.Environment));
+ }
+
+ attrs.Add(new KeyValuePair(AttributeNames.ProjectId, config.SdkKey));
+
+ return attrs;
+ }
+
+ public static CustomSampler GetSampler(ObservabilityConfig config)
+ {
+ return Samplers.GetOrAdd(config.SdkKey, _ =>
+ {
+ var sampler = new CustomSampler();
+ Task.Run(() => GetSamplingConfigAsync(sampler, config));
+ return sampler;
+ });
+ }
+
+ public static ResourceBuilder GetResourceBuilder(ObservabilityConfig config)
+ {
+ var resourceBuilder = ResourceBuilder.CreateDefault();
+ if (string.IsNullOrWhiteSpace(config.ServiceName)) return resourceBuilder;
+ resourceBuilder.AddService(config.ServiceName, serviceVersion: config.ServiceVersion);
+ resourceBuilder.AddAttributes(GetResourceAttributes(config));
+ return resourceBuilder;
+ }
+
+
+ public static TracerProviderBuilder WithCommonLaunchDarklyConfig(this TracerProviderBuilder builder,
+ ObservabilityConfig config, ResourceBuilder resourceBuilder, CustomSampler sampler)
+ {
+ var samplingTraceExporter = new SamplingTraceExporter(sampler, new OtlpExporterOptions
+ {
+ Endpoint = new Uri(config.OtlpEndpoint + TracesPath),
+ Protocol = OtlpExportProtocol.HttpProtobuf,
+ BatchExportProcessorOptions =
+ {
+ MaxExportBatchSize = MaxExportBatchSize,
+ MaxQueueSize = MaxQueueSize,
+ ScheduledDelayMilliseconds = FlushIntervalMs
+ }
+ });
+
+ builder.SetResourceBuilder(resourceBuilder)
+ .AddHttpClientInstrumentation()
+ .AddGrpcClientInstrumentation()
+ .AddWcfInstrumentation()
+ .AddQuartzInstrumentation()
+ .AddSqlClientInstrumentation(options => { options.SetDbStatementForText = true; })
+ .AddSource(DefaultNames.ActivitySourceNameOrDefault(config.ServiceName))
+ .AddProcessor(new BatchActivityExportProcessor(samplingTraceExporter, MaxQueueSize,
+ FlushIntervalMs, ExportTimeoutMs, MaxExportBatchSize));
+
+ config.ExtendedTracerConfiguration?.Invoke(builder);
+ return builder;
+ }
+
+ public static MeterProviderBuilder WithCommonLaunchDarklyConfig(this MeterProviderBuilder builder,
+ ObservabilityConfig config, ResourceBuilder resourceBuilder)
+ {
+ builder.SetResourceBuilder(resourceBuilder)
+ .AddMeter(DefaultNames.MeterNameOrDefault(config.ServiceName))
+ .AddRuntimeInstrumentation()
+ .AddProcessInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddSqlClientInstrumentation()
+ .AddReader(new PeriodicExportingMetricReader(new OtlpMetricExporter(new OtlpExporterOptions
+ {
+ Endpoint = new Uri(config.OtlpEndpoint + MetricsPath),
+ Protocol = ExportProtocol
+ })));
+ config.ExtendedMeterConfiguration?.Invoke(builder);
+ return builder;
+ }
+
+ public static void WithCommonLaunchDarklyLoggingExport(this OtlpExporterOptions options,
+ ObservabilityConfig config)
+ {
+ options.Endpoint = new Uri(config.OtlpEndpoint + LogsPath);
+ options.Protocol = ExportProtocol;
+ options.BatchExportProcessorOptions.MaxExportBatchSize = MaxExportBatchSize;
+ options.BatchExportProcessorOptions.MaxQueueSize = MaxQueueSize;
+ options.BatchExportProcessorOptions.ScheduledDelayMilliseconds = FlushIntervalMs;
+ }
+ }
+}