diff --git a/OpenTelemetry.slnx b/OpenTelemetry.slnx index 5e0d7941f0d..5c8f5df8d68 100644 --- a/OpenTelemetry.slnx +++ b/OpenTelemetry.slnx @@ -87,6 +87,7 @@ + diff --git a/build/Common.props b/build/Common.props index 44c697e393c..ff61b5f5bd0 100644 --- a/build/Common.props +++ b/build/Common.props @@ -12,7 +12,7 @@ all low - $(NoWarn);OTEL1000;OTEL1001;OTEL1002;OTEL1004 + $(NoWarn);OTEL1000;OTEL1001;OTEL1002;OTEL1004;OTEL1005 latest-All diff --git a/docs/diagnostics/experimental-apis/OTEL1005.md b/docs/diagnostics/experimental-apis/OTEL1005.md new file mode 100644 index 00000000000..b46b7200687 --- /dev/null +++ b/docs/diagnostics/experimental-apis/OTEL1005.md @@ -0,0 +1,18 @@ +# OpenTelemetry .NET Diagnostic: OTEL1005 + +## Overview + +This is an experimental API for allowing spans to always be recorded. + +### Details + +#### AlwaysRecordSampler + +TODO: Explanation. + +**Parameters:** + +* TODO: Details +* `span` - a read/write span object for the span which is about to be ended. + +**Returns:** `TODO` diff --git a/docs/diagnostics/experimental-apis/README.md b/docs/diagnostics/experimental-apis/README.md index daa80d34b38..a87f927b6ba 100644 --- a/docs/diagnostics/experimental-apis/README.md +++ b/docs/diagnostics/experimental-apis/README.md @@ -33,6 +33,12 @@ Description: ExemplarReservoir Support Details: [OTEL1004](./OTEL1004.md) +### OTEL1005 + +Description: AlwaysRecordSampler implementation for recording all spans + +Details: [OTEL1005](./OTEL1005.md) + ## Inactive Experimental APIs which have been released stable or removed: diff --git a/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt index b9507b58b38..00d40c1e9a1 100644 --- a/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt @@ -25,3 +25,6 @@ OpenTelemetry.Metrics.MetricStreamConfiguration.ExemplarReservoirFactory.set -> [OTEL1000]static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.UseOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, System.Action? configureBuilder, System.Action? configureOptions) -> Microsoft.Extensions.Logging.ILoggingBuilder! [OTEL1001]static OpenTelemetry.Sdk.CreateLoggerProviderBuilder() -> OpenTelemetry.Logs.LoggerProviderBuilder! [OTEL1004]virtual OpenTelemetry.Metrics.FixedSizeExemplarReservoir.OnCollected() -> void +[OTEL1005]OpenTelemetry.Trace.AlwaysRecordSampler +[OTEL1005]static OpenTelemetry.Trace.AlwaysRecordSampler.Create(OpenTelemetry.Trace.Sampler! rootSampler) -> OpenTelemetry.Trace.AlwaysRecordSampler! +[OTEL1005]override OpenTelemetry.Trace.AlwaysRecordSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index c46a345cd2f..f7b4d4ea2e5 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -12,6 +12,9 @@ Notes](../../RELEASENOTES.md). * Added support for `Meter.TelemetrySchemaUrl` property. ([#6714](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6714)) +* Added `AlwaysRecordSampler`. + ([#6732](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6732)) + ## 1.14.0 Released 2025-Nov-12 diff --git a/src/OpenTelemetry/Trace/Sampler/AlwaysRecordSampler.cs b/src/OpenTelemetry/Trace/Sampler/AlwaysRecordSampler.cs new file mode 100644 index 00000000000..38192c1813a --- /dev/null +++ b/src/OpenTelemetry/Trace/Sampler/AlwaysRecordSampler.cs @@ -0,0 +1,70 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Includes work from: +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#if EXPOSE_EXPERIMENTAL_FEATURES +using System.Diagnostics.CodeAnalysis; +#endif +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Trace; + +#if EXPOSE_EXPERIMENTAL_FEATURES +/// +/// This sampler will return the sampling result of the provided rootSampler, unless the +/// sampling result contains the sampling decision , in which case, a +/// new sampling result will be returned that is functionally equivalent to the original, except that +/// it contains the sampling decision . This ensures that all +/// spans are recorded, with no change to sampling. +/// +/// The intended use case of this sampler is to provide a means of sending all spans to a +/// processor without having an impact on the sampling rate. This may be desirable if a user wishes +/// to count or otherwise measure all spans produced in a service, without incurring the cost of 100% +/// sampling. +/// +[Experimental(DiagnosticDefinitions.AlwaysRecordSamplerExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +public +#else +internal +#endif + sealed class AlwaysRecordSampler : Sampler +{ + private readonly Sampler rootSampler; + + private AlwaysRecordSampler(Sampler rootSampler) + { + this.rootSampler = rootSampler; + this.Description = "AlwaysRecordSampler{" + rootSampler.Description + "}"; + } + + /// + /// Method to create an AlwaysRecordSampler. + /// + /// rootSampler to create AlwaysRecordSampler from. + /// Created AlwaysRecordSampler. + public static AlwaysRecordSampler Create(Sampler rootSampler) + { + Guard.ThrowIfNull(rootSampler); + return new AlwaysRecordSampler(rootSampler); + } + + /// + public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) + { + SamplingResult result = this.rootSampler.ShouldSample(samplingParameters); + if (result.Decision == SamplingDecision.Drop) + { + result = WrapResultWithRecordOnlyResult(result); + } + + return result; + } + + private static SamplingResult WrapResultWithRecordOnlyResult(SamplingResult result) + { + return new SamplingResult(SamplingDecision.RecordOnly, result.Attributes, result.TraceStateString); + } +} \ No newline at end of file diff --git a/src/Shared/DiagnosticDefinitions.cs b/src/Shared/DiagnosticDefinitions.cs index f6b449352ff..84afbcea909 100644 --- a/src/Shared/DiagnosticDefinitions.cs +++ b/src/Shared/DiagnosticDefinitions.cs @@ -10,6 +10,7 @@ internal static class DiagnosticDefinitions public const string LoggerProviderExperimentalApi = "OTEL1000"; public const string LogsBridgeExperimentalApi = "OTEL1001"; public const string ExemplarReservoirExperimentalApi = "OTEL1004"; + public const string AlwaysRecordSamplerExperimentalApi = "OTEL1005"; /* Definitions which have been released stable: public const string ExemplarExperimentalApi = "OTEL1002"; diff --git a/test/OpenTelemetry.Tests/Trace/AlwaysRecordSamplerTests.cs b/test/OpenTelemetry.Tests/Trace/AlwaysRecordSamplerTests.cs new file mode 100644 index 00000000000..cda19a84bdf --- /dev/null +++ b/test/OpenTelemetry.Tests/Trace/AlwaysRecordSamplerTests.cs @@ -0,0 +1,95 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Includes work from: +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using OpenTelemetry.Tests; +using Xunit; + +namespace OpenTelemetry.Trace.Tests; + +/// +/// AlwaysRecordSamplerTest test class. +/// +public class AlwaysRecordSamplerTests +{ + /// + /// Tests Description is set properly with AlwaysRecordSampler keyword. + /// + [Fact] + public void TestGetDescription() + { + var testSampler = new TestSampler(); + var sampler = AlwaysRecordSampler.Create(testSampler); + Assert.Equal("AlwaysRecordSampler{TestSampler}", sampler.Description); + } + + /// + /// Test RECORD_AND_SAMPLE sampling decision. + /// + [Fact] + public void TestRecordAndSampleSamplingDecision() + { + ValidateShouldSample(SamplingDecision.RecordAndSample, SamplingDecision.RecordAndSample); + } + + /// + /// Test RECORD_ONLY sampling decision. + /// + [Fact] + public void TestRecordOnlySamplingDecision() + { + ValidateShouldSample(SamplingDecision.RecordOnly, SamplingDecision.RecordOnly); + } + + /// + /// Test DROP sampling decision. + /// + [Fact] + public void TestDropSamplingDecision() + { + ValidateShouldSample(SamplingDecision.Drop, SamplingDecision.RecordOnly); + } + + private static SamplingResult BuildRootSamplingResult(SamplingDecision samplingDecision) + { + ActivityTagsCollection? attributes = new ActivityTagsCollection + { + { "key", samplingDecision.GetType().Name }, + }; + string traceState = samplingDecision.GetType().Name; +#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. + return new SamplingResult(samplingDecision, attributes, traceState); +#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. + } + + private static void ValidateShouldSample( + SamplingDecision rootDecision, SamplingDecision expectedDecision) + { + SamplingResult rootResult = BuildRootSamplingResult(rootDecision); + var testSampler = new TestSampler { SamplingAction = _ => rootResult }; + var sampler = AlwaysRecordSampler.Create(testSampler); + + SamplingParameters samplingParameters = new SamplingParameters( + default, default, "name", ActivityKind.Client, new ActivityTagsCollection(), new List()); + + SamplingResult actualResult = sampler.ShouldSample(samplingParameters); + + if (rootDecision.Equals(expectedDecision)) + { + Assert.True(actualResult.Equals(rootResult)); + Assert.True(actualResult.Decision.Equals(rootDecision)); + } + else + { + Assert.False(actualResult.Equals(rootResult)); + Assert.True(actualResult.Decision.Equals(expectedDecision)); + } + + Assert.Equal(rootResult.Attributes, actualResult.Attributes); + Assert.Equal(rootDecision.GetType().Name, actualResult.TraceStateString); + } +}