Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
654305b
Wire per-component cardinality limits from Config per Cardinality Lim…
dougqh Jun 22, 2026
e991d03
Raise RESOURCE cardinality limit to 1024 to avoid premature collapse
dougqh Jun 24, 2026
7f8ff30
Add span-derived primary tags (CSS v1.3.0)
dougqh May 18, 2026
18315db
Make Canonical schema-swap safe + add additional-tags benchmark
dougqh May 20, 2026
51702c3
Rebase dougqh/metrics-arbitrary-tags onto dougqh/control-tag-cardinality
dougqh Jun 3, 2026
6a363f3
Use TagCardinalityHandler for additional-tag UTF8 caching + per-tag c…
dougqh Jun 3, 2026
934e599
Drop AdditionalTagsCardinalityLimiter — TagCardinalityHandler per tag…
dougqh Jun 3, 2026
026e82b
Fix allNull check in toEntry + stale benchmark comments
dougqh Jun 3, 2026
93e145c
Add @Param(limitsEnabled) to AdditionalTagsMetricsBenchmark
dougqh Jun 3, 2026
4c32f87
Document limitsEnabled benchmark interpretation + fix stale cap refer…
dougqh Jun 3, 2026
1b5304a
Add @SuppressForbidden to benchmark teardown + key-validation tests
dougqh Jun 3, 2026
80dbbab
Store additional tags compactly, mirroring peer tags
dougqh Jun 3, 2026
432a713
Address #11402 review: trim dead code, fix stale docs, reorder/reduce…
dougqh Jun 4, 2026
62c68e6
Merge dougqh/control-tag-cardinality + fix stale toEntry javadoc link
dougqh Jun 4, 2026
69b64c8
Trim multi-paragraph javadocs to one-liners per conventions
dougqh Jun 4, 2026
f1aad6c
Fix double-reset, inbox-full regression, empty-string sentinel, and d…
dougqh Jun 4, 2026
aa1587e
Eliminate per-reset StatsD tag allocation via pre-built String[] in h…
dougqh Jun 4, 2026
dbfd69e
Move statsDTag[] to handlers; lazy-init on first block
dougqh Jun 4, 2026
248d7f9
Merge EMPTY_PEER_TAGS and EMPTY_ADDITIONAL_TAGS into EMPTY_TAGS
dougqh Jun 4, 2026
6d18a35
Break circular layering: AdditionalTagsSchema no longer reads Aggrega…
dougqh Jun 4, 2026
f647cff
Move healthMetrics to last arg in AdditionalTagsSchema.from()
dougqh Jun 4, 2026
b08d970
Fix warnedCardinality never cleared, add property-field warn log, fix…
dougqh Jun 4, 2026
b894b4f
Identity fast-path in property handler probes; inline hash loop in Ca…
dougqh Jun 4, 2026
2c41389
Extract static property handlers into PropertyHandlers; fix PeerTagSc…
dougqh Jun 5, 2026
e6e7d31
Improve test coverage for cardinality handler reset path
dougqh Jun 5, 2026
d92f060
Add end-to-end cycle-reset test for cardinality limits
dougqh Jun 5, 2026
2aab39e
Remove committed artifacts; fix stale Javadoc; extract captureTagValu…
dougqh Jun 5, 2026
c04a107
Remove per-registration warn-once tracking; move cardinality warn to …
dougqh Jun 9, 2026
df657af
Move cardinality LIMITS_ENABLED flag to MetricCardinalityLimits; add …
dougqh Jun 9, 2026
ff1f53c
Extract shared cardinality-block reporting; rename PeerTagSchema.rese…
dougqh Jun 9, 2026
08c9f9e
Document why AggregateTable reads cardinality flag fresh from Config
dougqh Jun 9, 2026
c4f56b5
Fix spotless violations in Config.java imports and ClientStatsAggrega…
dougqh Jun 9, 2026
9e13d22
Suppress false-positive SpotBugs MT_CORRECTNESS / ES findings on sing…
dougqh Jun 9, 2026
3b3ff93
Migrate MetricsIntegrationTest from Spock/Groovy to JUnit 5 Java
dougqh Jun 9, 2026
f990b9c
Inline cardinality-block reporting back into the three handlers; drop…
dougqh Jun 9, 2026
ef97e00
Fix post-rebase compile errors and remove LIMITS_ENABLED global toggle
dougqh Jun 29, 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
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ public final class GeneralConfig {
public static final String TRACER_METRICS_MAX_PENDING = "trace.tracer.metrics.max.pending";
public static final String TRACER_METRICS_IGNORED_RESOURCES =
"trace.tracer.metrics.ignored.resources";
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
Loading