diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/CHANGELOG.md b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/CHANGELOG.md index f92253e4f597..a8d1d292f74a 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/CHANGELOG.md +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/CHANGELOG.md @@ -4,6 +4,15 @@ ### Features Added +* Added support for configuring sampling via OpenTelemetry environment + variables: + * `OTEL_TRACES_SAMPLER` (supported values: `microsoft.rate_limited`, + `microsoft.fixed_percentage`). + * `OTEL_TRACES_SAMPLER_ARG` (rate limit in traces/sec for + `microsoft.rate_limited`, sampling ratio 0.0 - 1.0 for + `microsoft.fixed_percentage`). + ([#52720](https://github.com/Azure/azure-sdk-for-net/pull/52720)) + ### Breaking Changes ### Bugs Fixed @@ -260,7 +269,6 @@ - Update OpenTelemetry dependencies ([41398](https://github.com/Azure/azure-sdk-for-net/pull/41398)) - OpenTelemetry 1.7.0 - - OpenTelemetry.Extensions.Hosting 1.7.0 - NEW: OpenTelemetry.Instrumentation.AspNetCore 1.7.0 - NEW: OpenTelemetry.Instrumentation.Http 1.7.0 diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/AzureMonitorAspNetCoreEventSource.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/AzureMonitorAspNetCoreEventSource.cs index 9f3c3d251aef..68f881dddb39 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/AzureMonitorAspNetCoreEventSource.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/AzureMonitorAspNetCoreEventSource.cs @@ -127,5 +127,11 @@ public void ErrorInitializingPartOfSdkVersion(string typeName, System.Exception [Event(13, Message = "Failed to get Type version while initialize SDK version due to an exception. Not user actionable. Type: {0}. {1}", Level = EventLevel.Warning)] public void ErrorInitializingPartOfSdkVersion(string typeName, string exceptionMessage) => WriteEvent(13, typeName, exceptionMessage); + + [Event(14, Message = "Invalid sampler type '{0}'. Supported values: microsoft.rate_limited, microsoft.fixed_percentage", Level = EventLevel.Warning)] + public void InvalidSamplerType(string samplerType) => WriteEvent(14, samplerType); + + [Event(15, Message = "Invalid sampler argument '{1}' for sampler '{0}'. Ignoring.", Level = EventLevel.Warning)] + public void InvalidSamplerArgument(string samplerType, string samplerArg) => WriteEvent(15, samplerType, samplerArg); } } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/DefaultAzureMonitorOptions.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/DefaultAzureMonitorOptions.cs index 8d3e6220af01..9a54cf8ae102 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/DefaultAzureMonitorOptions.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/DefaultAzureMonitorOptions.cs @@ -4,6 +4,7 @@ using Azure.Monitor.OpenTelemetry.Exporter.Internals.Platform; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; +using System.Globalization; namespace Azure.Monitor.OpenTelemetry.AspNetCore { @@ -35,6 +36,11 @@ public void Configure(AzureMonitorOptions options) { options.ConnectionString = connectionStringFromIConfig; } + + // Sampler configuration via IConfiguration + var samplerFromConfig = _configuration[EnvironmentVariableConstants.OTEL_TRACES_SAMPLER]; + var samplerArgFromConfig = _configuration[EnvironmentVariableConstants.OTEL_TRACES_SAMPLER_ARG]; + ConfigureSamplingOptions(samplerFromConfig, samplerArgFromConfig, options); } // Environment Variable should take precedence. @@ -43,6 +49,69 @@ public void Configure(AzureMonitorOptions options) { options.ConnectionString = connectionStringFromEnvVar; } + + // Explicit environment variables for sampler should override IConfiguration. + var samplerTypeEnv = Environment.GetEnvironmentVariable(EnvironmentVariableConstants.OTEL_TRACES_SAMPLER); + var samplerArgEnv = Environment.GetEnvironmentVariable(EnvironmentVariableConstants.OTEL_TRACES_SAMPLER_ARG); + ConfigureSamplingOptions(samplerTypeEnv, samplerArgEnv, options); + } + catch (Exception ex) + { + AzureMonitorAspNetCoreEventSource.Log.ConfigureFailed(ex); + } + } + + private static void ConfigureSamplingOptions(string? samplerType, string? samplerArg, AzureMonitorOptions options) + { + if (string.IsNullOrEmpty(samplerType) || string.IsNullOrEmpty(samplerArg)) + { + return; + } + + try + { + var samplerKey = samplerType!.Trim().ToLowerInvariant(); + string samplerArgValue = samplerArg ?? string.Empty; + switch (samplerKey) + { + case "microsoft.rate_limited": + if (double.TryParse(samplerArg, NumberStyles.Float, CultureInfo.InvariantCulture, out var tracesPerSecond)) + { + if (tracesPerSecond >= 0) + { + options.TracesPerSecond = tracesPerSecond; + } + else + { + AzureMonitorAspNetCoreEventSource.Log.InvalidSamplerArgument(samplerKey, samplerArgValue); + } + } + else + { + AzureMonitorAspNetCoreEventSource.Log.InvalidSamplerArgument(samplerKey, samplerArgValue); + } + break; + case "microsoft.fixed_percentage": + if (double.TryParse(samplerArg, NumberStyles.Float, CultureInfo.InvariantCulture, out var ratio)) + { + if (ratio >= 0.0 && ratio <= 1.0) + { + options.SamplingRatio = (float)ratio; + } + else + { + AzureMonitorAspNetCoreEventSource.Log.InvalidSamplerArgument(samplerKey, samplerArgValue); + } + } + else + { + AzureMonitorAspNetCoreEventSource.Log.InvalidSamplerArgument(samplerKey, samplerArgValue); + } + break; + default: + AzureMonitorAspNetCoreEventSource.Log.InvalidSamplerType(samplerType ?? string.Empty); + break; + } } catch (Exception ex) { diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/DefaultAzureMonitorOptionsSamplerTests.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/DefaultAzureMonitorOptionsSamplerTests.cs new file mode 100644 index 000000000000..77e862ca7806 --- /dev/null +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/DefaultAzureMonitorOptionsSamplerTests.cs @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace Azure.Monitor.OpenTelemetry.AspNetCore.Tests +{ + [CollectionDefinition("AspNetCoreSamplerEnvVarTests", DisableParallelization = true)] + public class DefaultAzureMonitorOptionsSamplerTests + { + [Fact] + public void Configure_Sampler_From_IConfiguration_FixedPercentage() + { + var configValues = new List> + { + new("OTEL_TRACES_SAMPLER", "microsoft.fixed_percentage"), + new("OTEL_TRACES_SAMPLER_ARG", "0.40"), + }; + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configValues).Build(); + var configurator = new DefaultAzureMonitorOptions(configuration); + var options = new AzureMonitorOptions(); + + configurator.Configure(options); + + Assert.Equal(0.40f, options.SamplingRatio); + Assert.Null(options.TracesPerSecond); + } + + [Fact] + public void Configure_Sampler_From_IConfiguration_RateLimited() + { + var configValues = new List> + { + new("OTEL_TRACES_SAMPLER", "microsoft.rate_limited"), + new("OTEL_TRACES_SAMPLER_ARG", "15"), + }; + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configValues).Build(); + var configurator = new DefaultAzureMonitorOptions(configuration); + var options = new AzureMonitorOptions(); + + configurator.Configure(options); + + Assert.Equal(15d, options.TracesPerSecond); + Assert.Equal(1.0f, options.SamplingRatio); // default unchanged + } + + [Fact] + public void Configure_Sampler_InvalidArgs_Ignored() + { + // invalid percentage > 1 + var configValues = new List> + { + new("OTEL_TRACES_SAMPLER", "microsoft.fixed_percentage"), + new("OTEL_TRACES_SAMPLER_ARG", "1.5"), + }; + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configValues).Build(); + var configurator = new DefaultAzureMonitorOptions(configuration); + var options = new AzureMonitorOptions(); + configurator.Configure(options); + Assert.Equal(1.0f, options.SamplingRatio); // default + Assert.Null(options.TracesPerSecond); + + // invalid negative rate + configValues = new List> + { + new("OTEL_TRACES_SAMPLER", "microsoft.rate_limited"), + new("OTEL_TRACES_SAMPLER_ARG", "-2"), + }; + configuration = new ConfigurationBuilder().AddInMemoryCollection(configValues).Build(); + configurator = new DefaultAzureMonitorOptions(configuration); + options = new AzureMonitorOptions(); + configurator.Configure(options); + Assert.Null(options.TracesPerSecond); + Assert.Equal(1.0f, options.SamplingRatio); + } + + [Fact] + public void Configure_Sampler_EnvironmentVariable_Overrides_IConfiguration() + { + var configValues = new List> + { + new("OTEL_TRACES_SAMPLER", "microsoft.fixed_percentage"), + new("OTEL_TRACES_SAMPLER_ARG", "0.20"), + }; + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configValues).Build(); + var configurator = new DefaultAzureMonitorOptions(configuration); + var options = new AzureMonitorOptions(); + + string? prevSampler = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER"); + string? prevSamplerArg = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG"); + try + { + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", "microsoft.rate_limited"); + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", "11"); + + configurator.Configure(options); + + Assert.Equal(0.20f, options.SamplingRatio); // from config + Assert.Equal(11d, options.TracesPerSecond); // from env + } + finally + { + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", prevSampler); + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", prevSamplerArg); + } + } + + [Fact] + public void Configure_Sampler_EnvironmentVariable_Only_FixedPercentage() + { + string? prevSampler = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER"); + string? prevSamplerArg = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG"); + try + { + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", "microsoft.fixed_percentage"); + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", "0.55"); + + var configurator = new DefaultAzureMonitorOptions(); + var options = new AzureMonitorOptions(); + configurator.Configure(options); + + Assert.Equal(0.55f, options.SamplingRatio); + Assert.Null(options.TracesPerSecond); + } + finally + { + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", prevSampler); + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", prevSamplerArg); + } + } + } +} diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/CHANGELOG.md b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/CHANGELOG.md index 9fab36210a6f..daa94e377a68 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/CHANGELOG.md +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/CHANGELOG.md @@ -6,6 +6,17 @@ * Added mapping for `enduser.pseudo.id` attribute to `user_Id` +* Added support for configuring sampling via OpenTelemetry environment + variables: + * `OTEL_TRACES_SAMPLER` (supported values: `microsoft.rate_limited`, + `microsoft.fixed_percentage`). + * `OTEL_TRACES_SAMPLER_ARG` (rate limit in traces/sec for + `microsoft.rate_limited`, sampling ratio 0.0 - 1.0 for + `microsoft.fixed_percentage`). This now applies to both + `UseAzureMonitorExporter` and the direct + `Sdk.CreateTracerProviderBuilder().AddAzureMonitorTraceExporter(...)` path. + ([#52720](https://github.com/Azure/azure-sdk-for-net/pull/52720)) + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/AzureMonitorExporterExtensions.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/AzureMonitorExporterExtensions.cs index 90a19f000ca7..b2cdebe7ba02 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/AzureMonitorExporterExtensions.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/AzureMonitorExporterExtensions.cs @@ -9,6 +9,7 @@ using Azure.Monitor.OpenTelemetry.Exporter.Internals; using Azure.Monitor.OpenTelemetry.Exporter.Internals.Diagnostics; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using OpenTelemetry; using OpenTelemetry.Logs; @@ -47,12 +48,19 @@ public static TracerProviderBuilder AddAzureMonitorTraceExporter( var finalOptionsName = name ?? Options.DefaultName; - if (name != null && configure != null) + // Ensure our default options configurator (which reads IConfiguration + environment variables) + // is registered exactly once so that OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARG work for this path. + builder.ConfigureServices(services => { - // If we are using named options we register the - // configuration delegate into options pipeline. - builder.ConfigureServices(services => services.Configure(finalOptionsName, configure)); - } + services.TryAddEnumerable(ServiceDescriptor.Singleton, DefaultAzureMonitorExporterOptions>()); + + if (name != null && configure != null) + { + // If we are using named options we register the configuration delegate into the options pipeline + // After the DefaultAzureMonitorExporterOptions so explicit code configuration can override env/config values. + services.Configure(finalOptionsName, configure); + } + }); var deferredBuilder = builder as IDeferredTracerProviderBuilder; if (deferredBuilder == null) @@ -66,11 +74,7 @@ public static TracerProviderBuilder AddAzureMonitorTraceExporter( var exporterOptions = sp.GetRequiredService>().Get(finalOptionsName); if (name == null && configure != null) { - // If we are NOT using named options, we execute the - // configuration delegate inline. The reason for this is - // AzureMonitorExporterOptions is shared by all signals. Without a - // name, delegates for all signals will mix together. See: - // https://github.com/open-telemetry/opentelemetry-dotnet/issues/4043 + // For unnamed options execute configuration delegate inline so it overrides env/config values. configure(exporterOptions); } @@ -85,8 +89,6 @@ public static TracerProviderBuilder AddAzureMonitorTraceExporter( if (credential != null) { - // Credential can be set by either AzureMonitorExporterOptions or Extension Method Parameter. - // Options should take precedence. exporterOptions.Credential ??= credential; } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/DefaultAzureMonitorExporterOptions.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/DefaultAzureMonitorExporterOptions.cs index 587d796a4107..8d2090868430 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/DefaultAzureMonitorExporterOptions.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/DefaultAzureMonitorExporterOptions.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using Azure.Monitor.OpenTelemetry.Exporter.Internals.Diagnostics; using Azure.Monitor.OpenTelemetry.Exporter.Internals.Platform; using Microsoft.Extensions.Configuration; @@ -38,6 +39,11 @@ public void Configure(AzureMonitorExporterOptions options) { options.ConnectionString = connectionStringFromIConfig; } + + // Sampler configuration via IConfiguration + var samplerFromConfig = _configuration[EnvironmentVariableConstants.OTEL_TRACES_SAMPLER]; + var samplerArgFromConfig = _configuration[EnvironmentVariableConstants.OTEL_TRACES_SAMPLER_ARG]; + ConfigureSamplingOptions(samplerFromConfig, samplerArgFromConfig, options); } // Environment Variable should take precedence. @@ -46,6 +52,69 @@ public void Configure(AzureMonitorExporterOptions options) { options.ConnectionString = connectionStringFromEnvVar; } + + // Explicit environment variables for sampler should override IConfiguration. + var samplerTypeEnv = Environment.GetEnvironmentVariable(EnvironmentVariableConstants.OTEL_TRACES_SAMPLER); + var samplerArgEnv = Environment.GetEnvironmentVariable(EnvironmentVariableConstants.OTEL_TRACES_SAMPLER_ARG); + ConfigureSamplingOptions(samplerTypeEnv, samplerArgEnv, options); + } + catch (Exception ex) + { + AzureMonitorExporterEventSource.Log.ConfigureFailed(ex); + } + } + + private static void ConfigureSamplingOptions(string? samplerType, string? samplerArg, AzureMonitorExporterOptions options) + { + if (string.IsNullOrEmpty(samplerType) || string.IsNullOrEmpty(samplerArg)) + { + return; + } + + try + { + var samplerKey = samplerType!.Trim().ToLowerInvariant(); + string samplerArgValue = samplerArg ?? string.Empty; // ensure non-null for logging + switch (samplerKey) + { + case "microsoft.rate_limited": + if (double.TryParse(samplerArg, NumberStyles.Float, CultureInfo.InvariantCulture, out var tracesPerSecond)) + { + if (tracesPerSecond >= 0) + { + options.TracesPerSecond = tracesPerSecond; + } + else + { + AzureMonitorExporterEventSource.Log.InvalidSamplerArgument(samplerKey, samplerArgValue); + } + } + else + { + AzureMonitorExporterEventSource.Log.InvalidSamplerArgument(samplerKey, samplerArgValue); + } + break; + case "microsoft.fixed_percentage": + if (double.TryParse(samplerArg, NumberStyles.Float, CultureInfo.InvariantCulture, out var ratio)) + { + if (ratio >= 0.0 && ratio <= 1.0) + { + options.SamplingRatio = (float)ratio; + } + else + { + AzureMonitorExporterEventSource.Log.InvalidSamplerArgument(samplerKey, samplerArgValue); + } + } + else + { + AzureMonitorExporterEventSource.Log.InvalidSamplerArgument(samplerKey, samplerArgValue); + } + break; + default: + AzureMonitorExporterEventSource.Log.InvalidSamplerType(samplerType ?? string.Empty); + break; + } } catch (Exception ex) { diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Internals/Diagnostics/AzureMonitorExporterEventSource.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Internals/Diagnostics/AzureMonitorExporterEventSource.cs index 89bff7ca3e57..8affcf29db13 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Internals/Diagnostics/AzureMonitorExporterEventSource.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Internals/Diagnostics/AzureMonitorExporterEventSource.cs @@ -485,5 +485,11 @@ public void CustomerSdkStatsInitializationFailed(Exception ex) [Event(48, Message = "Customer SDK stats initialization failed due to an exception. This is only for internal telemetry and can safely be ignored. {0}", Level = EventLevel.Warning)] public void CustomerSdkStatsInitializationFailed(string exceptionMessage) => WriteEvent(48, exceptionMessage); + + [Event(49, Message = "Invalid sampler type '{0}'. Supported values: microsoft.rate_limited, microsoft.fixed_percentage", Level = EventLevel.Warning)] + public void InvalidSamplerType(string samplerType) => WriteEvent(49, samplerType); + + [Event(50, Message = "Invalid sampler argument '{1}' for sampler '{0}'. Ignoring.", Level = EventLevel.Warning)] + public void InvalidSamplerArgument(string samplerType, string samplerArg) => WriteEvent(50, samplerType, samplerArg); } } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Internals/Platform/EnvironmentVariableConstants.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Internals/Platform/EnvironmentVariableConstants.cs index 02eb3771e960..3c98c517a38e 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Internals/Platform/EnvironmentVariableConstants.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Internals/Platform/EnvironmentVariableConstants.cs @@ -28,6 +28,8 @@ internal static class EnvironmentVariableConstants EXPORT_RESOURCE_METRIC, ASPNETCORE_DISABLE_URL_QUERY_REDACTION, HTTPCLIENT_DISABLE_URL_QUERY_REDACTION, + OTEL_TRACES_SAMPLER, + OTEL_TRACES_SAMPLER_ARG, }; /// @@ -124,5 +126,18 @@ internal static class EnvironmentVariableConstants /// . /// public const string HTTPCLIENT_DISABLE_URL_QUERY_REDACTION = "OTEL_DOTNET_EXPERIMENTAL_HTTPCLIENT_DISABLE_URL_QUERY_REDACTION"; + + /// + /// OpenTelemetry environment variable to specify the sampler to use for traces. + /// Supported values: microsoft.rate_limited, microsoft.fixed_percentage + /// + public const string OTEL_TRACES_SAMPLER = "OTEL_TRACES_SAMPLER"; + + /// + /// OpenTelemetry environment variable to specify the sampler argument. + /// For microsoft.rate_limited sampler: traces per second (double). + /// For microsoft.fixed_percentage sampler: sampling ratio (double from 0 to 1). + /// + public const string OTEL_TRACES_SAMPLER_ARG = "OTEL_TRACES_SAMPLER_ARG"; } } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/AddAzureMonitorTraceExporterSamplerTests.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/AddAzureMonitorTraceExporterSamplerTests.cs new file mode 100644 index 000000000000..9ddbe0f5f8f9 --- /dev/null +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/AddAzureMonitorTraceExporterSamplerTests.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Reflection; +using OpenTelemetry; +using OpenTelemetry.Trace; +using Xunit; + +namespace Azure.Monitor.OpenTelemetry.Exporter.Tests +{ + [CollectionDefinition("TraceExporterEnvVarTests", DisableParallelization = true)] + public class AddAzureMonitorTraceExporterSamplerTests + { + private const string ConnStr = "InstrumentationKey=00000000-0000-0000-0000-000000000000"; + + [Fact] + public void EnvVar_RateLimitedSampler_Applied() + { + string? prevSampler = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER"); + string? prevArg = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG"); + try + { + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", "microsoft.rate_limited"); + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", "7"); + + using var provider = Sdk.CreateTracerProviderBuilder() + .AddAzureMonitorTraceExporter(o => o.ConnectionString = ConnStr) + .Build(); + + var sampler = GetSampler(provider); + Assert.Equal("Azure.Monitor.OpenTelemetry.Exporter.Internals.RateLimitedSampler", sampler.GetType().FullName); + } + finally + { + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", prevSampler); + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", prevArg); + } + } + + [Fact] + public void EnvVar_FixedPercentageSampler_Applied() + { + string? prevSampler = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER"); + string? prevArg = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG"); + try + { + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", "microsoft.fixed_percentage"); + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", "0.25"); + + using var provider = Sdk.CreateTracerProviderBuilder() + .AddAzureMonitorTraceExporter(o => o.ConnectionString = ConnStr) + .Build(); + + var sampler = GetSampler(provider); + Assert.Equal("Azure.Monitor.OpenTelemetry.Exporter.Internals.ApplicationInsightsSampler", sampler.GetType().FullName); + } + finally + { + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", prevSampler); + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", prevArg); + } + } + + [Fact] + public void ConfigureDelegate_OverridesEnvVar() + { + string? prevSampler = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER"); + string? prevArg = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG"); + try + { + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", "microsoft.fixed_percentage"); + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", "0.10"); + + using var provider = Sdk.CreateTracerProviderBuilder() + .AddAzureMonitorTraceExporter(o => { o.ConnectionString = ConnStr; o.TracesPerSecond = 5; }) + .Build(); + + var sampler = GetSampler(provider); + Assert.Equal("Azure.Monitor.OpenTelemetry.Exporter.Internals.RateLimitedSampler", sampler.GetType().FullName); + } + finally + { + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", prevSampler); + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", prevArg); + } + } + + private static Sampler GetSampler(TracerProvider provider) + { + // The concrete TracerProvider implementation is internal to OTel SDK; retrieve via reflection. + var samplerProperty = provider.GetType().GetProperty("Sampler", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + Assert.NotNull(samplerProperty); + var sampler = samplerProperty!.GetValue(provider) as Sampler; + Assert.NotNull(sampler); + return sampler!; + } + } +} diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/DefaultAzureMonitorExporterOptionsTests.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/DefaultAzureMonitorExporterOptionsTests.cs new file mode 100644 index 000000000000..4ccc2bd238db --- /dev/null +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/DefaultAzureMonitorExporterOptionsTests.cs @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace Azure.Monitor.OpenTelemetry.Exporter.Tests +{ + [Collection("ExporterEnvVarTests")] + public class DefaultAzureMonitorExporterOptionsTests + { + [Fact] + public void Configure_Sampler_From_IConfiguration_FixedPercentage() + { + string? prevSampler = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER"); + string? prevSamplerArg = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG"); + + try + { + // Clear environment variables to ensure they don't interfere with IConfiguration + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", null); + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", null); + + // Arrange + var configValues = new List> + { + new("OTEL_TRACES_SAMPLER", "microsoft.fixed_percentage"), + new("OTEL_TRACES_SAMPLER_ARG", "0.25"), + }; + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configValues).Build(); + var defaultConfigurator = new DefaultAzureMonitorExporterOptions(configuration); + var options = new AzureMonitorExporterOptions(); + + // Act + defaultConfigurator.Configure(options); + + // Assert + Assert.Equal(0.25f, options.SamplingRatio); + Assert.Null(options.TracesPerSecond); // Not set for fixed percentage + } + finally + { + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", prevSampler); + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", prevSamplerArg); + } + } + + [Fact] + public void Configure_Sampler_From_IConfiguration_RateLimited() + { + string? prevSampler = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER"); + string? prevSamplerArg = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG"); + + try + { + // Clear environment variables to ensure they don't interfere with IConfiguration + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", null); + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", null); + + // Arrange + var configValues = new List> + { + new("OTEL_TRACES_SAMPLER", "microsoft.rate_limited"), + new("OTEL_TRACES_SAMPLER_ARG", "5"), + }; + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configValues).Build(); + var defaultConfigurator = new DefaultAzureMonitorExporterOptions(configuration); + var options = new AzureMonitorExporterOptions(); + + // Act + defaultConfigurator.Configure(options); + + // Assert + Assert.Equal(5d, options.TracesPerSecond); + Assert.Equal(1.0f, options.SamplingRatio); // untouched + } + finally + { + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", prevSampler); + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", prevSamplerArg); + } + } + + [Fact] + public void Configure_Sampler_InvalidArgs_Ignored() + { + string? prevSampler = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER"); + string? prevSamplerArg = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG"); + + try + { + // Clear environment variables to ensure they don't interfere with IConfiguration + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", null); + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", null); + + // Arrange invalid fixed percentage (>1) + var configValues = new List> + { + new("OTEL_TRACES_SAMPLER", "microsoft.fixed_percentage"), + new("OTEL_TRACES_SAMPLER_ARG", "1.5"), + }; + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configValues).Build(); + var defaultConfigurator = new DefaultAzureMonitorExporterOptions(configuration); + var options = new AzureMonitorExporterOptions(); + + // Act + defaultConfigurator.Configure(options); + + // Assert - defaults unchanged + Assert.Equal(1.0f, options.SamplingRatio); + Assert.Null(options.TracesPerSecond); + + // Now test invalid negative rate_limited + configValues = new List> + { + new("OTEL_TRACES_SAMPLER", "microsoft.rate_limited"), + new("OTEL_TRACES_SAMPLER_ARG", "-2"), + }; + configuration = new ConfigurationBuilder().AddInMemoryCollection(configValues).Build(); + defaultConfigurator = new DefaultAzureMonitorExporterOptions(configuration); + options = new AzureMonitorExporterOptions(); + + // Act + defaultConfigurator.Configure(options); + + // Assert + Assert.Null(options.TracesPerSecond); + Assert.Equal(1.0f, options.SamplingRatio); + } + finally + { + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", prevSampler); + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", prevSamplerArg); + } + } + + [Fact] + public void Configure_Sampler_EnvironmentVariable_Only_FixedPercentage() + { + string? prevSampler = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER"); + string? prevSamplerArg = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG"); + + try + { + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", "microsoft.fixed_percentage"); + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", "0.30"); + + var defaultConfigurator = new DefaultAzureMonitorExporterOptions(); + var options = new AzureMonitorExporterOptions(); + + // Act + defaultConfigurator.Configure(options); + + // Assert + Assert.Equal(0.30f, options.SamplingRatio); + Assert.Null(options.TracesPerSecond); + } + finally + { + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", prevSampler); + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", prevSamplerArg); + } + } + + [Fact] + public void Configure_Sampler_EnvironmentVariable_Overrides_IConfiguration() + { + string? prevSampler = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER"); + string? prevSamplerArg = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG"); + + try + { + // Clear environment variables first to ensure clean state + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", null); + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", null); + + // Arrange - configuration provides fixed percentage + var configValues = new List> + { + new("OTEL_TRACES_SAMPLER", "microsoft.fixed_percentage"), + new("OTEL_TRACES_SAMPLER_ARG", "0.50"), + }; + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configValues).Build(); + var defaultConfigurator = new DefaultAzureMonitorExporterOptions(configuration); + var options = new AzureMonitorExporterOptions(); + + // Environment overrides to rate_limited 10 + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", "microsoft.rate_limited"); + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", "10"); + + // Act + defaultConfigurator.Configure(options); + + // Assert + Assert.Equal(0.50f, options.SamplingRatio); // value from config still present + Assert.Equal(10d, options.TracesPerSecond); // overridden by env var + } + finally + { + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", prevSampler); + Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", prevSamplerArg); + } + } + } +}