-
Notifications
You must be signed in to change notification settings - Fork 166
Add SpanSamplingRule for Single Span Ingestion
#3283
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
99768e3
7ba0b8e
a7e6cea
e17b72b
9f0fecb
5cbc4ff
46676b1
3e55d92
87a1ab1
65852ec
0a15375
2b3efca
dac2e3e
3126164
feabcf8
9e64387
6e89ebf
fd809c4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; } | ||
|
|
||
| /// <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); | ||
| } | ||
| } | ||
| 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) | ||
| { | ||
| } | ||
| } | ||
| } |
| 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; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not quite sure how
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 I'll try that out and see how that works with the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Split these up in 300ce78 |
||
| } | ||
|
|
||
| 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("\\*", ".*") + "$"; | ||
|
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); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe worth using the non-backtracking regex if available?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 I had to add a
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason for
floatoverdouble?There was a problem hiding this comment.
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
floatbecause the RFC called it afloat