Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/OpenTelemetry/.publicApi/Stable/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
OpenTelemetry.Trace.AlwaysRecordSampler
OpenTelemetry.Trace.AlwaysRecordSampler.AlwaysRecordSampler(OpenTelemetry.Trace.Sampler! rootSampler) -> void
override OpenTelemetry.Trace.AlwaysRecordSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult
3 changes: 3 additions & 0 deletions src/OpenTelemetry/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Notes](../../RELEASENOTES.md).

## Unreleased

* Added `AlwaysRecordSampler`.
([#7695](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7695))

## 1.18.0

Released 2026-Aug-21
Expand Down
51 changes: 51 additions & 0 deletions src/OpenTelemetry/Trace/Sampler/AlwaysRecordSampler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// 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 OpenTelemetry.Internal;

namespace OpenTelemetry.Trace;

/// <summary>
/// Sampler implementation which wraps another <see cref="Sampler"/> and replaces any
/// <see cref="SamplingDecision.Drop"/> decision made by that sampler with
/// <see cref="SamplingDecision.RecordOnly"/>, leaving every other decision unchanged.
/// This ensures that all spans are recorded, without changing the sampling rate.
/// </summary>
/// <remarks>
/// 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%
Comment thread
martincostello marked this conversation as resolved.
Resolved
/// sampling.
/// </remarks>
public sealed class AlwaysRecordSampler : Sampler
{
private readonly Sampler rootSampler;

/// <summary>
/// Initializes a new instance of the <see cref="AlwaysRecordSampler"/> class.
/// </summary>
/// <param name="rootSampler">The <see cref="Sampler"/> whose sampling decisions should always be recorded.</param>
public AlwaysRecordSampler(Sampler rootSampler)
{
Guard.ThrowIfNull(rootSampler);

this.rootSampler = rootSampler;
#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1
this.Description = "AlwaysRecordSampler{" + rootSampler.Description + "}";
#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1
}

/// <inheritdoc/>
public override SamplingResult ShouldSample(in SamplingParameters samplingParameters)
{
var result = this.rootSampler.ShouldSample(samplingParameters);

return result.Decision == SamplingDecision.Drop
Comment thread
martincostello marked this conversation as resolved.
Resolved
? new SamplingResult(SamplingDecision.RecordOnly, result.AttributesOrNull, result.TraceStateString)
: result;
}
}
47 changes: 47 additions & 0 deletions test/OpenTelemetry.Tests/Trace/AlwaysRecordSamplerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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;

namespace OpenTelemetry.Trace.Tests;

public class AlwaysRecordSamplerTests
{
[Fact]
public void ConstructorThrowsOnNullRootSampler()
=> Assert.Throws<ArgumentNullException>(() => new AlwaysRecordSampler(null!));

[Fact]
public void DescriptionIncludesRootSamplerDescription()
{
var sampler = new AlwaysRecordSampler(new TestSampler());

Assert.Equal("AlwaysRecordSampler{TestSampler}", sampler.Description);
}

[Theory]
[InlineData(SamplingDecision.Drop, SamplingDecision.RecordOnly)]
[InlineData(SamplingDecision.RecordAndSample, SamplingDecision.RecordAndSample)]
[InlineData(SamplingDecision.RecordOnly, SamplingDecision.RecordOnly)]
public void ShouldSampleReplacesDropWithRecordOnly(SamplingDecision rootDecision, SamplingDecision expectedDecision)
{
var attributes = new List<KeyValuePair<string, object>> { new("key", "value") };
var rootResult = new SamplingResult(rootDecision, attributes, "traceState");
var testSampler = new TestSampler { SamplingAction = _ => rootResult };
var sampler = new AlwaysRecordSampler(testSampler);

var samplingParameters = new SamplingParameters(
default, default, "name", ActivityKind.Client, [], []);

var actualResult = sampler.ShouldSample(samplingParameters);

Assert.Equal(expectedDecision, actualResult.Decision);
Assert.Equal(rootResult.Attributes, actualResult.Attributes);
Assert.Equal(rootResult.TraceStateString, actualResult.TraceStateString);
}
}