Skip to content

Commit 70fe2a8

Browse files
dougqhclaude
andcommitted
Report stats cardinality collapses under protobuf field names (RFC §5)
The approved Cardinality Limits RFC keys collapse telemetry by the lowercased protobuf field name. Two core-handler tags didn't match: - operation is carried on the wire as the field "name", so its health tag is now collapsed:name (the human-facing CardinalityLimitReporter still says "operation"). PropertyCardinalityHandler gains a decoupled statsDField for this case. - peer tags now aggregate into a single collapsed:peer_tags health metric across all configured peer tags (mirroring AdditionalTagsSchema), instead of emitting a per-tag-name collapsed:<tag> metric. Per-name detail is preserved in the human-facing reporter. TagCardinalityHandler.statsDTag() is now unused and removed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a8e423b commit 70fe2a8

6 files changed

Lines changed: 73 additions & 11 deletions

File tree

dd-trace-core/src/main/java/datadog/trace/common/metrics/CoreHandlers.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,12 @@ final class CoreHandlers {
4343
"service",
4444
MetricCardinalityLimits.SERVICE,
4545
MetricCardinalityLimits.USE_BLOCKED_SENTINEL);
46+
// operation is carried on the stats wire as the protobuf field "name", so its telemetry tag is
47+
// collapsed:name (RFC section 5) while the human-facing reporter keeps the clearer "operation".
4648
this.operation =
4749
new PropertyCardinalityHandler(
4850
"operation",
51+
"name",
4952
MetricCardinalityLimits.OPERATION,
5053
MetricCardinalityLimits.USE_BLOCKED_SENTINEL);
5154
this.serviceSource =

dd-trace-core/src/main/java/datadog/trace/common/metrics/PeerTagSchema.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ final class PeerTagSchema {
5454
/** Singleton schema for internal-kind spans -- only {@code base.service}. */
5555
static final PeerTagSchema INTERNAL = new PeerTagSchema(new String[] {BASE_SERVICE}, NO_STATE);
5656

57+
// Health/telemetry statsD tag per the approved Cardinality Limits RFC (section 5): peer-tag
58+
// collapses are reported under the lowercased protobuf field name peer_tags, aggregated across
59+
// every configured peer tag rather than per individual tag name.
60+
private static final String[] COLLAPSED_STATSD_TAG = {"collapsed:peer_tags"};
61+
5762
final String[] names;
5863
final TagCardinalityHandler[] handlers;
5964

@@ -121,12 +126,19 @@ UTF8BytesString register(int i, String value) {
121126
* handlers are not thread-safe.
122127
*/
123128
void resetHandlers(HealthMetrics healthMetrics, CardinalityLimitReporter reporter) {
129+
long totalCollapsed = 0;
124130
for (int i = 0; i < handlers.length; i++) {
125131
long numBlocked = handlers[i].reset();
126132
if (numBlocked > 0) {
127-
healthMetrics.onTagCardinalityBlocked(handlers[i].statsDTag(), numBlocked);
133+
// The human-facing reporter names the specific peer tag that triggered the block.
128134
reporter.record(names[i], numBlocked);
129135
}
136+
totalCollapsed += numBlocked;
137+
}
138+
// The health metric is reported at the peer_tags field granularity (not per tag name) per the
139+
// approved Cardinality Limits RFC.
140+
if (totalCollapsed > 0) {
141+
healthMetrics.onTagCardinalityBlocked(COLLAPSED_STATSD_TAG, totalCollapsed);
130142
}
131143
}
132144

dd-trace-core/src/main/java/datadog/trace/common/metrics/PropertyCardinalityHandler.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ final class PropertyCardinalityHandler {
3131
private static final int MAX_CARDINALITY_LIMIT = 1 << 29;
3232

3333
final String name;
34+
35+
/**
36+
* Protobuf field name this handler reports under in the {@code collapsed:} health/telemetry tag,
37+
* per the approved Cardinality Limits RFC (section 5), which keys collapses by the lowercased
38+
* protobuf field name. Usually equal to {@link #name}, but decoupled where the field's
39+
* human-facing name differs from its wire name -- e.g. {@code operation} is carried on the wire
40+
* as {@code name}, so its telemetry tag is {@code collapsed:name} while the human-facing {@link
41+
* CardinalityLimitReporter} still says {@code operation}.
42+
*/
43+
private final String statsDField;
44+
3445
private final int cardinalityLimit;
3546
private final int capacityMask;
3647

@@ -67,7 +78,13 @@ final class PropertyCardinalityHandler {
6778
}
6879

6980
PropertyCardinalityHandler(String name, int cardinalityLimit, boolean useBlockedSentinel) {
81+
this(name, name, cardinalityLimit, useBlockedSentinel);
82+
}
83+
84+
PropertyCardinalityHandler(
85+
String name, String statsDField, int cardinalityLimit, boolean useBlockedSentinel) {
7086
this.name = name;
87+
this.statsDField = statsDField;
7188
if (cardinalityLimit <= 0) {
7289
throw new IllegalArgumentException("cardinalityLimit must be positive: " + cardinalityLimit);
7390
}
@@ -159,7 +176,7 @@ private UTF8BytesString tracerBlockedValue() {
159176
*/
160177
String[] statsDTag() {
161178
if (statsDTag == null) {
162-
statsDTag = new String[] {"collapsed:" + name};
179+
statsDTag = new String[] {"collapsed:" + statsDField};
163180
}
164181
return statsDTag;
165182
}

dd-trace-core/src/main/java/datadog/trace/common/metrics/TagCardinalityHandler.java

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ final class TagCardinalityHandler {
1919
private static final int MAX_CARDINALITY_LIMIT = 1 << 29;
2020

2121
private final String tag;
22-
private String[] statsDTag = null;
2322
private final int cardinalityLimit;
2423
private final int maxValueLength;
2524
private final int capacityMask;
@@ -167,13 +166,6 @@ private UTF8BytesString tracerBlockedValue() {
167166
return cacheBlocked;
168167
}
169168

170-
String[] statsDTag() {
171-
if (statsDTag == null) {
172-
statsDTag = new String[] {"collapsed:" + tag};
173-
}
174-
return statsDTag;
175-
}
176-
177169
/**
178170
* Number of values collapsed by the per-value length cap in the current cycle. Must be read
179171
* before {@link #reset()}, which zeroes it along with the cardinality count.

dd-trace-core/src/test/java/datadog/trace/common/metrics/CoreHandlersTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ void resetReportsBlockedCountForAllNineHandlers() {
4646

4747
verify(metrics).onTagCardinalityBlocked(new String[] {"collapsed:resource"}, 1L);
4848
verify(metrics).onTagCardinalityBlocked(new String[] {"collapsed:service"}, 1L);
49-
verify(metrics).onTagCardinalityBlocked(new String[] {"collapsed:operation"}, 1L);
49+
// operation is carried on the wire as the protobuf field "name", so it reports as
50+
// collapsed:name
51+
verify(metrics).onTagCardinalityBlocked(new String[] {"collapsed:name"}, 1L);
5052
verify(metrics).onTagCardinalityBlocked(new String[] {"collapsed:service_source"}, 1L);
5153
verify(metrics).onTagCardinalityBlocked(new String[] {"collapsed:type"}, 1L);
5254
verify(metrics).onTagCardinalityBlocked(new String[] {"collapsed:span_kind"}, 1L);

dd-trace-core/src/test/java/datadog/trace/common/metrics/PeerTagSchemaTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
import static org.junit.jupiter.api.Assertions.assertTrue;
77

88
import datadog.trace.core.monitor.HealthMetrics;
9+
import java.util.ArrayList;
910
import java.util.Arrays;
1011
import java.util.Collections;
1112
import java.util.HashSet;
1213
import java.util.LinkedHashSet;
14+
import java.util.List;
1315
import java.util.Set;
1416
import org.junit.jupiter.api.Test;
1517

@@ -115,4 +117,38 @@ public void onTagCardinalityBlocked(String[] tag, long count) {
115117
schema.resetHandlers(hm, new CardinalityLimitReporter());
116118
assertEquals(0, recorded[0]);
117119
}
120+
121+
@Test
122+
void resetHandlersAggregatesAllPeerTagsUnderSinglePeerTagsField() {
123+
// The Cardinality Limits RFC reports peer-tag collapses under the protobuf field name peer_tags
124+
// aggregated across every configured peer tag, not per individual tag name.
125+
PeerTagSchema schema =
126+
new PeerTagSchema(new String[] {"peer.hostname", "peer.service"}, PeerTagSchema.NO_STATE);
127+
schema.handlers[0] = new TagCardinalityHandler("peer.hostname", 1, true);
128+
schema.handlers[1] = new TagCardinalityHandler("peer.service", 1, true);
129+
130+
schema.register(0, "host-a"); // within limit
131+
schema.register(0, "host-b"); // blocked
132+
schema.register(1, "svc-a"); // within limit
133+
schema.register(1, "svc-b"); // blocked
134+
schema.register(1, "svc-c"); // blocked
135+
136+
List<String[]> tags = new ArrayList<>();
137+
long[] total = {0};
138+
HealthMetrics hm =
139+
new HealthMetrics() {
140+
@Override
141+
public void onTagCardinalityBlocked(String[] tag, long count) {
142+
tags.add(tag);
143+
total[0] += count;
144+
}
145+
};
146+
147+
schema.resetHandlers(hm, new CardinalityLimitReporter());
148+
149+
// One health-metric call, tagged at the field granularity, summing both handlers' blocks.
150+
assertEquals(1, tags.size());
151+
assertArrayEquals(new String[] {"collapsed:peer_tags"}, tags.get(0));
152+
assertEquals(3, total[0]);
153+
}
118154
}

0 commit comments

Comments
 (0)