From 296e0ff3a74ce86541af799410e230927ed63d05 Mon Sep 17 00:00:00 2001 From: Paulo Janotti Date: Tue, 2 Jun 2020 15:03:29 -0700 Subject: [PATCH] Add Probability Sampler for Activity (#702) * Merging changes in OpenTelemetrySdk * Renames from span to activity --- .../InstrumentationWithActivitySource.cs | 2 + src/OpenTelemetry/Trace/ActivitySampler.cs | 16 +- .../Trace/ActivitySamplingParameters.cs | 85 +++++++ .../Trace/Configuration/OpenTelemetrySdk.cs | 40 +++- .../Samplers/AlwaysOffActivitySampler.cs | 4 +- .../Trace/Samplers/AlwaysOnActivitySampler.cs | 4 +- .../Samplers/ProbabilityActivitySampler.cs | 118 ++++++++++ .../Trace/ActivityListenerSdkTest.cs | 86 +++++++ .../Trace/Samplers/ActivitySamplersTest.cs | 10 +- .../ProbabilityActivitySamplerTest.cs | 212 ++++++++++++++++++ 10 files changed, 545 insertions(+), 32 deletions(-) create mode 100644 src/OpenTelemetry/Trace/ActivitySamplingParameters.cs create mode 100644 src/OpenTelemetry/Trace/Samplers/ProbabilityActivitySampler.cs create mode 100644 test/OpenTelemetry.Tests/Implementation/Trace/ActivityListenerSdkTest.cs create mode 100644 test/OpenTelemetry.Tests/Implementation/Trace/Samplers/ProbabilityActivitySamplerTest.cs diff --git a/samples/Exporters/Console/InstrumentationWithActivitySource.cs b/samples/Exporters/Console/InstrumentationWithActivitySource.cs index af123e49cbc..0580b4f6ec7 100644 --- a/samples/Exporters/Console/InstrumentationWithActivitySource.cs +++ b/samples/Exporters/Console/InstrumentationWithActivitySource.cs @@ -77,9 +77,11 @@ public void Start(string url) } string requestContent; + using (var childSpan = source.StartActivity("ReadStream", ActivityKind.Consumer)) using (var reader = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding)) { requestContent = reader.ReadToEnd(); + childSpan.AddEvent(new ActivityEvent("StreamReader.ReadToEnd")); } activity?.AddTag("request.content", requestContent); diff --git a/src/OpenTelemetry/Trace/ActivitySampler.cs b/src/OpenTelemetry/Trace/ActivitySampler.cs index 3f666fb6db7..bb8fc0495b1 100644 --- a/src/OpenTelemetry/Trace/ActivitySampler.cs +++ b/src/OpenTelemetry/Trace/ActivitySampler.cs @@ -13,7 +13,6 @@ // See the License for the specific language governing permissions and // limitations under the License. // -using System.Collections.Generic; using System.Diagnostics; namespace OpenTelemetry.Trace @@ -31,18 +30,11 @@ public abstract class ActivitySampler /// /// Checks whether activity needs to be created and tracked. /// - /// Parent activity context. Typically taken from the wire. - /// Trace ID of a activity to be created. - /// Span ID of a activity to be created. - /// Name (DisplayName) of the activity to be created. Note, that the name of the activity is settable. - /// So this name can be changed later and Sampler implementation should assume that. - /// Typical example of a name change is when representing incoming http request - /// has a name of url path and then being updated with route name when routing complete. + /// + /// The used by the + /// to decide if the to be created is going to be sampled or not. /// - /// The kind of the Activity. - /// Initial set of Tags for the Activity being constructed. - /// Links associated with the activity. /// Sampling decision on whether activity needs to be sampled or not. - public abstract SamplingResult ShouldSample(in ActivityContext parentContext, in ActivityTraceId traceId, in ActivitySpanId spanId, string name, ActivityKind activityKind, IEnumerable> tags, IEnumerable links); + public abstract SamplingResult ShouldSample(in ActivitySamplingParameters samplingParameters); } } diff --git a/src/OpenTelemetry/Trace/ActivitySamplingParameters.cs b/src/OpenTelemetry/Trace/ActivitySamplingParameters.cs new file mode 100644 index 00000000000..756aadca221 --- /dev/null +++ b/src/OpenTelemetry/Trace/ActivitySamplingParameters.cs @@ -0,0 +1,85 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +using System.Collections.Generic; +using System.Diagnostics; + +namespace OpenTelemetry.Trace +{ + /// + /// Sampling parameters passed to an for it to make a sampling decision. + /// + public readonly struct ActivitySamplingParameters + { + /// + /// Initializes a new instance of the struct. + /// + /// Parent activity context. Typically taken from the wire. + /// Trace ID of a activity to be created. + /// The name (DisplayName) of the activity to be created. Note, that the name of the activity is settable. + /// So this name can be changed later and Sampler implementation should assume that. + /// Typical example of a name change is when representing incoming http request + /// has a name of url path and then being updated with route name when routing complete. + /// + /// The kind of the Activity to be created. + /// Initial set of Tags for the Activity being constructed. + /// Links associated with the activity. + public ActivitySamplingParameters( + ActivityContext parentContext, + ActivityTraceId traceId, + string name, + ActivityKind kind, + IEnumerable> tags = null, // TODO: Empty + IEnumerable links = null) + { + this.ParentContext = parentContext; + this.TraceId = traceId; + this.Name = name; + this.Kind = kind; + this.Tags = tags; + this.Links = links; + } + + /// + /// Gets the parent activity context. + /// + public ActivityContext ParentContext { get; } + + /// + /// Gets the trace ID of parent activity or a new generated one for root span/activity. + /// + public ActivityTraceId TraceId { get; } + + /// + /// Gets the name to be given to the span/activity. + /// + public string Name { get; } + + /// + /// Gets the kind of span/activity to be created. + /// + public ActivityKind Kind { get; } + + /// + /// Gets the tags to be associated to the span/activity to be created. + /// + public IEnumerable> Tags { get; } + + /// + /// Gets the links to be added to the activity to be created. + /// + public IEnumerable Links { get; } + } +} diff --git a/src/OpenTelemetry/Trace/Configuration/OpenTelemetrySdk.cs b/src/OpenTelemetry/Trace/Configuration/OpenTelemetrySdk.cs index 76ade97ca02..ff6c5e64162 100644 --- a/src/OpenTelemetry/Trace/Configuration/OpenTelemetrySdk.cs +++ b/src/OpenTelemetry/Trace/Configuration/OpenTelemetrySdk.cs @@ -84,14 +84,8 @@ public static IDisposable EnableOpenTelemetry(Action confi // This prevents Activity from being created at all. GetRequestedDataUsingContext = (ref ActivityCreationOptions options) => { - var shouldSample = sampler.ShouldSample( - options.Parent, - options.Parent.TraceId, - spanId: default, // Passing default SpanId here. The actual SpanId is not known before actual Activity creation - options.Name, - options.Kind, - options.Tags, - options.Links); + BuildSamplingParameters(options, out var samplingParameters); + var shouldSample = sampler.ShouldSample(samplingParameters); if (shouldSample.IsSampled) { return ActivityDataRequest.AllDataAndRecorded; @@ -109,5 +103,35 @@ public static IDisposable EnableOpenTelemetry(Action confi return listener; } + + internal static void BuildSamplingParameters( + in ActivityCreationOptions options, out ActivitySamplingParameters samplingParameters) + { + ActivityContext parentContext = options.Parent; + if (parentContext == default) + { + // Check if there is already a parent for the current activity. + var parentActivity = Activity.Current; + if (parentActivity != null) + { + parentContext = parentActivity.Context; + } + } + + // This is not going to be the final traceId of the Activity (if one is created), however, it is + // needed in order for the sampling to work. This differs from other OTel SDKs in which it is + // the Sampler always receives the actual traceId of a root span/activity. + ActivityTraceId traceId = parentContext.TraceId != default + ? parentContext.TraceId + : ActivityTraceId.CreateRandom(); + + samplingParameters = new ActivitySamplingParameters( + parentContext, + traceId, + options.Name, + options.Kind, + options.Tags, + options.Links); + } } } diff --git a/src/OpenTelemetry/Trace/Samplers/AlwaysOffActivitySampler.cs b/src/OpenTelemetry/Trace/Samplers/AlwaysOffActivitySampler.cs index fe2ee3d32ce..cb5c39a67e5 100644 --- a/src/OpenTelemetry/Trace/Samplers/AlwaysOffActivitySampler.cs +++ b/src/OpenTelemetry/Trace/Samplers/AlwaysOffActivitySampler.cs @@ -13,8 +13,6 @@ // See the License for the specific language governing permissions and // limitations under the License. // -using System.Collections.Generic; -using System.Diagnostics; namespace OpenTelemetry.Trace.Samplers { @@ -27,7 +25,7 @@ public sealed class AlwaysOffActivitySampler : ActivitySampler public override string Description { get; } = nameof(AlwaysOffActivitySampler); /// - public override SamplingResult ShouldSample(in ActivityContext parentContext, in ActivityTraceId traceId, in ActivitySpanId spanId, string name, ActivityKind activityKind, IEnumerable> tags, IEnumerable links) + public override SamplingResult ShouldSample(in ActivitySamplingParameters samplingParameters) { return new SamplingResult(false); } diff --git a/src/OpenTelemetry/Trace/Samplers/AlwaysOnActivitySampler.cs b/src/OpenTelemetry/Trace/Samplers/AlwaysOnActivitySampler.cs index 0130c21921d..ef167ae3afc 100644 --- a/src/OpenTelemetry/Trace/Samplers/AlwaysOnActivitySampler.cs +++ b/src/OpenTelemetry/Trace/Samplers/AlwaysOnActivitySampler.cs @@ -13,8 +13,6 @@ // See the License for the specific language governing permissions and // limitations under the License. // -using System.Collections.Generic; -using System.Diagnostics; namespace OpenTelemetry.Trace.Samplers { @@ -28,7 +26,7 @@ public sealed class AlwaysOnActivitySampler : ActivitySampler public override string Description { get; } = nameof(AlwaysOnActivitySampler); /// - public override SamplingResult ShouldSample(in ActivityContext parentContext, in ActivityTraceId traceId, in ActivitySpanId spanId, string name, ActivityKind activityKind, IEnumerable> tags, IEnumerable links) + public override SamplingResult ShouldSample(in ActivitySamplingParameters samplingParameters) { return new SamplingResult(true); } diff --git a/src/OpenTelemetry/Trace/Samplers/ProbabilityActivitySampler.cs b/src/OpenTelemetry/Trace/Samplers/ProbabilityActivitySampler.cs new file mode 100644 index 00000000000..40cab31518d --- /dev/null +++ b/src/OpenTelemetry/Trace/Samplers/ProbabilityActivitySampler.cs @@ -0,0 +1,118 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +using System; +using System.Diagnostics; +using System.Globalization; + +namespace OpenTelemetry.Trace.Samplers +{ + /// + /// Sampler implementation which will take a sample if parent Activity or any linked Activity is sampled. + /// Otherwise, samples traces according to the specified probability. + /// + public sealed class ProbabilityActivitySampler : ActivitySampler + { + private readonly long idUpperBound; + private readonly double probability; + + /// + /// Initializes a new instance of the class. + /// + /// The desired probability of sampling. This must be between 0.0 and 1.0. + /// Higher the value, higher is the probability of a given Activity to be sampled in. + /// + public ProbabilityActivitySampler(double probability) + { + if (probability < 0.0 || probability > 1.0) + { + throw new ArgumentOutOfRangeException(nameof(probability), "Probability must be in range [0.0, 1.0]"); + } + + this.probability = probability; + + // The expected description is like ProbabilityActivitySampler{0.000100} + this.Description = "ProbabilityActivitySampler{" + this.probability.ToString("F6", CultureInfo.InvariantCulture) + "}"; + + // Special case the limits, to avoid any possible issues with lack of precision across + // double/long boundaries. For probability == 0.0, we use Long.MIN_VALUE as this guarantees + // that we will never sample a trace, even in the case where the id == Long.MIN_VALUE, since + // Math.Abs(Long.MIN_VALUE) == Long.MIN_VALUE. + if (this.probability == 0.0) + { + this.idUpperBound = long.MinValue; + } + else if (this.probability == 1.0) + { + this.idUpperBound = long.MaxValue; + } + else + { + this.idUpperBound = (long)(probability * long.MaxValue); + } + } + + /// + public override string Description { get; } + + /// + public override SamplingResult ShouldSample(in ActivitySamplingParameters samplingParameters) + { + // If the parent is sampled keep the sampling decision. + var parentContext = samplingParameters.ParentContext; + if ((parentContext.TraceFlags & ActivityTraceFlags.Recorded) != 0) + { + return new SamplingResult(true); + } + + if (samplingParameters.Links != null) + { + // If any parent link is sampled keep the sampling decision. + foreach (var parentLink in samplingParameters.Links) + { + if ((parentLink.Context.TraceFlags & ActivityTraceFlags.Recorded) != 0) + { + return new SamplingResult(true); + } + } + } + + // Always sample if we are within probability range. This is true even for child activities (that + // may have had a different sampling decision made) to allow for different sampling policies, + // and dynamic increases to sampling probabilities for debugging purposes. + // Note use of '<' for comparison. This ensures that we never sample for probability == 0.0, + // while allowing for a (very) small chance of *not* sampling if the id == Long.MAX_VALUE. + // This is considered a reasonable trade-off for the simplicity/performance requirements (this + // code is executed in-line for every Activity creation). + Span traceIdBytes = stackalloc byte[16]; + samplingParameters.TraceId.CopyTo(traceIdBytes); + return Math.Abs(this.GetLowerLong(traceIdBytes)) < this.idUpperBound ? new SamplingResult(true) : new SamplingResult(false); + } + + private long GetLowerLong(ReadOnlySpan bytes) + { + long result = 0; + for (var i = 0; i < 8; i++) + { + result <<= 8; +#pragma warning disable CS0675 // Bitwise-or operator used on a sign-extended operand + result |= bytes[i] & 0xff; +#pragma warning restore CS0675 // Bitwise-or operator used on a sign-extended operand + } + + return result; + } + } +} diff --git a/test/OpenTelemetry.Tests/Implementation/Trace/ActivityListenerSdkTest.cs b/test/OpenTelemetry.Tests/Implementation/Trace/ActivityListenerSdkTest.cs new file mode 100644 index 00000000000..ea9b82e3337 --- /dev/null +++ b/test/OpenTelemetry.Tests/Implementation/Trace/ActivityListenerSdkTest.cs @@ -0,0 +1,86 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Diagnostics; +using OpenTelemetry.Trace; +using OpenTelemetry.Trace.Configuration; +using Xunit; + +namespace OpenTelemetry.Tests.Implementation.Trace +{ + public class ActivityListenerSdkTest + { + static ActivityListenerSdkTest() + { + Activity.DefaultIdFormat = ActivityIdFormat.W3C; + Activity.ForceDefaultIdFormat = true; + } + + [Fact] + public void BuildSamplingParametersHandlesCurrentActivity() + { + using var activitySource = new ActivitySource(nameof(BuildSamplingParametersHandlesCurrentActivity)); + + var latestSamplingParameters = new ActivitySamplingParameters(); + + using var listener = new ActivityListener + { + ShouldListenTo = _ => true, + GetRequestedDataUsingContext = (ref ActivityCreationOptions options) => + { + OpenTelemetrySdk.BuildSamplingParameters(options, out latestSamplingParameters); + return ActivityDataRequest.AllDataAndRecorded; + }, + }; + + ActivitySource.AddActivityListener(listener); + + using (var root = activitySource.StartActivity("root")) + { + Assert.Equal(default(ActivitySpanId), root.ParentSpanId); + + // This enforces the current behavior that the traceId passed to the sampler for the + // root span/activity is not the traceId actually used. + Assert.NotEqual(root.TraceId, latestSamplingParameters.TraceId); + } + + using (var parent = activitySource.StartActivity("parent", ActivityKind.Client)) + { + // This enforces the current behavior that the traceId passed to the sampler for the + // root span/activity is not the traceId actually used. + Assert.NotEqual(parent.TraceId, latestSamplingParameters.TraceId); + using (var child = activitySource.StartActivity("child")) + { + Assert.Equal(parent.TraceId, latestSamplingParameters.TraceId); + Assert.Equal(parent.TraceId, child.TraceId); + Assert.Equal(parent.SpanId, child.ParentSpanId); + } + } + + var customContext = new ActivityContext( + ActivityTraceId.CreateRandom(), + ActivitySpanId.CreateRandom(), + ActivityTraceFlags.None); + using (var fromCustomContext = + activitySource.StartActivity("customContext", ActivityKind.Client, customContext)) + { + Assert.Equal(customContext.TraceId, fromCustomContext.TraceId); + Assert.Equal(customContext.SpanId, fromCustomContext.ParentSpanId); + Assert.NotEqual(customContext.SpanId, fromCustomContext.SpanId); + } + } + } +} diff --git a/test/OpenTelemetry.Tests/Implementation/Trace/Samplers/ActivitySamplersTest.cs b/test/OpenTelemetry.Tests/Implementation/Trace/Samplers/ActivitySamplersTest.cs index 76ac02f8e08..36cd260294b 100644 --- a/test/OpenTelemetry.Tests/Implementation/Trace/Samplers/ActivitySamplersTest.cs +++ b/test/OpenTelemetry.Tests/Implementation/Trace/Samplers/ActivitySamplersTest.cs @@ -43,14 +43,13 @@ public void AlwaysOnSampler_AlwaysReturnTrue(ActivityTraceFlags flags) Assert.True( new AlwaysOnActivitySampler() - .ShouldSample( + .ShouldSample(new ActivitySamplingParameters( parentContext, traceId, - spanId, "Another name", ActivityKindServer, null, - new List() { link }).IsSampled); + new List { link })).IsSampled); } [Fact] @@ -71,14 +70,13 @@ public void AlwaysOffSampler_AlwaysReturnFalse(ActivityTraceFlags flags) Assert.False( new AlwaysOffActivitySampler() - .ShouldSample( + .ShouldSample(new ActivitySamplingParameters( parentContext, traceId, - spanId, "Another name", ActivityKindServer, null, - new List() { link }).IsSampled); + new List { link })).IsSampled); } [Fact] diff --git a/test/OpenTelemetry.Tests/Implementation/Trace/Samplers/ProbabilityActivitySamplerTest.cs b/test/OpenTelemetry.Tests/Implementation/Trace/Samplers/ProbabilityActivitySamplerTest.cs new file mode 100644 index 00000000000..4936962d672 --- /dev/null +++ b/test/OpenTelemetry.Tests/Implementation/Trace/Samplers/ProbabilityActivitySamplerTest.cs @@ -0,0 +1,212 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +using System; +using System.Diagnostics; +using System.Collections.Generic; +using Xunit; + +namespace OpenTelemetry.Trace.Samplers.Test +{ + public class ProbabilityActivitySamplerTest + { + private const string ActivityDisplayName = "MyActivityName"; + private const int NumSampleTries = 1000; + private static readonly ActivityKind ActivityKindServer = ActivityKind.Server; + private readonly ActivityTraceId traceId; + private readonly ActivityContext sampledActivityContext; + private readonly ActivityContext notSampledActivityContext; + private readonly ActivityLink sampledLink; + + public ProbabilityActivitySamplerTest() + { + traceId = ActivityTraceId.CreateRandom(); + var parentSpanId = ActivitySpanId.CreateRandom(); + sampledActivityContext = new ActivityContext(traceId, parentSpanId, ActivityTraceFlags.Recorded); + notSampledActivityContext = new ActivityContext(traceId, parentSpanId, ActivityTraceFlags.None); + sampledLink = new ActivityLink(this.sampledActivityContext); + } + + [Fact] + public void ProbabilitySampler_OutOfRangeHighProbability() + { + Assert.Throws(() => new ProbabilityActivitySampler(1.01)); + } + + [Fact] + public void ProbabilitySampler_OutOfRangeLowProbability() + { + Assert.Throws(() => new ProbabilityActivitySampler(-0.00001)); + } + + [Fact] + public void ProbabilitySampler_DifferentProbabilities_NotSampledParent() + { + var neverSample = new ProbabilityActivitySampler(0.0); + AssertSamplerSamplesWithProbability( + neverSample, this.notSampledActivityContext, null, 0.0); + var alwaysSample = new ProbabilityActivitySampler(1.0); + AssertSamplerSamplesWithProbability( + alwaysSample, this.notSampledActivityContext, null, 1.0); + var fiftyPercentSample = new ProbabilityActivitySampler(0.5); + AssertSamplerSamplesWithProbability( + fiftyPercentSample, this.notSampledActivityContext, null, 0.5); + var twentyPercentSample = new ProbabilityActivitySampler(0.2); + AssertSamplerSamplesWithProbability( + twentyPercentSample, this.notSampledActivityContext, null, 0.2); + var twoThirdsSample = new ProbabilityActivitySampler(2.0 / 3.0); + AssertSamplerSamplesWithProbability( + twoThirdsSample, this.notSampledActivityContext, null, 2.0 / 3.0); + } + + [Fact] + public void ProbabilitySampler_DifferentProbabilities_SampledParent() + { + var neverSample = new ProbabilityActivitySampler(0.0); + AssertSamplerSamplesWithProbability( + neverSample, this.sampledActivityContext, null, 1.0); + var alwaysSample = new ProbabilityActivitySampler(1.0); + AssertSamplerSamplesWithProbability( + alwaysSample, this.sampledActivityContext, null, 1.0); + var fiftyPercentSample = new ProbabilityActivitySampler(0.5); + AssertSamplerSamplesWithProbability( + fiftyPercentSample, this.sampledActivityContext, null, 1.0); + var twentyPercentSample = new ProbabilityActivitySampler(0.2); + AssertSamplerSamplesWithProbability( + twentyPercentSample, this.sampledActivityContext, null, 1.0); + var twoThirdsSample = new ProbabilityActivitySampler(2.0 / 3.0); + AssertSamplerSamplesWithProbability( + twoThirdsSample, this.sampledActivityContext, null, 1.0); + } + + [Fact] + public void ProbabilitySampler_DifferentProbabilities_SampledParentLink() + { + var neverSample = new ProbabilityActivitySampler(0.0); + AssertSamplerSamplesWithProbability( + neverSample, this.notSampledActivityContext, new List() { sampledLink }, 1.0); + var alwaysSample = new ProbabilityActivitySampler(1.0); + AssertSamplerSamplesWithProbability( + alwaysSample, this.notSampledActivityContext, new List() { sampledLink }, 1.0); + var fiftyPercentSample = new ProbabilityActivitySampler(0.5); + AssertSamplerSamplesWithProbability( + fiftyPercentSample, this.notSampledActivityContext, new List() { sampledLink }, 1.0); + var twentyPercentSample = new ProbabilityActivitySampler(0.2); + AssertSamplerSamplesWithProbability( + twentyPercentSample, this.notSampledActivityContext, new List() { sampledLink }, 1.0); + var twoThirdsSample = new ProbabilityActivitySampler(2.0 / 3.0); + AssertSamplerSamplesWithProbability( + twoThirdsSample, this.notSampledActivityContext, new List() { sampledLink }, 1.0); + } + + [Fact] + public void ProbabilitySampler_SampleBasedOnTraceId() + { + ActivitySampler defaultProbability = new ProbabilityActivitySampler(0.0001); + // This traceId will not be sampled by the ProbabilityActivitySampler because the first 8 bytes as long + // is not less than probability * Long.MAX_VALUE; + var notSampledtraceId = + ActivityTraceId.CreateFromBytes( + new byte[] + { + 0x8F, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + }); + Assert.False( + defaultProbability.ShouldSample(new ActivitySamplingParameters( + default, + notSampledtraceId, + ActivityDisplayName, + ActivityKindServer, + null, + null)).IsSampled); + // This traceId will be sampled by the ProbabilityActivitySampler because the first 8 bytes as long + // is less than probability * Long.MAX_VALUE; + var sampledtraceId = + ActivityTraceId.CreateFromBytes( + new byte[] + { + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + }); + Assert.True( + defaultProbability.ShouldSample(new ActivitySamplingParameters( + default, + sampledtraceId, + ActivityDisplayName, + ActivityKindServer, + null, + null)).IsSampled); + } + + [Fact] + public void ProbabilitySampler_GetDescription() + { + var expectedDescription = "ProbabilityActivitySampler{0.500000}"; + Assert.Equal(expectedDescription, new ProbabilityActivitySampler(0.5).Description); + } + + // Applies the given sampler to NumSampleTries random traceId/spanId pairs. + private static void AssertSamplerSamplesWithProbability( + ActivitySampler sampler, ActivityContext parent, List links, double probability) + { + var count = 0; // Count of spans with sampling enabled + for (var i = 0; i < NumSampleTries; i++) + { + if (sampler.ShouldSample(new ActivitySamplingParameters( + parent, + ActivityTraceId.CreateRandom(), + ActivityDisplayName, + ActivityKindServer, + null, + links)).IsSampled) + { + count++; + } + } + var proportionSampled = (double)count / NumSampleTries; + // Allow for a large amount of slop (+/- 10%) in number of sampled traces, to avoid flakiness. + Assert.True(proportionSampled < probability + 0.1 && proportionSampled > probability - 0.1); + } + } +}