Add Probability Sampler for Activity (#702)

* Merging changes in OpenTelemetrySdk

* Renames from span to activity
This commit is contained in:
Paulo Janotti 2020-06-02 15:03:29 -07:00 committed by GitHub
parent 0867c66a0f
commit 296e0ff3a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 545 additions and 32 deletions

View File

@ -77,9 +77,11 @@ namespace Samples
}
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);

View File

@ -13,7 +13,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
using System.Collections.Generic;
using System.Diagnostics;
namespace OpenTelemetry.Trace
@ -31,18 +30,11 @@ namespace OpenTelemetry.Trace
/// <summary>
/// Checks whether activity needs to be created and tracked.
/// </summary>
/// <param name="parentContext">Parent activity context. Typically taken from the wire.</param>
/// <param name="traceId">Trace ID of a activity to be created.</param>
/// <param name="spanId">Span ID of a activity to be created.</param>
/// <param name="name"> 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 <see cref="Activity"/> representing incoming http request
/// has a name of url path and then being updated with route name when routing complete.
/// <param name="samplingParameters">
/// The <see cref="ActivitySamplingParameters"/> used by the <see cref="ActivitySampler"/>
/// to decide if the <see cref="Activity"/> to be created is going to be sampled or not.
/// </param>
/// <param name="activityKind">The kind of the Activity.</param>
/// <param name="tags">Initial set of Tags for the Activity being constructed.</param>
/// <param name="links">Links associated with the activity.</param>
/// <returns>Sampling decision on whether activity needs to be sampled or not.</returns>
public abstract SamplingResult ShouldSample(in ActivityContext parentContext, in ActivityTraceId traceId, in ActivitySpanId spanId, string name, ActivityKind activityKind, IEnumerable<KeyValuePair<string, string>> tags, IEnumerable<ActivityLink> links);
public abstract SamplingResult ShouldSample(in ActivitySamplingParameters samplingParameters);
}
}

View File

@ -0,0 +1,85 @@
// <copyright file="ActivitySamplingParameters.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
using System.Collections.Generic;
using System.Diagnostics;
namespace OpenTelemetry.Trace
{
/// <summary>
/// Sampling parameters passed to an <see cref="ActivitySampler"/> for it to make a sampling decision.
/// </summary>
public readonly struct ActivitySamplingParameters
{
/// <summary>
/// Initializes a new instance of the <see cref="ActivitySamplingParameters"/> struct.
/// </summary>
/// <param name="parentContext">Parent activity context. Typically taken from the wire.</param>
/// <param name="traceId">Trace ID of a activity to be created.</param>
/// <param name="name">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 <see cref="Activity"/> representing incoming http request
/// has a name of url path and then being updated with route name when routing complete.
/// </param>
/// <param name="kind">The kind of the Activity to be created.</param>
/// <param name="tags">Initial set of Tags for the Activity being constructed.</param>
/// <param name="links">Links associated with the activity.</param>
public ActivitySamplingParameters(
ActivityContext parentContext,
ActivityTraceId traceId,
string name,
ActivityKind kind,
IEnumerable<KeyValuePair<string, string>> tags = null, // TODO: Empty
IEnumerable<ActivityLink> links = null)
{
this.ParentContext = parentContext;
this.TraceId = traceId;
this.Name = name;
this.Kind = kind;
this.Tags = tags;
this.Links = links;
}
/// <summary>
/// Gets the parent activity context.
/// </summary>
public ActivityContext ParentContext { get; }
/// <summary>
/// Gets the trace ID of parent activity or a new generated one for root span/activity.
/// </summary>
public ActivityTraceId TraceId { get; }
/// <summary>
/// Gets the name to be given to the span/activity.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the kind of span/activity to be created.
/// </summary>
public ActivityKind Kind { get; }
/// <summary>
/// Gets the tags to be associated to the span/activity to be created.
/// </summary>
public IEnumerable<KeyValuePair<string, string>> Tags { get; }
/// <summary>
/// Gets the links to be added to the activity to be created.
/// </summary>
public IEnumerable<ActivityLink> Links { get; }
}
}

View File

@ -84,14 +84,8 @@ namespace OpenTelemetry.Trace.Configuration
// This prevents Activity from being created at all.
GetRequestedDataUsingContext = (ref ActivityCreationOptions<ActivityContext> 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 @@ namespace OpenTelemetry.Trace.Configuration
return listener;
}
internal static void BuildSamplingParameters(
in ActivityCreationOptions<ActivityContext> 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);
}
}
}

View File

@ -13,8 +13,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
using System.Collections.Generic;
using System.Diagnostics;
namespace OpenTelemetry.Trace.Samplers
{
@ -27,7 +25,7 @@ namespace OpenTelemetry.Trace.Samplers
public override string Description { get; } = nameof(AlwaysOffActivitySampler);
/// <inheritdoc />
public override SamplingResult ShouldSample(in ActivityContext parentContext, in ActivityTraceId traceId, in ActivitySpanId spanId, string name, ActivityKind activityKind, IEnumerable<KeyValuePair<string, string>> tags, IEnumerable<ActivityLink> links)
public override SamplingResult ShouldSample(in ActivitySamplingParameters samplingParameters)
{
return new SamplingResult(false);
}

View File

@ -13,8 +13,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
using System.Collections.Generic;
using System.Diagnostics;
namespace OpenTelemetry.Trace.Samplers
{
@ -28,7 +26,7 @@ namespace OpenTelemetry.Trace.Samplers
public override string Description { get; } = nameof(AlwaysOnActivitySampler);
/// <inheritdoc />
public override SamplingResult ShouldSample(in ActivityContext parentContext, in ActivityTraceId traceId, in ActivitySpanId spanId, string name, ActivityKind activityKind, IEnumerable<KeyValuePair<string, string>> tags, IEnumerable<ActivityLink> links)
public override SamplingResult ShouldSample(in ActivitySamplingParameters samplingParameters)
{
return new SamplingResult(true);
}

View File

@ -0,0 +1,118 @@
// <copyright file="ProbabilityActivitySampler.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
using System;
using System.Diagnostics;
using System.Globalization;
namespace OpenTelemetry.Trace.Samplers
{
/// <summary>
/// Sampler implementation which will take a sample if parent Activity or any linked Activity is sampled.
/// Otherwise, samples traces according to the specified probability.
/// </summary>
public sealed class ProbabilityActivitySampler : ActivitySampler
{
private readonly long idUpperBound;
private readonly double probability;
/// <summary>
/// Initializes a new instance of the <see cref="ProbabilityActivitySampler"/> class.
/// </summary>
/// <param name="probability">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.
/// </param>
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);
}
}
/// <inheritdoc />
public override string Description { get; }
/// <inheritdoc />
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<byte> 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<byte> 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;
}
}
}

View File

@ -0,0 +1,86 @@
// <copyright file="ActivityListenerSdkTest.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
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<ActivityContext> 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);
}
}
}
}

View File

@ -43,14 +43,13 @@ namespace OpenTelemetry.Trace.Samplers.Test
Assert.True(
new AlwaysOnActivitySampler()
.ShouldSample(
.ShouldSample(new ActivitySamplingParameters(
parentContext,
traceId,
spanId,
"Another name",
ActivityKindServer,
null,
new List<ActivityLink>() { link }).IsSampled);
new List<ActivityLink> { link })).IsSampled);
}
[Fact]
@ -71,14 +70,13 @@ namespace OpenTelemetry.Trace.Samplers.Test
Assert.False(
new AlwaysOffActivitySampler()
.ShouldSample(
.ShouldSample(new ActivitySamplingParameters(
parentContext,
traceId,
spanId,
"Another name",
ActivityKindServer,
null,
new List<ActivityLink>() { link }).IsSampled);
new List<ActivityLink> { link })).IsSampled);
}
[Fact]

View File

@ -0,0 +1,212 @@
// <copyright file="ProbabilityActivitySamplerTest.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
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<ArgumentOutOfRangeException>(() => new ProbabilityActivitySampler(1.01));
}
[Fact]
public void ProbabilitySampler_OutOfRangeLowProbability()
{
Assert.Throws<ArgumentOutOfRangeException>(() => 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<ActivityLink>() { sampledLink }, 1.0);
var alwaysSample = new ProbabilityActivitySampler(1.0);
AssertSamplerSamplesWithProbability(
alwaysSample, this.notSampledActivityContext, new List<ActivityLink>() { sampledLink }, 1.0);
var fiftyPercentSample = new ProbabilityActivitySampler(0.5);
AssertSamplerSamplesWithProbability(
fiftyPercentSample, this.notSampledActivityContext, new List<ActivityLink>() { sampledLink }, 1.0);
var twentyPercentSample = new ProbabilityActivitySampler(0.2);
AssertSamplerSamplesWithProbability(
twentyPercentSample, this.notSampledActivityContext, new List<ActivityLink>() { sampledLink }, 1.0);
var twoThirdsSample = new ProbabilityActivitySampler(2.0 / 3.0);
AssertSamplerSamplesWithProbability(
twoThirdsSample, this.notSampledActivityContext, new List<ActivityLink>() { 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<ActivityLink> 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);
}
}
}