Skip to content

Commit 4188b43

Browse files
dougqhclaude
andcommitted
Bound configurable trace-stats cardinality limit
A mis-set trace.stats.<tag>.cardinality.limit flowed straight into TagCardinalityHandler, which eagerly allocates four reference arrays of nextPow2(limit * 2) slots. A large value could exhaust the heap while the aggregator is constructed, before the tracer finished starting. Cap the accepted value at 1 << 16 (~2 MB per handler worst case, far above the largest built-in default of 1024) and fall back to the default with a warning when it is exceeded, mirroring the existing <= 0 fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 01bf9fe commit 4188b43

2 files changed

Lines changed: 48 additions & 1 deletion

File tree

internal-api/src/main/java/datadog/trace/api/Config.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3895,6 +3895,17 @@ public int getTraceStatsInterval() {
38953895
return traceStatsInterval;
38963896
}
38973897

3898+
/**
3899+
* Upper bound for a configured stats cardinality limit. Each limit sizes a {@code
3900+
* TagCardinalityHandler} that eagerly allocates four reference arrays of {@code nextPow2(limit *
3901+
* 2)} slots, so an unbounded value (e.g. a mis-set env var) can exhaust the heap while the
3902+
* aggregator is constructed, before the tracer finishes starting. {@code 1 << 16} caps a single
3903+
* handler near ~2 MB while staying far above any useful setting -- the highest built-in default
3904+
* is 1024, and the aggregate table itself caps at a few thousand rows, so a larger per-key budget
3905+
* cannot produce more aggregate rows anyway.
3906+
*/
3907+
private static final int MAX_TRACE_STATS_CARDINALITY_LIMIT = 1 << 16;
3908+
38983909
/**
38993910
* Returns the per-cycle cardinality limit for the named stats field, following the RFC naming
39003911
* pattern {@code DD_TRACE_STATS_{tagName}_CARDINALITY_LIMIT} (e.g. {@code
@@ -3903,7 +3914,9 @@ public int getTraceStatsInterval() {
39033914
*
39043915
* <p>A non-positive configured value is invalid -- each limit sizes a fixed-capacity handler
39053916
* table that requires a positive size -- so it falls back to {@code defaultLimit}, logged at
3906-
* debug.
3917+
* debug. A value above {@link #MAX_TRACE_STATS_CARDINALITY_LIMIT} would eagerly allocate handler
3918+
* tables large enough to exhaust the heap at startup, so it likewise falls back to {@code
3919+
* defaultLimit}, logged at warn.
39073920
*/
39083921
public int getTraceStatsCardinalityLimit(String tagName, int defaultLimit) {
39093922
int limit =
@@ -3916,6 +3929,15 @@ public int getTraceStatsCardinalityLimit(String tagName, int defaultLimit) {
39163929
defaultLimit);
39173930
return defaultLimit;
39183931
}
3932+
if (limit > MAX_TRACE_STATS_CARDINALITY_LIMIT) {
3933+
log.warn(
3934+
"trace.stats.{}.cardinality.limit={} exceeds the maximum of {}; using default {} to avoid excessive memory use.",
3935+
tagName,
3936+
limit,
3937+
MAX_TRACE_STATS_CARDINALITY_LIMIT,
3938+
defaultLimit);
3939+
return defaultLimit;
3940+
}
39193941
return limit;
39203942
}
39213943

internal-api/src/test/java/datadog/trace/api/TraceStatsCardinalityLimitTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
class TraceStatsCardinalityLimitTest {
99

1010
private static final int DEFAULT = 1024;
11+
// Mirrors Config.MAX_TRACE_STATS_CARDINALITY_LIMIT (package-private, not visible here).
12+
private static final int MAX = 1 << 16;
1113

1214
private static Config configWith(String limit) {
1315
Properties props = new Properties();
@@ -29,4 +31,27 @@ void zeroFallsBackToDefault() {
2931
void negativeFallsBackToDefault() {
3032
assertEquals(DEFAULT, configWith("-5").getTraceStatsCardinalityLimit("resource", DEFAULT));
3133
}
34+
35+
@Test
36+
void valueAtMaxIsUsed() {
37+
// 1 << 16 is the largest accepted value; it is used verbatim.
38+
assertEquals(
39+
MAX, configWith(Integer.toString(MAX)).getTraceStatsCardinalityLimit("resource", DEFAULT));
40+
}
41+
42+
@Test
43+
void valueAboveMaxFallsBackToDefault() {
44+
// One past the cap falls back rather than sizing an oversized handler table.
45+
assertEquals(
46+
DEFAULT,
47+
configWith(Integer.toString(MAX + 1)).getTraceStatsCardinalityLimit("resource", DEFAULT));
48+
}
49+
50+
@Test
51+
void heapExhaustingValueFallsBackToDefault() {
52+
// A value below TagCardinalityHandler's own 1<<29 guard but large enough to allocate
53+
// multi-gigabyte handler tables must not reach the handler; it falls back to the default.
54+
assertEquals(
55+
DEFAULT, configWith("500000000").getTraceStatsCardinalityLimit("resource", DEFAULT));
56+
}
3257
}

0 commit comments

Comments
 (0)