Skip to content
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

[tracer] add support for match sampling rules by resource and tags #5013

Merged
merged 27 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
29b78d0
convert glob to regex if needed
lucaspimentel Dec 15, 2023
dc059c3
add new json properties
lucaspimentel Dec 18, 2023
34c45a7
add unit tests
lucaspimentel Dec 18, 2023
49929e3
match by resource name
lucaspimentel Dec 18, 2023
20fdf85
fix compiler errors in unit tests
lucaspimentel Dec 18, 2023
6b747d0
deserialize tag regexes
lucaspimentel Dec 18, 2023
e1c1989
fix compiler errors in unit tests
lucaspimentel Dec 19, 2023
dc2509b
refactor things and support matching new fields in SpanSamplingRule
lucaspimentel Dec 20, 2023
44fbaa3
rewrite without the null coalescing
lucaspimentel Jan 3, 2024
0e03620
implement matching on tags
lucaspimentel Jan 3, 2024
167df72
refactor MatchTags()
lucaspimentel Jan 5, 2024
692eae1
refactor common code into SamplingRuleHelper
lucaspimentel Jan 8, 2024
6dbd80b
fix compiler errors in unit tests
lucaspimentel Jan 8, 2024
3303557
inline variable into condition
lucaspimentel Jan 10, 2024
b9336ac
fix tests
lucaspimentel Jan 11, 2024
c8a8c74
use Dictionary for tag patterns
lucaspimentel Jan 11, 2024
4ba7cda
add tests for matching on tags
lucaspimentel Jan 11, 2024
d43c031
build tag regex list in RegexBuilder.Build()
lucaspimentel Jan 11, 2024
2200314
TEMP
lucaspimentel Jan 11, 2024
c21454d
move more common sampling code into SamplingRuleHelper
lucaspimentel Jan 12, 2024
4f4cd6b
fix unit tests by setting the span's resource name
lucaspimentel Jan 12, 2024
971e800
if a tag regex is null (not specified), it always matches
lucaspimentel Jan 12, 2024
b36675b
annotate nullability
lucaspimentel Jan 12, 2024
1128802
more refactoring
lucaspimentel Jan 12, 2024
1108504
reformat test
lucaspimentel Jan 18, 2024
c5653da
add tags to test spans
lucaspimentel Jan 18, 2024
52aa432
fix test that matches rules by tags
lucaspimentel Jan 18, 2024
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
50 changes: 26 additions & 24 deletions tracer/src/Datadog.Trace/Sampling/CustomSamplingRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

using System;
using System.Collections.Generic;
using System.Linq;
using Datadog.Trace.Logging;
using Datadog.Trace.Vendors.Newtonsoft.Json;

Expand All @@ -27,6 +26,8 @@ internal class CustomSamplingRule : ISamplingRule
// TODO consider moving toward these 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 Regex _resourceNameRegex;
private readonly List<KeyValuePair<string, Regex>> _tagRegexes;

private bool _regexTimedOut;

Expand All @@ -35,16 +36,22 @@ public CustomSamplingRule(
string ruleName,
string patternFormat,
string serviceNamePattern,
string operationNamePattern)
string operationNamePattern,
string resourceNamePattern,
ICollection<KeyValuePair<string, string>> tagPatterns)
{
_samplingRate = rate;
RuleName = ruleName;

_serviceNameRegex = RegexBuilder.Build(serviceNamePattern, patternFormat);
_operationNameRegex = RegexBuilder.Build(operationNamePattern, patternFormat);
_resourceNameRegex = RegexBuilder.Build(resourceNamePattern, patternFormat);
_tagRegexes = RegexBuilder.Build(tagPatterns, patternFormat);

if (_serviceNameRegex is null &&
_operationNameRegex is null)
_operationNameRegex is null &&
_resourceNameRegex is null &&
(_tagRegexes is null || _tagRegexes.Count == 0))
{
// if no patterns were specified, this rule always matches (i.e. catch-all)
_alwaysMatch = true;
Expand Down Expand Up @@ -81,7 +88,9 @@ public static IEnumerable<CustomSamplingRule> BuildFromConfigurationString(strin
r.RuleName ?? $"config-rule-{index}",
patternFormat,
r.Service,
r.OperationName));
r.OperationName,
r.Resource,
r.Tags));
}

return samplingRules;
Expand Down Expand Up @@ -109,26 +118,13 @@ public bool IsMatch(Span span)
return false;
}

try
{
// if a regex is null (not specified), it always matches.
// stop as soon as we find a non-match.
return (_serviceNameRegex?.Match(span.ServiceName).Success ?? true) &&
(_operationNameRegex?.Match(span.OperationName).Success ?? true);
}
catch (RegexMatchTimeoutException e)
{
// flag regex so we don't try to use it again
_regexTimedOut = true;

Log.Error(
e,
"""Regex timed out when trying to match value "{Input}" against pattern "{Pattern}".""",
e.Input,
e.Pattern);

return false;
}
return SamplingRuleHelper.IsMatch(
span,
serviceNameRegex: _serviceNameRegex,
operationNameRegex: _operationNameRegex,
resourceNameRegex: _resourceNameRegex,
tagRegexes: _tagRegexes,
out _regexTimedOut);
}

public float GetSamplingRate(Span span)
Expand All @@ -152,6 +148,12 @@ private class CustomRuleConfig

[JsonProperty(PropertyName = "service")]
public string Service { get; set; }

[JsonProperty(PropertyName = "resource")]
public string Resource { get; set; }

[JsonProperty(PropertyName = "tags")]
public Dictionary<string, string> Tags { get; set; }
}
}
}
27 changes: 23 additions & 4 deletions tracer/src/Datadog.Trace/Sampling/RegexBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
// </copyright>

using System;
using Datadog.Trace.Logging;
using Datadog.Trace.Util;
using System.Collections.Generic;

#if NETCOREAPP3_1_OR_GREATER
using Datadog.Trace.Vendors.IndieSystem.Text.RegularExpressions;
Expand All @@ -19,8 +18,6 @@ namespace Datadog.Trace.Sampling;

internal static class RegexBuilder
{
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(RegexBuilder));

public static Regex? Build(string? pattern, string format)
{
if (pattern is null)
Expand Down Expand Up @@ -56,6 +53,28 @@ internal static class RegexBuilder
}
}

public static List<KeyValuePair<string, Regex?>> Build(ICollection<KeyValuePair<string, string?>> patterns, string format)
{
if (patterns is { Count: > 0 })
{
var regexList = new List<KeyValuePair<string, Regex?>>(patterns.Count);

foreach (var pattern in patterns)
{
var regex = Build(pattern.Value, format);

if (regex != null)
{
regexList.Add(new KeyValuePair<string, Regex?>(pattern.Key, regex));
}
}

return regexList;
}

return [];
}

private static string WrapWithLineCharacters(string regex)
{
var hasLineStart = regex.StartsWith("^");
Expand Down
131 changes: 131 additions & 0 deletions tracer/src/Datadog.Trace/Sampling/SamplingRuleHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// <copyright file="SamplingRuleHelper.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.Globalization;
using System.Runtime.CompilerServices;
using Datadog.Trace.Logging;

#if NETCOREAPP3_1_OR_GREATER
using Datadog.Trace.Vendors.IndieSystem.Text.RegularExpressions;
#else
using System.Text.RegularExpressions;
#endif

namespace Datadog.Trace.Sampling;

#nullable enable

internal static class SamplingRuleHelper
{
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(SamplingRuleHelper));

public static bool IsMatch(
Span span,
Regex? serviceNameRegex,
Regex? operationNameRegex,
Regex? resourceNameRegex,
List<KeyValuePair<string, Regex?>>? tagRegexes,
out bool timedOut)
{
timedOut = false;

if (span == null!)
{
return false;
}

try
{
// if a regex is null (not specified), it always matches.
// stop as soon as we find a non-match.
return IsMatch(serviceNameRegex, span.ServiceName) &&
IsMatch(operationNameRegex, span.OperationName) &&
IsMatch(resourceNameRegex, span.ResourceName) &&
MatchSpanByTags(span, tagRegexes);
}
catch (RegexMatchTimeoutException e)
{
// flag rule so we don't try to use one of its regexes again
timedOut = true;

Log.Error(
e,
"""Regex timed out when trying to match value "{Input}" against pattern "{Pattern}".""",
e.Input,
e.Pattern);

return false;
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsMatch(Regex? regex, string? input)
{
if (regex is null)
{
// if a regex is null (not specified), it always matches.
return true;
}

if (input is null)
{
return false;
}

return regex.Match(input).Success;
}

private static bool MatchSpanByTags(Span span, List<KeyValuePair<string, Regex?>>? tagRegexes)
{
if (tagRegexes is null || tagRegexes.Count == 0)
{
// if a regex is null (not specified), it always matches.
return true;
}

foreach (var pair in tagRegexes)
{
var tagName = pair.Key;
var tagRegex = pair.Value;

if (tagRegex is null)
{
// if a regex is null (not specified), it always matches.
continue;
}

var tagValue = GetSpanTag(span, tagName);

if (tagValue is null || !tagRegex.Match(tagValue).Success)
{
// stop as soon as we find a tag that isn't set or doesn't match
return false;
}
}

// all specified tags exist and matched
return true;
}

private static string? GetSpanTag(Span span, string tagName)
{
if (span.GetTag(tagName) is { } tagValue)
{
return tagValue;
}

// if the string tag doesn't exist, try to get it as a numeric tag...
if (span.GetMetric(tagName) is not { } numericTagValue)
{
return null;
}

// ...but only if it is an integer
var intValue = (int)numericTagValue;
return Math.Abs(intValue - numericTagValue) < 0.0001 ? intValue.ToString(CultureInfo.InvariantCulture) : null;
Copy link
Collaborator

Choose a reason for hiding this comment

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

How come we have to test the absolute value? Is a simple int conversion insufficient?

Copy link
Member Author

@lucaspimentel lucaspimentel Jan 18, 2024

Choose a reason for hiding this comment

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

I want to determine if the tag value is an integer, but it is stored as a double. If you convert it to int and try to compare them with intValue == doubleValue, Rider recommends using Math.Abs(value1 - value2) < tolerance) instead.

Using the ==/!= operators to compare floating-point numbers is, generally, a bad idea. Floating point values are inherently inaccurate, and comparing them for exact equality is almost never the desired semantics.

https://www.jetbrains.com/help/rider/CompareOfFloatsByEqualityOperator.html

}
}
44 changes: 33 additions & 11 deletions tracer/src/Datadog.Trace/Sampling/SpanSamplingRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

using System;
using System.Collections.Generic;
using System.Linq;
using Datadog.Trace.Logging;
using Datadog.Trace.Util;
using Datadog.Trace.Vendors.Newtonsoft.Json;
Expand All @@ -30,19 +29,26 @@ internal class SpanSamplingRule : ISpanSamplingRule
// 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 Regex _resourceNameRegex;
private readonly List<KeyValuePair<string, Regex>> _tagRegexes;

private readonly IRateLimiter _limiter;
private bool _regexTimedOut;

/// <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="resourceNameGlob">The glob pattern for the <see cref="Span.ResourceName"/>.</param>
/// <param name="tagGlobs">The glob pattern for the <see cref="Span.Tags"/>.</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,
string resourceNameGlob,
ICollection<KeyValuePair<string, string>> tagGlobs,
float samplingRate = 1.0f,
float? maxPerSecond = null)
{
Expand All @@ -64,9 +70,13 @@ public SpanSamplingRule(

_serviceNameRegex = RegexBuilder.Build(serviceNameGlob, SamplingRulesFormat.Glob);
_operationNameRegex = RegexBuilder.Build(operationNameGlob, SamplingRulesFormat.Glob);
_resourceNameRegex = RegexBuilder.Build(resourceNameGlob, SamplingRulesFormat.Glob);
_tagRegexes = RegexBuilder.Build(tagGlobs, SamplingRulesFormat.Glob);

if (_serviceNameRegex is null &&
_operationNameRegex is null)
_operationNameRegex is null &&
_resourceNameRegex is null &&
(_tagRegexes is null || _tagRegexes.Count == 0))
{
// if no patterns were specified, this rule always matches (i.e. catch-all)
_alwaysMatch = true;
Expand Down Expand Up @@ -99,6 +109,8 @@ public static IEnumerable<SpanSamplingRule> BuildFromConfigurationString(string
new SpanSamplingRule(
rule.ServiceNameGlob,
rule.OperationNameGlob,
rule.ResourceNameGlob,
rule.TagGlobs,
rule.SampleRate,
rule.MaxPerSecond));
}
Expand All @@ -117,21 +129,25 @@ public static IEnumerable<SpanSamplingRule> BuildFromConfigurationString(string
/// <inheritdoc/>
public bool IsMatch(Span span)
{
if (span is null)
{
return false;
}

if (_alwaysMatch)
{
// the rule is a catch-all
return true;
}

// if a regex is null (not specified), it always matches.
// stop as soon as we find a non-match.
return (_serviceNameRegex?.Match(span.ServiceName).Success ?? true) &&
(_operationNameRegex?.Match(span.OperationName).Success ?? true);
if (_regexTimedOut)
{
// the regex had a valid format, but it timed out previously. stop trying to use it.
return false;
}

return SamplingRuleHelper.IsMatch(
span,
serviceNameRegex: _serviceNameRegex,
operationNameRegex: _operationNameRegex,
resourceNameRegex: _resourceNameRegex,
tagRegexes: _tagRegexes,
out _regexTimedOut);
}

/// <inheritdoc/>
Expand Down Expand Up @@ -163,6 +179,12 @@ internal class SpanSamplingRuleConfig
[JsonProperty(PropertyName = "name")]
public string OperationNameGlob { get; set; } = "*";

[JsonProperty(PropertyName = "resource")]
public string ResourceNameGlob { get; set; }

[JsonProperty(PropertyName = "tags")]
public Dictionary<string, string> TagGlobs { get; set; }

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

Expand Down
Loading
Loading