From b14195ed43b94c2bfdf5d331f9bedcb5eda6475e Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Fri, 10 Apr 2026 12:21:18 +0100 Subject: [PATCH 1/8] Add unit tests for peer tag hash calculation --- .../Datadog.Trace/Agent/StatsAggregator.cs | 8 +- .../Agent/StatsAggregatorTests.cs | 100 ++++++++++++++++++ 2 files changed, 105 insertions(+), 3 deletions(-) diff --git a/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs b/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs index 449f069f25c8..c158d624411a 100644 --- a/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs +++ b/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs @@ -304,7 +304,6 @@ internal StatsAggregationKey BuildKey(Span span, List peerTagKeys, out L // Hash should be generated as TAGNAME:TAGVALUE, and should be in sorted order (we sort ahead of time) // peerTagKeys should already be in sorted order // We serialize to the utf-8 bytes because we need to serialize them during sending anyway - // TODO: Verify we get the same results as the go code utf8PeerTags = EmptyPeerTags; peerTagsHash = 0; foreach (var tagKey in peerTagKeys) @@ -316,21 +315,24 @@ internal StatsAggregationKey BuildKey(Span span, List peerTagKeys, out L } tagValue = IpAddressObfuscationUtil.QuantizePeerIpAddresses(tagValue); + var bytes = EncodingHelpers.Utf8NoBom.GetBytes($"{tagKey}:{tagValue}"); if (ReferenceEquals(utf8PeerTags, EmptyPeerTags)) { // We're not setting the capacity here, because there's // a _lot_ of potential peer tags, and _most_ of them won't apply utf8PeerTags = new(); + // hash the bytes of the tag (starting from the initial hash) + peerTagsHash = FnvHash64.GenerateHash(bytes, FnvHash64.Version.V1A); } else { // add the separator peerTagsHash = FnvHash64.GenerateHash(PeerTagSeparator, FnvHash64.Version.V1A, peerTagsHash); + // hash the bytes of the tag + peerTagsHash = FnvHash64.GenerateHash(bytes, FnvHash64.Version.V1A, peerTagsHash); } - var bytes = EncodingHelpers.Utf8NoBom.GetBytes($"{tagKey}:{tagValue}"); - peerTagsHash = FnvHash64.GenerateHash(bytes, FnvHash64.Version.V1A, peerTagsHash); utf8PeerTags.Add(bytes); } } diff --git a/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs b/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs index ebc6b1e91e64..3aac6563ecac 100644 --- a/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs @@ -1179,6 +1179,106 @@ public async Task PeerTagsCreateDistinctBuckets() } } + [Theory] + [InlineData(SpanKinds.Client)] + [InlineData(SpanKinds.Producer)] + public async Task PeerTagsHash_MatchesGoAgent_SingleTag(string spanKind) + { + // Golden value from Go agent's TestNewAggregation in aggregation_test.go: + // peer.service:remote-service → hash 3430395298086625290 + // https://github.com/DataDog/datadog-agent/blob/4c45a7cf23b97bf6b904565f88d16e73da83842a/pkg/trace/stats/aggregation_test.go + var start = DateTimeOffset.UtcNow; + await using var aggregator = new StatsAggregator(Mock.Of(), GetSettings(), Mock.Of(), isOtlp: false); + + var span = CreateTopLevelSpan(start, "svc"); + span.SetTag(Tags.SpanKind, spanKind); + span.Tags.SetTag("peer.service", "remote-service"); + + var peerTagKeys = new List { "peer.service" }; + var key = aggregator.BuildKey(span, peerTagKeys, out _); + + key.PeerTagsHash.Should().Be(3430395298086625290UL); + } + + [Fact] + public async Task PeerTagsHash_MatchesGoAgent_MultipleTags() + { + // Golden value from Go agent's TestNewAggregation in aggregation_test.go: + // db.instance:i-1234, db.system:postgres, peer.service:remote-service → hash 9894752672193411515 + // https://github.com/DataDog/datadog-agent/blob/4c45a7cf23b97bf6b904565f88d16e73da83842a/pkg/trace/stats/aggregation_test.go + var start = DateTimeOffset.UtcNow; + await using var aggregator = new StatsAggregator(Mock.Of(), GetSettings(), Mock.Of(), isOtlp: false); + + var span = CreateTopLevelSpan(start, "svc"); + span.SetTag(Tags.SpanKind, SpanKinds.Client); + span.Tags.SetTag("peer.service", "remote-service"); + span.Tags.SetTag("db.instance", "i-1234"); + span.Tags.SetTag("db.system", "postgres"); + + // Keys must be pre-sorted (matching agent behavior) + var peerTagKeys = new List { "db.instance", "db.system", "peer.service" }; + var key = aggregator.BuildKey(span, peerTagKeys, out _); + + key.PeerTagsHash.Should().Be(9894752672193411515UL); + } + + [Fact] + public async Task PeerTagsHash_MatchesGoAgent_ConsumerMessagingTags() + { + // Golden value from Go agent's TestNewAggregation in aggregation_test.go: + // messaging.destination:topic-foo, messaging.system:kafka → hash 0xf5eeb51fbe7929b4 + // https://github.com/DataDog/datadog-agent/blob/4c45a7cf23b97bf6b904565f88d16e73da83842a/pkg/trace/stats/aggregation_test.go + var start = DateTimeOffset.UtcNow; + await using var aggregator = new StatsAggregator(Mock.Of(), GetSettings(), Mock.Of(), isOtlp: false); + + var span = CreateTopLevelSpan(start, "svc"); + span.SetTag(Tags.SpanKind, SpanKinds.Consumer); + span.Tags.SetTag("messaging.destination", "topic-foo"); + span.Tags.SetTag("messaging.system", "kafka"); + + var peerTagKeys = new List { "db.instance", "db.system", "messaging.destination", "messaging.system" }; + var key = aggregator.BuildKey(span, peerTagKeys, out _); + + key.PeerTagsHash.Should().Be(0xf5eeb51fbe7929b4UL); + } + + [Fact] + public async Task PeerTagsHash_MatchesGoAgent_EmptyTagsSkipped() + { + // Same hash as single tag — empty db.instance and db.system values are skipped + // https://github.com/DataDog/datadog-agent/blob/4c45a7cf23b97bf6b904565f88d16e73da83842a/pkg/trace/stats/aggregation_test.go + var start = DateTimeOffset.UtcNow; + await using var aggregator = new StatsAggregator(Mock.Of(), GetSettings(), Mock.Of(), isOtlp: false); + + var span = CreateTopLevelSpan(start, "svc"); + span.SetTag(Tags.SpanKind, SpanKinds.Client); + span.Tags.SetTag("peer.service", "remote-service"); + span.Tags.SetTag("db.instance", string.Empty); + span.Tags.SetTag("db.system", string.Empty); + + var peerTagKeys = new List { "db.instance", "db.system", "peer.service" }; + var key = aggregator.BuildKey(span, peerTagKeys, out _); + + key.PeerTagsHash.Should().Be(3430395298086625290UL); + } + + [Fact] + public async Task PeerTagsHash_NoMatchingTags_ReturnsZero() + { + var start = DateTimeOffset.UtcNow; + await using var aggregator = new StatsAggregator(Mock.Of(), GetSettings(), Mock.Of(), isOtlp: false); + + var span = CreateTopLevelSpan(start, "svc"); + span.SetTag(Tags.SpanKind, SpanKinds.Client); + // No peer tags set on the span + + var peerTagKeys = new List { "peer.service" }; + var key = aggregator.BuildKey(span, peerTagKeys, out var utf8PeerTags); + + key.PeerTagsHash.Should().Be(0UL); + utf8PeerTags.Should().BeEmpty(); + } + /// /// Creates a top-level span with a TraceContext (required by GetWeight). /// From 2c0e776819246c351507c6059337691911b7efd8 Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Mon, 13 Apr 2026 12:09:49 +0100 Subject: [PATCH 2/8] Try reduce allocation by adding EncodedPeerTags --- .../Datadog.Trace/Agent/EncodedPeerTags.cs | 71 +++++++++++++++++++ .../Datadog.Trace/Agent/IStatsAggregator.cs | 4 +- .../Agent/NullStatsAggregator.cs | 10 ++- .../Datadog.Trace/Agent/StatsAggregator.cs | 52 +++++++++----- .../Agent/TraceSamplers/RareSampler.cs | 4 +- .../Agent/AgentWriterTests.cs | 6 +- .../Agent/StatsAggregatorTests.cs | 2 +- 7 files changed, 124 insertions(+), 25 deletions(-) create mode 100644 tracer/src/Datadog.Trace/Agent/EncodedPeerTags.cs diff --git a/tracer/src/Datadog.Trace/Agent/EncodedPeerTags.cs b/tracer/src/Datadog.Trace/Agent/EncodedPeerTags.cs new file mode 100644 index 000000000000..8bb189fa58a9 --- /dev/null +++ b/tracer/src/Datadog.Trace/Agent/EncodedPeerTags.cs @@ -0,0 +1,71 @@ +// +// 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. +// + +#nullable enable + +using System; +using System.Collections.Generic; +using Datadog.Trace.Util; + +namespace Datadog.Trace.Agent; + +/// +/// A class that holds the encoded peer tags for a given bucket. +/// Must always be disposed to ensure pooled arrays are released. +/// +internal sealed class EncodedPeerTags : IDisposable +{ + public static readonly List EmptyTags = []; + private List>? _utf8PeerTags; + + public ArraySegment EncodeAndSavePeerTag(string tagKey, string tagValue) + { + // Encode key, ':', and value directly into the rented buffer to avoid + // allocating an intermediate interpolated string. + var maxBytes = EncodingHelpers.Utf8NoBom.GetMaxByteCount(tagKey.Length + 1 + tagValue.Length); + var bytes = ArrayPool.Shared.Rent(maxBytes); + + var byteCount = EncodingHelpers.Utf8NoBom.GetBytes(tagKey, charIndex: 0, charCount: tagKey.Length, bytes, byteIndex: 0); + bytes[byteCount++] = (byte)':'; + byteCount += EncodingHelpers.Utf8NoBom.GetBytes(tagValue, charIndex: 0, charCount: tagValue.Length, bytes, byteIndex: byteCount); + + _utf8PeerTags ??= new(); + var arraySegment = new ArraySegment(bytes, offset: 0, count: byteCount); + _utf8PeerTags.Add(arraySegment); + return arraySegment; + } + + public List GetPeerTags() + { + if (_utf8PeerTags is null) + { + return EmptyTags; + } + + var list = new List(_utf8PeerTags.Count); + foreach (var tag in _utf8PeerTags) + { + // create an array of the correct size + var destination = new byte[tag.Count]; + tag.AsSpan().CopyTo(destination); + list.Add(destination); + } + + return list; + } + + public void Dispose() + { + if (_utf8PeerTags is not null) + { + foreach (var keyValuePair in _utf8PeerTags) + { + ArrayPool.Shared.Return(keyValuePair.Array!); + } + + _utf8PeerTags = null; + } + } +} diff --git a/tracer/src/Datadog.Trace/Agent/IStatsAggregator.cs b/tracer/src/Datadog.Trace/Agent/IStatsAggregator.cs index 5837ff767d3b..bb03185b6d2c 100644 --- a/tracer/src/Datadog.Trace/Agent/IStatsAggregator.cs +++ b/tracer/src/Datadog.Trace/Agent/IStatsAggregator.cs @@ -51,6 +51,8 @@ internal interface IStatsAggregator Task DisposeAsync(); - StatsAggregationKey BuildKey(Span span, out List utf8PeerTags); + StatsAggregationKey BuildKey(Span span); + + StatsAggregationKey BuildKey(Span span, out EncodedPeerTags utf8PeerTags); } } diff --git a/tracer/src/Datadog.Trace/Agent/NullStatsAggregator.cs b/tracer/src/Datadog.Trace/Agent/NullStatsAggregator.cs index b6a102b98cfd..861cb1cd1b19 100644 --- a/tracer/src/Datadog.Trace/Agent/NullStatsAggregator.cs +++ b/tracer/src/Datadog.Trace/Agent/NullStatsAggregator.cs @@ -28,10 +28,8 @@ public Task DisposeAsync() return Task.CompletedTask; } - public StatsAggregationKey BuildKey(Span span, out List utf8PeerTags) + public StatsAggregationKey BuildKey(Span span) { - utf8PeerTags = []; - var rawHttpStatusCode = span.GetTag(Tags.HttpStatusCode); if (rawHttpStatusCode is null || !int.TryParse(rawHttpStatusCode, out var httpStatusCode)) { @@ -55,5 +53,11 @@ public StatsAggregationKey BuildKey(Span span, out List utf8PeerTags) serviceSource: string.Empty, peerTagsHash: 0); } + + public StatsAggregationKey BuildKey(Span span, out EncodedPeerTags utf8PeerTags) + { + utf8PeerTags = null; + return BuildKey(span); + } } } diff --git a/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs b/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs index c158d624411a..df0b44e62ba4 100644 --- a/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs +++ b/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs @@ -26,7 +26,6 @@ internal sealed class StatsAggregator : IStatsAggregator private const int BufferCount = 2; private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(); - private static readonly List EmptyPeerTags = []; private static readonly byte[] PeerTagSeparator = [0]; private readonly StatsBuffer[] _buffers; @@ -248,10 +247,23 @@ internal SpanCollection ObfuscateTrace(in SpanCollection trace) return trace; } - public StatsAggregationKey BuildKey(Span span, out List utf8PeerTags) + public StatsAggregationKey BuildKey(Span span) + { + EncodedPeerTags utf8PeerTags = null; + try + { + return BuildKey(span, Volatile.Read(ref _peerTagKeys), out utf8PeerTags); + } + finally + { + utf8PeerTags?.Dispose(); + } + } + + public StatsAggregationKey BuildKey(Span span, out EncodedPeerTags utf8PeerTags) => BuildKey(span, Volatile.Read(ref _peerTagKeys), out utf8PeerTags); - internal StatsAggregationKey BuildKey(Span span, List peerTagKeys, out List utf8PeerTags) + internal StatsAggregationKey BuildKey(Span span, List peerTagKeys, out EncodedPeerTags utf8PeerTags) { var rawHttpStatusCode = span.GetTag(Tags.HttpStatusCode); @@ -296,15 +308,16 @@ internal StatsAggregationKey BuildKey(Span span, List peerTagKeys, out L ulong peerTagsHash; if ((string.IsNullOrEmpty(spanKind) || spanKind is SpanKinds.Internal) && span.GetTag(Tags.BaseService) is { Length: >0 } baseService) { - utf8PeerTags = [EncodingHelpers.Utf8NoBom.GetBytes($"{Tags.BaseService}:{baseService}")]; - peerTagsHash = FnvHash64.GenerateHash(utf8PeerTags[0], FnvHash64.Version.V1A); + utf8PeerTags = new(); + var encoded = utf8PeerTags.EncodeAndSavePeerTag(Tags.BaseService, baseService); + peerTagsHash = FnvHash64.GenerateHash(encoded, FnvHash64.Version.V1A); } else if (spanKind is SpanKinds.Client or SpanKinds.Server or SpanKinds.Producer or SpanKinds.Consumer) { // Hash should be generated as TAGNAME:TAGVALUE, and should be in sorted order (we sort ahead of time) // peerTagKeys should already be in sorted order // We serialize to the utf-8 bytes because we need to serialize them during sending anyway - utf8PeerTags = EmptyPeerTags; + utf8PeerTags = null; peerTagsHash = 0; foreach (var tagKey in peerTagKeys) { @@ -315,13 +328,13 @@ internal StatsAggregationKey BuildKey(Span span, List peerTagKeys, out L } tagValue = IpAddressObfuscationUtil.QuantizePeerIpAddresses(tagValue); - var bytes = EncodingHelpers.Utf8NoBom.GetBytes($"{tagKey}:{tagValue}"); - if (ReferenceEquals(utf8PeerTags, EmptyPeerTags)) + if (utf8PeerTags is null) { // We're not setting the capacity here, because there's // a _lot_ of potential peer tags, and _most_ of them won't apply utf8PeerTags = new(); + var bytes = utf8PeerTags.EncodeAndSavePeerTag(tagKey, tagValue); // hash the bytes of the tag (starting from the initial hash) peerTagsHash = FnvHash64.GenerateHash(bytes, FnvHash64.Version.V1A); } @@ -330,16 +343,15 @@ internal StatsAggregationKey BuildKey(Span span, List peerTagKeys, out L // add the separator peerTagsHash = FnvHash64.GenerateHash(PeerTagSeparator, FnvHash64.Version.V1A, peerTagsHash); // hash the bytes of the tag + var bytes = utf8PeerTags.EncodeAndSavePeerTag(tagKey, tagValue); peerTagsHash = FnvHash64.GenerateHash(bytes, FnvHash64.Version.V1A, peerTagsHash); } - - utf8PeerTags.Add(bytes); } } else { peerTagsHash = 0; - utf8PeerTags = EmptyPeerTags; + utf8PeerTags = null; } // When submitting trace metrics over OTLP, we must create inidividual timeseries @@ -442,14 +454,22 @@ private void AddToBuffer(Span span) return; } - var key = BuildKey(span, out var peerTags); - var buffer = CurrentBuffer; - if (!buffer.Buckets.TryGetValue(key, out var bucket)) + EncodedPeerTags peerTags = null; + StatsBucket bucket; + try + { + var key = BuildKey(span, out peerTags); + if (!buffer.Buckets.TryGetValue(key, out bucket)) + { + bucket = new StatsBucket(key, peerTags?.GetPeerTags() ?? EncodedPeerTags.EmptyTags); + buffer.Buckets.Add(key, bucket); + } + } + finally { - bucket = new StatsBucket(key, peerTags); - buffer.Buckets.Add(key, bucket); + peerTags?.Dispose(); } var weight = GetWeight(span); diff --git a/tracer/src/Datadog.Trace/Agent/TraceSamplers/RareSampler.cs b/tracer/src/Datadog.Trace/Agent/TraceSamplers/RareSampler.cs index 1d208d80f148..d71380b6a916 100644 --- a/tracer/src/Datadog.Trace/Agent/TraceSamplers/RareSampler.cs +++ b/tracer/src/Datadog.Trace/Agent/TraceSamplers/RareSampler.cs @@ -91,7 +91,7 @@ private bool SampleSpansAndUpdateSeenSpansIfKept(in SpanCollection trace) private bool SampleSpan(Span span) { - var key = _aggregator.BuildKey(span, out _); + var key = _aggregator.BuildKey(span); var isNewKey = _keys.Add(key); if (isNewKey) @@ -105,7 +105,7 @@ private bool SampleSpan(Span span) private void UpdateSpan(Span span) { - var key = _aggregator.BuildKey(span, out _); + var key = _aggregator.BuildKey(span); var isNewKey = _keys.Add(key); if (isNewKey) diff --git a/tracer/test/Datadog.Trace.Tests/Agent/AgentWriterTests.cs b/tracer/test/Datadog.Trace.Tests/Agent/AgentWriterTests.cs index 21c4baccf2b6..a77b6e7ac1be 100644 --- a/tracer/test/Datadog.Trace.Tests/Agent/AgentWriterTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Agent/AgentWriterTests.cs @@ -609,9 +609,11 @@ public TraceKeepState ProcessTrace(ref SpanCollection spans) public Task DisposeAsync() => Task.CompletedTask; - public StatsAggregationKey BuildKey(Span span, out List utf8PeerTags) + public StatsAggregationKey BuildKey(Span span) => new(); + + public StatsAggregationKey BuildKey(Span span, out EncodedPeerTags utf8PeerTags) { - utf8PeerTags = []; + utf8PeerTags = null; return new(); } } diff --git a/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs b/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs index 3aac6563ecac..a7686382c402 100644 --- a/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs @@ -1276,7 +1276,7 @@ public async Task PeerTagsHash_NoMatchingTags_ReturnsZero() var key = aggregator.BuildKey(span, peerTagKeys, out var utf8PeerTags); key.PeerTagsHash.Should().Be(0UL); - utf8PeerTags.Should().BeEmpty(); + utf8PeerTags.Should().BeNull(); } /// From 8e949275e79749440fdf290701ee6fc4f8e465bd Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Mon, 13 Apr 2026 10:11:32 +0100 Subject: [PATCH 3/8] Update EncodedPeerTags implementation --- .../Datadog.Trace/Agent/EncodedPeerTags.cs | 71 -------- .../Datadog.Trace/Agent/IStatsAggregator.cs | 2 - .../Agent/NullStatsAggregator.cs | 6 - .../Datadog.Trace/Agent/StatsAggregator.cs | 169 ++++++++++++------ .../Agent/AgentWriterTests.cs | 6 - .../Agent/StatsAggregatorTests.cs | 35 ++-- 6 files changed, 129 insertions(+), 160 deletions(-) delete mode 100644 tracer/src/Datadog.Trace/Agent/EncodedPeerTags.cs diff --git a/tracer/src/Datadog.Trace/Agent/EncodedPeerTags.cs b/tracer/src/Datadog.Trace/Agent/EncodedPeerTags.cs deleted file mode 100644 index 8bb189fa58a9..000000000000 --- a/tracer/src/Datadog.Trace/Agent/EncodedPeerTags.cs +++ /dev/null @@ -1,71 +0,0 @@ -// -// 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. -// - -#nullable enable - -using System; -using System.Collections.Generic; -using Datadog.Trace.Util; - -namespace Datadog.Trace.Agent; - -/// -/// A class that holds the encoded peer tags for a given bucket. -/// Must always be disposed to ensure pooled arrays are released. -/// -internal sealed class EncodedPeerTags : IDisposable -{ - public static readonly List EmptyTags = []; - private List>? _utf8PeerTags; - - public ArraySegment EncodeAndSavePeerTag(string tagKey, string tagValue) - { - // Encode key, ':', and value directly into the rented buffer to avoid - // allocating an intermediate interpolated string. - var maxBytes = EncodingHelpers.Utf8NoBom.GetMaxByteCount(tagKey.Length + 1 + tagValue.Length); - var bytes = ArrayPool.Shared.Rent(maxBytes); - - var byteCount = EncodingHelpers.Utf8NoBom.GetBytes(tagKey, charIndex: 0, charCount: tagKey.Length, bytes, byteIndex: 0); - bytes[byteCount++] = (byte)':'; - byteCount += EncodingHelpers.Utf8NoBom.GetBytes(tagValue, charIndex: 0, charCount: tagValue.Length, bytes, byteIndex: byteCount); - - _utf8PeerTags ??= new(); - var arraySegment = new ArraySegment(bytes, offset: 0, count: byteCount); - _utf8PeerTags.Add(arraySegment); - return arraySegment; - } - - public List GetPeerTags() - { - if (_utf8PeerTags is null) - { - return EmptyTags; - } - - var list = new List(_utf8PeerTags.Count); - foreach (var tag in _utf8PeerTags) - { - // create an array of the correct size - var destination = new byte[tag.Count]; - tag.AsSpan().CopyTo(destination); - list.Add(destination); - } - - return list; - } - - public void Dispose() - { - if (_utf8PeerTags is not null) - { - foreach (var keyValuePair in _utf8PeerTags) - { - ArrayPool.Shared.Return(keyValuePair.Array!); - } - - _utf8PeerTags = null; - } - } -} diff --git a/tracer/src/Datadog.Trace/Agent/IStatsAggregator.cs b/tracer/src/Datadog.Trace/Agent/IStatsAggregator.cs index bb03185b6d2c..c8600100732a 100644 --- a/tracer/src/Datadog.Trace/Agent/IStatsAggregator.cs +++ b/tracer/src/Datadog.Trace/Agent/IStatsAggregator.cs @@ -52,7 +52,5 @@ internal interface IStatsAggregator Task DisposeAsync(); StatsAggregationKey BuildKey(Span span); - - StatsAggregationKey BuildKey(Span span, out EncodedPeerTags utf8PeerTags); } } diff --git a/tracer/src/Datadog.Trace/Agent/NullStatsAggregator.cs b/tracer/src/Datadog.Trace/Agent/NullStatsAggregator.cs index 861cb1cd1b19..d76406d00ac7 100644 --- a/tracer/src/Datadog.Trace/Agent/NullStatsAggregator.cs +++ b/tracer/src/Datadog.Trace/Agent/NullStatsAggregator.cs @@ -53,11 +53,5 @@ public StatsAggregationKey BuildKey(Span span) serviceSource: string.Empty, peerTagsHash: 0); } - - public StatsAggregationKey BuildKey(Span span, out EncodedPeerTags utf8PeerTags) - { - utf8PeerTags = null; - return BuildKey(span); - } } } diff --git a/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs b/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs index df0b44e62ba4..bfa293e0511c 100644 --- a/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs +++ b/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs @@ -26,6 +26,7 @@ internal sealed class StatsAggregator : IStatsAggregator private const int BufferCount = 2; private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(); + private static readonly List EmptyPeerTags = []; private static readonly byte[] PeerTagSeparator = [0]; private readonly StatsBuffer[] _buffers; @@ -248,22 +249,13 @@ internal SpanCollection ObfuscateTrace(in SpanCollection trace) } public StatsAggregationKey BuildKey(Span span) - { - EncodedPeerTags utf8PeerTags = null; - try - { - return BuildKey(span, Volatile.Read(ref _peerTagKeys), out utf8PeerTags); - } - finally - { - utf8PeerTags?.Dispose(); - } - } + => BuildKey(span, Volatile.Read(ref _peerTagKeys)); - public StatsAggregationKey BuildKey(Span span, out EncodedPeerTags utf8PeerTags) - => BuildKey(span, Volatile.Read(ref _peerTagKeys), out utf8PeerTags); - - internal StatsAggregationKey BuildKey(Span span, List peerTagKeys, out EncodedPeerTags utf8PeerTags) + /// + /// Computes a for the given span, including the peer tags hash. + /// Peer tag selection logic is mirrored in for the cold path. + /// + internal StatsAggregationKey BuildKey(Span span, List peerTagKeys) { var rawHttpStatusCode = span.GetTag(Tags.HttpStatusCode); @@ -298,27 +290,20 @@ internal StatsAggregationKey BuildKey(Span span, List peerTagKeys, out E } // Based on https://github.com/DataDog/datadog-agent/blob/ce22e11ee71e55be717b9d9a3f8f3d7721a9c6d7/pkg/trace/stats/span_concentrator.go#L53-L99 - // Peer tags are extracted for client/producer/consumer spans + // Peer tags are extracted for client/server/producer/consumer spans. // If the span kind is missing or internal, and we have a "base service" tag `_dd.base_service` - // then we only aggregate based on the `_dd.base_service` - // TODO: work out how to optimize the peer tags allocations, to avoid all the extra utf8 allocations - // - on .NET Core these are only necessary for the first instance of the key, but this makes for a tricky - // chicken and egg - we need to convert everything to utf-8, so that we can get the hash, so that we - // know whether we need the tags as byte[] or not.. + // then we only aggregate based on the `_dd.base_service`. + // This computes only the hash; see GetEncodedPeerTags() for the cold-path encoding. ulong peerTagsHash; - if ((string.IsNullOrEmpty(spanKind) || spanKind is SpanKinds.Internal) && span.GetTag(Tags.BaseService) is { Length: >0 } baseService) + if ((string.IsNullOrEmpty(spanKind) || spanKind is SpanKinds.Internal) && span.GetTag(Tags.BaseService) is { Length: > 0 } baseService) { - utf8PeerTags = new(); - var encoded = utf8PeerTags.EncodeAndSavePeerTag(Tags.BaseService, baseService); - peerTagsHash = FnvHash64.GenerateHash(encoded, FnvHash64.Version.V1A); + peerTagsHash = HashTag(Tags.BaseService, baseService, FnvHash64.Version.V1A); } else if (spanKind is SpanKinds.Client or SpanKinds.Server or SpanKinds.Producer or SpanKinds.Consumer) { - // Hash should be generated as TAGNAME:TAGVALUE, and should be in sorted order (we sort ahead of time) - // peerTagKeys should already be in sorted order - // We serialize to the utf-8 bytes because we need to serialize them during sending anyway - utf8PeerTags = null; + // Hash should be generated as TAGNAME:TAGVALUE, in sorted order (peerTagKeys is pre-sorted). peerTagsHash = 0; + bool firstTag = true; foreach (var tagKey in peerTagKeys) { var tagValue = span.GetTag(tagKey); @@ -329,29 +314,19 @@ internal StatsAggregationKey BuildKey(Span span, List peerTagKeys, out E tagValue = IpAddressObfuscationUtil.QuantizePeerIpAddresses(tagValue); - if (utf8PeerTags is null) + if (!firstTag) { - // We're not setting the capacity here, because there's - // a _lot_ of potential peer tags, and _most_ of them won't apply - utf8PeerTags = new(); - var bytes = utf8PeerTags.EncodeAndSavePeerTag(tagKey, tagValue); - // hash the bytes of the tag (starting from the initial hash) - peerTagsHash = FnvHash64.GenerateHash(bytes, FnvHash64.Version.V1A); - } - else - { - // add the separator + // add the separator between tags peerTagsHash = FnvHash64.GenerateHash(PeerTagSeparator, FnvHash64.Version.V1A, peerTagsHash); - // hash the bytes of the tag - var bytes = utf8PeerTags.EncodeAndSavePeerTag(tagKey, tagValue); - peerTagsHash = FnvHash64.GenerateHash(bytes, FnvHash64.Version.V1A, peerTagsHash); } + + peerTagsHash = HashTag(tagKey, tagValue, FnvHash64.Version.V1A, firstTag ? null : peerTagsHash); + firstTag = false; } } else { peerTagsHash = 0; - utf8PeerTags = null; } // When submitting trace metrics over OTLP, we must create inidividual timeseries @@ -376,6 +351,93 @@ internal StatsAggregationKey BuildKey(Span span, List peerTagKeys, out E peerTagsHash); } + /// + /// Encodes "tagKey:tagValue" to UTF-8 and computes its FNV hash. + /// Uses stackalloc on .NET Core, ArrayPool on .NET Framework. + /// +#if NETCOREAPP + [System.Runtime.CompilerServices.SkipLocalsInit] +#endif + private static ulong HashTag(string tagKey, string tagValue, FnvHash64.Version version, ulong? initialHash = null) + { + const int MaxStackLimit = 256; + var maxByteCount = EncodingHelpers.Utf8NoBom.GetMaxByteCount(tagKey.Length + 1 + tagValue.Length); + +#if NETCOREAPP + if (maxByteCount <= MaxStackLimit) + { + Span buffer = stackalloc byte[MaxStackLimit]; + int written = EncodingHelpers.Utf8NoBom.GetBytes(tagKey, buffer); + buffer[written++] = (byte)':'; + written += EncodingHelpers.Utf8NoBom.GetBytes(tagValue, buffer.Slice(written)); + return initialHash is { } h + ? FnvHash64.GenerateHash(buffer.Slice(0, written), version, h) + : FnvHash64.GenerateHash(buffer.Slice(0, written), version); + } +#else + if (maxByteCount <= MaxStackLimit) + { + var buffer = ArrayPool.Shared.Rent(MaxStackLimit); + try + { + int written = EncodingHelpers.Utf8NoBom.GetBytes(tagKey, 0, tagKey.Length, buffer, 0); + buffer[written++] = (byte)':'; + written += EncodingHelpers.Utf8NoBom.GetBytes(tagValue, 0, tagValue.Length, buffer, written); + var segment = new ArraySegment(buffer, 0, written); + return initialHash is { } h + ? FnvHash64.GenerateHash(segment, version, h) + : FnvHash64.GenerateHash(segment, version); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } +#endif + + // Fallback for oversized tags: heap-allocate + var bytes = EncodingHelpers.Utf8NoBom.GetBytes($"{tagKey}:{tagValue}"); + return initialHash is { } ih + ? FnvHash64.GenerateHash(bytes, version, ih) + : FnvHash64.GenerateHash(bytes, version); + } + + /// + /// Encodes the peer tags for a span into a of UTF-8 byte arrays. + /// Called only on the cold path (new bucket creation). + /// Tag selection logic is mirrored in . + /// + internal static List GetEncodedPeerTags(Span span, List peerTagKeys) + { + var spanKind = (span.Tags is InstrumentationTags t ? t.SpanKind : span.GetTag(Tags.SpanKind)) ?? string.Empty; + + if ((string.IsNullOrEmpty(spanKind) || spanKind is SpanKinds.Internal) && span.GetTag(Tags.BaseService) is { Length: > 0 } baseService) + { + return [EncodingHelpers.Utf8NoBom.GetBytes($"{Tags.BaseService}:{baseService}")]; + } + + if (spanKind is not (SpanKinds.Client or SpanKinds.Server or SpanKinds.Producer or SpanKinds.Consumer)) + { + return EmptyPeerTags; + } + + List result = null; + foreach (var tagKey in peerTagKeys) + { + var tagValue = span.GetTag(tagKey); + if (string.IsNullOrEmpty(tagValue)) + { + continue; + } + + tagValue = IpAddressObfuscationUtil.QuantizePeerIpAddresses(tagValue); + result ??= new(); + result.Add(EncodingHelpers.Utf8NoBom.GetBytes($"{tagKey}:{tagValue}")); + } + + return result ?? EmptyPeerTags; + } + internal async Task Flush() { // Use a do/while loop to still flush once if _processExit is already completed (this makes testing easier) @@ -455,21 +517,14 @@ private void AddToBuffer(Span span) } var buffer = CurrentBuffer; + var peerTagKeys = Volatile.Read(ref _peerTagKeys); + var key = BuildKey(span, peerTagKeys); - EncodedPeerTags peerTags = null; - StatsBucket bucket; - try - { - var key = BuildKey(span, out peerTags); - if (!buffer.Buckets.TryGetValue(key, out bucket)) - { - bucket = new StatsBucket(key, peerTags?.GetPeerTags() ?? EncodedPeerTags.EmptyTags); - buffer.Buckets.Add(key, bucket); - } - } - finally + if (!buffer.Buckets.TryGetValue(key, out var bucket)) { - peerTags?.Dispose(); + // Cold path: encode the peer tags for storage in the new bucket + bucket = new StatsBucket(key, GetEncodedPeerTags(span, peerTagKeys)); + buffer.Buckets.Add(key, bucket); } var weight = GetWeight(span); diff --git a/tracer/test/Datadog.Trace.Tests/Agent/AgentWriterTests.cs b/tracer/test/Datadog.Trace.Tests/Agent/AgentWriterTests.cs index a77b6e7ac1be..88235d520b8d 100644 --- a/tracer/test/Datadog.Trace.Tests/Agent/AgentWriterTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Agent/AgentWriterTests.cs @@ -610,12 +610,6 @@ public TraceKeepState ProcessTrace(ref SpanCollection spans) public Task DisposeAsync() => Task.CompletedTask; public StatsAggregationKey BuildKey(Span span) => new(); - - public StatsAggregationKey BuildKey(Span span, out EncodedPeerTags utf8PeerTags) - { - utf8PeerTags = null; - return new(); - } } } } diff --git a/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs b/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs index a7686382c402..ab9a467a4dc9 100644 --- a/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs @@ -223,7 +223,7 @@ public async Task CreatesDistinctBuckets_TS003() foreach (var span in spans) { - var key = aggregator.BuildKey(span, out _); + var key = aggregator.BuildKey(span); buffer.Buckets.Should().ContainKey(key); var bucket = buffer.Buckets[key]; @@ -357,7 +357,7 @@ public async Task CollectsTopLevelSpans_TS005() buffer.Buckets.Should().HaveCount(2); - var serviceKey = aggregator.BuildKey(simpleSpan, out _); + var serviceKey = aggregator.BuildKey(simpleSpan); buffer.Buckets.Should().ContainKey(serviceKey); var serviceBucket = buffer.Buckets[serviceKey]; @@ -371,7 +371,7 @@ public async Task CollectsTopLevelSpans_TS005() serviceBucket.OkSummary.GetSum().Should().BeApproximately( expectedOkDuration, expectedOkDuration * serviceBucket.OkSummary.IndexMapping.RelativeAccuracy); - var httpClientServiceKey = aggregator.BuildKey(httpClientServiceSpan, out _); + var httpClientServiceKey = aggregator.BuildKey(httpClientServiceSpan); buffer.Buckets.Should().ContainKey(httpClientServiceKey); var httpClientServiceBucket = buffer.Buckets[httpClientServiceKey]; @@ -857,11 +857,11 @@ public async Task SpanKindEligibility_ServerAndClientSpansAreIncluded() // parent, server child, client child are included; internal and no-kind are not buffer.Buckets.Should().HaveCount(3); - buffer.Buckets.Should().ContainKey(aggregator.BuildKey(parentSpan, out _)); - buffer.Buckets.Should().ContainKey(aggregator.BuildKey(serverChildSpan, out _)); - buffer.Buckets.Should().ContainKey(aggregator.BuildKey(clientChildSpan, out _)); - buffer.Buckets.Should().NotContainKey(aggregator.BuildKey(internalChildSpan, out _)); - buffer.Buckets.Should().NotContainKey(aggregator.BuildKey(noKindChildSpan, out _)); + buffer.Buckets.Should().ContainKey(aggregator.BuildKey(parentSpan)); + buffer.Buckets.Should().ContainKey(aggregator.BuildKey(serverChildSpan)); + buffer.Buckets.Should().ContainKey(aggregator.BuildKey(clientChildSpan)); + buffer.Buckets.Should().NotContainKey(aggregator.BuildKey(internalChildSpan)); + buffer.Buckets.Should().NotContainKey(aggregator.BuildKey(noKindChildSpan)); } finally { @@ -924,8 +924,8 @@ public async Task IsTraceRootCreatesDistinctBuckets() // They have the same resource/operation/type but different IsTraceRoot → 2 buckets buffer.Buckets.Should().HaveCount(2); - var rootKey = aggregator.BuildKey(rootSpan, out _); - var entryKey = aggregator.BuildKey(entrySpan, out _); + var rootKey = aggregator.BuildKey(rootSpan); + var entryKey = aggregator.BuildKey(entrySpan); rootKey.IsTraceRoot.Should().BeTrue(); entryKey.IsTraceRoot.Should().BeFalse(); } @@ -1133,8 +1133,8 @@ public async Task SamplingWeightIsApplied() aggregator.Add(sampledSpan, unweightedSpan); var buffer = aggregator.CurrentBuffer; - var sampledKey = aggregator.BuildKey(sampledSpan, out _); - var unweightedKey = aggregator.BuildKey(unweightedSpan, out _); + var sampledKey = aggregator.BuildKey(sampledSpan); + var unweightedKey = aggregator.BuildKey(unweightedSpan); buffer.Buckets[sampledKey].Hits.Should().BeApproximately(10.0, 0.001); buffer.Buckets[unweightedKey].Hits.Should().BeApproximately(1.0, 0.001); @@ -1195,7 +1195,7 @@ public async Task PeerTagsHash_MatchesGoAgent_SingleTag(string spanKind) span.Tags.SetTag("peer.service", "remote-service"); var peerTagKeys = new List { "peer.service" }; - var key = aggregator.BuildKey(span, peerTagKeys, out _); + var key = aggregator.BuildKey(span, peerTagKeys); key.PeerTagsHash.Should().Be(3430395298086625290UL); } @@ -1217,7 +1217,7 @@ public async Task PeerTagsHash_MatchesGoAgent_MultipleTags() // Keys must be pre-sorted (matching agent behavior) var peerTagKeys = new List { "db.instance", "db.system", "peer.service" }; - var key = aggregator.BuildKey(span, peerTagKeys, out _); + var key = aggregator.BuildKey(span, peerTagKeys); key.PeerTagsHash.Should().Be(9894752672193411515UL); } @@ -1237,7 +1237,7 @@ public async Task PeerTagsHash_MatchesGoAgent_ConsumerMessagingTags() span.Tags.SetTag("messaging.system", "kafka"); var peerTagKeys = new List { "db.instance", "db.system", "messaging.destination", "messaging.system" }; - var key = aggregator.BuildKey(span, peerTagKeys, out _); + var key = aggregator.BuildKey(span, peerTagKeys); key.PeerTagsHash.Should().Be(0xf5eeb51fbe7929b4UL); } @@ -1257,7 +1257,7 @@ public async Task PeerTagsHash_MatchesGoAgent_EmptyTagsSkipped() span.Tags.SetTag("db.system", string.Empty); var peerTagKeys = new List { "db.instance", "db.system", "peer.service" }; - var key = aggregator.BuildKey(span, peerTagKeys, out _); + var key = aggregator.BuildKey(span, peerTagKeys); key.PeerTagsHash.Should().Be(3430395298086625290UL); } @@ -1273,10 +1273,9 @@ public async Task PeerTagsHash_NoMatchingTags_ReturnsZero() // No peer tags set on the span var peerTagKeys = new List { "peer.service" }; - var key = aggregator.BuildKey(span, peerTagKeys, out var utf8PeerTags); + var key = aggregator.BuildKey(span, peerTagKeys); key.PeerTagsHash.Should().Be(0UL); - utf8PeerTags.Should().BeNull(); } /// From d99fa347fdec6bc2a16085008ad50419577fc4ca Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Mon, 13 Apr 2026 11:02:44 +0100 Subject: [PATCH 4/8] More improvements to PeerTags --- .../DiscoveryService/DiscoveryService.cs | 2 +- .../Datadog.Trace/Agent/StatsAggregator.cs | 173 ++++++++++-------- .../Agent/StatsAggregatorTests.cs | 20 +- 3 files changed, 109 insertions(+), 86 deletions(-) diff --git a/tracer/src/Datadog.Trace/Agent/DiscoveryService/DiscoveryService.cs b/tracer/src/Datadog.Trace/Agent/DiscoveryService/DiscoveryService.cs index dda540566d3d..54b5844e35c4 100644 --- a/tracer/src/Datadog.Trace/Agent/DiscoveryService/DiscoveryService.cs +++ b/tracer/src/Datadog.Trace/Agent/DiscoveryService/DiscoveryService.cs @@ -348,7 +348,7 @@ private async Task ProcessDiscoveryResponse(IApiResponse response) var clientDropP0 = jObject["client_drop_p0s"]?.Value() ?? false; var spanMetaStructs = jObject["span_meta_structs"]?.Value() ?? false; var spanEvents = jObject["span_events"]?.Value() ?? false; - var peerTags = (jObject["peer_tags"] as JArray)?.Values().Where(x => !string.IsNullOrEmpty(x)).Distinct().OrderBy(x => x).ToList(); + var peerTags = (jObject["peer_tags"] as JArray)?.Values().ToList(); var obfuscationVersion = jObject["obfuscation_version"]?.Value() ?? 0; // Parse trace filter configuration diff --git a/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs b/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs index bfa293e0511c..a05ac80e11e3 100644 --- a/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs +++ b/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -28,6 +29,7 @@ internal sealed class StatsAggregator : IStatsAggregator private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(); private static readonly List EmptyPeerTags = []; private static readonly byte[] PeerTagSeparator = [0]; + private static readonly byte[] BaseServiceUtf8Prefix = EncodingHelpers.Utf8NoBom.GetBytes(Tags.BaseService + ":"); private readonly StatsBuffer[] _buffers; @@ -54,7 +56,7 @@ internal sealed class StatsAggregator : IStatsAggregator private int _tracerObfuscationVersion; private TraceFilter _traceFilter; - private List _peerTagKeys = []; + private List _peerTagKeys = []; internal StatsAggregator(IApi api, TracerSettings settings, IDiscoveryService discoveryService, bool isOtlp) { @@ -249,13 +251,15 @@ internal SpanCollection ObfuscateTrace(in SpanCollection trace) } public StatsAggregationKey BuildKey(Span span) - => BuildKey(span, Volatile.Read(ref _peerTagKeys)); + => BuildKey(span, Volatile.Read(ref _peerTagKeys), out _); /// /// Computes a for the given span, including the peer tags hash. - /// Peer tag selection logic is mirrored in for the cold path. + /// The carries context to + /// so the cold path can skip re-deriving spanKind/baseService and pre-allocate the result list. /// - internal StatsAggregationKey BuildKey(Span span, List peerTagKeys) + [TestingAndPrivateOnly] + internal StatsAggregationKey BuildKey(Span span, List peerTagKeys, out PeerTagResults peerTagResults) { var rawHttpStatusCode = span.GetTag(Tags.HttpStatusCode); @@ -297,16 +301,17 @@ internal StatsAggregationKey BuildKey(Span span, List peerTagKeys) ulong peerTagsHash; if ((string.IsNullOrEmpty(spanKind) || spanKind is SpanKinds.Internal) && span.GetTag(Tags.BaseService) is { Length: > 0 } baseService) { - peerTagsHash = HashTag(Tags.BaseService, baseService, FnvHash64.Version.V1A); + peerTagsHash = HashTag(BaseServiceUtf8Prefix, baseService, FnvHash64.Version.V1A); + peerTagResults = new PeerTagResults { BaseService = baseService }; } else if (spanKind is SpanKinds.Client or SpanKinds.Server or SpanKinds.Producer or SpanKinds.Consumer) { // Hash should be generated as TAGNAME:TAGVALUE, in sorted order (peerTagKeys is pre-sorted). - peerTagsHash = 0; - bool firstTag = true; - foreach (var tagKey in peerTagKeys) + ulong? previousHash = null; + var peerTagCount = 0; + foreach (var peerTag in peerTagKeys) { - var tagValue = span.GetTag(tagKey); + var tagValue = span.GetTag(peerTag.Name); if (string.IsNullOrEmpty(tagValue)) { continue; @@ -314,19 +319,23 @@ internal StatsAggregationKey BuildKey(Span span, List peerTagKeys) tagValue = IpAddressObfuscationUtil.QuantizePeerIpAddresses(tagValue); - if (!firstTag) + if (previousHash.HasValue) { // add the separator between tags - peerTagsHash = FnvHash64.GenerateHash(PeerTagSeparator, FnvHash64.Version.V1A, peerTagsHash); + previousHash = FnvHash64.GenerateHash(PeerTagSeparator, FnvHash64.Version.V1A, previousHash.Value); } - peerTagsHash = HashTag(tagKey, tagValue, FnvHash64.Version.V1A, firstTag ? null : peerTagsHash); - firstTag = false; + previousHash = HashTag(peerTag.Utf8Prefix, tagValue, FnvHash64.Version.V1A, previousHash); + peerTagCount++; } + + peerTagResults = new PeerTagResults { PeerTagCount = peerTagCount }; + peerTagsHash = previousHash ?? 0; } else { peerTagsHash = 0; + peerTagResults = default; } // When submitting trace metrics over OTLP, we must create inidividual timeseries @@ -352,90 +361,77 @@ internal StatsAggregationKey BuildKey(Span span, List peerTagKeys) } /// - /// Encodes "tagKey:tagValue" to UTF-8 and computes its FNV hash. - /// Uses stackalloc on .NET Core, ArrayPool on .NET Framework. + /// Hashes "keyPrefix + tagValue" using FNV-64. + /// The is a pre-encoded UTF-8 byte array (e.g. "tagKey:") and is + /// hashed directly. Only the needs UTF-8 encoding at call time. /// #if NETCOREAPP [System.Runtime.CompilerServices.SkipLocalsInit] #endif - private static ulong HashTag(string tagKey, string tagValue, FnvHash64.Version version, ulong? initialHash = null) + private static ulong HashTag(byte[] keyPrefix, string tagValue, FnvHash64.Version version, ulong? initialHash = null) { - const int MaxStackLimit = 256; - var maxByteCount = EncodingHelpers.Utf8NoBom.GetMaxByteCount(tagKey.Length + 1 + tagValue.Length); + // Hash the pre-encoded key prefix (e.g. "peer.service:") directly — no encoding needed + var hash = initialHash is { } h + ? FnvHash64.GenerateHash(keyPrefix, version, h) + : FnvHash64.GenerateHash(keyPrefix, version); + // Now encode and hash just the tag value + var maxByteCount = EncodingHelpers.Utf8NoBom.GetMaxByteCount(tagValue.Length); #if NETCOREAPP - if (maxByteCount <= MaxStackLimit) + const int maxStackLimit = 256; + + if (maxByteCount <= maxStackLimit) { - Span buffer = stackalloc byte[MaxStackLimit]; - int written = EncodingHelpers.Utf8NoBom.GetBytes(tagKey, buffer); - buffer[written++] = (byte)':'; - written += EncodingHelpers.Utf8NoBom.GetBytes(tagValue, buffer.Slice(written)); - return initialHash is { } h - ? FnvHash64.GenerateHash(buffer.Slice(0, written), version, h) - : FnvHash64.GenerateHash(buffer.Slice(0, written), version); - } -#else - if (maxByteCount <= MaxStackLimit) - { - var buffer = ArrayPool.Shared.Rent(MaxStackLimit); - try - { - int written = EncodingHelpers.Utf8NoBom.GetBytes(tagKey, 0, tagKey.Length, buffer, 0); - buffer[written++] = (byte)':'; - written += EncodingHelpers.Utf8NoBom.GetBytes(tagValue, 0, tagValue.Length, buffer, written); - var segment = new ArraySegment(buffer, 0, written); - return initialHash is { } h - ? FnvHash64.GenerateHash(segment, version, h) - : FnvHash64.GenerateHash(segment, version); - } - finally - { - ArrayPool.Shared.Return(buffer); - } + Span buffer = stackalloc byte[maxStackLimit]; + var written = EncodingHelpers.Utf8NoBom.GetBytes(tagValue, buffer); + return FnvHash64.GenerateHash(buffer.Slice(0, written), version, hash); } #endif - // Fallback for oversized tags: heap-allocate - var bytes = EncodingHelpers.Utf8NoBom.GetBytes($"{tagKey}:{tagValue}"); - return initialHash is { } ih - ? FnvHash64.GenerateHash(bytes, version, ih) - : FnvHash64.GenerateHash(bytes, version); + var rented = ArrayPool.Shared.Rent(maxByteCount); + try + { + var written = EncodingHelpers.Utf8NoBom.GetBytes(tagValue, charIndex: 0, charCount: tagValue.Length, rented, byteIndex: 0); + return FnvHash64.GenerateHash(new ArraySegment(rented, 0, written), version, hash); + } + finally + { + ArrayPool.Shared.Return(rented); + } } /// /// Encodes the peer tags for a span into a of UTF-8 byte arrays. /// Called only on the cold path (new bucket creation). - /// Tag selection logic is mirrored in . + /// Uses from + /// to skip re-deriving spanKind/baseService and to pre-allocate the result list. /// - internal static List GetEncodedPeerTags(Span span, List peerTagKeys) + internal static List GetEncodedPeerTags(Span span, List peerTagKeys, in PeerTagResults results) { - var spanKind = (span.Tags is InstrumentationTags t ? t.SpanKind : span.GetTag(Tags.SpanKind)) ?? string.Empty; - - if ((string.IsNullOrEmpty(spanKind) || spanKind is SpanKinds.Internal) && span.GetTag(Tags.BaseService) is { Length: > 0 } baseService) + if (results.BaseService is not null) { - return [EncodingHelpers.Utf8NoBom.GetBytes($"{Tags.BaseService}:{baseService}")]; + return [EncodingHelpers.Utf8NoBom.GetBytes($"{Tags.BaseService}:{results.BaseService}")]; } - if (spanKind is not (SpanKinds.Client or SpanKinds.Server or SpanKinds.Producer or SpanKinds.Consumer)) + if (results.PeerTagCount == 0) { return EmptyPeerTags; } - List result = null; - foreach (var tagKey in peerTagKeys) + var result = new List(results.PeerTagCount); + foreach (var peerTag in peerTagKeys) { - var tagValue = span.GetTag(tagKey); + var tagValue = span.GetTag(peerTag.Name); if (string.IsNullOrEmpty(tagValue)) { continue; } tagValue = IpAddressObfuscationUtil.QuantizePeerIpAddresses(tagValue); - result ??= new(); - result.Add(EncodingHelpers.Utf8NoBom.GetBytes($"{tagKey}:{tagValue}")); + result.Add(EncodingHelpers.Utf8NoBom.GetBytes($"{peerTag.Name}:{tagValue}")); } - return result ?? EmptyPeerTags; + return result; } internal async Task Flush() @@ -518,12 +514,12 @@ private void AddToBuffer(Span span) var buffer = CurrentBuffer; var peerTagKeys = Volatile.Read(ref _peerTagKeys); - var key = BuildKey(span, peerTagKeys); + var key = BuildKey(span, peerTagKeys, out var peerTagResults); if (!buffer.Buckets.TryGetValue(key, out var bucket)) { // Cold path: encode the peer tags for storage in the new bucket - bucket = new StatsBucket(key, GetEncodedPeerTags(span, peerTagKeys)); + bucket = new StatsBucket(key, GetEncodedPeerTags(span, peerTagKeys, in peerTagResults)); buffer.Buckets.Add(key, bucket); } @@ -567,9 +563,32 @@ private void HandleConfigUpdate(AgentConfiguration config) || parsedVersion.Major >= 8 || (parsedVersion.Major == 7 && parsedVersion.Minor >= 65)); - if (config.PeerTags is not null) + if (CanComputeStats.Value) { - Interlocked.Exchange(ref _peerTagKeys, config.PeerTags); + Log.Debug("Stats computation has been enabled."); + } + else + { + Log.Warning("Stats computation disabled because the detected agent does not support this feature."); + // early return, because there's no point doing all the extra work if stats isn't enabled anyway + return; + } + + if (config.PeerTags is { Count: > 0 }) + { + // Sort, deduplicate, and pre-compute the UTF-8 key prefixes so that + // BuildKey can hash without per-call string encoding. + var precomputed = new List(config.PeerTags.Count); + foreach (var tag in config.PeerTags.Where(x => !string.IsNullOrEmpty(x)).Distinct().OrderBy(x => x)) + { + precomputed.Add(new PeerTagKey(tag)); + } + + Interlocked.Exchange(ref _peerTagKeys, precomputed); + } + else + { + Interlocked.Exchange(ref _peerTagKeys, []); } // Update trace filter from agent configuration @@ -586,15 +605,19 @@ private void HandleConfigUpdate(AgentConfiguration config) const int tracerObfuscationVersion = 1; var agentVersion = config.ObfuscationVersion; Volatile.Write(ref _tracerObfuscationVersion, agentVersion > 0 && agentVersion <= tracerObfuscationVersion ? tracerObfuscationVersion : 0); + } - if (CanComputeStats.Value) - { - Log.Debug("Stats computation has been enabled."); - } - else - { - Log.Warning("Stats computation disabled because the detected agent does not support this feature."); - } + internal readonly struct PeerTagKey(string name) + { + public readonly string Name = name; + public readonly byte[] Utf8Prefix = EncodingHelpers.Utf8NoBom.GetBytes(name + ":"); + } + + internal readonly struct PeerTagResults + { + public int PeerTagCount { get; init; } + + public string BaseService { get; init; } } } } diff --git a/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs b/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs index ab9a467a4dc9..0db33d590fa7 100644 --- a/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs @@ -1194,8 +1194,8 @@ public async Task PeerTagsHash_MatchesGoAgent_SingleTag(string spanKind) span.SetTag(Tags.SpanKind, spanKind); span.Tags.SetTag("peer.service", "remote-service"); - var peerTagKeys = new List { "peer.service" }; - var key = aggregator.BuildKey(span, peerTagKeys); + List peerTagKeys = [new("peer.service")]; + var key = aggregator.BuildKey(span, peerTagKeys, out _); key.PeerTagsHash.Should().Be(3430395298086625290UL); } @@ -1216,8 +1216,8 @@ public async Task PeerTagsHash_MatchesGoAgent_MultipleTags() span.Tags.SetTag("db.system", "postgres"); // Keys must be pre-sorted (matching agent behavior) - var peerTagKeys = new List { "db.instance", "db.system", "peer.service" }; - var key = aggregator.BuildKey(span, peerTagKeys); + List peerTagKeys = [new("db.instance"), new("db.system"), new("peer.service")]; + var key = aggregator.BuildKey(span, peerTagKeys, out _); key.PeerTagsHash.Should().Be(9894752672193411515UL); } @@ -1236,8 +1236,8 @@ public async Task PeerTagsHash_MatchesGoAgent_ConsumerMessagingTags() span.Tags.SetTag("messaging.destination", "topic-foo"); span.Tags.SetTag("messaging.system", "kafka"); - var peerTagKeys = new List { "db.instance", "db.system", "messaging.destination", "messaging.system" }; - var key = aggregator.BuildKey(span, peerTagKeys); + List peerTagKeys = [new("db.instance"), new("db.system"), new("messaging.destination"), new("messaging.system")]; + var key = aggregator.BuildKey(span, peerTagKeys, out _); key.PeerTagsHash.Should().Be(0xf5eeb51fbe7929b4UL); } @@ -1256,8 +1256,8 @@ public async Task PeerTagsHash_MatchesGoAgent_EmptyTagsSkipped() span.Tags.SetTag("db.instance", string.Empty); span.Tags.SetTag("db.system", string.Empty); - var peerTagKeys = new List { "db.instance", "db.system", "peer.service" }; - var key = aggregator.BuildKey(span, peerTagKeys); + List peerTagKeys = [new("db.instance"), new("db.system"), new("peer.service")]; + var key = aggregator.BuildKey(span, peerTagKeys, out _); key.PeerTagsHash.Should().Be(3430395298086625290UL); } @@ -1272,8 +1272,8 @@ public async Task PeerTagsHash_NoMatchingTags_ReturnsZero() span.SetTag(Tags.SpanKind, SpanKinds.Client); // No peer tags set on the span - var peerTagKeys = new List { "peer.service" }; - var key = aggregator.BuildKey(span, peerTagKeys); + List peerTagKeys = [new("peer.service")]; + var key = aggregator.BuildKey(span, peerTagKeys, out _); key.PeerTagsHash.Should().Be(0UL); } From 0a1214004be5fb45a487f6e1221ac435fc45d49e Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Mon, 13 Apr 2026 11:16:00 +0100 Subject: [PATCH 5/8] Update implementation --- tracer/src/Datadog.Trace/Agent/StatsAggregator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs b/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs index a05ac80e11e3..8369efa41dcc 100644 --- a/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs +++ b/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs @@ -392,7 +392,7 @@ private static ulong HashTag(byte[] keyPrefix, string tagValue, FnvHash64.Versio try { var written = EncodingHelpers.Utf8NoBom.GetBytes(tagValue, charIndex: 0, charCount: tagValue.Length, rented, byteIndex: 0); - return FnvHash64.GenerateHash(new ArraySegment(rented, 0, written), version, hash); + return FnvHash64.GenerateHash(rented, 0, written, version, hash); } finally { From 5dceaf4ff558dc2c46cd0cdcfe373269df8e2619 Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Mon, 13 Apr 2026 12:19:22 +0100 Subject: [PATCH 6/8] Add some unit tests --- .../Agent/StatsAggregatorTests.cs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs b/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs index 0db33d590fa7..bd29c514be58 100644 --- a/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs @@ -17,6 +17,7 @@ using Datadog.Trace.Sampling; using Datadog.Trace.TestHelpers.TestTracer; using Datadog.Trace.Tests.Util; +using Datadog.Trace.Util; using FluentAssertions; using Moq; using Xunit; @@ -1262,6 +1263,56 @@ public async Task PeerTagsHash_MatchesGoAgent_EmptyTagsSkipped() key.PeerTagsHash.Should().Be(3430395298086625290UL); } + [Fact] + public async Task PeerTagsHash_MatchesEncodedPeerTags_MultipleTags() + { + // Verify that the fast-path hash from BuildKey matches + // what you'd get by hashing the GetEncodedPeerTags output directly + var start = DateTimeOffset.UtcNow; + await using var aggregator = new StatsAggregator(Mock.Of(), GetSettings(), Mock.Of(), isOtlp: false); + + var span = CreateTopLevelSpan(start, "svc"); + span.SetTag(Tags.SpanKind, SpanKinds.Client); + span.Tags.SetTag("peer.service", "remote-service"); + span.Tags.SetTag("db.instance", "i-1234"); + span.Tags.SetTag("db.system", "postgres"); + + List peerTagKeys = [new("db.instance"), new("db.system"), new("peer.service")]; + var key = aggregator.BuildKey(span, peerTagKeys, out var peerTagResults); + var encodedTags = StatsAggregator.GetEncodedPeerTags(span, peerTagKeys, in peerTagResults); + + // Hash the encoded tags the same way the Go agent does: + // FNV-1a of each tag's bytes, chained with a [0] separator + var expectedHash = FnvHash64.GenerateHash(encodedTags[0], FnvHash64.Version.V1A); + for (var i = 1; i < encodedTags.Count; i++) + { + expectedHash = FnvHash64.GenerateHash(new byte[] { 0 }, FnvHash64.Version.V1A, expectedHash); + expectedHash = FnvHash64.GenerateHash(encodedTags[i], FnvHash64.Version.V1A, expectedHash); + } + + key.PeerTagsHash.Should().Be(expectedHash); + } + + [Fact] + public async Task PeerTagsHash_MatchesEncodedPeerTags_BaseService() + { + // Verify that the fast-path hash from BuildKey matches the encoded base service tag + var start = DateTimeOffset.UtcNow; + await using var aggregator = new StatsAggregator(Mock.Of(), GetSettings(), Mock.Of(), isOtlp: false); + + var span = CreateTopLevelSpan(start, "svc"); + span.Tags.SetTag(Tags.BaseService, "my-base-service"); + + List peerTagKeys = [new("peer.service")]; + var key = aggregator.BuildKey(span, peerTagKeys, out var peerTagResults); + var encodedTags = StatsAggregator.GetEncodedPeerTags(span, peerTagKeys, in peerTagResults); + + encodedTags.Should().HaveCount(1); + var expectedHash = FnvHash64.GenerateHash(encodedTags[0], FnvHash64.Version.V1A); + + key.PeerTagsHash.Should().Be(expectedHash); + } + [Fact] public async Task PeerTagsHash_NoMatchingTags_ReturnsZero() { From cfefb708c60efd945e19ae8a91e6f1d947763751 Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Thu, 16 Apr 2026 08:46:04 +0100 Subject: [PATCH 7/8] PR Feedback --- tracer/src/Datadog.Trace/Agent/StatsAggregator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs b/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs index 8369efa41dcc..5f19a6b18504 100644 --- a/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs +++ b/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs @@ -565,7 +565,7 @@ private void HandleConfigUpdate(AgentConfiguration config) if (CanComputeStats.Value) { - Log.Debug("Stats computation has been enabled."); + Log.Debug("Stats computation enabled."); } else { From 7817c4847c5b1671d585a269d5d2dc27f12164b7 Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Thu, 16 Apr 2026 08:49:35 +0100 Subject: [PATCH 8/8] Rename IgnoreResources to IgnoreResourcesRegex --- .../Agent/DiscoveryService/AgentTraceFilterConfig.cs | 4 ++-- .../Datadog.Trace/Agent/TraceSamplers/TraceFilter.cs | 10 +++++----- .../Agent/StatsAggregatorTests.cs | 12 ++++++------ .../Datadog.Trace.Tests/Agent/TraceFilterTests.cs | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/tracer/src/Datadog.Trace/Agent/DiscoveryService/AgentTraceFilterConfig.cs b/tracer/src/Datadog.Trace/Agent/DiscoveryService/AgentTraceFilterConfig.cs index 7730dca828e9..2436203f368a 100644 --- a/tracer/src/Datadog.Trace/Agent/DiscoveryService/AgentTraceFilterConfig.cs +++ b/tracer/src/Datadog.Trace/Agent/DiscoveryService/AgentTraceFilterConfig.cs @@ -18,7 +18,7 @@ internal sealed record AgentTraceFilterConfig( List? FilterTagsReject, List? FilterTagsRegexRequire, List? FilterTagsRegexReject, - List? IgnoreResources) + List? IgnoreResourcesRegex) { public static readonly AgentTraceFilterConfig Empty = new(null, null, null, null, null); @@ -27,5 +27,5 @@ internal sealed record AgentTraceFilterConfig( FilterTagsReject is { Count: > 0 } || FilterTagsRegexRequire is { Count: > 0 } || FilterTagsRegexReject is { Count: > 0 } || - IgnoreResources is { Count: > 0 }; + IgnoreResourcesRegex is { Count: > 0 }; } diff --git a/tracer/src/Datadog.Trace/Agent/TraceSamplers/TraceFilter.cs b/tracer/src/Datadog.Trace/Agent/TraceSamplers/TraceFilter.cs index 59337471f04e..a4ebb5d0ae96 100644 --- a/tracer/src/Datadog.Trace/Agent/TraceSamplers/TraceFilter.cs +++ b/tracer/src/Datadog.Trace/Agent/TraceSamplers/TraceFilter.cs @@ -26,7 +26,7 @@ internal sealed class TraceFilter private readonly List> _filterTagKeyValuesReject; private readonly List _filterTagsRegexRequire; private readonly List _filterTagsRegexReject; - private readonly List _ignoreResources; + private readonly List _ignoreResourcesRegex; private readonly bool _hasFilters; public TraceFilter(AgentTraceFilterConfig config) @@ -35,7 +35,7 @@ public TraceFilter(AgentTraceFilterConfig config) BuildFilterTags(config.FilterTagsReject, out _filterTagKeysReject, out _filterTagKeyValuesReject); _filterTagsRegexRequire = CompileTagFilters(config.FilterTagsRegexRequire); _filterTagsRegexReject = CompileTagFilters(config.FilterTagsRegexReject); - _ignoreResources = CompilePatterns(config.IgnoreResources); + _ignoreResourcesRegex = CompilePatterns(config.IgnoreResourcesRegex); // Short circuit because these are _relatively_ rare, so we can avoid all the work if needs be _hasFilters = _filterTagKeysRequire.Count > 0 @@ -44,7 +44,7 @@ public TraceFilter(AgentTraceFilterConfig config) || _filterTagKeyValuesReject.Count > 0 || _filterTagsRegexRequire.Count > 0 || _filterTagsRegexReject.Count > 0 - || _ignoreResources.Count > 0; + || _ignoreResourcesRegex.Count > 0; static void BuildFilterTags(List? filters, out List keyFilters, out List> keyValueFilters) { @@ -82,9 +82,9 @@ public bool ShouldKeepTrace(Span rootSpan) } // 1. Resource filtering: reject if resource matches any ignore_resources pattern - if (_ignoreResources.Count > 0 && !string.IsNullOrEmpty(rootSpan.ResourceName)) + if (_ignoreResourcesRegex.Count > 0 && !string.IsNullOrEmpty(rootSpan.ResourceName)) { - foreach (var pattern in _ignoreResources) + foreach (var pattern in _ignoreResourcesRegex) { if (pattern.IsMatch(rootSpan.ResourceName)) { diff --git a/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs b/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs index bd29c514be58..54453877fb77 100644 --- a/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Agent/StatsAggregatorTests.cs @@ -569,7 +569,7 @@ public async Task ProcessTrace_WhenFilterRejects_ReturnsRejected() FilterTagsReject: ["env:production"], FilterTagsRegexRequire: null, FilterTagsRegexReject: null, - IgnoreResources: null); + IgnoreResourcesRegex: null); var discoveryService = new StubDiscoveryService(traceFilterConfig: filterConfig); await using var aggregator = new StatsAggregator(Mock.Of(), GetSettings(), discoveryService, isOtlp: false); @@ -595,7 +595,7 @@ public async Task ProcessTrace_WhenFilterKeepsAndSampled_ReturnsAggregateAndExpo FilterTagsReject: ["env:staging"], FilterTagsRegexRequire: null, FilterTagsRegexReject: null, - IgnoreResources: null); + IgnoreResourcesRegex: null); var discoveryService = new StubDiscoveryService(traceFilterConfig: filterConfig); await using var aggregator = new StatsAggregator(Mock.Of(), GetSettings(), discoveryService, isOtlp: false); @@ -622,7 +622,7 @@ public async Task ProcessTrace_WhenFilterKeepsAndNotSampled_ReturnsAggregateOnly FilterTagsReject: ["env:staging"], FilterTagsRegexRequire: null, FilterTagsRegexReject: null, - IgnoreResources: null); + IgnoreResourcesRegex: null); var discoveryService = new StubDiscoveryService(traceFilterConfig: filterConfig); await using var aggregator = new StatsAggregator(Mock.Of(), GetSettings(), discoveryService, isOtlp: false); @@ -663,7 +663,7 @@ public async Task ShouldFilterTrace_WhenFilterKeepsTrace_ReturnsFalse() FilterTagsReject: ["env:staging"], FilterTagsRegexRequire: null, FilterTagsRegexReject: null, - IgnoreResources: null); + IgnoreResourcesRegex: null); var discoveryService = new StubDiscoveryService(traceFilterConfig: filterConfig); await using var aggregator = new StatsAggregator(Mock.Of(), GetSettings(), discoveryService, isOtlp: false); @@ -688,7 +688,7 @@ public async Task ShouldFilterTrace_WhenFilterRejectsTrace_ReturnsTrue() FilterTagsReject: ["env:production"], FilterTagsRegexRequire: null, FilterTagsRegexReject: null, - IgnoreResources: null); + IgnoreResourcesRegex: null); var discoveryService = new StubDiscoveryService(traceFilterConfig: filterConfig); await using var aggregator = new StatsAggregator(Mock.Of(), GetSettings(), discoveryService, isOtlp: false); @@ -712,7 +712,7 @@ public async Task ShouldFilterTrace_WhenIgnoreResourceMatches_ReturnsTrue() FilterTagsReject: null, FilterTagsRegexRequire: null, FilterTagsRegexReject: null, - IgnoreResources: ["^GET /health"]); + IgnoreResourcesRegex: ["^GET /health"]); var discoveryService = new StubDiscoveryService(traceFilterConfig: filterConfig); await using var aggregator = new StatsAggregator(Mock.Of(), GetSettings(), discoveryService, isOtlp: false); diff --git a/tracer/test/Datadog.Trace.Tests/Agent/TraceFilterTests.cs b/tracer/test/Datadog.Trace.Tests/Agent/TraceFilterTests.cs index 07601b1d26ef..59efe05b8f31 100644 --- a/tracer/test/Datadog.Trace.Tests/Agent/TraceFilterTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Agent/TraceFilterTests.cs @@ -114,7 +114,7 @@ public void FilterOrder_ResourceThenRejectThenRequire() FilterTagsReject: null, FilterTagsRegexRequire: null, FilterTagsRegexReject: null, - IgnoreResources: ["GET /healthcheck"]); + IgnoreResourcesRegex: ["GET /healthcheck"]); var filter = new TraceFilter(config); // Even with correct required tag, resource reject wins