diff --git a/docs/trace/customizing-the-sdk/README.md b/docs/trace/customizing-the-sdk/README.md index 4d4a0c772e4..01db0f6fd6d 100644 --- a/docs/trace/customizing-the-sdk/README.md +++ b/docs/trace/customizing-the-sdk/README.md @@ -363,6 +363,26 @@ var tracerProvider = Sdk.CreateTracerProviderBuilder() .Build(); ``` +It is also possible to configure the sampler by using the following +environmental variables: + +| Environment variable | Description | +| -------------------------- | -------------------------------------------------- | +| `OTEL_TRACES_SAMPLER` | Sampler to be used for traces. See the [General SDK Configuration specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md#general-sdk-configuration) for more details. | +| `OTEL_TRACES_SAMPLER_ARG` | String value to be used as the sampler argument. | + +The supported values for `OTEL_TRACES_SAMPLER` are: + +* `always_off` +* `always_on` +* `traceidratio` +* `parentbased_always_on`, +* `parentbased_always_off` +* `parentbased_traceidratio` + +The options `traceidratio` and `parentbased_traceidratio` may have the sampler +probability configured via the `OTEL_TRACES_SAMPLER_ARG` environment variable. + Follow [this](../extending-the-sdk/README.md#sampler) document to learn about writing custom samplers. diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index d6856d52532..ae356a76d42 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -81,6 +81,16 @@ Released 2024-Mar-14 Specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md#exemplar). ([#5412](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5412)) +* `TracerProvider`s can now have a sampler configured via the + `OTEL_TRACES_SAMPLER` environment variable. The supported values are: + `always_off`, `always_on`, `traceidratio`, `parentbased_always_on`, + `parentbased_always_off`, and `parentbased_traceidratio`. The options + `traceidratio` and `parentbased_traceidratio` may have the sampler probability + configured via the `OTEL_TRACES_SAMPLER_ARG` environment variable. + For details see: [OpenTelemetry Environment Variable + Specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md#general-sdk-configuration). + ([#5448](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5448)) + ## 1.7.0 Released 2023-Dec-08 diff --git a/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs b/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs index 38765609b54..6e244cce5ed 100644 --- a/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs +++ b/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs @@ -346,6 +346,18 @@ public void MetricInstrumentRemoved(string instrumentName, string meterName) this.WriteEvent(53, instrumentName, meterName); } + [Event(54, Message = "OTEL_TRACES_SAMPLER configuration was found but the value '{0}' is invalid and will be ignored.", Level = EventLevel.Warning)] + public void TracesSamplerConfigInvalid(string configValue) + { + this.WriteEvent(54, configValue); + } + + [Event(55, Message = "OTEL_TRACES_SAMPLER_ARG configuration was found but the value '{0}' is invalid and will be ignored, default of value of '1.0' will be used.", Level = EventLevel.Warning)] + public void TracesSamplerArgConfigInvalid(string configValue) + { + this.WriteEvent(55, configValue); + } + #if DEBUG public class OpenTelemetryEventListener : EventListener { diff --git a/src/OpenTelemetry/Trace/TracerProviderSdk.cs b/src/OpenTelemetry/Trace/TracerProviderSdk.cs index 927285a4ee0..fc3fe02631a 100644 --- a/src/OpenTelemetry/Trace/TracerProviderSdk.cs +++ b/src/OpenTelemetry/Trace/TracerProviderSdk.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Internal; using OpenTelemetry.Resources; @@ -13,6 +14,9 @@ namespace OpenTelemetry.Trace; internal sealed class TracerProviderSdk : TracerProvider { + internal const string TracesSamplerConfigKey = "OTEL_TRACES_SAMPLER"; + internal const string TracesSamplerArgConfigKey = "OTEL_TRACES_SAMPLER_ARG"; + internal readonly IServiceProvider ServiceProvider; internal readonly IDisposable? OwnedServiceProvider; internal int ShutdownCount; @@ -57,7 +61,7 @@ internal TracerProviderSdk( resourceBuilder.ServiceProvider = serviceProvider; this.Resource = resourceBuilder.Build(); - this.sampler = state.Sampler ?? new ParentBasedSampler(new AlwaysOnSampler()); + this.sampler = GetSampler(serviceProvider!.GetRequiredService(), state.Sampler); OpenTelemetrySdkEventSource.Log.TracerProviderSdkEvent($"Sampler added = \"{this.sampler.GetType()}\"."); this.supportLegacyActivity = state.LegacyActivityOperationNames.Count > 0; @@ -401,6 +405,81 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } + private static Sampler GetSampler(IConfiguration configuration, Sampler? stateSampler) + { + Sampler? sampler = null; + + if (stateSampler != null) + { + sampler = stateSampler; + } + + if (configuration.TryGetStringValue(TracesSamplerConfigKey, out var configValue)) + { + if (sampler != null) + { + OpenTelemetrySdkEventSource.Log.TracerProviderSdkEvent( + $"Trace sampler configuration value '{configValue}' has been ignored because a value '{sampler.GetType().FullName}' was set programmatically."); + return sampler; + } + + switch (configValue) + { + case var _ when string.Equals(configValue, "always_on", StringComparison.OrdinalIgnoreCase): + sampler = new AlwaysOnSampler(); + break; + case var _ when string.Equals(configValue, "always_off", StringComparison.OrdinalIgnoreCase): + sampler = new AlwaysOffSampler(); + break; + case var _ when string.Equals(configValue, "traceidratio", StringComparison.OrdinalIgnoreCase): + { + var traceIdRatio = ReadTraceIdRatio(configuration); + sampler = new TraceIdRatioBasedSampler(traceIdRatio); + break; + } + + case var _ when string.Equals(configValue, "parentbased_always_on", StringComparison.OrdinalIgnoreCase): + sampler = new ParentBasedSampler(new AlwaysOnSampler()); + break; + case var _ when string.Equals(configValue, "parentbased_always_off", StringComparison.OrdinalIgnoreCase): + sampler = new ParentBasedSampler(new AlwaysOffSampler()); + break; + case var _ when string.Equals(configValue, "parentbased_traceidratio", StringComparison.OrdinalIgnoreCase): + { + var traceIdRatio = ReadTraceIdRatio(configuration); + sampler = new ParentBasedSampler(new TraceIdRatioBasedSampler(traceIdRatio)); + break; + } + + default: + OpenTelemetrySdkEventSource.Log.TracesSamplerConfigInvalid(configValue ?? string.Empty); + break; + } + + if (sampler != null) + { + OpenTelemetrySdkEventSource.Log.TracerProviderSdkEvent($"Trace sampler set to '{sampler.GetType().FullName}' from configuration."); + } + } + + return sampler ?? new ParentBasedSampler(new AlwaysOnSampler()); + } + + private static double ReadTraceIdRatio(IConfiguration configuration) + { + if (configuration.TryGetStringValue(TracesSamplerArgConfigKey, out var configValue) && + double.TryParse(configValue, out var traceIdRatio)) + { + return traceIdRatio; + } + else + { + OpenTelemetrySdkEventSource.Log.TracesSamplerArgConfigInvalid(configValue ?? string.Empty); + } + + return 1.0; + } + private static ActivitySamplingResult ComputeActivitySamplingResult( ref ActivityCreationOptions options, Sampler sampler) diff --git a/test/OpenTelemetry.Tests/Trace/TracerProviderSdkTest.cs b/test/OpenTelemetry.Tests/Trace/TracerProviderSdkTest.cs index b78744b49b5..9e6e7320371 100644 --- a/test/OpenTelemetry.Tests/Trace/TracerProviderSdkTest.cs +++ b/test/OpenTelemetry.Tests/Trace/TracerProviderSdkTest.cs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Instrumentation; using OpenTelemetry.Resources; using OpenTelemetry.Resources.Tests; @@ -1039,6 +1041,59 @@ public void SdkPopulatesSamplingParamsCorrectlyForLegacyActivityWithInProcParent activity.Stop(); } + [Theory] + [InlineData(null, null, "ParentBased{AlwaysOnSampler}")] + [InlineData("always_on", null, "AlwaysOnSampler")] + [InlineData("always_off", null, "AlwaysOffSampler")] + [InlineData("always_OFF", null, "AlwaysOffSampler")] + [InlineData("traceidratio", "0.5", "TraceIdRatioBasedSampler{0.500000}")] + [InlineData("traceidratio", "not_a_double", "TraceIdRatioBasedSampler{1.000000}")] + [InlineData("parentbased_always_on", null, "ParentBased{AlwaysOnSampler}")] + [InlineData("parentbased_always_off", null, "ParentBased{AlwaysOffSampler}")] + [InlineData("parentbased_traceidratio", "0.111", "ParentBased{TraceIdRatioBasedSampler{0.111000}}")] + [InlineData("parentbased_traceidratio", "not_a_double", "ParentBased{TraceIdRatioBasedSampler{1.000000}}")] + [InlineData("ParentBased_TraceIdRatio", "0.000001", "ParentBased{TraceIdRatioBasedSampler{0.000001}}")] + public void TestSamplerSetFromConfiguration(string configValue, string argValue, string samplerDescription) + { + var configBuilder = new ConfigurationBuilder(); + + configBuilder.AddInMemoryCollection(new Dictionary + { + [TracerProviderSdk.TracesSamplerConfigKey] = configValue, + [TracerProviderSdk.TracesSamplerArgConfigKey] = argValue, + }); + + var builder = Sdk.CreateTracerProviderBuilder(); + builder.ConfigureServices(s => s.AddSingleton(configBuilder.Build())); + using var tracerProvider = builder.Build(); + var tracerProviderSdk = tracerProvider as TracerProviderSdk; + + Assert.NotNull(tracerProviderSdk); + Assert.NotNull(tracerProviderSdk.Sampler); + Assert.Equal(samplerDescription, tracerProviderSdk.Sampler.Description); + } + + [Fact] + public void TestSamplerConfigurationIgnoredWhenSetProgrammatically() + { + var configBuilder = new ConfigurationBuilder(); + configBuilder.AddInMemoryCollection(new Dictionary + { + [TracerProviderSdk.TracesSamplerConfigKey] = "always_off", + }); + + var builder = Sdk.CreateTracerProviderBuilder(); + builder.ConfigureServices(s => s.AddSingleton(configBuilder.Build())); + builder.SetSampler(new AlwaysOnSampler()); + + using var tracerProvider = builder.Build(); + var tracerProviderSdk = tracerProvider as TracerProviderSdk; + + Assert.NotNull(tracerProviderSdk); + Assert.NotNull(tracerProviderSdk.Sampler); + Assert.Equal("AlwaysOnSampler", tracerProviderSdk.Sampler.Description); + } + [Fact] public void TracerProvideSdkCreatesAndDiposesInstrumentation() {