Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
b0b157b
Add span-derived primary tags (CSS v1.3.0)
dougqh May 18, 2026
b6b8af9
Make Canonical schema-swap safe + add additional-tags benchmark
dougqh May 20, 2026
a4af772
Rebase dougqh/metrics-arbitrary-tags onto dougqh/control-tag-cardinality
dougqh Jun 3, 2026
e4ef70a
Use TagCardinalityHandler for additional-tag UTF8 caching + per-tag c…
dougqh Jun 3, 2026
f767482
Drop AdditionalTagsCardinalityLimiter — TagCardinalityHandler per tag…
dougqh Jun 3, 2026
82b063f
Fix allNull check in toEntry + stale benchmark comments
dougqh Jun 3, 2026
e25c0d3
Add @Param(limitsEnabled) to AdditionalTagsMetricsBenchmark
dougqh Jun 3, 2026
9ed6302
Document limitsEnabled benchmark interpretation + fix stale cap refer…
dougqh Jun 3, 2026
642e973
Add @SuppressForbidden to benchmark teardown + key-validation tests
dougqh Jun 3, 2026
0a9efd8
Store additional tags compactly, mirroring peer tags
dougqh Jun 3, 2026
dbe71df
Address #11402 review: trim dead code, fix stale docs, reorder/reduce…
dougqh Jun 4, 2026
c9713bb
Merge branch 'dougqh/control-tag-cardinality' into dougqh/metrics-arb…
dougqh Jun 4, 2026
8db6f93
Merge remote-tracking branch 'origin/dougqh/control-tag-cardinality' …
dougqh Jun 4, 2026
0e7afa1
Merge dougqh/control-tag-cardinality + fix stale toEntry javadoc link
dougqh Jun 4, 2026
380b52e
Merge remote-tracking branch 'origin/dougqh/control-tag-cardinality' …
dougqh Jun 4, 2026
d2f545a
Trim multi-paragraph javadocs to one-liners per conventions
dougqh Jun 4, 2026
ad10fc0
Fix double-reset, inbox-full regression, empty-string sentinel, and d…
dougqh Jun 4, 2026
7ada566
Eliminate per-reset StatsD tag allocation via pre-built String[] in h…
dougqh Jun 4, 2026
238ba87
Move statsDTag[] to handlers; lazy-init on first block
dougqh Jun 4, 2026
b7cea17
Merge EMPTY_PEER_TAGS and EMPTY_ADDITIONAL_TAGS into EMPTY_TAGS
dougqh Jun 4, 2026
78fab50
Break circular layering: AdditionalTagsSchema no longer reads Aggrega…
dougqh Jun 4, 2026
96c40f4
Move healthMetrics to last arg in AdditionalTagsSchema.from()
dougqh Jun 4, 2026
2a4cf1c
Merge dougqh/control-tag-cardinality — port review fixes into #11402
dougqh Jun 4, 2026
35bb387
Fix warnedCardinality never cleared, add property-field warn log, fix…
dougqh Jun 4, 2026
cf9cded
Merge control-tag-cardinality; add @Nullable to SpanSnapshot.addition…
dougqh Jun 4, 2026
73566a8
Merge control-tag-cardinality: HealthMetrics passed through PeerTagSc…
dougqh Jun 4, 2026
4217b7b
Identity fast-path in property handler probes; inline hash loop in Ca…
dougqh Jun 4, 2026
785c49c
Merge remote-tracking branch 'origin/dougqh/control-tag-cardinality' …
dougqh Jun 4, 2026
f7baa0a
Merge remote-tracking branch 'origin/dougqh/control-tag-cardinality' …
dougqh Jun 4, 2026
16716c3
Fix SerializingMetricWriterTest: use injected SHA instead of global p…
dougqh Jun 4, 2026
41bf82a
Merge control-tag-cardinality: remove shouldWarnThisCycle; computeKey…
dougqh Jun 5, 2026
b5dd736
Add @BeforeEach resetCardinalityHandlers to ClientStatsAggregatorTest…
dougqh Jun 5, 2026
a1c8fe0
Extract static property handlers into PropertyHandlers; fix PeerTagSc…
dougqh Jun 5, 2026
ed063a1
Improve test coverage for cardinality handler reset path
dougqh Jun 5, 2026
40d8025
Add end-to-end cycle-reset test for cardinality limits
dougqh Jun 5, 2026
942e993
Remove committed artifacts; fix stale Javadoc; extract captureTagValu…
dougqh Jun 5, 2026
8078514
Remove per-registration warn-once tracking; move cardinality warn to …
dougqh Jun 9, 2026
fe868ab
Move cardinality LIMITS_ENABLED flag to MetricCardinalityLimits; add …
dougqh Jun 9, 2026
0a62853
Extract shared cardinality-block reporting; rename PeerTagSchema.rese…
dougqh Jun 9, 2026
76e665e
Document why AggregateTable reads cardinality flag fresh from Config
dougqh Jun 9, 2026
80f248c
Merge dougqh/control-tag-cardinality: port cardinality review fixes i…
dougqh Jun 9, 2026
7b74eb0
Fix spotless violations in Config.java imports and ClientStatsAggrega…
dougqh Jun 9, 2026
2e89bf4
Add missing package declaration to MetricsIntegrationTest
dougqh Jun 9, 2026
6376998
Drop now-redundant same-package imports in MetricsIntegrationTest
dougqh Jun 9, 2026
d40d99c
Suppress false-positive SpotBugs MT_CORRECTNESS / ES findings on sing…
dougqh Jun 9, 2026
9acec1d
Migrate MetricsIntegrationTest from Spock/Groovy to JUnit 5 Java
dougqh Jun 9, 2026
9aff892
Inline cardinality-block reporting back into the three handlers; drop…
dougqh Jun 9, 2026
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
1 change: 1 addition & 0 deletions .claude/worktrees/agent-a2dfcea2
Submodule agent-a2dfcea2 added at fc4b1a
1 change: 1 addition & 0 deletions .claude/worktrees/agent-adf53b58
Submodule agent-adf53b58 added at 4666c8
1 change: 1 addition & 0 deletions .claude/worktrees/master-bench
Submodule master-bench added at 26f1bc
1 change: 1 addition & 0 deletions .claude/worktrees/v1.62.0-bench
Submodule v1.62.0-bench added at 16c6a5
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,7 @@ protected long getFeaturesDiscoveryMinDelayMillis() {

private synchronized void discoverIfOutdated(final long maxElapsedMs) {
final long now = System.currentTimeMillis();
final State previous = discoveryState;
final long elapsed = now - previous.lastTimeDiscovered;
final long elapsed = now - discoveryState.lastTimeDiscovered;
if (elapsed > maxElapsedMs) {
final State newState = new State();
doDiscovery(newState);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public final class GeneralConfig {
"trace.tracer.metrics.ignored.resources";
public static final String TRACE_STATS_CARDINALITY_LIMITS_ENABLED =
"trace.stats.cardinality.limits.enabled";
public static final String TRACE_STATS_ADDITIONAL_TAGS = "trace.stats.additional.tags";

public static final String AZURE_APP_SERVICES = "azure.app.services";
public static final String INTERNAL_EXIT_ON_FAILURE = "trace.internal.exit.on.failure";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package datadog.trace.common.metrics;

import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND;
import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_CLIENT;
import static java.util.concurrent.TimeUnit.SECONDS;

import datadog.trace.api.WellKnownTags;
import datadog.trace.core.CoreSpan;
import de.thetaphi.forbiddenapis.SuppressForbidden;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.TearDown;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;

/**
* Regression benchmark for the additional-tags hot path; {@code limitsEnabled=true} is slower here
* because it records more (sentinel collapses), not because limiting is expensive.
*/
@State(Scope.Benchmark)
@Warmup(iterations = 2, time = 15, timeUnit = SECONDS)
@Measurement(iterations = 5, time = 15, timeUnit = SECONDS)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(SECONDS)
@Threads(8)
@Fork(value = 1)
public class AdditionalTagsMetricsBenchmark {

private ClientStatsAggregator aggregator;
private AdversarialMetricsBenchmark.CountingHealthMetrics health;

@Param({"false", "true"})
public boolean limitsEnabled;

@State(Scope.Thread)
public static class ThreadState {
int cursor;
}

@Setup
public void setup() {
this.health = new AdversarialMetricsBenchmark.CountingHealthMetrics();
AdditionalTagsSchema additionalTagsSchema =
AdditionalTagsSchema.from(
new LinkedHashSet<>(Arrays.asList("region", "tenant_id")), limitsEnabled, this.health);
this.aggregator =
new ClientStatsAggregator(
new WellKnownTags("", "", "", "", "", ""),
Collections.emptySet(),
additionalTagsSchema,
new ClientStatsAggregatorBenchmark.FixedAgentFeaturesDiscovery(
Collections.singleton("peer.hostname"), Collections.emptySet()),
this.health,
new ClientStatsAggregatorBenchmark.NullSink(),
2048,
2048,
false);
this.aggregator.start();
}

@TearDown
@SuppressForbidden
public void tearDown() {
aggregator.close();
System.err.println("[ADDITIONAL-TAGS] counters (across all threads, single fork):");
System.err.println(" onStatsInboxFull = " + health.inboxFull.sum());
System.err.println(" onStatsAggregateDropped = " + health.aggregateDropped.sum());
}

@Benchmark
public void publish(ThreadState ts, Blackhole blackhole) {
int idx = ts.cursor++;
ThreadLocalRandom rng = ThreadLocalRandom.current();

int scrambled = idx * 0x9E3779B1;
String region = "region-" + ((scrambled >>> 4) & 0xFFFF);
String tenant = "tenant-" + ((scrambled >>> 16) & 0xFFFF);
long durationNanos = 1L + (rng.nextLong() & 0x3FFFFFFFL);

SimpleSpan span =
new SimpleSpan("svc", "op", "res", "web", true, true, false, 0, durationNanos, 200);
span.setTag(SPAN_KIND, SPAN_KIND_CLIENT);
span.setTag("region", region);
span.setTag("tenant_id", tenant);

List<CoreSpan<?>> trace = Collections.singletonList(span);
blackhole.consume(aggregator.publish(trace));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public void setup() {
new ClientStatsAggregator(
new WellKnownTags("", "", "", "", "", ""),
Collections.emptySet(),
AdditionalTagsSchema.EMPTY,
new ClientStatsAggregatorBenchmark.FixedAgentFeaturesDiscovery(
Collections.singleton("peer.hostname"), Collections.emptySet()),
this.health,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public class ClientStatsAggregatorBenchmark {
new ClientStatsAggregator(
new WellKnownTags("", "", "", "", "", ""),
Collections.emptySet(),
AdditionalTagsSchema.EMPTY,
featuresDiscovery,
HealthMetrics.NO_OP,
new NullSink(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public class ClientStatsAggregatorDDSpanBenchmark {
new ClientStatsAggregator(
new WellKnownTags("", "", "", "", "", ""),
Collections.emptySet(),
AdditionalTagsSchema.EMPTY,
featuresDiscovery,
HealthMetrics.NO_OP,
new ClientStatsAggregatorBenchmark.NullSink(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public void setup() {
new ClientStatsAggregator(
new WellKnownTags("", "", "", "", "", ""),
Collections.emptySet(),
AdditionalTagsSchema.EMPTY,
new ClientStatsAggregatorBenchmark.FixedAgentFeaturesDiscovery(
Collections.singleton("peer.hostname"), Collections.emptySet()),
this.health,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public void setup() {
new ClientStatsAggregator(
new WellKnownTags("", "", "", "", "", ""),
Collections.emptySet(),
AdditionalTagsSchema.EMPTY,
new ClientStatsAggregatorBenchmark.FixedAgentFeaturesDiscovery(
Collections.singleton("peer.hostname"), Collections.emptySet()),
this.health,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package datadog.trace.common.metrics;

import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
import datadog.trace.core.monitor.HealthMetrics;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Immutable schema of configured span-derived primary tag keys; built once at aggregator
* construction.
*/
final class AdditionalTagsSchema {

private static final Logger log = LoggerFactory.getLogger(AdditionalTagsSchema.class);

// Backend pipeline supports ~4 primary tag dimensions by default; drop overflow at startup.
static final int MAX_ADDITIONAL_TAG_KEYS = 10;

/** Singleton empty schema returned when no additional tags are configured. */
static final AdditionalTagsSchema EMPTY =
new AdditionalTagsSchema(new String[0], new TagCardinalityHandler[0], HealthMetrics.NO_OP);

final String[] names;

/** Per-key handlers providing UTF8 caching and per-cycle cardinality limiting. */
private final TagCardinalityHandler[] handlers;

private final HealthMetrics healthMetrics;

private AdditionalTagsSchema(
String[] names, TagCardinalityHandler[] handlers, HealthMetrics healthMetrics) {
this.names = names;
this.handlers = handlers;
this.healthMetrics = healthMetrics;
}

/** Test convenience: uses {@link HealthMetrics#NO_OP} and limits enabled. */
static AdditionalTagsSchema from(Set<String> configured) {
return from(configured, true, HealthMetrics.NO_OP);
}

static AdditionalTagsSchema from(
Set<String> configured, boolean useBlockedSentinel, HealthMetrics healthMetrics) {
if (configured == null || configured.isEmpty()) {
return EMPTY;
}
List<String> valid = new ArrayList<>();
for (String key : configured) {
if (key == null || key.isEmpty()) {
log.warn("Ignoring empty additional metric tag key");
continue;
}
if (key.contains(":")) {
log.warn("Ignoring additional metric tag key '{}': keys must not contain ':'", key);
continue;
}
valid.add(key);
}
if (valid.isEmpty()) {
return EMPTY;
}
Collections.sort(valid);
// Dedup (sort brings duplicates adjacent)
List<String> deduped = new ArrayList<>(valid.size());
String prev = null;
for (String key : valid) {
if (!key.equals(prev)) {
deduped.add(key);
prev = key;
}
}
if (deduped.size() > MAX_ADDITIONAL_TAG_KEYS) {
log.warn(
"Configured additional metric tag keys ({}) exceeds the supported limit of {}; "
+ "dropping extra keys: {}",
deduped.size(),
MAX_ADDITIONAL_TAG_KEYS,
deduped.subList(MAX_ADDITIONAL_TAG_KEYS, deduped.size()));
deduped = deduped.subList(0, MAX_ADDITIONAL_TAG_KEYS);
}
String[] namesArr = deduped.toArray(new String[0]);
TagCardinalityHandler[] handlersArr = new TagCardinalityHandler[namesArr.length];
for (int i = 0; i < namesArr.length; i++) {
handlersArr[i] =
new TagCardinalityHandler(
namesArr[i], MetricCardinalityLimits.ADDITIONAL_TAG_VALUE, useBlockedSentinel);
}
return new AdditionalTagsSchema(namesArr, handlersArr, healthMetrics);
}

int size() {
return names.length;
}

String name(int i) {
return names[i];
}

UTF8BytesString register(int i, String value) {
return handlers[i].register(value);
}

void resetHandlers() {
for (int i = 0; i < handlers.length; i++) {
long blocked = handlers[i].reset();
if (blocked > 0) {
log.warn(
"Cardinality limit reached for additional metric tag '{}'; further values will be reported as blocked_by_tracer",
names[i]);
healthMetrics.onTagCardinalityBlocked(handlers[i].statsDTag(), blocked);
}
}
}
}
Loading