Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
60 changes: 60 additions & 0 deletions tracer/src/Datadog.Trace/Sampling/ISpanSamplingRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// <copyright file="ISpanSamplingRule.cs" company="Datadog">
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
// </copyright>

namespace Datadog.Trace.Sampling
{
/// <summary>
/// Defines the contract for a sampling rule for single span ingestion.
/// </summary>
internal interface ISpanSamplingRule
{
/// <summary>
/// Gets the probability of keeping a span for this rule.
/// </summary>
/// <value><see langword="float"/> in range <c>[0.0, 1.0]</c>.
/// <para><c>0.0</c> is drop all.</para>
/// <para><c>1.0</c> is accept all.</para>
/// </value>
float SamplingRate { get; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason for float over double?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question
I chose a float because the RFC called it a float


/// <summary>
/// Gets the maximum number of allowed spans per second for this rule.
/// </summary>
/// <value>
/// A nullable <see langword="float"/>.
/// <para><see langword="null"/> is the default and indicates unlimited.</para>
/// </value>
float? MaxPerSecond { get; }

/// <summary>
/// Gets the cached <see cref="SamplingRate"/> string for tagging.
/// </summary>
string SamplingRateString { get; }

/// <summary>
/// Gets the cached <see cref="MaxPerSecond"/> string for tagging.
/// </summary>
string MaxPerSecondString { get; }

/// <summary>
/// Gets the cached <see cref="SamplingMechanism"/> string for tagging.
/// </summary>
string SamplingMechanismString { get; }

/// <summary>
/// Checks whether or not the <paramref name="span"/> matches the glob patterns defined by this rule.
/// </summary>
/// <param name="span">The <see cref="Span"/> to check.</param>
/// <returns><see langword="true"/> when the span matches the rule's glob patterns; otherwise, <see langword="false"/>.</returns>
bool IsMatch(Span span);

/// <summary>
/// Checks whether or not the <paramref name="span"/> should be sampled after matching on a rule.
/// </summary>
/// <param name="span">The <see cref="Span"/> to check.</param>
/// <returns><see langword="true"/> when the span should be kept; otherwise, <see langword="false"/>.</returns>
bool ShouldSample(Span span);
}
}
40 changes: 40 additions & 0 deletions tracer/src/Datadog.Trace/Sampling/SpanRateLimiter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// <copyright file="SpanRateLimiter.cs" company="Datadog">
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
// </copyright>

using Datadog.Trace.Logging;

namespace Datadog.Trace.Sampling
{
/// <summary>
/// Represents a <see cref="RateLimiter" /> specifically for single span ingestion.
/// See <see cref="TracerRateLimiter" /> for trace-based rate limiting.
/// </summary>
internal class SpanRateLimiter : RateLimiter
{
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor<SpanRateLimiter>();

/// <summary>
/// Initializes a new instance of the <see cref="SpanRateLimiter"/> class.
/// </summary>
/// <param name="maxTracesPerInterval">
/// The maximum number of <em>spans</em> allowed per interval.
/// A negative value indicates no limit.
/// A <see langword="null"/> value will be converted to a default value.
/// </param>
public SpanRateLimiter(int? maxTracesPerInterval)
: base(maxTracesPerInterval)
{
}

public override void OnDisallowed(Span span, int count, int intervalMs, int maxTracesPerInterval)
{
Log.Debug<ulong, int, int>("Dropping span id {SpanId} with count of {Count} for last {Interval}ms.", span.SpanId, count, intervalMs);
}

public override void OnFinally(Span span)
{
}
}
}
173 changes: 173 additions & 0 deletions tracer/src/Datadog.Trace/Sampling/SpanSamplingRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// <copyright file="SpanSamplingRule.cs" company="Datadog">
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
// </copyright>

using System;
using System.Collections.Generic;
using System.Linq;
#if NETCOREAPP3_1_OR_GREATER
using Datadog.Trace.Vendors.IndieSystem.Text.RegularExpressions;
#else
using System.Text.RegularExpressions;
#endif
using Datadog.Trace.Logging;
using Datadog.Trace.Util;
using Datadog.Trace.Vendors.Newtonsoft.Json;

namespace Datadog.Trace.Sampling
{
/// <summary>
/// Represents a sampling rules for single span ingestion.
/// </summary>
internal class SpanSamplingRule : ISpanSamplingRule
{
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor<SpanSamplingRule>();
private static readonly TimeSpan RegexTimeout = TimeSpan.FromSeconds(1);

// TODO consider moving toward this https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/Text/SimpleRegex.cs
private readonly Regex _serviceNameRegex;
private readonly Regex _operationNameRegex;

private readonly IRateLimiter _limiter;

/// <summary>
/// Initializes a new instance of the <see cref="SpanSamplingRule"/> class.
/// </summary>
/// <param name="serviceNameGlob">The glob pattern for the <see cref="Span.ServiceName"/>.</param>
/// <param name="operationNameGlob">The glob pattern for the <see cref="Span.OperationName"/>.</param>
/// <param name="samplingRate">The proportion of spans that are kept. <c>1.0</c> indicates keep all where <c>0.0</c> would be drop all.</param>
/// <param name="maxPerSecond">The maximum number of spans allowed to be kept per second - <see langword="null"/> indicates that there is no limit</param>
public SpanSamplingRule(string serviceNameGlob, string operationNameGlob, float samplingRate = 1.0f, float? maxPerSecond = null)
{
if (string.IsNullOrWhiteSpace(serviceNameGlob))
{
throw new ArgumentException("Value cannot be null or whitespace.", nameof(serviceNameGlob));
}

if (string.IsNullOrWhiteSpace(operationNameGlob))
{
throw new ArgumentException("Value cannot be null or whitespace.", nameof(operationNameGlob));
}

SamplingRate = samplingRate;
MaxPerSecond = maxPerSecond;

_serviceNameRegex = ConvertGlobToRegex(serviceNameGlob);
_operationNameRegex = ConvertGlobToRegex(operationNameGlob);

// null/absent for MaxPerSecond indicates unlimited, which is a negative value in the limiter
_limiter = MaxPerSecond is null ? new SpanRateLimiter(-1) : new SpanRateLimiter((int?)MaxPerSecond);

// cache strings for tagging to reduce allocations
SamplingRateString = SamplingRate.ToString();
MaxPerSecondString = MaxPerSecond?.ToString();
SamplingMechanismString = SamplingMechanism.SpanSamplingRule.ToString();
}

/// <inheritdoc/>
public float SamplingRate { get; } = 1.0f;

/// <inheritdoc/>
public float? MaxPerSecond { get; } = null;

/// <inheritdoc/>
public string SamplingRateString { get; }

/// <inheritdoc/>
public string MaxPerSecondString { get; }

/// <inheritdoc/>
public string SamplingMechanismString { get; }

/// <summary>
/// Creates <see cref="SpanSamplingRule"/>s from the supplied JSON <paramref name="configuration"/>.
/// </summary>
/// <param name="configuration">The JSON-serialized configuration.</param>
/// <returns><see cref="IEnumerable{T}"/> of <see cref="SpanSamplingRule"/>.</returns>
public static IEnumerable<SpanSamplingRule> BuildFromConfigurationString(string configuration)
{
if (string.IsNullOrWhiteSpace(configuration))
{
return Enumerable.Empty<SpanSamplingRule>();
}

try
{
var rules = JsonConvert.DeserializeObject<List<SpanSamplingRuleConfig>>(configuration);
return rules?.Select(
rule => new SpanSamplingRule(
rule.ServiceNameGlob,
rule.OperationNameGlob,
rule.SampleRate,
rule.MaxPerSecond))
?? Enumerable.Empty<SpanSamplingRule>();
}
catch (Exception e)
{
Log.Error(e, "Unable to parse the span sampling rule.");
return Enumerable.Empty<SpanSamplingRule>();
}
}

/// <inheritdoc/>
public bool IsMatch(Span span)
{
if (span is null)
{
return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not quite sure how ShouldKeep will be used, but I'm a little concerned that the decision for "didn't match" and "chose not to sample" both result in a false return value. What do you think about making the return type bool?? Or perhaps we require the caller to call IsMatch first so we always know that the rule applies?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of breaking it up to try and call the IsMatch first and then go and making the sampling decision with some separate function

I'll try that out and see how that works with the SpanSampler implementation

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split these up in 300ce78
To be a IsMatch that returns a bool for whether the glob patterns match and ShouldSample that returns bool for whether the sampling rate/limit are good

}

return _serviceNameRegex.Match(span.ServiceName).Success && _operationNameRegex.Match(span.OperationName).Success;
}

/// <inheritdoc/>
public bool ShouldSample(Span span)
{
if (span is null)
{
return false;
}

var sampleKeep = SamplingHelpers.SampleByRate(span.SpanId, SamplingRate);

if (!sampleKeep)
{
return false;
}

var limitKeep = _limiter.Allowed(span);

return limitKeep;
}

private static Regex ConvertGlobToRegex(string glob)
{
// TODO default glob (maybe null/empty/whitespace) should be *
var regexPattern = "^" + Regex.Escape(glob).Replace("\\?", ".").Replace("\\*", ".*") + "$";
Comment thread
bouwkast marked this conversation as resolved.
#if NETCOREAPP3_1_OR_GREATER
var regex = new Regex(regexPattern, RegexOptions.Compiled | RegexOptions.NonBacktracking, RegexTimeout);
#else
var regex = new Regex(regexPattern, RegexOptions.Compiled, RegexTimeout);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe worth using the non-backtracking regex if available?

@bouwkast bouwkast Oct 3, 2022

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

I had to add a #if NETCOREAPP3_1_OR_GREATER to it to get that option 2b186e9
Is there a way to do non-backtracking regex in previous frameworks?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately not, but probably still worth it 🙂

#endif

return regex;
}

[Serializable]
internal class SpanSamplingRuleConfig
{
[JsonProperty(PropertyName = "service")]
public string ServiceNameGlob { get; set; } = "*";

[JsonProperty(PropertyName = "name")]
public string OperationNameGlob { get; set; } = "*";

[JsonProperty(PropertyName = "sample_rate")]
public float SampleRate { get; set; } = 1.0f; // default to accept all

[JsonProperty(PropertyName = "max_per_second")]
public float? MaxPerSecond { get; set; } = null;
}
}
}
Loading