Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Static class containing extension methods for configuring observability
/// </summary>
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<ILoggerProvider>();
_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));
}

/// <summary>
/// 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.
/// </summary>
/// <param name="services">The service collection</param>
/// <param name="sdkKey">The LaunchDarkly SDK</param>
/// <param name="configure">A method to configure the services</param>
/// <returns>The service collection</returns>
public static IServiceCollection AddLaunchDarklyObservability(
this IServiceCollection services,
string sdkKey,
Action<ObservabilityConfig.ObservabilityConfigBuilder> configure)
{
var builder = ObservabilityConfig.Builder();
configure(builder);

var config = builder.Build(sdkKey);
AddLaunchDarklyObservabilityWithConfig(services, config);
return services;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,6 +34,7 @@ public class ObservabilityPlugin : Plugin
/// <para>
/// When using this builder, LaunchDarkly client must be constructed before your application is built.
/// For example:
///
/// <code>
/// var builder = WebApplication.CreateBuilder(args);
///
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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;
using OpenTelemetry.Logs;

// ReSharper disable once CheckNamespace
namespace LaunchDarkly.Observability
{
public class ObservabilityPlugin : Plugin
{
private readonly ObservabilityPluginBuilder _config;
private readonly IServiceCollection _services;

/// <summary>
/// Create a new builder for <see cref="ObservabilityPlugin"/>.
/// <para>
/// When using this builder, LaunchDarkly client must be constructed before your application is built.
/// For example:
///
/// <code>
/// TODO: Add example.
/// </code>
/// </para>
/// </summary>
/// <returns>A new <see cref="ObservabilityPluginBuilder"/> instance for configuring the observability plugin.</returns>
public static ObservabilityPluginBuilder Builder() =>
new ObservabilityPluginBuilder();

internal ObservabilityPlugin(ObservabilityPluginBuilder config) : base(
"LaunchDarkly.Observability")
{
_config = config;
}

internal ObservabilityPlugin() : base("LaunchDarkly.Observability")
{
_services = null;
_config = null;
}

/// <inheritdoc />
public override void Register(ILdClient client, EnvironmentMetadata metadata)
{
if (_services == null || _config == null) return;
var config = _config.BuildConfig(metadata.Credential);
OpenTelemetry.Register(config);
}

/// <inheritdoc />
public override IList<Hook> GetHooks(EnvironmentMetadata metadata)
{
return new List<Hook>
{
TracingHook.Builder().IncludeValue().Build()
};
}

/// <summary>
/// Used to build an instance of the Observability Plugin.
/// </summary>
public sealed class ObservabilityPluginBuilder : BaseBuilder<ObservabilityPluginBuilder>
{
/// <summary>
/// Build an <see cref="ObservabilityPlugin"/> instance with the configured settings.
/// </summary>
/// <returns>The constructed <see cref="ObservabilityPlugin"/>.</returns>
public ObservabilityPlugin Build()
{
return new ObservabilityPlugin(this);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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();

/// <summary>
/// Extension method which adds LaunchDarkly logging to a logging factory.
///
/// <code>
/// using (var factory = LoggerFactory.Create(builder => { builder.AddLaunchDarklyLogging(config); })) {
/// // Use factory to get logger which uses LaunchDarkly loging.
/// }
/// </code>
/// </summary>
/// <param name="loggingBuilder">the logging builder for the factory</param>
/// <param name="config">the LaunchDarkly observability configuration</param>
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); });
});
}

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();
}

using (var factory = LoggerFactory.Create(builder => { builder.AddLaunchDarklyLogging(config); }))
{
var logger = factory.CreateLogger<ObservabilityPlugin>();
Observe.Initialize(config, logger);
}
}

public static void Shutdown()
{
lock (ProviderLock)
{
_tracerProvider?.Dispose();
_meterProvider?.Dispose();
_tracerProvider = null;
_meterProvider = null;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@

namespace LaunchDarkly.Observability
{
#if NETFRAMEWORK
using LoggerBuilderType = OpenTelemetryLoggerOptions;
#else
using LoggerBuilderType = LoggerProviderBuilder;
#endif

/// <summary>
/// Base builder which allows for methods to be shared between building a config directly and building a plugin.
/// <remarks>
Expand All @@ -23,7 +29,7 @@ public class BaseBuilder<TBuilder> where TBuilder : BaseBuilder<TBuilder>
private string _environment = string.Empty;
private string _serviceVersion = string.Empty;
private Action<TracerProviderBuilder> _extendedTracerConfiguration;
private Action<LoggerProviderBuilder> _extendedLoggerConfiguration;
private Action<LoggerBuilderType> _extendedLoggerConfiguration;
private Action<MeterProviderBuilder> _extendedMeterConfiguration;

protected BaseBuilder()
Expand Down Expand Up @@ -171,7 +177,7 @@ public TBuilder WithExtendedTracingConfig(Action<TracerProviderBuilder> extended
/// </example>
/// <param name="extendedLoggerConfiguration">A function used to extend the logging configuration.</param>
/// <returns>A reference to this builder.</returns>
public TBuilder WithExtendedLoggerConfiguration(Action<LoggerProviderBuilder> extendedLoggerConfiguration)
public TBuilder WithExtendedLoggerConfiguration(Action<LoggerBuilderType> extendedLoggerConfiguration)
{
_extendedLoggerConfiguration = extendedLoggerConfiguration;
return (TBuilder)this;
Expand Down
Loading
Loading