diff --git a/.github/workflows/bootstrap/action.yml b/.github/workflows/bootstrap/action.yml index a70b672..6ceef31 100644 --- a/.github/workflows/bootstrap/action.yml +++ b/.github/workflows/bootstrap/action.yml @@ -38,6 +38,9 @@ runs: uses: actions/setup-dotnet@v4 with: global-json-file: ./global.json + # 7.x is required for the dotnet-project-licenses tool. + dotnet-version: | + 7.x - id: dotnet shell: bash diff --git a/.github/workflows/ci-docs.yml b/.github/workflows/ci-docs.yml index 631600c..cbd8535 100644 --- a/.github/workflows/ci-docs.yml +++ b/.github/workflows/ci-docs.yml @@ -27,7 +27,13 @@ jobs: - run: 'echo "Not required for docs"' # dummy steps that allow to bypass those mandatory checks for tests - build: + test-linux: + runs-on: ubuntu-latest + steps: + - run: 'echo "Not required for docs"' + + # dummy steps that allow to bypass those mandatory checks for tests + release-build: runs-on: ubuntu-latest steps: - run: 'echo "Not required for docs"' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31da39d..563088b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,7 @@ env: # update ci-docs.yml jobs: test-windows: + name: Windows Tests runs-on: windows-latest steps: - uses: actions/checkout@v4 @@ -36,11 +37,12 @@ jobs: id: bootstrap uses: ./.github/workflows/bootstrap - - name: Test - run: build.bat test --test-suite=skip-e2e + - name: Unit Tests + run: build.bat test --test-suite=unit shell: cmd - build: + test-linux: + name: Linux Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -49,9 +51,19 @@ jobs: id: bootstrap uses: ./.github/workflows/bootstrap - - name: Test - run: ./build.sh test --test-suite=skip-e2e + - name: Unit Tests + run: ./build.sh test --test-suite=unit # For now, we limit to unit tests only, until we have a better way to run integration tests only for autoinstrumentation builds + + # We still run the full release build on pull-requests this ensures packages are validated ahead of time + release-build: + name: Release Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Bootstrap Action Workspace + id: bootstrap + uses: ./.github/workflows/bootstrap - # We still run the full release build on pull-requests this ensures packages are validated ahead of time - name: Release run: ./build.sh release -c \ No newline at end of file diff --git a/build/build.fsproj b/build/build.fsproj index 41c071b..271f9db 100644 --- a/build/build.fsproj +++ b/build/build.fsproj @@ -1,35 +1,37 @@ + net9.0 Exe $(NoWarn);NU1701 false + - - + + - - - - + + + + - + - - + + - - - - - - - + + + + + + + diff --git a/build/scripts/CommandLine.fs b/build/scripts/CommandLine.fs index 4be8fd7..7b0ac5a 100644 --- a/build/scripts/CommandLine.fs +++ b/build/scripts/CommandLine.fs @@ -75,7 +75,6 @@ with | Skip_Dirty_Check -> "Skip the clean checkout check that guards the release/publish targets" | Test_Suite _ -> "Specify the test suite to run, defaults to all" - member this.StepName = match FSharpValue.GetUnionFields(this, typeof) with | case, _ -> case.Name.ToLowerInvariant() diff --git a/examples/Example.AspNetCore.Mvc/Example.AspNetCore.Mvc.csproj b/examples/Example.AspNetCore.Mvc/Example.AspNetCore.Mvc.csproj index 837362b..0ff4122 100644 --- a/examples/Example.AspNetCore.Mvc/Example.AspNetCore.Mvc.csproj +++ b/examples/Example.AspNetCore.Mvc/Example.AspNetCore.Mvc.csproj @@ -1,10 +1,11 @@ - + - net8.0 + net9.0 enable enable Linux + 1efafe93-6112-431d-b30f-786205a20ebe @@ -13,7 +14,7 @@ - + diff --git a/examples/Example.AspNetCore.Mvc/Program.cs b/examples/Example.AspNetCore.Mvc/Program.cs index 035d661..48dc787 100644 --- a/examples/Example.AspNetCore.Mvc/Program.cs +++ b/examples/Example.AspNetCore.Mvc/Program.cs @@ -2,18 +2,29 @@ // 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 Example.AspNetCore.Mvc.Controllers; using OpenTelemetry; +using OpenTelemetry.Resources; var builder = WebApplication.CreateBuilder(args); -builder.AddServiceDefaults(); +using var loggerFactory = LoggerFactory.Create(loggingBuilder => loggingBuilder + .SetMinimumLevel(LogLevel.Trace) + .AddConsole()); + +var logger = loggerFactory.CreateLogger("OpenTelemetry"); // Add services to the container. builder.Services .AddHttpClient() .AddOpenTelemetry() - .WithTracing(t => t.AddSource(HomeController.ActivitySourceName)); + .ConfigureResource(r => r.AddService("MyNewService1")) + .WithElasticDefaults(builder.Configuration); + +builder.Services.AddOpenTelemetry() + .ConfigureResource(r => r.AddService("MyNewService2")) + .WithElasticDefaults(builder.Configuration); + +//OpenTelemetrySdk.Create(b => b.WithElasticDefaults(builder.Configuration)); builder.Services .AddControllersWithViews(); diff --git a/examples/Example.AspNetCore.Mvc/Properties/launchSettings.json b/examples/Example.AspNetCore.Mvc/Properties/launchSettings.json index 8e18550..69ae136 100644 --- a/examples/Example.AspNetCore.Mvc/Properties/launchSettings.json +++ b/examples/Example.AspNetCore.Mvc/Properties/launchSettings.json @@ -9,6 +9,9 @@ "https": { "commandName": "Project", "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, "dotnetRunMessages": true, "applicationUrl": "https://localhost:7295;http://localhost:5247" }, diff --git a/examples/Example.AspNetCore.Mvc/appsettings.json b/examples/Example.AspNetCore.Mvc/appsettings.json index 5033b13..0ff52be 100644 --- a/examples/Example.AspNetCore.Mvc/appsettings.json +++ b/examples/Example.AspNetCore.Mvc/appsettings.json @@ -2,8 +2,7 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning", - "Elastic.OpenTelemetry": "Information" + "Microsoft.AspNetCore": "Warning" }, "OpenTelemetry": { "IncludeFormattedMessage": true, @@ -11,5 +10,11 @@ "ParseStateValues": true } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "Elastic": { + "OpenTelemetry": { + "LogLevel": "Trace", + "LogDirectory": "C:\\Logs\\BrandNewLogs" + } + } } diff --git a/examples/Example.AutoInstrumentation/Example.AutoInstrumentation.csproj b/examples/Example.AutoInstrumentation/Example.AutoInstrumentation.csproj index 2a040bd..e8c41ec 100644 --- a/examples/Example.AutoInstrumentation/Example.AutoInstrumentation.csproj +++ b/examples/Example.AutoInstrumentation/Example.AutoInstrumentation.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 enable enable Linux diff --git a/examples/Example.Console/Example.Console.csproj b/examples/Example.Console/Example.Console.csproj index e1e708b..f0eae0e 100644 --- a/examples/Example.Console/Example.Console.csproj +++ b/examples/Example.Console/Example.Console.csproj @@ -8,7 +8,7 @@ - + diff --git a/examples/Example.Console/Usage.cs b/examples/Example.Console/Usage.cs index 7877e5f..ca01ff3 100644 --- a/examples/Example.Console/Usage.cs +++ b/examples/Example.Console/Usage.cs @@ -3,8 +3,6 @@ // See the LICENSE file in the project root for more information using System.Diagnostics; -using Elastic.OpenTelemetry; -using Elastic.OpenTelemetry.Extensions; using OpenTelemetry; using OpenTelemetry.Resources; using OpenTelemetry.Trace; @@ -21,45 +19,50 @@ public static async Task BasicBuilderUsageAsync() { // NOTE: This sample assumes ENV VARs have been set to configure the Endpoint and Authorization header. - // Build an instrumentation session by creating an ElasticOpenTelemetryBuilder. - // The application will be instrumented until the session is disposed. - await using var session = new ElasticOpenTelemetryBuilder() - .WithTracing(b => b.AddSource(ActivitySourceName)) - .Build(); + //// Build an instrumentation session by creating an ElasticOpenTelemetryBuilder. + //// The application will be instrumented until the session is disposed. + //await using var session = new ElasticOpenTelemetryBuilder() + // .WithTracing(b => b.AddSource(ActivitySourceName)) + // .Build(); - await using var session2 = new ElasticOpenTelemetryBuilder().Build(); + //await using var session2 = new ElasticOpenTelemetryBuilder().Build(); - // This example adds the application activity source and fully customises the resource - await using var session3 = new ElasticOpenTelemetryBuilder() - .WithTracing(b => b - .AddSource(ActivitySourceName) - .ConfigureResource(r => r.Clear().AddService("CustomServiceName", serviceVersion: "2.2.2"))) - .Build(); + //// This example adds the application activity source and fully customises the resource + //await using var session3 = new ElasticOpenTelemetryBuilder() + // .WithTracing(b => b + // .AddSource(ActivitySourceName) + // .ConfigureResource(r => r.Clear().AddService("CustomServiceName", serviceVersion: "2.2.2"))) + // .Build(); - await using var session4 = new ElasticOpenTelemetryBuilder() - .WithTracing(t => t - .ConfigureResource(rb => rb.AddService("TracerProviderBuilder", "3.3.3")) - .AddRedisInstrumentation() // This can currently only be achieved using this overload or adding Elastic processors to the TPB (as below) - .AddSource(ActivitySourceName) - .AddConsoleExporter() - ) - .WithTracing(tpb => tpb - .ConfigureResource(rb => rb.AddService("TracerProviderBuilder", "3.3.3")) - .AddRedisInstrumentation() // This can currently only be achieved using this overload or adding Elastic processors to the TPB (as below) - .AddSource(ActivitySourceName) - .AddConsoleExporter()) - .Build(); + //await using var session4 = new ElasticOpenTelemetryBuilder() + // .WithTracing(t => t + // .ConfigureResource(rb => rb.AddService("TracerProviderBuilder", "3.3.3")) + // .AddRedisInstrumentation() // This can currently only be achieved using this overload or adding Elastic processors to the TPB (as below) + // .AddSource(ActivitySourceName) + // .AddConsoleExporter() + // ) + // .WithTracing(tpb => tpb + // .ConfigureResource(rb => rb.AddService("TracerProviderBuilder", "3.3.3")) + // .AddRedisInstrumentation() // This can currently only be achieved using this overload or adding Elastic processors to the TPB (as below) + // .AddSource(ActivitySourceName) + // .AddConsoleExporter()) + // .Build(); - //This is the most flexible approach for a consumer as they can include our processor(s) and exporter(s) - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(ActivitySourceName) - .ConfigureResource(resource => - resource.AddService( - serviceName: "OtelSdkApp", - serviceVersion: "1.0.0")) - .AddConsoleExporter() - .AddElasticProcessors() - .Build(); + using var sdk = OpenTelemetrySdk.Create(builder => builder + .WithElasticMetrics() + .WithElasticMetrics() + .ConfigureResource(resource => resource.AddService("MyCustomServiceName"))); + + //This is the most flexible approach for a consumer as they can include our processor(s) + //using var tracerProvider = Sdk.CreateTracerProviderBuilder() + // .AddSource(ActivitySourceName) + // .ConfigureResource(resource => + // resource.AddService( + // serviceName: "OtelSdkApp", + // serviceVersion: "1.0.0")) + // .AddConsoleExporter() + // .AddElasticProcessors() + // .Build(); await DoStuffAsync(); diff --git a/examples/Example.MinimalApi/Example.MinimalApi.csproj b/examples/Example.MinimalApi/Example.MinimalApi.csproj index d48e6b0..e322683 100644 --- a/examples/Example.MinimalApi/Example.MinimalApi.csproj +++ b/examples/Example.MinimalApi/Example.MinimalApi.csproj @@ -12,7 +12,7 @@ - + diff --git a/examples/Example.MinimalApi/Program.cs b/examples/Example.MinimalApi/Program.cs index 7489ac0..46215a7 100644 --- a/examples/Example.MinimalApi/Program.cs +++ b/examples/Example.MinimalApi/Program.cs @@ -8,12 +8,14 @@ var builder = WebApplication.CreateBuilder(args); +//builder.AddElasticOpenTelemetry(); + // This will add the OpenTelemetry services using Elastic defaults builder.AddServiceDefaults(); builder.Services .AddHttpClient() // Adds IHttpClientFactory - .AddOpenTelemetry() // Adds app specific tracing + .AddElasticOpenTelemetry() // Adds app specific tracing .WithTracing(t => t.AddSource(Api.ActivitySourceName)); var app = builder.Build(); diff --git a/examples/Example.MinimalApi/Properties/launchSettings.json b/examples/Example.MinimalApi/Properties/launchSettings.json index cb647e9..dc8153d 100644 --- a/examples/Example.MinimalApi/Properties/launchSettings.json +++ b/examples/Example.MinimalApi/Properties/launchSettings.json @@ -28,7 +28,9 @@ "applicationUrl": "https://localhost:7140;http://localhost:5146", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", - "OTEL_RESOURCE_ATTRIBUTES": "service.name=minimal-api-example" + "OTEL_RESOURCE_ATTRIBUTES": "service.name=minimal-api-example", + "OTEL_EXPORTER_OTLP_ENDPOINT": "https://opentelemetry-playground-adbd73.apm.eu-west-1.aws.elastic.cloud:443", + "OTEL_EXPORTER_OTLP_HEADERS": "Authorization=ApiKey cVY3d3NKTUJXcWZFRWJwb2xURjA6bmNrZ0JiQ29SRWlEdUM1dzVORGYwZw==" } }, "IIS Express": { diff --git a/examples/Example.MinimalApi/appsettings.Development.json b/examples/Example.MinimalApi/appsettings.Development.json index 0c208ae..b06e135 100644 --- a/examples/Example.MinimalApi/appsettings.Development.json +++ b/examples/Example.MinimalApi/appsettings.Development.json @@ -2,7 +2,8 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Microsoft.AspNetCore": "Warning", + "Elastic.OpenTelemetry": "Trace" } } } diff --git a/examples/Example.WorkerService/Example.WorkerService.csproj b/examples/Example.WorkerService/Example.WorkerService.csproj index e0aefbd..481bfc2 100644 --- a/examples/Example.WorkerService/Example.WorkerService.csproj +++ b/examples/Example.WorkerService/Example.WorkerService.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/examples/Example.WorkerService/Program.cs b/examples/Example.WorkerService/Program.cs index cc417be..6596be7 100644 --- a/examples/Example.WorkerService/Program.cs +++ b/examples/Example.WorkerService/Program.cs @@ -10,7 +10,7 @@ var builder = Host.CreateApplicationBuilder(args); -builder.Services.AddOpenTelemetry() +builder.Services.AddElasticOpenTelemetry() .ConfigureResource(r => r.AddService(serviceName: "MyService")) .WithTracing(t => t.AddSource(Worker.ActivitySourceName).AddConsoleExporter()) .WithMetrics(m => m.AddMeter(Worker.MeterName).AddConsoleExporter()); diff --git a/examples/ServiceDefaults/Extensions.cs b/examples/ServiceDefaults/Extensions.cs index a142d88..36f184f 100644 --- a/examples/ServiceDefaults/Extensions.cs +++ b/examples/ServiceDefaults/Extensions.cs @@ -6,9 +6,7 @@ using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.Extensions.Logging; using OpenTelemetry; -using OpenTelemetry.Metrics; using OpenTelemetry.Trace; // ReSharper disable once CheckNamespace @@ -38,20 +36,8 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) { - builder.Logging.AddOpenTelemetry(logging => - { - logging.IncludeFormattedMessage = true; - logging.IncludeScopes = true; - }); - builder.Services.AddElasticOpenTelemetry(builder.Configuration) - .WithMetrics(metrics => - { - metrics.AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation() - .AddProcessInstrumentation() - .AddRuntimeInstrumentation(); - }) + .WithMetrics() .WithTracing(tracing => { if (builder.Environment.IsDevelopment()) @@ -59,10 +45,6 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati // We want to view all traces in development tracing.SetSampler(new AlwaysOnSampler()); } - - tracing.AddAspNetCoreInstrumentation() - .AddGrpcClientInstrumentation() - .AddHttpClientInstrumentation(); }); builder.AddOpenTelemetryExporters(); @@ -86,8 +68,8 @@ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostAppli // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) //{ - // builder.Services.AddOpenTelemetry() - // .UseAzureMonitor(); + //builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); //} builder; diff --git a/examples/ServiceDefaults/ServiceDefaults.csproj b/examples/ServiceDefaults/ServiceDefaults.csproj index 2d20800..b1c2cad 100644 --- a/examples/ServiceDefaults/ServiceDefaults.csproj +++ b/examples/ServiceDefaults/ServiceDefaults.csproj @@ -10,15 +10,15 @@ - + - - + + - - - - + + + + diff --git a/src/Elastic.OpenTelemetry/AutoInstrumentationPlugin.cs b/src/Elastic.OpenTelemetry/AutoInstrumentationPlugin.cs deleted file mode 100644 index 6bec4b5..0000000 --- a/src/Elastic.OpenTelemetry/AutoInstrumentationPlugin.cs +++ /dev/null @@ -1,76 +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.Diagnostics.Tracing; -using Elastic.OpenTelemetry.Configuration; -using Elastic.OpenTelemetry.Extensions; -using Microsoft.Extensions.Logging; -using OpenTelemetry.Logs; -using OpenTelemetry.Metrics; -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; - -namespace Elastic.OpenTelemetry; - -/// -/// Elastic Distribution of OpenTelemetry .NET plugin for Auto Instrumentation. -/// Ensures all signals are rich enough to report to Elastic -/// -// ReSharper disable once UnusedType.Global -public class AutoInstrumentationPlugin -{ - private readonly ILogger _logger; - private readonly EventListener _eventListener; - - private readonly bool _skipOtlp; - - /// - public AutoInstrumentationPlugin() - { - var options = new ElasticOpenTelemetryBuilderOptions(); - var (eventListener, logger) = ElasticOpenTelemetryBuilder.Bootstrap(options); - - _logger = logger; - _eventListener = eventListener; - - var skipOtlpString = Environment.GetEnvironmentVariable("ELASTIC_OTEL_SKIP_OTLP_EXPORTER"); - - if (skipOtlpString is not null && bool.TryParse(skipOtlpString, out var skipOtlp)) - _skipOtlp = skipOtlp; - } - - /// To access TracerProvider right after TracerProviderBuilder.Build() is executed. - public void TracerProviderInitialized(TracerProvider tracerProvider) - { - } - - /// To access MeterProvider right after MeterProviderBuilder.Build() is executed. - public void MeterProviderInitialized(MeterProvider meterProvider) - { - } - - /// To configure tracing SDK before Auto Instrumentation configured SDK - public TracerProviderBuilder BeforeConfigureTracerProvider(TracerProviderBuilder builder) => - builder.UseAutoInstrumentationElasticDefaults(_skipOtlp, _logger); - - /// To configure tracing SDK after Auto Instrumentation configured SDK - public TracerProviderBuilder AfterConfigureTracerProvider(TracerProviderBuilder builder) => - builder; - - /// To configure metrics SDK before Auto Instrumentation configured SDK - public MeterProviderBuilder BeforeConfigureMeterProvider(MeterProviderBuilder builder) => - builder.UseElasticDefaults(_skipOtlp, _logger); - - /// To configure metrics SDK after Auto Instrumentation configured SDK - public MeterProviderBuilder AfterConfigureMeterProvider(MeterProviderBuilder builder) => - builder; - - /// To configure logs SDK (the method name is the same as for other logs options) - public void ConfigureLogsOptions(OpenTelemetryLoggerOptions options) => - options.UseElasticDefaults(_logger); - - /// To configure Resource - public ResourceBuilder ConfigureResource(ResourceBuilder builder) => - builder.UseElasticDefaults(_logger); -} diff --git a/src/Elastic.OpenTelemetry/Configuration/CompositeElasticOpenTelemetryOptions.cs b/src/Elastic.OpenTelemetry/Configuration/CompositeElasticOpenTelemetryOptions.cs new file mode 100644 index 0000000..29f67d3 --- /dev/null +++ b/src/Elastic.OpenTelemetry/Configuration/CompositeElasticOpenTelemetryOptions.cs @@ -0,0 +1,302 @@ +// 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; +using System.Diagnostics.Tracing; +using System.Runtime.InteropServices; +using Elastic.OpenTelemetry.Configuration.Instrumentations; +using Elastic.OpenTelemetry.Configuration.Parsers; +using Elastic.OpenTelemetry.Diagnostics; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using static System.Environment; +using static System.Runtime.InteropServices.RuntimeInformation; +using static Elastic.OpenTelemetry.Configuration.EnvironmentVariables; +using static Elastic.OpenTelemetry.Configuration.Parsers.SharedParsers; + +namespace Elastic.OpenTelemetry.Configuration; + +/// +/// Defines advanced options which can be used to finely-tune the behaviour of the Elastic +/// distribution of OpenTelemetry. +/// +/// +/// Options are bound from the following sources: +/// +/// Environment variables +/// An instance +/// +/// Options initialised via property initializers take precedence over bound values. +/// Environment variables take precedence over values. +/// +internal sealed class CompositeElasticOpenTelemetryOptions +{ + private readonly EventLevel _eventLevel = EventLevel.Informational; + + private readonly ConfigCell _logDirectory = new(nameof(LogDirectory), null); + private readonly ConfigCell _logTargets = new(nameof(LogTargets), null); + + private readonly ConfigCell _logLevel = new(nameof(LogLevel), LogLevel.Warning); + private readonly ConfigCell _skipOtlpExporter = new(nameof(SkipOtlpExporter), false); + private readonly ConfigCell _runningInContainer = new(nameof(_runningInContainer), false); + + private readonly ConfigCell _signals = new(nameof(Signals), Signals.All); + private readonly ConfigCell _tracing = new(nameof(Tracing), TraceInstrumentations.All); + private readonly ConfigCell _metrics = new(nameof(Metrics), MetricInstrumentations.All); + private readonly ConfigCell _logging = new(nameof(Logging), LogInstrumentations.All); + + private readonly IDictionary _environmentVariables; + + internal static CompositeElasticOpenTelemetryOptions DefaultOptions = new(); + internal static CompositeElasticOpenTelemetryOptions SkipOtlpOptions = new() { SkipOtlpExporter = true }; + + /// + /// Creates a new instance of with properties + /// bound from environment variables. + /// + internal CompositeElasticOpenTelemetryOptions() : this((IDictionary?)null) + { + } + + internal CompositeElasticOpenTelemetryOptions(IDictionary? environmentVariables) + { + LogDirectoryDefault = GetDefaultLogDirectory(); + _environmentVariables = environmentVariables ?? GetEnvironmentVariables(); + + SetFromEnvironment(DOTNET_RUNNING_IN_CONTAINER, _runningInContainer, BoolParser); + SetFromEnvironment(OTEL_DOTNET_AUTO_LOG_DIRECTORY, _logDirectory, StringParser); + SetFromEnvironment(OTEL_LOG_LEVEL, _logLevel, LogLevelParser); + SetFromEnvironment(ELASTIC_OTEL_LOG_TARGETS, _logTargets, LogTargetsParser); + SetFromEnvironment(ELASTIC_OTEL_SKIP_OTLP_EXPORTER, _skipOtlpExporter, BoolParser); + + var parser = new EnvironmentParser(_environmentVariables); + parser.ParseInstrumentationVariables(_signals, _tracing, _metrics, _logging); + } + + internal CompositeElasticOpenTelemetryOptions(IConfiguration? configuration, IDictionary? environmentVariables = null) + : this(environmentVariables) + { + if (configuration is null) + return; + + var parser = new ConfigurationParser(configuration); + + parser.ParseLogDirectory(_logDirectory); + parser.ParseLogTargets(_logTargets); + parser.ParseLogLevel(_logLevel, ref _eventLevel); + parser.ParseSkipOtlpExporter(_skipOtlpExporter); + } + + internal CompositeElasticOpenTelemetryOptions(ElasticOpenTelemetryOptions options) + : this((IDictionary?)null) + { + if (options is null) + return; + + // Having configured the base settings from env vars, we now override anything that was + // explicitly configured in the user provided options. + + if (options.SkipOtlpExporter.HasValue) + _skipOtlpExporter.Assign(options.SkipOtlpExporter.Value, ConfigSource.Options); + + if (!string.IsNullOrEmpty(options.LogDirectory)) + _logDirectory.Assign(options.LogDirectory, ConfigSource.Options); + + if (options.LogLevel.HasValue) + _logLevel.Assign(options.LogLevel.Value, ConfigSource.Options); + + if (options.LogTargets.HasValue) + _logTargets.Assign(options.LogTargets.Value, ConfigSource.Options); + + AdditionalLogger = options.AdditionalLogger ?? options.AdditionalLoggerFactory?.CreateElasticLogger(); + } + + internal CompositeElasticOpenTelemetryOptions(IConfiguration configuration, ILoggerFactory loggerFactory) : + this(configuration) => AdditionalLogger = loggerFactory?.CreateElasticLogger(); + + /// + /// Calculates whether global logging is enabled based on + /// , and + /// + internal bool GlobalLogEnabled + { + get + { + var level = _logLevel.Value; + var targets = _logTargets.Value; + var isActive = level is <= LogLevel.Debug || !string.IsNullOrWhiteSpace(_logDirectory.Value) || targets.HasValue; + if (!isActive) + return isActive; + + if (level is LogLevel.None) + isActive = false; + else if (targets is LogTargets.None) + isActive = false; + return isActive; + } + } + + private static string GetDefaultLogDirectory() + { + var applicationMoniker = "elastic-otel-dotnet"; + if (IsOSPlatform(OSPlatform.Windows)) + return Path.Combine(GetFolderPath(SpecialFolder.ApplicationData), "elastic", applicationMoniker); + if (IsOSPlatform(OSPlatform.OSX)) + return Path.Combine(GetFolderPath(SpecialFolder.LocalApplicationData), "elastic", applicationMoniker); + + return $"/var/log/elastic/{applicationMoniker}"; + } + + /// + /// The default log directory if file logging was enabled but non was specified + /// Defaults to: + /// - %PROGRAMDATA%\elastic\apm-agent-dotnet (on Windows) + /// - /var/log/elastic/apm-agent-dotnet (on Linux) + /// - ~/Library/Application_Support/elastic/apm-agent-dotnet (on OSX) + /// + internal string LogDirectoryDefault { get; } + + /// + public string LogDirectory + { + get => _logDirectory.Value ?? LogDirectoryDefault; + init => _logDirectory.Assign(value, ConfigSource.Property); + } + + /// + /// Used by to determine the appropiate event level to subscribe to + /// + internal EventLevel EventLogLevel => _eventLevel; + + /// + public LogLevel LogLevel + { + get => _logLevel.Value ?? LogLevel.Warning; + init => _logLevel.Assign(value, ConfigSource.Property); + } + + /// + public LogTargets LogTargets + { + get => _logTargets.Value ?? (GlobalLogEnabled + ? _runningInContainer.Value.HasValue && _runningInContainer.Value.Value ? LogTargets.StdOut : LogTargets.File + : LogTargets.None); + init => _logTargets.Assign(value, ConfigSource.Property); + } + + /// + public bool SkipOtlpExporter + { + get => _skipOtlpExporter.Value ?? false; + init => _skipOtlpExporter.Assign(value, ConfigSource.Property); + } + + public ILogger? AdditionalLogger { get; internal set; } + + /// + /// Control which signals will be automatically enabled by the Elastic Distribution of OpenTelemetry .NET. + /// + /// This configuration respects the open telemetry environment configuration out of the box: + /// + /// + /// + /// + /// + /// + /// Setting this propery in code or configuration will take precedence over environment variables. + /// + public Signals Signals + { + get => _signals.Value ?? Signals.All; + init => _signals.Assign(value, ConfigSource.Property); + } + + /// + /// Enabled trace instrumentations. + /// + public TraceInstrumentations Tracing + { + get => _tracing.Value ?? TraceInstrumentations.All; + init => _tracing.Assign(value, ConfigSource.Property); + } + + /// + /// Enabled trace instrumentations. + /// + public MetricInstrumentations Metrics + { + get => _metrics.Value ?? MetricInstrumentations.All; + init => _metrics.Assign(value, ConfigSource.Property); + } + + /// + /// Enabled trace instrumentations. + /// + public LogInstrumentations Logging + { + get => _logging.Value ?? LogInstrumentations.All; + init => _logging.Assign(value, ConfigSource.Property); + } + + public override bool Equals(object? obj) + { + if (obj is not CompositeElasticOpenTelemetryOptions other) + return false; + + return LogDirectory == other.LogDirectory && + LogLevel == other.LogLevel && + LogTargets == other.LogTargets && + SkipOtlpExporter == other.SkipOtlpExporter && + Signals == other.Signals && + Tracing.SetEquals(other.Tracing) && + Metrics.SetEquals(other.Metrics) && + Logging.SetEquals(other.Logging) && + ReferenceEquals(AdditionalLogger, other.AdditionalLogger); + } + + public override int GetHashCode() + { +#if NET462 || NETSTANDARD2_0 + return LogDirectory.GetHashCode() + ^ LogLevel.GetHashCode() + ^ LogTargets.GetHashCode() + ^ SkipOtlpExporter.GetHashCode() + ^ Signals.GetHashCode() + ^ Tracing.GetHashCode() + ^ Metrics.GetHashCode() + ^ Logging.GetHashCode() + ^ (AdditionalLogger?.GetHashCode() ?? 0); +#else + var hash1 = HashCode.Combine(LogDirectory, LogLevel, LogTargets, SkipOtlpExporter); + var hash2 = HashCode.Combine(Signals, Tracing, Metrics, Logging, AdditionalLogger); + return HashCode.Combine(hash1, hash2); +#endif + } + + private void SetFromEnvironment(string key, ConfigCell field, Func parser) + { + var value = parser(GetSafeEnvironmentVariable(key)); + if (value is null) + return; + + field.Assign(value, ConfigSource.Environment); + } + + private string GetSafeEnvironmentVariable(string key) + { + var value = _environmentVariables.Contains(key) ? _environmentVariables[key]?.ToString() : null; + return value ?? string.Empty; + } + + internal void LogConfigSources(ILogger logger) + { + logger.LogInformation("Configured value for {Configuration}", _logDirectory); + logger.LogInformation("Configured value for {Configuration}", _logLevel); + logger.LogInformation("Configured value for {Configuration}", _skipOtlpExporter); + logger.LogInformation("Configured value for {Configuration}", _signals); + logger.LogInformation("Configured value for {Configuration}", _tracing); + logger.LogInformation("Configured value for {Configuration}", _metrics); + logger.LogInformation("Configured value for {Configuration}", _logging); + } +} diff --git a/src/Elastic.OpenTelemetry/Configuration/ConfigCell.cs b/src/Elastic.OpenTelemetry/Configuration/ConfigCell.cs index f140cfd..159f6fc 100644 --- a/src/Elastic.OpenTelemetry/Configuration/ConfigCell.cs +++ b/src/Elastic.OpenTelemetry/Configuration/ConfigCell.cs @@ -18,11 +18,3 @@ public void Assign(T value, ConfigSource source) public override string ToString() => $"{Key}: '{Value}' from [{Source}]"; } -internal enum ConfigSource -{ - Default, // Default value assigned within this class - Environment, // Loaded from an environment variable - // ReSharper disable once InconsistentNaming - IConfiguration, // Bound from an IConfiguration instance - Property // Set via property initializer -} diff --git a/src/Elastic.OpenTelemetry/Configuration/ConfigSource.cs b/src/Elastic.OpenTelemetry/Configuration/ConfigSource.cs new file mode 100644 index 0000000..aecbcd8 --- /dev/null +++ b/src/Elastic.OpenTelemetry/Configuration/ConfigSource.cs @@ -0,0 +1,15 @@ +// 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.Configuration; + +internal enum ConfigSource +{ + Default, // Default value assigned within this class + Environment, // Loaded from an environment variable + // ReSharper disable once InconsistentNaming + IConfiguration, // Bound from an IConfiguration instance + Property, // Set via property initializer + Options // Set via user provided ElasticOpenTelemetryOptions +} diff --git a/src/Elastic.OpenTelemetry/Configuration/ElasticDefaults.cs b/src/Elastic.OpenTelemetry/Configuration/ElasticDefaults.cs deleted file mode 100644 index 3bc90f9..0000000 --- a/src/Elastic.OpenTelemetry/Configuration/ElasticDefaults.cs +++ /dev/null @@ -1,29 +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 - -namespace Elastic.OpenTelemetry.Configuration; - -/// -/// Control which elastic defaults you want to include. -/// NOTE: this is an expert level option only use this if you want to take full control of the OTEL configuration -/// defaults to -/// -[Flags] -public enum ElasticDefaults -{ - /// No Elastic defaults will be included, acting effectively as a vanilla OpenTelemetry - None, - - /// Include Elastic Distribution of OpenTelemetry .NET tracing defaults - Traces = 1 << 0, //1 - - /// Include Elastic Distribution of OpenTelemetry .NET metrics defaults - Metrics = 1 << 1, //2 - - /// Include Elastic Distribution of OpenTelemetry .NET logging defaults - Logs = 1 << 2, //4 - - /// (default) Include all Elastic Distribution of OpenTelemetry .NET logging defaults - All = ~0 -} diff --git a/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryBuilderOptions.cs b/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryBuilderOptions.cs deleted file mode 100644 index 50a547b..0000000 --- a/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryBuilderOptions.cs +++ /dev/null @@ -1,41 +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 Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Elastic.OpenTelemetry.Configuration; - -/// -/// Expert options to provide to to control its initial OpenTelemetry registration. -/// -public record ElasticOpenTelemetryBuilderOptions -{ - private ElasticOpenTelemetryOptions? _elasticOpenTelemetryOptions; - - /// - /// Provide an additional logger to the internal file logger. - /// - /// The distribution will always log to file if a path is provided using the ELASTIC_OTEL_LOG_DIRECTORY. - /// environment variable. - /// - public ILogger? Logger { get; init; } - - /// - /// Provides an to register the into. - /// If null, a new local instance will be used. - /// - internal IServiceCollection? Services { get; init; } - - private static readonly ElasticOpenTelemetryOptions DefaultDistroOptions = new(); - /// - /// Advanced options which can be used to finely-tune the behaviour of the Elastic - /// distribution of OpenTelemetry. - /// - public ElasticOpenTelemetryOptions DistroOptions - { - get => _elasticOpenTelemetryOptions ?? DefaultDistroOptions; - init => _elasticOpenTelemetryOptions = value; - } -} diff --git a/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryOptions.cs b/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryOptions.cs index 57ec5a4..8706d85 100644 --- a/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryOptions.cs +++ b/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryOptions.cs @@ -2,137 +2,19 @@ // 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; -using System.Diagnostics.Tracing; -using System.Runtime.InteropServices; -using Elastic.OpenTelemetry.Configuration.Instrumentations; -using Elastic.OpenTelemetry.Configuration.Parsers; -using Elastic.OpenTelemetry.Diagnostics; -using Microsoft.Extensions.Configuration; +using Elastic.OpenTelemetry.Configuration; using Microsoft.Extensions.Logging; -using static System.Environment; -using static System.Runtime.InteropServices.RuntimeInformation; -using static Elastic.OpenTelemetry.Configuration.EnvironmentVariables; -using static Elastic.OpenTelemetry.Configuration.Parsers.SharedParsers; -namespace Elastic.OpenTelemetry.Configuration; +#pragma warning disable IDE0130 // Namespace does not match folder structure +namespace Elastic.OpenTelemetry; +#pragma warning restore IDE0130 // Namespace does not match folder structure /// -/// Defines advanced options which can be used to finely-tune the behaviour of the Elastic +/// Defines options which can be used to finely-tune the behaviour of the Elastic /// distribution of OpenTelemetry. /// -/// -/// Options are bound from the following sources: -/// -/// Environment variables -/// An instance -/// -/// Options initialised via property initializers take precedence over bound values. -/// Environment variables take precedence over values. -/// public class ElasticOpenTelemetryOptions { - private readonly ConfigCell _logDirectory = new(nameof(LogDirectory), null); - private readonly ConfigCell _logTargets = new(nameof(LogTargets), null); - - private readonly EventLevel _eventLevel = EventLevel.Informational; - private readonly ConfigCell _logLevel = new(nameof(LogLevel), LogLevel.Warning); - private readonly ConfigCell _skipOtlpExporter = new(nameof(SkipOtlpExporter), false); - private readonly ConfigCell _enabledDefaults = new(nameof(ElasticDefaults), ElasticDefaults.All); - private readonly ConfigCell _runningInContainer = new(nameof(_runningInContainer), false); - private readonly ConfigCell _signals = new(nameof(Signals), Signals.All); - - private readonly ConfigCell _tracing = new(nameof(Tracing), TraceInstrumentations.All); - private readonly ConfigCell _metrics = new(nameof(Metrics), MetricInstrumentations.All); - private readonly ConfigCell _logging = new(nameof(Logging), LogInstrumentations.All); - - private readonly IDictionary _environmentVariables; - - /// - /// Creates a new instance of with properties - /// bound from environment variables. - /// - public ElasticOpenTelemetryOptions(IDictionary? environmentVariables = null) - { - LogDirectoryDefault = GetDefaultLogDirectory(); - _environmentVariables = environmentVariables ?? GetEnvironmentVariables(); - SetFromEnvironment(DOTNET_RUNNING_IN_CONTAINER, _runningInContainer, BoolParser); - - SetFromEnvironment(OTEL_DOTNET_AUTO_LOG_DIRECTORY, _logDirectory, StringParser); - SetFromEnvironment(OTEL_LOG_LEVEL, _logLevel, LogLevelParser); - SetFromEnvironment(ELASTIC_OTEL_LOG_TARGETS, _logTargets, LogTargetsParser); - SetFromEnvironment(ELASTIC_OTEL_SKIP_OTLP_EXPORTER, _skipOtlpExporter, BoolParser); - SetFromEnvironment(ELASTIC_OTEL_DEFAULTS_ENABLED, _enabledDefaults, ElasticDefaultsParser); - - var parser = new EnvironmentParser(_environmentVariables); - parser.ParseInstrumentationVariables(_signals, _tracing, _metrics, _logging); - - } - - /// - /// Creates a new instance of with properties - /// bound from environment variables and an instance. - /// - internal ElasticOpenTelemetryOptions(IConfiguration? configuration, IDictionary? environmentVariables = null) - : this(environmentVariables) - { - if (configuration is null) - return; - - var parser = new ConfigurationParser(configuration); - parser.ParseLogDirectory(_logDirectory); - parser.ParseLogTargets(_logTargets); - parser.ParseLogLevel(_logLevel, ref _eventLevel); - parser.ParseSkipOtlpExporter(_skipOtlpExporter); - parser.ParseElasticDefaults(_enabledDefaults); - parser.ParseSignals(_signals); - - parser.ParseInstrumentations(_tracing, _metrics, _logging); - - } - - /// - /// Calculates whether global logging is enabled based on - /// , and - /// - public bool GlobalLogEnabled - { - get - { - var level = _logLevel.Value; - var targets = _logTargets.Value; - var isActive = level is <= LogLevel.Debug || !string.IsNullOrWhiteSpace(_logDirectory.Value) || targets.HasValue; - if (!isActive) - return isActive; - - if (level is LogLevel.None) - isActive = false; - else if (targets is LogTargets.None) - isActive = false; - return isActive; - } - } - - private static string GetDefaultLogDirectory() - { - var applicationMoniker = "elastic-otel-dotnet"; - if (IsOSPlatform(OSPlatform.Windows)) - return Path.Combine(GetFolderPath(SpecialFolder.ApplicationData), "elastic", applicationMoniker); - if (IsOSPlatform(OSPlatform.OSX)) - return Path.Combine(GetFolderPath(SpecialFolder.LocalApplicationData), "elastic", applicationMoniker); - - return $"/var/log/elastic/{applicationMoniker}"; - } - - /// - /// The default log directory if file logging was enabled but non was specified - /// Defaults to: - /// - %PROGRAMDATA%\elastic\apm-agent-dotnet (on Windows) - /// - /var/log/elastic/apm-agent-dotnet (on Linux) - /// - ~/Library/Application_Support/elastic/apm-agent-dotnet (on OSX) - /// - public string LogDirectoryDefault { get; } - /// /// The output directory where the Elastic Distribution of OpenTelemetry .NET will write log files. /// @@ -141,16 +23,7 @@ private static string GetDefaultLogDirectory() /// {ProcessName}_{UtcUnixTimeMilliseconds}_{ProcessId}.instrumentation.log. /// This log file includes log messages from the OpenTelemetry SDK and the Elastic distribution. /// - public string LogDirectory - { - get => _logDirectory.Value ?? LogDirectoryDefault; - init => _logDirectory.Assign(value, ConfigSource.Property); - } - - /// - /// Used by to determine the appropiate event level to subscribe to - /// - internal EventLevel EventLogLevel => _eventLevel; + public string? LogDirectory { get; init; } /// /// The log level to use when writing log files. @@ -167,115 +40,26 @@ public string LogDirectory /// TraceContain the most detailed messages. /// /// - public LogLevel LogLevel - { - get => _logLevel.Value ?? LogLevel.Warning; - init => _logLevel.Assign(value, ConfigSource.Property); - } - - /// - public LogTargets LogTargets - { - get => _logTargets.Value ?? (GlobalLogEnabled - ? _runningInContainer.Value.HasValue && _runningInContainer.Value.Value ? LogTargets.StdOut : LogTargets.File - : LogTargets.None); - init => _logTargets.Assign(value, ConfigSource.Property); - } + public LogLevel? LogLevel { get; init; } /// - /// Stops from registering OLTP exporters, useful for testing scenarios. + /// Control the targets that the Elastic Distribution of OpenTelemetry .NET will log to. /// - public bool SkipOtlpExporter - { - get => _skipOtlpExporter.Value ?? false; - init => _skipOtlpExporter.Assign(value, ConfigSource.Property); - } + public LogTargets? LogTargets { get; init; } /// - /// Allows flags to be set based of to selectively opt in to Elastic Distribution of OpenTelemetry .NET features. - /// Defaults to + /// Stops Elastic Distribution of OpenTelemetry .NET from registering OLTP exporters, useful for testing scenarios. /// - /// - /// Valid values are: - /// - /// None Disables all Elastic defaults resulting in the use of the "vanilla" SDK. - /// All Enables all defaults (default if this option is not specified). - /// Tracing Enables Elastic defaults for tracing. - /// Metrics Enables Elastic defaults for metrics. - /// Logging Enables Elastic defaults for logging. - /// - /// - public ElasticDefaults ElasticDefaults - { - get => _enabledDefaults.Value ?? ElasticDefaults.All; - init => _enabledDefaults.Assign(value, ConfigSource.Property); - } + public bool? SkipOtlpExporter { get; init; } /// - /// Control which signals will be automatically enabled by the Elastic Distribution of OpenTelemetry .NET. - /// - /// This configuration respects the open telemetry environment configuration out of the box: - /// - /// - /// - /// - /// - /// - /// Setting this propery in code or configuration will take precedence over environment variables + /// An additional to which logs will be written. /// - public Signals Signals - { - get => _signals.Value ?? Signals.All; - init => _signals.Assign(value, ConfigSource.Property); - } - - /// Enabled trace instrumentations - public TraceInstrumentations Tracing - { - get => _tracing.Value ?? TraceInstrumentations.All; - init => _tracing.Assign(value, ConfigSource.Property); - } - - /// Enabled trace instrumentations - public MetricInstrumentations Metrics - { - get => _metrics.Value ?? MetricInstrumentations.All; - init => _metrics.Assign(value, ConfigSource.Property); - } - - /// Enabled trace instrumentations - public LogInstrumentations Logging - { - get => _logging.Value ?? LogInstrumentations.All; - init => _logging.Assign(value, ConfigSource.Property); - } - - private void SetFromEnvironment(string key, ConfigCell field, Func parser) - { - var value = parser(GetSafeEnvironmentVariable(key)); - if (value is null) - return; - - field.Assign(value, ConfigSource.Environment); + public ILogger? AdditionalLogger { get; init; } - } - - private string GetSafeEnvironmentVariable(string key) - { - var value = _environmentVariables.Contains(key) ? _environmentVariables[key]?.ToString() : null; - return value ?? string.Empty; - } - - - internal void LogConfigSources(ILogger logger) - { - logger.LogInformation("Configured value for {Configuration}", _logDirectory); - logger.LogInformation("Configured value for {Configuration}", _logLevel); - logger.LogInformation("Configured value for {Configuration}", _skipOtlpExporter); - logger.LogInformation("Configured value for {Configuration}", _enabledDefaults); - logger.LogInformation("Configured value for {Configuration}", _signals); - logger.LogInformation("Configured value for {Configuration}", _tracing); - logger.LogInformation("Configured value for {Configuration}", _metrics); - logger.LogInformation("Configured value for {Configuration}", _logging); - } + /// + /// An that can be used to create an additional + /// to which logs will be written. + /// + public ILoggerFactory? AdditionalLoggerFactory { get; init; } } diff --git a/src/Elastic.OpenTelemetry/Configuration/EnvironmentVariables.cs b/src/Elastic.OpenTelemetry/Configuration/EnvironmentVariables.cs index 61a6aea..7efeeb3 100644 --- a/src/Elastic.OpenTelemetry/Configuration/EnvironmentVariables.cs +++ b/src/Elastic.OpenTelemetry/Configuration/EnvironmentVariables.cs @@ -9,22 +9,36 @@ internal static class EnvironmentVariables // ReSharper disable InconsistentNaming // ReSharper disable IdentifierTypo public const string ELASTIC_OTEL_SKIP_OTLP_EXPORTER = nameof(ELASTIC_OTEL_SKIP_OTLP_EXPORTER); + public const string ELASTIC_OTEL_LOG_TARGETS = nameof(ELASTIC_OTEL_LOG_TARGETS); public const string OTEL_DOTNET_AUTO_LOG_DIRECTORY = nameof(OTEL_DOTNET_AUTO_LOG_DIRECTORY); public const string OTEL_LOG_LEVEL = nameof(OTEL_LOG_LEVEL); - public const string ELASTIC_OTEL_LOG_TARGETS = nameof(ELASTIC_OTEL_LOG_TARGETS); - public const string DOTNET_RUNNING_IN_CONTAINER = nameof(DOTNET_RUNNING_IN_CONTAINER); - - public const string ELASTIC_OTEL_DEFAULTS_ENABLED = nameof(ELASTIC_OTEL_DEFAULTS_ENABLED); - public const string OTEL_DOTNET_AUTO_INSTRUMENTATION_ENABLED = nameof(OTEL_DOTNET_AUTO_INSTRUMENTATION_ENABLED); public const string OTEL_DOTNET_AUTO_TRACES_INSTRUMENTATION_ENABLED = nameof(OTEL_DOTNET_AUTO_TRACES_INSTRUMENTATION_ENABLED); public const string OTEL_DOTNET_AUTO_METRICS_INSTRUMENTATION_ENABLED = nameof(OTEL_DOTNET_AUTO_METRICS_INSTRUMENTATION_ENABLED); public const string OTEL_DOTNET_AUTO_LOGS_INSTRUMENTATION_ENABLED = nameof(OTEL_DOTNET_AUTO_LOGS_INSTRUMENTATION_ENABLED); + public const string OTEL_EXPORTER_OTLP_ENDPOINT = nameof(OTEL_EXPORTER_OTLP_ENDPOINT); + public const string OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = nameof(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT); + public const string OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = nameof(OTEL_EXPORTER_OTLP_METRICS_ENDPOINT); + public const string OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = nameof(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT); + + public const string OTEL_EXPORTER_OTLP_PROTOCOL = nameof(OTEL_EXPORTER_OTLP_PROTOCOL); + public const string OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = nameof(OTEL_EXPORTER_OTLP_TRACES_PROTOCOL); + public const string OTEL_EXPORTER_OTLP_METRICS_PROTOCOL = nameof(OTEL_EXPORTER_OTLP_METRICS_PROTOCOL); + public const string OTEL_EXPORTER_OTLP_LOGS_PROTOCOL = nameof(OTEL_EXPORTER_OTLP_LOGS_PROTOCOL); + + public const string OTEL_EXPORTER_OTLP_TIMEOUT = nameof(OTEL_EXPORTER_OTLP_TIMEOUT); + public const string OTEL_EXPORTER_OTLP_TRACES_TIMEOUT = nameof(OTEL_EXPORTER_OTLP_TRACES_TIMEOUT); + public const string OTEL_EXPORTER_OTLP_METRICS_TIMEOUT = nameof(OTEL_EXPORTER_OTLP_METRICS_TIMEOUT); + public const string OTEL_EXPORTER_OTLP_LOGS_TIMEOUT = nameof(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT); + public const string OTEL_EXPORTER_OTLP_HEADERS = nameof(OTEL_EXPORTER_OTLP_HEADERS); + public const string OTEL_EXPORTER_OTLP_TRACES_HEADERS = nameof(OTEL_EXPORTER_OTLP_TRACES_HEADERS); + public const string OTEL_EXPORTER_OTLP_METRICS_HEADERS = nameof(OTEL_EXPORTER_OTLP_METRICS_HEADERS); + public const string OTEL_EXPORTER_OTLP_LOGS_HEADERS = nameof(OTEL_EXPORTER_OTLP_LOGS_HEADERS); // ReSharper enable IdentifierTypo // ReSharper enable InconsistentNaming } diff --git a/src/Elastic.OpenTelemetry/Configuration/Instrumentations/LogInstrumentation.cs b/src/Elastic.OpenTelemetry/Configuration/Instrumentations/LogInstrumentation.cs index b2c0240..68c8a01 100644 --- a/src/Elastic.OpenTelemetry/Configuration/Instrumentations/LogInstrumentation.cs +++ b/src/Elastic.OpenTelemetry/Configuration/Instrumentations/LogInstrumentation.cs @@ -6,15 +6,19 @@ namespace Elastic.OpenTelemetry.Configuration.Instrumentations; -/// A hash set to enable -public class LogInstrumentations : HashSet +/// +/// A hash set to enable for auto-instrumentation. +/// +/// +/// Explicitly enable specific libraries. +/// +internal class LogInstrumentations(IEnumerable instrumentations) : HashSet(instrumentations) { - /// All available + /// + /// All available libraries. + /// public static readonly LogInstrumentations All = new([.. LogInstrumentationExtensions.GetValues()]); - /// Explicitly enable specific - public LogInstrumentations(IEnumerable instrumentations) : base(instrumentations) { } - /// public override string ToString() { @@ -22,17 +26,20 @@ public override string ToString() return "None"; if (Count == All.Count) return "All"; - if (All.Count - Count < 5) + if (All.Count - Count < All.Count) return $"All Except: {string.Join(", ", All.Except(this).Select(i => i.ToStringFast()))}"; + return string.Join(", ", this.Select(i => i.ToStringFast())); } } -/// Available logs instrumentations. +/// +/// Available logging instrumentations. +/// [EnumExtensions] -public enum LogInstrumentation +internal enum LogInstrumentation { - /// ILogger instrumentation + /// ILogger instrumentation. // ReSharper disable once InconsistentNaming ILogger } diff --git a/src/Elastic.OpenTelemetry/Configuration/Instrumentations/MetricInstrumentation.cs b/src/Elastic.OpenTelemetry/Configuration/Instrumentations/MetricInstrumentation.cs index 1bc5dbf..d8b0adc 100644 --- a/src/Elastic.OpenTelemetry/Configuration/Instrumentations/MetricInstrumentation.cs +++ b/src/Elastic.OpenTelemetry/Configuration/Instrumentations/MetricInstrumentation.cs @@ -6,15 +6,19 @@ namespace Elastic.OpenTelemetry.Configuration.Instrumentations; -/// A hash set to enable -public class MetricInstrumentations : HashSet +/// +/// A hash set to enable for auto-instrumentation. +/// +/// +/// Explicitly enable specific libraries. +/// +internal class MetricInstrumentations(IEnumerable instrumentations) : HashSet(instrumentations) { - /// All available + /// + /// All available libraries. + /// public static readonly MetricInstrumentations All = new([.. MetricInstrumentationExtensions.GetValues()]); - /// Explicitly enable specific - public MetricInstrumentations(IEnumerable instrumentations) : base(instrumentations) { } - /// public override string ToString() { @@ -22,26 +26,29 @@ public override string ToString() return "None"; if (Count == All.Count) return "All"; - if (All.Count - Count < 5) + if (All.Count - Count < All.Count) return $"All Except: {string.Join(", ", All.Except(this).Select(i => i.ToStringFast()))}"; + return string.Join(", ", this.Select(i => i.ToStringFast())); } } -/// Available metric instrumentations. +/// +/// Available metric instrumentations. +/// [EnumExtensions] -public enum MetricInstrumentation +internal enum MetricInstrumentation { - ///ASP.NET Framework + /// ASP.NET Framework. AspNet, - ///ASP.NET Core + /// ASP.NET Core. AspNetCore, - ///System.Net.Http.HttpClient and System.Net.HttpWebRequest, HttpClient metrics + /// System.Net.Http.HttpClient and System.Net.HttpWebRequest metrics. HttpClient, - ///OpenTelemetry.Instrumentation.Runtime, Runtime metrics + /// OpenTelemetry.Instrumentation.Runtime metrics. NetRuntime, - ///OpenTelemetry.Instrumentation.Process,Process metrics + /// OpenTelemetry.Instrumentation.Process metrics. Process, - ///NServiceBus metrics + /// NServiceBus metrics. NServiceBus } diff --git a/src/Elastic.OpenTelemetry/Configuration/Instrumentations/TraceInstrumentation.cs b/src/Elastic.OpenTelemetry/Configuration/Instrumentations/TraceInstrumentation.cs index eaca173..953427f 100644 --- a/src/Elastic.OpenTelemetry/Configuration/Instrumentations/TraceInstrumentation.cs +++ b/src/Elastic.OpenTelemetry/Configuration/Instrumentations/TraceInstrumentation.cs @@ -6,15 +6,19 @@ namespace Elastic.OpenTelemetry.Configuration.Instrumentations; -/// A hash set to enable -public class TraceInstrumentations : HashSet +/// +/// A hash set to enable for auto-instrumentation. +/// +/// +/// Explicitly enable specific libraries. +/// +internal class TraceInstrumentations(IEnumerable instrumentations) : HashSet(instrumentations) { - /// All available + /// + /// All available libraries. + /// public static readonly TraceInstrumentations All = new([.. TraceInstrumentationExtensions.GetValues()]); - /// Explicitly enable specific - public TraceInstrumentations(IEnumerable instrumentations) : base(instrumentations) { } - /// public override string ToString() { @@ -22,79 +26,82 @@ public override string ToString() return "None"; if (Count == All.Count) return "All"; - if (All.Count - Count < 5) + if (All.Count - Count < All.Count) return $"All Except: {string.Join(", ", All.Except(this).Select(i => i.ToStringFast()))}"; + return string.Join(", ", this.Select(i => i.ToStringFast())); } } -/// Available trace instrumentations. +/// +/// Available trace instrumentations. +/// [EnumExtensions] -public enum TraceInstrumentation +internal enum TraceInstrumentation { - ///ASP.NET (.NET Framework) MVC / WebApi + /// ASP.NET (.NET Framework) MVC / WebApi. AspNet, - ///ASP.NET Core + /// ASP.NET Core. AspNetCore, - ///Azure SDK + /// Azure SDK. Azure, - ///Elastic.Clients.Elasticsearch + /// Elastic.Clients.Elasticsearch. Elasticsearch, - ///Elastic.Transport >=0.4.16 + /// Elastic.Transport. ElasticTransport, - ///Microsoft.EntityFrameworkCore Not supported on.NET Framework >=6.0.12 + /// Microsoft.EntityFrameworkCore. EntityFrameworkCore, - ///GraphQL Not supported on.NET Framework >=7.5.0 + /// GraphQL. Graphql, - ///Grpc.Net.Client >=2.52 .0 & < 3.0.0 + /// Grpc.Net.Client. GrpcNetClient, - ///System.Net.Http.HttpClient and System.Net.HttpWebRequest + /// System.Net.Http.HttpClient and System.Net.HttpWebRequest. HttpClient, - ///Confluent.Kafka >=1.4 .0 & < 3.0.0 + /// Confluent.Kafka. Kafka, - ///MassTransit Not supported on.NET Framework ≥8.0.0 + /// MassTransit. MassTransit, - ///MongoDB.Driver.Core >=2.13 .3 & < 3.0.0 + /// MongoDB.Driver.Core. MongoDb, - ///MySqlConnector >=2.0.0 + /// MySqlConnector. MysqlConnector, - ///MySql.Data Not supported on.NET Framework >=8.1.0 + /// MySql.Data. MysqlData, - ///Npgsql >=6.0.0 + /// Npgsql >=6.0.0. Npgsql, - ///NServiceBus >=8.0.0 & < 10.0.0 + /// NServiceBus. NServiceBus, - ///Oracle.ManagedDataAccess.Core and Oracle.ManagedDataAccess Not supported on ARM64 >=23.4.0 + /// Oracle.ManagedDataAccess.Core and Oracle.ManagedDataAccess. OracleMda, - ///Quartz Not supported on.NET Framework 4.7.1 and older >=3.4.0 + /// Quartz. Quartz, - ///Microsoft.Data.SqlClient, System.Data.SqlClient and System.Data (shipped with.NET Framework) + /// Microsoft.Data.SqlClient, System.Data.SqlClient and System.Data (shipped with.NET Framework). SqlClient, - ///StackExchange.Redis Not supported on.NET Framework >=2.0.405 & < 3.0.0 + /// StackExchange.Redis. StackExchangeRedis, - ///WCF + /// WCF client. WcfClient, - ///WCF Not supported on.NET. + /// WCF server. WcfService } diff --git a/src/Elastic.OpenTelemetry/Configuration/LogTargets.cs b/src/Elastic.OpenTelemetry/Configuration/LogTargets.cs index 04ab903..8d1c4a7 100644 --- a/src/Elastic.OpenTelemetry/Configuration/LogTargets.cs +++ b/src/Elastic.OpenTelemetry/Configuration/LogTargets.cs @@ -10,14 +10,17 @@ namespace Elastic.OpenTelemetry.Configuration; [Flags] public enum LogTargets { - - /// No global logging + /// + /// No global logging. + /// None, + /// /// Enable file logging. Use - /// and to set any values other than the defaults + /// and to set any values other than the defaults. /// File = 1 << 0, //1 + /// /// Write to standard out, useful in scenarios where file logging might not be an option or harder to set up. /// e.g. containers, k8s, etc. diff --git a/src/Elastic.OpenTelemetry/Configuration/Parsers/ConfigurationParser.cs b/src/Elastic.OpenTelemetry/Configuration/Parsers/ConfigurationParser.cs index 2d359dd..ea95fd8 100644 --- a/src/Elastic.OpenTelemetry/Configuration/Parsers/ConfigurationParser.cs +++ b/src/Elastic.OpenTelemetry/Configuration/Parsers/ConfigurationParser.cs @@ -3,11 +3,9 @@ // See the LICENSE file in the project root for more information using System.Diagnostics.Tracing; -using Elastic.OpenTelemetry.Configuration.Instrumentations; -using Elastic.OpenTelemetry.Diagnostics.Logging; +using Elastic.OpenTelemetry.Diagnostics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; -using static System.StringSplitOptions; using static Elastic.OpenTelemetry.Configuration.Parsers.SharedParsers; namespace Elastic.OpenTelemetry.Configuration.Parsers; @@ -32,7 +30,6 @@ public ConfigurationParser(IConfiguration configuration) LoggingSectionLogLevel = configuration.GetValue("Logging:LogLevel:Default"); } - private static void SetFromConfiguration(IConfiguration configuration, ConfigCell cell, Func parser) { //environment configuration takes precedence, assume already configured @@ -75,6 +72,7 @@ public void ParseLogLevel(ConfigCell logLevel, ref EventLevel eventLe if (sectionLogLevel < eventLogLevel) eventLogLevel = sectionLogLevel; } + eventLevel = eventLogLevel switch { LogLevel.Trace => EventLevel.Verbose, @@ -82,87 +80,11 @@ public void ParseLogLevel(ConfigCell logLevel, ref EventLevel eventLe LogLevel.Warning => EventLevel.Warning, LogLevel.Error => EventLevel.Error, LogLevel.Critical => EventLevel.Critical, + _ => EventLevel.Informational // fallback to info level }; - } public void ParseSkipOtlpExporter(ConfigCell skipOtlpExporter) => SetFromConfiguration(_configuration, skipOtlpExporter, BoolParser); - - public void ParseSignals(ConfigCell signals) => - SetFromConfiguration(_configuration, signals, SignalsParser); - - public void ParseElasticDefaults(ConfigCell defaults) => - SetFromConfiguration(_configuration, defaults, ElasticDefaultsParser); - - public void ParseInstrumentations( - ConfigCell tracing, - ConfigCell metrics, - ConfigCell logging - ) - { - if (tracing.Source != ConfigSource.Environment) - SetFromConfiguration(_configuration, tracing, ParseTracing); - - if (metrics.Source != ConfigSource.Environment) - SetFromConfiguration(_configuration, metrics, ParseMetrics); - - if (logging.Source != ConfigSource.Environment) - SetFromConfiguration(_configuration, logging, ParseLogs); - - } - - private static IEnumerable? ParseInstrumentation(string? config, T[] all, Func getter) - where T : struct - { - if (string.IsNullOrWhiteSpace(config)) - return null; - - var toRemove = new HashSet(); - var toAdd = new HashSet(); - - foreach (var token in config.Split(new[] { ';', ',' }, RemoveEmptyEntries)) - { - var candidate = token.Trim(); - var remove = candidate.StartsWith("-"); - candidate = candidate.TrimStart('-'); - - var instrumentation = getter(candidate); - if (!instrumentation.HasValue) - continue; - - if (remove) - toRemove.Add(instrumentation.Value); - else - toAdd.Add(instrumentation.Value); - } - if (toAdd.Count > 0) - return toAdd; - if (toRemove.Count > 0) - return all.Except(toRemove); - return null; - - } - - private static TraceInstrumentations? ParseTracing(string? tracing) - { - var instrumentations = ParseInstrumentation(tracing, TraceInstrumentationExtensions.GetValues(), - s => TraceInstrumentationExtensions.TryParse(s, out var instrumentation) ? instrumentation : null); - return instrumentations != null ? new TraceInstrumentations(instrumentations) : null; - } - - private static MetricInstrumentations? ParseMetrics(string? metrics) - { - var instrumentations = ParseInstrumentation(metrics, MetricInstrumentationExtensions.GetValues(), - s => MetricInstrumentationExtensions.TryParse(s, out var instrumentation) ? instrumentation : null); - return instrumentations != null ? new MetricInstrumentations(instrumentations) : null; - } - - private static LogInstrumentations? ParseLogs(string? logs) - { - var instrumentations = ParseInstrumentation(logs, LogInstrumentationExtensions.GetValues(), - s => LogInstrumentationExtensions.TryParse(s, out var instrumentation) ? instrumentation : null); - return instrumentations != null ? new LogInstrumentations(instrumentations) : null; - } } diff --git a/src/Elastic.OpenTelemetry/Configuration/Parsers/EnvironmentParser.cs b/src/Elastic.OpenTelemetry/Configuration/Parsers/EnvironmentParser.cs index b9d491c..4ac9ea0 100644 --- a/src/Elastic.OpenTelemetry/Configuration/Parsers/EnvironmentParser.cs +++ b/src/Elastic.OpenTelemetry/Configuration/Parsers/EnvironmentParser.cs @@ -32,8 +32,8 @@ private string GetSafeEnvironmentVariable(string key) if (enabled.HasValue) opted = true; } - return (opted, instrumentations); + return (opted, instrumentations); } internal (bool, HashSet) EnabledTraceInstrumentations(bool allEnabled) => @@ -49,8 +49,7 @@ public void ParseInstrumentationVariables( ConfigCell signalsCell, ConfigCell tracingCell, ConfigCell metricsCell, - ConfigCell loggingCell - ) + ConfigCell loggingCell) { var allEnabled = BoolParser(GetSafeEnvironmentVariable(OTEL_DOTNET_AUTO_INSTRUMENTATION_ENABLED)); var defaultSignals = allEnabled.HasValue @@ -68,7 +67,6 @@ ConfigCell loggingCell if (optedTraces) tracingCell.Assign(new TraceInstrumentations(traceInstrumentations), ConfigSource.Environment); - var metricEnabled = Configured(metrics); var (optedMetrics, metricInstrumentations) = EnabledMetricInstrumentations(metricEnabled); if (optedMetrics) @@ -98,8 +96,8 @@ ConfigCell loggingCell if (logs.HasValue || traces.HasValue || traces.HasValue || allEnabled.HasValue) signalsCell.Assign(signals, ConfigSource.Environment); + if (optedLogs || optedMetrics || optedTraces) signalsCell.Assign(signals, ConfigSource.Environment); - } } diff --git a/src/Elastic.OpenTelemetry/Configuration/Parsers/SharedParsers.cs b/src/Elastic.OpenTelemetry/Configuration/Parsers/SharedParsers.cs index ce40226..33f7746 100644 --- a/src/Elastic.OpenTelemetry/Configuration/Parsers/SharedParsers.cs +++ b/src/Elastic.OpenTelemetry/Configuration/Parsers/SharedParsers.cs @@ -2,7 +2,7 @@ // 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 Elastic.OpenTelemetry.Diagnostics.Logging; +using Elastic.OpenTelemetry.Diagnostics; using Microsoft.Extensions.Logging; using static System.StringComparison; using static System.StringSplitOptions; @@ -11,20 +11,18 @@ namespace Elastic.OpenTelemetry.Configuration.Parsers; internal static class SharedParsers { - internal static LogLevel? LogLevelParser(string? s) => !string.IsNullOrEmpty(s) ? LogLevelHelpers.ToLogLevel(s) : null; internal static LogTargets? LogTargetsParser(string? s) { - //var tokens = s?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries }); if (string.IsNullOrWhiteSpace(s)) return null; var logTargets = LogTargets.None; var found = false; - foreach (var target in s.Split(new[] { ';', ',' }, RemoveEmptyEntries)) + foreach (var target in s.Split([';', ','], RemoveEmptyEntries)) { if (IsSet(target, "stdout")) logTargets |= LogTargets.StdOut; @@ -44,82 +42,6 @@ bool IsSet(string k, string v) } } - internal static ElasticDefaults? ElasticDefaultsParser(string? s) - { - if (string.IsNullOrWhiteSpace(s)) - return null; - - var enabledDefaults = ElasticDefaults.None; - var found = false; - - foreach (var target in s.Split(new[] { ';', ',' }, RemoveEmptyEntries)) - { - if (IsSet(target, nameof(ElasticDefaults.Traces))) - enabledDefaults |= ElasticDefaults.Traces; - else if (IsSet(target, nameof(ElasticDefaults.Metrics))) - enabledDefaults |= ElasticDefaults.Metrics; - else if (IsSet(target, nameof(ElasticDefaults.Logs))) - enabledDefaults |= ElasticDefaults.Logs; - else if (IsSet(target, nameof(ElasticDefaults.All))) - { - enabledDefaults = ElasticDefaults.All; - break; - } - else if (IsSet(target, "none")) - { - enabledDefaults = ElasticDefaults.None; - break; - } - } - return !found ? null : enabledDefaults; - - bool IsSet(string k, string v) - { - var b = k.Trim().Equals(v, InvariantCultureIgnoreCase); - if (b) - found = true; - return b; - } - } - - internal static Signals? SignalsParser(string? s) - { - if (string.IsNullOrWhiteSpace(s)) - return null; - - var enabledDefaults = Signals.None; - var found = false; - - foreach (var target in s.Split(new[] { ';', ',' }, RemoveEmptyEntries)) - { - if (IsSet(target, nameof(Signals.Traces))) - enabledDefaults |= Signals.Traces; - else if (IsSet(target, nameof(Signals.Metrics))) - enabledDefaults |= Signals.Metrics; - else if (IsSet(target, nameof(Signals.Logs))) - enabledDefaults |= Signals.Logs; - else if (IsSet(target, nameof(Signals.All))) - { - enabledDefaults = Signals.All; - break; - } - else if (IsSet(target, "none")) - { - enabledDefaults = Signals.None; - break; - } - } - return !found ? null : enabledDefaults; - - bool IsSet(string k, string v) - { - var b = k.Trim().Equals(v, InvariantCultureIgnoreCase); - if (b) - found = true; - return b; - } - } - internal static string? StringParser(string? s) => !string.IsNullOrEmpty(s) ? s : null; internal static bool? BoolParser(string? s) => diff --git a/src/Elastic.OpenTelemetry/EmptyInstrumentationLifetime.cs b/src/Elastic.OpenTelemetry/Configuration/SdkActivationMethod.cs similarity index 53% rename from src/Elastic.OpenTelemetry/EmptyInstrumentationLifetime.cs rename to src/Elastic.OpenTelemetry/Configuration/SdkActivationMethod.cs index b60d8b0..ede4b2e 100644 --- a/src/Elastic.OpenTelemetry/EmptyInstrumentationLifetime.cs +++ b/src/Elastic.OpenTelemetry/Configuration/SdkActivationMethod.cs @@ -2,11 +2,10 @@ // 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; +namespace Elastic.OpenTelemetry.Configuration; -internal sealed class EmptyInstrumentationLifetime : IInstrumentationLifetime +internal enum SdkActivationMethod { - public void Dispose() { } - - public ValueTask DisposeAsync() => default; + NuGet, + AutoInstrumentation } diff --git a/src/Elastic.OpenTelemetry/Configuration/Signals.cs b/src/Elastic.OpenTelemetry/Configuration/Signals.cs index a0c9840..75ae079 100644 --- a/src/Elastic.OpenTelemetry/Configuration/Signals.cs +++ b/src/Elastic.OpenTelemetry/Configuration/Signals.cs @@ -6,23 +6,35 @@ namespace Elastic.OpenTelemetry.Configuration; -/// Observability signals to enable, defaults to all. +// +// Observability signals to enable, defaults to all. +// [Flags] [EnumExtensions] -public enum Signals +internal enum Signals { - /// No Elastic defaults will be included, acting effectively as a vanilla OpenTelemetry + /// + /// No Elastic defaults will be included, acting effectively as a vanilla OpenTelemetry. + /// None, - /// Include Elastic Distribution of OpenTelemetry .NET tracing defaults + /// + /// Include Elastic Distribution of OpenTelemetry .NET tracing defaults. + /// Traces = 1 << 0, //1 - /// Include Elastic Distribution of OpenTelemetry .NET metrics defaults + /// + /// Include Elastic Distribution of OpenTelemetry .NET metrics defaults. + /// Metrics = 1 << 1, //2 - /// Include Elastic Distribution of OpenTelemetry .NET logging defaults + /// + /// Include Elastic Distribution of OpenTelemetry .NET logging defaults. + /// Logs = 1 << 2, //4 - /// (default) Include all Elastic Distribution of OpenTelemetry .NET logging defaults + /// + /// (Default) Include all Elastic Distribution of OpenTelemetry .NET logging defaults. + /// All = ~0 } diff --git a/src/Elastic.OpenTelemetry/Core/AutoInstrumentationPlugin.cs b/src/Elastic.OpenTelemetry/Core/AutoInstrumentationPlugin.cs new file mode 100644 index 0000000..c706a7c --- /dev/null +++ b/src/Elastic.OpenTelemetry/Core/AutoInstrumentationPlugin.cs @@ -0,0 +1,108 @@ +// 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 Elastic.OpenTelemetry.Configuration; +using Elastic.OpenTelemetry.Core; +using OpenTelemetry; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +#pragma warning disable IDE0130 // Namespace does not match folder structure +namespace Elastic.OpenTelemetry; +#pragma warning restore IDE0130 // Namespace does not match folder structure + +/// +/// Elastic Distribution of OpenTelemetry .NET plugin for Auto Instrumentation. +/// Ensures all signals are rich enough to report to Elastic. +/// +public class AutoInstrumentationPlugin +{ + private readonly BootstrapInfo _bootstrapInfo; + private readonly ElasticOpenTelemetryComponents? _components; + + /// + public AutoInstrumentationPlugin() + { + SetError(); + + _bootstrapInfo = GetBootstrapInfo(out var components); + + if (!_bootstrapInfo.Succeeded) + { + var errorMessage = $"Unable to bootstrap EDOT .NET due to {_bootstrapInfo.Exception!.Message}"; + + Console.Error.WriteLine(errorMessage); + + try // Attempt to log the bootstrap failure to a file + { + var options = new CompositeElasticOpenTelemetryOptions(); + + var directory = options.LogDirectory; + + if (string.IsNullOrEmpty(directory)) + return; + + var process = Process.GetCurrentProcess(); + var logFileName = $"{process.ProcessName}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}_{process.Id}.auto_instrumentation.log"; + + Directory.CreateDirectory(directory); + + using var streamWriter = File.CreateText(Path.Combine(directory, logFileName)); + + streamWriter.WriteLine(errorMessage); + } + catch + { + // Intended empty catch here. We don't want to crash the application. + } + + return; + } + + _components = components!; + } + + // Used for testing + internal virtual BootstrapInfo GetBootstrapInfo(out ElasticOpenTelemetryComponents? components) => + ElasticOpenTelemetry.TryBootstrap(SdkActivationMethod.AutoInstrumentation, out components); + + // Used for testing + internal virtual void SetError() { } + + /// + /// To configure tracing SDK before Auto Instrumentation configured SDK. + /// + public TracerProviderBuilder BeforeConfigureTracerProvider(TracerProviderBuilder builder) => + !_bootstrapInfo.Succeeded || _components is null + ? builder + : builder.UseAutoInstrumentationElasticDefaults(_components); + + /// + /// To configure metrics SDK before Auto Instrumentation configured SDK. + /// /// + public MeterProviderBuilder BeforeConfigureMeterProvider(MeterProviderBuilder builder) => + !_bootstrapInfo.Succeeded || _components is null + ? builder + : builder.UseElasticDefaults(_components); + + /// + /// To configure logs SDK (the method name is the same as for other logs options). + /// + public void ConfigureLogsOptions(OpenTelemetryLoggerOptions options) + { + if (_bootstrapInfo.Succeeded && _components is not null) + options.UseElasticDefaults(_components.Logger); + } + + /// + /// To configure Resource. + /// + public ResourceBuilder ConfigureResource(ResourceBuilder builder) => + !_bootstrapInfo.Succeeded || _components is null + ? builder + : builder.AddElasticDistroAttributes(); +} diff --git a/src/Elastic.OpenTelemetry/Core/BootstrapInfo.cs b/src/Elastic.OpenTelemetry/Core/BootstrapInfo.cs new file mode 100644 index 0000000..637b740 --- /dev/null +++ b/src/Elastic.OpenTelemetry/Core/BootstrapInfo.cs @@ -0,0 +1,27 @@ +// 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 Elastic.OpenTelemetry.Configuration; + +namespace Elastic.OpenTelemetry.Core; + +internal sealed class BootstrapInfo(SdkActivationMethod activationMethod, StackTrace stackTrace, Exception? exception) +{ + public BootstrapInfo(SdkActivationMethod activationMethod, StackTrace stackTrace) + : this(activationMethod, stackTrace, null) { } + + public BootstrapInfo(SdkActivationMethod activationMethod, Exception exception) + : this(activationMethod, new StackTrace(exception), exception) { } + + public SdkActivationMethod ActivationMethod { get; } = activationMethod; + + public StackTrace StackTrace { get; } = stackTrace; + + public Exception? Exception { get; } = exception; + + public bool Succeeded => Exception is null; + + public bool Failed => !Succeeded; +} diff --git a/src/Elastic.OpenTelemetry/Core/BuilderState.cs b/src/Elastic.OpenTelemetry/Core/BuilderState.cs new file mode 100644 index 0000000..5f04384 --- /dev/null +++ b/src/Elastic.OpenTelemetry/Core/BuilderState.cs @@ -0,0 +1,29 @@ +// 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; + +/// +/// Used to store bootstrap information and a single component instance that will later be +/// tracked per builder (OpenTelemetryBuilder, TracerProviderBuilder, MeterProviderBuilder +/// or LoggerProviderBuilder) instance. +/// +internal sealed class BuilderState( + BootstrapInfo bootstrapInfo, + ElasticOpenTelemetryComponents components, + Guid? instanceIdentifier = null) +{ + private int _useElasticDefaultsCounter; + + public BootstrapInfo BootstrapInfo { get; } = bootstrapInfo; + + public ElasticOpenTelemetryComponents Components { get; } = components; + + public Guid InstanceIdentifier { get; } = instanceIdentifier ?? Guid.NewGuid(); + + public void IncrementUseElasticDefaults() => + Interlocked.Increment(ref _useElasticDefaultsCounter); + + public int UseElasticDefaultsCounter => _useElasticDefaultsCounter; +} diff --git a/src/Elastic.OpenTelemetry/Core/ElasticOpenTelemetry.cs b/src/Elastic.OpenTelemetry/Core/ElasticOpenTelemetry.cs new file mode 100644 index 0000000..3630eee --- /dev/null +++ b/src/Elastic.OpenTelemetry/Core/ElasticOpenTelemetry.cs @@ -0,0 +1,179 @@ +// 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 System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Elastic.OpenTelemetry.Configuration; +using Elastic.OpenTelemetry.Diagnostics; +using Elastic.OpenTelemetry.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Elastic.OpenTelemetry.Core; + +internal static class ElasticOpenTelemetry +{ + private static volatile ElasticOpenTelemetryComponents? SharedComponents; + +#pragma warning disable IDE0028 // Simplify collection initialization + internal static readonly ConditionalWeakTable BuilderStateTable = new(); +#pragma warning restore IDE0028 // Simplify collection initialization + + private static readonly Lock Lock = new(); + + internal static BootstrapInfo TryBootstrap(out ElasticOpenTelemetryComponents components) => + TryBootstrap(SdkActivationMethod.NuGet, CompositeElasticOpenTelemetryOptions.DefaultOptions, null, out components); + + internal static BootstrapInfo TryBootstrap(SdkActivationMethod activationMethod, out ElasticOpenTelemetryComponents components) => + TryBootstrap(activationMethod, CompositeElasticOpenTelemetryOptions.DefaultOptions, null, out components); + + internal static BootstrapInfo TryBootstrap( + CompositeElasticOpenTelemetryOptions options, + out ElasticOpenTelemetryComponents components) => + TryBootstrap(SdkActivationMethod.NuGet, options, null, out components); + + internal static BootstrapInfo TryBootstrap( + IServiceCollection? services, + out ElasticOpenTelemetryComponents components) => + TryBootstrap(SdkActivationMethod.NuGet, CompositeElasticOpenTelemetryOptions.DefaultOptions, services, out components); + + internal static BootstrapInfo TryBootstrap( + CompositeElasticOpenTelemetryOptions options, + IServiceCollection? services, + out ElasticOpenTelemetryComponents components) => + TryBootstrap(SdkActivationMethod.NuGet, options, services, out components); + + /// + /// Shared bootstrap routine for the Elastic Distribution of OpenTelemetry .NET. + /// Used to ensure auto instrumentation and manual instrumentation bootstrap the same way. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static BootstrapInfo TryBootstrap( + SdkActivationMethod activationMethod, + CompositeElasticOpenTelemetryOptions options, + IServiceCollection? services, + out ElasticOpenTelemetryComponents components) + { + BootstrapInfo? bootstrapInfo = null; + + try + { + // If an IServiceCollection is provided, we attempt to access any existing + // components to reuse them. + if (TryGetExistingComponents(services, out var existingComponents)) + { + components = existingComponents; + return existingComponents.BootstrapInfo; + } + + // We only expect this to be allocated a handful of times, generally once. + var stackTrace = new StackTrace(true); + + // If an IServiceCollection is provided, but it doesn't yet include any components then + // create new components. + if (services is not null) + { + using (var scope = Lock.EnterScope()) + { + if (TryGetExistingComponents(services, out existingComponents)) + { + components = existingComponents; + return existingComponents.BootstrapInfo; + } + + bootstrapInfo = new BootstrapInfo(activationMethod, stackTrace); + components = CreateComponents(bootstrapInfo, options, stackTrace); + } + + services.AddSingleton(components); + services.AddHostedService(); + + return bootstrapInfo; + } + + // When no IServiceCollection is provided we attempt to avoid bootstrapping more than + // once. The first call into Bootstrap wins and thereafter the same components are reused. + if (TryGetSharedComponents(SharedComponents, stackTrace, out var shared)) + { + // We compare whether the options (values) equal those from the shared components. If the + // values of the options differ, we will not reuse the shared components. + if (shared.Options.Equals(options)) + { + components = shared; + components.Logger.LogSharedComponentsReused(Environment.NewLine, stackTrace); + return shared.BootstrapInfo; + } + + bootstrapInfo = new BootstrapInfo(activationMethod, stackTrace); + components = CreateComponents(bootstrapInfo, options, stackTrace); + components.Logger.LogSharedComponentsNotReused(Environment.NewLine, new StackTrace(true)); + return bootstrapInfo; + } + + using (var scope = Lock.EnterScope()) + { + if (TryGetSharedComponents(SharedComponents, stackTrace, out shared) && shared.Options.Equals(options)) + { + components = shared; + return shared.BootstrapInfo; + } + + // If we get this far, we've been unable to get the shared components + bootstrapInfo = new BootstrapInfo(activationMethod, stackTrace); + components = SharedComponents = CreateComponents(bootstrapInfo, options, stackTrace); + return bootstrapInfo; + } + } + catch (Exception ex) + { + options.AdditionalLogger?.LogCritical(ex, "Unable to bootstrap the Elastic Distribution of OpenTelemetry .NET SDK."); + bootstrapInfo = new(activationMethod, ex); + components = ElasticOpenTelemetryComponents.CreateDefault(bootstrapInfo); + + return bootstrapInfo; + } + + static bool TryGetExistingComponents(IServiceCollection? services, [NotNullWhen(true)] out ElasticOpenTelemetryComponents? components) + { + components = null; + + if (services?.FirstOrDefault(s => s.ServiceType == typeof(ElasticOpenTelemetryComponents)) + ?.ImplementationInstance as ElasticOpenTelemetryComponents is not { } existingComponents) + return false; + + existingComponents.Logger.LogComponentsReused(Environment.NewLine, new StackTrace(true)); + components = existingComponents; + return true; + } + + static bool TryGetSharedComponents(ElasticOpenTelemetryComponents? components, StackTrace stackTrace, + [NotNullWhen(true)] out ElasticOpenTelemetryComponents? sharedComponents) + { + sharedComponents = null; + + if (components is null) + return false; + + sharedComponents = components; + + return true; + } + + static ElasticOpenTelemetryComponents CreateComponents( + BootstrapInfo bootstrap, + CompositeElasticOpenTelemetryOptions options, + StackTrace stackTrace) + { + var logger = new CompositeLogger(options); + var eventListener = new LoggingEventListener(logger, options); + var components = new ElasticOpenTelemetryComponents(bootstrap, logger, eventListener, options); + + logger.LogDistroPreamble(bootstrap.ActivationMethod, components); + logger.LogElasticOpenTelemetryBootstrapped(Environment.NewLine, stackTrace); + + return components; + } + } +} diff --git a/src/Elastic.OpenTelemetry/Core/ElasticOpenTelemetryComponents.cs b/src/Elastic.OpenTelemetry/Core/ElasticOpenTelemetryComponents.cs new file mode 100644 index 0000000..51b94b1 --- /dev/null +++ b/src/Elastic.OpenTelemetry/Core/ElasticOpenTelemetryComponents.cs @@ -0,0 +1,49 @@ +// 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 Elastic.OpenTelemetry.Configuration; +using Elastic.OpenTelemetry.Diagnostics; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Elastic.OpenTelemetry.Core; + +internal sealed class ElasticOpenTelemetryComponents( + BootstrapInfo bootstrapInfo, + CompositeLogger logger, + LoggingEventListener loggingEventListener, + CompositeElasticOpenTelemetryOptions options) : IDisposable, IAsyncDisposable +{ + public CompositeLogger Logger { get; } = logger; + public LoggingEventListener LoggingEventListener { get; } = loggingEventListener; + public CompositeElasticOpenTelemetryOptions Options { get; } = options; + public BootstrapInfo BootstrapInfo { get; } = bootstrapInfo; + + internal void SetAdditionalLogger(ILogger? logger, SdkActivationMethod activationMethod) + { + if (logger is not null && logger is not NullLogger) + Logger.SetAdditionalLogger(logger, activationMethod, this); + } + + // This is used as a rare fallback should an exception occur during bootstrapping + internal static ElasticOpenTelemetryComponents CreateDefault(BootstrapInfo bootstrapInfo) + { + var options = CompositeElasticOpenTelemetryOptions.DefaultOptions; + var logger = new CompositeLogger(options); + var eventListener = new LoggingEventListener(logger, options); + return new(bootstrapInfo, logger, eventListener, options); + } + + public void Dispose() + { + Logger.Dispose(); + LoggingEventListener.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await Logger.DisposeAsync().ConfigureAwait(false); + await LoggingEventListener.DisposeAsync().ConfigureAwait(false); + } +} diff --git a/src/Elastic.OpenTelemetry/Core/GlobalProviderBuilderState.cs b/src/Elastic.OpenTelemetry/Core/GlobalProviderBuilderState.cs new file mode 100644 index 0000000..57520ee --- /dev/null +++ b/src/Elastic.OpenTelemetry/Core/GlobalProviderBuilderState.cs @@ -0,0 +1,22 @@ +// 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; + +/// +/// Each XyzProviderBuilder (e.g. TracerProviderBuilder) uses a shared instance +/// of this to track the number of calls made to their `UseElasticDefaults` methods. +/// Generally, we expect only a single call. While we don't prohibit multiple calls, +/// by tracking the actual number, we can ensure we log this to enhance diagnostics +/// and support later on. +/// +internal sealed class GlobalProviderBuilderState +{ + private int _useElasticDefaultsCounter; + + public int IncrementUseElasticDefaults() => + Interlocked.Increment(ref _useElasticDefaultsCounter); + + public int UseElasticDefaultsCounter => _useElasticDefaultsCounter; +} diff --git a/src/Elastic.OpenTelemetry/Core/SignalBuilder.cs b/src/Elastic.OpenTelemetry/Core/SignalBuilder.cs new file mode 100644 index 0000000..96df4fc --- /dev/null +++ b/src/Elastic.OpenTelemetry/Core/SignalBuilder.cs @@ -0,0 +1,134 @@ +// 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 System.Diagnostics.CodeAnalysis; +using Elastic.OpenTelemetry.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Elastic.OpenTelemetry.Core; + +internal static class SignalBuilder +{ + /// + /// Hold common logic for configuring a builder, either a TracerProviderBuilder, + /// MeterProviderBuilder or LoggingProviderBuilder. + /// + public static bool ConfigureBuilder( + string methodName, + string builderName, + T builder, + GlobalProviderBuilderState globalProviderBuilderState, + CompositeElasticOpenTelemetryOptions? options, + IServiceCollection? services, + Action configure, + [NotNullWhen(true)] ref ElasticOpenTelemetryComponents? components) where T : class + { + var callCount = globalProviderBuilderState.IncrementUseElasticDefaults(); + + // If we are provided with options and components, we can avoid attempting to bootstrap again. + // This scenario occurs if for example `AddElasticOpenTelemetry` is called multipled times + // on the same `IServiceCollection`. In this case, a new `OpenTelemetryBuilder` would be + // created (inside the SDK) for each call to `AddOpenTelemetry`, so the `BuilderState`, is + // not be useful. `TryBootstrap` would be eventually reuse cached components registered + // against the `IServiceCollection`, but we can still be more efficient to avoid calling that + // code in this particular case by shortcutting and returning early. + if (options is not null && components is not null) + { + ValidateGlobalCallCount(methodName, builderName, options, components, callCount); + configure(builder, components); + return true; + } + + // This will later be set to false if `CreateState`, is invoked. + var existingStateFound = true; + + // Note: This incurs a closure, but should only be called a few times at most during application + // startup, so we are not too concerned with the performance impact. + var state = ElasticOpenTelemetry.BuilderStateTable.GetValue(builder, _ => + CreateState(builder, builderName, services, ref options, ref existingStateFound)); + + components = state.Components; + + Debug.Assert(components is not null); + + ValidateGlobalCallCount(methodName, builderName, options, components, callCount); + + // 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. + state.IncrementUseElasticDefaults(); + + if (state.UseElasticDefaultsCounter > 1) + components.Logger.LogWarning("The `{MethodName}` method has been called {UseElasticDefaultsCount} " + + "times on the same `{BuilderType}` (instance: {BuilderInstanceId}). This method is " + + "expected to be invoked a maximum of one time.", methodName, + state.UseElasticDefaultsCounter, builderName, state.InstanceIdentifier); + + if (existingStateFound && state.BootstrapInfo.Succeeded) + { + // If `UseElasticDefaults` is invoked more than once on the same builder instance, + // we reuse the same components and skip the configure action. + + components.Logger.LogTrace("Existing components have been found for the current {Builder} " + + "instance (instance: {BuilderInstanceId}) and will be reused.", builderName, + state.InstanceIdentifier); + + return true; + } + + configure(builder, components); + + if (state.BootstrapInfo.Failed) + { + components.Logger.LogError("Unable to bootstrap EDOT."); + + // Remove the builder from the state table so that if a later call attempts to configure + // it, we try again. + ElasticOpenTelemetry.BuilderStateTable.Remove(builder); + } + + return state.BootstrapInfo.Succeeded; + + static BuilderState CreateState(T builder, string builderName, IServiceCollection? services, + [NotNull] ref CompositeElasticOpenTelemetryOptions? options, ref bool existingStateFound) + { + existingStateFound = false; + + var instanceId = Guid.NewGuid(); // Used in logging to track duplicate calls to the same builder + + // 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. + options?.AdditionalLogger?.LogTrace($"No existing {nameof(ElasticOpenTelemetryComponents)} have " + + "been found for the current {Builder} (instance: {BuilderInstanceId}, hash: {BuilderHashCode}).", + builderName, instanceId, builder.GetHashCode()); + + options ??= CompositeElasticOpenTelemetryOptions.DefaultOptions; + + var bootStrapInfo = ElasticOpenTelemetry.TryBootstrap(options, services, out var components); + var builderState = new BuilderState(bootStrapInfo, components, instanceId); + + components.Logger?.LogTrace("Storing state for the current {Builder} " + + "instance (instance: {BuilderInstanceId}, hash: {BuilderHashCode}).", + builderName, builderState.InstanceIdentifier, builder.GetHashCode()); + + return builderState; + } + + static void ValidateGlobalCallCount(string methodName, string builderName, CompositeElasticOpenTelemetryOptions? options, + ElasticOpenTelemetryComponents? components, int callCount) + { + if (callCount > 1) + { + var logger = components is not null ? components.Logger : options?.AdditionalLogger; + logger?.LogWarning("The `{MethodName}` method has been called {UseElasticDefaultsCount} " + + "times across all {Builder} instances. This method is generally expected to be invoked " + + "once. Consider reviewing the usage at the callsite(s).", methodName, + callCount, builderName); + } + } + } +} diff --git a/src/Elastic.OpenTelemetry/VersionHelper.cs b/src/Elastic.OpenTelemetry/Core/VersionHelper.cs similarity index 97% rename from src/Elastic.OpenTelemetry/VersionHelper.cs rename to src/Elastic.OpenTelemetry/Core/VersionHelper.cs index 3cec814..d1b6274 100644 --- a/src/Elastic.OpenTelemetry/VersionHelper.cs +++ b/src/Elastic.OpenTelemetry/Core/VersionHelper.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information using System.Reflection; -namespace Elastic.OpenTelemetry; +namespace Elastic.OpenTelemetry.Core; internal static class VersionHelper { diff --git a/src/Elastic.OpenTelemetry/DependencyInjection/OpenTelemetryServicesExtensions.cs b/src/Elastic.OpenTelemetry/DependencyInjection/OpenTelemetryServicesExtensions.cs deleted file mode 100644 index b76a403..0000000 --- a/src/Elastic.OpenTelemetry/DependencyInjection/OpenTelemetryServicesExtensions.cs +++ /dev/null @@ -1,32 +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 Microsoft.Extensions.DependencyInjection; - -/// -/// Extensions for to add OpenTelemetry services using Elastic defaults. -/// -// ReSharper disable once CheckNamespace -public static class OpenTelemetryServicesExtensions -{ - // ReSharper disable RedundantNameQualifier -#pragma warning disable IDE0001 - /// - /// - /// Uses defaults particularly well suited for Elastic's Observability offering because Elastic.OpenTelemetry is referenced - /// - /// - /// - /// - /// - /// - /// - /// - public static global::OpenTelemetry.IOpenTelemetryBuilder AddOpenTelemetry( - this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services - ) => services.AddElasticOpenTelemetry(); - - // ReSharper enable RedundantNameQualifier -#pragma warning restore IDE0001 -} diff --git a/src/Elastic.OpenTelemetry/DependencyInjection/ServiceCollectionExtensions.cs b/src/Elastic.OpenTelemetry/DependencyInjection/ServiceCollectionExtensions.cs deleted file mode 100644 index 6ca0388..0000000 --- a/src/Elastic.OpenTelemetry/DependencyInjection/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,70 +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 Elastic.OpenTelemetry; -using Elastic.OpenTelemetry.Configuration; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using OpenTelemetry; - -// ReSharper disable once CheckNamespace -namespace Microsoft.Extensions.DependencyInjection; - -/// -/// Extension methods for . -/// -public static class ServiceCollectionExtensions -{ - /// - /// Registers the Elastic OpenTelemetry builder with the provided . - /// - /// The for adding services. - /// - /// An instance of that can be used to further configure the - /// OpenTelemetry SDK. - /// - public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollection services) => - services.AddElasticOpenTelemetry(new ElasticOpenTelemetryBuilderOptions { Services = services }); - - /// - /// Registers the Elastic OpenTelemetry builder with the provided . - /// - /// The for adding services. - /// - /// An instance from which to attempt binding of configuration values. - /// - /// - /// An instance of that can be used to further configure the - /// OpenTelemetry SDK. - /// - public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollection services, IConfiguration configuration) => - services.AddElasticOpenTelemetry(new ElasticOpenTelemetryBuilderOptions - { - Services = services, - DistroOptions = new ElasticOpenTelemetryOptions(configuration) - }); - - /// - /// Registers the Elastic OpenTelemetry builder with the provided . - /// - /// The for adding services. - /// for the initial OpenTelemetry registration. - /// - /// An instance of that can be used to further configure the - /// OpenTelemetry SDK. - /// - public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollection services, ElasticOpenTelemetryBuilderOptions options) - { - var descriptor = services.SingleOrDefault(s => s.ServiceType == typeof(ElasticOpenTelemetryBuilder)); - - if (descriptor?.ImplementationInstance is ElasticOpenTelemetryBuilder builder) - { - builder.Logger.LogWarning($$"""{{nameof(AddElasticOpenTelemetry)}} was called more than once {StackTrace}""", Environment.StackTrace.TrimStart()); - return builder; - } - - options = options.Services is null ? options with { Services = services } : options; - return new ElasticOpenTelemetryBuilder(options); - } -} diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/AgentLoggingHelpers.cs b/src/Elastic.OpenTelemetry/Diagnostics/AgentLoggingHelpers.cs similarity index 69% rename from src/Elastic.OpenTelemetry/Diagnostics/Logging/AgentLoggingHelpers.cs rename to src/Elastic.OpenTelemetry/Diagnostics/AgentLoggingHelpers.cs index 9fb96f7..372224b 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/Logging/AgentLoggingHelpers.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/AgentLoggingHelpers.cs @@ -5,11 +5,18 @@ using System.Diagnostics; using Microsoft.Extensions.Logging; -namespace Elastic.OpenTelemetry.Diagnostics.Logging; +namespace Elastic.OpenTelemetry.Diagnostics; internal static class AgentLoggingHelpers { - public static void WriteLogLine(this ILogger logger, Activity? activity, int managedThreadId, DateTime dateTime, LogLevel logLevel, string logLine, string? spanId) + public static void WriteLogLine( + this ILogger logger, + Activity? activity, + int managedThreadId, + DateTime dateTime, + LogLevel logLevel, + string logLine, + string? spanId) { var state = new LogState { @@ -18,6 +25,7 @@ public static void WriteLogLine(this ILogger logger, Activity? activity, int man DateTime = dateTime, SpanId = spanId }; + logger.Log(logLevel, 0, state, null, (_, _) => logLine); } } diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/CompositeLogger.cs b/src/Elastic.OpenTelemetry/Diagnostics/CompositeLogger.cs similarity index 61% rename from src/Elastic.OpenTelemetry/Diagnostics/Logging/CompositeLogger.cs rename to src/Elastic.OpenTelemetry/Diagnostics/CompositeLogger.cs index 6243819..0b7950b 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/Logging/CompositeLogger.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/CompositeLogger.cs @@ -3,25 +3,26 @@ // See the LICENSE file in the project root for more information using Elastic.OpenTelemetry.Configuration; +using Elastic.OpenTelemetry.Core; using Microsoft.Extensions.Logging; -namespace Elastic.OpenTelemetry.Diagnostics.Logging; +namespace Elastic.OpenTelemetry.Diagnostics; /// -/// A composite logger for use inside the distribution which logs to the +/// A composite logger for use inside the distribution which logs to the /// and optionally an additional . /// /// -/// If disposed, triggers disposal of the . +/// If disposed, triggers disposal of the . /// -internal sealed class CompositeLogger(ElasticOpenTelemetryBuilderOptions options) : IDisposable, IAsyncDisposable, ILogger +internal sealed class CompositeLogger(CompositeElasticOpenTelemetryOptions options) : IDisposable, IAsyncDisposable, ILogger { public const string LogCategory = "Elastic.OpenTelemetry"; - public FileLogger FileLogger { get; } = new(options.DistroOptions); - public StandardOutLogger ConsoleLogger { get; } = new(options.DistroOptions); + public FileLogger FileLogger { get; } = new(options); + public StandardOutLogger ConsoleLogger { get; } = new(options); - private ILogger? _additionalLogger = options.Logger; + private ILogger? _additionalLogger = options.AdditionalLogger; private bool _isDisposed; public void Dispose() @@ -51,19 +52,29 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except if (ConsoleLogger.IsEnabled(logLevel)) ConsoleLogger.Log(logLevel, eventId, state, exception, formatter); - if (_additionalLogger == null) - return; - - if (_additionalLogger.IsEnabled(logLevel)) + if (_additionalLogger is not null && _additionalLogger.IsEnabled(logLevel)) _additionalLogger.Log(logLevel, eventId, state, exception, formatter); } public bool LogFileEnabled => FileLogger.FileLoggingEnabled; + public string LogFilePath => FileLogger.LogFilePath ?? string.Empty; - public void SetAdditionalLogger(ILogger? logger) => _additionalLogger ??= logger; + public void SetAdditionalLogger(ILogger logger, SdkActivationMethod activationMethod, ElasticOpenTelemetryComponents components) + { + if (HasAdditionalLogger) + return; + + components.Logger.LogInformation("Added additional ILogger to composite logger."); + + _additionalLogger = logger; + _additionalLogger.LogDistroPreamble(activationMethod, components); + } + + internal bool HasAdditionalLogger => _additionalLogger is not null; - public bool IsEnabled(LogLevel logLevel) => ConsoleLogger.IsEnabled(logLevel) || FileLogger.IsEnabled(logLevel) || (_additionalLogger?.IsEnabled(logLevel) ?? false); + public bool IsEnabled(LogLevel logLevel) => + ConsoleLogger.IsEnabled(logLevel) || FileLogger.IsEnabled(logLevel) || (_additionalLogger?.IsEnabled(logLevel) ?? false); public IDisposable BeginScope(TState state) where TState : notnull => new CompositeDisposable(FileLogger.BeginScope(state), _additionalLogger?.BeginScope(state)); diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/FileLogger.cs b/src/Elastic.OpenTelemetry/Diagnostics/FileLogger.cs similarity index 53% rename from src/Elastic.OpenTelemetry/Diagnostics/Logging/FileLogger.cs rename to src/Elastic.OpenTelemetry/Diagnostics/FileLogger.cs index 19d49e6..420401b 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/Logging/FileLogger.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/FileLogger.cs @@ -8,7 +8,7 @@ using Elastic.OpenTelemetry.Configuration; using Microsoft.Extensions.Logging; -namespace Elastic.OpenTelemetry.Diagnostics.Logging; +namespace Elastic.OpenTelemetry.Diagnostics; internal sealed class FileLogger : IDisposable, IAsyncDisposable, ILogger { @@ -29,41 +29,62 @@ internal sealed class FileLogger : IDisposable, IAsyncDisposable, ILogger private readonly LoggerExternalScopeProvider _scopeProvider; - public FileLogger(ElasticOpenTelemetryOptions options) + public FileLogger(CompositeElasticOpenTelemetryOptions options) { _scopeProvider = new LoggerExternalScopeProvider(); _configuredLogLevel = options.LogLevel; + FileLoggingEnabled = options.GlobalLogEnabled && options.LogTargets.HasFlag(LogTargets.File); if (!FileLoggingEnabled) return; - var process = Process.GetCurrentProcess(); - // When ordered by filename, we get see logs from the same process grouped, then ordered by oldest to newest, then the PID for that instance - var logFileName = $"{process.ProcessName}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}_{process.Id}.instrumentation.log"; + try + { + var process = Process.GetCurrentProcess(); - var logDirectory = options.LogDirectory; - LogFilePath = Path.Combine(logDirectory, logFileName); + // 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 = $"{process.ProcessName}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}_{process.Id}.instrumentation.log"; + var logDirectory = options.LogDirectory; - if (!Directory.Exists(logDirectory)) - Directory.CreateDirectory(logDirectory); + LogFilePath = Path.Combine(logDirectory, logFileName); - //StreamWriter.Dispose disposes underlying stream too - var stream = new FileStream(LogFilePath, FileMode.OpenOrCreate, FileAccess.Write); - _streamWriter = new StreamWriter(stream, Encoding.UTF8); + if (!Directory.Exists(logDirectory)) + Directory.CreateDirectory(logDirectory); - WritingTask = Task.Run(async () => - { - while (await _channel.Reader.WaitToReadAsync().ConfigureAwait(false) && !_disposing) - while (_channel.Reader.TryRead(out var logLine) && !_disposing) - await _streamWriter.WriteLineAsync(logLine).ConfigureAwait(false); + //StreamWriter.Dispose disposes underlying stream too + var stream = new FileStream(LogFilePath, FileMode.OpenOrCreate, FileAccess.Write); + + _streamWriter = new StreamWriter(stream, Encoding.UTF8); + + WritingTask = Task.Run(async () => + { + while (await _channel.Reader.WaitToReadAsync().ConfigureAwait(false) && !_disposing) + while (_channel.Reader.TryRead(out var logLine) && !_disposing) + await _streamWriter.WriteLineAsync(logLine).ConfigureAwait(false); + + _syncDisposeWaitHandle.Set(); + }); - _syncDisposeWaitHandle.Set(); - }); + _streamWriter.AutoFlush = true; // Ensure we don't lose logs by not flushing to the file. - _streamWriter.AutoFlush = true; // Ensure we don't lose logs by not flushing to the file. + if (options?.AdditionalLogger is not null) + options?.AdditionalLogger.LogInformation("File logging for EDOT .NET enabled. Logs are being written to '{LogFilePath}'", LogFilePath); + else + Console.Out.WriteLine($"File logging for EDOT .NET enabled. Logs are being written to '{LogFilePath}'"); + + return; + } + catch (Exception ex) + { + if (options?.AdditionalLogger is not null) + options?.AdditionalLogger.LogError(ex, "Failed to set up file logging due to exception: {ExceptionMessage}.", ex.Message); + else + Console.Error.WriteLine($"Failed to set up file logging due to exception: {ex.Message}."); + } - FileLoggingEnabled = true; + // If we fall through the `try` block, consider file logging disabled. + FileLoggingEnabled = false; } public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) @@ -93,7 +114,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except public void Dispose() { - //tag that we are running a dispose this allows running tasks and spin waits to short circuit + // Tag that we are running a dispose. This allows running tasks and spin waits to short circuit _disposing = true; _channel.Writer.TryComplete(); @@ -104,7 +125,7 @@ public void Dispose() public async ValueTask DisposeAsync() { - //tag that we are running a dispose this allows running tasks and spin waits to short circuit + // Tag that we are running a dispose. This allows running tasks and spin waits to short circuit _disposing = true; _channel.Writer.TryComplete(); diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/LogFormatter.cs b/src/Elastic.OpenTelemetry/Diagnostics/LogFormatter.cs similarity index 88% rename from src/Elastic.OpenTelemetry/Diagnostics/Logging/LogFormatter.cs rename to src/Elastic.OpenTelemetry/Diagnostics/LogFormatter.cs index caf6895..5d6e199 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/Logging/LogFormatter.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/LogFormatter.cs @@ -6,7 +6,7 @@ using System.Text; using Microsoft.Extensions.Logging; -namespace Elastic.OpenTelemetry.Diagnostics.Logging; +namespace Elastic.OpenTelemetry.Diagnostics; internal static class LogFormatter { @@ -28,11 +28,19 @@ public static string Format(LogLevel logLevel, EventId eventId, TState s var builder = StringBuilderCache.Acquire(); + // Force exceptions to be written as errors + if (exception is not null) + logLevel = LogLevel.Error; + WriteLogPrefix(managedThreadId, dateTime, logLevel, builder, spanId ?? activity?.SpanId.ToHexString() ?? string.Empty); var message = formatter(state, exception); builder.Append(message); - //todo force Exception to be written as error + if (eventId != default) + if (!string.IsNullOrEmpty(eventId.Name)) + builder.Append(" {{EventId: " + eventId.Id + ", EventName: " + eventId.Name + "}}"); + else + builder.Append(" {{EventId: " + eventId.Id + "}}"); if (activity is not null) { @@ -92,5 +100,4 @@ private static void WriteLogPrefix(int managedThreadId, DateTime dateTime, LogLe for (var i = 0; i < padding; i++) builder.Append(' '); } - } diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/LogLevelHelpers.cs b/src/Elastic.OpenTelemetry/Diagnostics/LogLevelHelpers.cs similarity index 97% rename from src/Elastic.OpenTelemetry/Diagnostics/Logging/LogLevelHelpers.cs rename to src/Elastic.OpenTelemetry/Diagnostics/LogLevelHelpers.cs index 9e1f939..9b3ba70 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/Logging/LogLevelHelpers.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/LogLevelHelpers.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Logging; -namespace Elastic.OpenTelemetry.Diagnostics.Logging; +namespace Elastic.OpenTelemetry.Diagnostics; internal static class LogLevelHelpers { diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/LogState.cs b/src/Elastic.OpenTelemetry/Diagnostics/LogState.cs similarity index 95% rename from src/Elastic.OpenTelemetry/Diagnostics/Logging/LogState.cs rename to src/Elastic.OpenTelemetry/Diagnostics/LogState.cs index 7ba27a2..21ad32e 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/Logging/LogState.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/LogState.cs @@ -5,7 +5,7 @@ using System.Collections; using System.Diagnostics; -namespace Elastic.OpenTelemetry.Diagnostics.Logging; +namespace Elastic.OpenTelemetry.Diagnostics; internal class LogState : IReadOnlyList> { diff --git a/src/Elastic.OpenTelemetry/Diagnostics/LoggerMessages.cs b/src/Elastic.OpenTelemetry/Diagnostics/LoggerMessages.cs index 26686f1..d10c4a5 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/LoggerMessages.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/LoggerMessages.cs @@ -2,72 +2,148 @@ // 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.Buffers; using System.Diagnostics; using Elastic.OpenTelemetry.Configuration; -using Elastic.OpenTelemetry.Diagnostics.Logging; +using Elastic.OpenTelemetry.Core; using Microsoft.Extensions.Logging; namespace Elastic.OpenTelemetry.Diagnostics; internal static partial class LoggerMessages { -#pragma warning disable SYSLIB1006 // Multiple logging methods cannot use the same event id within a class - // We explictly reuse the same event ID and this is the same log message, but with different types for the structured data + [LoggerMessage(EventId = 1, EventName = "Bootstapped", Level = LogLevel.Information, Message = "Elastic OpenTelemetry bootstrap invoked. {newline}{StackTrace}", SkipEnabledCheck = true)] + public static partial void LogElasticOpenTelemetryBootstrapped(this ILogger logger, string newline, StackTrace stackTrace); + + [LoggerMessage(EventId = 2, EventName = "SharedComponentsCreated", Level = LogLevel.Debug, Message = "Shared components created.")] + public static partial void LogSharedComponentsCreated(this ILogger logger); + + [LoggerMessage(EventId = 3, EventName = "SharedComponentsReused", Level = LogLevel.Debug, Message = "Reusing existing shared components. {newline}{StackTrace}", SkipEnabledCheck = true)] + public static partial void LogSharedComponentsReused(this ILogger logger, string newline, StackTrace stackTrace); + + [LoggerMessage(EventId = 4, EventName = "SharedComponentsNotReused", Level = LogLevel.Debug, Message = "Unable to reuse existing shared components as the provided `CompositeElasticOpenTelemetryOptions` differ. {newline}{StackTrace}", SkipEnabledCheck = true)] + public static partial void LogSharedComponentsNotReused(this ILogger logger, string newline, StackTrace stackTrace); + + [LoggerMessage(EventId = 5, EventName = "ServiceCollectionComponentsReused", Level = LogLevel.Debug, Message = "Reusing existing components on IServiceCollection. {newline}{StackTrace}")] + public static partial void LogComponentsReused(this ILogger logger, string newline, StackTrace stackTrace); + + [LoggerMessage(EventId = 6, EventName = "ConfiguredSignalProvider", Level = LogLevel.Debug, Message = "Configured EDOT defaults for {Signal} via the {Provider}.")] + public static partial void LogConfiguredSignalProvider(this ILogger logger, string signal, string provider); + + [LoggerMessage(EventId = 7, EventName = "SkippingOtlpExporter", Level = LogLevel.Information, Message = "Skipping OTLP exporter for {Signal} based on the provided `ElasticOpenTelemetryOptions` via the {Provider}.")] + public static partial void LogSkippingOtlpExporter(this ILogger logger, string signal, string provider); - [LoggerMessage(EventId = 100, Level = LogLevel.Debug, Message = "{ProcessorName} found `{AttributeName}` attribute with value '{AttributeValue}' on the span.")] + [LoggerMessage(EventId = 8, EventName = "LocatedInstrumentationAssembly", Level = LogLevel.Information, Message = "Located {AssemblyFilename} in {Path}.")] + public static partial void LogLocatedInstrumentationAssembly(this ILogger logger, string assemblyFilename, string path); + + [LoggerMessage(EventId = 9, EventName = "AddedInstrumentation", Level = LogLevel.Information, Message = "Added {InstrumentationName} to {Provider}.")] + public static partial void LogAddedInstrumentation(this ILogger logger, string instrumentationName, string provider); + + // We explictly reuse the same event ID and this is the same log message, but with different types for the structured data + [LoggerMessage(EventId = 9, Level = LogLevel.Debug, Message = "{ProcessorName} found `{AttributeName}` attribute with value '{AttributeValue}' on the span.")] internal static partial void FoundTag(this ILogger logger, string processorName, string attributeName, string attributeValue); - [LoggerMessage(EventId = 100, Level = LogLevel.Debug, Message = "{ProcessorName} found `{AttributeName}` attribute with value '{AttributeValue}' on the span.")] + // We explictly reuse the same event ID and this is the same log message, but with different types for the structured data + [LoggerMessage(EventId = 9, Level = LogLevel.Debug, Message = "{ProcessorName} found `{AttributeName}` attribute with value '{AttributeValue}' on the span.")] internal static partial void FoundTag(this ILogger logger, string processorName, string attributeName, int attributeValue); - [LoggerMessage(EventId = 101, Level = LogLevel.Debug, Message = "{ProcessorName} set `{AttributeName}` attribute with value '{AttributeValue}' on the span.")] + // We explictly reuse the same event ID and this is the same log message, but with different types for the structured data + [LoggerMessage(EventId = 10, Level = LogLevel.Debug, Message = "{ProcessorName} set `{AttributeName}` attribute with value '{AttributeValue}' on the span.")] internal static partial void SetTag(this ILogger logger, string processorName, string attributeName, string attributeValue); - [LoggerMessage(EventId = 101, Level = LogLevel.Debug, Message = "{ProcessorName} set `{AttributeName}` attribute with value '{AttributeValue}' on the span.")] + // We explictly reuse the same event ID and this is the same log message, but with different types for the structured data + [LoggerMessage(EventId = 10, Level = LogLevel.Debug, Message = "{ProcessorName} set `{AttributeName}` attribute with value '{AttributeValue}' on the span.")] internal static partial void SetTag(this ILogger logger, string processorName, string attributeName, int attributeValue); -#pragma warning restore SYSLIB1006 // Multiple logging methods cannot use the same event id within a class - [LoggerMessage(EventId = 20, Level = LogLevel.Debug, Message = "Added '{ProcessorTypeName}' processor to '{BuilderTypeName}'.")] + [LoggerMessage(EventId = 11, Level = LogLevel.Debug, Message = "Added '{ProcessorTypeName}' processor to '{BuilderTypeName}'.")] public static partial void LogProcessorAdded(this ILogger logger, string processorTypeName, string builderTypeName); - [LoggerMessage(EventId = 21, Level = LogLevel.Debug, Message = "Added '{MeterName}' meter to '{BuilderTypeName}'.")] + [LoggerMessage(EventId = 12, Level = LogLevel.Debug, Message = "Added '{MeterName}' meter to '{BuilderTypeName}'.")] public static partial void LogMeterAdded(this ILogger logger, string meterName, string builderTypeName); - public static void LogAgentPreamble(this ILogger logger) + [LoggerMessage(EventId = 13, Level = LogLevel.Error, Message = "Unable to configure {BuilderTypeName} with EDOT .NET logging defaults.")] + public static partial void UnableToConfigureLoggingDefaultsError(this ILogger logger, string builderTypeName); + + public static void LogDistroPreamble(this ILogger logger, SdkActivationMethod activationMethod, ElasticOpenTelemetryComponents components) { - var process = Process.GetCurrentProcess(); - logger.LogInformation("Elastic Distribution of OpenTelemetry .NET: {AgentInformationalVersion}", VersionHelper.InformationalVersion); - if (logger is CompositeLogger distributionLogger) + // This occurs once per initialisation, so we don't use `LoggerMessage`s. + + logger.LogInformation("Elastic Distribution of OpenTelemetry (EDOT) .NET: {AgentInformationalVersion}", VersionHelper.InformationalVersion); + + if (components.Logger.LogFileEnabled) { - if (distributionLogger.LogFileEnabled) - logger.LogInformation("Elastic Distribution of OpenTelemetry .NET, log file: {LogFilePath}", distributionLogger.LogFilePath); - else - logger.LogInformation("Elastic Distribution of OpenTelemetry .NET, log file: "); + logger.LogInformation("EDOT log file: {LogFilePath}", components.Logger.LogFilePath); } + else + { + logger.LogInformation("EDOT log file: "); + } + + logger.LogInformation("Activation method: {ActivationMethod}", activationMethod.ToString()); - logger.LogInformation("Process ID: {ProcessId}", process.Id); - logger.LogInformation("Process name: {ProcessName}", process.ProcessName); -#if NET6_0_OR_GREATER - logger.LogInformation("Process path: {ProcessPath}", Environment.ProcessPath); +#if NET8_0 + logger.LogDebug("Matched TFM: {TargetFrameworkMoniker}", "net8.0"); +#elif NET9_0 + logger.LogDebug("Matched TFM: {TargetFrameworkMoniker}", "net9.0"); +#elif NETSTANDARD2_0 + logger.LogDebug("Matched TFM: {TargetFrameworkMoniker}", "netstandard2.0"); +#elif NETSTANDARD2_1 + logger.LogDebug("Matched TFM: {TargetFrameworkMoniker}", "netstandard2.1"); +#elif NET462 + logger.LogDebug("Matched TFM: {TargetFrameworkMoniker}", "net462"); #else - logger.LogInformation("Process path: {ProcessPath}", ""); + logger.LogDebug("Matched TFM: {TargetFrameworkMoniker}", ""); #endif - logger.LogInformation("Process started: {ProcessStartTime:yyyy-MM-dd HH:mm:ss.fff}", process.StartTime.ToUniversalTime()); - logger.LogInformation("Machine name: {MachineName}", Environment.MachineName); - logger.LogInformation("Process username: {UserName}", Environment.UserName); - logger.LogInformation("User domain name: {UserDomainName}", Environment.UserDomainName); - // Don't think we should log this for PII purposes? - //logger.LogInformation("Command line: {ProcessCommandLine}", Environment.CommandLine); - logger.LogInformation("Command current directory: {CurrentDirectory}", Environment.CurrentDirectory); - logger.LogInformation("Processor count: {ProcessorCount}", Environment.ProcessorCount); - logger.LogInformation("OS version: {OSVersion}", Environment.OSVersion); - logger.LogInformation("CLR version: {CLRVersion}", Environment.Version); + try + { + var process = Process.GetCurrentProcess(); + + logger.LogDebug("Process ID: {ProcessId}", process.Id); + logger.LogDebug("Process name: {ProcessName}", process.ProcessName); + + logger.LogDebug("Process started: {ProcessStartTime:yyyy-MM-dd HH:mm:ss.fff}", process.StartTime.ToUniversalTime()); + } + catch + { + // GetCurrentProcess can throw PlatformNotSupportedException + } + +#if NET + logger.LogDebug("Process path: {ProcessPath}", Environment.ProcessPath); +#elif NETSTANDARD + logger.LogDebug("Process path: {ProcessPath}", ""); +#elif NETFRAMEWORK + logger.LogDebug("Process path: {ProcessPath}", ""); +#endif + + logger.LogDebug("Machine name: {MachineName}", Environment.MachineName); + logger.LogDebug("Process username: {UserName}", Environment.UserName); + logger.LogDebug("User domain name: {UserDomainName}", Environment.UserDomainName); + logger.LogDebug("Command current directory: {CurrentDirectory}", Environment.CurrentDirectory); + logger.LogDebug("Processor count: {ProcessorCount}", Environment.ProcessorCount); + logger.LogDebug("OS version: {OSVersion}", Environment.OSVersion); + logger.LogDebug("CLR version: {CLRVersion}", Environment.Version); string[] environmentVariables = [ EnvironmentVariables.OTEL_DOTNET_AUTO_LOG_DIRECTORY, - EnvironmentVariables.OTEL_LOG_LEVEL + EnvironmentVariables.OTEL_LOG_LEVEL, + EnvironmentVariables.ELASTIC_OTEL_LOG_TARGETS, + EnvironmentVariables.DOTNET_RUNNING_IN_CONTAINER, + EnvironmentVariables.ELASTIC_OTEL_SKIP_OTLP_EXPORTER, + EnvironmentVariables.OTEL_EXPORTER_OTLP_ENDPOINT, + EnvironmentVariables.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + EnvironmentVariables.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, + EnvironmentVariables.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, + EnvironmentVariables.OTEL_EXPORTER_OTLP_TIMEOUT, + EnvironmentVariables.OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, + EnvironmentVariables.OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, + EnvironmentVariables.OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, + EnvironmentVariables.OTEL_EXPORTER_OTLP_PROTOCOL, + EnvironmentVariables.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, + EnvironmentVariables.OTEL_EXPORTER_OTLP_METRICS_PROTOCOL, + EnvironmentVariables.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL, ]; foreach (var variable in environmentVariables) @@ -75,9 +151,75 @@ public static void LogAgentPreamble(this ILogger logger) var envVarValue = Environment.GetEnvironmentVariable(variable); if (string.IsNullOrEmpty(envVarValue)) + { logger.LogDebug("Environment variable '{EnvironmentVariable}' is not configured.", variable); + } else + { logger.LogDebug("Environment variable '{EnvironmentVariable}' = '{EnvironmentVariableValue}'.", variable, envVarValue); + } } + + // This next set of env vars might include sensitive information, so we redact the values. + string[] headerEnvironmentVariables = + [ + EnvironmentVariables.OTEL_EXPORTER_OTLP_HEADERS, + EnvironmentVariables.OTEL_EXPORTER_OTLP_TRACES_HEADERS, + EnvironmentVariables.OTEL_EXPORTER_OTLP_METRICS_HEADERS, + EnvironmentVariables.OTEL_EXPORTER_OTLP_LOGS_HEADERS, + ]; + + foreach (var variable in headerEnvironmentVariables) + { + var envVarValue = Environment.GetEnvironmentVariable(variable); + + const string redacted = "="; + + if (string.IsNullOrEmpty(envVarValue)) + { + logger.LogDebug("Environment variable '{EnvironmentVariable}' is not configured.", variable); + } + else + { + var valueSpan = envVarValue.AsSpan(); + var buffer = ArrayPool.Shared.Rent(1024); + var bufferSpan = buffer.AsSpan(); + var position = 0; + var count = 0; + + while (true) + { + var indexOfComma = valueSpan.IndexOf(','); + var header = valueSpan.Slice(0, indexOfComma > 0 ? indexOfComma : valueSpan.Length); + + var indexOfEquals = valueSpan.IndexOf('='); + + if (indexOfEquals > 0) + { + var key = header.Slice(0, indexOfEquals); + var value = header.Slice(indexOfEquals + 1); + + if (count++ > 0) + bufferSpan[position++] = ','; + + key.CopyTo(bufferSpan.Slice(position)); + position += key.Length; + redacted.AsSpan().CopyTo(bufferSpan.Slice(position)); + position += redacted.Length; + } + + if (indexOfComma <= 0) + break; + + valueSpan = valueSpan.Slice(indexOfComma + 1); + } + + logger.LogDebug("Environment variable '{EnvironmentVariable}' = '{EnvironmentVariableValue}'.", variable, bufferSpan.Slice(0, position).ToString()); + + ArrayPool.Shared.Return(buffer); + } + } + + components.Options.LogConfigSources(logger); } } diff --git a/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs b/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs index 3eeed6f..4648068 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs @@ -6,49 +6,44 @@ using System.Text; using System.Text.RegularExpressions; using Elastic.OpenTelemetry.Configuration; -using Elastic.OpenTelemetry.Diagnostics.Logging; using Microsoft.Extensions.Logging; namespace Elastic.OpenTelemetry.Diagnostics; +/// +/// Enables logging of OpenTelemetry-SDK event source events. +/// internal sealed #if NET8_0_OR_GREATER partial #endif - class LoggingEventListener : EventListener, IAsyncDisposable + class LoggingEventListener(ILogger logger, CompositeElasticOpenTelemetryOptions options) : EventListener, IAsyncDisposable { public const string OpenTelemetrySdkEventSourceNamePrefix = "OpenTelemetry-"; - private readonly ILogger _logger; - private readonly EventLevel _eventLevel; + private readonly ILogger _logger = logger; + private readonly EventLevel _eventLevel = options.EventLogLevel; private const string TraceParentRegularExpressionString = "^\\d{2}-[a-f0-9]{32}-[a-f0-9]{16}-\\d{2}$"; #if NET8_0_OR_GREATER [GeneratedRegex(TraceParentRegularExpressionString)] private static partial Regex TraceParentRegex(); #else - private static readonly Regex _traceParentRegex = new(TraceParentRegularExpressionString); - private static Regex TraceParentRegex() => _traceParentRegex; + private static readonly Regex TraceParentRegexExpression = new(TraceParentRegularExpressionString); + private static Regex TraceParentRegex() => TraceParentRegexExpression; #endif - public LoggingEventListener(ILogger logger, ElasticOpenTelemetryOptions options) - { - _logger = logger; - _eventLevel = options.EventLogLevel; - - } - public override void Dispose() { if (_logger is IDisposable d) d.Dispose(); + base.Dispose(); } public ValueTask DisposeAsync() => _logger is IAsyncDisposable d ? d.DisposeAsync() : default; - protected override void OnEventSourceCreated(EventSource eventSource) { if (eventSource.Name.StartsWith(OpenTelemetrySdkEventSourceNamePrefix, StringComparison.Ordinal)) diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/StandardOutLogger.cs b/src/Elastic.OpenTelemetry/Diagnostics/StandardOutLogger.cs similarity index 75% rename from src/Elastic.OpenTelemetry/Diagnostics/Logging/StandardOutLogger.cs rename to src/Elastic.OpenTelemetry/Diagnostics/StandardOutLogger.cs index 5c56217..6ba739b 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/Logging/StandardOutLogger.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/StandardOutLogger.cs @@ -5,9 +5,9 @@ using Elastic.OpenTelemetry.Configuration; using Microsoft.Extensions.Logging; -namespace Elastic.OpenTelemetry.Diagnostics.Logging; +namespace Elastic.OpenTelemetry.Diagnostics; -internal sealed class StandardOutLogger(ElasticOpenTelemetryOptions options) : ILogger +internal sealed class StandardOutLogger(CompositeElasticOpenTelemetryOptions options) : ILogger { private readonly LogLevel _configuredLogLevel = options.LogLevel; @@ -17,15 +17,18 @@ internal sealed class StandardOutLogger(ElasticOpenTelemetryOptions options) : I public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { - // We skip logging for any log level higher (numerically) than the configured log level if (!IsEnabled(logLevel)) return; var logLine = LogFormatter.Format(logLevel, eventId, state, exception, formatter); - Console.WriteLine(logLine); + if (logLevel > LogLevel.Warning) + Console.Error.WriteLine(logLine); + else + Console.Out.WriteLine(logLine); } + // We skip logging for any log level higher (numerically) than the configured log level public bool IsEnabled(LogLevel logLevel) => StandardOutLoggingEnabled && _configuredLogLevel <= logLevel; public IDisposable BeginScope(TState state) where TState : notnull => _scopeProvider.Push(state); diff --git a/src/Elastic.OpenTelemetry/Elastic.OpenTelemetry.csproj b/src/Elastic.OpenTelemetry/Elastic.OpenTelemetry.csproj index 78f1ec3..9d2728f 100644 --- a/src/Elastic.OpenTelemetry/Elastic.OpenTelemetry.csproj +++ b/src/Elastic.OpenTelemetry/Elastic.OpenTelemetry.csproj @@ -2,16 +2,17 @@ Library - netstandard2.0;netstandard2.1;net462;net8.0 + netstandard2.0;netstandard2.1;net462;net8.0;net9.0 Elastic Distribution of OpenTelemetry .NET - OpenTelemetry extensions for Elastic Observability, fully native with zero code changes. - elastic;opentelemetry;observabillity;apm;logs;metrics;traces;monitoring + OpenTelemetry extensions for Elastic Observability. + elastic;opentelemetry;otel;observabillity;apm;logs;metrics;traces;monitoring enable enable True false $(NoWarn);OTEL1000 latest + @@ -19,23 +20,27 @@ - - - + + + - + - - + + - + - + + + + + diff --git a/src/Elastic.OpenTelemetry/ElasticOpenTelemetryBuilder.cs b/src/Elastic.OpenTelemetry/ElasticOpenTelemetryBuilder.cs deleted file mode 100644 index d8cd735..0000000 --- a/src/Elastic.OpenTelemetry/ElasticOpenTelemetryBuilder.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.Diagnostics; -using System.Diagnostics.Tracing; -using Elastic.OpenTelemetry.Configuration; -using Elastic.OpenTelemetry.Diagnostics; -using Elastic.OpenTelemetry.Diagnostics.Logging; -using Elastic.OpenTelemetry.Extensions; -using Elastic.OpenTelemetry.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using OpenTelemetry; - -namespace Elastic.OpenTelemetry; - -/// -/// An implementation of which configures Elastic defaults, but can also be customised. -/// -/// -/// Currently this builder enables both tracing and metrics, and configures the following: -/// -/// -/// Instrumentation -/// Enables commonly used instrumentation such as HTTP, gRPC and EntityFramework. -/// -/// -/// Processors -/// Enables Elastic processors to add additional features and to -/// ensure data is compatible with Elastic backends. -/// -/// -/// OTLP Exporter -/// Enables exporting of signals over OTLP to a configured endpoint(s). -/// -/// -/// -public class ElasticOpenTelemetryBuilder : IOpenTelemetryBuilder -{ - internal CompositeLogger Logger { get; } - internal LoggingEventListener EventListener { get; } - - /// - public IServiceCollection Services { get; } - - /// - /// Shared bootstrap routine for the Elastic Distribution of OpenTelemetry .NET. - /// Used to ensure auto instrumentation and manual instrumentation bootstrap the same way. - /// - public static (EventListener, ILogger) Bootstrap(ElasticOpenTelemetryBuilderOptions options) - { - var logger = new CompositeLogger(options); - - // Enables logging of OpenTelemetry-SDK event source events - var eventListener = new LoggingEventListener(logger, options.DistroOptions); - - logger.LogAgentPreamble(); - logger.LogElasticOpenTelemetryBuilderInitialized(Environment.NewLine, new StackTrace(true)); - options.DistroOptions.LogConfigSources(logger); - return (eventListener, logger); - } - - /// - /// Creates an instance of the configured with default options. - /// - public ElasticOpenTelemetryBuilder() - : this(new ElasticOpenTelemetryBuilderOptions()) - { } - - /// - /// Creates an instance of the configured with the provided - /// . - /// - public ElasticOpenTelemetryBuilder(ElasticOpenTelemetryBuilderOptions options) - { - var (eventListener, logger) = Bootstrap(options); - - Logger = (CompositeLogger)logger; - EventListener = (LoggingEventListener)eventListener; - - Services = options.Services ?? new ServiceCollection(); - - if (options.Services is not null && !options.Services.Any(d => d.ImplementationType == typeof(ElasticOpenTelemetryService))) - Services.Insert(0, ServiceDescriptor.Singleton()); - - Services.TryAddSingleton(this); - - // Directly invoke the SDK extension method to ensure SDK components are registered. - var openTelemetry = - Microsoft.Extensions.DependencyInjection.OpenTelemetryServicesExtensions.AddOpenTelemetry(Services); - - // We always add this so we can identify a distro is being used, even if all Elastic defaults are disabled. - openTelemetry.ConfigureResource(r => r.UseElasticDefaults()); - - if (options.DistroOptions.ElasticDefaults.Equals(ElasticDefaults.None)) - { - Logger.LogNoElasticDefaults(); - - // We always add the distro attribute so that we can identify a distro is being used, even if all Elastic defaults are disabled. - openTelemetry.ConfigureResource(r => r.AddDistroAttributes()); - return; - } - - openTelemetry.ConfigureResource(r => r.UseElasticDefaults(Logger)); - var distro = options.DistroOptions; - - if (distro.Signals.HasFlag(Signals.Logs)) - { - openTelemetry.WithLogging(logging => - { - if (distro.ElasticDefaults.HasFlag(ElasticDefaults.Logs)) - logging.UseElasticDefaults(distro.SkipOtlpExporter, Logger); - else - Logger.LogDefaultsDisabled(nameof(ElasticDefaults.Logs)); - }); - } - else - { - Logger.LogSignalDisabled(nameof(Signals.Logs)); - } - - if (distro.Signals.HasFlag(Signals.Traces)) - { - openTelemetry.WithTracing(tracing => - { - if (distro.ElasticDefaults.HasFlag(ElasticDefaults.Traces)) - tracing.UseElasticDefaults(distro.SkipOtlpExporter, Logger); - else - Logger.LogDefaultsDisabled(nameof(ElasticDefaults.Traces)); - }); - } - else - { - Logger.LogSignalDisabled(nameof(Signals.Metrics)); - } - - if (distro.Signals.HasFlag(Signals.Metrics)) - { - openTelemetry.WithMetrics(metrics => - { - if (distro.ElasticDefaults.HasFlag(ElasticDefaults.Metrics)) - metrics.UseElasticDefaults(distro.SkipOtlpExporter, Logger); - else - Logger.LogDefaultsDisabled(nameof(ElasticDefaults.Metrics)); - }); - } - else - { - Logger.LogSignalDisabled(nameof(Signals.Metrics)); - } - } -} - -internal static partial class LoggerMessages -{ - [LoggerMessage(EventId = 0, Level = LogLevel.Information, Message = "ElasticOpenTelemetryBuilder initialized{newline}{StackTrace}.")] - public static partial void LogElasticOpenTelemetryBuilderInitialized(this ILogger logger, string newline, StackTrace stackTrace); - - [LoggerMessage(EventId = 1, Level = LogLevel.Debug, Message = "ElasticOpenTelemetryBuilder configured {Signal} via the {Provider}.")] - public static partial void LogConfiguredSignalProvider(this ILogger logger, string signal, string provider); - - [LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = "No Elastic defaults were enabled.")] - public static partial void LogNoElasticDefaults(this ILogger logger); - - [LoggerMessage(EventId = 3, Level = LogLevel.Information, Message = "ElasticOpenTelemetryBuilder {Signal} skipped, configured to be disabled")] - public static partial void LogSignalDisabled(this ILogger logger, string signal); - - [LoggerMessage(EventId = 4, Level = LogLevel.Information, Message = "Elastic defaults for {Signal} skipped, configured to be disabled")] - public static partial void LogDefaultsDisabled(this ILogger logger, string signal); -} diff --git a/src/Elastic.OpenTelemetry/Extensions/ActivityExtensions.cs b/src/Elastic.OpenTelemetry/Extensions/ActivityExtensions.cs index 1053ec9..6a27af1 100644 --- a/src/Elastic.OpenTelemetry/Extensions/ActivityExtensions.cs +++ b/src/Elastic.OpenTelemetry/Extensions/ActivityExtensions.cs @@ -5,7 +5,9 @@ using Elastic.OpenTelemetry.Processors; -namespace Elastic.OpenTelemetry.Extensions; +#pragma warning disable IDE0130 // Namespace does not match folder structure +namespace Elastic.OpenTelemetry; +#pragma warning restore IDE0130 // Namespace does not match folder structure internal static class ActivityExtensions { @@ -68,19 +70,13 @@ private static bool TryToCompressRegular(this Activity buffered, Activity siblin return true; } - private static bool TryToCompressComposite(this Activity buffered, Activity sibling, Composite composite) - { - switch (composite.CompressionStrategy) + private static bool TryToCompressComposite(this Activity buffered, Activity sibling, Composite composite) => + composite.CompressionStrategy switch { - case "exact_match": - return buffered.IsSameKind(sibling) && buffered.OperationName == sibling.OperationName; // && sibling.Duration <= Configuration.SpanCompressionExactMatchMaxDuration; - - case "same_kind": - return buffered.IsSameKind(sibling); // && sibling.Duration <= Configuration.SpanCompressionSameKindMaxDuration; - } - - return false; - } + "exact_match" => buffered.IsSameKind(sibling) && buffered.OperationName == sibling.OperationName,// && sibling.Duration <= Configuration.SpanCompressionExactMatchMaxDuration; + "same_kind" => buffered.IsSameKind(sibling),// && sibling.Duration <= Configuration.SpanCompressionSameKindMaxDuration; + _ => false, + }; // TODO - Further implementation if possible private static bool IsSameKind(this Activity current, Activity other) => diff --git a/src/Elastic.OpenTelemetry/Extensions/HostApplicationBuilderExtensions.cs b/src/Elastic.OpenTelemetry/Extensions/HostApplicationBuilderExtensions.cs new file mode 100644 index 0000000..5683111 --- /dev/null +++ b/src/Elastic.OpenTelemetry/Extensions/HostApplicationBuilderExtensions.cs @@ -0,0 +1,70 @@ +// 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; +using Microsoft.Extensions.Hosting; +using OpenTelemetry; +using OpenTelemetry.Resources; + +#pragma warning disable IDE0130 // Namespace does not match folder structure +namespace Microsoft.Extensions.Hosting; +#pragma warning restore IDE0130 // Namespace does not match folder structure + +/// +/// Elastic extension methods for used to register +/// the Elastic Distribution of OpenTelemetry (EDOT) defaults. +/// +public static class HostApplicationBuilderExtensions +{ + /// + /// Registers the OpenTelemetry SDK with the application, configured with Elastic Distribution of OpenTelemetry (EDOT) defaults. + /// + /// The for the application being configured. + /// The supplied for chaining calls. + public static IHostApplicationBuilder AddElasticOpenTelemetry(this IHostApplicationBuilder builder) + { + builder.Services.AddElasticOpenTelemetry(builder.Configuration); + return builder; + } + + /// + /// + /// + /// + /// A representing the logical name of the service sent with resource attributes. + /// + public static IHostApplicationBuilder AddElasticOpenTelemetry(this IHostApplicationBuilder builder, string serviceName) + { +#if NET + ArgumentException.ThrowIfNullOrEmpty(serviceName); +#else + if (string.IsNullOrEmpty(serviceName)) + throw new ArgumentNullException(nameof(serviceName)); +#endif + + return AddElasticOpenTelemetry(builder, r => r.AddService(serviceName)); + } + + /// + /// + /// + /// + /// configuration action. + /// + public static IHostApplicationBuilder AddElasticOpenTelemetry(this IHostApplicationBuilder builder, Action configureResource) + { +#if NET + ArgumentNullException.ThrowIfNull(configureResource); +#else + if (configureResource is null) + throw new ArgumentNullException(nameof(configureResource)); +#endif + + builder.Services + .AddElasticOpenTelemetry(builder.Configuration) + .ConfigureResource(configureResource); + + return builder; + } +} diff --git a/src/Elastic.OpenTelemetry/Extensions/LoggerFactoryExtensions.cs b/src/Elastic.OpenTelemetry/Extensions/LoggerFactoryExtensions.cs new file mode 100644 index 0000000..4eaeee4 --- /dev/null +++ b/src/Elastic.OpenTelemetry/Extensions/LoggerFactoryExtensions.cs @@ -0,0 +1,14 @@ +// 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 Elastic.OpenTelemetry.Diagnostics; +using Microsoft.Extensions.Logging; + +namespace Elastic.OpenTelemetry; + +internal static class LoggerFactoryExtensions +{ + public static ILogger CreateElasticLogger(this ILoggerFactory loggerFactory) => + loggerFactory.CreateLogger(CompositeLogger.LogCategory); +} diff --git a/src/Elastic.OpenTelemetry/Extensions/LoggingProviderBuilderExtensions.cs b/src/Elastic.OpenTelemetry/Extensions/LoggingProviderBuilderExtensions.cs index 6450b8c..0d6a046 100644 --- a/src/Elastic.OpenTelemetry/Extensions/LoggingProviderBuilderExtensions.cs +++ b/src/Elastic.OpenTelemetry/Extensions/LoggingProviderBuilderExtensions.cs @@ -2,30 +2,136 @@ // 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 Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using OpenTelemetry.Logs; -using OpenTelemetry.Trace; -using static Elastic.OpenTelemetry.Configuration.Signals; -namespace Elastic.OpenTelemetry.Extensions; +// Matching namespace with LoggerProviderBuilder +#pragma warning disable IDE0130 // Namespace does not match folder structure +namespace OpenTelemetry.Logs; +#pragma warning restore IDE0130 // Namespace does not match folder structure /// -/// Elastic extensions for . +/// Extension methods for used to register +/// the Elastic Distribution of OpenTelemetry (EDOT) defaults. /// public static class LoggingProviderBuilderExtensions { /// - /// Use Elastic Distribution of OpenTelemetry .NET defaults for . + /// Used to track the number of times any overload of `UseElasticDefaults` is invoked on a + /// `LoggingProviderBuilder`. Generally, we expect one builder to be used per application, + /// and for it to be configured once. By tracking the total count of invocations, we can + /// log scenarios where the consumer may have inadvertently misconfigured OpenTelemetry in + /// their application. /// - public static LoggerProviderBuilder UseElasticDefaults(this LoggerProviderBuilder builder, bool skipOtlp = false, ILogger? logger = null) + private static readonly GlobalProviderBuilderState GlobalLoggerProviderBuilderState = new(); + + /// + /// Use Elastic Distribution of OpenTelemetry (EDOT) defaults for . + /// + /// The to configure. + /// The for chaining configuration. + public static LoggerProviderBuilder UseElasticDefaults(this LoggerProviderBuilder builder) => + UseElasticDefaultsCore(builder, null, null); + + /// + /// + /// + /// + /// When registering Elastic defaults, skip automatic registration of the OTLP exporter for logging. + /// + public static LoggerProviderBuilder UseElasticDefaults(this LoggerProviderBuilder builder, bool skipOtlpExporter) => + UseElasticDefaultsCore(builder, skipOtlpExporter ? CompositeElasticOpenTelemetryOptions.SkipOtlpOptions : CompositeElasticOpenTelemetryOptions.DefaultOptions, null); + + /// + /// + /// + /// + /// used to configure the Elastic Distribution of OpenTelemetry (EDOT) for .NET. + /// + public static LoggerProviderBuilder UseElasticDefaults(this LoggerProviderBuilder builder, ElasticOpenTelemetryOptions options) { - logger ??= NullLogger.Instance; +#if NET + ArgumentNullException.ThrowIfNull(options); +#else + if (options is null) + throw new ArgumentNullException(nameof(options)); +#endif - if (!skipOtlp) - builder.AddOtlpExporter(); + return UseElasticDefaultsCore(builder, new(options), null); + } + + /// + /// + /// + /// + /// An instance from which to load the OpenTelemetry SDK options. + /// + public static LoggerProviderBuilder UseElasticDefaults(this LoggerProviderBuilder builder, IConfiguration configuration) + { +#if NET + ArgumentNullException.ThrowIfNull(configuration); +#else + if (configuration is null) + throw new ArgumentNullException(nameof(configuration)); +#endif + return UseElasticDefaultsCore(builder, new(configuration), null); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static LoggerProviderBuilder UseElasticDefaults(this LoggerProviderBuilder builder, ElasticOpenTelemetryComponents components) => + UseElasticDefaultsCore(builder, components.Options, components); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static LoggerProviderBuilder UseElasticDefaults(this LoggerProviderBuilder builder, ElasticOpenTelemetryComponents components, IServiceCollection? services) => + UseElasticDefaultsCore(builder, components.Options, components, services); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static LoggerProviderBuilder UseElasticDefaultsCore( + this LoggerProviderBuilder builder, + CompositeElasticOpenTelemetryOptions? options, + ElasticOpenTelemetryComponents? components, + IServiceCollection? services = null) + { + var logger = components?.Logger ?? options?.AdditionalLogger; + + try + { + if (!SignalBuilder.ConfigureBuilder(nameof(UseElasticDefaults), nameof(LoggerProviderBuilder), builder, + GlobalLoggerProviderBuilderState, options, services, ConfigureBuilder, ref components)) + { + logger = components?.Logger ?? options?.AdditionalLogger; // Update with ref-returned components + logger?.UnableToConfigureLoggingDefaultsError(nameof(LoggerProviderBuilder)); + return builder; + } + } + catch (Exception ex) + { + // NOTE: Not using LoggerMessage as we want to pass the exception. As this should be rare, performance isn't critical here. + logger?.LogError(ex, "Failed to fully register EDOT .NET logging defaults for {Provider}.", nameof(LoggerProviderBuilder)); + } - logger.LogConfiguredSignalProvider(nameof(Logs), nameof(LoggerProviderBuilder)); return builder; + + static void ConfigureBuilder(LoggerProviderBuilder builder, ElasticOpenTelemetryComponents components) + { + builder.ConfigureResource(r => r.AddElasticDistroAttributes()); + + if (components.Options.SkipOtlpExporter || components.Options.SkipOtlpExporter) + { + components.Logger.LogSkippingOtlpExporter(nameof(Signals.Logs), nameof(LoggerProviderBuilder)); + } + else + { + builder.AddOtlpExporter(); + } + + components.Logger.LogConfiguredSignalProvider(nameof(Signals.Logs), nameof(LoggerProviderBuilder)); + } } } diff --git a/src/Elastic.OpenTelemetry/Extensions/MeterProviderBuilderExtensions.cs b/src/Elastic.OpenTelemetry/Extensions/MeterProviderBuilderExtensions.cs index 6e52755..1ea5656 100644 --- a/src/Elastic.OpenTelemetry/Extensions/MeterProviderBuilderExtensions.cs +++ b/src/Elastic.OpenTelemetry/Extensions/MeterProviderBuilderExtensions.cs @@ -2,33 +2,239 @@ // 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.CodeAnalysis; +using System.Reflection; +using System.Runtime.CompilerServices; +using Elastic.OpenTelemetry; +using Elastic.OpenTelemetry.Configuration; +using Elastic.OpenTelemetry.Core; +using Elastic.OpenTelemetry.Diagnostics; +using Elastic.OpenTelemetry.Instrumentation; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using OpenTelemetry.Metrics; -using static Elastic.OpenTelemetry.Configuration.Signals; -namespace Elastic.OpenTelemetry.Extensions; +// Matching namespace with MeterProviderBuilder +#pragma warning disable IDE0130 // Namespace does not match folder structure +namespace OpenTelemetry.Metrics; +#pragma warning restore IDE0130 // Namespace does not match folder structure /// -/// Elastic extensions for . +/// Extension methods for used to register +/// the Elastic Distribution of OpenTelemetry (EDOT) defaults. /// public static class MeterProviderBuilderExtensions { - /// Use Elastic Distribution of OpenTelemetry .NET defaults for - public static MeterProviderBuilder UseElasticDefaults(this MeterProviderBuilder builder, bool skipOtlp = false, ILogger? logger = null) + private static readonly GlobalProviderBuilderState GlobalMeterProviderBuilderState = new(); + + // Note: This is defined as a static method and allocates the array each time. + // This is intentional, as we expect this to be invoked once (or worst case, few times). + // After initialisation, the array is no longer required and can be reclaimed by the GC. + // This is likley to be overall more efficient for the common scenario as we don't keep + // an object alive for the lifetime of the application. + private static InstrumentationAssemblyInfo[] GetReflectionInstrumentationAssemblies() => + [ + new() + { + Name = "AspNetCore", + Filename = "OpenTelemetry.Instrumentation.AspNetCore.dll", + FullyQualifiedType = "OpenTelemetry.Metrics.AspNetCoreInstrumentationMeterProviderBuilderExtensions", + InstrumentationMethod = "AddAspNetCoreInstrumentation" + } + ]; + + /// + /// Use Elastic Distribution of OpenTelemetry .NET defaults for . + /// + /// + /// This is not neccesary if + /// has been called previously as that automatically adds the . + /// + /// The to configure. + /// The for chaining configuration. + public static MeterProviderBuilder UseElasticDefaults(this MeterProviderBuilder builder) => + UseElasticDefaultsCore(builder, null, null); + + /// + /// + /// + /// + /// When registering Elastic defaults, skip automatic registration of the OTLP exporter for metrics. + /// + public static MeterProviderBuilder UseElasticDefaults(this MeterProviderBuilder builder, bool skipOtlpExporter) => + UseElasticDefaultsCore(builder, skipOtlpExporter ? CompositeElasticOpenTelemetryOptions.SkipOtlpOptions : null, null); + + /// + /// + /// + /// + /// used to configure the Elastic Distribution of OpenTelemetry (EDOT) for .NET. + /// + public static MeterProviderBuilder UseElasticDefaults(this MeterProviderBuilder builder, ElasticOpenTelemetryOptions options) + { +#if NET + ArgumentNullException.ThrowIfNull(options); +#else + if (options is null) + throw new ArgumentNullException(nameof(options)); +#endif + + return UseElasticDefaultsCore(builder, new(options), null); + } + + /// + /// + /// + /// + /// An instance from which to load the Elastic Distribution of + /// OpenTelemetry (EDOT) options. + /// + public static MeterProviderBuilder UseElasticDefaults(this MeterProviderBuilder builder, IConfiguration configuration) { - logger ??= NullLogger.Instance; +#if NET + ArgumentNullException.ThrowIfNull(configuration); +#else + if (configuration is null) + throw new ArgumentNullException(nameof(configuration)); +#endif + return UseElasticDefaultsCore(builder, new(configuration), null); + } - builder - .AddProcessInstrumentation() - .AddRuntimeInstrumentation() - .AddHttpClientInstrumentation(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static MeterProviderBuilder UseElasticDefaults( + this MeterProviderBuilder builder, + ElasticOpenTelemetryComponents components) => + UseElasticDefaultsCore(builder, components.Options, components, null); - if (!skipOtlp) - builder.AddOtlpExporter(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static MeterProviderBuilder UseElasticDefaults( + this MeterProviderBuilder builder, + ElasticOpenTelemetryComponents components, + IServiceCollection serviceCollection) => + UseElasticDefaultsCore(builder, components.Options, components, serviceCollection); - logger.LogConfiguredSignalProvider(nameof(Metrics), nameof(MeterProviderBuilder)); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static MeterProviderBuilder UseElasticDefaults( + this MeterProviderBuilder builder, + IConfiguration configuration, + IServiceCollection serviceCollection) => + UseElasticDefaultsCore(builder, new(configuration), null, serviceCollection); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static MeterProviderBuilder UseElasticDefaults( + this MeterProviderBuilder builder, + IServiceCollection serviceCollection) => + UseElasticDefaultsCore(builder, null, null, serviceCollection); + + [RequiresDynamicCode("Requires reflection for dynamic assembly loading and instrumentation activation.")] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static MeterProviderBuilder UseElasticDefaultsCore( + this MeterProviderBuilder builder, + CompositeElasticOpenTelemetryOptions? options, + ElasticOpenTelemetryComponents? components, + IServiceCollection? services = null) + { + try + { + if (!SignalBuilder.ConfigureBuilder(nameof(UseElasticDefaults), nameof(MeterProviderBuilder), builder, + GlobalMeterProviderBuilderState, options, services, ConfigureBuilder, ref components)) + { + var logger = components?.Logger ?? options?.AdditionalLogger; + logger?.LogError("Unable to configure {Builder} with Elastic defaults.", nameof(MeterProviderBuilder)); + return builder; + } + } + catch (Exception ex) + { + var exceptionLogger = components is not null ? components.Logger : options?.AdditionalLogger; + exceptionLogger?.LogError(ex, "Failed to fully register EDOT .NET meter defaults for {Provider}.", nameof(MeterProviderBuilder)); + } return builder; + + static void ConfigureBuilder(MeterProviderBuilder builder, ElasticOpenTelemetryComponents components) + { + builder.ConfigureResource(r => r.AddElasticDistroAttributes()); + + AddWithLogging(builder, components.Logger, "HttpClient", b => b.AddHttpClientInstrumentation()); + AddWithLogging(builder, components.Logger, "Process", b => b.AddProcessInstrumentation()); + + // TODO - Guard this behind runtime checks e.g. RuntimeFeature.IsDynamicCodeSupported to support AoT users. + // see https://github.com/elastic/elastic-otel-dotnet/issues/198 + AddInstrumentationViaReflection(builder, components.Logger); + + if (components.Options.SkipOtlpExporter || components.Options.SkipOtlpExporter) + { + components.Logger.LogSkippingOtlpExporter(nameof(Signals.Traces), nameof(MeterProviderBuilder)); + } + else + { + builder.AddOtlpExporter(); + } + + components.Logger.LogConfiguredSignalProvider(nameof(Signals.Logs), nameof(MeterProviderBuilder)); + } + + static void AddWithLogging(MeterProviderBuilder builder, ILogger logger, string name, Action add) + { + add.Invoke(builder); + logger.LogAddedInstrumentation(name, nameof(MeterProviderBuilder)); + } + + static void AddInstrumentationViaReflection(MeterProviderBuilder builder, ILogger logger) + { + try + { + // This section is in its own try/catch because we don't want failures in the reflection-based + // registration to prevent completion of registering the more general defaults we apply. + + var assemblyLocation = Path.GetDirectoryName(typeof(ElasticOpenTelemetry).Assembly.Location); + if (assemblyLocation is not null) + { + foreach (var assembly in GetReflectionInstrumentationAssemblies()) + AddInstrumentationLibraryViaReflection(builder, logger, assemblyLocation, assembly); + } + } + catch + { + // TODO - Logging + } + } + + static void AddInstrumentationLibraryViaReflection( + MeterProviderBuilder builder, + ILogger logger, + string assemblyLocation, + in InstrumentationAssemblyInfo info) + { + try + { + var assemblyPath = Path.Combine(assemblyLocation, info.Filename); + + if (File.Exists(Path.Combine(assemblyLocation, info.Filename))) + { + logger.LogLocatedInstrumentationAssembly(info.Filename, assemblyLocation); + + var assembly = Assembly.LoadFrom(assemblyPath); + var type = assembly?.GetType(info.FullyQualifiedType); + var method = type?.GetMethod(info.InstrumentationMethod, BindingFlags.Static | BindingFlags.Public, + Type.DefaultBinder, [typeof(MeterProviderBuilder)], null); + + if (method is not null) + { + logger.LogAddedInstrumentation(info.Name, nameof(MeterProviderBuilder)); + method.Invoke(null, [builder]); + } + else + { + logger.LogWarning("Unable to invoke {TypeName}.{Method} on {AssemblyPath}.", info.FullyQualifiedType, info.InstrumentationMethod, assemblyPath); + } + } + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to dynamically enable {InstrumentationName} on {Provider}.", info.Name, nameof(MeterProviderBuilder)); + } + } } } diff --git a/src/Elastic.OpenTelemetry/Extensions/OpenTelemetryBuilderExtensions.cs b/src/Elastic.OpenTelemetry/Extensions/OpenTelemetryBuilderExtensions.cs index 95417db..f74633a 100644 --- a/src/Elastic.OpenTelemetry/Extensions/OpenTelemetryBuilderExtensions.cs +++ b/src/Elastic.OpenTelemetry/Extensions/OpenTelemetryBuilderExtensions.cs @@ -2,77 +2,372 @@ // 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 Elastic.OpenTelemetry.Diagnostics.Logging; +using System.Diagnostics; +using Elastic.OpenTelemetry; +using Elastic.OpenTelemetry.Configuration; +using Elastic.OpenTelemetry.Core; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Logging; -using OpenTelemetry; +using OpenTelemetry.Logs; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; -namespace Elastic.OpenTelemetry.Extensions; +// Matching namespace with OpenTelemetryBuilder +#pragma warning disable IDE0130 // Namespace does not match folder structure +namespace OpenTelemetry; +#pragma warning restore IDE0130 // Namespace does not match folder structure /// -/// Extension methods on . +/// Extension methods on and +/// used to register the Elastic Distribution of OpenTelemetry (EDOT) defaults. /// public static class OpenTelemetryBuilderExtensions { /// - /// Add an to the to which - /// logs will be written in addition to the configured diagnostic log file. + /// Used to track the number of times any variation of `WithElasticDefaults` is invoked by consuming + /// code, to allow us to warn about potenital misconfigurations. /// - public static IOpenTelemetryBuilder WithLogger(this IOpenTelemetryBuilder builder, ILogger logger) + private static int WithElasticDefaultsCallCounter; + + /// + /// Enables collection of all signals using Elastic Distribution of OpenTelemetry .NET defaults. + /// + /// The being configured. + /// + /// The supplied for chaining calls. + /// + public static IOpenTelemetryBuilder WithElasticDefaults(this IOpenTelemetryBuilder builder) => + WithElasticDefaultsCore(builder, CompositeElasticOpenTelemetryOptions.DefaultOptions); + + /// + /// + /// + /// + /// An from which to attempt binding of Elastic Distribution of OpenTelemetry + /// (EDOT) options. + /// + public static IOpenTelemetryBuilder WithElasticDefaults(this IOpenTelemetryBuilder builder, IConfiguration configuration) + { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(configuration); +#else + if (configuration is null) + throw new ArgumentNullException(nameof(configuration)); +#endif + + return WithElasticDefaultsCore(builder, new(configuration)); + } + + /// + /// + /// + /// + /// + /// An instance used to configure the initial Elastic Distribution of OpenTelemetry (EDOT) defaults. + /// Note that only the first use for a given instance applies the options. Subsequent builder methods may + /// accept but those will not be reapplied. + /// + /// + public static IOpenTelemetryBuilder WithElasticDefaults(this IOpenTelemetryBuilder builder, ElasticOpenTelemetryOptions options) { - if (builder is not ElasticOpenTelemetryBuilder distributionBuilder) +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(options); +#else + if (options is null) + throw new ArgumentNullException(nameof(options)); +#endif + + return WithElasticDefaultsCore(builder, new(options)); + } + + internal static IOpenTelemetryBuilder WithElasticDefaultsCore( + this IOpenTelemetryBuilder builder, + CompositeElasticOpenTelemetryOptions options) + { + var usingExistingState = true; // Will be set to false, if we later create state for this builder. + + // Attempt to load existing state if any Elastic extension methods on this builder have been called + // previously. This allows reuse of existing components, and ensures we bootstrap once per builder. + // If the builder is linked to an existing IServiceCollection, then we boostrapping will return the + // same bootstrapped components, ensuring we also bootstrap once per IServiceCollection. + + // We assign state to each instance of IOpenTelemetryBuilder, so that we can shortcut access to + // components on subsequent calls where we know bootstrapping has occurred. It also enables us to + // warn (via logs) when the same method on the same instance is invoked more than once, which is + // likely to be a user error. + var builderState = ElasticOpenTelemetry.BuilderStateTable.GetValue(builder, builder => + { + var bootstrapInfo = ElasticOpenTelemetry.TryBootstrap(options, ((IOpenTelemetryBuilder)builder).Services, out var components); + var builderState = new BuilderState(bootstrapInfo, components); + usingExistingState = false; + return builderState; + }); + + builderState.IncrementUseElasticDefaults(); + + var callCount = Interlocked.Increment(ref WithElasticDefaultsCallCounter); + + if (builderState.UseElasticDefaultsCounter > 1) + { + // TODO - Log warning + } + else if (callCount > 1) + { + // TODO - Log warning + } + + if (!usingExistingState) + { + // TODO - Log + } + + var bootstrapInfo = builderState.BootstrapInfo; + var components = builderState.Components; + + Debug.Assert(bootstrapInfo is not null, "BootstrapInfo should not be null after successful bootstrap."); + Debug.Assert(components is not null, "Components should not be null after successful bootstrap."); + + if (!bootstrapInfo.Succeeded) + { + options?.AdditionalLogger?.LogError("Unable to bootstrap EDOT."); + ElasticOpenTelemetry.BuilderStateTable.Remove(builder); return builder; + } - distributionBuilder.Logger.SetAdditionalLogger(logger); - return distributionBuilder; + builder.WithLogging(b => b.UseElasticDefaults(components, builder.Services)); + builder.WithMetrics(b => b.UseElasticDefaults(components, builder.Services)); + builder.WithTracing(b => b.UseElasticDefaults(components, builder.Services)); + + return builder; } /// - /// Triggers creation and registration of the OpenTelemetry components required to begin observing the application. + /// Adds metric services into the using Elastic Distribution of OpenTelemetry (EDOT) defaults. /// - /// A new instance of which supports disposing of the - /// OpenTelemetry providers to end signal collection. - public static IInstrumentationLifetime Build(this IOpenTelemetryBuilder builder) - => builder.Build(null, null); + /// . + /// + /// 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 . + /// + /// + /// The supplied for chaining + /// calls. + public static IOpenTelemetryBuilder WithElasticLogging(this IOpenTelemetryBuilder builder) => + builder.WithLogging(lpb => lpb.UseElasticDefaults()); /// - /// Triggers creation and registration of the OpenTelemetry components required to begin observing the application. + /// /// - /// A new instance of which supports disposing of the - /// OpenTelemetry providers to end signal collection. - public static IInstrumentationLifetime Build(this IOpenTelemetryBuilder builder, ILogger logger) - => builder.Build(logger, null); + /// + /// + /// configuration callback. + /// + public static IOpenTelemetryBuilder WithElasticLogging(this IOpenTelemetryBuilder builder, Action configure) + { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(configure); +#else + if (configure is null) + throw new ArgumentNullException(nameof(configure)); +#endif + + return builder.WithLogging(lpb => + { + lpb.UseElasticDefaults(); + configure?.Invoke(lpb); + }); + } + + /// + /// Adds metric services into the using Elastic Distribution of OpenTelemetry (EDOT) 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 . + /// + /// + /// The supplied for chaining + /// calls. + public static IOpenTelemetryBuilder WithElasticMetrics(this IOpenTelemetryBuilder builder) => + builder.WithMetrics(mpb => mpb.UseElasticDefaults()); /// - /// Triggers creation and registration of the OpenTelemetry components required to begin observing the application. + /// /// - /// A new instance of which supports disposing of the - /// OpenTelemetry providers to end signal collection. - internal static IInstrumentationLifetime Build(this IOpenTelemetryBuilder builder, ILogger? logger = null, IServiceProvider? serviceProvider = null) + /// + /// + /// An instance from which to load the Elastic Distribution of + /// OpenTelemetry (EDOT) options. + /// + public static IOpenTelemetryBuilder WithElasticMetrics(this IOpenTelemetryBuilder builder, IConfiguration configuration) { - // this happens if someone calls Build() while using IServiceCollection and AddOpenTelemetry() and NOT Add*Elastic*OpenTelemetry() - // we treat this a NOOP - // NOTE for AddElasticOpenTelemetry(this IServiceCollection services) calling Build() manually is NOT required. - if (builder is not ElasticOpenTelemetryBuilder elasticOtelBuilder) - return new EmptyInstrumentationLifetime(); - - var compositeLogger = elasticOtelBuilder.Logger; - compositeLogger.SetAdditionalLogger(logger); - - var sp = serviceProvider ?? elasticOtelBuilder.Services.BuildServiceProvider(); - var tracerProvider = sp.GetService()!; - var meterProvider = sp.GetService()!; - - var lifetime = new InstrumentationLifetime(compositeLogger, elasticOtelBuilder.EventListener, tracerProvider, meterProvider); - compositeLogger.LogElasticOpenTelemetryBuilderBuilt(); - return lifetime; +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(configuration); +#else + if (configuration is null) + throw new ArgumentNullException(nameof(configuration)); +#endif + + return builder.WithMetrics(mpb => mpb.UseElasticDefaults(configuration, builder.Services)); } -} -internal static partial class LoggerMessages -{ - [LoggerMessage(EventId = 10, Level = LogLevel.Debug, Message = "ElasticOpenTelemetryBuilder built.")] - public static partial void LogElasticOpenTelemetryBuilderBuilt(this ILogger logger); + /// + /// + /// + /// + /// + /// configuration callback. + /// + public static IOpenTelemetryBuilder WithElasticMetrics(this IOpenTelemetryBuilder builder, Action configure) + { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(configure); +#else + if (configure is null) + throw new ArgumentNullException(nameof(configure)); +#endif + + return builder.WithMetrics(mpb => + { + mpb.UseElasticDefaults(builder.Services); + configure?.Invoke(mpb); + }); + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static IOpenTelemetryBuilder WithElasticMetrics(this IOpenTelemetryBuilder builder, IConfiguration configuration, + Action configure) + { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(configuration); + ArgumentNullException.ThrowIfNull(configure); +#else + if (configuration is null) + throw new ArgumentNullException(nameof(configuration)); + + if (configure is null) + throw new ArgumentNullException(nameof(configure)); +#endif + + return builder.WithMetrics(mpb => + { + mpb.UseElasticDefaults(configuration, builder.Services); + configure?.Invoke(mpb); + }); + } + + /// + /// Adds tracing services into the using Elastic Distribution of OpenTelemetry (EDOT) defaults. + /// + /// . + /// + /// Note: This is safe to be called multiple times and by library authors. + /// Only a single will be created for a given + /// . + /// + /// The supplied for chaining calls. + public static IOpenTelemetryBuilder WithElasticTracing(this IOpenTelemetryBuilder builder) => + builder.WithTracing(m => m.UseElasticDefaults()); + + /// + /// + /// + /// + /// + /// An instance from which to load the Elastic Distribution of + /// OpenTelemetry (EDOT) options. + /// + public static IOpenTelemetryBuilder WithElasticTracing(this IOpenTelemetryBuilder builder, IConfiguration configuration) + { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(configuration); +#else + if (configuration is null) + throw new ArgumentNullException(nameof(configuration)); +#endif + + return builder.WithTracing(tpb => tpb.UseElasticDefaults(configuration, builder.Services)); + } + + /// + /// Adds tracing services into the builder using Elastic defaults. + /// + /// + /// + /// configuration callback. + /// + public static IOpenTelemetryBuilder WithElasticTracing(this IOpenTelemetryBuilder builder, Action configure) + { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(configure); +#else + if (configure is null) + throw new ArgumentNullException(nameof(configure)); +#endif + + return builder.WithTracing(tpb => + { + tpb.UseElasticDefaults(); + configure?.Invoke(tpb); + }); + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static IOpenTelemetryBuilder WithElasticTracing(this IOpenTelemetryBuilder builder, IConfiguration configuration, + Action configure) + { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(configuration); + ArgumentNullException.ThrowIfNull(configure); +#else + if (configuration is null) + throw new ArgumentNullException(nameof(configuration)); + + if (configure is null) + throw new ArgumentNullException(nameof(configure)); +#endif + + return builder.WithTracing(tpb => + { + tpb.UseElasticDefaults(configuration, builder.Services); + configure?.Invoke(tpb); + }); + } } diff --git a/src/Elastic.OpenTelemetry/Extensions/OpenTelemetryLoggerOptionsExtensions.cs b/src/Elastic.OpenTelemetry/Extensions/OpenTelemetryLoggerOptionsExtensions.cs index 72f6638..ffa6de0 100644 --- a/src/Elastic.OpenTelemetry/Extensions/OpenTelemetryLoggerOptionsExtensions.cs +++ b/src/Elastic.OpenTelemetry/Extensions/OpenTelemetryLoggerOptionsExtensions.cs @@ -2,26 +2,30 @@ // 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 Elastic.OpenTelemetry.Configuration; +using Elastic.OpenTelemetry.Diagnostics; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using OpenTelemetry.Logs; -using static Elastic.OpenTelemetry.Configuration.Signals; -namespace Elastic.OpenTelemetry.Extensions; +#pragma warning disable IDE0130 // Namespace does not match folder structure +namespace OpenTelemetry; +#pragma warning restore IDE0130 // Namespace does not match folder structure /// -/// Elastic extensions for . +/// Extension methods for used to register +/// the Elastic Distribution of OpenTelemetry (EDOT) defaults. /// public static class OpenTelemetryLoggerOptionsExtensions { /// - /// Ensures Elastic distro options are set for + /// Ensures Elastic Distribution of OpenTelemetry (EDOT) options are set for /// public static void UseElasticDefaults(this OpenTelemetryLoggerOptions options, ILogger? logger = null) { logger ??= NullLogger.Instance; options.IncludeFormattedMessage = true; options.IncludeScopes = true; - logger.LogConfiguredSignalProvider(nameof(Logs), nameof(OpenTelemetryLoggerOptions)); + logger.LogConfiguredSignalProvider(nameof(Signals.Logs), nameof(OpenTelemetryLoggerOptions)); } } diff --git a/src/Elastic.OpenTelemetry/Extensions/ResourceBuilderExtensions.cs b/src/Elastic.OpenTelemetry/Extensions/ResourceBuilderExtensions.cs index 879d298..ee8aa63 100644 --- a/src/Elastic.OpenTelemetry/Extensions/ResourceBuilderExtensions.cs +++ b/src/Elastic.OpenTelemetry/Extensions/ResourceBuilderExtensions.cs @@ -3,13 +3,15 @@ // See the LICENSE file in the project root for more information using System.Diagnostics; +using Elastic.OpenTelemetry.Core; using Elastic.OpenTelemetry.SemanticConventions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using OpenTelemetry.ResourceDetectors.Host; using OpenTelemetry.Resources; -namespace Elastic.OpenTelemetry.Extensions; +#pragma warning disable IDE0130 // Namespace does not match folder structure +namespace OpenTelemetry; +#pragma warning restore IDE0130 // Namespace does not match folder structure /// /// Extension methods for . @@ -65,15 +67,14 @@ public static ResourceBuilder UseElasticDefaults(this ResourceBuilder builder, I { ResourceSemanticConventions.AttributeServiceInstanceId, InstanceId } }) .AddTelemetrySdk() - .AddDistroAttributes() - .AddEnvironmentVariableDetector(); - - builder.AddDetector(new HostDetector(logger)); + .AddElasticDistroAttributes() + .AddEnvironmentVariableDetector() + .AddHostDetector(); return builder; } - internal static ResourceBuilder AddDistroAttributes(this ResourceBuilder builder) => + internal static ResourceBuilder AddElasticDistroAttributes(this ResourceBuilder builder) => builder.AddAttributes(new Dictionary { { ResourceSemanticConventions.AttributeTelemetryDistroName, "elastic" }, diff --git a/src/Elastic.OpenTelemetry/Extensions/ServiceCollectionExtensions.cs b/src/Elastic.OpenTelemetry/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..46d9e91 --- /dev/null +++ b/src/Elastic.OpenTelemetry/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,133 @@ +// 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 Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using OpenTelemetry; + +// ReSharper disable once CheckNamespace +#pragma warning disable IDE0130 // Namespace does not match folder structure +namespace Microsoft.Extensions.DependencyInjection; +#pragma warning restore IDE0130 // Namespace does not match folder structure + +/// +/// Extension methods for used to register +/// the Elastic Distribution of OpenTelemetry (EDOT) defaults. +/// +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) defaults. + /// + /// The for adding services. + /// + /// An instance of that can be used to further configure the + /// OpenTelemetry SDK. + /// + public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollection services) => + AddElasticOpenTelemetryCore(services, CompositeElasticOpenTelemetryOptions.DefaultOptions); + + /// + /// + /// + /// + /// Controls whether the OTLP exporter is enabled automatically. + /// + public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollection services, bool skipOtlpExporter) => + AddElasticOpenTelemetryCore(services, skipOtlpExporter ? CompositeElasticOpenTelemetryOptions.SkipOtlpOptions : CompositeElasticOpenTelemetryOptions.DefaultOptions); + + /// + /// + /// + /// + /// + /// An instance from which to attempt binding of configuration values. + /// + /// + public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollection services, IConfiguration configuration) + { +#if NET + ArgumentNullException.ThrowIfNull(configuration); +#else + if (configuration is null) + throw new ArgumentNullException(nameof(configuration)); +#endif + + return AddElasticOpenTelemetryCore(services, new(configuration)); + } + + /// + /// + /// + /// + /// + /// + /// An that Elastic Distribution of OpenTelemetry (EDOT) can use to create an additional + /// used for diagnostic logging. + /// + /// + public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollection services, + IConfiguration configuration, ILoggerFactory additionalLoggerFactory) + { +#if NET + ArgumentNullException.ThrowIfNull(configuration); + ArgumentNullException.ThrowIfNull(additionalLoggerFactory); +#else + if (configuration is null) + throw new ArgumentNullException(nameof(configuration)); + if (additionalLoggerFactory is null) + throw new ArgumentNullException(nameof(additionalLoggerFactory)); +#endif + + return AddElasticOpenTelemetryCore(services, new(configuration, additionalLoggerFactory)); + } + + /// + /// + /// + /// + /// + /// An additional to be used for diagnostic logging. + /// + public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollection services, + IConfiguration configuration, ILogger additionalLogger) + { +#if NET + ArgumentNullException.ThrowIfNull(configuration); +#else + if (configuration is null) + throw new ArgumentNullException(nameof(configuration)); +#endif + + return AddElasticOpenTelemetryCore(services, new(configuration) { AdditionalLogger = additionalLogger }); + } + + /// + /// + /// + /// + /// + /// The used to configure the Elastic Distribution of OpenTelemetry (EDOT) for .NET. + /// + /// + public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollection services, ElasticOpenTelemetryOptions options) => + AddElasticOpenTelemetryCore(services, new(options)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static IOpenTelemetryBuilder AddElasticOpenTelemetryCore(IServiceCollection services, CompositeElasticOpenTelemetryOptions options) + { +#if NET + ArgumentNullException.ThrowIfNull(options); +#else + if (options is null) + throw new ArgumentNullException(nameof(options)); +#endif + + return services.AddOpenTelemetry().WithElasticDefaultsCore(options); + } +} diff --git a/src/Elastic.OpenTelemetry/Extensions/TracerProviderBuilderExtensions.cs b/src/Elastic.OpenTelemetry/Extensions/TracerProviderBuilderExtensions.cs index 7743bed..6bfe4c1 100644 --- a/src/Elastic.OpenTelemetry/Extensions/TracerProviderBuilderExtensions.cs +++ b/src/Elastic.OpenTelemetry/Extensions/TracerProviderBuilderExtensions.cs @@ -3,75 +3,315 @@ // See the LICENSE file in the project root for more information using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.CompilerServices; +using Elastic.OpenTelemetry; +using Elastic.OpenTelemetry.Configuration; +using Elastic.OpenTelemetry.Core; using Elastic.OpenTelemetry.Diagnostics; +using Elastic.OpenTelemetry.Instrumentation; using Elastic.OpenTelemetry.Processors; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using OpenTelemetry; -using OpenTelemetry.Logs; -using OpenTelemetry.Trace; -using static Elastic.OpenTelemetry.Configuration.Signals; +using OpenTelemetry.Metrics; -namespace Elastic.OpenTelemetry.Extensions; +// Matching namespace with MeterProviderBuilder +#pragma warning disable IDE0130 // Namespace does not match folder structure +namespace OpenTelemetry.Trace; +#pragma warning restore IDE0130 // Namespace does not match folder structure /// -/// Elastic extensions for . +/// Extension methods for used to register +/// the Elastic Distribution of OpenTelemetry (EDOT) defaults. /// public static class TracerProviderBuilderExtensions { + private static readonly GlobalProviderBuilderState GlobalTracerProviderBuilderState = new(); + + // Note: This is defined as a static method and allocates the array each time. + // This is intentional, as we expect this to be invoked once (or worst case, few times). + // After initialisation, the array is no longer required and can be reclaimed by the GC. + // This is likley to be overall more efficient for the common scenario as we don't keep + // an object alive for the lifetime of the application. + private static InstrumentationAssemblyInfo[] GetReflectionInstrumentationAssemblies() => + [ + new() + { + Name = "AspNetCore", + Filename = "OpenTelemetry.Instrumentation.AspNetCore.dll", + FullyQualifiedType = "OpenTelemetry.Trace.AspNetCoreInstrumentationTracerProviderBuilderExtensions", + InstrumentationMethod = "AddAspNetCoreInstrumentation" + } + ]; + /// - /// Include Elastic trace processors to ensure data is enriched and extended. + /// Use Elastic Distribution of OpenTelemetry .NET defaults for . /// - public static TracerProviderBuilder AddElasticProcessors(this TracerProviderBuilder builder, ILogger? logger = null) - { - logger ??= NullLogger.Instance; + /// + /// This is not neccesary if + /// has been called previously as that automatically adds the . + /// + /// The to configure. + /// The for chaining configuration. + public static TracerProviderBuilder UseElasticDefaults(this TracerProviderBuilder builder) => + UseElasticDefaultsCore(builder, null, null); - return builder - .LogAndAddProcessor(new TransactionIdProcessor(logger), logger) - .LogAndAddProcessor(new ElasticCompatibilityProcessor(logger), logger); - } + /// + /// + /// + /// + /// When registering Elastic defaults, skip automatic registration of the OTLP exporter for traces. + /// + public static TracerProviderBuilder UseElasticDefaults(this TracerProviderBuilder builder, bool skipOtlpExporter) => + UseElasticDefaultsCore(builder, skipOtlpExporter ? CompositeElasticOpenTelemetryOptions.SkipOtlpOptions : null, null); - private static TracerProviderBuilder LogAndAddProcessor(this TracerProviderBuilder builder, BaseProcessor processor, ILogger logger) + /// + /// + /// + /// + /// used to configure the Elastic Distribution of OpenTelemetry (EDOT) for .NET. + /// + public static TracerProviderBuilder UseElasticDefaults(this TracerProviderBuilder builder, ElasticOpenTelemetryOptions options) { - logger.LogProcessorAdded(processor.GetType().ToString(), builder.GetType().Name); - return builder.AddProcessor(processor); +#if NET + ArgumentNullException.ThrowIfNull(options); +#else + if (options is null) + throw new ArgumentNullException(nameof(options)); +#endif + + return UseElasticDefaultsCore(builder, new(options), null); } /// - /// Use Elastic Distribution of OpenTelemetry .NET defaults for . + /// /// - public static TracerProviderBuilder UseElasticDefaults(this TracerProviderBuilder builder, bool skipOtlp = false, ILogger? logger = null) + /// + /// An instance from which to load the OpenTelemetry SDK options. + /// + public static TracerProviderBuilder UseElasticDefaults(this TracerProviderBuilder builder, IConfiguration configuration) { - logger ??= NullLogger.Instance; +#if NET + ArgumentNullException.ThrowIfNull(configuration); +#else + if (configuration is null) + throw new ArgumentNullException(nameof(configuration)); +#endif + return UseElasticDefaultsCore(builder, new(configuration), null); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static TracerProviderBuilder UseElasticDefaults(this TracerProviderBuilder builder, ElasticOpenTelemetryComponents components) => + UseElasticDefaultsCore(builder, components.Options, components); - builder - .AddHttpClientInstrumentation() - .AddGrpcClientInstrumentation() - .AddEntityFrameworkCoreInstrumentation() - .AddElasticsearchClientInstrumentation() - .AddSqlClientInstrumentation() - .AddSource("Elastic.Transport") - .AddElasticProcessors(logger); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static TracerProviderBuilder UseElasticDefaults( + this TracerProviderBuilder builder, + IConfiguration configuration, + IServiceCollection serviceCollection) => + UseElasticDefaultsCore(builder, new(configuration), null, serviceCollection); - if (!skipOtlp) - builder.AddOtlpExporter(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static TracerProviderBuilder UseElasticDefaults(this TracerProviderBuilder builder, ElasticOpenTelemetryComponents components, IServiceCollection? services) => + UseElasticDefaultsCore(builder, components.Options, components, services); + + [RequiresDynamicCode("Requires reflection for dynamic assembly loading and instrumentation activation.")] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TracerProviderBuilder UseElasticDefaultsCore( + TracerProviderBuilder builder, + CompositeElasticOpenTelemetryOptions? options, + ElasticOpenTelemetryComponents? components, + IServiceCollection? services = null) + { + try + { + if (!SignalBuilder.ConfigureBuilder(nameof(UseElasticDefaults), nameof(TracerProviderBuilder), builder, + GlobalTracerProviderBuilderState, options, services, ConfigureBuilder, ref components)) + { + var logger = components?.Logger ?? options?.AdditionalLogger; + logger?.LogError("Unable to configure {Builder} with Elastic defaults.", nameof(TracerProviderBuilder)); + return builder; + } + } + catch (Exception ex) + { + var exceptionLogger = components is not null ? components.Logger : options?.AdditionalLogger; + exceptionLogger?.LogError(ex, "Failed to fully register EDOT .NET tracer defaults for {Provider}.", nameof(TracerProviderBuilder)); + } - logger.LogConfiguredSignalProvider(nameof(Traces), nameof(TracerProviderBuilder)); return builder; + + static void ConfigureBuilder(TracerProviderBuilder builder, ElasticOpenTelemetryComponents components) + { + builder.ConfigureResource(r => r.AddElasticDistroAttributes()); + + AddWithLogging(builder, components.Logger, "HttpClient", b => b.AddHttpClientInstrumentation()); + AddWithLogging(builder, components.Logger, "GrpcClient", b => b.AddGrpcClientInstrumentation()); + AddWithLogging(builder, components.Logger, "EntityFrameworkCore", b => b.AddEntityFrameworkCoreInstrumentation()); + AddWithLogging(builder, components.Logger, "NEST", b => b.AddElasticsearchClientInstrumentation()); + AddWithLogging(builder, components.Logger, "SqlClient", b => b.AddSqlClientInstrumentation()); + AddWithLogging(builder, components.Logger, "ElasticTransport", b => b.AddSource("Elastic.Transport")); + + // TODO - Guard this behind runtime checks e.g. RuntimeFeature.IsDynamicCodeSupported to support AoT users. + // see https://github.com/elastic/elastic-otel-dotnet/issues/198 + AddInstrumentationViaReflection(builder, components.Logger); + + AddElasticProcessorsCore(builder, components); + + if (components.Options.SkipOtlpExporter || components.Options.SkipOtlpExporter) + { + components.Logger.LogSkippingOtlpExporter(nameof(Signals.Traces), nameof(TracerProviderBuilder)); + } + else + { + builder.AddOtlpExporter(); + } + + components.Logger.LogConfiguredSignalProvider(nameof(Signals.Traces), nameof(TracerProviderBuilder)); + } + + static void AddWithLogging(TracerProviderBuilder builder, ILogger logger, string name, Action add) + { + add.Invoke(builder); + logger.LogAddedInstrumentation(name, nameof(TracerProviderBuilder)); + } + + static void AddInstrumentationViaReflection(TracerProviderBuilder builder, ILogger logger) + { + try + { + // This section is in its own try/catch because we don't want failures in the reflection-based + // registration to prevent completion of registering the more general defaults we apply. + + var assemblyLocation = Path.GetDirectoryName(typeof(ElasticOpenTelemetry).Assembly.Location); + if (assemblyLocation is not null) + { + foreach (var assembly in GetReflectionInstrumentationAssemblies()) + AddInstrumentationLibraryViaReflection(builder, logger, assemblyLocation, assembly); + } + } + catch + { + // TODO - Logging + } + } + + static void AddInstrumentationLibraryViaReflection( + TracerProviderBuilder builder, + ILogger logger, + string assemblyLocation, + in InstrumentationAssemblyInfo info) + { + try + { + var assemblyPath = Path.Combine(assemblyLocation, info.Filename); + if (File.Exists(Path.Combine(assemblyLocation, info.Filename))) + { + logger.LogLocatedInstrumentationAssembly(info.Filename, assemblyLocation); + + var assembly = Assembly.LoadFrom(assemblyPath); + var type = assembly?.GetType(info.FullyQualifiedType); + var method = type?.GetMethod(info.InstrumentationMethod, BindingFlags.Static | BindingFlags.Public, + Type.DefaultBinder, [typeof(TracerProviderBuilder)], null); + + if (method is not null) + { + logger.LogAddedInstrumentation(info.Name, nameof(TracerProviderBuilder)); + method.Invoke(null, [builder]); + } + } + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to dynamically enable {InstrumentationName} on {Provider}.", info.Name, nameof(TracerProviderBuilder)); + } + } } - internal static TracerProviderBuilder UseAutoInstrumentationElasticDefaults(this TracerProviderBuilder builder, bool skipOtlp = false, ILogger? logger = null) + // We use a different method here to ensure we don't cause a crash depending on instrumentation libraries which are not present. + // We can't assume that any DLLs are available besides OpenTelemetry.dll, which auto-instrumentation includes. + // The auto instrumentation enables a set of default instrumentation of it's own, so we rely on that. + // In the future, we can assess if we should copy instrumentation DLLs into the autoinstrumentation zip file and enable them. + internal static TracerProviderBuilder UseAutoInstrumentationElasticDefaults(this TracerProviderBuilder builder, ElasticOpenTelemetryComponents components) { - logger ??= NullLogger.Instance; + Debug.Assert(components is not null, "Components should not be null when invoked from the auto instrumentation."); + + try + { + builder + .ConfigureResource(r => r.AddElasticDistroAttributes()) + .AddSource("Elastic.Transport") + .AddElasticProcessorsCore(components); - builder - .AddSource("Elastic.Transport") - .AddElasticProcessors(logger); + if (components.Options.SkipOtlpExporter) + { + components.Logger.LogSkippingOtlpExporter(nameof(Signals.Traces), nameof(TracerProviderBuilder)); + } + else + { + builder.AddOtlpExporter(); + } - if (!skipOtlp) - builder.AddOtlpExporter(); + components.Logger.LogConfiguredSignalProvider("Traces", nameof(TracerProviderBuilder)); - logger.LogConfiguredSignalProvider(nameof(Traces), nameof(TracerProviderBuilder)); + return builder; + } + catch (Exception ex) + { + components?.Logger?.LogError(ex, "Failed to register EDOT defaults for tracing auto-instrumentation to the {Provider}.", nameof(TracerProviderBuilder)); + } + + return builder; + } + + /// + /// Include Elastic trace processors for best compatibility with Elastic Observability. + /// + /// + /// It is not neccessary to call this method if `UseElasticDefaults` has already been called. + /// + /// The where the Elastic trace + /// processors should be added. + /// The for chaining. + public static TracerProviderBuilder AddElasticProcessors(this TracerProviderBuilder builder) => + AddElasticProcessorsCore(builder, null); + + private static TracerProviderBuilder AddElasticProcessorsCore( + this TracerProviderBuilder builder, + ElasticOpenTelemetryComponents? components) + { + var options = components?.Options ?? CompositeElasticOpenTelemetryOptions.DefaultOptions; + + try + { + if (!SignalBuilder.ConfigureBuilder(nameof(UseElasticDefaults), nameof(TracerProviderBuilder), builder, + GlobalTracerProviderBuilderState, options, null, ConfigureBuilder, ref components)) + { + var logger = components?.Logger ?? options?.AdditionalLogger; + logger?.LogError("Unable to configure {Builder} with Elastic defaults.", nameof(TracerProviderBuilder)); + return builder; + } + } + catch (Exception ex) + { + var exceptionLogger = components is not null ? components.Logger : options?.AdditionalLogger; + exceptionLogger?.LogError(ex, "Failed to fully register EDOT .NET tracer defaults for {Provider}.", nameof(TracerProviderBuilder)); + } + + return builder; + + static void ConfigureBuilder(TracerProviderBuilder builder, ElasticOpenTelemetryComponents components) + { + builder.LogAndAddProcessor(new ElasticCompatibilityProcessor(components.Logger), components.Logger); + } + } + + private static TracerProviderBuilder LogAndAddProcessor(this TracerProviderBuilder builder, BaseProcessor processor, ILogger logger) + { + builder.AddProcessor(processor); + logger.LogProcessorAdded(processor.GetType().ToString(), builder.GetType().Name); return builder; } } diff --git a/src/Elastic.OpenTelemetry/Hosting/ElasticOpenTelemetryService.cs b/src/Elastic.OpenTelemetry/Hosting/ElasticOpenTelemetryService.cs index 203b1f4..8fd74d6 100644 --- a/src/Elastic.OpenTelemetry/Hosting/ElasticOpenTelemetryService.cs +++ b/src/Elastic.OpenTelemetry/Hosting/ElasticOpenTelemetryService.cs @@ -2,8 +2,7 @@ // 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 Elastic.OpenTelemetry.Diagnostics.Logging; -using Elastic.OpenTelemetry.Extensions; +using Elastic.OpenTelemetry.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -12,26 +11,39 @@ namespace Elastic.OpenTelemetry.Hosting; internal sealed class ElasticOpenTelemetryService(IServiceProvider serviceProvider) : IHostedLifecycleService { - private IInstrumentationLifetime? _lifeTime; + private ElasticOpenTelemetryComponents? _components; public Task StartingAsync(CancellationToken cancellationToken) { var loggerFactory = serviceProvider.GetService(); - var logger = loggerFactory?.CreateLogger(CompositeLogger.LogCategory); + var logger = loggerFactory?.CreateElasticLogger(); - _lifeTime = serviceProvider.GetRequiredService().Build(logger, serviceProvider); + var bootstrapInfo = serviceProvider.GetService(); + + _components = serviceProvider.GetService(); + + if (bootstrapInfo is not null && bootstrapInfo.Succeeded && _components is not null && logger is not null) + { + _components.SetAdditionalLogger(logger, bootstrapInfo.ActivationMethod); + } return Task.CompletedTask; } public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; public async Task StoppedAsync(CancellationToken cancellationToken) { - if (_lifeTime != null) - await _lifeTime.DisposeAsync().ConfigureAwait(false); + if (_components?.Logger is not null) + await _components.Logger.DisposeAsync().ConfigureAwait(false); + + if (_components?.LoggingEventListener is not null) + await _components.LoggingEventListener.DisposeAsync().ConfigureAwait(false); } } diff --git a/src/Elastic.OpenTelemetry/IInstrumentationLifetime.cs b/src/Elastic.OpenTelemetry/IInstrumentationLifetime.cs deleted file mode 100644 index 29d41dd..0000000 --- a/src/Elastic.OpenTelemetry/IInstrumentationLifetime.cs +++ /dev/null @@ -1,9 +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 -namespace Elastic.OpenTelemetry; - -/// -/// A logical Elastic Distribution of OpenTelemetry .NET instance. -/// -public interface IInstrumentationLifetime : IDisposable, IAsyncDisposable; diff --git a/src/Elastic.OpenTelemetry/Instrumentation/InstrumentationAssemblyInfo.cs b/src/Elastic.OpenTelemetry/Instrumentation/InstrumentationAssemblyInfo.cs new file mode 100644 index 0000000..74c3054 --- /dev/null +++ b/src/Elastic.OpenTelemetry/Instrumentation/InstrumentationAssemblyInfo.cs @@ -0,0 +1,15 @@ +// 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 + +#pragma warning disable IDE0130 // Namespace does not match folder structure +namespace Elastic.OpenTelemetry.Instrumentation; +#pragma warning restore IDE0130 // Namespace does not match folder structure + +internal readonly struct InstrumentationAssemblyInfo +{ + public readonly required string Name { get; init; } + public readonly required string Filename { get; init; } + public readonly required string FullyQualifiedType { get; init; } + public readonly required string InstrumentationMethod { get; init; } +} diff --git a/src/Elastic.OpenTelemetry/InstrumentationLifetime.cs b/src/Elastic.OpenTelemetry/InstrumentationLifetime.cs deleted file mode 100644 index 4e99580..0000000 --- a/src/Elastic.OpenTelemetry/InstrumentationLifetime.cs +++ /dev/null @@ -1,34 +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 Elastic.OpenTelemetry.Diagnostics; -using Elastic.OpenTelemetry.Diagnostics.Logging; -using OpenTelemetry.Metrics; -using OpenTelemetry.Trace; - -namespace Elastic.OpenTelemetry; - -internal class InstrumentationLifetime( - CompositeLogger logger, - LoggingEventListener loggingEventListener, - TracerProvider? tracerProvider, - MeterProvider? meterProvider -) : IInstrumentationLifetime -{ - public void Dispose() - { - tracerProvider?.Dispose(); - meterProvider?.Dispose(); - loggingEventListener.Dispose(); - logger.Dispose(); - } - - public async ValueTask DisposeAsync() - { - tracerProvider?.Dispose(); - meterProvider?.Dispose(); - await loggingEventListener.DisposeAsync().ConfigureAwait(false); - await logger.DisposeAsync().ConfigureAwait(false); - } -} diff --git a/src/Elastic.OpenTelemetry/Processors/Composite.cs b/src/Elastic.OpenTelemetry/Processors/Composite.cs index 223c2d7..b85c801 100644 --- a/src/Elastic.OpenTelemetry/Processors/Composite.cs +++ b/src/Elastic.OpenTelemetry/Processors/Composite.cs @@ -1,6 +1,7 @@ // 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.Processors; // TODO - Consider a struct, but consider if this would get copied too much diff --git a/src/Elastic.OpenTelemetry/Processors/ElasticCompatibilityProcessor.cs b/src/Elastic.OpenTelemetry/Processors/ElasticCompatibilityProcessor.cs index fd7f7b6..580bb2a 100644 --- a/src/Elastic.OpenTelemetry/Processors/ElasticCompatibilityProcessor.cs +++ b/src/Elastic.OpenTelemetry/Processors/ElasticCompatibilityProcessor.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using Elastic.OpenTelemetry.Diagnostics; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using OpenTelemetry; using static Elastic.OpenTelemetry.SemanticConventions.TraceSemanticConventions; @@ -19,9 +20,9 @@ namespace Elastic.OpenTelemetry.Processors; /// /// /// -public class ElasticCompatibilityProcessor(ILogger logger) : BaseProcessor +public sealed class ElasticCompatibilityProcessor(ILogger? logger) : BaseProcessor { - private readonly ILogger _logger = logger; + private readonly ILogger _logger = logger ?? NullLogger.Instance; /// public override void OnEnd(Activity activity) diff --git a/src/Elastic.OpenTelemetry/Processors/SpanCompressionProcessor.cs b/src/Elastic.OpenTelemetry/Processors/SpanCompressionProcessor.cs index 6c5b508..b9a696b 100644 --- a/src/Elastic.OpenTelemetry/Processors/SpanCompressionProcessor.cs +++ b/src/Elastic.OpenTelemetry/Processors/SpanCompressionProcessor.cs @@ -3,13 +3,12 @@ // See the LICENSE file in the project root for more information using System.Diagnostics; using System.Runtime.CompilerServices; -using Elastic.OpenTelemetry.Extensions; using OpenTelemetry; namespace Elastic.OpenTelemetry.Processors; /// A processor that can mark spans as compressed/composite -public class SpanCompressionProcessor : BaseProcessor +internal sealed class SpanCompressionProcessor : BaseProcessor { private readonly ConditionalWeakTable _compressionBuffer = new(); diff --git a/src/Elastic.OpenTelemetry/Processors/SpanCounterProcessor.cs b/src/Elastic.OpenTelemetry/Processors/SpanCounterProcessor.cs index 03b1d59..cab648b 100644 --- a/src/Elastic.OpenTelemetry/Processors/SpanCounterProcessor.cs +++ b/src/Elastic.OpenTelemetry/Processors/SpanCounterProcessor.cs @@ -8,7 +8,7 @@ namespace Elastic.OpenTelemetry.Processors; /// An example processor that emits the number of spans as a metric -public class SpanCounterProcessor : BaseProcessor +internal sealed class SpanCounterProcessor : BaseProcessor { private static readonly Meter Meter = new("Elastic.OpenTelemetry", "1.0.0"); private static readonly Counter Counter = Meter.CreateCounter("span-export-count"); diff --git a/src/Elastic.OpenTelemetry/Processors/StackTraceProcessor.cs b/src/Elastic.OpenTelemetry/Processors/StackTraceProcessor.cs index 273d72c..a7dae97 100644 --- a/src/Elastic.OpenTelemetry/Processors/StackTraceProcessor.cs +++ b/src/Elastic.OpenTelemetry/Processors/StackTraceProcessor.cs @@ -6,8 +6,8 @@ namespace Elastic.OpenTelemetry.Processors; -/// A processor that includes stack trace information of long running span -public class StackTraceProcessor : BaseProcessor +/// A processor that includes stack trace information of long running spans. +internal sealed class StackTraceProcessor : BaseProcessor { /// public override void OnStart(Activity data) diff --git a/src/Elastic.OpenTelemetry/Processors/TransactionIdProcessor.cs b/src/Elastic.OpenTelemetry/Processors/TransactionIdProcessor.cs deleted file mode 100644 index 720d214..0000000 --- a/src/Elastic.OpenTelemetry/Processors/TransactionIdProcessor.cs +++ /dev/null @@ -1,36 +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.Diagnostics; -using Elastic.OpenTelemetry.Diagnostics; -using Microsoft.Extensions.Logging; -using OpenTelemetry; - -namespace Elastic.OpenTelemetry.Processors; - -/// -/// -/// -public sealed class TransactionIdProcessor(ILogger logger) : BaseProcessor -{ - private readonly AsyncLocal _currentTransactionId = new(); - - /// - /// - /// - public const string TransactionIdTagName = "transaction.id"; - - /// - public override void OnStart(Activity activity) - { - if (activity.Parent == null) - _currentTransactionId.Value = activity.SpanId; - - if (!_currentTransactionId.Value.HasValue) - return; - - activity.SetTag(TransactionIdTagName, _currentTransactionId.Value.Value.ToString()); - logger.SetTag(nameof(TransactionIdProcessor), TransactionIdTagName, _currentTransactionId.Value.Value.ToString()); - } -} diff --git a/src/Elastic.OpenTelemetry/Resources/HostDetector.cs b/src/Elastic.OpenTelemetry/Resources/HostDetector.cs deleted file mode 100644 index aef3b39..0000000 --- a/src/Elastic.OpenTelemetry/Resources/HostDetector.cs +++ /dev/null @@ -1,193 +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 - -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -// Modified from https://github.com/open-telemetry/opentelemetry-dotnet-contrib/blob/44c8576ef1290d9fbb6fbbdc973ae1b344afb4c2/src/OpenTelemetry.ResourceDetectors.Host/HostDetector.cs -// As the host.id is support not yet released, we are using the code from the contrib project directly for now. - -// TODO - Switch to the contrib package once the features we need are released. - -using System.Diagnostics; -using System.Text; -using Elastic.OpenTelemetry.SemanticConventions; -using Microsoft.Extensions.Logging; -using OpenTelemetry.Resources; -#if NETFRAMEWORK || NET6_0_OR_GREATER -using Microsoft.Win32; -#endif - -namespace OpenTelemetry.ResourceDetectors.Host; - -/// -/// Host detector. -/// -internal sealed class HostDetector : IResourceDetector -{ - private const string ETCMACHINEID = "/etc/machine-id"; - private const string ETCVARDBUSMACHINEID = "/var/lib/dbus/machine-id"; - - private readonly PlatformID _platformId; - private readonly Func> _getFilePaths; - private readonly Func _getMacOsMachineId; - private readonly Func _getWindowsMachineId; - - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - internal HostDetector(ILogger logger) - { - _platformId = Environment.OSVersion.Platform; - _getFilePaths = GetFilePaths; - _getMacOsMachineId = GetMachineIdMacOs; - _getWindowsMachineId = GetMachineIdWindows; - _logger = logger; - } - - /// - /// Detects the resource attributes from host. - /// - /// Resource with key-value pairs of resource attributes. - public Resource Detect() - { - try - { - var attributes = new List>(2) - { - new(ResourceSemanticConventions.AttributeHostName, Environment.MachineName), - }; - var machineId = GetMachineId(); - - if (machineId != null && !string.IsNullOrEmpty(machineId)) - { - attributes.Add(new(ResourceSemanticConventions.AttributeHostId, machineId)); - } - - return new Resource(attributes); - } - catch (InvalidOperationException ex) - { - // Handling InvalidOperationException due to https://learn.microsoft.com/en-us/dotnet/api/system.environment.machinename#exceptions - _logger.LogError("Failed to detect host resource due to {Exception}", ex); - } - - return Resource.Empty; - } - - internal static string? ParseMacOsOutput(string? output) - { - if (output == null || string.IsNullOrEmpty(output)) - { - return null; - } - - var lines = output.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); - - foreach (var line in lines) - { -#if NETFRAMEWORK - if (line.IndexOf("IOPlatformUUID", StringComparison.OrdinalIgnoreCase) >= 0) -#else - if (line.Contains("IOPlatformUUID", StringComparison.OrdinalIgnoreCase)) -#endif - { - var parts = line.Split('"'); - - if (parts.Length > 3) - { - return parts[3]; - } - } - } - - return null; - } - - private static IEnumerable GetFilePaths() - { - yield return ETCMACHINEID; - yield return ETCVARDBUSMACHINEID; - } - - private string? GetMachineIdMacOs() - { - try - { - var startInfo = new ProcessStartInfo - { - FileName = "sh", - Arguments = "ioreg -rd1 -c IOPlatformExpertDevice", - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardOutput = true, - }; - - var sb = new StringBuilder(); - using var process = Process.Start(startInfo); - process?.WaitForExit(); - sb.Append(process?.StandardOutput.ReadToEnd()); - return sb.ToString(); - } - catch (Exception ex) - { - _logger.LogError("Failed to get machine ID on MacOS due to {Exception}", ex); - } - - return null; - } - -#pragma warning disable CA1416 - // stylecop wants this protected by System.OperatingSystem.IsWindows - // this type only exists in .NET 5+ - private string? GetMachineIdWindows() - { -#if NETFRAMEWORK || NET6_0_OR_GREATER - try - { - using var subKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography", false); - return subKey?.GetValue("MachineGuid") as string ?? null; - } - catch (Exception ex) - { - _logger.LogError("Failed to get machine ID on Windows due to {Exception}", ex); - } -#endif - - return null; - } -#pragma warning restore CA1416 - - private string? GetMachineId() => _platformId switch - { - PlatformID.Unix => GetMachineIdLinux(), - PlatformID.MacOSX => ParseMacOsOutput(_getMacOsMachineId()), - PlatformID.Win32NT => _getWindowsMachineId(), - _ => null, - }; - - private string? GetMachineIdLinux() - { - var paths = _getFilePaths(); - - foreach (var path in paths) - { - if (File.Exists(path)) - { - try - { - return File.ReadAllText(path).Trim(); - } - catch (Exception ex) - { - _logger.LogError("Failed to get machine ID on Linux due to {Exception}", ex); - } - } - } - - return null; - } -} diff --git a/tests/Elastic.OpenTelemetry.Tests/AutoInstrumentationPluginTests.cs b/tests/Elastic.OpenTelemetry.Tests/AutoInstrumentationPluginTests.cs new file mode 100644 index 0000000..b6caca8 --- /dev/null +++ b/tests/Elastic.OpenTelemetry.Tests/AutoInstrumentationPluginTests.cs @@ -0,0 +1,39 @@ +// 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 Elastic.OpenTelemetry.Configuration; +using Elastic.OpenTelemetry.Core; + +namespace Elastic.OpenTelemetry.Tests; + +public class AutoInstrumentationPluginTests +{ + [Fact] + public void WritesErrorWhenUnableToBootstrap() + { + var sut = new TestableAutoInstrumentationPlugin(); + + var error = sut.GetErrorText(); + + error.Should().StartWith("Unable to bootstrap EDOT .NET due to"); + error.Should().Contain(TestableAutoInstrumentationPlugin.ExceptionMessage); + } + + private class TestableAutoInstrumentationPlugin : AutoInstrumentationPlugin + { + public const string ExceptionMessage = "This is a test exception!!"; + + private readonly StringWriter _textWriter = new(); + + internal override void SetError() => Console.SetError(_textWriter); + + internal override BootstrapInfo GetBootstrapInfo(out ElasticOpenTelemetryComponents? components) + { + components = null; + return new(SdkActivationMethod.NuGet, new Exception(ExceptionMessage)); + } + + public string GetErrorText() => _textWriter.ToString(); + } +} diff --git a/tests/Elastic.OpenTelemetry.Tests/Configuration/CompositeElasticOpenTelemetryOptionsTests.cs b/tests/Elastic.OpenTelemetry.Tests/Configuration/CompositeElasticOpenTelemetryOptionsTests.cs new file mode 100644 index 0000000..afaab09 --- /dev/null +++ b/tests/Elastic.OpenTelemetry.Tests/Configuration/CompositeElasticOpenTelemetryOptionsTests.cs @@ -0,0 +1,56 @@ +// 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 Elastic.OpenTelemetry.Configuration; + +namespace Elastic.OpenTelemetry.Tests.Configuration; + +public class CompositeElasticOpenTelemetryOptionsTests +{ + [Fact] + public void TwoInstancesAreEqual_WhenAllValuesMatch() + { + var options1 = new CompositeElasticOpenTelemetryOptions(); + var options2 = new CompositeElasticOpenTelemetryOptions(); + + Assert.Equal(options1, options2); + } + + [Fact] + public void TwoInstancesAreEqual_WhenTraceInstrumentationValuesMatch() + { + var options1 = new CompositeElasticOpenTelemetryOptions(new Dictionary() + { + { "OTEL_DOTNET_AUTO_TRACES_INSTRUMENTATION_ENABLED", false }, + { "OTEL_DOTNET_AUTO_TRACES_ELASTICSEARCH_INSTRUMENTATION_ENABLED", true } + }); + + var options2 = new CompositeElasticOpenTelemetryOptions(new Dictionary() + { + { "OTEL_DOTNET_AUTO_TRACES_INSTRUMENTATION_ENABLED", false }, + { "OTEL_DOTNET_AUTO_TRACES_ELASTICSEARCH_INSTRUMENTATION_ENABLED", true } + }); + + Assert.Equal(options1, options2); + } + + [Fact] + public void TwoInstancesAreNotEqual_WhenValuesDoNotMatch() + { + var options1 = new CompositeElasticOpenTelemetryOptions(new Dictionary() + { + { "OTEL_DOTNET_AUTO_TRACES_INSTRUMENTATION_ENABLED", false }, + { "OTEL_DOTNET_AUTO_TRACES_ELASTICSEARCH_INSTRUMENTATION_ENABLED", true }, + { "OTEL_DOTNET_AUTO_TRACES_ASPNETCORE_INSTRUMENTATION_ENABLED", true } + }); + + var options2 = new CompositeElasticOpenTelemetryOptions(new Dictionary() + { + { "OTEL_DOTNET_AUTO_TRACES_INSTRUMENTATION_ENABLED", false }, + { "OTEL_DOTNET_AUTO_TRACES_ELASTICSEARCH_INSTRUMENTATION_ENABLED", true } + }); + + Assert.NotEqual(options1, options2); + } +} diff --git a/tests/Elastic.OpenTelemetry.Tests/Configuration/ElasticOpenTelemetryOptionsTests.cs b/tests/Elastic.OpenTelemetry.Tests/Configuration/ElasticOpenTelemetryOptionsTests.cs index 9a40c6a..b924138 100644 --- a/tests/Elastic.OpenTelemetry.Tests/Configuration/ElasticOpenTelemetryOptionsTests.cs +++ b/tests/Elastic.OpenTelemetry.Tests/Configuration/ElasticOpenTelemetryOptionsTests.cs @@ -11,32 +11,21 @@ using Xunit.Abstractions; using static Elastic.OpenTelemetry.Configuration.EnvironmentVariables; -using static Elastic.OpenTelemetry.Diagnostics.Logging.LogLevelHelpers; +using static Elastic.OpenTelemetry.Diagnostics.LogLevelHelpers; namespace Elastic.OpenTelemetry.Tests.Configuration; public sealed class ElasticOpenTelemetryOptionsTests(ITestOutputHelper output) { - private const int ExpectedLogsLength = 8; - - [Fact] - public void EnabledElasticDefaults_NoneIncludesExpectedValues() - { - var sut = ElasticDefaults.None; - - sut.HasFlag(ElasticDefaults.Traces).Should().BeFalse(); - sut.HasFlag(ElasticDefaults.Logs).Should().BeFalse(); - sut.HasFlag(ElasticDefaults.Metrics).Should().BeFalse(); - } + private const int ExpectedLogsLength = 7; [Fact] public void DefaultCtor_SetsExpectedDefaults_WhenNoEnvironmentVariablesAreConfigured() { - var sut = new ElasticOpenTelemetryOptions(new Hashtable + var sut = new CompositeElasticOpenTelemetryOptions(new Hashtable { { OTEL_DOTNET_AUTO_LOG_DIRECTORY, null }, { OTEL_LOG_LEVEL, null }, - { ELASTIC_OTEL_DEFAULTS_ENABLED, null }, { ELASTIC_OTEL_SKIP_OTLP_EXPORTER, null }, }); @@ -45,10 +34,6 @@ public void DefaultCtor_SetsExpectedDefaults_WhenNoEnvironmentVariablesAreConfig sut.LogDirectory.Should().Be(sut.LogDirectoryDefault); sut.LogLevel.Should().Be(LogLevel.Warning); - sut.ElasticDefaults.Should().Be(ElasticDefaults.All); - sut.ElasticDefaults.Should().HaveFlag(ElasticDefaults.Traces); - sut.ElasticDefaults.Should().HaveFlag(ElasticDefaults.Metrics); - sut.ElasticDefaults.Should().HaveFlag(ElasticDefaults.Logs); sut.SkipOtlpExporter.Should().Be(false); var logger = new TestLogger(output); @@ -65,19 +50,16 @@ public void DefaultCtor_LoadsConfigurationFromEnvironmentVariables() { const string fileLogDirectory = "C:\\Temp"; const string fileLogLevel = "Critical"; - const string enabledElasticDefaults = "None"; - var sut = new ElasticOpenTelemetryOptions(new Hashtable + var sut = new CompositeElasticOpenTelemetryOptions(new Hashtable { { OTEL_DOTNET_AUTO_LOG_DIRECTORY, fileLogDirectory }, { OTEL_LOG_LEVEL, fileLogLevel }, - { ELASTIC_OTEL_DEFAULTS_ENABLED, enabledElasticDefaults }, { ELASTIC_OTEL_SKIP_OTLP_EXPORTER, "true" }, }); sut.LogDirectory.Should().Be(fileLogDirectory); sut.LogLevel.Should().Be(ToLogLevel(fileLogLevel)); - sut.ElasticDefaults.Should().Be(ElasticDefaults.None); sut.SkipOtlpExporter.Should().Be(true); var logger = new TestLogger(output); @@ -88,8 +70,6 @@ public void DefaultCtor_LoadsConfigurationFromEnvironmentVariables() .Contain(s => s.EndsWith("from [Environment]")) .And.Contain(s => s.EndsWith("from [Default]")) .And.NotContain(s => s.EndsWith("from [IConfiguration]")); - - } [Fact] @@ -122,11 +102,10 @@ public void ConfigurationCtor_LoadsConfigurationFromIConfiguration() .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) .Build(); - var sut = new ElasticOpenTelemetryOptions(config, new Hashtable()); + var sut = new CompositeElasticOpenTelemetryOptions(config, new Hashtable()); sut.LogDirectory.Should().Be(@"C:\Temp"); sut.LogLevel.Should().Be(ToLogLevel(fileLogLevel)); - sut.ElasticDefaults.Should().Be(ElasticDefaults.None); sut.SkipOtlpExporter.Should().Be(true); sut.EventLogLevel.Should().Be(EventLevel.Warning); sut.LogLevel.Should().Be(LogLevel.Critical); @@ -170,11 +149,10 @@ public void ConfigurationCtor_LoadsConfigurationFromIConfiguration_AndFallsBackT .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) .Build(); - var sut = new ElasticOpenTelemetryOptions(config, new Hashtable()); + var sut = new CompositeElasticOpenTelemetryOptions(config, new Hashtable()); sut.LogDirectory.Should().Be(@"C:\Temp"); sut.LogLevel.Should().Be(ToLogLevel(loggingSectionLogLevel)); - sut.ElasticDefaults.Should().Be(ElasticDefaults.None); sut.SkipOtlpExporter.Should().Be(true); sut.LogLevel.Should().Be(LogLevel.Warning); sut.EventLogLevel.Should().Be(EventLevel.Warning); @@ -187,7 +165,6 @@ public void ConfigurationCtor_LoadsConfigurationFromIConfiguration_AndFallsBackT .Contain(s => s.EndsWith("from [IConfiguration]")) .And.Contain(s => s.EndsWith("from [Default]")) .And.NotContain(s => s.EndsWith("from [Environment]")); - } [Fact] @@ -217,11 +194,10 @@ public void ConfigurationCtor_LoadsConfigurationFromIConfiguration_AndFallsBackT .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) .Build(); - var sut = new ElasticOpenTelemetryOptions(config, new Hashtable()); + var sut = new CompositeElasticOpenTelemetryOptions(config, new Hashtable()); sut.LogDirectory.Should().Be(@"C:\Temp"); sut.LogLevel.Should().Be(ToLogLevel(loggingSectionDefaultLogLevel)); - sut.ElasticDefaults.Should().Be(ElasticDefaults.None); sut.SkipOtlpExporter.Should().Be(true); sut.EventLogLevel.Should().Be(EventLevel.Informational); @@ -240,7 +216,6 @@ public void EnvironmentVariables_TakePrecedenceOver_ConfigValues() { const string fileLogDirectory = "C:\\Temp"; const string fileLogLevel = "Critical"; - const string enabledElasticDefaults = "None"; var json = $$""" { @@ -259,17 +234,15 @@ public void EnvironmentVariables_TakePrecedenceOver_ConfigValues() .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) .Build(); - var sut = new ElasticOpenTelemetryOptions(config, new Hashtable + var sut = new CompositeElasticOpenTelemetryOptions(config, new Hashtable { { OTEL_DOTNET_AUTO_LOG_DIRECTORY, fileLogDirectory }, { OTEL_LOG_LEVEL, fileLogLevel }, - { ELASTIC_OTEL_DEFAULTS_ENABLED, enabledElasticDefaults }, { ELASTIC_OTEL_SKIP_OTLP_EXPORTER, "true" }, }); sut.LogDirectory.Should().Be(fileLogDirectory); sut.LogLevel.Should().Be(ToLogLevel(fileLogLevel)); - sut.ElasticDefaults.Should().Be(ElasticDefaults.None); sut.SkipOtlpExporter.Should().Be(true); } @@ -279,23 +252,20 @@ public void InitializedProperties_TakePrecedenceOver_EnvironmentValues() const string fileLogDirectory = "C:\\Property"; const string fileLogLevel = "Critical"; - var sut = new ElasticOpenTelemetryOptions(new Hashtable + var sut = new CompositeElasticOpenTelemetryOptions(new Hashtable { { OTEL_DOTNET_AUTO_LOG_DIRECTORY, "C:\\Temp" }, { OTEL_LOG_LEVEL, "Information" }, - { ELASTIC_OTEL_DEFAULTS_ENABLED, "All" }, { ELASTIC_OTEL_SKIP_OTLP_EXPORTER, "true" }, }) { LogDirectory = fileLogDirectory, LogLevel = ToLogLevel(fileLogLevel) ?? LogLevel.None, SkipOtlpExporter = false, - ElasticDefaults = ElasticDefaults.None }; sut.LogDirectory.Should().Be(fileLogDirectory); sut.LogLevel.Should().Be(ToLogLevel(fileLogLevel)); - sut.ElasticDefaults.Should().Be(ElasticDefaults.None); sut.SkipOtlpExporter.Should().Be(false); var logger = new TestLogger(output); diff --git a/tests/Elastic.OpenTelemetry.Tests/Configuration/EnabledDefaultsConfigurationTests.cs b/tests/Elastic.OpenTelemetry.Tests/Configuration/EnabledDefaultsConfigurationTests.cs deleted file mode 100644 index e022d67..0000000 --- a/tests/Elastic.OpenTelemetry.Tests/Configuration/EnabledDefaultsConfigurationTests.cs +++ /dev/null @@ -1,124 +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.Collections; -using System.Text; -using Elastic.OpenTelemetry.Configuration; -using Microsoft.Extensions.Configuration; -using static Elastic.OpenTelemetry.Configuration.ElasticDefaults; - -namespace Elastic.OpenTelemetry.Tests.Configuration; - -public class ElasticDefaultsConfigurationTest -{ - - [Theory] - [ClassData(typeof(DefaultsData))] - public void ParsesFromConfiguration(string optionValue, Action asserts) - { - var json = $$""" - { - "Elastic": { - "OpenTelemetry": { - "ElasticDefaults": "{{optionValue}}", - } - } - } - """; - - var config = new ConfigurationBuilder() - .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) - .Build(); - var sut = new ElasticOpenTelemetryOptions(config, new Hashtable()); - asserts(sut.ElasticDefaults); - } - - [Theory] - [ClassData(typeof(DefaultsData))] - internal void ParseFromEnvironment(string optionValue, Action asserts) - { - - var env = new Hashtable { { EnvironmentVariables.ELASTIC_OTEL_DEFAULTS_ENABLED, optionValue } }; - var sut = new ElasticOpenTelemetryOptions(env); - - asserts(sut.ElasticDefaults); - } - - internal class DefaultsData : TheoryData> - { - public DefaultsData() - { - Add("All", a => - { - a.HasFlag(Traces).Should().BeTrue(); - a.HasFlag(Metrics).Should().BeTrue(); - a.HasFlag(Logs).Should().BeTrue(); - a.Equals(None).Should().BeFalse(); - }); - - Add("all", a => - { - a.HasFlag(Traces).Should().BeTrue(); - a.HasFlag(Metrics).Should().BeTrue(); - a.HasFlag(Logs).Should().BeTrue(); - a.Equals(None).Should().BeFalse(); - }); - - Add("Traces", a => - { - a.HasFlag(Traces).Should().BeTrue(); - a.HasFlag(Metrics).Should().BeFalse(); - a.HasFlag(Logs).Should().BeFalse(); - a.Equals(None).Should().BeFalse(); - }); - - Add("Metrics", a => - { - a.HasFlag(Traces).Should().BeFalse(); - a.HasFlag(Metrics).Should().BeTrue(); - a.HasFlag(Logs).Should().BeFalse(); - a.Equals(None).Should().BeFalse(); - }); - - Add("Logs", a => - { - a.HasFlag(Traces).Should().BeFalse(); - a.HasFlag(Metrics).Should().BeFalse(); - a.HasFlag(Logs).Should().BeTrue(); - a.Equals(None).Should().BeFalse(); - }); - - Add("Traces,Logs", a => - { - a.HasFlag(Traces).Should().BeTrue(); - a.HasFlag(Metrics).Should().BeFalse(); - a.HasFlag(Logs).Should().BeTrue(); - a.Equals(None).Should().BeFalse(); - }); - Add("Traces;Logs", a => - { - a.HasFlag(Traces).Should().BeTrue(); - a.HasFlag(Metrics).Should().BeFalse(); - a.HasFlag(Logs).Should().BeTrue(); - a.Equals(None).Should().BeFalse(); - }); - - Add("traces,logs,metrics", a => - { - a.HasFlag(Traces).Should().BeTrue(); - a.HasFlag(Metrics).Should().BeTrue(); - a.HasFlag(Logs).Should().BeTrue(); - a.Equals(None).Should().BeFalse(); - }); - - Add("None", a => - { - a.HasFlag(Traces).Should().BeFalse(); - a.HasFlag(Metrics).Should().BeFalse(); - a.HasFlag(Logs).Should().BeFalse(); - a.Equals(None).Should().BeTrue(); - }); - } - }; -} diff --git a/tests/Elastic.OpenTelemetry.Tests/Configuration/EnabledSignalsConfigurationTests.cs b/tests/Elastic.OpenTelemetry.Tests/Configuration/EnabledSignalsConfigurationTests.cs deleted file mode 100644 index 3f660d8..0000000 --- a/tests/Elastic.OpenTelemetry.Tests/Configuration/EnabledSignalsConfigurationTests.cs +++ /dev/null @@ -1,248 +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.Collections; -using System.Text; -using Elastic.OpenTelemetry.Configuration; -using Elastic.OpenTelemetry.Configuration.Instrumentations; -using Elastic.OpenTelemetry.Extensions; -using Microsoft.Extensions.Configuration; -using OpenTelemetry; -using Xunit.Abstractions; -using static Elastic.OpenTelemetry.Configuration.EnvironmentVariables; -using static Elastic.OpenTelemetry.Configuration.Signals; - -namespace Elastic.OpenTelemetry.Tests.Configuration; - -public class EnabledSignalsConfigurationTest(ITestOutputHelper output) -{ - - [Theory] - [ClassData(typeof(SignalsAsStringInConfigurationData))] - public void ParsesFromConfiguration(string optionValue, Action asserts) - { - var json = $$""" - { - "Elastic": { - "OpenTelemetry": { - "Signals": "{{optionValue}}", - } - } - } - """; - - var config = new ConfigurationBuilder() - .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) - .Build(); - var sut = new ElasticOpenTelemetryOptions(config, new Hashtable()); - asserts(sut.Signals); - } - - [Fact] - internal void ExplicitlySettingASignalDoesNotDisableOthers() - { - var env = new Hashtable { { OTEL_DOTNET_AUTO_LOGS_INSTRUMENTATION_ENABLED, "1" } }; - var options = new ElasticOpenTelemetryOptions(env); - options.Signals.Should().HaveFlag(Logs); - options.Signals.Should().HaveFlag(Metrics); - options.Signals.Should().HaveFlag(Traces); - options.Signals.Should().HaveFlag(All); - } - - [Fact] - internal void ExplicitlyDisablingASignalDoesNotDisableOthers() - { - var env = new Hashtable { { OTEL_DOTNET_AUTO_LOGS_INSTRUMENTATION_ENABLED, "0" } }; - var options = new ElasticOpenTelemetryOptions(env); - options.Signals.Should().NotHaveFlag(Logs); - options.Signals.Should().HaveFlag(Metrics); - options.Signals.Should().HaveFlag(Traces); - options.Signals.Should().NotHaveFlag(All); - } - - [Fact] - public void OptInFromConfig() - { - var json = $$""" - { - "Elastic": { - "OpenTelemetry": { - "Signals": "All", - "Tracing" : "AspNet;ElasticTransport" - } - } - } - """; - - var config = new ConfigurationBuilder() - .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) - .Build(); - var options = new ElasticOpenTelemetryOptions(config, new Hashtable()); - - options.Tracing.Should().HaveCount(2); - } - [Fact] - public void OptOutFromConfig() - { - var json = $$""" - { - "Elastic": { - "OpenTelemetry": { - "Signals": "All", - "Tracing" : "-AspNet;-ElasticTransport" - } - } - } - """; - - var config = new ConfigurationBuilder() - .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) - .Build(); - - var logger = new TestLogger(output); - var options = new ElasticOpenTelemetryOptions(config, new Hashtable()); - options.LogConfigSources(logger); - - options.Tracing.Should().HaveCount(TraceInstrumentations.All.Count - 2); - - logger.Messages.Should().ContainMatch("*Configured value for Tracing: 'All Except: AspNet, ElasticTransport'*"); - } - - - [Theory] - [InlineData("1", "1", true, true)] - [InlineData("0", "1", true, false)] - [InlineData("0", "0", false, false)] - [InlineData("1", "0", false, true)] - internal void RespectsOveralSignalsEnvironmentVar(string instrumentation, string metrics, bool metricsEnabled, bool traceEnabled) - { - var env = new Hashtable { { OTEL_DOTNET_AUTO_INSTRUMENTATION_ENABLED, instrumentation }, { OTEL_DOTNET_AUTO_METRICS_INSTRUMENTATION_ENABLED, metrics } }; - var options = new ElasticOpenTelemetryOptions(env); - if (metricsEnabled) - options.Signals.Should().HaveFlag(Metrics); - else - options.Signals.Should().NotHaveFlag(Metrics); - - if (traceEnabled) - options.Signals.Should().HaveFlag(Traces); - else - options.Signals.Should().NotHaveFlag(Traces); - - if (instrumentation == "0" && metrics == "0") - options.Signals.Should().Be(None); - else - options.Signals.Should().NotBe(None); - } - - [Theory] - [InlineData("1", "0", true)] - [InlineData("0", "1", true)] - [InlineData("0", "0", false)] - internal void OptInOverridesDefaults(string instrumentation, string metrics, bool enabledMetrics) - { - var env = new Hashtable - { - { OTEL_DOTNET_AUTO_INSTRUMENTATION_ENABLED, instrumentation }, - { "OTEL_DOTNET_AUTO_METRICS_ASPNET_INSTRUMENTATION_ENABLED", metrics } - }; - var options = new ElasticOpenTelemetryOptions(env); - if (metrics == "1") - { - options.Metrics.Should().Contain(MetricInstrumentation.AspNet); - //ensure opt in behavior - if (instrumentation == "0") - options.Metrics.Should().HaveCount(1); - //ensure opt out behaviour - else - options.Metrics.Should().HaveCount(MetricInstrumentations.All.Count - 1); - - } - else - options.Metrics.Should().NotContain(MetricInstrumentation.AspNet); - - if (enabledMetrics) - options.Signals.Should().HaveFlag(Metrics); - else - options.Signals.Should().NotHaveFlag(Metrics); - } - - - - private class SignalsAsStringInConfigurationData : TheoryData> - { - public SignalsAsStringInConfigurationData() - { - Add("All", a => - { - a.HasFlag(Traces).Should().BeTrue(); - a.HasFlag(Metrics).Should().BeTrue(); - a.HasFlag(Logs).Should().BeTrue(); - a.Equals(None).Should().BeFalse(); - }); - - Add("all", a => - { - a.HasFlag(Traces).Should().BeTrue(); - a.HasFlag(Metrics).Should().BeTrue(); - a.HasFlag(Logs).Should().BeTrue(); - a.Equals(None).Should().BeFalse(); - }); - - Add("Traces", a => - { - a.HasFlag(Traces).Should().BeTrue(); - a.HasFlag(Metrics).Should().BeFalse(); - a.HasFlag(Logs).Should().BeFalse(); - a.Equals(None).Should().BeFalse(); - }); - - Add("Metrics", a => - { - a.HasFlag(Traces).Should().BeFalse(); - a.HasFlag(Metrics).Should().BeTrue(); - a.HasFlag(Logs).Should().BeFalse(); - a.Equals(None).Should().BeFalse(); - }); - - Add("Logs", a => - { - a.HasFlag(Traces).Should().BeFalse(); - a.HasFlag(Metrics).Should().BeFalse(); - a.HasFlag(Logs).Should().BeTrue(); - a.Equals(None).Should().BeFalse(); - }); - - Add("Traces,Logs", a => - { - a.HasFlag(Traces).Should().BeTrue(); - a.HasFlag(Metrics).Should().BeFalse(); - a.HasFlag(Logs).Should().BeTrue(); - a.Equals(None).Should().BeFalse(); - }); - Add("Traces;Logs", a => - { - a.HasFlag(Traces).Should().BeTrue(); - a.HasFlag(Metrics).Should().BeFalse(); - a.HasFlag(Logs).Should().BeTrue(); - a.Equals(None).Should().BeFalse(); - }); - - Add("traces,logs,metrics", a => - { - a.HasFlag(Traces).Should().BeTrue(); - a.HasFlag(Metrics).Should().BeTrue(); - a.HasFlag(Logs).Should().BeTrue(); - a.Equals(None).Should().BeFalse(); - }); - - Add("None", a => - { - a.HasFlag(Traces).Should().BeFalse(); - a.HasFlag(Metrics).Should().BeFalse(); - a.HasFlag(Logs).Should().BeFalse(); - a.Equals(None).Should().BeTrue(); - }); - } - }; -} diff --git a/tests/Elastic.OpenTelemetry.Tests/Configuration/GlobalLogConfigurationTests.cs b/tests/Elastic.OpenTelemetry.Tests/Configuration/GlobalLogConfigurationTests.cs index c18d584..58a5850 100644 --- a/tests/Elastic.OpenTelemetry.Tests/Configuration/GlobalLogConfigurationTests.cs +++ b/tests/Elastic.OpenTelemetry.Tests/Configuration/GlobalLogConfigurationTests.cs @@ -14,7 +14,7 @@ public class GlobalLogConfigurationTests [Fact] public void Check_Defaults() { - var config = new ElasticOpenTelemetryOptions(new Hashtable()); + var config = new CompositeElasticOpenTelemetryOptions(new Hashtable()); config.GlobalLogEnabled.Should().BeFalse(); config.LogLevel.Should().Be(LogLevel.Warning); config.LogDirectory.Should().Be(config.LogDirectoryDefault); @@ -29,7 +29,7 @@ public void Check_Defaults() [InlineData(ELASTIC_OTEL_LOG_TARGETS, "file")] public void CheckActivation(string environmentVariable, string value) { - var config = new ElasticOpenTelemetryOptions(new Hashtable { { environmentVariable, value } }); + var config = new CompositeElasticOpenTelemetryOptions(new Hashtable { { environmentVariable, value } }); config.GlobalLogEnabled.Should().BeTrue(); config.LogTargets.Should().Be(LogTargets.File); } @@ -42,7 +42,7 @@ public void CheckActivation(string environmentVariable, string value) [InlineData(ELASTIC_OTEL_LOG_TARGETS, "none")] public void CheckDeactivation(string environmentVariable, string value) { - var config = new ElasticOpenTelemetryOptions(new Hashtable + var config = new CompositeElasticOpenTelemetryOptions(new Hashtable { { OTEL_DOTNET_AUTO_LOG_DIRECTORY, "" }, { environmentVariable, value } @@ -59,7 +59,7 @@ public void CheckDeactivation(string environmentVariable, string value) [InlineData(OTEL_LOG_LEVEL, "None")] public void CheckNonActivation(string environmentVariable, string value) { - var config = new ElasticOpenTelemetryOptions(new Hashtable { { environmentVariable, value } }); + var config = new CompositeElasticOpenTelemetryOptions(new Hashtable { { environmentVariable, value } }); config.GlobalLogEnabled.Should().BeFalse(); } @@ -157,14 +157,14 @@ internal void LogTargetDefaultsToStandardOutIfRunningInContainerWithLogLevelDebu if (!string.IsNullOrWhiteSpace(logTargetsEnvValue)) env.Add(ELASTIC_OTEL_LOG_TARGETS, logTargetsEnvValue); - var config = new ElasticOpenTelemetryOptions(env); + var config = new CompositeElasticOpenTelemetryOptions(env); config.GlobalLogEnabled.Should().Be(globalLogging); config.LogTargets.Should().Be(targets); } - private static ElasticOpenTelemetryOptions CreateConfig(string key, string? envVarValue) + private static CompositeElasticOpenTelemetryOptions CreateConfig(string key, string? envVarValue) { var environment = new Hashtable { { key, envVarValue } }; - return new ElasticOpenTelemetryOptions(environment); + return new CompositeElasticOpenTelemetryOptions(environment); } } diff --git a/tests/Elastic.OpenTelemetry.Tests/Configuration/Instrumentations/LogInstrumentationsTests.cs b/tests/Elastic.OpenTelemetry.Tests/Configuration/Instrumentations/LogInstrumentationsTests.cs new file mode 100644 index 0000000..c66d54f --- /dev/null +++ b/tests/Elastic.OpenTelemetry.Tests/Configuration/Instrumentations/LogInstrumentationsTests.cs @@ -0,0 +1,26 @@ +// 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 Elastic.OpenTelemetry.Configuration.Instrumentations; + +namespace Elastic.OpenTelemetry.Tests.Configuration.Instrumentations; + +public class LogInstrumentationsTests +{ + [Fact] + public void AllTest() + { + var instrumentations = new LogInstrumentations([LogInstrumentation.ILogger]); + + Assert.Equal("All", instrumentations.ToString()); + } + + [Fact] + public void NoneTest() + { + var instrumentations = new LogInstrumentations([]); + + Assert.Equal("None", instrumentations.ToString()); + } +} diff --git a/tests/Elastic.OpenTelemetry.Tests/Configuration/Instrumentations/MetricInstrumentationsTests.cs b/tests/Elastic.OpenTelemetry.Tests/Configuration/Instrumentations/MetricInstrumentationsTests.cs new file mode 100644 index 0000000..3a44393 --- /dev/null +++ b/tests/Elastic.OpenTelemetry.Tests/Configuration/Instrumentations/MetricInstrumentationsTests.cs @@ -0,0 +1,48 @@ +// 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 Elastic.OpenTelemetry.Configuration.Instrumentations; + +namespace Elastic.OpenTelemetry.Tests.Configuration.Instrumentations; + +public class MetricInstrumentationsTests +{ + [Fact] + public void AllTest() + { + var instrumentations = new MetricInstrumentations( + [ + MetricInstrumentation.AspNet, + MetricInstrumentation.AspNetCore, + MetricInstrumentation.HttpClient, + MetricInstrumentation.NetRuntime, + MetricInstrumentation.NServiceBus, + MetricInstrumentation.Process + ]); + + Assert.Equal("All", instrumentations.ToString()); + } + + [Fact] + public void SomeTest() + { + var instrumentations = new MetricInstrumentations( + [ + MetricInstrumentation.HttpClient, + MetricInstrumentation.NetRuntime, + MetricInstrumentation.NServiceBus, + MetricInstrumentation.Process + ]); + + Assert.StartsWith("All Except: AspNet, AspNetCore", instrumentations.ToString()); + } + + [Fact] + public void NoneTest() + { + var instrumentations = new MetricInstrumentations([]); + + Assert.Equal("None", instrumentations.ToString()); + } +} diff --git a/tests/Elastic.OpenTelemetry.Tests/Configuration/Instrumentations/TraceInstrumentationsTests.cs b/tests/Elastic.OpenTelemetry.Tests/Configuration/Instrumentations/TraceInstrumentationsTests.cs new file mode 100644 index 0000000..5fcb8c2 --- /dev/null +++ b/tests/Elastic.OpenTelemetry.Tests/Configuration/Instrumentations/TraceInstrumentationsTests.cs @@ -0,0 +1,80 @@ +// 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 Elastic.OpenTelemetry.Configuration.Instrumentations; + +namespace Elastic.OpenTelemetry.Tests.Configuration.Instrumentations; + +public class TraceInstrumentationsTests +{ + [Fact] + public void AllTest() + { + var instrumentations = new TraceInstrumentations( + [ + TraceInstrumentation.AspNet, + TraceInstrumentation.AspNetCore, + TraceInstrumentation.Azure, + TraceInstrumentation.Elasticsearch, + TraceInstrumentation.ElasticTransport, + TraceInstrumentation.EntityFrameworkCore, + TraceInstrumentation.Graphql, + TraceInstrumentation.GrpcNetClient, + TraceInstrumentation.HttpClient, + TraceInstrumentation.Kafka, + TraceInstrumentation.MassTransit, + TraceInstrumentation.MongoDb, + TraceInstrumentation.MysqlConnector, + TraceInstrumentation.MysqlData, + TraceInstrumentation.Npgsql, + TraceInstrumentation.NServiceBus, + TraceInstrumentation.OracleMda, + TraceInstrumentation.Quartz, + TraceInstrumentation.SqlClient, + TraceInstrumentation.StackExchangeRedis, + TraceInstrumentation.WcfClient, + TraceInstrumentation.WcfService + ]); + + Assert.Equal("All", instrumentations.ToString()); + } + + [Fact] + public void SomeTest() + { + var instrumentations = new TraceInstrumentations( + [ + TraceInstrumentation.Azure, + TraceInstrumentation.Elasticsearch, + TraceInstrumentation.ElasticTransport, + TraceInstrumentation.EntityFrameworkCore, + TraceInstrumentation.Graphql, + TraceInstrumentation.GrpcNetClient, + TraceInstrumentation.HttpClient, + TraceInstrumentation.Kafka, + TraceInstrumentation.MassTransit, + TraceInstrumentation.MongoDb, + TraceInstrumentation.MysqlConnector, + TraceInstrumentation.MysqlData, + TraceInstrumentation.Npgsql, + TraceInstrumentation.NServiceBus, + TraceInstrumentation.OracleMda, + TraceInstrumentation.Quartz, + TraceInstrumentation.SqlClient, + TraceInstrumentation.StackExchangeRedis, + TraceInstrumentation.WcfClient, + TraceInstrumentation.WcfService + ]); + + Assert.StartsWith("All Except: AspNet, AspNetCore", instrumentations.ToString()); + } + + [Fact] + public void NoneTest() + { + var instrumentations = new TraceInstrumentations([]); + + Assert.Equal("None", instrumentations.ToString()); + } +} diff --git a/tests/Elastic.OpenTelemetry.Tests/Elastic.OpenTelemetry.Tests.csproj b/tests/Elastic.OpenTelemetry.Tests/Elastic.OpenTelemetry.Tests.csproj index 67f5186..ee74b26 100644 --- a/tests/Elastic.OpenTelemetry.Tests/Elastic.OpenTelemetry.Tests.csproj +++ b/tests/Elastic.OpenTelemetry.Tests/Elastic.OpenTelemetry.Tests.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/tests/Elastic.OpenTelemetry.Tests/LoggingTests.cs b/tests/Elastic.OpenTelemetry.Tests/LoggingTests.cs index 43ded66..144cb0c 100644 --- a/tests/Elastic.OpenTelemetry.Tests/LoggingTests.cs +++ b/tests/Elastic.OpenTelemetry.Tests/LoggingTests.cs @@ -2,46 +2,90 @@ // 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 Elastic.OpenTelemetry.Configuration; -using Elastic.OpenTelemetry.Extensions; +using System.Text.RegularExpressions; using OpenTelemetry; +using OpenTelemetry.Metrics; using Xunit.Abstractions; namespace Elastic.OpenTelemetry.Tests; -public class LoggingTests(ITestOutputHelper output) +public partial class LoggingTests(ITestOutputHelper output) { + // This regex pattern matches: + // - A date in the format YYYY-MM-DD + // - A time in the format HH:MM:SS.mmm + // - A five-digit number + // - Any number of dashes within square brackets + // - The literal string "[Information]" followed by any number of space characters + // - The literal string "Elastic Distribution of OpenTelemetry(EDOT) .NET:" + [GeneratedRegex(@"\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\]\[\d{5}\]\[-*\]\[Information\]\s+Elastic Distribution of OpenTelemetry \(EDOT\) \.NET:.*")] + private static partial Regex EdotPreamble(); + + // This regex pattern matches: + // - A date in the format YYYY-MM-DD + // - A time in the format HH:MM:SS.mmm + // - A five-digit number + // - Any number of dashes within square brackets + // - The literal string "[Debug]" followed by any number of space characters + // - The literal string "Elastic Distribution of OpenTelemetry(EDOT) .NET:" + [GeneratedRegex(@"\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\]\[\d{5}\]\[-*\]\[Debug\]\s+Reusing existing shared components\.\s+")] + private static partial Regex UsingSharedComponents(); + [Fact] - public async Task ObserveLogging() + public void LoggingIsEnabled_WhenConfiguredViaTracerProviderBuilder() { var logger = new TestLogger(output); - var options = new ElasticOpenTelemetryBuilderOptions { Logger = logger, DistroOptions = new() { SkipOtlpExporter = true } }; - const string activitySourceName = nameof(ObserveLogging); - - var activitySource = new ActivitySource(activitySourceName, "1.0.0"); - - await using (new ElasticOpenTelemetryBuilder(options) - .WithTracing(tpb => tpb - .ConfigureResource(rb => rb.AddService("Test", "1.0.0")) - .AddSource(activitySourceName) - .AddInMemoryExporter([]) - ) - .Build()) - { - using var activity = activitySource.StartActivity(ActivityKind.Internal); - activity?.SetStatus(ActivityStatusCode.Ok); - } - - //assert preamble information gets logged - logger.Messages.Should().ContainMatch("*Elastic Distribution of OpenTelemetry .NET:*"); - - var preambles = logger.Messages.Where(l => l.Contains("[Information] Elastic Distribution of OpenTelemetry .NET:")).ToList(); - preambles.Should().NotBeNull().And.HaveCount(1); - - // assert instrumentation session logs initialized and stack trace gets dumped. - logger.Messages.Should().ContainMatch("*ElasticOpenTelemetryBuilder initialized*"); - - // very lenient format check - logger.Messages.Should().ContainMatch("[*][*][*][Information]*"); + var options = new ElasticOpenTelemetryOptions { AdditionalLogger = logger, SkipOtlpExporter = true }; + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .UseElasticDefaults(options) + .ConfigureResource(rb => rb.AddService("Test", "1.0.0")) + .AddInMemoryExporter([]) + .Build(); + + Assert.Single(logger.Messages.ToArray(), m => EdotPreamble().IsMatch(m)); + } + + [Fact] + public void LoggingIsEnabled_WhenConfiguredViaMeterProviderBuilder() + { + var logger = new TestLogger(output); + var options = new ElasticOpenTelemetryOptions { AdditionalLogger = logger, SkipOtlpExporter = true }; + + using var tracerProvider = Sdk.CreateMeterProviderBuilder() + .UseElasticDefaults(options) + .ConfigureResource(rb => rb.AddService("Test", "1.0.0")) + .AddInMemoryExporter(new List()) + .Build(); + + Assert.Single(logger.Messages.ToArray(), m => EdotPreamble().IsMatch(m)); + } + + [Fact] + public void LoggingPreamble_IsSkipped_WhenReusingSharedComponents() + { + var logger = new TestLogger(output); + var options = new ElasticOpenTelemetryOptions { AdditionalLogger = logger, SkipOtlpExporter = true }; + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .UseElasticDefaults(options) + .ConfigureResource(rb => rb.AddService("Test", "1.0.0")) + .AddInMemoryExporter([]) + .Build(); + + Assert.Single(logger.Messages, m => EdotPreamble().IsMatch(m)); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .UseElasticDefaults(options) + .ConfigureResource(rb => rb.AddService("Test", "1.0.0")) + .AddInMemoryExporter(new List()) + .Build(); + + var messages = logger.Messages.ToArray(); + + // On this builder, because we are reusing the same options, shared components will be available, + // and as such, the pre-amble will not be output a second time. + Assert.Single(messages, m => EdotPreamble().IsMatch(m)); + Assert.Single(messages, m => UsingSharedComponents().IsMatch(m)); } } diff --git a/tests/Elastic.OpenTelemetry.Tests/Processors/TransactionProcessorTests.cs b/tests/Elastic.OpenTelemetry.Tests/Processors/TransactionProcessorTests.cs deleted file mode 100644 index 5ea3cbc..0000000 --- a/tests/Elastic.OpenTelemetry.Tests/Processors/TransactionProcessorTests.cs +++ /dev/null @@ -1,52 +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 Elastic.OpenTelemetry.Configuration; -using OpenTelemetry; -using Xunit.Abstractions; -using OpenTelemetryBuilderExtensions = Elastic.OpenTelemetry.Extensions.OpenTelemetryBuilderExtensions; - -namespace Elastic.OpenTelemetry.Tests.Processors; - -public class TransactionProcessorTests(ITestOutputHelper output) -{ - [Fact] - public void TransactionId_IsNotAdded_WhenElasticDefaultsDoesNotIncludeTracing() - { - var options = new ElasticOpenTelemetryBuilderOptions - { - Logger = new TestLogger(output), - DistroOptions = new ElasticOpenTelemetryOptions() - { - SkipOtlpExporter = true, - ElasticDefaults = ElasticDefaults.None - } - }; - - const string activitySourceName = nameof(TransactionId_IsNotAdded_WhenElasticDefaultsDoesNotIncludeTracing); - - var activitySource = new ActivitySource(activitySourceName, "1.0.0"); - - var exportedItems = new List(); - - using var session = OpenTelemetryBuilderExtensions.Build(new ElasticOpenTelemetryBuilder(options) - .WithTracing(tpb => - { - tpb - .ConfigureResource(rb => rb.AddService("Test", "1.0.0")) - .AddSource(activitySourceName).AddInMemoryExporter(exportedItems); - })); - - using (var activity = activitySource.StartActivity(ActivityKind.Internal)) - activity?.SetStatus(ActivityStatusCode.Ok); - - exportedItems.Should().ContainSingle(); - - var exportedActivity = exportedItems[0]; - - var transactionId = exportedActivity.GetTagItem(TransactionIdProcessor.TransactionIdTagName); - - transactionId.Should().BeNull(); - } -} diff --git a/tests/Elastic.OpenTelemetry.Tests/Resources/ResourceAttributeTests.cs b/tests/Elastic.OpenTelemetry.Tests/Resources/ResourceAttributeTests.cs deleted file mode 100644 index b830241..0000000 --- a/tests/Elastic.OpenTelemetry.Tests/Resources/ResourceAttributeTests.cs +++ /dev/null @@ -1,126 +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 Elastic.OpenTelemetry.Configuration; -using Elastic.OpenTelemetry.Extensions; -using OpenTelemetry; -using OpenTelemetry.Exporter; -using Xunit.Abstractions; - -namespace Elastic.OpenTelemetry.Tests.Resources; - -public sealed class ResourceAttributeTests : IDisposable -{ - private const string OtelResourceAttributes = "OTEL_RESOURCE_ATTRIBUTES"; - - private readonly string? _originalOtelResourceAttributesEnvVar = Environment.GetEnvironmentVariable(OtelResourceAttributes); - - private readonly ElasticOpenTelemetryBuilderOptions _options; - - public ResourceAttributeTests(ITestOutputHelper output) => - _options = new ElasticOpenTelemetryBuilderOptions() - { - Logger = new TestLogger(output), - DistroOptions = new ElasticOpenTelemetryOptions() { SkipOtlpExporter = true } - }; - - [Fact] - public void DefaultServiceResourceAttributesAreAdded() - { - var exportedItems = new List(0); - var exporter = new InMemoryExporter(exportedItems); - - using var session = new ElasticOpenTelemetryBuilder(_options) - .WithTracing(tpb => tpb.AddProcessor(new SimpleActivityExportProcessor(exporter))) - .Build(); - - var resource = exporter.ParentProvider.GetResource(); - - resource.Attributes.Should().ContainSingle(a => a.Key == "service.name") - .Subject.Value.Should().NotBeNull().And.BeAssignableTo().Which.Should().StartWith("unknown_service:"); - - var instanceId = resource.Attributes.Should().ContainSingle(a => a.Key == "service.instance.id") - .Subject.Value.Should().NotBeNull().And.BeAssignableTo().Subject; - - Guid.TryParseExact(instanceId, "D", out _).Should().BeTrue(); // The distro should set a GUID for the instance ID if not configured by the user - } - - [Fact] - public void UserProvidedConfigureResourceValues_ShouldOverrideDefaults() - { - const string serviceName = "Test"; - const string serviceVersion = "1.0.0"; - const string serviceInstanceId = "FromConfigureResource"; - - var exportedItems = new List(0); - var exporter = new InMemoryExporter(exportedItems); - - using var session = new ElasticOpenTelemetryBuilder(_options) - .WithTracing(tpb => tpb - .ConfigureResource(rb => rb.AddService(serviceName, serviceVersion: serviceVersion, serviceInstanceId: serviceInstanceId)) - .AddProcessor(new SimpleActivityExportProcessor(exporter))) - .Build(); - - var resource = exporter.ParentProvider.GetResource(); - - resource.Attributes.Should().ContainSingle(a => a.Key == "service.name") - .Subject.Value.Should().NotBeNull().And.BeAssignableTo().Which.Should().Be(serviceName); - - resource.Attributes.Should().ContainSingle(a => a.Key == "service.version") - .Subject.Value.Should().NotBeNull().And.BeAssignableTo().Which.Should().Be(serviceVersion); - - resource.Attributes.Should().ContainSingle(a => a.Key == "service.instance.id") - .Subject.Value.Should().NotBeNull().And.BeAssignableTo().Which.Should().Be(serviceInstanceId); - } - - [Fact] - public void UserProvided_ResourceEnvironmentVariable_ShouldOverrideDefaults() - { - const string serviceName = "service-from-env-var"; - const string serviceInstanceId = "instance-from-env-var"; - - Environment.SetEnvironmentVariable(OtelResourceAttributes, $"service.name={serviceName},service.instance.id={serviceInstanceId}"); - - var exportedItems = new List(0); - var exporter = new InMemoryExporter(exportedItems); - - using var session = new ElasticOpenTelemetryBuilder(_options) - .WithTracing(tpb => tpb.AddProcessor(new SimpleActivityExportProcessor(exporter))) - .Build(); - - var resource = exporter.ParentProvider.GetResource(); - - resource.Attributes.Should().ContainSingle(a => a.Key == "service.name") - .Subject.Value.Should().NotBeNull().And.BeAssignableTo().Which.Should().Be(serviceName); - - resource.Attributes.Should().ContainSingle(a => a.Key == "service.instance.id") - .Subject.Value.Should().NotBeNull().And.BeAssignableTo().Which.Should().Be(serviceInstanceId); - - ResetEnvironmentVariables(); - } - - [Fact] - public void DefaultHostResourceAttributesAreAdded() - { - var exportedItems = new List(0); - var exporter = new InMemoryExporter(exportedItems); - - using var session = new ElasticOpenTelemetryBuilder(_options) - .WithTracing(tpb => tpb.AddProcessor(new SimpleActivityExportProcessor(exporter))) - .Build(); - - var resource = exporter.ParentProvider.GetResource(); - - resource.Attributes.Should().ContainSingle(a => a.Key == "host.name") - .Subject.Value.Should().NotBeNull().And.BeAssignableTo().Which.Should().NotBeEmpty(); - - resource.Attributes.Should().ContainSingle(a => a.Key == "host.id") - .Subject.Value.Should().NotBeNull().And.BeAssignableTo().Which.Should().NotBeEmpty(); - } - - private void ResetEnvironmentVariables() => - Environment.SetEnvironmentVariable(OtelResourceAttributes, _originalOtelResourceAttributesEnvVar); - - public void Dispose() => ResetEnvironmentVariables(); -} diff --git a/tests/Elastic.OpenTelemetry.Tests/ServiceCollectionTests.cs b/tests/Elastic.OpenTelemetry.Tests/ServiceCollectionTests.cs index 8495c35..4d074c2 100644 --- a/tests/Elastic.OpenTelemetry.Tests/ServiceCollectionTests.cs +++ b/tests/Elastic.OpenTelemetry.Tests/ServiceCollectionTests.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information using Elastic.OpenTelemetry.Configuration; +using Elastic.OpenTelemetry.Core; +using Elastic.OpenTelemetry.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using OpenTelemetry; @@ -13,15 +15,9 @@ namespace Elastic.OpenTelemetry.Tests; public class ServiceCollectionTests(ITestOutputHelper output) { [Fact] - public async Task ServiceCollectionAddIsSafeToCallMultipleTimes() + public async Task ServiceCollection_AddOpenTelemetry_IsSafeToCallMultipleTimes() { - var options = new ElasticOpenTelemetryBuilderOptions - { - Logger = new TestLogger(output), - DistroOptions = new ElasticOpenTelemetryOptions() { SkipOtlpExporter = true } - }; - - const string activitySourceName = nameof(ServiceCollectionAddIsSafeToCallMultipleTimes); + const string activitySourceName = nameof(ServiceCollection_AddOpenTelemetry_IsSafeToCallMultipleTimes); var activitySource = new ActivitySource(activitySourceName, "1.0.0"); var exportedItems = new List(); @@ -29,6 +25,12 @@ public async Task ServiceCollectionAddIsSafeToCallMultipleTimes() var host = Host.CreateDefaultBuilder(); host.ConfigureServices(s => { + var options = new ElasticOpenTelemetryOptions() + { + SkipOtlpExporter = true, + AdditionalLogger = new TestLogger(output) + }; + s.AddElasticOpenTelemetry(options) .WithTracing(tpb => tpb .ConfigureResource(rb => rb.AddService("Test", "1.0.0")) @@ -50,19 +52,77 @@ public async Task ServiceCollectionAddIsSafeToCallMultipleTimes() await ctx.DisposeAsync(); } - exportedItems.Should().ContainSingle(); + Assert.Single(exportedItems); } [Fact] - public void ServiceCollectionAddElasticOpenTelemetry_ReturnsSameBuilder_WhenCalledMultipleTimes() + public async Task ServiceCollection_AddElasticOpenTelemetry_IsSafeToCallMultipleTimes() { + const string activitySourceName = nameof(ServiceCollection_AddElasticOpenTelemetry_IsSafeToCallMultipleTimes); + var activitySource = new ActivitySource(activitySourceName, "1.0.0"); + + var exportedItems = new List(); + + var host = Host.CreateDefaultBuilder(); + host.ConfigureServices(s => + { + var options = new ElasticOpenTelemetryOptions() + { + SkipOtlpExporter = true, + AdditionalLogger = new TestLogger(output) + }; + + s.AddElasticOpenTelemetry(options) + .WithTracing(tpb => tpb + .ConfigureResource(rb => rb.AddService("Test", "1.0.0")) + .AddSource(activitySourceName) + .AddInMemoryExporter(exportedItems) + ); + + s.AddElasticOpenTelemetry(); + s.AddElasticOpenTelemetry(); + s.AddElasticOpenTelemetry(); + }); + + var ctx = new CancellationTokenRegistration(); + using (var app = host.Build()) + { + _ = app.RunAsync(ctx.Token); + using (var activity = activitySource.StartActivity(ActivityKind.Internal)) + activity?.SetStatus(ActivityStatusCode.Ok); + await ctx.DisposeAsync(); + } + + Assert.Single(exportedItems); + } + + [Fact] + public void ServiceCollectionAddElasticOpenTelemetry_ReturnsSameComponents_WhenCalledMultipleTimes() + { + // Ensure that when AddElasticOpenTelemetry is called multiple times on the same IServiceCollection, + // a single instance of the components is registered, as we expect those to be cached per IServiceCollection. + // Even though each call operates on a new `OpenTelemetryBuilder`, our code is designed to reduce accidental, + // duplication of bootstrapping in such scenarios. + var serviceCollection = new ServiceCollection(); - var builder1 = serviceCollection.AddElasticOpenTelemetry(); - builder1.ConfigureResource(r => r.AddService("test-service")); + serviceCollection.AddElasticOpenTelemetry(); + + var initialComponents = serviceCollection.Single(d => d.ServiceType == typeof(ElasticOpenTelemetryComponents)).ImplementationInstance; + + serviceCollection.AddElasticOpenTelemetry(); + + using var serviceProvider = serviceCollection.BuildServiceProvider(); + + var components = serviceProvider.GetServices(); + + Assert.Single(components); + Assert.Same(initialComponents, components.Single()); - var builder2 = serviceCollection.AddElasticOpenTelemetry(); + var hostedService = serviceProvider.GetServices() + .Where(t => t is ElasticOpenTelemetryService) + .Cast(); - builder1.Should().Be(builder2); + Assert.Single(hostedService); } } diff --git a/tests/Elastic.OpenTelemetry.Tests/TestLogger.cs b/tests/Elastic.OpenTelemetry.Tests/TestLogger.cs index 5a89eca..9f3f48e 100644 --- a/tests/Elastic.OpenTelemetry.Tests/TestLogger.cs +++ b/tests/Elastic.OpenTelemetry.Tests/TestLogger.cs @@ -2,7 +2,7 @@ // 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 Elastic.OpenTelemetry.Diagnostics.Logging; +using Elastic.OpenTelemetry.Diagnostics; using Microsoft.Extensions.Logging; using Xunit.Abstractions; diff --git a/tests/Elastic.OpenTelemetry.Tests/TransactionIdProcessorTests.cs b/tests/Elastic.OpenTelemetry.Tests/TransactionIdProcessorTests.cs deleted file mode 100644 index 9464893..0000000 --- a/tests/Elastic.OpenTelemetry.Tests/TransactionIdProcessorTests.cs +++ /dev/null @@ -1,47 +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 Elastic.OpenTelemetry.Configuration; -using Elastic.OpenTelemetry.Extensions; -using OpenTelemetry; -using Xunit.Abstractions; - -namespace Elastic.OpenTelemetry.Tests; - -public class TransactionIdProcessorTests(ITestOutputHelper output) -{ - private readonly ITestOutputHelper _output = output; - - [Fact] - public void TransactionId_IsAddedToTags() - { - var options = new ElasticOpenTelemetryBuilderOptions { Logger = new TestLogger(_output), DistroOptions = new ElasticOpenTelemetryOptions() { SkipOtlpExporter = true } }; - const string activitySourceName = nameof(TransactionId_IsAddedToTags); - - var activitySource = new ActivitySource(activitySourceName, "1.0.0"); - - var exportedItems = new List(); - - using var session = new ElasticOpenTelemetryBuilder(options) - .WithTracing(tpb => - { - tpb - .ConfigureResource(rb => rb.AddService("Test", "1.0.0")) - .AddSource(activitySourceName) - .AddInMemoryExporter(exportedItems); - }) - .Build(); - - using (var activity = activitySource.StartActivity(ActivityKind.Internal)) - activity?.SetStatus(ActivityStatusCode.Ok); - - exportedItems.Should().ContainSingle(); - - var exportedActivity = exportedItems[0]; - - var transactionId = exportedActivity.GetTagItem(TransactionIdProcessor.TransactionIdTagName); - - transactionId.Should().NotBeNull().And.BeAssignableTo().Which.Should().NotBeEmpty(); - } -}