Skip to content

Commit 994813e

Browse files
[AzureMonitorExporter] Add OTEL_TRACES_SAMPLER support to Exporter and AspNetCore distro (Azure#52720)
* Initial support for env variable * Add logs to event source. * Distro changes. * changelog patch * copilot feedback * Event source fix * Fix intermittent test failures * left out test fix
1 parent 3daa168 commit 994813e

File tree

11 files changed

+640
-13
lines changed

11 files changed

+640
-13
lines changed

sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@
44

55
### Features Added
66

7+
* Added support for configuring sampling via OpenTelemetry environment
8+
variables:
9+
* `OTEL_TRACES_SAMPLER` (supported values: `microsoft.rate_limited`,
10+
`microsoft.fixed_percentage`).
11+
* `OTEL_TRACES_SAMPLER_ARG` (rate limit in traces/sec for
12+
`microsoft.rate_limited`, sampling ratio 0.0 - 1.0 for
13+
`microsoft.fixed_percentage`).
14+
([#52720](https://github.com/Azure/azure-sdk-for-net/pull/52720))
15+
716
### Breaking Changes
817

918
### Bugs Fixed
@@ -260,7 +269,6 @@
260269
- Update OpenTelemetry dependencies
261270
([41398](https://github.com/Azure/azure-sdk-for-net/pull/41398))
262271
- OpenTelemetry 1.7.0
263-
- OpenTelemetry.Extensions.Hosting 1.7.0
264272
- NEW: OpenTelemetry.Instrumentation.AspNetCore 1.7.0
265273
- NEW: OpenTelemetry.Instrumentation.Http 1.7.0
266274

sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/AzureMonitorAspNetCoreEventSource.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,5 +127,11 @@ public void ErrorInitializingPartOfSdkVersion(string typeName, System.Exception
127127

128128
[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)]
129129
public void ErrorInitializingPartOfSdkVersion(string typeName, string exceptionMessage) => WriteEvent(13, typeName, exceptionMessage);
130+
131+
[Event(14, Message = "Invalid sampler type '{0}'. Supported values: microsoft.rate_limited, microsoft.fixed_percentage", Level = EventLevel.Warning)]
132+
public void InvalidSamplerType(string samplerType) => WriteEvent(14, samplerType);
133+
134+
[Event(15, Message = "Invalid sampler argument '{1}' for sampler '{0}'. Ignoring.", Level = EventLevel.Warning)]
135+
public void InvalidSamplerArgument(string samplerType, string samplerArg) => WriteEvent(15, samplerType, samplerArg);
130136
}
131137
}

sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/DefaultAzureMonitorOptions.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Azure.Monitor.OpenTelemetry.Exporter.Internals.Platform;
55
using Microsoft.Extensions.Configuration;
66
using Microsoft.Extensions.Options;
7+
using System.Globalization;
78

89
namespace Azure.Monitor.OpenTelemetry.AspNetCore
910
{
@@ -35,6 +36,11 @@ public void Configure(AzureMonitorOptions options)
3536
{
3637
options.ConnectionString = connectionStringFromIConfig;
3738
}
39+
40+
// Sampler configuration via IConfiguration
41+
var samplerFromConfig = _configuration[EnvironmentVariableConstants.OTEL_TRACES_SAMPLER];
42+
var samplerArgFromConfig = _configuration[EnvironmentVariableConstants.OTEL_TRACES_SAMPLER_ARG];
43+
ConfigureSamplingOptions(samplerFromConfig, samplerArgFromConfig, options);
3844
}
3945

4046
// Environment Variable should take precedence.
@@ -43,6 +49,69 @@ public void Configure(AzureMonitorOptions options)
4349
{
4450
options.ConnectionString = connectionStringFromEnvVar;
4551
}
52+
53+
// Explicit environment variables for sampler should override IConfiguration.
54+
var samplerTypeEnv = Environment.GetEnvironmentVariable(EnvironmentVariableConstants.OTEL_TRACES_SAMPLER);
55+
var samplerArgEnv = Environment.GetEnvironmentVariable(EnvironmentVariableConstants.OTEL_TRACES_SAMPLER_ARG);
56+
ConfigureSamplingOptions(samplerTypeEnv, samplerArgEnv, options);
57+
}
58+
catch (Exception ex)
59+
{
60+
AzureMonitorAspNetCoreEventSource.Log.ConfigureFailed(ex);
61+
}
62+
}
63+
64+
private static void ConfigureSamplingOptions(string? samplerType, string? samplerArg, AzureMonitorOptions options)
65+
{
66+
if (string.IsNullOrEmpty(samplerType) || string.IsNullOrEmpty(samplerArg))
67+
{
68+
return;
69+
}
70+
71+
try
72+
{
73+
var samplerKey = samplerType!.Trim().ToLowerInvariant();
74+
string samplerArgValue = samplerArg ?? string.Empty;
75+
switch (samplerKey)
76+
{
77+
case "microsoft.rate_limited":
78+
if (double.TryParse(samplerArg, NumberStyles.Float, CultureInfo.InvariantCulture, out var tracesPerSecond))
79+
{
80+
if (tracesPerSecond >= 0)
81+
{
82+
options.TracesPerSecond = tracesPerSecond;
83+
}
84+
else
85+
{
86+
AzureMonitorAspNetCoreEventSource.Log.InvalidSamplerArgument(samplerKey, samplerArgValue);
87+
}
88+
}
89+
else
90+
{
91+
AzureMonitorAspNetCoreEventSource.Log.InvalidSamplerArgument(samplerKey, samplerArgValue);
92+
}
93+
break;
94+
case "microsoft.fixed_percentage":
95+
if (double.TryParse(samplerArg, NumberStyles.Float, CultureInfo.InvariantCulture, out var ratio))
96+
{
97+
if (ratio >= 0.0 && ratio <= 1.0)
98+
{
99+
options.SamplingRatio = (float)ratio;
100+
}
101+
else
102+
{
103+
AzureMonitorAspNetCoreEventSource.Log.InvalidSamplerArgument(samplerKey, samplerArgValue);
104+
}
105+
}
106+
else
107+
{
108+
AzureMonitorAspNetCoreEventSource.Log.InvalidSamplerArgument(samplerKey, samplerArgValue);
109+
}
110+
break;
111+
default:
112+
AzureMonitorAspNetCoreEventSource.Log.InvalidSamplerType(samplerType ?? string.Empty);
113+
break;
114+
}
46115
}
47116
catch (Exception ex)
48117
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using Microsoft.Extensions.Configuration;
7+
using Xunit;
8+
9+
namespace Azure.Monitor.OpenTelemetry.AspNetCore.Tests
10+
{
11+
[CollectionDefinition("AspNetCoreSamplerEnvVarTests", DisableParallelization = true)]
12+
public class DefaultAzureMonitorOptionsSamplerTests
13+
{
14+
[Fact]
15+
public void Configure_Sampler_From_IConfiguration_FixedPercentage()
16+
{
17+
var configValues = new List<KeyValuePair<string, string?>>
18+
{
19+
new("OTEL_TRACES_SAMPLER", "microsoft.fixed_percentage"),
20+
new("OTEL_TRACES_SAMPLER_ARG", "0.40"),
21+
};
22+
var configuration = new ConfigurationBuilder().AddInMemoryCollection(configValues).Build();
23+
var configurator = new DefaultAzureMonitorOptions(configuration);
24+
var options = new AzureMonitorOptions();
25+
26+
configurator.Configure(options);
27+
28+
Assert.Equal(0.40f, options.SamplingRatio);
29+
Assert.Null(options.TracesPerSecond);
30+
}
31+
32+
[Fact]
33+
public void Configure_Sampler_From_IConfiguration_RateLimited()
34+
{
35+
var configValues = new List<KeyValuePair<string, string?>>
36+
{
37+
new("OTEL_TRACES_SAMPLER", "microsoft.rate_limited"),
38+
new("OTEL_TRACES_SAMPLER_ARG", "15"),
39+
};
40+
var configuration = new ConfigurationBuilder().AddInMemoryCollection(configValues).Build();
41+
var configurator = new DefaultAzureMonitorOptions(configuration);
42+
var options = new AzureMonitorOptions();
43+
44+
configurator.Configure(options);
45+
46+
Assert.Equal(15d, options.TracesPerSecond);
47+
Assert.Equal(1.0f, options.SamplingRatio); // default unchanged
48+
}
49+
50+
[Fact]
51+
public void Configure_Sampler_InvalidArgs_Ignored()
52+
{
53+
// invalid percentage > 1
54+
var configValues = new List<KeyValuePair<string, string?>>
55+
{
56+
new("OTEL_TRACES_SAMPLER", "microsoft.fixed_percentage"),
57+
new("OTEL_TRACES_SAMPLER_ARG", "1.5"),
58+
};
59+
var configuration = new ConfigurationBuilder().AddInMemoryCollection(configValues).Build();
60+
var configurator = new DefaultAzureMonitorOptions(configuration);
61+
var options = new AzureMonitorOptions();
62+
configurator.Configure(options);
63+
Assert.Equal(1.0f, options.SamplingRatio); // default
64+
Assert.Null(options.TracesPerSecond);
65+
66+
// invalid negative rate
67+
configValues = new List<KeyValuePair<string, string?>>
68+
{
69+
new("OTEL_TRACES_SAMPLER", "microsoft.rate_limited"),
70+
new("OTEL_TRACES_SAMPLER_ARG", "-2"),
71+
};
72+
configuration = new ConfigurationBuilder().AddInMemoryCollection(configValues).Build();
73+
configurator = new DefaultAzureMonitorOptions(configuration);
74+
options = new AzureMonitorOptions();
75+
configurator.Configure(options);
76+
Assert.Null(options.TracesPerSecond);
77+
Assert.Equal(1.0f, options.SamplingRatio);
78+
}
79+
80+
[Fact]
81+
public void Configure_Sampler_EnvironmentVariable_Overrides_IConfiguration()
82+
{
83+
var configValues = new List<KeyValuePair<string, string?>>
84+
{
85+
new("OTEL_TRACES_SAMPLER", "microsoft.fixed_percentage"),
86+
new("OTEL_TRACES_SAMPLER_ARG", "0.20"),
87+
};
88+
var configuration = new ConfigurationBuilder().AddInMemoryCollection(configValues).Build();
89+
var configurator = new DefaultAzureMonitorOptions(configuration);
90+
var options = new AzureMonitorOptions();
91+
92+
string? prevSampler = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER");
93+
string? prevSamplerArg = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG");
94+
try
95+
{
96+
Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", "microsoft.rate_limited");
97+
Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", "11");
98+
99+
configurator.Configure(options);
100+
101+
Assert.Equal(0.20f, options.SamplingRatio); // from config
102+
Assert.Equal(11d, options.TracesPerSecond); // from env
103+
}
104+
finally
105+
{
106+
Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", prevSampler);
107+
Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", prevSamplerArg);
108+
}
109+
}
110+
111+
[Fact]
112+
public void Configure_Sampler_EnvironmentVariable_Only_FixedPercentage()
113+
{
114+
string? prevSampler = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER");
115+
string? prevSamplerArg = Environment.GetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG");
116+
try
117+
{
118+
Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", "microsoft.fixed_percentage");
119+
Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", "0.55");
120+
121+
var configurator = new DefaultAzureMonitorOptions();
122+
var options = new AzureMonitorOptions();
123+
configurator.Configure(options);
124+
125+
Assert.Equal(0.55f, options.SamplingRatio);
126+
Assert.Null(options.TracesPerSecond);
127+
}
128+
finally
129+
{
130+
Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER", prevSampler);
131+
Environment.SetEnvironmentVariable("OTEL_TRACES_SAMPLER_ARG", prevSamplerArg);
132+
}
133+
}
134+
}
135+
}

sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@
66

77
* Added mapping for `enduser.pseudo.id` attribute to `user_Id`
88

9+
* Added support for configuring sampling via OpenTelemetry environment
10+
variables:
11+
* `OTEL_TRACES_SAMPLER` (supported values: `microsoft.rate_limited`,
12+
`microsoft.fixed_percentage`).
13+
* `OTEL_TRACES_SAMPLER_ARG` (rate limit in traces/sec for
14+
`microsoft.rate_limited`, sampling ratio 0.0 - 1.0 for
15+
`microsoft.fixed_percentage`). This now applies to both
16+
`UseAzureMonitorExporter` and the direct
17+
`Sdk.CreateTracerProviderBuilder().AddAzureMonitorTraceExporter(...)` path.
18+
([#52720](https://github.com/Azure/azure-sdk-for-net/pull/52720))
19+
920
### Breaking Changes
1021

1122
### Bugs Fixed

sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/AzureMonitorExporterExtensions.cs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Azure.Monitor.OpenTelemetry.Exporter.Internals;
1010
using Azure.Monitor.OpenTelemetry.Exporter.Internals.Diagnostics;
1111
using Microsoft.Extensions.DependencyInjection;
12+
using Microsoft.Extensions.DependencyInjection.Extensions;
1213
using Microsoft.Extensions.Options;
1314
using OpenTelemetry;
1415
using OpenTelemetry.Logs;
@@ -47,12 +48,19 @@ public static TracerProviderBuilder AddAzureMonitorTraceExporter(
4748

4849
var finalOptionsName = name ?? Options.DefaultName;
4950

50-
if (name != null && configure != null)
51+
// Ensure our default options configurator (which reads IConfiguration + environment variables)
52+
// is registered exactly once so that OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARG work for this path.
53+
builder.ConfigureServices(services =>
5154
{
52-
// If we are using named options we register the
53-
// configuration delegate into options pipeline.
54-
builder.ConfigureServices(services => services.Configure(finalOptionsName, configure));
55-
}
55+
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<AzureMonitorExporterOptions>, DefaultAzureMonitorExporterOptions>());
56+
57+
if (name != null && configure != null)
58+
{
59+
// If we are using named options we register the configuration delegate into the options pipeline
60+
// After the DefaultAzureMonitorExporterOptions so explicit code configuration can override env/config values.
61+
services.Configure(finalOptionsName, configure);
62+
}
63+
});
5664

5765
var deferredBuilder = builder as IDeferredTracerProviderBuilder;
5866
if (deferredBuilder == null)
@@ -66,11 +74,7 @@ public static TracerProviderBuilder AddAzureMonitorTraceExporter(
6674
var exporterOptions = sp.GetRequiredService<IOptionsMonitor<AzureMonitorExporterOptions>>().Get(finalOptionsName);
6775
if (name == null && configure != null)
6876
{
69-
// If we are NOT using named options, we execute the
70-
// configuration delegate inline. The reason for this is
71-
// AzureMonitorExporterOptions is shared by all signals. Without a
72-
// name, delegates for all signals will mix together. See:
73-
// https://github.com/open-telemetry/opentelemetry-dotnet/issues/4043
77+
// For unnamed options execute configuration delegate inline so it overrides env/config values.
7478
configure(exporterOptions);
7579
}
7680

@@ -85,8 +89,6 @@ public static TracerProviderBuilder AddAzureMonitorTraceExporter(
8589

8690
if (credential != null)
8791
{
88-
// Credential can be set by either AzureMonitorExporterOptions or Extension Method Parameter.
89-
// Options should take precedence.
9092
exporterOptions.Credential ??= credential;
9193
}
9294

0 commit comments

Comments
 (0)