Skip to content

Commit 1c2a386

Browse files
Add srv_src field to client stats payload (#8339)
## Summary of changes Add `srv_src` field to the client stats payload, propagating the service name source alongside trace stats. ## Reason for change Per the [service name source RFC](https://docs.google.com/document/d/11OnbVYMDK-c5D-_V4QfOvL0Pc0z5oFQFGY3xSI-W7xk/edit?tab=t.0#heading=h.uoivahvmuvbg), the source must be propagated in both trace and stats payloads. The trace payload (`_dd.svc_src` span meta) was added in PR #8302. This completes the implementation by adding `srv_src` at the stats bucket level. ## Implementation details - **`StatsAggregationKey.cs`**: Added `ServiceSource` field. It participates in equality and hash code, so stats with different sources aggregate into distinct buckets. - **`StatsAggregator.cs`**: `BuildKey()` now reads `span.Context.ServiceNameSource` and passes it to the aggregation key. - **`StatsBuffer.cs`**: `SerializeBucket()` conditionally serializes `srv_src` when `ServiceSource` is non-null (dynamic map size: 13 with source, 12 without). ## Test coverage - `KeyEquality_WithServiceSource`: Verifies that keys with different sources are not equal. - `Serialization_WithServiceSource`: Verifies `srv_src` is serialized when present and absent when null. - Existing `StatsBufferTests` and `StatsAggregatorTests` updated and passing. ## Other details [APMLP-1015](https://datadoghq.atlassian.net/browse/APMLP-1015) Related agent PR: DataDog/datadog-agent#45982 Reference Java implementation: DataDog/dd-trace-java#10653 [APMLP-1015]: https://datadoghq.atlassian.net/browse/APMLP-1015?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent d5d94dd commit 1c2a386

5 files changed

Lines changed: 78 additions & 9 deletions

File tree

tracer/src/Datadog.Trace/Agent/StatsAggregationKey.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ namespace Datadog.Trace.Agent
1313
public readonly string Service;
1414
public readonly string OperationName;
1515
public readonly string Type;
16+
public readonly string ServiceSource;
1617
public readonly int HttpStatusCode;
1718
public readonly bool IsSyntheticsRequest;
1819
public readonly bool IsError;
@@ -25,13 +26,15 @@ public StatsAggregationKey(
2526
string service,
2627
string operationName,
2728
string type,
29+
string serviceSource,
2830
int httpStatusCode,
2931
bool isSyntheticsRequest)
3032
{
3133
Resource = resource;
3234
Service = service;
3335
OperationName = operationName;
3436
Type = type;
37+
ServiceSource = serviceSource;
3538
HttpStatusCode = httpStatusCode;
3639
IsSyntheticsRequest = isSyntheticsRequest;
3740
IsError = false;
@@ -45,6 +48,7 @@ public StatsAggregationKey(
4548
string service,
4649
string operationName,
4750
string type,
51+
string serviceSource,
4852
int httpStatusCode,
4953
bool isSyntheticsRequest,
5054
bool isError,
@@ -54,6 +58,7 @@ public StatsAggregationKey(
5458
Service = service;
5559
OperationName = operationName;
5660
Type = type;
61+
ServiceSource = serviceSource;
5762
HttpStatusCode = httpStatusCode;
5863
IsSyntheticsRequest = isSyntheticsRequest;
5964
IsError = isError;
@@ -67,6 +72,7 @@ public bool Equals(StatsAggregationKey other)
6772
&& Service == other.Service
6873
&& OperationName == other.OperationName
6974
&& Type == other.Type
75+
&& ServiceSource == other.ServiceSource
7076
&& HttpStatusCode == other.HttpStatusCode
7177
&& IsSyntheticsRequest == other.IsSyntheticsRequest
7278
&& IsError == other.IsError
@@ -86,6 +92,7 @@ public override int GetHashCode()
8692
hashCode = (hashCode * 397) ^ (Service != null ? Service.GetHashCode() : 0);
8793
hashCode = (hashCode * 397) ^ (OperationName != null ? OperationName.GetHashCode() : 0);
8894
hashCode = (hashCode * 397) ^ (Type != null ? Type.GetHashCode() : 0);
95+
hashCode = (hashCode * 397) ^ (ServiceSource != null ? ServiceSource.GetHashCode() : 0);
8996
hashCode = (hashCode * 397) ^ HttpStatusCode;
9097
hashCode = (hashCode * 397) ^ IsSyntheticsRequest.GetHashCode();
9198
hashCode = (hashCode * 397) ^ IsError.GetHashCode();

tracer/src/Datadog.Trace/Agent/StatsAggregator.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,16 @@ internal static StatsAggregationKey BuildKey(Span span, bool isOtlp = false)
191191
httpStatusCode = 0;
192192
}
193193

194+
// Normalize service source to match trace serialization behavior:
195+
// clear the source when service name equals the default, unless it's
196+
// a configuration-driven override (opt.*).
197+
var serviceNameSource = span.Context.ServiceNameSource;
198+
var serviceNameEqualsDefault = string.Equals(span.ServiceName, span.Context.TraceContext?.Tracer?.DefaultServiceName, StringComparison.OrdinalIgnoreCase);
199+
if (serviceNameEqualsDefault && serviceNameSource?.StartsWith("opt.", StringComparison.Ordinal) != true)
200+
{
201+
serviceNameSource = null;
202+
}
203+
194204
// When submitting trace metrics over OTLP, we must create inidividual timeseries
195205
// timeseries for each unique set of attributes, including the Error and IsTopLevel attributes.
196206
// As a result, we must create distinct Aggregation keys (and consequently, unique stats) by these attributes.
@@ -200,6 +210,7 @@ internal static StatsAggregationKey BuildKey(Span span, bool isOtlp = false)
200210
span.ServiceName,
201211
span.OperationName,
202212
span.Type,
213+
serviceNameSource,
203214
httpStatusCode,
204215
span.Context.Origin == "synthetics",
205216
isOtlp ? span.Error : false,

tracer/src/Datadog.Trace/Agent/StatsBuffer.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,9 @@ public void Serialize(Stream stream, long bucketDuration)
107107

108108
private static void SerializeBucket(Stream stream, StatsBucket bucket)
109109
{
110-
MessagePackBinary.WriteMapHeader(stream, 12);
110+
var hasServiceSource = !string.IsNullOrEmpty(bucket.Key.ServiceSource);
111+
var mapSize = hasServiceSource ? 13 : 12;
112+
MessagePackBinary.WriteMapHeader(stream, mapSize);
111113

112114
MessagePackBinary.WriteString(stream, "Service");
113115
MessagePackBinary.WriteString(stream, bucket.Key.Service ?? string.Empty);
@@ -127,6 +129,12 @@ private static void SerializeBucket(Stream stream, StatsBucket bucket)
127129
MessagePackBinary.WriteString(stream, "Type");
128130
MessagePackBinary.WriteString(stream, bucket.Key.Type ?? string.Empty);
129131

132+
if (hasServiceSource)
133+
{
134+
MessagePackBinary.WriteString(stream, "srv_src");
135+
MessagePackBinary.WriteString(stream, bucket.Key.ServiceSource);
136+
}
137+
130138
MessagePackBinary.WriteString(stream, "Hits");
131139
MessagePackBinary.WriteInt64(stream, bucket.Hits);
132140

tracer/test/Datadog.Trace.TestHelpers/Stats/MockClientGroupedStats.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,7 @@ public class MockClientGroupedStats
4848

4949
[Key("TopLevelHits")]
5050
public long TopLevelHits { get; set; }
51+
52+
[Key("srv_src")]
53+
public string ServiceSource { get; set; }
5154
}

tracer/test/Datadog.Trace.Tests/Agent/StatsBufferTests.cs

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,23 @@ public class StatsBufferTests
2020
[Fact]
2121
public void KeyEquality()
2222
{
23-
var key1 = new StatsAggregationKey("resource1", "service1", "operation1", "type1", 1, false);
24-
var key2 = new StatsAggregationKey("resource1", "service1", "operation1", "type1", 1, false);
23+
var key1 = new StatsAggregationKey("resource1", "service1", "operation1", "type1", null, 1, false);
24+
var key2 = new StatsAggregationKey("resource1", "service1", "operation1", "type1", null, 1, false);
2525

2626
key1.Should().Be(key2);
2727
}
2828

29+
[Fact]
30+
public void KeyEquality_WithServiceSource()
31+
{
32+
var key1 = new StatsAggregationKey("resource1", "service1", "operation1", "type1", "m", 1, false);
33+
var key2 = new StatsAggregationKey("resource1", "service1", "operation1", "type1", "m", 1, false);
34+
var key3 = new StatsAggregationKey("resource1", "service1", "operation1", "type1", null, 1, false);
35+
36+
key1.Should().Be(key2);
37+
key1.Should().NotBe(key3);
38+
}
39+
2940
[Theory]
3041
[CombinatorialData]
3142
public void Serialization(bool propagateProcessTags, bool setServiceName)
@@ -55,9 +66,9 @@ public void Serialization(bool propagateProcessTags, bool setServiceName)
5566

5667
var buffer = new StatsBuffer(payload);
5768

58-
var key1 = new StatsAggregationKey("resource1", "service1", "operation1", "type1", 1, true);
59-
var key2 = new StatsAggregationKey("resource2", "service2", "operation2", "type2", 2, false);
60-
var key3 = new StatsAggregationKey("resource3", "service3", "operation3", "type3", 2, true);
69+
var key1 = new StatsAggregationKey("resource1", "service1", "operation1", "type1", null, 1, true);
70+
var key2 = new StatsAggregationKey("resource2", "service2", "operation2", "type2", null, 2, false);
71+
var key3 = new StatsAggregationKey("resource3", "service3", "operation3", "type3", null, 2, true);
6172

6273
var statsBucket1 = new StatsBucket(key1) { Duration = 1, Errors = 11, Hits = 111, TopLevelHits = 10 };
6374
var statsBucket2 = new StatsBucket(key2) { Duration = 2, Errors = 22, Hits = 222, TopLevelHits = 20 };
@@ -103,13 +114,41 @@ public void Serialization(bool propagateProcessTags, bool setServiceName)
103114
AssertStatsGroup(bucket.Stats.Single(g => g.Name == key2.OperationName), key2, statsBucket2);
104115
}
105116

117+
[Fact]
118+
public void Serialization_WithServiceSource()
119+
{
120+
var buffer = new StatsBuffer(new ClientStatsPayload(MutableSettings.CreateForTesting(new(), [])) { HostName = "host" });
121+
122+
var keyWithSource = new StatsAggregationKey("resource1", "service1", "operation1", "type1", "m", 1, false);
123+
var keyWithoutSource = new StatsAggregationKey("resource2", "service2", "operation2", "type2", null, 2, false);
124+
125+
var bucket1 = new StatsBucket(keyWithSource) { Duration = 1, Errors = 0, Hits = 1, TopLevelHits = 1 };
126+
var bucket2 = new StatsBucket(keyWithoutSource) { Duration = 2, Errors = 0, Hits = 1, TopLevelHits = 1 };
127+
128+
buffer.Buckets.Add(keyWithSource, bucket1);
129+
buffer.Buckets.Add(keyWithoutSource, bucket2);
130+
131+
var stream = new MemoryStream();
132+
buffer.Serialize(stream, 10);
133+
var result = MessagePackSerializer.Deserialize<MockClientStatsPayload>(stream.ToArray());
134+
135+
var stats = result.Stats[0].Stats;
136+
stats.Should().HaveCount(2);
137+
138+
var groupWithSource = stats.Single(g => g.Name == keyWithSource.OperationName);
139+
groupWithSource.ServiceSource.Should().Be("m");
140+
141+
var groupWithoutSource = stats.Single(g => g.Name == keyWithoutSource.OperationName);
142+
groupWithoutSource.ServiceSource.Should().BeNull();
143+
}
144+
106145
[Fact]
107146
public void Reset()
108147
{
109148
var buffer = new StatsBuffer(new ClientStatsPayload(MutableSettings.CreateForTesting(new(), [])));
110149

111-
var key1 = new StatsAggregationKey("resource1", "service1", "operation1", "type1", 1, false);
112-
var key2 = new StatsAggregationKey("resource2", "service2", "operation2", "type2", 2, false);
150+
var key1 = new StatsAggregationKey("resource1", "service1", "operation1", "type1", null, 1, false);
151+
var key2 = new StatsAggregationKey("resource2", "service2", "operation2", "type2", null, 2, false);
113152

114153
var statsBucket1 = new StatsBucket(key1) { Duration = 1, Errors = 11, Hits = 111, TopLevelHits = 10 };
115154
var statsBucket2 = new StatsBucket(key2) { Duration = 2, Errors = 0, Hits = 0, TopLevelHits = 0 };
@@ -144,7 +183,7 @@ public void IncrementSequence()
144183
{
145184
var buffer = new StatsBuffer(new ClientStatsPayload(MutableSettings.CreateForTesting(new(), [])));
146185

147-
var key = new StatsAggregationKey("resource1", "service1", "operation1", "type1", 1, false);
186+
var key = new StatsAggregationKey("resource1", "service1", "operation1", "type1", null, 1, false);
148187
var statsBucket = new StatsBucket(key) { Duration = 1, Errors = 11, Hits = 111, TopLevelHits = 10 };
149188

150189
buffer.Buckets.Add(key, statsBucket);
@@ -175,6 +214,7 @@ private static void AssertStatsGroup(MockClientGroupedStats group, StatsAggregat
175214
group.Duration.Should().Be(expectedBucket.Duration);
176215
group.Synthetics.Should().Be(expectedKey.IsSyntheticsRequest);
177216
group.TopLevelHits.Should().Be(expectedBucket.TopLevelHits);
217+
group.ServiceSource.Should().Be(expectedKey.ServiceSource);
178218

179219
var stream = new MemoryStream();
180220
expectedBucket.ErrorSummary.Serialize(stream);

0 commit comments

Comments
 (0)