diff --git a/docs/file-based-configuration.md b/docs/file-based-configuration.md index c5ee3fd538..4400e23945 100644 --- a/docs/file-based-configuration.md +++ b/docs/file-based-configuration.md @@ -56,7 +56,7 @@ tracer_provider: processors: # Batch processor for OTLP HTTP - batch: - # Configure delay interval (in milliseconds) between two consecutive exports. + # Configure delay interval (in milliseconds) between two consecutive exports. # Value must be non-negative. # If omitted or null, 5000 is used. schedule_delay: 5000 @@ -110,6 +110,36 @@ tracer_provider: - simple: exporter: console: + + # Configure the sampler. If omitted, parent based sampler with a root of always_on is used. + sampler: + # Configure sampler to be parent_based. + parent_based: + # Configure root sampler. + # If omitted or null, always_on is used. + root: + # Configure sampler to be always_on. + always_on: + # Configure remote_parent_sampled sampler. + # If omitted or null, always_on is used. + remote_parent_sampled: + # Configure sampler to be always_on. + always_on: + # Configure remote_parent_not_sampled sampler. + # If omitted or null, always_off is used. + remote_parent_not_sampled: + # Configure sampler to be always_off. + always_off: + # Configure local_parent_sampled sampler. + # If omitted or null, always_on is used. + local_parent_sampled: + # Configure sampler to be always_on. + always_on: + # Configure local_parent_not_sampled sampler. + # If omitted or null, always_off is used. + local_parent_not_sampled: + # Configure sampler to be always_off. + always_off: ``` ### Resource Configuration diff --git a/src/OpenTelemetry.AutoInstrumentation/Configurations/EnvironmentConfigurationTracerHelper.cs b/src/OpenTelemetry.AutoInstrumentation/Configurations/EnvironmentConfigurationTracerHelper.cs index 48fc7b6441..312780201f 100644 --- a/src/OpenTelemetry.AutoInstrumentation/Configurations/EnvironmentConfigurationTracerHelper.cs +++ b/src/OpenTelemetry.AutoInstrumentation/Configurations/EnvironmentConfigurationTracerHelper.cs @@ -62,11 +62,17 @@ public static TracerProviderBuilder UseEnvironmentVariables( builder.AddOpenTracingShimSource(); } - builder + builder = builder // Exporters can cause dependency loads. // Should be called later if dependency listeners are already setup. - .SetExporter(settings, pluginManager) - .AddSource(settings.ActivitySources.ToArray()); + .SetExporter(settings, pluginManager); + + if (settings.Sampler != null) + { + builder = builder.SetSampler(settings.Sampler); + } + + builder = builder.AddSource(settings.ActivitySources.ToArray()); foreach (var legacySource in settings.AdditionalLegacySources) { diff --git a/src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/ParentBasedSamplerConfig.cs b/src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/ParentBasedSamplerConfig.cs new file mode 100644 index 0000000000..9fb72ca84b --- /dev/null +++ b/src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/ParentBasedSamplerConfig.cs @@ -0,0 +1,26 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.AutoInstrumentation.Configurations.FileBasedConfiguration.Parser; +using Vendors.YamlDotNet.Serialization; + +namespace OpenTelemetry.AutoInstrumentation.Configurations.FileBasedConfiguration; + +[EmptyObjectOnEmptyYaml] +internal class ParentBasedSamplerConfig +{ + [YamlMember(Alias = "root")] + public SamplerConfig? Root { get; set; } + + [YamlMember(Alias = "remote_parent_sampled")] + public SamplerConfig? RemoteParentSampled { get; set; } + + [YamlMember(Alias = "remote_parent_not_sampled")] + public SamplerConfig? RemoteParentNotSampled { get; set; } + + [YamlMember(Alias = "local_parent_sampled")] + public SamplerConfig? LocalParentSampled { get; set; } + + [YamlMember(Alias = "local_parent_not_sampled")] + public SamplerConfig? LocalParentNotSampled { get; set; } +} diff --git a/src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/SamplerConfig.cs b/src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/SamplerConfig.cs new file mode 100644 index 0000000000..61f44e0a91 --- /dev/null +++ b/src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/SamplerConfig.cs @@ -0,0 +1,22 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.AutoInstrumentation.Configurations.FileBasedConfiguration.Parser; +using Vendors.YamlDotNet.Serialization; + +namespace OpenTelemetry.AutoInstrumentation.Configurations.FileBasedConfiguration; + +internal class SamplerConfig +{ + [YamlMember(Alias = "always_on")] + public object? AlwaysOn { get; set; } + + [YamlMember(Alias = "always_off")] + public object? AlwaysOff { get; set; } + + [YamlMember(Alias = "trace_id_ratio")] + public TraceIdRatioSamplerConfig? TraceIdRatio { get; set; } + + [YamlMember(Alias = "parent_based")] + public ParentBasedSamplerConfig? ParentBased { get; set; } +} diff --git a/src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/SamplerFactory.cs b/src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/SamplerFactory.cs new file mode 100644 index 0000000000..0b488a1dbd --- /dev/null +++ b/src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/SamplerFactory.cs @@ -0,0 +1,163 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.AutoInstrumentation.Logging; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.AutoInstrumentation.Configurations.FileBasedConfiguration; + +internal static class SamplerFactory +{ + private static readonly IOtelLogger Logger = OtelLogging.GetLogger(); + + public static Sampler? CreateSampler(SamplerConfig? samplerConfig, bool failFast) + { + try + { + return CreateSamplerInternal(samplerConfig, failFast, "tracer_provider.sampler"); + } + catch (Exception ex) when (!failFast) + { + Logger.Error(ex, "Failed to create sampler from file-based configuration."); + return null; + } + } + + private static Sampler? CreateSamplerInternal(SamplerConfig? samplerConfig, bool failFast, string path) + { + if (samplerConfig == null) + { + return null; + } + + var configuredSamplers = new Dictionary(); + + if (samplerConfig.AlwaysOn != null) + { + configuredSamplers.Add("always_on", new AlwaysOnSampler()); + } + + if (samplerConfig.AlwaysOff != null) + { + configuredSamplers.Add("always_off", new AlwaysOffSampler()); + } + + if (samplerConfig.TraceIdRatio != null) + { + configuredSamplers.Add("trace_id_ratio", CreateTraceIdRatioSampler(samplerConfig.TraceIdRatio, failFast, path + ".trace_id_ratio")); + } + + if (samplerConfig.ParentBased != null) + { + configuredSamplers.Add("parent_based", CreateParentBasedSampler(samplerConfig.ParentBased, failFast, path + ".parent_based")); + } + + if (configuredSamplers.Count == 0) + { + var message = $"Sampler configuration '{path}' does not specify a sampler type."; + Logger.Warning(message); + + if (failFast) + { + throw new InvalidOperationException(message); + } + + return null; + } + + if (configuredSamplers.Count > 1) + { + var configuredNames = string.Join(", ", configuredSamplers.Keys); + var message = $"Sampler configuration '{path}' specifies multiple sampler types ({configuredNames}). Only one sampler can be configured."; + Logger.Error(message); + + if (failFast) + { + throw new InvalidOperationException(message); + } + + return null; + } + + var configuredSampler = configuredSamplers.Values.First(); + if (configuredSampler == null) + { + var message = $"Sampler configuration '{path}' is invalid."; + Logger.Error(message); + + if (failFast) + { + throw new InvalidOperationException(message); + } + } + + return configuredSampler; + } + + private static Sampler? CreateTraceIdRatioSampler(TraceIdRatioSamplerConfig config, bool failFast, string path) + { + if (!config.Ratio.HasValue) + { + var message = $"Sampler configuration '{path}' must define the 'ratio' property."; + Logger.Error(message); + + if (failFast) + { + throw new InvalidOperationException(message); + } + + return null; + } + + var ratio = config.Ratio.Value; + if (ratio is < 0 or > 1) + { + var message = $"Sampler configuration '{path}' ratio must be between 0 and 1 inclusive."; + Logger.Error(message); + + if (failFast) + { + throw new InvalidOperationException(message); + } + + return null; + } + + return new TraceIdRatioBasedSampler(ratio); + } + + private static Sampler CreateParentBasedSampler(ParentBasedSamplerConfig config, bool failFast, string path) + { + var rootSampler = GetSamplerOrDefault(config.Root, new AlwaysOnSampler(), failFast, path + ".root", "always_on"); + var remoteParentSampled = GetSamplerOrDefault(config.RemoteParentSampled, new AlwaysOnSampler(), failFast, path + ".remote_parent_sampled", "always_on"); + var remoteParentNotSampled = GetSamplerOrDefault(config.RemoteParentNotSampled, new AlwaysOffSampler(), failFast, path + ".remote_parent_not_sampled", "always_off"); + var localParentSampled = GetSamplerOrDefault(config.LocalParentSampled, new AlwaysOnSampler(), failFast, path + ".local_parent_sampled", "always_on"); + var localParentNotSampled = GetSamplerOrDefault(config.LocalParentNotSampled, new AlwaysOffSampler(), failFast, path + ".local_parent_not_sampled", "always_off"); + + return new ParentBasedSampler(rootSampler, remoteParentSampled, remoteParentNotSampled, localParentSampled, localParentNotSampled); + } + + private static Sampler GetSamplerOrDefault(SamplerConfig? samplerConfig, Sampler defaultSampler, bool failFast, string path, string defaultSamplerName) + { + if (samplerConfig == null) + { + return defaultSampler; + } + + var sampler = CreateSamplerInternal(samplerConfig, failFast, path); + if (sampler != null) + { + return sampler; + } + + var message = $"Sampler configuration '{path}' is invalid. Falling back to default '{defaultSamplerName}' sampler."; + Logger.Warning(message); + + if (failFast) + { + throw new InvalidOperationException(message); + } + + return defaultSampler; + } +} diff --git a/src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/TraceIdRatioSamplerConfig.cs b/src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/TraceIdRatioSamplerConfig.cs new file mode 100644 index 0000000000..521f8d8e46 --- /dev/null +++ b/src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/TraceIdRatioSamplerConfig.cs @@ -0,0 +1,12 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Vendors.YamlDotNet.Serialization; + +namespace OpenTelemetry.AutoInstrumentation.Configurations.FileBasedConfiguration; + +internal class TraceIdRatioSamplerConfig +{ + [YamlMember(Alias = "ratio")] + public double? Ratio { get; set; } +} diff --git a/src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/TracerProviderConfiguration.cs b/src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/TracerProviderConfiguration.cs index 1d52312968..f67811fc44 100644 --- a/src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/TracerProviderConfiguration.cs +++ b/src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/TracerProviderConfiguration.cs @@ -9,4 +9,7 @@ internal class TracerProviderConfiguration { [YamlMember(Alias = "processors")] public List Processors { get; set; } = new(); + + [YamlMember(Alias = "sampler")] + public SamplerConfig? Sampler { get; set; } } diff --git a/src/OpenTelemetry.AutoInstrumentation/Configurations/TracerSettings.cs b/src/OpenTelemetry.AutoInstrumentation/Configurations/TracerSettings.cs index bb3d6b01bf..52b894feb4 100644 --- a/src/OpenTelemetry.AutoInstrumentation/Configurations/TracerSettings.cs +++ b/src/OpenTelemetry.AutoInstrumentation/Configurations/TracerSettings.cs @@ -4,6 +4,7 @@ using OpenTelemetry.AutoInstrumentation.Configurations.FileBasedConfiguration; using OpenTelemetry.AutoInstrumentation.Configurations.Otlp; using OpenTelemetry.AutoInstrumentation.Logging; +using OpenTelemetry.Trace; namespace OpenTelemetry.AutoInstrumentation.Configurations; @@ -63,6 +64,11 @@ internal class TracerSettings : Settings /// public IReadOnlyList? Processors { get; private set; } = null; + /// + /// Gets the sampler configured via file-based configuration. + /// + public Sampler? Sampler { get; private set; } + protected override void OnLoadEnvVar(Configuration configuration) { TracesExporters = ParseTracesExporter(configuration); @@ -112,6 +118,8 @@ protected override void OnLoadFile(YamlConfiguration configuration) } Processors = processors; + + Sampler = SamplerFactory.CreateSampler(configuration.TracerProvider?.Sampler, configuration.FailFast); } private static List ParseTracesExporter(Configuration configuration) diff --git a/test/OpenTelemetry.AutoInstrumentation.Tests/Configurations/FileBased/FilebasedTracesSettingsTests.cs b/test/OpenTelemetry.AutoInstrumentation.Tests/Configurations/FileBased/FilebasedTracesSettingsTests.cs index d3adca198d..7010f67e5f 100644 --- a/test/OpenTelemetry.AutoInstrumentation.Tests/Configurations/FileBased/FilebasedTracesSettingsTests.cs +++ b/test/OpenTelemetry.AutoInstrumentation.Tests/Configurations/FileBased/FilebasedTracesSettingsTests.cs @@ -1,8 +1,10 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics; using OpenTelemetry.AutoInstrumentation.Configurations; using OpenTelemetry.AutoInstrumentation.Configurations.FileBasedConfiguration; +using OpenTelemetry.Trace; using Xunit; namespace OpenTelemetry.AutoInstrumentation.Tests.Configurations.FileBased; @@ -349,6 +351,55 @@ public void LoadMethod_SkipWrongExporterConfiguration(SkipConfigurationTestCase Assert.Empty(settings.TracesExporters); } + [Fact] + public void LoadFile_ConfiguresParentBasedSampler() + { + var samplerConfig = new SamplerConfig + { + ParentBased = new ParentBasedSamplerConfig + { + Root = new SamplerConfig { AlwaysOn = new object() }, + RemoteParentSampled = new SamplerConfig { AlwaysOn = new object() }, + RemoteParentNotSampled = new SamplerConfig { AlwaysOff = new object() }, + LocalParentSampled = new SamplerConfig { AlwaysOn = new object() }, + LocalParentNotSampled = new SamplerConfig { AlwaysOff = new object() } + } + }; + + var conf = new YamlConfiguration + { + TracerProvider = new TracerProviderConfiguration + { + Sampler = samplerConfig + } + }; + + var settings = new TracerSettings(); + + settings.LoadFile(conf); + + var sampler = Assert.IsType(settings.Sampler); + + Assert.Equal(SamplingDecision.RecordAndSample, sampler.ShouldSample(CreateSamplingParameters(default)).Decision); + + var remoteSampledParent = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded, traceState: null, isRemote: true); + Assert.Equal(SamplingDecision.RecordAndSample, sampler.ShouldSample(CreateSamplingParameters(remoteSampledParent)).Decision); + + var remoteNotSampledParent = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None, traceState: null, isRemote: true); + Assert.Equal(SamplingDecision.Drop, sampler.ShouldSample(CreateSamplingParameters(remoteNotSampledParent)).Decision); + + var localSampledParent = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded, traceState: null, isRemote: false); + Assert.Equal(SamplingDecision.RecordAndSample, sampler.ShouldSample(CreateSamplingParameters(localSampledParent)).Decision); + + var localNotSampledParent = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None, traceState: null, isRemote: false); + Assert.Equal(SamplingDecision.Drop, sampler.ShouldSample(CreateSamplingParameters(localNotSampledParent)).Decision); + } + + private static SamplingParameters CreateSamplingParameters(ActivityContext parentContext) + { + return new SamplingParameters(parentContext, ActivityTraceId.CreateRandom(), "span", ActivityKind.Internal, default(TagList), new ActivityLink[] { }); + } + public class SkipConfigurationTestCase { internal SkipConfigurationTestCase(YamlConfiguration configuration) diff --git a/test/OpenTelemetry.AutoInstrumentation.Tests/Configurations/FileBased/Files/TestTracesFile.yaml b/test/OpenTelemetry.AutoInstrumentation.Tests/Configurations/FileBased/Files/TestTracesFile.yaml index 514737454e..0caaaf6cdc 100644 --- a/test/OpenTelemetry.AutoInstrumentation.Tests/Configurations/FileBased/Files/TestTracesFile.yaml +++ b/test/OpenTelemetry.AutoInstrumentation.Tests/Configurations/FileBased/Files/TestTracesFile.yaml @@ -9,7 +9,7 @@ tracer_provider: otlp_http: endpoint: http://localhost:4318/v1/traces timeout: 10000 - headers: + headers: - name: header1234 value: "1234" - batch: @@ -24,3 +24,15 @@ tracer_provider: - simple: exporter: console: + sampler: + parent_based: + root: + always_on: + remote_parent_sampled: + always_on: + remote_parent_not_sampled: + always_off: + local_parent_sampled: + always_on: + local_parent_not_sampled: + always_off: diff --git a/test/OpenTelemetry.AutoInstrumentation.Tests/Configurations/FileBased/Parser/ParserTracesTests.cs b/test/OpenTelemetry.AutoInstrumentation.Tests/Configurations/FileBased/Parser/ParserTracesTests.cs index 48eeb664e3..c2e32addb5 100644 --- a/test/OpenTelemetry.AutoInstrumentation.Tests/Configurations/FileBased/Parser/ParserTracesTests.cs +++ b/test/OpenTelemetry.AutoInstrumentation.Tests/Configurations/FileBased/Parser/ParserTracesTests.cs @@ -57,6 +57,20 @@ public void Parse_FullConfigYaml_ShouldPopulateModelCorrectly() Assert.NotNull(simpleProcessor); Assert.NotNull(simpleProcessor.Exporter); Assert.NotNull(simpleProcessor.Exporter.Console); + + var sampler = config.TracerProvider.Sampler; + Assert.NotNull(sampler); + Assert.NotNull(sampler.ParentBased); + Assert.NotNull(sampler.ParentBased.Root); + Assert.NotNull(sampler.ParentBased.Root.AlwaysOn); + Assert.NotNull(sampler.ParentBased.RemoteParentSampled); + Assert.NotNull(sampler.ParentBased.RemoteParentSampled.AlwaysOn); + Assert.NotNull(sampler.ParentBased.RemoteParentNotSampled); + Assert.NotNull(sampler.ParentBased.RemoteParentNotSampled.AlwaysOff); + Assert.NotNull(sampler.ParentBased.LocalParentSampled); + Assert.NotNull(sampler.ParentBased.LocalParentSampled.AlwaysOn); + Assert.NotNull(sampler.ParentBased.LocalParentNotSampled); + Assert.NotNull(sampler.ParentBased.LocalParentNotSampled.AlwaysOff); } [Fact]