diff --git a/examples/Example.Console/Example.Console.csproj b/examples/Example.Console/Example.Console.csproj index 54f69987..19251a4a 100644 --- a/examples/Example.Console/Example.Console.csproj +++ b/examples/Example.Console/Example.Console.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/examples/Example.MinimalApi/CustomProcessor.cs b/examples/Example.MinimalApi/CustomProcessor.cs new file mode 100644 index 00000000..7f6488b8 --- /dev/null +++ b/examples/Example.MinimalApi/CustomProcessor.cs @@ -0,0 +1,18 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Diagnostics; +using OpenTelemetry; + +namespace Example.MinimalApi; + +public class CustomProcessor : BaseProcessor +{ + public override void OnEnd(Activity data) + { + Thread.Sleep(1000); // Enough time for the export to have happened + + data.SetTag("custom-processor-tag", "ProcessedByCustomProcessor"); + } +} diff --git a/examples/Example.MinimalApi/Example.MinimalApi.csproj b/examples/Example.MinimalApi/Example.MinimalApi.csproj index 6421eb72..7101206b 100644 --- a/examples/Example.MinimalApi/Example.MinimalApi.csproj +++ b/examples/Example.MinimalApi/Example.MinimalApi.csproj @@ -1,9 +1,10 @@ - + net9.0 enable enable + 4b1d6eb4-a323-4148-8195-1ec759984c7d @@ -12,7 +13,7 @@ - + diff --git a/examples/Example.MinimalApi/Program.cs b/examples/Example.MinimalApi/Program.cs index f79b9b6c..f207b9d6 100644 --- a/examples/Example.MinimalApi/Program.cs +++ b/examples/Example.MinimalApi/Program.cs @@ -5,18 +5,41 @@ using System.Diagnostics; using Example.MinimalApi; using OpenTelemetry; +using OpenTelemetry.Exporter; +using OpenTelemetry.Trace; var builder = WebApplication.CreateBuilder(args); -//builder.AddElasticOpenTelemetry(); +var options = new Elastic.OpenTelemetry.ElasticOpenTelemetryOptions() +{ + // You can customize options here if needed +}; -// This will add the OpenTelemetry services using Elastic defaults -builder.AddServiceDefaults(); +//// Force small batch size and no delay for quicker exports in this example +builder.Services.Configure(o => +{ + o.BatchExportProcessorOptions.MaxExportBatchSize = 1; + o.BatchExportProcessorOptions.ScheduledDelayMilliseconds = 1; +}); + +//builder.Services.AddOpenTelemetry() +// .WithTracing(t => t +// .WithElasticDefaults(builder.Configuration, t => t +// .AddSource(Api.ActivitySourceName) +// .AddProcessor(new CustomProcessor()))); + +builder.Services.AddOpenTelemetry() + .WithElasticTracing(builder.Configuration, t => t + .AddSource(Api.ActivitySourceName) + .AddProcessor(new CustomProcessor())); + +// Preferred method to add Elastic OpenTelemetry integration +//builder.AddElasticOpenTelemetry(options, otelBuilder => otelBuilder +// .WithTracing(t => t +// .AddSource(Api.ActivitySourceName) +// .AddProcessor(new CustomProcessor()))); -builder.Services - .AddHttpClient() // Adds IHttpClientFactory - .AddElasticOpenTelemetry() // Adds app specific tracing - .WithTracing(t => t.AddSource(Api.ActivitySourceName)); +builder.AddServiceDefaults(); var app = builder.Build(); diff --git a/examples/ServiceDefaults/Extensions.cs b/examples/ServiceDefaults/Extensions.cs index 7cea9cba..88da21b5 100644 --- a/examples/ServiceDefaults/Extensions.cs +++ b/examples/ServiceDefaults/Extensions.cs @@ -18,7 +18,7 @@ public static class Extensions { public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) { - builder.ConfigureOpenTelemetry(); + //builder.ConfigureOpenTelemetry(); builder.AddDefaultHealthChecks(); @@ -49,8 +49,6 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati } }); - builder.AddOpenTelemetryExporters(); - return builder; } diff --git a/examples/ServiceDefaults/ServiceDefaults.csproj b/examples/ServiceDefaults/ServiceDefaults.csproj index 6caf2c8d..e568d85f 100644 --- a/examples/ServiceDefaults/ServiceDefaults.csproj +++ b/examples/ServiceDefaults/ServiceDefaults.csproj @@ -1,7 +1,7 @@ - net9.0 + net8.0;net9.0 enable enable true @@ -9,13 +9,13 @@ - + - - - - + + + + diff --git a/src/Elastic.OpenTelemetry.AutoInstrumentation/AutoInstrumentationPlugin.cs b/src/Elastic.OpenTelemetry.AutoInstrumentation/AutoInstrumentationPlugin.cs index 341b103d..58cbe41b 100644 --- a/src/Elastic.OpenTelemetry.AutoInstrumentation/AutoInstrumentationPlugin.cs +++ b/src/Elastic.OpenTelemetry.AutoInstrumentation/AutoInstrumentationPlugin.cs @@ -43,6 +43,7 @@ public TracerProviderBuilder BeforeConfigureTracerProvider(TracerProviderBuilder builder.ConfigureResource(r => r.WithElasticDefaultsCore(_components, null, null)); builder.ConfigureServices(sc => sc.Configure(OtlpExporterDefaults.OtlpExporterOptions)); + logger.LogConfiguredOtlpExporterOptions(); TracerProvderBuilderExtensions.AddActivitySourceWithLogging(builder, logger, "Elastic.Transport", ""); TracerProvderBuilderExtensions.AddElasticProcessorsCore(builder, null, _components, null); @@ -74,6 +75,7 @@ public MeterProviderBuilder BeforeConfigureMeterProvider(MeterProviderBuilder bu builder.ConfigureServices(sc => sc .Configure(OtlpExporterDefaults.OtlpExporterOptions) .Configure(o => o.TemporalityPreference = MetricReaderTemporalityPreference.Delta)); + logger.LogConfiguredOtlpExporterOptions(); logger.LogConfiguredSignalProvider(nameof(Signals.Metrics), nameof(MeterProviderBuilder), ""); @@ -91,8 +93,7 @@ public MeterProviderBuilder BeforeConfigureMeterProvider(MeterProviderBuilder bu /// /// To configure logs SDK (the method name is the same as for other logs options). /// - public void ConfigureLogsOptions(OpenTelemetryLoggerOptions options) => - options.WithElasticDefaults(_components.Logger); + public void ConfigureLogsOptions(OpenTelemetryLoggerOptions options) => options.WithElasticDefaults(_components.Logger); /// /// To configure Resource. diff --git a/src/Elastic.OpenTelemetry.Core/BuilderContext.cs b/src/Elastic.OpenTelemetry.Core/BuilderContext.cs new file mode 100644 index 00000000..0ed1943f --- /dev/null +++ b/src/Elastic.OpenTelemetry.Core/BuilderContext.cs @@ -0,0 +1,21 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Microsoft.Extensions.DependencyInjection; + +namespace Elastic.OpenTelemetry.Core; + +/// +/// This is used internally to pass context around during the fluent builder configuration process. +/// +internal sealed class BuilderContext where T : class +{ + internal required T Builder { get; init; } + + internal required BuilderState BuilderState { get; init; } + + internal required BuilderOptions BuilderOptions { get; init; } + + internal IServiceCollection? Services { get; init; } +} diff --git a/src/Elastic.OpenTelemetry.Core/BuilderOptions.cs b/src/Elastic.OpenTelemetry.Core/BuilderOptions.cs new file mode 100644 index 00000000..90b80d05 --- /dev/null +++ b/src/Elastic.OpenTelemetry.Core/BuilderOptions.cs @@ -0,0 +1,9 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +namespace Elastic.OpenTelemetry.Core; + +internal readonly record struct BuilderOptions( + Action? UserProvidedConfigureBuilder, + bool DeferAddOtlpExporter) where T : class; diff --git a/src/Elastic.OpenTelemetry.Core/Diagnostics/CompositeLogger.cs b/src/Elastic.OpenTelemetry.Core/Diagnostics/CompositeLogger.cs index 7fc46501..0b7950b2 100644 --- a/src/Elastic.OpenTelemetry.Core/Diagnostics/CompositeLogger.cs +++ b/src/Elastic.OpenTelemetry.Core/Diagnostics/CompositeLogger.cs @@ -4,7 +4,6 @@ using Elastic.OpenTelemetry.Configuration; using Elastic.OpenTelemetry.Core; -using Elastic.OpenTelemetry.Diagnostics; using Microsoft.Extensions.Logging; namespace Elastic.OpenTelemetry.Diagnostics; diff --git a/src/Elastic.OpenTelemetry.Core/Diagnostics/DeferredLogger.cs b/src/Elastic.OpenTelemetry.Core/Diagnostics/DeferredLogger.cs new file mode 100644 index 00000000..b9f12bfb --- /dev/null +++ b/src/Elastic.OpenTelemetry.Core/Diagnostics/DeferredLogger.cs @@ -0,0 +1,109 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using Elastic.OpenTelemetry.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Elastic.OpenTelemetry.Diagnostics; + +/// +/// Used to capture log entries before the file logger is initialized. +/// +/// +/// Log entries are stored in a concurrent queue and drained to the first file logger that is available. +/// Log entries are only captured if file logging is enabled in the options. +/// +internal sealed class DeferredLogger : ILogger +{ + private readonly bool _isEnabled = false; + private readonly LogLevel _configuredLogLevel; + private readonly ConcurrentQueue _logQueue = new(); + private readonly ILogger? _additionalLogger; + + private static readonly Lock Lock = new(); + + /// + /// Create an instance of . + /// + /// The options used to configure the logger. + /// An optional additional logger to which log entries will be written. For example, + /// this may be available in ASP.NET Core scenarios and ensures the early log entries are sent to other sinks, such + /// as the console, when available. + private DeferredLogger(CompositeElasticOpenTelemetryOptions options, ILogger? additionalLogger = null) + { + _isEnabled = options.GlobalLogEnabled && options.LogTargets.HasFlag(LogTargets.File); + + if (!_isEnabled) + return; + + _configuredLogLevel = options.LogLevel; + _additionalLogger = additionalLogger; + } + + public IDisposable BeginScope(TState state) where TState : notnull => NullScope.Instance; + + public bool IsEnabled(LogLevel logLevel) => _isEnabled && _configuredLogLevel <= logLevel; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + _additionalLogger?.Log(logLevel, eventId, state, exception, formatter); + + if (!IsEnabled(logLevel)) + return; + + var logLine = LogFormatter.Format(logLevel, eventId, state, exception, formatter); + + if (exception is not null) + logLine = $"{logLine}{Environment.NewLine}{exception}"; + + _logQueue.Enqueue(logLine); + } + + internal void DrainAndRelease(StreamWriter streamWriter) + { + using (Lock.EnterScope()) + { + while (_logQueue.TryDequeue(out var deferredLog)) + { + streamWriter.WriteLine(deferredLog); + } + + Instance = null; + } + } + + private static DeferredLogger? Instance; + + internal static ILogger GetOrCreate(CompositeElasticOpenTelemetryOptions options) + { + using (Lock.EnterScope()) + { + // We want, at most, one instance of DeferredLogger for the application. + // We only create a DeferredFileLogger if file logging is enabled + if (options.GlobalLogEnabled && options.LogTargets.HasFlag(LogTargets.File)) + return Instance ??= new DeferredLogger(options, options.AdditionalLogger); + + return NullLogger.Instance; + } + } + + internal static bool TryGetInstance([NotNullWhen(true)] out DeferredLogger? instance) + { + instance = Instance; + return instance is not null; + } + + private class NullScope : IDisposable + { + public static readonly NullScope Instance = new(); + + public void Dispose() + { + // No-op + } + } +} diff --git a/src/Elastic.OpenTelemetry.Core/Diagnostics/FileLogger.cs b/src/Elastic.OpenTelemetry.Core/Diagnostics/FileLogger.cs index dce157c7..77899bd3 100644 --- a/src/Elastic.OpenTelemetry.Core/Diagnostics/FileLogger.cs +++ b/src/Elastic.OpenTelemetry.Core/Diagnostics/FileLogger.cs @@ -17,13 +17,12 @@ internal sealed class FileLogger : IDisposable, IAsyncDisposable, ILogger private readonly CancellationTokenSource _cancellationTokenSource = new(); private readonly StreamWriter _streamWriter; private readonly LogLevel _configuredLogLevel; + private readonly LoggerExternalScopeProvider _scopeProvider; private bool _disposed; public bool FileLoggingEnabled { get; } - private readonly LoggerExternalScopeProvider _scopeProvider; - public FileLogger(CompositeElasticOpenTelemetryOptions options) { _scopeProvider = new LoggerExternalScopeProvider(); @@ -40,8 +39,8 @@ public FileLogger(CompositeElasticOpenTelemetryOptions options) { var process = Process.GetCurrentProcess(); - // When ordered by filename, we see logs from the same process grouped, then ordered by oldest to newest, then the PID for that instance - var logFileName = $"EDOT.{process.ProcessName}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}_{process.Id}.log"; + // This naming resembles the naming structure for OpenTelemetry log files. + var logFileName = $"edot-dotnet-{process.Id}-{process.ProcessName}-{DateTimeOffset.UtcNow:yyyyMMdd-hhMMssfffZ}.log"; var logDirectory = options.LogDirectory; LogFilePath = Path.Combine(logDirectory, logFileName); @@ -50,13 +49,20 @@ public FileLogger(CompositeElasticOpenTelemetryOptions options) Directory.CreateDirectory(logDirectory); // StreamWriter.Dispose disposes underlying stream too. - var stream = new FileStream(LogFilePath, FileMode.OpenOrCreate, FileAccess.Write); + var stream = new FileStream(LogFilePath, FileMode.Append, FileAccess.Write, FileShare.Read); _streamWriter = new StreamWriter(stream, Encoding.UTF8); _streamWriter.WriteLine("DateTime (UTC) Thread SpanId Level Message"); _streamWriter.WriteLine(); + // Drain any deferred log entries captured before the file logger was initialized. + // These appear before the preamble to ensure correct timestamping order. + if (DeferredLogger.TryGetInstance(out var deferredLogger)) + { + deferredLogger.DrainAndRelease(_streamWriter); + } + WritingTask = Task.Run(async () => { var cancellationToken = _cancellationTokenSource.Token; diff --git a/src/Elastic.OpenTelemetry.Core/Diagnostics/LoggerMessages.cs b/src/Elastic.OpenTelemetry.Core/Diagnostics/LoggerMessages.cs index f3139c1c..7ecfda58 100644 --- a/src/Elastic.OpenTelemetry.Core/Diagnostics/LoggerMessages.cs +++ b/src/Elastic.OpenTelemetry.Core/Diagnostics/LoggerMessages.cs @@ -7,6 +7,9 @@ using Elastic.OpenTelemetry.Configuration; using Elastic.OpenTelemetry.Core; using Microsoft.Extensions.Logging; +#if NET || NETSTANDARD2_1 +using System.Runtime.CompilerServices; +#endif namespace Elastic.OpenTelemetry.Diagnostics; @@ -54,22 +57,26 @@ internal static partial class LoggerMessages "times across all {Target} instances.")] public static partial void LogWithElasticDefaultsCallCount(this ILogger logger, int callCount, string target); + [LoggerMessage(EventId = 12, EventName = "ConfiguredOtlpExporterOptions", Level = LogLevel.Debug, Message = "The `OtlpExporterOptions` have been configured to use the" + + "`ElasticUserAgentHandler` to set the EDOT .NET user agent.")] + public static partial void LogConfiguredOtlpExporterOptions(this ILogger logger); + [LoggerMessage(EventId = 20, EventName = "ConfiguredSignalProvider", Level = LogLevel.Debug, Message = "Configured EDOT defaults for {Signal} via the {ProviderBuilderType} (instance:{BuilderInstanceId}).")] public static partial void LogConfiguredSignalProvider(this ILogger logger, string signal, string providerBuilderType, string builderInstanceId); - [LoggerMessage(EventId = 21, EventName = "SkippingOtlpExporter", Level = LogLevel.Information, Message = "Skipping OTLP exporter for {Signal} based on the provided `ElasticOpenTelemetryOptions` " + + [LoggerMessage(EventId = 21, EventName = "SkippedOtlpExporter", Level = LogLevel.Information, Message = "Skipped OTLP exporter for {Signal} based on the provided `ElasticOpenTelemetryOptions` " + "via the {ProviderBuilderType} (instance:{BuilderInstanceId}).")] - public static partial void LogSkippingOtlpExporter(this ILogger logger, string signal, string providerBuilderType, string builderInstanceId); + public static partial void LogSkippedOtlpExporter(this ILogger logger, string signal, string providerBuilderType, string builderInstanceId); - [LoggerMessage(EventId = 22, EventName = "ProviderBuilderSignalDisabled", Level = LogLevel.Information, Message = "Skipping configuring and setting EDOT defaults for {Signal} on {ProviderBuilderType}" + + [LoggerMessage(EventId = 22, EventName = "ProviderBuilderSignalDisabled", Level = LogLevel.Information, Message = "Skipped configuring and setting EDOT defaults for {Signal} on {ProviderBuilderType}" + "(instance:{BuilderInstanceId}), as these have been disabled via configuration.")] public static partial void LogSignalDisabled(this ILogger logger, string signal, string providerBuilderType, string builderInstanceId); [LoggerMessage(EventId = 23, EventName = "SkippingBootstrap", Level = LogLevel.Warning, Message = "Skipping EDOT bootstrap and provider configuration because the `Signals` configuration is set to `None`. " + - "This likely represents a misconfiguration. If you do not want to use the EDOT for any signals, avoid calling `WithElasticDefaults` on the builder.")] + "This likely represents a misconfiguration. If you do not want to use the EDOT for any signals, avoid calling `AddElasticOpenTelemetry` or `WithElasticDefaults` on the builder.")] public static partial void LogSkippingBootstrapWarning(this ILogger logger); [LoggerMessage(EventId = 24, EventName = "BuilderAlreadyConfigured", Level = LogLevel.Debug, Message = "The {ProviderBuilderType} (instance:{BuilderInstanceId}) has already been configured with EDOT defaults.")] @@ -78,6 +85,16 @@ internal static partial class LoggerMessages [LoggerMessage(EventId = 25, EventName = "ConfiguringBuilder", Level = LogLevel.Information, Message = "Configuring the {ProviderBuilderType} (instance:{BuilderInstanceId}) with EDOT defaults.")] public static partial void LogConfiguringBuilder(this ILogger logger, string providerBuilderType, string builderInstanceId); + [LoggerMessage(EventId = 26, EventName = "AddedOtlpExporter", Level = LogLevel.Debug, Message = "Added OTLP exporter for {Signal} for {ProviderBuilderType} (instance:{BuilderInstanceId}).")] + public static partial void LogAddedOtlpExporter(this ILogger logger, string signal, string providerBuilderType, string builderInstanceId); + + [LoggerMessage(EventId = 27, EventName = "InvokedConfigureAction", Level = LogLevel.Debug, Message = "Invoked user-provided builder configuration action on" + + " the {ProviderBuilderType} (instance:{BuilderInstanceId}).")] + public static partial void LogInvokedConfigureAction(this ILogger logger, string providerBuilderType, string builderInstanceId); + + [LoggerMessage(EventId = 28, EventName = "DeferredOtlpExporter", Level = LogLevel.Debug, Message = "Deferred adding OTLP exporter on {ProviderBuilderType} (instance:{BuilderInstanceId}).")] + public static partial void LogDeferredOtlpExporter(this ILogger logger, string providerBuilderType, string builderInstanceId); + [LoggerMessage(EventId = 30, EventName = "LocatedInstrumentationAssembly", Level = LogLevel.Debug, Message = "Located {AssemblyFilename} in {Path}.")] @@ -208,6 +225,11 @@ public static void LogDistroPreamble(this ILogger logger, SdkActivationMethod ac logger.LogDebug("Processor count: {ProcessorCount}", Environment.ProcessorCount); logger.LogDebug("OS version: {OSVersion}", Environment.OSVersion); logger.LogDebug("CLR version: {CLRVersion}", Environment.Version); +#if NETFRAMEWORK || NETSTANDARD2_0 + logger.LogDebug("Dynamic code supported: {IsDynamicCodeSupported}", true); +#else + logger.LogDebug("Dynamic code supported: {IsDynamicCodeSupported}", RuntimeFeature.IsDynamicCodeSupported); +#endif string[] environmentVariables = [ diff --git a/src/Elastic.OpenTelemetry.Core/Diagnostics/LoggingEventListener.cs b/src/Elastic.OpenTelemetry.Core/Diagnostics/LoggingEventListener.cs index 93bccdbe..0dfdb965 100644 --- a/src/Elastic.OpenTelemetry.Core/Diagnostics/LoggingEventListener.cs +++ b/src/Elastic.OpenTelemetry.Core/Diagnostics/LoggingEventListener.cs @@ -6,7 +6,6 @@ using System.Text; using System.Text.RegularExpressions; using Elastic.OpenTelemetry.Configuration; -using Elastic.OpenTelemetry.Diagnostics; using Microsoft.Extensions.Logging; namespace Elastic.OpenTelemetry.Diagnostics; diff --git a/src/Elastic.OpenTelemetry.Core/ElasticOpenTelemetry.cs b/src/Elastic.OpenTelemetry.Core/ElasticOpenTelemetry.cs index bb3390c6..4904d22e 100644 --- a/src/Elastic.OpenTelemetry.Core/ElasticOpenTelemetry.cs +++ b/src/Elastic.OpenTelemetry.Core/ElasticOpenTelemetry.cs @@ -24,6 +24,11 @@ internal static class ElasticOpenTelemetry internal static ElasticOpenTelemetryComponents Bootstrap(SdkActivationMethod activationMethod) => Bootstrap(activationMethod, CompositeElasticOpenTelemetryOptions.DefaultOptions, null); + /// + /// This checks for any existing components on the IServiceCollection and reuse them if found. + /// It also attempts to used a shared components instance if available and suitable. + /// If neither are available, it will create a new instance. + /// internal static ElasticOpenTelemetryComponents Bootstrap(CompositeElasticOpenTelemetryOptions options, IServiceCollection? services) => Bootstrap(SdkActivationMethod.NuGet, options, services); diff --git a/src/Elastic.OpenTelemetry.Core/Extensions/OpenTelemetryLoggerOptionsExtensions.cs b/src/Elastic.OpenTelemetry.Core/Extensions/OpenTelemetryLoggerOptionsExtensions.cs index fc37b587..7ae9b496 100644 --- a/src/Elastic.OpenTelemetry.Core/Extensions/OpenTelemetryLoggerOptionsExtensions.cs +++ b/src/Elastic.OpenTelemetry.Core/Extensions/OpenTelemetryLoggerOptionsExtensions.cs @@ -45,6 +45,9 @@ public static void WithElasticDefaults(this OpenTelemetryLoggerOptions options, // - https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/39304 // options.IncludeScopes = true; + // TODO - Verify if we can configure the OTLP exporter to add the user agent header. + // See: https://github.com/elastic/elastic-otel-dotnet/issues/338 + logger.LogConfiguredSignalProvider(nameof(Signals.Logs), nameof(OpenTelemetryLoggerOptions), ""); } } diff --git a/src/Elastic.OpenTelemetry.Core/Extensions/ResourceBuilderExtensions.cs b/src/Elastic.OpenTelemetry.Core/Extensions/ResourceBuilderExtensions.cs index c939051e..c1096c1b 100644 --- a/src/Elastic.OpenTelemetry.Core/Extensions/ResourceBuilderExtensions.cs +++ b/src/Elastic.OpenTelemetry.Core/Extensions/ResourceBuilderExtensions.cs @@ -43,9 +43,13 @@ internal static ResourceBuilder WithElasticDefaultsCore( components.Logger.LogWithElasticDefaultsCallCount(callCount, nameof(ResourceBuilder)); - return SignalBuilder.WithElasticDefaults(builder, components.Options, components, services, - (ResourceBuilder builder, BuilderState builderState, IServiceCollection? services) => + var builderOptions = new BuilderOptions(); + + return SignalBuilder.WithElasticDefaults(builder, components.Options, components, services, builderOptions, + (BuilderContext builderContext) => { + var builderState = builderContext.BuilderState; + var attributes = new Dictionary { { ResourceSemanticConventions.AttributeServiceInstanceId, ApplicationInstanceId }, diff --git a/src/Elastic.OpenTelemetry.Core/Extensions/TracerProviderBuilderExtensions.cs b/src/Elastic.OpenTelemetry.Core/Extensions/TracerProviderBuilderExtensions.cs index 6fd853c5..1d48dd8d 100644 --- a/src/Elastic.OpenTelemetry.Core/Extensions/TracerProviderBuilderExtensions.cs +++ b/src/Elastic.OpenTelemetry.Core/Extensions/TracerProviderBuilderExtensions.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using Elastic.OpenTelemetry; -using Elastic.OpenTelemetry.Configuration; using Elastic.OpenTelemetry.Core; using Elastic.OpenTelemetry.Diagnostics; using Elastic.OpenTelemetry.Resources; @@ -81,6 +80,9 @@ internal static TracerProviderBuilder AddElasticProcessorsCore( ElasticOpenTelemetryComponents? components, IServiceCollection? services) { + // NOTE: This method doesn't currently add any processors and just configures the resource. + // Once we start adding processors, we might need to consider passing in the BuilderOptions + components ??= builderState?.Components; var callCount = Interlocked.Increment(ref AddElasticProcessorsCallCount); @@ -94,14 +96,22 @@ internal static TracerProviderBuilder AddElasticProcessorsCore( { // When we have existing builderState, this method is being invoked from the main WithElasticDefaults method. // In that scenario, we skip configuring the resource, as it will have already been configured by the caller. + // We don't log here, as the main WithElasticDefaults method will log the complete set of processors added. return builder; } - return SignalBuilder.WithElasticDefaults(builder, Signals.Traces, components?.Options, components, null, ConfigureBuilder); + // This is sufficient as the configure method doesn't register anything but the resource, for now. + var builderOptions = new BuilderOptions(); + + return SignalBuilder.WithElasticDefaults(builder, components?.Options, components, services, builderOptions, ConfigureBuilder); [MethodImpl(MethodImplOptions.AggressiveInlining)] - static void ConfigureBuilder(TracerProviderBuilder builder, BuilderState builderState, IServiceCollection? services) + static void ConfigureBuilder(BuilderContext builderContext) { + var builder = builderContext.Builder; + var builderState = builderContext.BuilderState; + var services = builderContext.Services; + builder.ConfigureResource(r => r.WithElasticDefaultsCore(builderState, services, null)); } } @@ -109,6 +119,8 @@ static void ConfigureBuilder(TracerProviderBuilder builder, BuilderState builder [MethodImpl(MethodImplOptions.AggressiveInlining)] private static TracerProviderBuilder LogAndAddProcessor(this TracerProviderBuilder builder, BaseProcessor processor, BuilderState builderState) { + // NOTE: This is not currently used but here for when we do have processors to register for any feature gaps we close. + builder.AddProcessor(processor); builderState.Components.Logger.LogProcessorAdded(processor.GetType().ToString(), builderState.InstanceIdentifier); return builder; diff --git a/src/Elastic.OpenTelemetry.Core/Processors/SpanCompressionProcessor.cs b/src/Elastic.OpenTelemetry.Core/Processors/SpanCompressionProcessor.cs index 91d5ea8a..a320e8c3 100644 --- a/src/Elastic.OpenTelemetry.Core/Processors/SpanCompressionProcessor.cs +++ b/src/Elastic.OpenTelemetry.Core/Processors/SpanCompressionProcessor.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using Elastic.OpenTelemetry.Core; -using Elastic.OpenTelemetry.Processors; using OpenTelemetry; #pragma warning disable IDE0130 // Namespace does not match folder structure diff --git a/src/Elastic.OpenTelemetry.Core/SignalBuilder.cs b/src/Elastic.OpenTelemetry.Core/SignalBuilder.cs index 5aec7111..502510d0 100644 --- a/src/Elastic.OpenTelemetry.Core/SignalBuilder.cs +++ b/src/Elastic.OpenTelemetry.Core/SignalBuilder.cs @@ -10,6 +10,11 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using OpenTelemetry; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; namespace Elastic.OpenTelemetry.Core; @@ -17,6 +22,18 @@ namespace Elastic.OpenTelemetry.Core; /// Provides static helper methods which centralise provider builder logic used when registering EDOT /// defaults on the various builders. /// +/// +/// This class is used internally by the various builder extension methods to centralise common logic. +///
+/// It is currently used when registering EDOT .NET defaults on the following builders: +/// +/// +/// +/// +/// +/// +/// +///
internal static class SignalBuilder { #pragma warning disable IDE0028 // Simplify collection initialization @@ -28,15 +45,28 @@ internal static class SignalBuilder /// /// Returns the most relevant for builder extension methods to use. /// - public static ILogger GetLogger( + internal static ILogger GetLogger( ElasticOpenTelemetryComponents? components, - CompositeElasticOpenTelemetryOptions? options) => - components?.Logger ?? options?.AdditionalLogger ?? NullLogger.Instance; + CompositeElasticOpenTelemetryOptions? options) + { + if (components?.Logger is not null) + return components.Logger; + + if (options is not null) + { + var deferredLogger = DeferredLogger.GetOrCreate(options); + + if (deferredLogger is NullLogger && options.AdditionalLogger is not null) + return options.AdditionalLogger; + } + + return NullLogger.Instance; + } /// /// Returns the most relevant for builder extension methods to use. /// - public static ILogger GetLogger( + internal static ILogger GetLogger( T builder, ElasticOpenTelemetryComponents? components, CompositeElasticOpenTelemetryOptions? options, @@ -45,123 +75,127 @@ public static ILogger GetLogger( if (builderState is not null) return builderState.Components.Logger; - var logger = components?.Logger ?? options?.AdditionalLogger ?? NullLogger.Instance; - if (BuilderStateTable.TryGetValue(builder, out builderState)) - logger = builderState.Components.Logger; + return builderState.Components.Logger; - return logger; + return GetLogger(components, options); } - public static T WithElasticDefaults( + /// + /// This overload is needed to handle scenarios where we don't yet have a builder state and context. + /// + internal static T WithElasticDefaults( T builder, - Signals signal, CompositeElasticOpenTelemetryOptions? options, ElasticOpenTelemetryComponents? components, IServiceCollection? services, - Action configure) where T : class + in BuilderOptions builderOptions, + Action> configureBuilder) where T : class { - var providerBuilderName = builder.GetType().Name; - var logger = GetLogger(components, options); + // FullName may return null so we fallback to Name when required. + var providerBuilderName = builder.GetType().FullName ?? builder.GetType().Name; - BuilderState? builderState = null; + if (BuilderStateTable.TryGetValue(builder, out var existingBuilderState)) + { + return HandleExistingBuilderState(builder, providerBuilderName, existingBuilderState); + } + + var logger = GetLogger(components, options); + var builderInstanceId = Guid.NewGuid().ToString(); // Used in logging to track duplicate calls to the same builder - try + // When dealing with a signal specific builder, we need to check if that signal is enabled. + // If not, we can log and return early. + if (builder is not IOpenTelemetryBuilder && builder is not ResourceBuilder) { - var builderInstanceId = ""; + var configuredSignals = components?.Options.Signals ?? options?.Signals ?? Signals.All; - if (BuilderStateTable.TryGetValue(builder, out var existingBuilderState)) + var signal = Signals.None; + switch (builder) { - builderState = existingBuilderState; - builderInstanceId = existingBuilderState.InstanceIdentifier; + case TracerProviderBuilder: + signal = Signals.Traces; + break; + case MeterProviderBuilder: + signal = Signals.Metrics; + break; + case LoggerProviderBuilder: + signal = Signals.Logs; + break; } - if (builderState is not null) - logger = builderState.Components.Logger; - - // If the signal is disabled via configuration we skip any potential bootstrapping. - var configuredSignals = components?.Options.Signals ?? options?.Signals ?? Signals.All; - if (!configuredSignals.HasFlagFast(signal)) + if (configuredSignals.HasFlagFast(signal) is false) { logger.LogSignalDisabled(signal.ToString().ToLower(), providerBuilderName, builderInstanceId); return builder; } - - return WithElasticDefaults(builder, options, components, services, configure); } - catch (Exception ex) - { - var signalNameForLogging = signal.ToStringFast().ToLowerInvariant(); - logger.LogError(new EventId(501, "BuilderDefaultsFailed"), ex, "Failed to fully register EDOT .NET " + - "{Signal} defaults on the {ProviderBuilderName}.", signalNameForLogging, providerBuilderName); - } - - return builder; - } - - public static T WithElasticDefaults( - T builder, - CompositeElasticOpenTelemetryOptions? options, - ElasticOpenTelemetryComponents? components, - IServiceCollection? services, - Action configure) where T : class - { - var providerBuilderName = builder.GetType().Name; - var logger = GetLogger(components, options); - try + // This should not be a hot path, so locking here is reasonable. + using (var scope = Lock.EnterScope()) { - if (BuilderStateTable.TryGetValue(builder, out var builderState)) + // Double check after acquiring the lock. + if (BuilderStateTable.TryGetValue(builder, out existingBuilderState)) { - builderState.Components.Logger.LogBuilderAlreadyConfigured(providerBuilderName, builderState.InstanceIdentifier); + return HandleExistingBuilderState(builder, providerBuilderName, existingBuilderState); + } - // This allows us to track the number of times a specific instance of a builder is configured. - // We expect each builder to be configured at most once and log a warning if multiple invocations - // are detected. - builderState.IncrementWithElasticDefaults(); + // We can't log to the file here as we don't yet have any bootstrapped components. + // Therefore, this message will only appear if the consumer provides an additional logger. + // This is fine as it's a trace level message for advanced debugging. + logger.LogNoExistingComponents(providerBuilderName, builderInstanceId); - if (builderState.WithElasticDefaultsCounter > 1) - builderState.Components.Logger.LogWarning("The `WithElasticDefaults` method has been called {WithElasticDefaultsCount} " + - "times on the same `{BuilderType}` (instance: {BuilderInstanceId}). This method is " + - "expected to be invoked a maximum of one time.", builderState.WithElasticDefaultsCounter, providerBuilderName, - builderState.InstanceIdentifier); + options ??= CompositeElasticOpenTelemetryOptions.DefaultOptions; - return builder; - } + // This will check for any existing components on the IServiceCollection and reuse them if found. + // It would also attempt to used a shared components instance if available. + // If neither are available, it will create a new instance. + components = ElasticOpenTelemetry.Bootstrap(options, services); + var builderState = new BuilderState(components, builderInstanceId); - // This should not be a hot path, so locking here is reasonable. - using (var scope = Lock.EnterScope()) + var builderContext = new BuilderContext { - var instanceId = Guid.NewGuid().ToString(); // Used in logging to track duplicate calls to the same builder + Builder = builder, + BuilderState = builderState, + BuilderOptions = builderOptions, + Services = services + }; - // We can't log to the file here as we don't yet have any bootstrapped components. - // Therefore, this message will only appear if the consumer provides an additional logger. - // This is fine as it's a trace level message for advanced debugging. - logger.LogNoExistingComponents(providerBuilderName, instanceId); + configureBuilder(builderContext); - options ??= CompositeElasticOpenTelemetryOptions.DefaultOptions; - components = ElasticOpenTelemetry.Bootstrap(options, services); - builderState = new BuilderState(components, instanceId); + components.Logger.LogStoringBuilderState(providerBuilderName, builderInstanceId); + BuilderStateTable.Add(builder, builderState); + } - configure(builder, builderState, services); + return builder; - components.Logger.LogStoringBuilderState(providerBuilderName, instanceId); - BuilderStateTable.Add(builder, builderState); - } - } - catch (Exception ex) + static T HandleExistingBuilderState( + T builder, + string providerBuilderName, + BuilderState builderState) { - logger.LogError(new EventId(502, "BuilderDefaultsFailed"), ex, "Failed to fully register EDOT .NET " + - "defaults on the {ProviderBuilderName}.", builder.GetType().Name); + var logger = builderState.Components.Logger; + logger.LogBuilderAlreadyConfigured(providerBuilderName, builderState.InstanceIdentifier); + + // This allows us to track the number of times a specific instance of a builder is configured. + // We expect each builder to be configured at most once and log a warning if multiple invocations + // are detected. + builderState.IncrementWithElasticDefaults(); + + if (builderState.WithElasticDefaultsCounter > 1) + // NOTE: Log using the logger from the existing builder state. + builderState.Components.Logger.LogWarning("The `WithElasticDefaults` method has been called {WithElasticDefaultsCount} " + + "times on the same `{BuilderType}` (instance: {BuilderInstanceId}). This method is " + + "expected to be invoked a maximum of one time.", builderState.WithElasticDefaultsCounter, providerBuilderName, + builderState.InstanceIdentifier); + + return builder; } - - return builder; } /// /// Identifies whether a specific instrumentation assembly is present alongside the executing application. /// - public static bool InstrumentationAssemblyExists(string assemblyName) + internal static bool InstrumentationAssemblyExists(string assemblyName) { var assemblyLocation = Path.GetDirectoryName(AppContext.BaseDirectory); @@ -171,8 +205,8 @@ public static bool InstrumentationAssemblyExists(string assemblyName) return File.Exists(Path.Combine(assemblyLocation, assemblyName)); } - [RequiresUnreferencedCode("Accesses assemblies and methods dynamically using refelction. This is by design and cannot be made trim compatible.")] - public static void AddInstrumentationViaReflection(T builder, ElasticOpenTelemetryComponents components, ReadOnlySpan assemblyInfos, string builderInstanceId) + [RequiresUnreferencedCode("Accesses assemblies and methods dynamically using reflection. This is by design and cannot be made trim compatible.")] + internal static void AddInstrumentationViaReflection(T builder, ElasticOpenTelemetryComponents components, ReadOnlySpan assemblyInfos, string builderInstanceId) where T : class { if (components.Options.SkipInstrumentationAssemblyScanning) @@ -184,8 +218,8 @@ public static void AddInstrumentationViaReflection(T builder, ElasticOpenTele AddInstrumentationViaReflection(builder, components.Logger, assemblyInfos, builderInstanceId); } - [RequiresUnreferencedCode("Accesses assemblies and methods dynamically using refelction. This is by design and cannot be made trim compatible.")] - public static void AddInstrumentationViaReflection(T builder, ILogger logger, ReadOnlySpan assemblyInfos, string builderInstanceId) + [RequiresUnreferencedCode("Accesses assemblies and methods dynamically using reflection. This is by design and cannot be made trim compatible.")] + internal static void AddInstrumentationViaReflection(T builder, ILogger logger, ReadOnlySpan assemblyInfos, string builderInstanceId) where T : class { var builderTypeName = builder.GetType().Name; diff --git a/src/Elastic.OpenTelemetry/Extensions/HostApplicationBuilderExtensions.cs b/src/Elastic.OpenTelemetry/Extensions/HostApplicationBuilderExtensions.cs index 1af0b26f..a4fcff4f 100644 --- a/src/Elastic.OpenTelemetry/Extensions/HostApplicationBuilderExtensions.cs +++ b/src/Elastic.OpenTelemetry/Extensions/HostApplicationBuilderExtensions.cs @@ -3,10 +3,14 @@ // See the LICENSE file in the project root for more information using Elastic.OpenTelemetry; +using Elastic.OpenTelemetry.Core; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using OpenTelemetry; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; #pragma warning disable IDE0130 // Namespace does not match folder structure namespace Microsoft.Extensions.Hosting; @@ -14,13 +18,36 @@ namespace Microsoft.Extensions.Hosting; /// /// Elastic extension methods for used to register -/// the Elastic Distribution of OpenTelemetry (EDOT) .NET defaults. +/// the Elastic Distribution of OpenTelemetry (EDOT) .NET defaults. /// public static class HostApplicationBuilderExtensions { /// - /// Registers the OpenTelemetry SDK with the application, configured with Elastic Distribution of OpenTelemetry (EDOT) .NET defaults. + /// Registers the OpenTelemetry SDK with the application, configured with Elastic Distribution of OpenTelemetry (EDOT) .NET + /// defaults for traces, + /// metrics and logs. /// + /// + /// + /// For applications built on the Generic Host (i.e. using Host.CreateDefaultBuilder), this is the prefered way + /// to register the OpenTelemetry SDK with EDOT .NET defaults. It enables EDOT .NET defaults for all signals, requiring + /// minimal code. + /// + /// + /// When using AddElasticOpenTelemetry, you may need to further customize the OpenTelemetry SDK configuration + /// such as when you need to add additional sources, processors or instrumentations. You can do this by calling the overload + /// accepting a configuration action . + /// + /// + /// Avoid combining this method with calls to WithElasticDefaults, WithElasticMetrics, WithElasticLogs or WithElasticTraces + /// extension methods on the , , or + /// directly as this may lead to unexpected results. + /// + /// + /// Alternatively, the option + /// can be used to prevent automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// /// The for the application being configured. /// Thrown when the is null. /// The supplied for chaining calls. @@ -42,93 +69,157 @@ public static IHostApplicationBuilder AddElasticOpenTelemetry(this IHostApplicat /// /// /// - /// Configuration is first bound from and then overridden by any options configured on - /// the provided . + /// + /// For applications built on the Generic Host (i.e. using Host.CreateDefaultBuilder), this is the prefered way + /// to register the OpenTelemetry SDK with EDOT .NET defaults. It enables EDOT .NET defaults for all signals, requiring + /// minimal code. + /// + /// + /// Avoid combining this method with calls to WithElasticDefaults, WithElasticMetrics, WithElasticLogs or WithElasticTraces + /// extension methods on the , , or + /// directly as this may lead to unexpected results. + /// + /// + /// Alternatively, the option + /// can be used to prevent automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// /// /// - /// used to configure the Elastic Distribution of OpenTelemetry (EDOT) .NET. + /// A configuration used to further customise + /// the OpenTelemetry SDK after Elastic Distribution of OpenTelemetry (EDOT) .NET defaults have been applied and before the OTLP + /// exporter is added. /// Thrown when the is null. - /// Thrown when the is null. + /// Thrown when the is null. /// - public static IHostApplicationBuilder AddElasticOpenTelemetry(this IHostApplicationBuilder builder, ElasticOpenTelemetryOptions options) + public static IHostApplicationBuilder AddElasticOpenTelemetry(this IHostApplicationBuilder builder, Action configure) { + // TODO - Breaking change: In a future major release, rename this parameter to 'configureBuilder' for clarity and consistency. + // This would be a source breaking change only but we'll reserve it for a major version to avoid disrupting consumers. #if NET - ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configure); #else if (builder is null) throw new ArgumentNullException(nameof(builder)); - if (options is null) - throw new ArgumentNullException(nameof(options)); + if (configure is null) + throw new ArgumentNullException(nameof(configure)); #endif - builder.Services.AddElasticOpenTelemetry(builder.Configuration, options); + var builderOptions = new BuilderOptions + { + UserProvidedConfigureBuilder = configure, + // We don't set defer as we expect the callee to handle executing the configure action at the correct time. + DeferAddOtlpExporter = false + }; + builder.Services.AddElasticOpenTelemetryCore(new(builder.Configuration), builderOptions); return builder; } /// /// /// + /// + /// + /// Configuration is first bound from and then overridden by any options configured on + /// the provided . + /// + /// + /// For applications built on the Generic Host (i.e. using Host.CreateDefaultBuilder), this is the prefered way + /// to register the OpenTelemetry SDK with EDOT .NET defaults. It enables EDOT .NET defaults for all signals, requiring + /// minimal code. + /// + /// + /// When using AddElasticOpenTelemetry, you may need to further customize the OpenTelemetry SDK configuration + /// such as when you need to add additional sources, processors or instrumentations. You can do this by calling the overload + /// accepting a configuration action . + /// + /// + /// Avoid combining this method with calls to WithElasticDefaults, WithElasticMetrics, WithElasticLogs or WithElasticTraces + /// extension methods on the , , or + /// directly as this may lead to unexpected results. + /// + /// + /// Alternatively, the option + /// can be used to prevent automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// /// /// used to configure the Elastic Distribution of OpenTelemetry (EDOT) .NET. - /// configuration action. /// Thrown when the is null. /// Thrown when the is null. - /// Thrown when the is null. /// - public static IHostApplicationBuilder AddElasticOpenTelemetry(this IHostApplicationBuilder builder, ElasticOpenTelemetryOptions options, Action configure) + public static IHostApplicationBuilder AddElasticOpenTelemetry(this IHostApplicationBuilder builder, ElasticOpenTelemetryOptions options) { #if NET - ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(options); - ArgumentNullException.ThrowIfNull(configure); #else if (builder is null) throw new ArgumentNullException(nameof(builder)); if (options is null) throw new ArgumentNullException(nameof(options)); - - if (configure is null) - throw new ArgumentNullException(nameof(configure)); #endif - var otelBuilder = builder.Services - .AddElasticOpenTelemetry(builder.Configuration, options); - - configure.Invoke(otelBuilder); - + builder.Services.AddElasticOpenTelemetryCore(new(builder.Configuration, options), default); return builder; } /// /// /// + /// + /// + /// For applications built on the Generic Host (i.e. using Host.CreateDefaultBuilder), this is the prefered way + /// to register the OpenTelemetry SDK with EDOT .NET defaults. It enables EDOT .NET defaults for all signals, requiring + /// minimal code. + /// + /// + /// Avoid combining this method with calls to WithElasticDefaults, WithElasticMetrics, WithElasticLogs or WithElasticTraces + /// extension methods on the , , or + /// directly as this may lead to unexpected results. + /// + /// + /// Alternatively, the option + /// can be used to prevent automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// /// - /// configuration action. + /// + /// /// Thrown when the is null. + /// Thrown when the is null. /// Thrown when the is null. /// - public static IHostApplicationBuilder AddElasticOpenTelemetry(this IHostApplicationBuilder builder, Action configure) + public static IHostApplicationBuilder AddElasticOpenTelemetry(this IHostApplicationBuilder builder, + ElasticOpenTelemetryOptions options, Action configure) { + // TODO - Breaking change: In a future major release, rename this parameter to 'configureBuilder' for clarity and consistency. + // This would be a source breaking change only but we'll reserve it for a major version to avoid disrupting consumers. #if NET ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(configure); #else if (builder is null) throw new ArgumentNullException(nameof(builder)); + if (options is null) + throw new ArgumentNullException(nameof(options)); + if (configure is null) throw new ArgumentNullException(nameof(configure)); #endif - - var otelBuilder = builder.Services - .AddElasticOpenTelemetry(builder.Configuration); - - configure.Invoke(otelBuilder); - + var builderOptions = new BuilderOptions + { + UserProvidedConfigureBuilder = configure, + // We don't set defer as we expect the callee to handle executing the configure action at the correct time. + DeferAddOtlpExporter = false + }; + + builder.Services.AddElasticOpenTelemetryCore(new(builder.Configuration, options), builderOptions); return builder; } } diff --git a/src/Elastic.OpenTelemetry/Extensions/LoggerProviderBuilderExtensions.cs b/src/Elastic.OpenTelemetry/Extensions/LoggerProviderBuilderExtensions.cs new file mode 100644 index 00000000..5cfae1aa --- /dev/null +++ b/src/Elastic.OpenTelemetry/Extensions/LoggerProviderBuilderExtensions.cs @@ -0,0 +1,439 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Runtime.CompilerServices; +using Elastic.OpenTelemetry; +using Elastic.OpenTelemetry.Configuration; +using Elastic.OpenTelemetry.Core; +using Elastic.OpenTelemetry.Diagnostics; +using Elastic.OpenTelemetry.Exporters; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using OpenTelemetry.Exporter; +using OpenTelemetry.Logs; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +// Matching namespace with LoggerProviderBuilder +#pragma warning disable IDE0130 // Namespace does not match folder structure +namespace OpenTelemetry; +#pragma warning restore IDE0130 // Namespace does not match folder structure + +/// +/// Provides extension methods on the used to register +/// the Elastic Distribution of OpenTelemetry (EDOT) .NET defaults +/// for logging. +/// +public static class LoggerProviderBuilderExtensions +{ + /// + /// Used to track the number of times any variation of `WithElasticDefaults` is invoked by consuming + /// code across all instances. This allows us to warn about potential + /// misconfigurations. + /// + private static int WithElasticDefaultsCallCount; + + /// + /// Apply Elastic Distribution of OpenTelemetry (EDOT) .NET + /// defaults + /// to the . + /// + /// + /// + /// This method is intended for advanced scenarios where per signal Elastic Distribution of OpenTelemetry (EDOT) .NET + /// defaults are being enabled. In most scenarios, prefer the AddElasticOpenTelemetry method on + /// or . + /// + /// + /// Warning: Avoid calling this method when using the AddElasticOpenTelemetry method on + /// or . In those scenarios, the OpenTelemetry SDK will have already been configured with + /// defaults, including the OTLP exporter. Calling this method again may lead to unexpected behavior. + /// + /// + /// When using AddElasticOpenTelemetry, use the overload on the + /// or which accepts a configuration + /// to further customize the SDK setup. For example, + /// This ensures that your configuration is invoked after EDOT .NET defaults have been applied, but before the OTLP exporter is added. + /// + /// + /// Note: If you wish to customise the after the EDOT .NET defaults have been applied, + /// use the overload that accepts a + /// configuration action. + /// + /// + /// The to configure. + /// Thrown when the is null. + /// + /// An instance of that can be used to further configure the logging signal. + /// + /// Warning: Any further configuration applied via the returned will + /// be applied after the OTLP exporter has been added. + /// + /// + /// Recommendation:It is strongly recommended to use the + /// overload instead. This accepts an configuration callback and ensures any + /// customisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the + /// option + /// automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// + public static LoggerProviderBuilder WithElasticDefaults(this LoggerProviderBuilder builder) + { +#if NET + ArgumentNullException.ThrowIfNull(builder); +#else + if (builder is null) + throw new ArgumentNullException(nameof(builder)); +#endif + + return WithElasticDefaultsCore(builder, null, null, null, default); + } + + /// + /// + /// + /// This method is intended for advanced scenarios where per signal Elastic Distribution of OpenTelemetry (EDOT) .NET + /// defaults are being enabled. In most scenarios, prefer the AddElasticOpenTelemetry method on + /// or . + /// + /// + /// Warning: Avoid calling this method when using the AddElasticOpenTelemetry method on + /// or . In those scenarios, the OpenTelemetry SDK will have already been configured with + /// defaults, including the OTLP exporter. Calling this method again may lead to unexpected behavior. + /// + /// + /// When using AddElasticOpenTelemetry, use the overload on the + /// or which accepts a configuration + /// to further customize the SDK setup. For example, + /// This ensures that your configuration is invoked after EDOT .NET defaults have been applied, but before the OTLP exporter is added. + /// + /// + /// Note: If you wish to customise the after the EDOT .NET defaults have been applied, + /// use the action. + /// + /// + /// + /// + /// An used to further configure the . + /// This action is invoked after EDOT .NET defaults + /// have been applied, but before the OTLP exporter is added. This ensures that any custom processors run before the exporter. + /// + /// Thrown when the is null. + /// Thrown when the is null. + /// + /// + /// An instance of that can be used to further configure the logging signal. + /// + /// + /// Warning: Any further configuration applied via the returned will + /// be applied after the OTLP exporter has been added. + /// + /// + /// Recommendation:It is strongly recommended to use the action to customise + /// the after EDOT .NET defaults are applied and before the OTLP exporter is added. + /// + /// + public static LoggerProviderBuilder WithElasticDefaults(this LoggerProviderBuilder builder, Action configureBuilder) + { +#if NET + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configureBuilder); +#else + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + + if (configureBuilder is null) + throw new ArgumentNullException(nameof(configureBuilder)); +#endif + var builderOptions = new BuilderOptions { UserProvidedConfigureBuilder = configureBuilder }; + return WithElasticDefaultsCore(builder, null, null, null, builderOptions); + } + + /// + /// + /// + /// This method is intended for advanced scenarios where per signal Elastic Distribution of OpenTelemetry (EDOT) .NET + /// defaults are being enabled. In most scenarios, prefer the AddElasticOpenTelemetry method on + /// or . + /// + /// + /// Warning: Avoid calling this method when using the AddElasticOpenTelemetry method on + /// or . In those scenarios, the OpenTelemetry SDK will have already been configured with + /// defaults, including the OTLP exporter. Calling this method again may lead to unexpected behavior. + /// + /// + /// When using AddElasticOpenTelemetry, use the overload on the + /// or which accepts a configuration + /// to further customize the SDK setup. For example, + /// + /// This ensures that your configuration is invoked after EDOT .NET defaults have been applied, but before the OTLP exporter is added. + /// + /// + /// Note: If you wish to customise the after the EDOT .NET defaults have been applied, + /// use the overload that accepts a + /// configuration action. + /// + /// + /// + /// used to configure the Elastic Distribution of OpenTelemetry (EDOT) .NET. + /// Thrown when the is null. + /// Thrown when the is null. + /// + /// An instance of that can be used to further configure the logging signal. + /// + /// Warning: Any further configuration applied via the returned will + /// be applied after the OTLP exporter has been added. + /// + /// + /// Recommendation:It is strongly recommended to use the + /// overload instead. This accepts an configuration callback and ensures any + /// customisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the + /// option + /// automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// + public static LoggerProviderBuilder WithElasticDefaults(this LoggerProviderBuilder builder, ElasticOpenTelemetryOptions options) + { +#if NET + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(options); +#else + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + + if (options is null) + throw new ArgumentNullException(nameof(options)); +#endif + + return WithElasticDefaultsCore(builder, new(options), null, null, default); + } + + /// + /// + /// + /// + /// + /// Thrown when the is null. + /// Thrown when the is null. + /// Thrown when the is null. + /// + public static LoggerProviderBuilder WithElasticDefaults(this LoggerProviderBuilder builder, + ElasticOpenTelemetryOptions options, Action configureBuilder) + { +#if NET + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(configureBuilder); +#else + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + + if (options is null) + throw new ArgumentNullException(nameof(options)); + + if (configureBuilder is null) + throw new ArgumentNullException(nameof(configureBuilder)); +#endif + var builderOptions = new BuilderOptions { UserProvidedConfigureBuilder = configureBuilder }; + return WithElasticDefaultsCore(builder, new(options), null, null, builderOptions); + } + + /// + /// + /// + /// This method is intended for advanced scenarios where per signal Elastic Distribution of OpenTelemetry (EDOT) .NET + /// defaults are being enabled. In most scenarios, prefer the AddElasticOpenTelemetry method on + /// or . + /// + /// + /// Warning: Avoid calling this method when using the AddElasticOpenTelemetry method on + /// or . In those scenarios, the OpenTelemetry SDK will have already been configured with + /// defaults, including the OTLP exporter. Calling this method again may lead to unexpected behavior. + /// + /// + /// When using AddElasticOpenTelemetry, use the overload on the + /// or which accepts a configuration + /// to further customize the SDK setup. For example, + /// This ensures that your configuration is invoked after EDOT .NET defaults have been applied, but before the OTLP exporter is added. + /// + /// + /// Note: If you wish to customise the after the EDOT .NET defaults have been applied, + /// use the overload that accepts a + /// configuration action. + /// + /// + /// + /// An instance from which to load the Elastic Distribution of + /// OpenTelemetry (EDOT) .NET options. + /// Thrown when the is null. + /// Thrown when the is null. + /// + /// An instance of that can be used to further configure the logging signal. + /// + /// Warning: Any further configuration applied via the returned will + /// be applied after the OTLP exporter has been added. + /// + /// + /// Recommendation:It is strongly recommended to use the + /// overload instead. This accepts an configuration callback and ensures any + /// customisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the + /// option + /// automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// + public static LoggerProviderBuilder WithElasticDefaults(this LoggerProviderBuilder builder, IConfiguration configuration) + { +#if NET + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configuration); +#else + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + + if (configuration is null) + throw new ArgumentNullException(nameof(configuration)); +#endif + return WithElasticDefaultsCore(builder, new(configuration), null, null, default); + } + + /// + /// + /// + /// + /// + /// Thrown when the is null. + /// Thrown when the is null. + /// Thrown when the is null. + /// + public static LoggerProviderBuilder WithElasticDefaults(this LoggerProviderBuilder builder, + IConfiguration configuration, Action configureBuilder) + { +#if NET + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configuration); + ArgumentNullException.ThrowIfNull(configureBuilder); +#else + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + + if (configuration is null) + throw new ArgumentNullException(nameof(configuration)); + + if (configureBuilder is null) + throw new ArgumentNullException(nameof(configureBuilder)); +#endif + var builderOptions = new BuilderOptions { UserProvidedConfigureBuilder = configureBuilder }; + return WithElasticDefaultsCore(builder, new(configuration), null, null, builderOptions); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static LoggerProviderBuilder WithElasticDefaultsCore( + this LoggerProviderBuilder builder, + CompositeElasticOpenTelemetryOptions? options, + ElasticOpenTelemetryComponents? components, + IServiceCollection? services, + in BuilderOptions builderOptions) + { + var logger = SignalBuilder.GetLogger(builder, components, options, null); + + var callCount = Interlocked.Increment(ref WithElasticDefaultsCallCount); + + if (callCount > 1) + { + logger.LogMultipleWithElasticDefaultsCallsWarning(callCount, nameof(LoggerProviderBuilder)); + } + else + { + logger.LogWithElasticDefaultsCallCount(callCount, nameof(LoggerProviderBuilder)); + } + + return SignalBuilder.WithElasticDefaults(builder, options, components, services, builderOptions, ConfigureBuilder); + + static void ConfigureBuilder(BuilderContext builderContext) + { + var builder = builderContext.Builder; + var builderState = builderContext.BuilderState; + var components = builderState.Components; + var logger = components.Logger; + var services = builderContext.Services; + + // FullName may return null so we fallback to Name when required. + var loggingProviderBuilderName = builder.GetType().FullName ?? builder.GetType().Name; + + logger.LogConfiguringBuilder(loggingProviderBuilderName, builderState.InstanceIdentifier); + + builder.ConfigureResource(r => r.WithElasticDefaults(builderState, services)); + + // When services is not null here, the options will have already been configured by the calling code so + // we don't need to do it again. + if (services is null) + { + builder.ConfigureServices(sc => sc.Configure(OtlpExporterDefaults.OtlpExporterOptions)); + logger.LogConfiguredOtlpExporterOptions(); + } + + builder.ConfigureServices(sc => sc.Configure(o => o.WithElasticDefaults(logger))); + + // This check is to detect if ASP.NET Core is present in the application. + // If it is, we check if IncludeScopes is enabled and log a warning because the upstream OTLP exporter + // exports duplicate attributes which does not conform to the spec and breaks the EDOT Collector. + if (builder is IDeferredLoggerProviderBuilder deferredBuilder) + { + var httpContextType = Type.GetType("Microsoft.AspNetCore.Http.HttpContext, Microsoft.AspNetCore.Http.Abstractions"); + + if (httpContextType is not null) + { + var options = deferredBuilder.Configure((sp, _) => + { + var options = sp.GetService>(); + + if (options is not null && options.Value.IncludeScopes == true) + { + logger.LogDetectedIncludeScopesWarning(); + } + }); + } + } + + // Invoke any user-provided configuration. + var userProvidedConfigureBuilder = builderContext.BuilderOptions.UserProvidedConfigureBuilder; + if (userProvidedConfigureBuilder is not null) + { + userProvidedConfigureBuilder(builder); + logger.LogInvokedConfigureAction(loggingProviderBuilderName, builderState.InstanceIdentifier); + } + + if (builderContext.BuilderOptions.DeferAddOtlpExporter) + { + // If a callee will register the OTLP exporter later, we skip adding it here. + logger.LogDeferredOtlpExporter(loggingProviderBuilderName, builderState.InstanceIdentifier); + } + else + { + if (components.Options.SkipOtlpExporter) + { + logger.LogSkippedOtlpExporter(nameof(Signals.Logs), loggingProviderBuilderName, builderState.InstanceIdentifier); + } + else + { + builder.AddOtlpExporter(); + logger.LogAddedOtlpExporter(nameof(Signals.Logs), loggingProviderBuilderName, builderState.InstanceIdentifier); + } + } + + logger.LogConfiguredSignalProvider(nameof(Signals.Logs), loggingProviderBuilderName, builderState.InstanceIdentifier); + } + } +} diff --git a/src/Elastic.OpenTelemetry/Extensions/LoggingProviderBuilderExtensions.cs b/src/Elastic.OpenTelemetry/Extensions/LoggingProviderBuilderExtensions.cs deleted file mode 100644 index 7b0911a7..00000000 --- a/src/Elastic.OpenTelemetry/Extensions/LoggingProviderBuilderExtensions.cs +++ /dev/null @@ -1,173 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System.Runtime.CompilerServices; -using Elastic.OpenTelemetry; -using Elastic.OpenTelemetry.Configuration; -using Elastic.OpenTelemetry.Core; -using Elastic.OpenTelemetry.Diagnostics; -using Elastic.OpenTelemetry.Exporters; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using OpenTelemetry.Exporter; -using OpenTelemetry.Logs; -using OpenTelemetry.Resources; - -// Matching namespace with LoggerProviderBuilder -#pragma warning disable IDE0130 // Namespace does not match folder structure -namespace OpenTelemetry; -#pragma warning restore IDE0130 // Namespace does not match folder structure - -/// -/// Provides extension methods on the used to register -/// the Elastic Distribution of OpenTelemetry (EDOT) .NET defaults. -/// -public static class LoggingProviderBuilderExtensions -{ - /// - /// Used to track the number of times any variation of `WithElasticDefaults` is invoked by consuming - /// code across all instances. This allows us to warn about potential - /// misconfigurations. - /// - private static int WithElasticDefaultsCallCount; - - /// - /// Use Elastic Distribution of OpenTelemetry (EDOT) .NET defaults for . - /// - /// The to configure. - /// Thrown when the is null. - /// The for chaining configuration. - public static LoggerProviderBuilder WithElasticDefaults(this LoggerProviderBuilder builder) - { -#if NET - ArgumentNullException.ThrowIfNull(builder); -#else - if (builder is null) - throw new ArgumentNullException(nameof(builder)); -#endif - - return WithElasticDefaultsCore(builder, null, null, null); - } - - /// - /// - /// - /// - /// used to configure the Elastic Distribution of OpenTelemetry (EDOT) .NET. - /// Thrown when the is null. - /// Thrown when the is null. - /// - public static LoggerProviderBuilder WithElasticDefaults(this LoggerProviderBuilder builder, ElasticOpenTelemetryOptions options) - { -#if NET - ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(options); -#else - if (builder is null) - throw new ArgumentNullException(nameof(builder)); - - if (options is null) - throw new ArgumentNullException(nameof(options)); -#endif - - return WithElasticDefaultsCore(builder, new(options), null, null); - } - - /// - /// - /// - /// - /// An instance from which to load the OpenTelemetry SDK options. - /// Thrown when the is null. - /// Thrown when the is null. - /// - public static LoggerProviderBuilder WithElasticDefaults(this LoggerProviderBuilder builder, IConfiguration configuration) - { -#if NET - ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(configuration); -#else - if (builder is null) - throw new ArgumentNullException(nameof(builder)); - - if (configuration is null) - throw new ArgumentNullException(nameof(configuration)); -#endif - return WithElasticDefaultsCore(builder, new(configuration), null, null); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static LoggerProviderBuilder WithElasticDefaults(this LoggerProviderBuilder builder, ElasticOpenTelemetryComponents components, IServiceCollection? services) => - WithElasticDefaultsCore(builder, components.Options, components, services); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static LoggerProviderBuilder WithElasticDefaultsCore( - this LoggerProviderBuilder builder, - CompositeElasticOpenTelemetryOptions? options, - ElasticOpenTelemetryComponents? components, - IServiceCollection? services) - { - var logger = SignalBuilder.GetLogger(builder, components, options, null); - - var callCount = Interlocked.Increment(ref WithElasticDefaultsCallCount); - - if (callCount > 1) - { - logger.LogMultipleWithElasticDefaultsCallsWarning(callCount, nameof(LoggerProviderBuilder)); - } - else - { - logger.LogWithElasticDefaultsCallCount(callCount, nameof(LoggerProviderBuilder)); - } - - return SignalBuilder.WithElasticDefaults(builder, Signals.Traces, options, components, services, ConfigureBuilder); - - static void ConfigureBuilder(LoggerProviderBuilder builder, BuilderState builderState, IServiceCollection? services) - { - const string loggingProviderName = nameof(LoggerProviderBuilder); - var components = builderState.Components; - var logger = components.Logger; - - logger.LogConfiguringBuilder(loggingProviderName, builderState.InstanceIdentifier); - - builder.ConfigureResource(r => r.WithElasticDefaults(builderState, services)); - - // When services is not null here, the options will have already been configured by the calling code. - if (services is null) - builder.ConfigureServices(sc => sc.Configure(OtlpExporterDefaults.OtlpExporterOptions)); - - builder.ConfigureServices(sc => sc.Configure(o => o.WithElasticDefaults(logger))); - - if (builder is IDeferredLoggerProviderBuilder deferredBuilder) - { - var httpContextType = Type.GetType("Microsoft.AspNetCore.Http.HttpContext, Microsoft.AspNetCore.Http.Abstractions"); - - if (httpContextType is not null) - { - var options = deferredBuilder.Configure((sp, _) => - { - var options = sp.GetService>(); - - if (options is not null && options.Value.IncludeScopes == true) - { - logger.LogDetectedIncludeScopesWarning(); - } - }); - } - } - - if (components.Options.SkipOtlpExporter) - { - logger.LogSkippingOtlpExporter(nameof(Signals.Logs), loggingProviderName, builderState.InstanceIdentifier); - } - else - { - builder.AddOtlpExporter(); - } - - logger.LogConfiguredSignalProvider(nameof(Signals.Logs), loggingProviderName, builderState.InstanceIdentifier); - } - } -} diff --git a/src/Elastic.OpenTelemetry/Extensions/MeterProviderBuilderExtensions.cs b/src/Elastic.OpenTelemetry/Extensions/MeterProviderBuilderExtensions.cs index e9e98e23..f3919099 100644 --- a/src/Elastic.OpenTelemetry/Extensions/MeterProviderBuilderExtensions.cs +++ b/src/Elastic.OpenTelemetry/Extensions/MeterProviderBuilderExtensions.cs @@ -12,10 +12,13 @@ using Elastic.OpenTelemetry.Instrumentation; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using OpenTelemetry.Exporter; +using OpenTelemetry.Logs; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; +using OpenTelemetry.Trace; // Matching namespace with MeterProviderBuilder #pragma warning disable IDE0130 // Namespace does not match folder structure @@ -24,7 +27,8 @@ namespace OpenTelemetry; /// /// Provides extension methods on the used to register -/// the Elastic Distribution of OpenTelemetry (EDOT) .NET defaults. +/// the Elastic Distribution of OpenTelemetry (EDOT) .NET defaults +/// for metrics. /// public static class MeterProviderBuilderExtensions { @@ -39,12 +43,47 @@ public static class MeterProviderBuilderExtensions /// Use Elastic Distribution of OpenTelemetry .NET defaults for . /// /// - /// Calling this method is not neccesary if - /// has been called previously as that automatically adds the defaults for all signals. + /// + /// This method is intended for advanced scenarios where per signal Elastic Distribution of OpenTelemetry (EDOT) .NET + /// defaults are being enabled. In most scenarios, prefer the AddElasticOpenTelemetry method on + /// or . + /// + /// + /// Warning: Avoid calling this method when using the AddElasticOpenTelemetry method on + /// or . In those scenarios, the OpenTelemetry SDK will have already been configured with + /// defaults, including the OTLP exporter. Calling this method again may lead to unexpected behavior. + /// + /// + /// When using AddElasticOpenTelemetry, use the overload on the + /// or which accepts a configuration + /// to further customize the SDK setup. This action is invoked after EDOT .NET defaults have been applied, but + /// before the OTLP exporter is added. + /// + /// + /// Note: If you wish to customise the after the EDOT .NET defaults have been applied, + /// use the overload that accepts a + /// configuration action. + /// /// /// The to configure. /// Thrown when the is null. - /// The for chaining configuration. + /// + /// An instance of that can be used to further configure the metrics signal. + /// + /// Warning: Any further configuration applied via the returned will + /// be applied after the OTLP exporter has been added. + /// + /// + /// Recommendation:It is strongly recommended to use the + /// overload instead. This accepts an configuration callback and ensures any + /// customisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the + /// option + /// automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// public static MeterProviderBuilder WithElasticDefaults(this MeterProviderBuilder builder) { #if NET @@ -54,17 +93,111 @@ public static MeterProviderBuilder WithElasticDefaults(this MeterProviderBuilder throw new ArgumentNullException(nameof(builder)); #endif - return WithElasticDefaultsCore(builder, null, null, null); + return WithElasticDefaultsCore(builder, null, null, null, default); } - /// - /// - /// + /// + /// + /// + /// This method is intended for advanced scenarios where per signal Elastic Distribution of OpenTelemetry (EDOT) .NET + /// defaults are being enabled. In most scenarios, prefer the AddElasticOpenTelemetry method on + /// or . + /// + /// + /// Warning: Avoid calling this method when using the AddElasticOpenTelemetry method on + /// or . In those scenarios, the OpenTelemetry SDK will have already been configured with + /// defaults, including the OTLP exporter. Calling this method again may lead to unexpected behavior. + /// + /// + /// When using AddElasticOpenTelemetry, use the overload on the + /// or which accepts a configuration + /// to further customize the SDK setup. This action is invoked after EDOT .NET defaults have been applied, but + /// before the OTLP exporter is added. + /// + /// + /// + /// + /// An used to further configure the . + /// This action is invoked after EDOT .NET defaults + /// have been applied, but before the OTLP exporter is added. This ensures that any custom processors run before the exporter. + /// + /// Thrown when the is null. + /// Thrown when the is null. + /// + /// + /// An instance of that can be used to further configure the metrics signal. + /// + /// + /// Warning: Any further configuration applied via the returned will + /// be applied after the OTLP exporter has been added. + /// + /// + /// Recommendation:It is strongly recommended to use the action to customise + /// the after EDOT .NET defaults are applied and before the OTLP exporter is added. + /// + /// + public static MeterProviderBuilder WithElasticDefaults(this MeterProviderBuilder builder, Action configureBuilder) + { +#if NET + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configureBuilder); +#else + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + + if (configureBuilder is null) + throw new ArgumentNullException(nameof(configureBuilder)); +#endif + + var builderOptions = new BuilderOptions { UserProvidedConfigureBuilder = configureBuilder }; + return WithElasticDefaultsCore(builder, null, null, null, builderOptions); + } + + /// + /// + /// + /// This method is intended for advanced scenarios where per signal Elastic Distribution of OpenTelemetry (EDOT) .NET + /// defaults are being enabled. In most scenarios, prefer the AddElasticOpenTelemetry method on + /// or . + /// + /// + /// Warning: Avoid calling this method when using the AddElasticOpenTelemetry method on + /// or . In those scenarios, the OpenTelemetry SDK will have already been configured with + /// defaults, including the OTLP exporter. Calling this method again may lead to unexpected behavior. + /// + /// + /// When using AddElasticOpenTelemetry, use the overload on the + /// or which accepts a configuration + /// to further customize the SDK setup. This action is invoked after EDOT .NET defaults have been applied, but + /// before the OTLP exporter is added. + /// + /// + /// Note: If you wish to customise the after the EDOT .NET defaults have been applied, + /// use the overload that accepts a + /// configuration action. + /// + /// /// /// used to configure the Elastic Distribution of OpenTelemetry (EDOT) .NET. /// Thrown when the is null. /// Thrown when the is null. - /// + /// + /// An instance of that can be used to further configure the metrics signal. + /// + /// Warning: Any further configuration applied via the returned will + /// be applied after the OTLP exporter has been added. + /// + /// + /// Recommendation:It is strongly recommended to use the + /// overload instead. This accepts an configuration callback and ensures any + /// customisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the + /// option + /// automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// public static MeterProviderBuilder WithElasticDefaults(this MeterProviderBuilder builder, ElasticOpenTelemetryOptions options) { #if NET @@ -78,18 +211,85 @@ public static MeterProviderBuilder WithElasticDefaults(this MeterProviderBuilder throw new ArgumentNullException(nameof(options)); #endif - return WithElasticDefaultsCore(builder, new(options), null, null); + return WithElasticDefaultsCore(builder, new(options), null, null, default); } - /// - /// - /// + /// + /// + /// + /// + /// + /// Thrown when the is null. + /// Thrown when the is null. + /// Thrown when the is null. + /// + public static MeterProviderBuilder WithElasticDefaults(this MeterProviderBuilder builder, + ElasticOpenTelemetryOptions options, Action configureBuilder) + { +#if NET + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(configureBuilder); +#else + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + + if (options is null) + throw new ArgumentNullException(nameof(options)); + + if (configureBuilder is null) + throw new ArgumentNullException(nameof(configureBuilder)); +#endif + var builderOptions = new BuilderOptions { UserProvidedConfigureBuilder = configureBuilder }; + return WithElasticDefaultsCore(builder, new(options), null, null, builderOptions); + } + + /// + /// + /// + /// This method is intended for advanced scenarios where per signal Elastic Distribution of OpenTelemetry (EDOT) .NET + /// defaults are being enabled. In most scenarios, prefer the AddElasticOpenTelemetry method on + /// or . + /// + /// + /// Warning: Avoid calling this method when using the AddElasticOpenTelemetry method on + /// or . In those scenarios, the OpenTelemetry SDK will have already been configured with + /// defaults, including the OTLP exporter. Calling this method again may lead to unexpected behavior. + /// + /// + /// When using AddElasticOpenTelemetry, use the overload on the + /// or which accepts a configuration + /// to further customize the SDK setup. This action is invoked after EDOT .NET defaults have been applied, but + /// before the OTLP exporter is added. + /// + /// + /// Note: If you wish to customise the after the EDOT .NET defaults have been applied, + /// use the overload that accepts a + /// configuration action. + /// + /// /// /// An instance from which to load the Elastic Distribution of /// OpenTelemetry (EDOT) .NET options. /// Thrown when the is null. /// Thrown when the is null. - /// + /// + /// An instance of that can be used to further configure the metrics signal. + /// + /// Warning: Any further configuration applied via the returned will + /// be applied after the OTLP exporter has been added. + /// + /// + /// Recommendation:It is strongly recommended to use the + /// overload instead. This accepts an configuration callback and ensures any + /// customisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the + /// option + /// automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// public static MeterProviderBuilder WithElasticDefaults(this MeterProviderBuilder builder, IConfiguration configuration) { #if NET @@ -102,28 +302,38 @@ public static MeterProviderBuilder WithElasticDefaults(this MeterProviderBuilder if (configuration is null) throw new ArgumentNullException(nameof(configuration)); #endif - return WithElasticDefaultsCore(builder, new(configuration), null, null); + return WithElasticDefaultsCore(builder, new(configuration), null, null, default); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static MeterProviderBuilder WithElasticDefaults( - this MeterProviderBuilder builder, - ElasticOpenTelemetryComponents components, - IServiceCollection serviceCollection) => - WithElasticDefaultsCore(builder, components.Options, components, serviceCollection); + /// + /// + /// + /// + /// + /// Thrown when the is null. + /// Thrown when the is null. + /// Thrown when the is null. + /// + public static MeterProviderBuilder WithElasticDefaults(this MeterProviderBuilder builder, + IConfiguration configuration, Action configureBuilder) + { +#if NET + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configuration); + ArgumentNullException.ThrowIfNull(configureBuilder); +#else + if (builder is null) + throw new ArgumentNullException(nameof(builder)); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static MeterProviderBuilder WithElasticDefaults( - this MeterProviderBuilder builder, - IConfiguration configuration, - IServiceCollection serviceCollection) => - WithElasticDefaultsCore(builder, new(configuration), null, serviceCollection); + if (configuration is null) + throw new ArgumentNullException(nameof(configuration)); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static MeterProviderBuilder WithElasticDefaults( - this MeterProviderBuilder builder, - IServiceCollection serviceCollection) => - WithElasticDefaultsCore(builder, null, null, serviceCollection); + if (configureBuilder is null) + throw new ArgumentNullException(nameof(configureBuilder)); +#endif + var builderOptions = new BuilderOptions { UserProvidedConfigureBuilder = configureBuilder }; + return WithElasticDefaultsCore(builder, new(configuration), null, null, builderOptions); + } [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "The calls to `AddSqlClientInstrumentation` and `AssemblyScanning.AddInstrumentationViaReflection` " + "are guarded by a RuntimeFeature.IsDynamicCodeSupported` check and therefore this method is safe to call in AoT scenarios.")] @@ -131,7 +341,8 @@ internal static MeterProviderBuilder WithElasticDefaultsCore( this MeterProviderBuilder builder, CompositeElasticOpenTelemetryOptions? options, ElasticOpenTelemetryComponents? components, - IServiceCollection? services) + IServiceCollection? services, + in BuilderOptions builderOptions) { var logger = SignalBuilder.GetLogger(builder, components, options, null); @@ -146,21 +357,30 @@ internal static MeterProviderBuilder WithElasticDefaultsCore( logger.LogWithElasticDefaultsCallCount(callCount, nameof(MeterProviderBuilder)); } - return SignalBuilder.WithElasticDefaults(builder, Signals.Traces, options, components, services, ConfigureBuilder); + return SignalBuilder.WithElasticDefaults(builder, options, components, services, builderOptions, ConfigureBuilder); - static void ConfigureBuilder(MeterProviderBuilder builder, BuilderState builderState, IServiceCollection? services) + static void ConfigureBuilder(BuilderContext builderContext) { - const string meterProviderBuilderName = nameof(MeterProviderBuilder); + var builder = builderContext.Builder; + var builderState = builderContext.BuilderState; var components = builderState.Components; var logger = components.Logger; + var services = builderContext.Services; + + // FullName may return null so we fallback to Name when required. + var meterProviderBuilderName = builder.GetType().FullName ?? builder.GetType().Name; logger.LogConfiguringBuilder(meterProviderBuilderName, builderState.InstanceIdentifier); builder.ConfigureResource(r => r.WithElasticDefaults(builderState, services)); - // When services is not null here, the options will have already been configured by the calling code. + // When services is not null here, the options will have already been configured by the calling code so + // we don't need to do it again. if (services is null) + { builder.ConfigureServices(sc => sc.Configure(OtlpExporterDefaults.OtlpExporterOptions)); + logger.LogConfiguredOtlpExporterOptions(); + } builder.ConfigureServices(sc => sc.Configure(o => o.TemporalityPreference = MetricReaderTemporalityPreference.Delta)); @@ -227,16 +447,32 @@ static void ConfigureBuilder(MeterProviderBuilder builder, BuilderState builderS ContribMetricsInstrumentation.GetMetricsInstrumentationAssembliesInfo(), builderState.InstanceIdentifier); } - if (components.Options.SkipOtlpExporter) + // Execute user-provided configuration action + var userProvidedConfigureBuilder = builderContext.BuilderOptions.UserProvidedConfigureBuilder; + if (userProvidedConfigureBuilder is not null) + { + userProvidedConfigureBuilder(builder); + logger.LogInvokedConfigureAction(meterProviderBuilderName, builderState.InstanceIdentifier); + } + + if (builderContext.BuilderOptions.DeferAddOtlpExporter) { - logger.LogSkippingOtlpExporter(nameof(Signals.Traces), nameof(MeterProviderBuilder), builderState.InstanceIdentifier); + logger.LogDeferredOtlpExporter(meterProviderBuilderName, builderState.InstanceIdentifier); } else { - builder.AddOtlpExporter(); + if (components.Options.SkipOtlpExporter) + { + logger.LogSkippedOtlpExporter(nameof(Signals.Metrics), meterProviderBuilderName, builderState.InstanceIdentifier); + } + else + { + builder.AddOtlpExporter(); + logger.LogAddedOtlpExporter(nameof(Signals.Metrics), meterProviderBuilderName, builderState.InstanceIdentifier); + } } - logger.LogConfiguredSignalProvider(nameof(Signals.Logs), nameof(MeterProviderBuilder), builderState.InstanceIdentifier); + logger.LogConfiguredSignalProvider(nameof(Signals.Metrics), meterProviderBuilderName, builderState.InstanceIdentifier); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/Elastic.OpenTelemetry/Extensions/OpenTelemetryBuilderExtensions.cs b/src/Elastic.OpenTelemetry/Extensions/OpenTelemetryBuilderExtensions.cs index 83419f7c..7549aaa5 100644 --- a/src/Elastic.OpenTelemetry/Extensions/OpenTelemetryBuilderExtensions.cs +++ b/src/Elastic.OpenTelemetry/Extensions/OpenTelemetryBuilderExtensions.cs @@ -7,15 +7,11 @@ using Elastic.OpenTelemetry.Configuration; using Elastic.OpenTelemetry.Core; using Elastic.OpenTelemetry.Diagnostics; -using Elastic.OpenTelemetry.Exporters; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Diagnostics.Metrics; -using Microsoft.Extensions.Logging; -using OpenTelemetry.Exporter; +using Microsoft.Extensions.Hosting; using OpenTelemetry.Logs; using OpenTelemetry.Metrics; -using OpenTelemetry.Resources; using OpenTelemetry.Trace; // Matching namespace with OpenTelemetryBuilder @@ -25,7 +21,8 @@ namespace OpenTelemetry; /// /// Provides extension methods on the -/// used to register the Elastic Distribution of OpenTelemetry (EDOT) .NET defaults. +/// used to register the Elastic Distribution of OpenTelemetry (EDOT) .NET +/// defaults. /// public static class OpenTelemetryBuilderExtensions { @@ -37,12 +34,45 @@ public static class OpenTelemetryBuilderExtensions private static int WithElasticDefaultsCallCount; /// - /// Enables collection of all signals using Elastic Distribution of OpenTelemetry .NET defaults. + /// Enables collection of all signals using Elastic Distribution of OpenTelemetry .NET + /// defaults. /// + /// + /// + /// Guidance: Prefer using the AddElasticOpenTelemetry method on the + /// or rather than calling this method on the directly. + /// + /// + /// The WithElasticDefaults methods are primarily intended for advanced scenarios, non-host-based applications or when developing + /// libraries that need to customize the OpenTelemetry configuration. + /// + /// + /// Recommendation:When not using AddElasticOpenTelemetry, it is strongly recommended to use the + /// overload instead. This accepts + /// an configuration action and ensures any customisations are applied before + /// the OTLP exporter is added. + /// + /// /// The being configured. - /// /// Thrown when the is null. - /// The supplied for chaining calls. + /// + /// + /// An instance of that can be used to further configure the + /// OpenTelemetry SDK. + /// + /// + /// Warning: Any further configuration applied via the returned will + /// be applied after the OTLP exporter has been added. + /// + /// + /// Recommendation:It is strongly recommended to use the overload + /// to customise the builder instead. This accepts an configuration action and ensures any + /// customisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the option + /// can be used to prevent automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// /// public static IOpenTelemetryBuilder WithElasticDefaults(this IOpenTelemetryBuilder builder) { @@ -53,18 +83,103 @@ public static IOpenTelemetryBuilder WithElasticDefaults(this IOpenTelemetryBuild throw new ArgumentNullException(nameof(builder)); #endif - return WithElasticDefaultsCore(builder, CompositeElasticOpenTelemetryOptions.DefaultOptions); + return WithElasticDefaultsCore(builder, CompositeElasticOpenTelemetryOptions.DefaultOptions, default); } - /// - /// - /// + /// + /// + /// + /// Guidance: Prefer using the AddElasticOpenTelemetry method on the + /// or rather than calling this method on the directly. + /// + /// + /// The WithElasticDefaults methods are primarily intended for advanced scenarios, non-host-based applications or when developing + /// libraries that need to customize the OpenTelemetry configuration. + /// + /// + /// The being configured. + /// A configuration action used to further customise + /// the OpenTelemetry SDK after Elastic Distribution of OpenTelemetry (EDOT) .NET defaults have been applied. This + /// callback is invoked before the OTLP exporter is added. + /// Thrown when the is null. + /// Thrown when the is null. + /// The supplied for chaining calls. + /// + /// + /// An instance of that can be used to further configure the + /// OpenTelemetry SDK. + /// + /// + /// Warning: Any further configuration applied via the returned will + /// be applied after the OTLP exporter has been added. + /// + /// + /// Recommendation:It is strongly recommended to use the parameter to customise + /// the builder instead. This accepts an configuration action and ensures any + /// customisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the option + /// can be used to prevent automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// + public static IOpenTelemetryBuilder WithElasticDefaults(this IOpenTelemetryBuilder builder, Action configureBuilder) + { +#if NET + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configureBuilder); +#else + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + + if (configureBuilder is null) + throw new ArgumentNullException(nameof(configureBuilder)); +#endif + var builderOptions = new BuilderOptions { UserProvidedConfigureBuilder = configureBuilder }; + return WithElasticDefaultsCore(builder, CompositeElasticOpenTelemetryOptions.DefaultOptions, builderOptions); + } + + /// + /// + /// + /// Guidance: Prefer using the AddElasticOpenTelemetry method on the + /// or rather than calling this method on the directly. + /// + /// + /// The WithElasticDefaults methods are primarily intended for advanced scenarios, non-host-based applications or when developing + /// libraries that need to customize the OpenTelemetry configuration. + /// + /// + /// Recommendation:When not using AddElasticOpenTelemetry, it is strongly recommended to use the + /// overload instead. This accepts + /// an configuration callback and ensures any customisations are applied before + /// the OTLP exporter is added. + /// + /// /// /// An from which to attempt binding of Elastic Distribution of OpenTelemetry /// (EDOT) options. /// Thrown when the is null. /// Thrown when the is null. - /// + /// + /// + /// An instance of that can be used to further configure the + /// OpenTelemetry SDK. + /// + /// + /// Warning: Any further configuration applied via the returned will + /// be applied after the OTLP exporter has been added. + /// + /// + /// Recommendation:It is strongly recommended to use the overload + /// to customise the builder instead. This accepts an configuration callback and ensures any + /// customisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the option + /// can be used to prevent automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// public static IOpenTelemetryBuilder WithElasticDefaults(this IOpenTelemetryBuilder builder, IConfiguration configuration) { #if NET @@ -78,12 +193,58 @@ public static IOpenTelemetryBuilder WithElasticDefaults(this IOpenTelemetryBuild throw new ArgumentNullException(nameof(configuration)); #endif - return WithElasticDefaultsCore(builder, new(configuration)); + return WithElasticDefaultsCore(builder, new(configuration), default); + } + + /// + /// + /// + /// + /// + /// Thrown when the is null. + /// Thrown when the is null. + /// Thrown when the is null. + /// + public static IOpenTelemetryBuilder WithElasticDefaults(this IOpenTelemetryBuilder builder, + IConfiguration configuration, Action configureBuilder) + { +#if NET + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configuration); + ArgumentNullException.ThrowIfNull(configureBuilder); +#else + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + + if (configuration is null) + throw new ArgumentNullException(nameof(configuration)); + + if (configureBuilder is null) + throw new ArgumentNullException(nameof(configureBuilder)); +#endif + var builderOptions = new BuilderOptions { UserProvidedConfigureBuilder = configureBuilder }; + return WithElasticDefaultsCore(builder, new(configuration), builderOptions); } /// /// /// + /// + /// + /// Guidance: Prefer using the AddElasticOpenTelemetry method on the + /// or rather than calling this method on the directly. + /// + /// + /// The WithElasticDefaults methods are primarily intended for advanced scenarios, non-host-based applications or when developing + /// libraries that need to customize the OpenTelemetry configuration. + /// + /// + /// Recommendation:When not using AddElasticOpenTelemetry, it is strongly recommended to use the + /// overload instead. This accepts + /// an configuration callback and ensures any customisations are applied before + /// the OTLP exporter is added. + /// + /// /// /// /// An instance used to configure the initial Elastic Distribution of OpenTelemetry (EDOT) .NET defaults. @@ -92,7 +253,25 @@ public static IOpenTelemetryBuilder WithElasticDefaults(this IOpenTelemetryBuild /// /// Thrown when the is null. /// Thrown when the is null. - /// + /// + /// + /// An instance of that can be used to further configure the + /// OpenTelemetry SDK. + /// + /// + /// Warning: Any further configuration applied via the returned by calling WithLogging, + /// WithTracing or WithMetrics will be applied after the OTLP exporter has been added. + /// + /// + /// Recommendation:It is strongly recommended to use the + /// overload to customise the builder instead. This accepts an configuration callback and ensures any + /// customisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the option + /// can be used to prevent automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// public static IOpenTelemetryBuilder WithElasticDefaults(this IOpenTelemetryBuilder builder, ElasticOpenTelemetryOptions options) { #if NET @@ -106,55 +285,80 @@ public static IOpenTelemetryBuilder WithElasticDefaults(this IOpenTelemetryBuild throw new ArgumentNullException(nameof(options)); #endif - return WithElasticDefaultsCore(builder, new(options)); + return WithElasticDefaultsCore(builder, new(options), default); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static IOpenTelemetryBuilder WithElasticDefaultsCore( - this IOpenTelemetryBuilder builder, - CompositeElasticOpenTelemetryOptions options) + /// + /// + /// + /// + /// + /// Thrown when the is null. + /// Thrown when the is null. + /// Thrown when the is null. + /// + public static IOpenTelemetryBuilder WithElasticDefaults(this IOpenTelemetryBuilder builder, + ElasticOpenTelemetryOptions options, Action configureBuilder) { - var callCount = Interlocked.Increment(ref WithElasticDefaultsCallCount); - - var providerBuilderName = builder.GetType().Name; - - var logger = SignalBuilder.GetLogger(builder, null, options, null); +#if NET + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(configureBuilder); +#else + if (builder is null) + throw new ArgumentNullException(nameof(builder)); - if (callCount > 1) - { - logger.LogMultipleWithElasticDefaultsCallsWarning(callCount, nameof(IOpenTelemetryBuilder)); - } + if (options is null) + throw new ArgumentNullException(nameof(options)); - // If for some reason `WithElasticDefaults` is invoked with the `Signals` option set to - // none, we skip bootstrapping entirely. We log this as a warning since it's best to - // simply not call `WithElasticDefaults` in this scenario and it may indicate a misconfiguration. - if (options.Signals == Signals.None) - { - logger.LogSkippingBootstrapWarning(); - return builder; - } + if (configureBuilder is null) + throw new ArgumentNullException(nameof(configureBuilder)); +#endif - return SignalBuilder.WithElasticDefaults(builder, options, null, builder.Services, ConfigureBuilder); + return WithElasticDefaultsCore(builder, new(options), default); } /// - /// Adds metric services into the using Elastic Distribution of - /// OpenTelemetry (EDOT) .NET defaults. + /// Adds logging services into the using Elastic Distribution of + /// OpenTelemetry (EDOT) .NET defaults. /// - /// . /// - /// Notes: - /// - /// This is safe to be called multiple times and by library authors. - /// Only a single will be created for a given - /// . - /// This method automatically registers an named 'OpenTelemetry' into the . - /// + /// + /// Guidance: Prefer using the AddElasticOpenTelemetry method on the + /// or rather than calling this method on the directly. + /// + /// + /// The WithElasticLogging methods are primarily intended for advanced scenarios, non-host-based applications or when developing + /// libraries that need to customize the OpenTelemetry configuration. + /// + /// + /// Recommendation:When not using AddElasticOpenTelemetry, it is strongly recommended to use the + /// overload instead. This accepts + /// an configuration callback and ensures any customisations are applied before + /// the OTLP exporter is added. + /// /// + /// /// Thrown when the is null. - /// The supplied for chaining calls. + /// + /// + /// An instance of that can be used to further configure the + /// OpenTelemetry SDK. + /// + /// + /// Warning: Any further configuration applied via the returned by calling WithLogging will + /// be applied after the OTLP exporter has been added to the . + /// + /// + /// Recommendation:It is strongly recommended to use the overload + /// to customise the builder instead. This accepts an configuration callback and ensures any + /// customisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the option + /// can be used to prevent automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// public static IOpenTelemetryBuilder WithElasticLogging(this IOpenTelemetryBuilder builder) { #if NET @@ -167,15 +371,36 @@ public static IOpenTelemetryBuilder WithElasticLogging(this IOpenTelemetryBuilde return builder.WithLogging(lpb => lpb.WithElasticDefaults()); } - /// - /// - /// - /// - /// - /// configuration callback. + /// + /// + /// + /// Guidance: Prefer using the AddElasticOpenTelemetry method on the + /// or rather than calling this method on the directly. + /// + /// + /// The WithElasticLogging methods are primarily intended for advanced scenarios, non-host-based applications or when developing + /// libraries that need to customize the OpenTelemetry configuration. + /// + /// + /// + /// + /// An used to further configure the . + /// This action is invoked after EDOT .NET defaults + /// have been applied, but before the OTLP exporter is added. This ensures that any custom processors run before the exporter. + /// /// Thrown when the is null. /// Thrown when the is null. - /// + /// + /// + /// An instance of that can be used to further configure the + /// OpenTelemetry SDK. + /// + /// + /// Warning: Any further configuration applied via the returned by calling WithLogging will + /// be applied after the OTLP exporter has been added to the . Use the + /// parameter to apply customisations before the exporter is added. + /// + /// public static IOpenTelemetryBuilder WithElasticLogging(this IOpenTelemetryBuilder builder, Action configure) { #if NET @@ -189,30 +414,49 @@ public static IOpenTelemetryBuilder WithElasticLogging(this IOpenTelemetryBuilde throw new ArgumentNullException(nameof(configure)); #endif - return builder.WithLogging(lpb => - { - lpb.WithElasticDefaults(); - configure?.Invoke(lpb); - }); + return builder.WithLogging(lpb => lpb.WithElasticDefaults(configure)); } /// - /// Adds metric services into the using Elastic Distribution of OpenTelemetry (EDOT) .NET defaults. + /// Adds metrics services into the using Elastic Distribution of + /// OpenTelemetry (EDOT) .NET defaults. /// - /// . /// - /// Notes: - /// - /// This is safe to be called multiple times and by library authors. - /// Only a single will be created for a given - /// . - /// This method automatically registers an named 'OpenTelemetry' into the . - /// + /// + /// Guidance: Prefer using the AddElasticOpenTelemetry method on the + /// or rather than calling this method on the directly. + /// + /// + /// The WithElasticMetrics methods are primarily intended for advanced scenarios, non-host-based applications or when developing + /// libraries that need to customize the OpenTelemetry configuration. + /// + /// + /// Recommendation:When not using AddElasticOpenTelemetry, it is strongly recommended to use the + /// overload instead. This accepts + /// an configuration callback and ensures any customisations are applied before + /// the OTLP exporter is added. + /// /// + /// /// Thrown when the is null. - /// The supplied for chaining calls. + /// + /// + /// An instance of that can be used to further configure the OpenTelemetry SDK. + /// + /// + /// Warning: Any further configuration applied via the returned by calling WithMetrics will + /// be applied after the OTLP exporter has been added to the . + /// + /// + /// Recommendation:It is strongly recommended to use the overload + /// to customise the builder instead. This accepts an configuration callback and ensures any + /// customisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the option + /// can be used to prevent automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// public static IOpenTelemetryBuilder WithElasticMetrics(this IOpenTelemetryBuilder builder) { #if NET @@ -225,76 +469,121 @@ public static IOpenTelemetryBuilder WithElasticMetrics(this IOpenTelemetryBuilde return builder.WithMetrics(mpb => mpb.WithElasticDefaults()); } - /// - /// - /// - /// - /// - /// An instance from which to load the Elastic Distribution of - /// OpenTelemetry (EDOT) .NET options. + /// + /// + /// + /// Guidance: Prefer using the AddElasticOpenTelemetry method on the + /// or rather than calling this method on the directly. + /// + /// + /// The WithElasticMetrics methods are primarily intended for advanced scenarios, non-host-based applications or when developing + /// libraries that need to customize the OpenTelemetry configuration. + /// + /// + /// + /// + /// An used to further configure the . + /// This action is invoked after EDOT .NET defaults + /// have been applied, but before the OTLP exporter is added. This ensures that any custom processors run before the exporter. + /// /// Thrown when the is null. - /// Thrown when the is null. - /// - public static IOpenTelemetryBuilder WithElasticMetrics(this IOpenTelemetryBuilder builder, IConfiguration configuration) + /// Thrown when the is null. + /// + /// + /// An instance of that can be used to further configure the + /// OpenTelemetry SDK. + /// + /// + /// Warning: Any further configuration applied via the returned by calling WithMetrics will + /// be applied after the OTLP exporter has been added to the . Use the + /// parameter to apply customisations before the exporter is added. + /// + /// + public static IOpenTelemetryBuilder WithElasticMetrics(this IOpenTelemetryBuilder builder, Action configure) { #if NET ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(configuration); + ArgumentNullException.ThrowIfNull(configure); #else if (builder is null) throw new ArgumentNullException(nameof(builder)); - if (configuration is null) - throw new ArgumentNullException(nameof(configuration)); + if (configure is null) + throw new ArgumentNullException(nameof(configure)); #endif - return builder.WithMetrics(mpb => mpb.WithElasticDefaults(configuration, builder.Services)); + var builderOptions = new BuilderOptions { UserProvidedConfigureBuilder = configure }; + return builder.WithMetrics(mpb => mpb.WithElasticDefaults(configure)); } /// - /// + /// Adds metrics services into the using Elastic Distribution of + /// OpenTelemetry (EDOT) .NET defaults. /// - /// - /// - /// configuration callback. + /// + /// + /// Guidance: Prefer using the AddElasticOpenTelemetry method on the + /// or rather than calling this method on the directly. + /// + /// + /// The WithElasticMetrics methods are primarily intended for advanced scenarios, non-host-based applications or when developing + /// libraries that need to customize the OpenTelemetry configuration. + /// + /// + /// Recommendation:When not using AddElasticOpenTelemetry, it is strongly recommended to use the + /// overload instead. This accepts + /// an configuration callback and ensures any customisations are applied before + /// the OTLP exporter is added. + /// + /// + /// + /// /// Thrown when the is null. - /// Thrown when the is null. - /// - public static IOpenTelemetryBuilder WithElasticMetrics(this IOpenTelemetryBuilder builder, Action configure) + /// Thrown when the is null. + /// + /// + /// An instance of that can be used to further configure the + /// OpenTelemetry SDK. + /// + /// + /// Warning: Any further configuration applied via the returned by calling WithMetrics will + /// be applied after the OTLP exporter has been added to the . + /// + /// + /// Recommendation:It is strongly recommended to use the overload + /// to customise the builder instead. This accepts an configuration callback and ensures any + /// customisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the option + /// can be used to prevent automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// + public static IOpenTelemetryBuilder WithElasticMetrics(this IOpenTelemetryBuilder builder, IConfiguration configuration) { #if NET ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(configure); + ArgumentNullException.ThrowIfNull(configuration); #else if (builder is null) throw new ArgumentNullException(nameof(builder)); - if (configure is null) - throw new ArgumentNullException(nameof(configure)); + if (configuration is null) + throw new ArgumentNullException(nameof(configuration)); #endif - return builder.WithMetrics(mpb => - { - mpb.WithElasticDefaults(builder.Services); - configure?.Invoke(mpb); - }); + return builder.WithMetrics(mpb => mpb.WithElasticDefaultsCore(new(configuration), null, builder.Services, default)); } - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// + /// + /// + /// + /// + /// /// Thrown when the is null. /// Thrown when the is null. /// Thrown when the is null. - /// + /// public static IOpenTelemetryBuilder WithElasticMetrics(this IOpenTelemetryBuilder builder, IConfiguration configuration, Action configure) { @@ -313,24 +602,50 @@ public static IOpenTelemetryBuilder WithElasticMetrics(this IOpenTelemetryBuilde throw new ArgumentNullException(nameof(configure)); #endif - return builder.WithMetrics(mpb => - { - mpb.WithElasticDefaults(configuration, builder.Services); - configure?.Invoke(mpb); - }); + var builderOptions = new BuilderOptions { UserProvidedConfigureBuilder = configure }; + return builder.WithMetrics(mpb => mpb.WithElasticDefaultsCore(new(configuration), null, builder.Services, builderOptions)); } /// - /// Adds tracing services into the using Elastic Distribution of OpenTelemetry (EDOT) .NET defaults. + /// Adds tracing services into the using Elastic Distribution of + /// OpenTelemetry (EDOT) .NET defaults. /// - /// . /// - /// Note: This is safe to be called multiple times and by library authors. - /// Only a single will be created for a given - /// . + /// + /// Guidance: Prefer using the AddElasticOpenTelemetry method on the + /// or rather than calling this method on the directly. + /// + /// + /// The WithElasticTracing methods are primarily intended for advanced scenarios, non-host-based applications or when developing + /// libraries that need to customize the OpenTelemetry configuration. + /// + /// + /// Recommendation:When not using AddElasticOpenTelemetry, it is strongly recommended to use the + /// overload instead. This accepts + /// an configuration callback and ensures any customisations are applied before + /// the OTLP exporter is added. + /// /// + /// /// Thrown when the is null. - /// The supplied for chaining calls. + /// + /// + /// An instance of that can be used to further configure the OpenTelemetry SDK. + /// + /// + /// Warning: Any further configuration applied via the returned by calling WithTracing will + /// be applied after the OTLP exporter has been added to the . + /// + /// + /// Recommendation:It is strongly recommended to use the overload + /// to customise the builder instead. This accepts an configuration callback and ensures any + /// customisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the option + /// can be used to prevent automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// public static IOpenTelemetryBuilder WithElasticTracing(this IOpenTelemetryBuilder builder) { #if NET @@ -343,76 +658,121 @@ public static IOpenTelemetryBuilder WithElasticTracing(this IOpenTelemetryBuilde return builder.WithTracing(m => m.WithElasticDefaults()); } - /// - /// - /// - /// + /// + /// + /// + /// Guidance: Prefer using the AddElasticOpenTelemetry method on the + /// or rather than calling this method on the directly. + /// + /// + /// The WithElasticTracing methods are primarily intended for advanced scenarios, non-host-based applications or when developing + /// libraries that need to customize the OpenTelemetry configuration. + /// + /// /// - /// An instance from which to load the Elastic Distribution of - /// OpenTelemetry (EDOT) .NET options. + /// + /// An used to further configure the . + /// This action is invoked after EDOT .NET defaults + /// have been applied, but before the OTLP exporter is added. This ensures that any custom processors run before the exporter. + /// /// Thrown when the is null. - /// Thrown when the is null. - /// - public static IOpenTelemetryBuilder WithElasticTracing(this IOpenTelemetryBuilder builder, IConfiguration configuration) + /// Thrown when the is null. + /// + /// + /// An instance of that can be used to further configure the + /// OpenTelemetry SDK. + /// + /// + /// Warning: Any further configuration applied via the returned by calling WithTracing will + /// be applied after the OTLP exporter has been added to the . Use the + /// parameter to apply customisations before the exporter is added. + /// + /// + public static IOpenTelemetryBuilder WithElasticTracing(this IOpenTelemetryBuilder builder, Action configure) { #if NET ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(configuration); + ArgumentNullException.ThrowIfNull(configure); #else if (builder is null) throw new ArgumentNullException(nameof(builder)); - if (configuration is null) - throw new ArgumentNullException(nameof(configuration)); + if (configure is null) + throw new ArgumentNullException(nameof(configure)); #endif - return builder.WithTracing(tpb => tpb.WithElasticDefaults(configuration, builder.Services)); + var builderOptions = new BuilderOptions { UserProvidedConfigureBuilder = configure }; + return builder.WithTracing(tpb => tpb.WithElasticDefaultsCore(null, null, builder.Services, builderOptions)); } /// - /// Adds tracing services into the builder using Elastic defaults. + /// Adds tracing services into the using Elastic Distribution of + /// OpenTelemetry (EDOT) .NET defaults. /// - /// + /// + /// + /// Guidance: Prefer using the AddElasticOpenTelemetry method on the + /// or rather than calling this method on the directly. + /// + /// + /// The WithElasticTracing methods are primarily intended for advanced scenarios, non-host-based applications or when developing + /// libraries that need to customize the OpenTelemetry configuration. + /// + /// + /// Recommendation:When not using AddElasticOpenTelemetry, it is strongly recommended to use the + /// overload instead. This accepts + /// an configuration callback and ensures any customisations are applied before + /// the OTLP exporter is added. + /// + /// /// - /// configuration callback. + /// /// Thrown when the is null. - /// Thrown when the is null. - /// - public static IOpenTelemetryBuilder WithElasticTracing(this IOpenTelemetryBuilder builder, Action configure) + /// Thrown when the is null. + /// + /// + /// An instance of that can be used to further configure the + /// OpenTelemetry SDK. + /// + /// + /// Warning: Any further configuration applied via the returned by calling WithTracing will + /// be applied after the OTLP exporter has been added to the . + /// + /// + /// Recommendation:It is strongly recommended to use the overload + /// to customise the builder instead. This accepts an configuration callback and ensures any + /// customisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the option + /// can be used to prevent automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// + public static IOpenTelemetryBuilder WithElasticTracing(this IOpenTelemetryBuilder builder, IConfiguration configuration) { #if NET ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(configure); + ArgumentNullException.ThrowIfNull(configuration); #else if (builder is null) throw new ArgumentNullException(nameof(builder)); - if (configure is null) - throw new ArgumentNullException(nameof(configure)); + if (configuration is null) + throw new ArgumentNullException(nameof(configuration)); #endif - return builder.WithTracing(tpb => - { - tpb.WithElasticDefaults(); - configure?.Invoke(tpb); - }); + return builder.WithTracing(tpb => tpb.WithElasticDefaultsCore(new(configuration), null, builder.Services, default)); } - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// + /// + /// + /// + /// + /// /// Thrown when the is null. /// Thrown when the is null. /// Thrown when the is null. - /// + /// public static IOpenTelemetryBuilder WithElasticTracing(this IOpenTelemetryBuilder builder, IConfiguration configuration, Action configure) { @@ -431,21 +791,66 @@ public static IOpenTelemetryBuilder WithElasticTracing(this IOpenTelemetryBuilde throw new ArgumentNullException(nameof(configure)); #endif - return builder.WithTracing(tpb => + var builderOptions = new BuilderOptions { UserProvidedConfigureBuilder = configure }; + return builder.WithTracing(tpb => tpb.WithElasticDefaultsCore(new(configuration), null, builder.Services, builderOptions)); + } + + /// + /// Internal core implementation for the various overloads of WithElasticDefaults. + /// May also be called by other internal methods that need to apply EDOT .NET defaults. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static IOpenTelemetryBuilder WithElasticDefaultsCore( + this IOpenTelemetryBuilder builder, + CompositeElasticOpenTelemetryOptions options, + in BuilderOptions builderOptions) + { + var callCount = Interlocked.Increment(ref WithElasticDefaultsCallCount); + + // FullName may return null so we fallback to Name when required. + var providerBuilderName = builder.GetType().FullName ?? builder.GetType().Name; + + var logger = SignalBuilder.GetLogger(builder, null, options, null); + + if (callCount > 1) + { + logger.LogMultipleWithElasticDefaultsCallsWarning(callCount, providerBuilderName); + } + + // If for some reason `WithElasticDefaults` is invoked with the `Signals` option set to + // none, we skip bootstrapping entirely. We log this as a warning since it's best to + // simply not call `WithElasticDefaults` in this scenario and it may indicate a misconfiguration. + if (options.Signals == Signals.None) { - tpb.WithElasticDefaults(configuration, builder.Services); - configure?.Invoke(tpb); - }); + logger.LogSkippingBootstrapWarning(); + return builder; + } + + SignalBuilder.WithElasticDefaults(builder, options, null, builder.Services, builderOptions, ConfigureBuilder); + + return builder; } - private static void ConfigureBuilder(IOpenTelemetryBuilder builder, BuilderState builderState, IServiceCollection? services) + private static void ConfigureBuilder(BuilderContext builderContext) { - var components = builderState.Components; - var options = builderState.Components.Options; + var builder = builderContext.Builder; + var components = builderContext.BuilderState.Components; + var options = builderContext.BuilderState.Components.Options; + var builderState = builderContext.BuilderState; + var services = builderContext.Services; + // Configure tracing, if the signal is enabled. if (options.Signals.HasFlagFast(Signals.Traces)) { - builder.WithTracing(tpb => tpb.WithElasticDefaults(components, builder.Services)); + // When the user has provided their own configuration callback for the IOpenTelemetryBuilder + // we don't want WithElasticDefaults for the TracerProviderBuilder to add the OTLP exporter so + // we defer it until after this method runs the user-provided callback. + var builderOptions = new BuilderOptions + { + DeferAddOtlpExporter = builderContext.BuilderOptions.UserProvidedConfigureBuilder is not null + }; + + builder.WithTracing(tpb => tpb.WithElasticDefaultsCore(components.Options, components, services, builderOptions)); } else { @@ -455,7 +860,15 @@ private static void ConfigureBuilder(IOpenTelemetryBuilder builder, BuilderState if (options.Signals.HasFlagFast(Signals.Metrics)) { - builder.WithMetrics(mpb => mpb.WithElasticDefaults(components, builder.Services)); + // When the user has provided their own configuration callback for the IOpenTelemetryBuilder + // we don't want WithElasticDefaults for the MeterProviderBuilder to add the OTLP exporter so + // we defer it until after this method runs the user-provided callback. + var builderOptions = new BuilderOptions + { + DeferAddOtlpExporter = builderContext.BuilderOptions.UserProvidedConfigureBuilder is not null + }; + + builder.WithMetrics(mpb => mpb.WithElasticDefaultsCore(components.Options, components, builder.Services, builderOptions)); } else { @@ -465,12 +878,55 @@ private static void ConfigureBuilder(IOpenTelemetryBuilder builder, BuilderState if (options.Signals.HasFlagFast(Signals.Logs)) { - builder.WithLogging(lpb => lpb.WithElasticDefaults(components, builder.Services)); + // When the user has provided their own configuration callback for the IOpenTelemetryBuilder + // we don't want WithElasticDefaults for the MeterProviderBuilder to add the OTLP exporter so + // we defer it until after this method runs the user-provided callback. + var builderOptions = new BuilderOptions + { + DeferAddOtlpExporter = builderContext.BuilderOptions.UserProvidedConfigureBuilder is not null + }; + + builder.WithLogging(lpb => lpb.WithElasticDefaultsCore(components.Options, components, builder.Services, builderOptions)); } else { components.Logger.LogSignalDisabled(Signals.Logs.ToString().ToLower(), nameof(IOpenTelemetryBuilder), builderState.InstanceIdentifier); } + + // FullName may return null so we fallback to Name when required. + var builderTypeName = builder.GetType().FullName ?? builder.GetType().Name; + + if (builderContext.BuilderOptions.UserProvidedConfigureBuilder is not null) + { + // Run the user-provided builder configuration after the EDOT defaults have been applied. + builderContext.BuilderOptions.UserProvidedConfigureBuilder(builder); + components.Logger.LogInvokedConfigureAction(builderTypeName, builderState.InstanceIdentifier); + + // Add OTLP exporters (if enabled) after user-provided configuration + HandleOtlpExporter(Signals.Traces, b => b.WithTracing(tpb => tpb.AddOtlpExporter())); + HandleOtlpExporter(Signals.Metrics, b => b.WithMetrics(tpb => tpb.AddOtlpExporter())); + HandleOtlpExporter(Signals.Logs, b => b.WithLogging(tpb => tpb.AddOtlpExporter())); + } + + void HandleOtlpExporter(Signals signal, Action configure) + { + // If the signal is enabled and the user provided their own configuration callback, + // we need to ensure the OTLP exporter is added after running the user-provided callback. + if (options.Signals.HasFlagFast(signal)) + { + if (builderState.Components.Options.SkipOtlpExporter) + { + components.Logger.LogSkippedOtlpExporter(signal.ToString(), builderTypeName, builderState.InstanceIdentifier); + } + else + { + configure(builder); + components.Logger.LogAddedOtlpExporter(signal.ToString(), builderTypeName, builderState.InstanceIdentifier); + } + } + + // NOTE: We don't log signal disabled here since that will already have been logged above. + } } } diff --git a/src/Elastic.OpenTelemetry/Extensions/ServiceCollectionExtensions.cs b/src/Elastic.OpenTelemetry/Extensions/ServiceCollectionExtensions.cs index 133f67fb..4a725d91 100644 --- a/src/Elastic.OpenTelemetry/Extensions/ServiceCollectionExtensions.cs +++ b/src/Elastic.OpenTelemetry/Extensions/ServiceCollectionExtensions.cs @@ -5,10 +5,14 @@ using System.Runtime.CompilerServices; using Elastic.OpenTelemetry; using Elastic.OpenTelemetry.Configuration; +using Elastic.OpenTelemetry.Core; +using Elastic.OpenTelemetry.Diagnostics; +using Elastic.OpenTelemetry.Exporters; using Elastic.OpenTelemetry.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using OpenTelemetry; +using OpenTelemetry.Exporter; // ReSharper disable once CheckNamespace #pragma warning disable IDE0130 // Namespace does not match folder structure @@ -22,14 +26,31 @@ namespace Microsoft.Extensions.DependencyInjection; public static class ServiceCollectionExtensions { /// - /// Registers the OpenTelemetry SDK services with the provided to include the - /// OpenTelemetry SDK in the application, configured with Elastic Distribution of OpenTelemetry (EDOT) .NET defaults. + /// Registers the OpenTelemetry SDK with the application, configured with Elastic Distribution of OpenTelemetry (EDOT) .NET + /// defaults for traces, + /// metrics and logs. /// /// The for adding services. /// Thrown when the is null. /// - /// An instance of that can be used to further configure the - /// OpenTelemetry SDK. + /// + /// An instance of that can be used to further configure the + /// OpenTelemetry SDK. + /// + /// + /// Warning: Any further configuration applied via the returned will + /// be applied after the OTLP exporter has been added. + /// + /// + /// Recommendation:It is strongly recommended to use the overload + /// to customise the builder instead. This accepts an configuration action and ensures any + /// customisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the + /// option + /// can be used to prevent automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// /// public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollection services) { @@ -40,7 +61,54 @@ public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollect throw new ArgumentNullException(nameof(services)); #endif - return AddElasticOpenTelemetryCore(services, CompositeElasticOpenTelemetryOptions.DefaultOptions); + return AddElasticOpenTelemetryCore(services, CompositeElasticOpenTelemetryOptions.DefaultOptions, default); + } + + /// + /// + /// + /// The for adding services. + /// A configuration action used to further customise + /// the OpenTelemetry SDK after Elastic Distribution of OpenTelemetry (EDOT) .NET defaults have been applied and + /// before the OTLP exporter is added. + /// Thrown when the is null. + /// Thrown when the is null. + /// + /// + /// An instance of that can be used to further configure the + /// OpenTelemetry SDK. + /// + /// + /// Warning: Any further configuration applied via the returned will + /// be applied after the OTLP exporter has been added. + /// + /// + /// Recommendation:It is strongly recommended to use the parameter to customise + /// the builder instead. This accepts an configuration callback and ensures any + /// customisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the option + /// can be used to prevent automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// + public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollection services, Action configure) + { + // TODO - Breaking change: In a future major release, rename this parameter to 'configureBuilder' for clarity and consistency. + // This would be a source breaking change only but we'll reserve it for a major version to avoid disrupting consumers. +#if NET + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configure); +#else + if (services is null) + throw new ArgumentNullException(nameof(services)); + + if (configure is null) + throw new ArgumentNullException(nameof(configure)); +#endif + + var builderOptions = new BuilderOptions { UserProvidedConfigureBuilder = configure }; + return AddElasticOpenTelemetryCore(services, CompositeElasticOpenTelemetryOptions.DefaultOptions, builderOptions); } /// @@ -52,7 +120,27 @@ public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollect /// /// Thrown when the is null. /// Thrown when the is null. - /// + /// + /// + /// An instance of that can be used to further configure the + /// OpenTelemetry SDK. + /// + /// + /// Warning: Any further configuration applied via the returned will + /// be applied after the OTLP exporter has been added. + /// + /// + /// Recommendation:It is strongly recommended to use the + /// overload + /// to customise the builder instead. This accepts an configuration action and ensures any + /// customisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the + /// option + /// can be used to prevent automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollection services, ElasticOpenTelemetryOptions options) { #if NET @@ -66,7 +154,62 @@ public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollect throw new ArgumentNullException(nameof(options)); #endif - return AddElasticOpenTelemetryCore(services, new(options)); + return AddElasticOpenTelemetryCore(services, new(options), default); + } + + /// + /// + /// + /// + /// + /// + /// The used to configure the Elastic Distribution of OpenTelemetry (EDOT) .NET. + /// + /// Thrown when the is null. + /// Thrown when the is null. + /// Thrown when the is null. + /// + /// + /// An instance of that can be used to further configure the + /// OpenTelemetry SDK. + /// + /// + /// Warning: Any further configuration applied via the returned will + /// be applied after the OTLP exporter has been added. + /// + /// + /// Recommendation:It is strongly recommended to use the parameter to customise + /// the builder instead. This accepts an configuration callback and ensures any + /// customisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the + /// option + /// can be used to prevent automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// + public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollection services, + ElasticOpenTelemetryOptions options, Action configure) + { + // TODO - Breaking change: In a future major release, rename this parameter to 'configureBuilder' for clarity and consistency. + // This would be a source breaking change only but we'll reserve it for a major version to avoid disrupting consumers. +#if NET + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(configure); +#else + if (services is null) + throw new ArgumentNullException(nameof(services)); + + if (options is null) + throw new ArgumentNullException(nameof(options)); + + if (options is null) + throw new ArgumentNullException(nameof(configure)); +#endif + + var builderOptions = new BuilderOptions { UserProvidedConfigureBuilder = configure }; + return AddElasticOpenTelemetryCore(services, new(options), builderOptions); } /// @@ -78,7 +221,23 @@ public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollect /// /// Thrown when the is null. /// Thrown when the is null. - /// + /// + /// An instance of that can be used to further configure the + /// OpenTelemetry SDK. + /// + /// Warning: Any further configuration applied via the returned will + /// be applied after the OTLP exporter has been added. + /// + /// + /// Recommendation: When not using AddElasticOpenTelemetry, it is strongly recommended to use the + /// overload instead. This accepts + /// an configuration callback and ensures anycustomisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the option + /// can be used to prevent automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollection services, IConfiguration configuration) { #if NET @@ -92,29 +251,47 @@ public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollect throw new ArgumentNullException(nameof(configuration)); #endif - return AddElasticOpenTelemetryCore(services, new(configuration)); + return AddElasticOpenTelemetryCore(services, new(configuration), default); } /// - /// + /// /// - /// - /// Configuration is first bound from and then overridden by any options configured on - /// the provided . - /// /// - /// An instance from which to attempt binding of configuration values. - /// used to configure the Elastic Distribution of OpenTelemetry (EDOT) .NET. + /// + /// /// Thrown when the is null. /// Thrown when the is null. - /// Thrown when the is null. - /// - internal static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollection services, IConfiguration configuration, ElasticOpenTelemetryOptions options) + /// Thrown when the is null. + /// + /// + /// An instance of that can be used to further configure the + /// OpenTelemetry SDK. + /// + /// + /// Warning: Any further configuration applied via the returned will + /// be applied after the OTLP exporter has been added. + /// + /// + /// Recommendation:It is strongly recommended to use the parameter to customise + /// the builder instead. This accepts an configuration callback and ensures any + /// customisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the + /// option + /// can be used to prevent automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// + public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollection services, + IConfiguration configuration, Action configure) { + // TODO - Breaking change: In a future major release, rename this parameter to 'configureBuilder' for clarity and consistency. + // This would be a source breaking change only but we'll reserve it for a major version to avoid disrupting consumers. #if NET ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(configuration); - ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(configure); #else if (services is null) throw new ArgumentNullException(nameof(services)); @@ -122,19 +299,28 @@ internal static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceColle if (configuration is null) throw new ArgumentNullException(nameof(configuration)); - if (options is null) - throw new ArgumentNullException(nameof(options)); + if (configure is null) + throw new ArgumentNullException(nameof(configure)); #endif - return AddElasticOpenTelemetryCore(services, new(configuration, options)); + var builderOptions = new BuilderOptions { UserProvidedConfigureBuilder = configure }; + return AddElasticOpenTelemetryCore(services, new(configuration), builderOptions); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static IOpenTelemetryBuilder AddElasticOpenTelemetryCore(IServiceCollection services, CompositeElasticOpenTelemetryOptions options) + internal static IOpenTelemetryBuilder AddElasticOpenTelemetryCore( + this IServiceCollection services, + CompositeElasticOpenTelemetryOptions options, + in BuilderOptions builderOptions) { - var builder = services.AddOpenTelemetry().WithElasticDefaultsCore(options); + var logger = DeferredLogger.GetOrCreate(options); + + services.Configure(OtlpExporterDefaults.OtlpExporterOptions); + logger.LogConfiguredOtlpExporterOptions(); + + var builder = services.AddOpenTelemetry().WithElasticDefaultsCore(options, builderOptions); - if (!services.Any((ServiceDescriptor d) => d.ServiceType == typeof(IHostedService) && d.ImplementationType == typeof(ElasticOpenTelemetryService))) + if (!services.Any(d => d.ServiceType == typeof(IHostedService) && d.ImplementationType == typeof(ElasticOpenTelemetryService))) { services.Insert(0, ServiceDescriptor.Singleton()); } diff --git a/src/Elastic.OpenTelemetry/Extensions/TracerProviderBuilderExtensions.cs b/src/Elastic.OpenTelemetry/Extensions/TracerProviderBuilderExtensions.cs index 58037d8a..bdb1d559 100644 --- a/src/Elastic.OpenTelemetry/Extensions/TracerProviderBuilderExtensions.cs +++ b/src/Elastic.OpenTelemetry/Extensions/TracerProviderBuilderExtensions.cs @@ -14,6 +14,7 @@ using Elastic.OpenTelemetry.Instrumentation; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using OpenTelemetry.Exporter; using OpenTelemetry.Resources; @@ -26,43 +27,177 @@ namespace OpenTelemetry; /// /// Provides extension methods on the used to register -/// the Elastic Distribution of OpenTelemetry (EDOT) .NET defaults. +/// the Elastic Distribution of OpenTelemetry (EDOT) .NET defaults. /// public static class TracerProviderBuilderExtensions { private static int WithElasticDefaultsCallCount; /// - /// Use Elastic Distribution of OpenTelemetry .NET defaults for . + /// Apply Elastic Distribution of OpenTelemetry (EDOT) .NET + /// defaults + /// to the . /// /// - /// This is not neccesary if - /// has been called previously as that automatically adds the . + /// + /// This method is intended for advanced scenarios where per signal Elastic Distribution of OpenTelemetry (EDOT) .NET + /// defaults are being enabled. In most scenarios, prefer the AddElasticOpenTelemetry method on + /// or . + /// + /// + /// Warning: Avoid calling this method when using the AddElasticOpenTelemetry method on + /// or . In those scenarios, the OpenTelemetry SDK will have already been configured with + /// defaults, including the OTLP exporter. Calling this method again may lead to unexpected behavior. + /// + /// + /// When using AddElasticOpenTelemetry, use the overload on the + /// or which accepts a configuration + /// to further customize the SDK setup. This action is invoked after EDOT .NET defaults have been applied, but + /// before the OTLP exporter is added. + /// + /// + /// Note: If you wish to customise the after the EDOT .NET defaults have been applied, + /// use the overload that accepts a + /// configuration action. + /// /// /// The to configure. /// Thrown when the is null. - /// The for chaining configuration. + /// + /// An instance of that can be used to further configure the trace signal. + /// + /// Warning: Any further configuration applied via the returned will + /// be applied after the OTLP exporter has been added. + /// + /// + /// Recommendation:It is strongly recommended to use the + /// overload instead. This accepts an configuration callback and ensures any + /// customisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the + /// option + /// automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// public static TracerProviderBuilder WithElasticDefaults(this TracerProviderBuilder builder) { #if NET - ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(builder); #else if (builder is null) throw new ArgumentNullException(nameof(builder)); #endif - return WithElasticDefaultsCore(builder, null, null, null); + return WithElasticDefaultsCore(builder, null, null, null, default); } + /// + /// + /// + /// This method is intended for advanced scenarios where per signal Elastic Distribution of OpenTelemetry (EDOT) .NET + /// defaults are being enabled. In most scenarios, prefer the AddElasticOpenTelemetry method on + /// or . + /// + /// + /// Warning: Avoid calling this method when using the AddElasticOpenTelemetry method on + /// or . In those scenarios, the OpenTelemetry SDK will have already been configured with + /// defaults, including the OTLP exporter. Calling this method again may lead to unexpected behavior. + /// + /// + /// When using AddElasticOpenTelemetry, use the overload on the + /// or which accepts a configuration + /// to further customize the SDK setup. For example, + /// This ensures that your configuration is invoked after EDOT .NET defaults have been applied, but before the OTLP exporter is added. + /// + /// + /// Note: If you wish to customise the after the EDOT .NET defaults have been applied, + /// use the action. + /// + /// + /// + /// + /// An used to further configure the . + /// This action is invoked after EDOT .NET defaults + /// have been applied, but before the OTLP exporter is added. This ensures that any custom processors run before the exporter. + /// + /// Thrown when the is null. + /// Thrown when the is null. + /// + /// + /// An instance of that can be used to further configure the trace signal. + /// + /// + /// Warning: Any further configuration applied via the returned will + /// be applied after the OTLP exporter has been added. + /// + /// + /// Recommendation:It is strongly recommended to use the action to customise + /// the after EDOT .NET defaults are applied and before the OTLP exporter is added. + /// + /// + public static TracerProviderBuilder WithElasticDefaults(this TracerProviderBuilder builder, Action configureBuilder) + { +#if NET + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configureBuilder); +#else + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + + if (configureBuilder is null) + throw new ArgumentNullException(nameof(configureBuilder)); +#endif + var builderOptions = new BuilderOptions { UserProvidedConfigureBuilder = configureBuilder }; + return WithElasticDefaultsCore(builder, null, null, null, builderOptions); + } - /// - /// - /// + /// + /// + /// + /// This method is intended for advanced scenarios where per signal Elastic Distribution of OpenTelemetry (EDOT) .NET + /// defaults are being enabled. In most scenarios, prefer the AddElasticOpenTelemetry method on + /// or . + /// + /// + /// Warning: Avoid calling this method when using the AddElasticOpenTelemetry method on + /// or . In those scenarios, the OpenTelemetry SDK will have already been configured with + /// defaults, including the OTLP exporter. Calling this method again may lead to unexpected behavior. + /// + /// + /// When using AddElasticOpenTelemetry, use the overload on the + /// or which accepts a configuration + /// to further customize the SDK setup. This action is invoked after EDOT .NET defaults have been applied, but + /// before the OTLP exporter is added. + /// + /// + /// Note: If you wish to customise the after the EDOT .NET defaults have been applied, + /// use the overload that accepts a + /// configuration action. + /// + /// /// /// used to configure the Elastic Distribution of OpenTelemetry (EDOT) .NET. /// Thrown when the is null. /// Thrown when the is null. - /// + /// + /// An instance of that can be used to further configure the trace signal. + /// + /// Warning: Any further configuration applied via the returned will + /// be applied after the OTLP exporter has been added. + /// + /// + /// Recommendation:It is strongly recommended to use the + /// + /// overload instead. This accepts an configuration action and ensures any + /// customisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the + /// option + /// automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// public static TracerProviderBuilder WithElasticDefaults(this TracerProviderBuilder builder, ElasticOpenTelemetryOptions options) { #if NET @@ -76,17 +211,86 @@ public static TracerProviderBuilder WithElasticDefaults(this TracerProviderBuild throw new ArgumentNullException(nameof(options)); #endif - return WithElasticDefaultsCore(builder, new(options), null, null); + return WithElasticDefaultsCore(builder, new(options), null, null, default); } - /// - /// - /// + /// + /// /// - /// An instance from which to load the OpenTelemetry SDK options. + /// + /// + /// Thrown when the is null. + /// Thrown when the is null. + /// Thrown when the is null. + /// + public static TracerProviderBuilder WithElasticDefaults(this TracerProviderBuilder builder, + ElasticOpenTelemetryOptions options, Action configureBuilder) + { +#if NET + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(configureBuilder); +#else + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + + if (options is null) + throw new ArgumentNullException(nameof(options)); + + if (configureBuilder is null) + throw new ArgumentNullException(nameof(configureBuilder)); +#endif + + var builderOptions = new BuilderOptions { UserProvidedConfigureBuilder = configureBuilder }; + return WithElasticDefaultsCore(builder, new(options), null, null, builderOptions); + } + + /// + /// + /// + /// This method is intended for advanced scenarios where per signal Elastic Distribution of OpenTelemetry (EDOT) .NET + /// defaults are being enabled. In most scenarios, prefer the AddElasticOpenTelemetry method on + /// or . + /// + /// + /// Warning: Avoid calling this method when using the AddElasticOpenTelemetry method on + /// or . In those scenarios, the OpenTelemetry SDK will have already been configured with + /// defaults, including the OTLP exporter. Calling this method again may lead to unexpected behavior. + /// + /// + /// When using AddElasticOpenTelemetry, use the overload on the + /// or which accepts a configuration + /// to further customize the SDK setup. This action is invoked after EDOT .NET defaults have been applied, but + /// before the OTLP exporter is added. + /// + /// + /// Note: If you wish to customise the after the EDOT .NET defaults have been applied, + /// use the overload that accepts a + /// configuration action. + /// + /// + /// + /// An instance from which to bind the OpenTelemetry SDK options. /// Thrown when the is null. /// Thrown when the is null. - /// + /// + /// An instance of that can be used to further configure the trace signal. + /// + /// Warning: Any further configuration applied via the returned will + /// be applied after the OTLP exporter has been added. + /// + /// + /// Recommendation:It is strongly recommended to use the + /// + /// overload instead. This accepts an configuration action and ensures any + /// customisations are applied before the OTLP exporter is added. + /// + /// + /// Alternatively, the + /// option + /// automatic addition of the OTLP exporter, allowing you to add it manually at a later stage. + /// + /// public static TracerProviderBuilder WithElasticDefaults(this TracerProviderBuilder builder, IConfiguration configuration) { #if NET @@ -99,27 +303,46 @@ public static TracerProviderBuilder WithElasticDefaults(this TracerProviderBuild if (configuration is null) throw new ArgumentNullException(nameof(configuration)); #endif - return WithElasticDefaultsCore(builder, new(configuration), null, null); + return WithElasticDefaultsCore(builder, new(configuration), null, null, default); } - internal static TracerProviderBuilder WithElasticDefaults( - this TracerProviderBuilder builder, - IConfiguration configuration, - IServiceCollection serviceCollection) => - WithElasticDefaultsCore(builder, new(configuration), null, serviceCollection); + /// + /// + /// + /// + /// + /// Thrown when the is null. + /// Thrown when the is null. + /// Thrown when the is null. + /// + public static TracerProviderBuilder WithElasticDefaults(this TracerProviderBuilder builder, + IConfiguration configuration, Action configureBuilder) + { +#if NET + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configuration); + ArgumentNullException.ThrowIfNull(configureBuilder); +#else + if (builder is null) + throw new ArgumentNullException(nameof(builder)); - internal static TracerProviderBuilder WithElasticDefaults( - this TracerProviderBuilder builder, - ElasticOpenTelemetryComponents components, - IServiceCollection? services) => - WithElasticDefaultsCore(builder, components.Options, components, services); + if (configuration is null) + throw new ArgumentNullException(nameof(configuration)); + + if (configureBuilder is null) + throw new ArgumentNullException(nameof(configureBuilder)); +#endif + var builderOptions = new BuilderOptions { UserProvidedConfigureBuilder = configureBuilder }; + return WithElasticDefaultsCore(builder, new(configuration), null, null, builderOptions); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static TracerProviderBuilder WithElasticDefaultsCore( - TracerProviderBuilder builder, + internal static TracerProviderBuilder WithElasticDefaultsCore( + this TracerProviderBuilder builder, CompositeElasticOpenTelemetryOptions? options, ElasticOpenTelemetryComponents? components, - IServiceCollection? services) + IServiceCollection? services, + in BuilderOptions builderOptions) { var logger = SignalBuilder.GetLogger(builder, components, options, null); @@ -134,25 +357,34 @@ private static TracerProviderBuilder WithElasticDefaultsCore( logger.LogWithElasticDefaultsCallCount(callCount, nameof(TracerProviderBuilder)); } - return SignalBuilder.WithElasticDefaults(builder, Signals.Traces, options, components, services, ConfigureBuilder); + return SignalBuilder.WithElasticDefaults(builder, options, components, services, builderOptions, ConfigureBuilder); } [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "The call to AssemblyScanning.AddInstrumentationViaReflection` " + "is guarded by a RuntimeFeature.IsDynamicCodeSupported` check and, therefore, this method is safe to call in AoT scenarios.")] - private static void ConfigureBuilder(TracerProviderBuilder builder, BuilderState builderState, IServiceCollection? services) + private static void ConfigureBuilder(BuilderContext builderContext) { - const string tracerProviderBuilderName = nameof(TracerProviderBuilder); - + var builder = builderContext.Builder; + var builderState = builderContext.BuilderState; var components = builderState.Components; var logger = components.Logger; + var services = builderContext.Services; + + // FullName may return null so we fallback to Name when required. + var tracerProviderBuilderName = builder.GetType().FullName ?? builder.GetType().Name; logger.LogConfiguringBuilder(tracerProviderBuilderName, builderState.InstanceIdentifier); builder.ConfigureResource(r => r.WithElasticDefaults(builderState, services)); + // When services is not null here, the options will have already been configured by the calling code so + // we don't need to do it again. if (services is null) + { builder.ConfigureServices(sc => sc.Configure(OtlpExporterDefaults.OtlpExporterOptions)); + logger.LogConfiguredOtlpExporterOptions(); + } #if NET9_0_OR_GREATER // .NET 9 introduced semantic convention compatible instrumentation in System.Net.Http so it's recommended to no longer @@ -190,16 +422,32 @@ private static void ConfigureBuilder(TracerProviderBuilder builder, BuilderState TracerProvderBuilderExtensions.AddElasticProcessorsCore(builder, builderState, null, services); - if (components.Options.SkipOtlpExporter) + var userProvidedConfigureBuilder = builderContext.BuilderOptions.UserProvidedConfigureBuilder; + if (userProvidedConfigureBuilder is not null) + { + userProvidedConfigureBuilder(builder); + logger.LogInvokedConfigureAction(tracerProviderBuilderName, builderState.InstanceIdentifier); + } + + // If a callee will register the OTLP exporter later, we skip adding it here. + if (builderContext.BuilderOptions.DeferAddOtlpExporter) { - logger.LogSkippingOtlpExporter(nameof(Signals.Traces), nameof(TracerProviderBuilder), builderState.InstanceIdentifier); + logger.LogDeferredOtlpExporter(tracerProviderBuilderName, builderState.InstanceIdentifier); } else { - builder.AddOtlpExporter(); + if (components.Options.SkipOtlpExporter) + { + logger.LogSkippedOtlpExporter(nameof(Signals.Traces), tracerProviderBuilderName, builderState.InstanceIdentifier); + } + else + { + builder.AddOtlpExporter(); + logger.LogAddedOtlpExporter(nameof(Signals.Traces), tracerProviderBuilderName, builderState.InstanceIdentifier); + } } - logger.LogConfiguredSignalProvider(nameof(Signals.Traces), nameof(TracerProviderBuilder), builderState.InstanceIdentifier); + logger.LogConfiguredSignalProvider(nameof(Signals.Traces), tracerProviderBuilderName, builderState.InstanceIdentifier); } [UnconditionalSuppressMessage("DynamicCode", "IL2026", Justification = "The call to this method is guarded by a RuntimeFeature.IsDynamicCodeSupported` " + diff --git a/tests/Elastic.OpenTelemetry.EndToEndTests/DistributedFixture/ApmUIBrowserContext.cs b/tests/Elastic.OpenTelemetry.EndToEndTests/DistributedFixture/ApmUIBrowserContext.cs index 299e7ad2..3a22c9f2 100644 --- a/tests/Elastic.OpenTelemetry.EndToEndTests/DistributedFixture/ApmUIBrowserContext.cs +++ b/tests/Elastic.OpenTelemetry.EndToEndTests/DistributedFixture/ApmUIBrowserContext.cs @@ -2,7 +2,6 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using System.IO.Compression; using System.Runtime.CompilerServices; using Microsoft.Extensions.Configuration; using Microsoft.Playwright; diff --git a/tests/Elastic.OpenTelemetry.EndToEndTests/DistributedFixture/DistributedApplicationFixture.cs b/tests/Elastic.OpenTelemetry.EndToEndTests/DistributedFixture/DistributedApplicationFixture.cs index 4cf85006..bad61def 100644 --- a/tests/Elastic.OpenTelemetry.EndToEndTests/DistributedFixture/DistributedApplicationFixture.cs +++ b/tests/Elastic.OpenTelemetry.EndToEndTests/DistributedFixture/DistributedApplicationFixture.cs @@ -6,7 +6,6 @@ using System.Security.Cryptography; using System.Text; using Microsoft.Extensions.Configuration; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Nullean.Xunit.Partitions.Sdk; using Xunit.Abstractions; diff --git a/tests/Elastic.OpenTelemetry.Tests/Extensions/ServiceCollectionTests.cs b/tests/Elastic.OpenTelemetry.Tests/Extensions/ServiceCollectionTests.cs index 66eacfef..3ee1a60f 100644 --- a/tests/Elastic.OpenTelemetry.Tests/Extensions/ServiceCollectionTests.cs +++ b/tests/Elastic.OpenTelemetry.Tests/Extensions/ServiceCollectionTests.cs @@ -157,7 +157,7 @@ public void AddElasticOpenTelemetry_AppliesConfigAndOptions_InExpectedOrder() .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) .Build(); - serviceCollection.AddElasticOpenTelemetry(config, options); + serviceCollection.AddElasticOpenTelemetryCore(new(config, options), default); using var serviceProvider = serviceCollection.BuildServiceProvider(); diff --git a/tests/Elastic.OpenTelemetry.Tests/TestLogger.cs b/tests/Elastic.OpenTelemetry.Tests/TestLogger.cs index 6145958e..675156ff 100644 --- a/tests/Elastic.OpenTelemetry.Tests/TestLogger.cs +++ b/tests/Elastic.OpenTelemetry.Tests/TestLogger.cs @@ -22,9 +22,17 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except { var message = LogFormatter.Format(logLevel, eventId, state, exception, formatter); _messages.Add(message); - _testOutputHelper.WriteLine(message); - if (exception != null) - _testOutputHelper.WriteLine(exception.ToString()); + + try + { + _testOutputHelper.WriteLine(message); + if (exception != null) + _testOutputHelper.WriteLine(exception.ToString()); + } + catch + { + // Swallow exceptions from the test output helper + } } private class NoopDisposable : IDisposable