Skip to content

Commit 9c1c993

Browse files
committed
Refactor sampler config to use SamplerOptions and IOptions
- Introduce `SamplerOptions` to encapsulate trace sampler configuration, parsing OTEL_TRACES_SAMPLER and OTEL_TRACES_SAMPLER_ARG from IConfiguration. - Refactor `TracerProviderSdk` to resolve sampler settings via `IOptions<SamplerOptions>`, enabling standard options pipeline overrides and improved diagnostics. - Add tests for configuration, overrides, and error handling. - Update public API and diagnostics to reflect new options-based approach.
1 parent 6232fb2 commit 9c1c993

8 files changed

Lines changed: 744 additions & 38 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
OpenTelemetry.Trace.SamplerOptions
2+
OpenTelemetry.Trace.SamplerOptions.SamplerArg.get -> double?
3+
OpenTelemetry.Trace.SamplerOptions.SamplerArg.set -> void
4+
OpenTelemetry.Trace.SamplerOptions.SamplerType.get -> string?
5+
OpenTelemetry.Trace.SamplerOptions.SamplerType.set -> void

src/OpenTelemetry/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ Notes](../../RELEASENOTES.md).
66

77
## Unreleased
88

9+
* Introduce SamplerOptions to encapsulate trace sampler configuration, parsing
10+
OTEL_TRACES_SAMPLER and OTEL_TRACES_SAMPLER_ARG from IConfiguration.
11+
([#7192](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7192))
12+
913
## 1.15.3
1014

1115
Released 2026-Apr-21

src/OpenTelemetry/Internal/Builder/ProviderBuilderServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public static IServiceCollection AddOpenTelemetryTracerProviderBuilderServices(t
5151
#pragma warning disable CS8604 // Possible null reference argument.
5252
services.TryAddSingleton<TracerProviderBuilderSdk>();
5353
services.RegisterOptionsFactory(configuration => new BatchExportActivityProcessorOptions(configuration));
54+
services.RegisterOptionsFactory(configuration => new SamplerOptions(configuration));
5455
services.RegisterOptionsFactory(
5556
(sp, configuration, name) => new ActivityExportProcessorOptions(
5657
sp.GetRequiredService<IOptionsMonitor<BatchExportActivityProcessorOptions>>().Get(name)));
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using System.Globalization;
5+
using Microsoft.Extensions.Configuration;
6+
7+
namespace OpenTelemetry.Trace;
8+
9+
/// <summary>
10+
/// Options for configuring the trace sampler.
11+
/// OTEL_TRACES_SAMPLER and OTEL_TRACES_SAMPLER_ARG environment variables
12+
/// are parsed during object construction.
13+
/// </summary>
14+
public sealed class SamplerOptions
15+
{
16+
internal const string TracesSamplerConfigKey = "OTEL_TRACES_SAMPLER";
17+
internal const string TracesSamplerArgConfigKey = "OTEL_TRACES_SAMPLER_ARG";
18+
19+
// Unlike some other options classes, we don't have a public parameterless constructor.
20+
// The internal-only constructor is the right long-term design for any new options class using
21+
// DelegatingOptionsFactory. Consumers never instantiate SamplerOptions directly, so there's no
22+
// need for a public constructor. The public constructor on BatchExportActivityProcessorOptions
23+
// pre-dates this pattern and is a historical artefact, not a template to follow for new work.
24+
25+
private double? samplerArg;
26+
27+
internal SamplerOptions(IConfiguration configuration)
28+
{
29+
if (configuration.TryGetStringValue(TracesSamplerConfigKey, out var samplerType))
30+
{
31+
this.SamplerType = samplerType;
32+
}
33+
34+
if (configuration.TryGetStringValue(TracesSamplerArgConfigKey, out var samplerArgStr))
35+
{
36+
this.SamplerArgRaw = samplerArgStr;
37+
38+
if (double.TryParse(
39+
samplerArgStr,
40+
NumberStyles.Float | NumberStyles.AllowThousands,
41+
CultureInfo.InvariantCulture,
42+
out var parsedArg))
43+
{
44+
// Bypass the public setter so that SamplerArgRaw is not cleared.
45+
this.samplerArg = parsedArg;
46+
}
47+
48+
// If unparsable, samplerArg stays null; SamplerArgRaw carries the bad string
49+
// for ReadTraceIdRatio to log when called for ratio-based samplers. This matches
50+
// the behavior prior to introducing this options type.
51+
}
52+
}
53+
54+
/// <summary>
55+
/// Gets or sets the sampler type.
56+
/// </summary>
57+
public string? SamplerType { get; set; }
58+
59+
/// <summary>
60+
/// Gets or sets the sampler argument.
61+
/// </summary>
62+
public double? SamplerArg
63+
{
64+
get => this.samplerArg;
65+
set
66+
{
67+
this.samplerArg = value;
68+
this.SamplerArgRaw = null; // no original config string when set programmatically
69+
}
70+
}
71+
72+
/// <summary>
73+
/// Gets the original configuration string so it can be logged.
74+
/// </summary>
75+
internal string? SamplerArgRaw { get; private set; }
76+
}

src/OpenTelemetry/Trace/TracerProviderSdk.cs

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,15 @@
66
using System.Runtime.CompilerServices;
77
using System.Text;
88
using System.Text.RegularExpressions;
9-
using Microsoft.Extensions.Configuration;
109
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.Options;
1111
using OpenTelemetry.Internal;
1212
using OpenTelemetry.Resources;
1313

1414
namespace OpenTelemetry.Trace;
1515

1616
internal sealed class TracerProviderSdk : TracerProvider
1717
{
18-
internal const string TracesSamplerConfigKey = "OTEL_TRACES_SAMPLER";
19-
internal const string TracesSamplerArgConfigKey = "OTEL_TRACES_SAMPLER_ARG";
20-
2118
internal readonly IServiceProvider ServiceProvider;
2219
internal IDisposable? OwnedServiceProvider;
2320
internal int ShutdownCount;
@@ -59,7 +56,9 @@ internal TracerProviderSdk(
5956
resourceBuilder.ServiceProvider = serviceProvider;
6057
this.Resource = resourceBuilder.Build();
6158

62-
this.Sampler = GetSampler(serviceProvider!.GetRequiredService<IConfiguration>(), state.Sampler);
59+
this.Sampler = GetSampler(
60+
serviceProvider!.GetRequiredService<IOptions<SamplerOptions>>().Value,
61+
state.Sampler);
6362
OpenTelemetrySdkEventSource.Log.TracerProviderSdkEvent($"Sampler added = \"{this.Sampler.GetType()}\".");
6463

6564
this.supportLegacyActivity = state.LegacyActivityOperationNames.Count > 0;
@@ -386,77 +385,78 @@ protected override void Dispose(bool disposing)
386385
base.Dispose(disposing);
387386
}
388387

389-
private static Sampler GetSampler(IConfiguration configuration, Sampler? stateSampler)
388+
private static Sampler GetSampler(SamplerOptions options, Sampler? stateSampler)
390389
{
391390
var sampler = stateSampler;
392391

393-
if (configuration.TryGetStringValue(TracesSamplerConfigKey, out var configValue))
392+
if (!string.IsNullOrWhiteSpace(options.SamplerType))
394393
{
395394
if (sampler != null)
396395
{
397396
OpenTelemetrySdkEventSource.Log.TracerProviderSdkEvent(
398-
$"Trace sampler configuration value '{configValue}' has been ignored because a value '{sampler.GetType().FullName}' was set programmatically.");
397+
$"Trace sampler configuration value '{options.SamplerType}' has been ignored because a value '{sampler.GetType().FullName}' was set programmatically.");
399398
return sampler;
400399
}
401400

402-
switch (configValue)
401+
switch (options.SamplerType)
403402
{
404-
case var _ when string.Equals(configValue, "always_on", StringComparison.OrdinalIgnoreCase):
403+
case var _ when string.Equals(options.SamplerType, "always_on", StringComparison.OrdinalIgnoreCase):
405404
sampler = AlwaysOnSampler.Instance;
406405
break;
407-
case var _ when string.Equals(configValue, "always_off", StringComparison.OrdinalIgnoreCase):
406+
case var _ when string.Equals(options.SamplerType, "always_off", StringComparison.OrdinalIgnoreCase):
408407
sampler = AlwaysOffSampler.Instance;
409408
break;
410-
case var _ when string.Equals(configValue, "traceidratio", StringComparison.OrdinalIgnoreCase):
409+
case var _ when string.Equals(options.SamplerType, "traceidratio", StringComparison.OrdinalIgnoreCase):
411410
{
412-
var traceIdRatio = ReadTraceIdRatio(configuration);
411+
var traceIdRatio = ReadTraceIdRatio(options);
413412
sampler = new TraceIdRatioBasedSampler(traceIdRatio);
414413
break;
415414
}
416415

417-
case var _ when string.Equals(configValue, "parentbased_always_on", StringComparison.OrdinalIgnoreCase):
416+
case var _ when string.Equals(options.SamplerType, "parentbased_always_on", StringComparison.OrdinalIgnoreCase):
418417
sampler = new ParentBasedSampler(AlwaysOnSampler.Instance);
419418
break;
420-
case var _ when string.Equals(configValue, "parentbased_always_off", StringComparison.OrdinalIgnoreCase):
419+
case var _ when string.Equals(options.SamplerType, "parentbased_always_off", StringComparison.OrdinalIgnoreCase):
421420
sampler = new ParentBasedSampler(AlwaysOffSampler.Instance);
422421
break;
423-
case var _ when string.Equals(configValue, "parentbased_traceidratio", StringComparison.OrdinalIgnoreCase):
424-
{
425-
var traceIdRatio = ReadTraceIdRatio(configuration);
426-
sampler = new ParentBasedSampler(new TraceIdRatioBasedSampler(traceIdRatio));
427-
break;
428-
}
429-
422+
case var _ when string.Equals(options.SamplerType, "parentbased_traceidratio", StringComparison.OrdinalIgnoreCase):
423+
sampler = new ParentBasedSampler(new TraceIdRatioBasedSampler(ReadTraceIdRatio(options)));
424+
break;
430425
default:
431-
OpenTelemetrySdkEventSource.Log.TracesSamplerConfigInvalid(configValue);
426+
OpenTelemetrySdkEventSource.Log.TracesSamplerConfigInvalid(options.SamplerType!);
432427
break;
433428
}
434429

435430
if (sampler != null)
436431
{
437432
OpenTelemetrySdkEventSource.Log.TracerProviderSdkEvent($"Trace sampler set to '{sampler.GetType().FullName}' from configuration.");
438433
}
434+
435+
return sampler ?? new ParentBasedSampler(new AlwaysOnSampler());
439436
}
440437

441438
return sampler ?? new ParentBasedSampler(AlwaysOnSampler.Instance);
442439
}
443440

444-
private static double ReadTraceIdRatio(IConfiguration configuration)
441+
private static double ReadTraceIdRatio(SamplerOptions options)
445442
{
446-
if (configuration.TryGetStringValue(TracesSamplerArgConfigKey, out var configValue) &&
447-
double.TryParse(configValue, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var traceIdRatio) &&
448-
!double.IsNaN(traceIdRatio) &&
449-
!double.IsInfinity(traceIdRatio) &&
450-
traceIdRatio >= 0.0 &&
451-
traceIdRatio <= 1.0)
443+
var samplerArg = options.SamplerArg;
444+
if (samplerArg.HasValue
445+
&& !double.IsNaN(samplerArg.Value)
446+
&& !double.IsInfinity(samplerArg.Value)
447+
&& samplerArg.Value >= 0.0
448+
&& samplerArg.Value <= 1.0)
452449
{
453-
return traceIdRatio;
454-
}
455-
else
456-
{
457-
OpenTelemetrySdkEventSource.Log.TracesSamplerArgConfigInvalid(configValue ?? string.Empty);
450+
return samplerArg.Value;
458451
}
459452

453+
// No valid ratio. Log the original config string for diagnostic fidelity and fall back to 1.0.
454+
OpenTelemetrySdkEventSource.Log.TracesSamplerArgConfigInvalid(
455+
options.SamplerArgRaw
456+
?? (samplerArg.HasValue
457+
? samplerArg.Value.ToString(CultureInfo.InvariantCulture)
458+
: string.Empty));
459+
460460
return 1.0;
461461
}
462462

test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryServicesExtensionsTests.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,63 @@ public async Task AddOpenTelemetry_WithTracing_HostConfigurationHonoredTest()
168168
host.Dispose();
169169
}
170170

171+
[Fact]
172+
public void AddOpenTelemetry_WithTracing_SamplerResolvedFromHostConfigurationTest()
173+
{
174+
using var clearSamplerEnv = EnvironmentVariableScope.Create(SamplerOptions.TracesSamplerConfigKey, null);
175+
using var clearSamplerArgEnv = EnvironmentVariableScope.Create(SamplerOptions.TracesSamplerArgConfigKey, null);
176+
177+
var builder = new HostBuilder()
178+
.ConfigureAppConfiguration(configBuilder =>
179+
{
180+
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
181+
{
182+
[SamplerOptions.TracesSamplerConfigKey] = "traceidratio",
183+
[SamplerOptions.TracesSamplerArgConfigKey] = "0.25",
184+
});
185+
})
186+
.ConfigureServices(services =>
187+
{
188+
services.AddOpenTelemetry().WithTracing();
189+
});
190+
191+
using var host = builder.Build();
192+
193+
var tracerProvider = host.Services.GetRequiredService<TracerProvider>() as TracerProviderSdk;
194+
195+
Assert.NotNull(tracerProvider);
196+
Assert.Equal("TraceIdRatioBasedSampler{0.250000}", tracerProvider.Sampler.Description);
197+
}
198+
199+
[Fact]
200+
public void AddOpenTelemetry_WithTracing_ConfigureSamplerOptionsOverridesHostConfigurationTest()
201+
{
202+
using var clearSamplerEnv = EnvironmentVariableScope.Create(SamplerOptions.TracesSamplerConfigKey, null);
203+
using var clearSamplerArgEnv = EnvironmentVariableScope.Create(SamplerOptions.TracesSamplerArgConfigKey, null);
204+
205+
var builder = new HostBuilder()
206+
.ConfigureAppConfiguration(configBuilder =>
207+
{
208+
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
209+
{
210+
[SamplerOptions.TracesSamplerConfigKey] = "traceidratio",
211+
[SamplerOptions.TracesSamplerArgConfigKey] = "0.1",
212+
});
213+
})
214+
.ConfigureServices(services =>
215+
{
216+
services.AddOpenTelemetry().WithTracing();
217+
services.Configure<SamplerOptions>(o => o.SamplerArg = 0.9);
218+
});
219+
220+
using var host = builder.Build();
221+
222+
var tracerProvider = host.Services.GetRequiredService<TracerProvider>() as TracerProviderSdk;
223+
224+
Assert.NotNull(tracerProvider);
225+
Assert.Equal("TraceIdRatioBasedSampler{0.900000}", tracerProvider.Sampler.Description);
226+
}
227+
171228
[Fact]
172229
public void AddOpenTelemetry_WithTracing_NestedResolutionUsingConfigureTest()
173230
{

0 commit comments

Comments
 (0)