From 4b4a98fbb6a6ea524ad0a1a06c8b66d24caec177 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Thu, 13 Feb 2025 08:42:03 +0000 Subject: [PATCH] Refactor public API and align with 1.10 and 1.11 of the SDK (#210) This is quite a significant update from the previous code and includes a lot of refactorings as a result. In short, as the SDK since 1.10 includes an `OpenTelemetryBuilder`, we can base many extension methods directly on the interface it implements `IOpenTelemetryBuilder`. A significant change in this PR is removing the "magic" global override we used to replace `AddOpenTelemetry` with our behaviour. We no longer do this to avoid surprises and prefer an explicit small code change from consumers. We now also track the calls to the various builders to avoid bootstrapping components more than is required and log such circumstances. We expect the auto instrumentation tests to fail for now as those need to be uncoupled from the SDK workflow. Closes #164 Closes #175 --- .github/workflows/bootstrap/action.yml | 3 + .github/workflows/ci-docs.yml | 8 +- .github/workflows/ci.yml | 24 +- build/build.fsproj | 34 +- build/scripts/CommandLine.fs | 1 - .../Example.AspNetCore.Mvc.csproj | 7 +- examples/Example.AspNetCore.Mvc/Program.cs | 17 +- .../Properties/launchSettings.json | 3 + .../Example.AspNetCore.Mvc/appsettings.json | 11 +- .../Example.AutoInstrumentation.csproj | 2 +- .../Example.Console/Example.Console.csproj | 2 +- examples/Example.Console/Usage.cs | 77 ++-- .../Example.MinimalApi.csproj | 2 +- examples/Example.MinimalApi/Program.cs | 4 +- .../Properties/launchSettings.json | 4 +- .../appsettings.Development.json | 3 +- .../Example.WorkerService.csproj | 4 +- examples/Example.WorkerService/Program.cs | 2 +- examples/ServiceDefaults/Extensions.cs | 24 +- .../ServiceDefaults/ServiceDefaults.csproj | 14 +- .../AutoInstrumentationPlugin.cs | 76 ---- .../CompositeElasticOpenTelemetryOptions.cs | 302 ++++++++++++++ .../Configuration/ConfigCell.cs | 8 - .../Configuration/ConfigSource.cs | 15 + .../Configuration/ElasticDefaults.cs | 29 -- .../ElasticOpenTelemetryBuilderOptions.cs | 41 -- .../ElasticOpenTelemetryOptions.cs | 252 +----------- .../Configuration/EnvironmentVariables.cs | 24 +- .../Instrumentations/LogInstrumentation.cs | 27 +- .../Instrumentations/MetricInstrumentation.cs | 37 +- .../Instrumentations/TraceInstrumentation.cs | 69 ++-- .../Configuration/LogTargets.cs | 9 +- .../Parsers/ConfigurationParser.cs | 84 +--- .../Parsers/EnvironmentParser.cs | 8 +- .../Configuration/Parsers/SharedParsers.cs | 82 +--- .../SdkActivationMethod.cs} | 9 +- .../Configuration/Signals.cs | 26 +- .../Core/AutoInstrumentationPlugin.cs | 108 +++++ .../Core/BootstrapInfo.cs | 27 ++ .../Core/BuilderState.cs | 29 ++ .../Core/ElasticOpenTelemetry.cs | 179 ++++++++ .../Core/ElasticOpenTelemetryComponents.cs | 49 +++ .../Core/GlobalProviderBuilderState.cs | 22 + .../Core/SignalBuilder.cs | 134 ++++++ .../{ => Core}/VersionHelper.cs | 2 +- .../OpenTelemetryServicesExtensions.cs | 32 -- .../ServiceCollectionExtensions.cs | 70 ---- .../{Logging => }/AgentLoggingHelpers.cs | 12 +- .../{Logging => }/CompositeLogger.cs | 37 +- .../Diagnostics/{Logging => }/FileLogger.cs | 67 +-- .../Diagnostics/{Logging => }/LogFormatter.cs | 13 +- .../{Logging => }/LogLevelHelpers.cs | 2 +- .../Diagnostics/{Logging => }/LogState.cs | 2 +- .../Diagnostics/LoggerMessages.cs | 210 ++++++++-- .../Diagnostics/LoggingEventListener.cs | 23 +- .../{Logging => }/StandardOutLogger.cs | 11 +- .../Elastic.OpenTelemetry.csproj | 27 +- .../ElasticOpenTelemetryBuilder.cs | 173 -------- .../Extensions/ActivityExtensions.cs | 22 +- .../HostApplicationBuilderExtensions.cs | 70 ++++ .../Extensions/LoggerFactoryExtensions.cs | 14 + .../LoggingProviderBuilderExtensions.cs | 130 +++++- .../MeterProviderBuilderExtensions.cs | 236 ++++++++++- .../OpenTelemetryBuilderExtensions.cs | 385 ++++++++++++++++-- .../OpenTelemetryLoggerOptionsExtensions.cs | 14 +- .../Extensions/ResourceBuilderExtensions.cs | 15 +- .../Extensions/ServiceCollectionExtensions.cs | 133 ++++++ .../TracerProviderBuilderExtensions.cs | 320 +++++++++++++-- .../Hosting/ElasticOpenTelemetryService.cs | 26 +- .../IInstrumentationLifetime.cs | 9 - .../InstrumentationAssemblyInfo.cs | 15 + .../InstrumentationLifetime.cs | 34 -- .../Processors/Composite.cs | 1 + .../ElasticCompatibilityProcessor.cs | 5 +- .../Processors/SpanCompressionProcessor.cs | 3 +- .../Processors/SpanCounterProcessor.cs | 2 +- .../Processors/StackTraceProcessor.cs | 4 +- .../Processors/TransactionIdProcessor.cs | 36 -- .../Resources/HostDetector.cs | 193 --------- .../AutoInstrumentationPluginTests.cs | 39 ++ ...mpositeElasticOpenTelemetryOptionsTests.cs | 56 +++ .../ElasticOpenTelemetryOptionsTests.cs | 48 +-- .../EnabledDefaultsConfigurationTests.cs | 124 ------ .../EnabledSignalsConfigurationTests.cs | 248 ----------- .../GlobalLogConfigurationTests.cs | 14 +- .../LogInstrumentationsTests.cs | 26 ++ .../MetricInstrumentationsTests.cs | 48 +++ .../TraceInstrumentationsTests.cs | 80 ++++ .../Elastic.OpenTelemetry.Tests.csproj | 4 +- .../LoggingTests.cs | 108 +++-- .../Processors/TransactionProcessorTests.cs | 52 --- .../Resources/ResourceAttributeTests.cs | 126 ------ .../ServiceCollectionTests.cs | 88 +++- .../Elastic.OpenTelemetry.Tests/TestLogger.cs | 2 +- .../TransactionIdProcessorTests.cs | 47 --- 95 files changed, 3116 insertions(+), 2234 deletions(-) delete mode 100644 src/Elastic.OpenTelemetry/AutoInstrumentationPlugin.cs create mode 100644 src/Elastic.OpenTelemetry/Configuration/CompositeElasticOpenTelemetryOptions.cs create mode 100644 src/Elastic.OpenTelemetry/Configuration/ConfigSource.cs delete mode 100644 src/Elastic.OpenTelemetry/Configuration/ElasticDefaults.cs delete mode 100644 src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryBuilderOptions.cs rename src/Elastic.OpenTelemetry/{EmptyInstrumentationLifetime.cs => Configuration/SdkActivationMethod.cs} (53%) create mode 100644 src/Elastic.OpenTelemetry/Core/AutoInstrumentationPlugin.cs create mode 100644 src/Elastic.OpenTelemetry/Core/BootstrapInfo.cs create mode 100644 src/Elastic.OpenTelemetry/Core/BuilderState.cs create mode 100644 src/Elastic.OpenTelemetry/Core/ElasticOpenTelemetry.cs create mode 100644 src/Elastic.OpenTelemetry/Core/ElasticOpenTelemetryComponents.cs create mode 100644 src/Elastic.OpenTelemetry/Core/GlobalProviderBuilderState.cs create mode 100644 src/Elastic.OpenTelemetry/Core/SignalBuilder.cs rename src/Elastic.OpenTelemetry/{ => Core}/VersionHelper.cs (97%) delete mode 100644 src/Elastic.OpenTelemetry/DependencyInjection/OpenTelemetryServicesExtensions.cs delete mode 100644 src/Elastic.OpenTelemetry/DependencyInjection/ServiceCollectionExtensions.cs rename src/Elastic.OpenTelemetry/Diagnostics/{Logging => }/AgentLoggingHelpers.cs (69%) rename src/Elastic.OpenTelemetry/Diagnostics/{Logging => }/CompositeLogger.cs (61%) rename src/Elastic.OpenTelemetry/Diagnostics/{Logging => }/FileLogger.cs (53%) rename src/Elastic.OpenTelemetry/Diagnostics/{Logging => }/LogFormatter.cs (88%) rename src/Elastic.OpenTelemetry/Diagnostics/{Logging => }/LogLevelHelpers.cs (97%) rename src/Elastic.OpenTelemetry/Diagnostics/{Logging => }/LogState.cs (95%) rename src/Elastic.OpenTelemetry/Diagnostics/{Logging => }/StandardOutLogger.cs (75%) delete mode 100644 src/Elastic.OpenTelemetry/ElasticOpenTelemetryBuilder.cs create mode 100644 src/Elastic.OpenTelemetry/Extensions/HostApplicationBuilderExtensions.cs create mode 100644 src/Elastic.OpenTelemetry/Extensions/LoggerFactoryExtensions.cs create mode 100644 src/Elastic.OpenTelemetry/Extensions/ServiceCollectionExtensions.cs delete mode 100644 src/Elastic.OpenTelemetry/IInstrumentationLifetime.cs create mode 100644 src/Elastic.OpenTelemetry/Instrumentation/InstrumentationAssemblyInfo.cs delete mode 100644 src/Elastic.OpenTelemetry/InstrumentationLifetime.cs delete mode 100644 src/Elastic.OpenTelemetry/Processors/TransactionIdProcessor.cs delete mode 100644 src/Elastic.OpenTelemetry/Resources/HostDetector.cs create mode 100644 tests/Elastic.OpenTelemetry.Tests/AutoInstrumentationPluginTests.cs create mode 100644 tests/Elastic.OpenTelemetry.Tests/Configuration/CompositeElasticOpenTelemetryOptionsTests.cs delete mode 100644 tests/Elastic.OpenTelemetry.Tests/Configuration/EnabledDefaultsConfigurationTests.cs delete mode 100644 tests/Elastic.OpenTelemetry.Tests/Configuration/EnabledSignalsConfigurationTests.cs create mode 100644 tests/Elastic.OpenTelemetry.Tests/Configuration/Instrumentations/LogInstrumentationsTests.cs create mode 100644 tests/Elastic.OpenTelemetry.Tests/Configuration/Instrumentations/MetricInstrumentationsTests.cs create mode 100644 tests/Elastic.OpenTelemetry.Tests/Configuration/Instrumentations/TraceInstrumentationsTests.cs delete mode 100644 tests/Elastic.OpenTelemetry.Tests/Processors/TransactionProcessorTests.cs delete mode 100644 tests/Elastic.OpenTelemetry.Tests/Resources/ResourceAttributeTests.cs delete mode 100644 tests/Elastic.OpenTelemetry.Tests/TransactionIdProcessorTests.cs 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(); - } -}