From 8e5771c747ee9d36c57cd1e54a8a87cc8c097814 Mon Sep 17 00:00:00 2001 From: labbati Date: Fri, 15 May 2026 15:18:09 +0200 Subject: [PATCH 1/8] feat(tt): plumb tt_extraction_patterns through DynamicConfig Adds the TransactionTrackingPatterns util (hand-rolled glob matcher with a single volatile snapshot, zero allocations on the empty fast path), wires the new optional 'tt_extraction_patterns' field through the APM_TRACING remote-config DTO and DynamicConfig snapshot, exposes a Config static fallback (always empty for now) and adds the InstrumentationTags TT_EXTRACTION_SOURCES constant. No tagging behaviour yet \u2014 only the configuration plumbing and the compiled-pattern snapshot publication. tag: ai generated --- .../trace/core/TracingConfigPoller.java | 8 + .../trace/core/TracingConfigPollerTest.java | 103 ++++++++++ .../main/java/datadog/trace/api/Config.java | 9 + .../java/datadog/trace/api/DynamicConfig.java | 24 +++ .../java/datadog/trace/api/TraceConfig.java | 6 + .../api/tt/TransactionTrackingPatterns.java | 188 ++++++++++++++++++ .../instrumentation/api/AgentTracer.java | 5 + .../api/InstrumentationTags.java | 4 + .../tt/TransactionTrackingPatternsTest.java | 115 +++++++++++ 9 files changed, 462 insertions(+) create mode 100644 internal-api/src/main/java/datadog/trace/api/tt/TransactionTrackingPatterns.java create mode 100644 internal-api/src/test/java/datadog/trace/api/tt/TransactionTrackingPatternsTest.java diff --git a/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java b/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java index 6bbc13151e3..0e712a88cf2 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java @@ -256,6 +256,8 @@ void applyConfigOverrides(LibConfig libConfig) { maybeOverride(builder::setTraceSampleRate, libConfig.traceSampleRate); maybeOverride(builder::setTracingTags, parseTagListToMap(libConfig.tracingTags)); + maybeOverride( + builder::setTransactionTrackingExtractionPatterns, libConfig.ttExtractionPatterns); DebuggerConfigBridge.updateConfig( new DebuggerConfigUpdate( libConfig.dynamicInstrumentationEnabled, @@ -419,6 +421,9 @@ static final class LibConfig { @Json(name = "data_streams_transaction_extractors") public DataStreamsTransactionExtractors dataStreamsTransactionExtractors; + @Json(name = "tt_extraction_patterns") + public List ttExtractionPatterns; + /** * Merges a list of LibConfig objects by taking the first non-null value for each field. * @@ -482,6 +487,9 @@ public static LibConfig mergeLibConfigs(List configs) { if (merged.liveDebuggingEnabled == null) { merged.liveDebuggingEnabled = config.liveDebuggingEnabled; } + if (merged.ttExtractionPatterns == null) { + merged.ttExtractionPatterns = config.ttExtractionPatterns; + } } return merged; diff --git a/dd-trace-core/src/test/java/datadog/trace/core/TracingConfigPollerTest.java b/dd-trace-core/src/test/java/datadog/trace/core/TracingConfigPollerTest.java index a5105d326c9..d45773f103e 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/TracingConfigPollerTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/TracingConfigPollerTest.java @@ -19,6 +19,7 @@ import datadog.remoteconfig.state.ParsedConfigKey; import datadog.remoteconfig.state.ProductListener; import datadog.trace.api.datastreams.DataStreamsTransactionExtractor; +import datadog.trace.api.tt.TransactionTrackingPatterns; import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -219,6 +220,108 @@ void actualConfigCommitWithServiceAndOrgLevelConfigs() throws Exception { } } + @Test + void ttExtractionPatternsArePropagatedAndPublished() throws Exception { + ParsedConfigKey key = ParsedConfigKey.parse("datadog/2/APM_TRACING/org_config/config"); + ConfigurationPoller poller = mock(ConfigurationPoller.class); + SharedCommunicationObjects sco = createScoWithPoller(poller); + + ProductListener[] capturedUpdater = {null}; + doAnswer( + inv -> { + capturedUpdater[0] = inv.getArgument(1, ProductListener.class); + return null; + }) + .when(poller) + .addListener(eq(Product.APM_TRACING), any(ProductListener.class)); + + CoreTracer tracer = + CoreTracer.builder().sharedCommunicationObjects(sco).pollForTracingConfiguration().build(); + unclosedTracers.add(tracer); + + try { + TransactionTrackingPatterns.resetForTest(); + assertEquals( + Collections.emptyList(), + tracer.captureTraceConfig().getTransactionTrackingExtractionPatterns()); + assertTrue(TransactionTrackingPatterns.isEmpty()); + + ProductListener updater = capturedUpdater[0]; + updater.accept( + key, + ("{\n" + + " \"service_target\": {\"service\": \"*\", \"env\": \"*\"},\n" + + " \"lib_config\": {\n" + + " \"tt_extraction_patterns\": [\"x-trace-*\", \"*-tenant\"]\n" + + " }\n" + + "}") + .getBytes(StandardCharsets.UTF_8), + null); + updater.commit(null); + + assertEquals( + Arrays.asList("x-trace-*", "*-tenant"), + tracer.captureTraceConfig().getTransactionTrackingExtractionPatterns()); + assertFalse(TransactionTrackingPatterns.isEmpty()); + assertTrue(TransactionTrackingPatterns.matchesAny("X-Trace-Id")); + assertTrue(TransactionTrackingPatterns.matchesAny("customer-tenant")); + assertFalse(TransactionTrackingPatterns.matchesAny("unrelated")); + + // Removing the config should clear the static snapshot back to empty. + updater.remove(key, null); + updater.commit(null); + assertEquals( + Collections.emptyList(), + tracer.captureTraceConfig().getTransactionTrackingExtractionPatterns()); + assertTrue(TransactionTrackingPatterns.isEmpty()); + } finally { + TransactionTrackingPatterns.resetForTest(); + tracer.close(); + } + } + + @Test + void absentTtExtractionPatternsFieldKeepsSnapshotEmpty() throws Exception { + ParsedConfigKey key = ParsedConfigKey.parse("datadog/2/APM_TRACING/org_config/config"); + ConfigurationPoller poller = mock(ConfigurationPoller.class); + SharedCommunicationObjects sco = createScoWithPoller(poller); + + ProductListener[] capturedUpdater = {null}; + doAnswer( + inv -> { + capturedUpdater[0] = inv.getArgument(1, ProductListener.class); + return null; + }) + .when(poller) + .addListener(eq(Product.APM_TRACING), any(ProductListener.class)); + + CoreTracer tracer = + CoreTracer.builder().sharedCommunicationObjects(sco).pollForTracingConfiguration().build(); + unclosedTracers.add(tracer); + + try { + TransactionTrackingPatterns.resetForTest(); + ProductListener updater = capturedUpdater[0]; + updater.accept( + key, + ("{\n" + + " \"service_target\": {\"service\": \"*\", \"env\": \"*\"},\n" + + " \"lib_config\": {\"tracing_sampling_rate\": 0.5}\n" + + "}") + .getBytes(StandardCharsets.UTF_8), + null); + updater.commit(null); + + assertEquals( + Collections.emptyList(), + tracer.captureTraceConfig().getTransactionTrackingExtractionPatterns()); + assertTrue(TransactionTrackingPatterns.isEmpty()); + } finally { + TransactionTrackingPatterns.resetForTest(); + tracer.close(); + } + } + @Test void twoOrgLevelsConfigSettingDifferentFlagsWorks() throws Exception { ParsedConfigKey orgConfig1Key = diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index a463887f61a..094518eddab 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -4872,6 +4872,15 @@ public String getDataStreamsTransactionExtractors() { return dataStreamsTransactionExtractors; } + /** + * Static fallback for Transaction Tracking extraction patterns. Always returns an empty list; + * non-empty values are only delivered through {@code APM_TRACING} remote-config (field {@code + * tt_extraction_patterns}). + */ + public java.util.List getTransactionTrackingExtractionPatterns() { + return java.util.Collections.emptyList(); + } + public long getDataStreamsBucketDurationNanoseconds() { // Rounds to the nearest millisecond before converting to nanos int milliseconds = Math.round(dataStreamsBucketDurationSeconds * 1000); diff --git a/internal-api/src/main/java/datadog/trace/api/DynamicConfig.java b/internal-api/src/main/java/datadog/trace/api/DynamicConfig.java index 19a1ca84abf..1e314dc129b 100644 --- a/internal-api/src/main/java/datadog/trace/api/DynamicConfig.java +++ b/internal-api/src/main/java/datadog/trace/api/DynamicConfig.java @@ -17,6 +17,7 @@ import datadog.trace.api.datastreams.DataStreamsTransactionExtractor; import datadog.trace.api.sampling.SamplingRule.SpanSamplingRule; import datadog.trace.api.sampling.SamplingRule.TraceSamplingRule; +import datadog.trace.api.tt.TransactionTrackingPatterns; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -87,6 +88,7 @@ public Builder current() { public void resetTraceConfig() { currentSnapshot = initialSnapshot; reportConfigChange(initialSnapshot); + TransactionTrackingPatterns.update(initialSnapshot.ttExtractionPatterns); } @Override @@ -113,6 +115,7 @@ public final class Builder { Pair preferredServiceNameAndSource; List dataStreamsTransactionExtractors; + List ttExtractionPatterns; Builder() {} @@ -137,6 +140,7 @@ public final class Builder { this.preferredServiceNameAndSource = snapshot.preferredServiceNameAndSource; this.dataStreamsTransactionExtractors = snapshot.dataStreamsTransactionExtractors; + this.ttExtractionPatterns = snapshot.ttExtractionPatterns; } public Builder setRuntimeMetricsEnabled(boolean runtimeMetricsEnabled) { @@ -232,6 +236,15 @@ public Builder setPreferredServiceNameAndSource( return this; } + /** + * Sets the list of {@code *}-glob patterns used by Transaction Tracking to flag inbound HTTP + * header / query-string parameter names. A {@code null} or empty list disables the feature. + */ + public Builder setTransactionTrackingExtractionPatterns(List patterns) { + this.ttExtractionPatterns = patterns; + return this; + } + /** Overwrites the current configuration with a new snapshot. */ public DynamicConfig apply() { S oldSnapshot = currentSnapshot; @@ -243,6 +256,8 @@ public DynamicConfig apply() { currentSnapshot = newSnapshot; reportConfigChange(newSnapshot); } + // Publish the compiled snapshot to the static holder used on the request hot path. + TransactionTrackingPatterns.update(newSnapshot.ttExtractionPatterns); return DynamicConfig.this; } } @@ -335,6 +350,7 @@ public static class Snapshot implements TraceConfig { final Pair preferredServiceNameAndSource; final List dataStreamsTransactionExtractors; + final List ttExtractionPatterns; protected Snapshot(DynamicConfig.Builder builder, Snapshot oldSnapshot) { @@ -357,6 +373,7 @@ protected Snapshot(DynamicConfig.Builder builder, Snapshot oldSnapshot) { this.preferredServiceNameAndSource = builder.preferredServiceNameAndSource; this.dataStreamsTransactionExtractors = builder.dataStreamsTransactionExtractors; + this.ttExtractionPatterns = nullToEmpty(builder.ttExtractionPatterns); } private static Map nullToEmpty(Map mapping) { @@ -432,6 +449,11 @@ public List getDataStreamsTransactionExtractors return dataStreamsTransactionExtractors; } + @Override + public List getTransactionTrackingExtractionPatterns() { + return ttExtractionPatterns; + } + @Override public Map getTracingTags() { return tracingTags; @@ -466,6 +488,8 @@ public String toString() { + tracingTags + ", preferredServiceNameAndSource=" + preferredServiceNameAndSource + + ", ttExtractionPatterns=" + + ttExtractionPatterns + '}'; } } diff --git a/internal-api/src/main/java/datadog/trace/api/TraceConfig.java b/internal-api/src/main/java/datadog/trace/api/TraceConfig.java index 801e8e516b4..ab91ae401fb 100644 --- a/internal-api/src/main/java/datadog/trace/api/TraceConfig.java +++ b/internal-api/src/main/java/datadog/trace/api/TraceConfig.java @@ -55,4 +55,10 @@ public interface TraceConfig { * @return List of Data Streams Transactions extractors. */ List getDataStreamsTransactionExtractors(); + + /** + * Glob patterns used by Transaction Tracking to flag inbound HTTP header / query-string parameter + * names. An empty list disables the feature. + */ + List getTransactionTrackingExtractionPatterns(); } diff --git a/internal-api/src/main/java/datadog/trace/api/tt/TransactionTrackingPatterns.java b/internal-api/src/main/java/datadog/trace/api/tt/TransactionTrackingPatterns.java new file mode 100644 index 00000000000..edc101ee085 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/tt/TransactionTrackingPatterns.java @@ -0,0 +1,188 @@ +package datadog.trace.api.tt; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Snapshot of compiled "transaction tracking" extraction glob patterns delivered through + * remote-config under the {@code APM_TRACING} product (field {@code tt_extraction_patterns}). + * + *

The hot path on every server request only does a single volatile read followed by an {@link + * List#isEmpty()} check when no patterns are configured, so this class is zero-allocation in the + * disabled case. + * + *

Matching is case-insensitive on the candidate name and the supported wildcard alphabet is + * limited to {@code *} (zero-or-more characters). The matcher is hand-rolled to avoid {@link + * java.util.regex.Pattern} compilation on the request hot path. + */ +public final class TransactionTrackingPatterns { + + private static final Logger log = LoggerFactory.getLogger(TransactionTrackingPatterns.class); + + /** Shared empty snapshot — referenced when no patterns are configured. */ + private static final List EMPTY = Collections.emptyList(); + + private static volatile List snapshot = EMPTY; + + private TransactionTrackingPatterns() {} + + /** + * Re-compile the raw pattern list and atomically publish a new snapshot. A {@code null} or empty + * input clears the snapshot back to the no-op default. Malformed entries (null/blank) are dropped + * with a debug log; the remaining entries are kept. + */ + public static void update(List rawPatterns) { + if (rawPatterns == null || rawPatterns.isEmpty()) { + snapshot = EMPTY; + return; + } + List compiled = new ArrayList<>(rawPatterns.size()); + for (String raw : rawPatterns) { + if (raw == null) { + log.debug("Ignoring null tt_extraction_pattern entry"); + continue; + } + String trimmed = raw.trim(); + if (trimmed.isEmpty()) { + log.debug("Ignoring blank tt_extraction_pattern entry"); + continue; + } + compiled.add(CompiledPattern.compile(trimmed)); + } + if (compiled.isEmpty()) { + snapshot = EMPTY; + } else { + snapshot = Collections.unmodifiableList(compiled); + } + } + + /** Fast no-allocation check used as the hot-path guard. */ + public static boolean isEmpty() { + return snapshot.isEmpty(); + } + + /** Returns the current immutable snapshot. */ + public static List currentSnapshot() { + return snapshot; + } + + /** True if {@code candidate} matches any compiled pattern in the current snapshot. */ + public static boolean matchesAny(String candidate) { + if (candidate == null) { + return false; + } + List local = snapshot; + if (local.isEmpty()) { + return false; + } + String lowered = candidate.toLowerCase(Locale.ROOT); + // noinspection ForLoopReplaceableByForEach -- avoid iterator allocation on the hot path + for (int i = 0; i < local.size(); i++) { + if (local.get(i).matchesLowercased(lowered)) { + return true; + } + } + return false; + } + + /** Test-only: replaces the snapshot atomically. */ + public static void resetForTest() { + snapshot = EMPTY; + } + + /** + * Compiled glob pattern. Supports only {@code *} (zero-or-more) wildcard. Comparison is + * case-insensitive: the pattern is lowercased once at compile time and the candidate must be + * lowercased by the caller before invoking {@link #matchesLowercased(String)}. + */ + public static final class CompiledPattern { + private final String original; + private final String[] segments; + private final boolean anchorStart; + private final boolean anchorEnd; + + private CompiledPattern( + String original, String[] segments, boolean anchorStart, boolean anchorEnd) { + this.original = original; + this.segments = segments; + this.anchorStart = anchorStart; + this.anchorEnd = anchorEnd; + } + + static CompiledPattern compile(String raw) { + String lower = raw.toLowerCase(Locale.ROOT); + boolean anchorStart = !lower.startsWith("*"); + boolean anchorEnd = !lower.endsWith("*"); + // hand-rolled split on '*' that preserves empty segments only when needed + List parts = new ArrayList<>(); + int start = 0; + for (int i = 0; i < lower.length(); i++) { + if (lower.charAt(i) == '*') { + if (i > start) { + parts.add(lower.substring(start, i)); + } + start = i + 1; + } + } + if (start < lower.length()) { + parts.add(lower.substring(start)); + } + return new CompiledPattern(raw, parts.toArray(new String[0]), anchorStart, anchorEnd); + } + + public String original() { + return original; + } + + /** Caller must pass an already-lowercased candidate. */ + public boolean matchesLowercased(String candidate) { + // Pattern was just "*" / "***" etc. -> matches anything. + if (segments.length == 0) { + return true; + } + + // Special case: literal pattern (no '*' at all) -> exact match. + if (anchorStart && anchorEnd && segments.length == 1) { + return candidate.equals(segments[0]); + } + + int idx = 0; + int firstFloating = 0; + if (anchorStart) { + String first = segments[0]; + if (!candidate.startsWith(first)) { + return false; + } + idx = first.length(); + firstFloating = 1; + } + + int endIdx = candidate.length(); + int lastFloating = segments.length; // exclusive + if (anchorEnd) { + String last = segments[segments.length - 1]; + int tailStart = candidate.length() - last.length(); + if (tailStart < idx || !candidate.regionMatches(tailStart, last, 0, last.length())) { + return false; + } + endIdx = tailStart; + lastFloating = segments.length - 1; + } + + // walk remaining floating segments via indexOf, all must fit within [idx, endIdx) + for (int segIndex = firstFloating; segIndex < lastFloating; segIndex++) { + String seg = segments[segIndex]; + int found = candidate.indexOf(seg, idx); + if (found < 0 || found + seg.length() > endIdx) { + return false; + } + idx = found + seg.length(); + } + return true; + } + } +} diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentTracer.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentTracer.java index 14a5b5d6d21..dd3fca04482 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentTracer.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentTracer.java @@ -735,5 +735,10 @@ public List getTraceSamplingRules() { public List getDataStreamsTransactionExtractors() { return null; } + + @Override + public List getTransactionTrackingExtractionPatterns() { + return Collections.emptyList(); + } } } diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java index 0c1054e7776..92b59663c34 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java @@ -8,6 +8,10 @@ public class InstrumentationTags { // start looking at generating constants based on the // enabled instrumentations. + // Transaction tracking — extraction sources tag (set by HttpServerDecorator when a configured + // glob pattern matches an inbound HTTP header name or query-string parameter name). + public static final String TT_EXTRACTION_SOURCES = "_dd.tt.extraction_sources"; + public static final String PARTITION = "partition"; public static final String OFFSET = "offset"; public static final String CONSUMER_GROUP = "kafka.group"; diff --git a/internal-api/src/test/java/datadog/trace/api/tt/TransactionTrackingPatternsTest.java b/internal-api/src/test/java/datadog/trace/api/tt/TransactionTrackingPatternsTest.java new file mode 100644 index 00000000000..159d7add80b --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/tt/TransactionTrackingPatternsTest.java @@ -0,0 +1,115 @@ +package datadog.trace.api.tt; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +class TransactionTrackingPatternsTest { + + @AfterEach + void reset() { + TransactionTrackingPatterns.resetForTest(); + } + + @Test + void emptyByDefault() { + assertTrue(TransactionTrackingPatterns.isEmpty()); + assertFalse(TransactionTrackingPatterns.matchesAny("x-foo")); + } + + @Test + void nullOrEmptyUpdateKeepsEmpty() { + TransactionTrackingPatterns.update(null); + assertTrue(TransactionTrackingPatterns.isEmpty()); + TransactionTrackingPatterns.update(Collections.emptyList()); + assertTrue(TransactionTrackingPatterns.isEmpty()); + } + + @Test + void literalPatternIsExactCaseInsensitive() { + TransactionTrackingPatterns.update(Collections.singletonList("X-Request-Id")); + assertTrue(TransactionTrackingPatterns.matchesAny("x-request-id")); + assertTrue(TransactionTrackingPatterns.matchesAny("X-REQUEST-ID")); + assertFalse(TransactionTrackingPatterns.matchesAny("x-request-id-2")); + assertFalse(TransactionTrackingPatterns.matchesAny("yx-request-id")); + } + + @Test + void prefixWildcard() { + TransactionTrackingPatterns.update(Collections.singletonList("*-id")); + assertTrue(TransactionTrackingPatterns.matchesAny("X-Request-Id")); + assertTrue(TransactionTrackingPatterns.matchesAny("-id")); + assertFalse(TransactionTrackingPatterns.matchesAny("id")); + assertFalse(TransactionTrackingPatterns.matchesAny("X-Request-Token")); + } + + @Test + void suffixWildcard() { + TransactionTrackingPatterns.update(Collections.singletonList("x-trace-*")); + assertTrue(TransactionTrackingPatterns.matchesAny("X-Trace-Id")); + assertTrue(TransactionTrackingPatterns.matchesAny("x-trace-")); + assertFalse(TransactionTrackingPatterns.matchesAny("x-trace")); + assertFalse(TransactionTrackingPatterns.matchesAny("y-trace-id")); + } + + @Test + void middleWildcard() { + TransactionTrackingPatterns.update(Collections.singletonList("x-*-id")); + assertTrue(TransactionTrackingPatterns.matchesAny("X-Request-Id")); + assertTrue(TransactionTrackingPatterns.matchesAny("x--id")); + assertFalse(TransactionTrackingPatterns.matchesAny("x-request")); + assertFalse(TransactionTrackingPatterns.matchesAny("y-request-id")); + } + + @Test + void multipleWildcards() { + TransactionTrackingPatterns.update(Collections.singletonList("*foo*bar*")); + assertTrue(TransactionTrackingPatterns.matchesAny("xxfooyybarzz")); + assertTrue(TransactionTrackingPatterns.matchesAny("foobar")); + assertFalse(TransactionTrackingPatterns.matchesAny("barfoo")); + assertFalse(TransactionTrackingPatterns.matchesAny("fooxx")); + } + + @Test + void starOnlyMatchesAnything() { + TransactionTrackingPatterns.update(Collections.singletonList("*")); + assertTrue(TransactionTrackingPatterns.matchesAny("anything")); + assertTrue(TransactionTrackingPatterns.matchesAny("")); + } + + @Test + void multiplePatternsAnyMatch() { + TransactionTrackingPatterns.update(Arrays.asList("x-foo-*", "*-trace")); + assertTrue(TransactionTrackingPatterns.matchesAny("x-foo-1")); + assertTrue(TransactionTrackingPatterns.matchesAny("dd-trace")); + assertFalse(TransactionTrackingPatterns.matchesAny("dd-other")); + } + + @Test + void blankAndNullEntriesAreSkipped() { + TransactionTrackingPatterns.update(Arrays.asList(null, "", " ", "x-keep")); + assertFalse(TransactionTrackingPatterns.isEmpty()); + assertTrue(TransactionTrackingPatterns.matchesAny("x-keep")); + assertEquals(1, TransactionTrackingPatterns.currentSnapshot().size()); + } + + @Test + void allBlankClearsSnapshot() { + TransactionTrackingPatterns.update(Arrays.asList(null, "", " ")); + assertTrue(TransactionTrackingPatterns.isEmpty()); + } + + @Test + void overlappingSegmentsDoNotMatch() { + TransactionTrackingPatterns.update(Collections.singletonList("ab*ab")); + assertTrue(TransactionTrackingPatterns.matchesAny("abxab")); + assertTrue(TransactionTrackingPatterns.matchesAny("abab")); + assertFalse(TransactionTrackingPatterns.matchesAny("aba")); + assertFalse(TransactionTrackingPatterns.matchesAny("xab")); + } +} From 61ffc5258530bcf1f09b915a9691e0e5e0bce835 Mon Sep 17 00:00:00 2001 From: labbati Date: Fri, 15 May 2026 15:22:53 +0200 Subject: [PATCH 2/8] feat(tt): tag server spans with _dd.tt.extraction_sources Adds the HttpServerDecorator.forEachRequestHeaderName extension point (no-op default) and a guarded tagging block in onRequest. When the TransactionTrackingPatterns snapshot is non-empty the server span gets a single _dd.tt.extraction_sources tag whose value is a deterministic CSV of matching header / query-string parameter names, sorted within each source bucket (headers first, then qs) and lowercased. Fast path on the empty snapshot is a single volatile read + isEmpty() check, no allocation. A negative unit test asserts no tag is set when the pattern list is empty. tag: ai generated --- .../decorator/HttpServerDecorator.java | 129 +++++++++++++ ...HttpServerDecoratorTtExtractionTest.groovy | 181 ++++++++++++++++++ 2 files changed, 310 insertions(+) create mode 100644 dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecoratorTtExtractionTest.groovy diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java index a60401f5811..346423f5f1e 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java @@ -26,12 +26,14 @@ import datadog.trace.api.gateway.RequestContext; import datadog.trace.api.gateway.RequestContextSlot; import datadog.trace.api.naming.SpanNaming; +import datadog.trace.api.tt.TransactionTrackingPatterns; import datadog.trace.bootstrap.instrumentation.api.AgentPropagation; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; import datadog.trace.bootstrap.instrumentation.api.AgentTracer; import datadog.trace.bootstrap.instrumentation.api.ClientIpAddressData; import datadog.trace.bootstrap.instrumentation.api.ErrorPriorities; +import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags; import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes; import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities; import datadog.trace.bootstrap.instrumentation.api.TagContext; @@ -44,8 +46,10 @@ import java.util.BitSet; import java.util.Locale; import java.util.Map; +import java.util.TreeSet; import java.util.concurrent.ExecutionException; import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import javax.annotation.Nonnull; @@ -119,6 +123,17 @@ protected String getRequestHeader(REQUEST request, String key) { return null; } + /** + * Iterates the names of every inbound HTTP request header, invoking {@code consumer} once per + * name. Default no-op implementation: subclasses with cheap access to the underlying request's + * header enumeration should override this so the Transaction Tracking extraction-sources tag + * works for that stack. Used only when {@link TransactionTrackingPatterns#isEmpty()} returns + * false. + */ + protected void forEachRequestHeaderName(REQUEST request, Consumer consumer) { + // no-op: stacks without cheap header enumeration silently produce no tag. + } + protected String requestedSessionId(REQUEST request) { return null; } @@ -344,6 +359,15 @@ public AgentSpan onRequest( } catch (final Exception e) { log.debug("Error tagging url", e); } + // Transaction Tracking: tag span with matching header / query-param names when the + // remote-config snapshot is non-empty. Fast path is a single volatile read + isEmpty(). + if (!TransactionTrackingPatterns.isEmpty()) { + try { + tagTransactionTrackingExtractionSources(span, request); + } catch (Exception e) { + log.debug("Error tagging tt extraction sources", e); + } + } } String peerIp = null; @@ -420,6 +444,111 @@ public AgentSpan onRequest( return span; } + /** + * Adds the {@code _dd.tt.extraction_sources} tag based on the currently active {@link + * TransactionTrackingPatterns} snapshot. Caller must have already verified that the snapshot is + * non-empty. + * + *

The tag value is a CSV with deterministic ordering: {@code header:} entries (sorted), then + * {@code qs:} entries (sorted). Names are lowercased and de-duplicated within each bucket. The + * tag is only set if at least one match is found. + */ + private void tagTransactionTrackingExtractionSources(AgentSpan span, REQUEST request) { + if (request == null) { + return; + } + TreeSet headerHits = null; + TreeSet qsHits = null; + + // 1. Header names. + HeaderNameCollector collector = new HeaderNameCollector<>(); + forEachRequestHeaderName(request, collector); + if (collector.matches != null) { + headerHits = collector.matches; + } + + // 2. Query-string parameter names. Re-resolve the URL adapter so this code path does not + // depend on whether the URL block above succeeded. + try { + URIDataAdapter url = url(request); + String rawQuery = url == null ? null : url.rawQuery(); + if (rawQuery != null && !rawQuery.isEmpty()) { + qsHits = collectQueryParameterMatches(rawQuery); + } + } catch (Exception e) { + log.debug("Error resolving URL for tt extraction sources", e); + } + + if (headerHits == null && qsHits == null) { + return; + } + StringBuilder sb = new StringBuilder(); + if (headerHits != null) { + for (String name : headerHits) { + if (sb.length() > 0) { + sb.append(','); + } + sb.append("header:").append(name); + } + } + if (qsHits != null) { + for (String name : qsHits) { + if (sb.length() > 0) { + sb.append(','); + } + sb.append("qs:").append(name); + } + } + if (sb.length() > 0) { + span.setTag(InstrumentationTags.TT_EXTRACTION_SOURCES, sb.toString()); + } + } + + private static TreeSet collectQueryParameterMatches(String rawQuery) { + TreeSet hits = null; + int len = rawQuery.length(); + int start = 0; + while (start <= len) { + int amp = rawQuery.indexOf('&', start); + int end = amp < 0 ? len : amp; + if (end > start) { + int eq = rawQuery.indexOf('=', start); + int nameEnd = (eq < 0 || eq > end) ? end : eq; + if (nameEnd > start) { + String name = rawQuery.substring(start, nameEnd); + if (TransactionTrackingPatterns.matchesAny(name)) { + if (hits == null) { + hits = new TreeSet<>(); + } + hits.add(name.toLowerCase(Locale.ROOT)); + } + } + } + if (amp < 0) { + break; + } + start = amp + 1; + } + return hits; + } + + private static final class HeaderNameCollector implements Consumer { + TreeSet matches; + + @Override + public void accept(String name) { + if (name == null) { + return; + } + if (TransactionTrackingPatterns.matchesAny(name)) { + if (matches == null) { + matches = new TreeSet<>(); + } + matches.add(name.toLowerCase(Locale.ROOT)); + } + } + } + protected static AgentSpanContext.Extracted getExtractedSpanContext(Context parentContext) { AgentSpan extractedSpan = AgentSpan.fromContext(parentContext); if (extractedSpan != null) { diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecoratorTtExtractionTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecoratorTtExtractionTest.groovy new file mode 100644 index 00000000000..87575d34152 --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecoratorTtExtractionTest.groovy @@ -0,0 +1,181 @@ +package datadog.trace.bootstrap.instrumentation.decorator + +import datadog.trace.api.tt.TransactionTrackingPatterns +import datadog.trace.bootstrap.instrumentation.api.AgentPropagation +import datadog.trace.bootstrap.instrumentation.api.AgentSpan +import datadog.trace.bootstrap.instrumentation.api.AgentTracer +import datadog.trace.bootstrap.instrumentation.api.AgentTracer.TracerAPI +import datadog.trace.bootstrap.instrumentation.api.ContextVisitors +import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags +import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter +import datadog.trace.bootstrap.instrumentation.api.URIDefaultDataAdapter +import datadog.trace.config.inversion.ConfigHelper +import datadog.trace.test.util.DDSpecification + +import java.util.function.Consumer + +class HttpServerDecoratorTtExtractionTest extends DDSpecification { + + def setupSpec() { + ConfigHelper.get().setConfigInversionStrict(ConfigHelper.StrictnessPolicy.TEST) + } + + def span = Mock(AgentSpan) + Map setTags = [:] + + void setup() { + TransactionTrackingPatterns.resetForTest() + span.setTag(_, _) >> { String k, Object v -> setTags[k] = v; null } + span.getTag(_) >> { String k -> setTags[k] } + } + + void cleanup() { + TransactionTrackingPatterns.resetForTest() + } + + def "no tag when pattern list is empty regardless of headers / qs"() { + setup: + def decorator = newDecorator(["X-Trace-Id", "tenant"], URI.create("http://h/p?tenant=42&debug=1")) + + when: + decorator.onRequest(span, null, [marker: "anything"], datadog.context.Context.root()) + + then: + // Fast path: no allocation, no tag. + setTags[InstrumentationTags.TT_EXTRACTION_SOURCES] == null + } + + def "tags matching headers and qs with deterministic order and lowercasing"() { + setup: + TransactionTrackingPatterns.update(["x-trace-*", "tenant", "*-id"]) + def decorator = newDecorator( + ["X-Trace-Id", "X-Trace-Source", "Authorization", "USER-ID"], + URI.create("http://h/p?tenant=42&debug=1&request-id=abc")) + + when: + decorator.onRequest(span, null, [marker: "anything"], datadog.context.Context.root()) + + then: + def csv = setTags[InstrumentationTags.TT_EXTRACTION_SOURCES] + csv != null + // headers first (sorted), then qs (sorted), all lowercased + deduped per bucket + csv == "header:user-id,header:x-trace-id,header:x-trace-source,qs:request-id,qs:tenant" + } + + def "headers only (no query string)"() { + setup: + TransactionTrackingPatterns.update(["x-foo"]) + def decorator = newDecorator(["X-FOO", "X-Bar"], URI.create("http://h/p")) + + when: + decorator.onRequest(span, null, [:], datadog.context.Context.root()) + + then: + setTags[InstrumentationTags.TT_EXTRACTION_SOURCES] == "header:x-foo" + } + + def "qs only (no header overrides)"() { + setup: + TransactionTrackingPatterns.update(["tenant*"]) + def decorator = newDecorator([], URI.create("http://h/p?tenantId=7&other=x")) + + when: + decorator.onRequest(span, null, [:], datadog.context.Context.root()) + + then: + setTags[InstrumentationTags.TT_EXTRACTION_SOURCES] == "qs:tenantid" + } + + def "no match means no tag even with non-empty patterns"() { + setup: + TransactionTrackingPatterns.update(["nope-*"]) + def decorator = newDecorator(["X-Foo"], URI.create("http://h/p?a=1")) + + when: + decorator.onRequest(span, null, [:], datadog.context.Context.root()) + + then: + setTags[InstrumentationTags.TT_EXTRACTION_SOURCES] == null + } + + def "duplicates within a bucket collapse to one entry"() { + setup: + TransactionTrackingPatterns.update(["x-trace-*"]) + def decorator = newDecorator(["X-Trace-Id", "x-trace-id", "X-TRACE-ID"], URI.create("http://h/p")) + + when: + decorator.onRequest(span, null, [:], datadog.context.Context.root()) + + then: + setTags[InstrumentationTags.TT_EXTRACTION_SOURCES] == "header:x-trace-id" + } + + def newDecorator(List headerNames, URI uri) { + return new HttpServerDecorator>() { + @Override + protected TracerAPI tracer() { + return AgentTracer.NOOP_TRACER + } + + @Override + protected String[] instrumentationNames() { + ["test1", "test2"] + } + + @Override + protected CharSequence component() { + "test-component" + } + + @Override + protected AgentPropagation.ContextVisitor> getter() { + return ContextVisitors.stringValuesMap() + } + + @Override + protected AgentPropagation.ContextVisitor responseGetter() { + null + } + + @Override + CharSequence spanName() { + "http-tt-span" + } + + @Override + protected String method(Map m) { + "GET" + } + + @Override + protected URIDataAdapter url(Map m) { + new URIDefaultDataAdapter(uri) + } + + @Override + protected String peerHostIP(Map m) { + null + } + + @Override + protected int peerPort(Map m) { + 0 + } + + @Override + protected int status(Map m) { + 0 + } + + @Override + protected String getRequestHeader(Map m, String key) { + null + } + + @Override + protected void forEachRequestHeaderName(Map m, Consumer consumer) { + headerNames.each { consumer.accept(it) } + } + } + } +} From bd58e17257652b431ed9badfa072270ec28e3b04 Mon Sep 17 00:00:00 2001 From: labbati Date: Fri, 15 May 2026 15:39:49 +0200 Subject: [PATCH 3/8] feat(tt): enumerate servlet request headers for tt extraction Overrides forEachRequestHeaderName in the javax-servlet 2.2 and 3.0 decorators using HttpServletRequest.getHeaderNames(), so Spring WebMVC running on top of any javax-servlet container (Tomcat, Jetty, etc.) emits the _dd.tt.extraction_sources tag transparently. Adds a Spring WebMVC 5.3 integration test (MockMvc) covering mixed header/qs matches and asserting absence of the tag when the pattern list is empty. The InstrumentationSpecification MOCK_DSM_TRACE_CONFIG now implements the new TraceConfig.getTransactionTrackingExtractionPatterns() with an empty list so the test infrastructure still compiles. tag: ai generated --- .../test/InstrumentationSpecification.groovy | 5 ++ .../servlet2/Servlet2Decorator.java | 22 ++++++ .../servlet3/Servlet3Decorator.java | 22 ++++++ .../test/tt/TtExtractionSpringBootTest.groovy | 79 +++++++++++++++++++ 4 files changed, 128 insertions(+) create mode 100644 dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/src/test/groovy/test/tt/TtExtractionSpringBootTest.groovy diff --git a/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/InstrumentationSpecification.groovy b/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/InstrumentationSpecification.groovy index 0c9bfd035b8..320407969fe 100644 --- a/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/InstrumentationSpecification.groovy +++ b/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/InstrumentationSpecification.groovy @@ -260,6 +260,11 @@ abstract class InstrumentationSpecification extends DDSpecification implements A List getDataStreamsTransactionExtractors() { return null } + + @Override + List getTransactionTrackingExtractionPatterns() { + return Collections.emptyList() + } } @SuppressFBWarnings(value = "AT_STALE_THREAD_WRITE_OF_PRIMITIVE", justification = "The variable is accessed only by the test thread in setup and cleanup.") diff --git a/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-2.2/src/main/java/datadog/trace/instrumentation/servlet2/Servlet2Decorator.java b/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-2.2/src/main/java/datadog/trace/instrumentation/servlet2/Servlet2Decorator.java index b93877f73ed..31f81ce85c1 100644 --- a/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-2.2/src/main/java/datadog/trace/instrumentation/servlet2/Servlet2Decorator.java +++ b/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-2.2/src/main/java/datadog/trace/instrumentation/servlet2/Servlet2Decorator.java @@ -8,6 +8,8 @@ import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import datadog.trace.bootstrap.instrumentation.decorator.HttpServerDecorator; +import java.util.Enumeration; +import java.util.function.Consumer; import javax.servlet.http.HttpServletRequest; public class Servlet2Decorator @@ -74,6 +76,26 @@ protected String getRequestHeader(final HttpServletRequest request, String key) return request.getHeader(key); } + @Override + @SuppressWarnings("unchecked") + protected void forEachRequestHeaderName( + final HttpServletRequest request, final Consumer consumer) { + if (request == null) { + return; + } + try { + Enumeration names = request.getHeaderNames(); + if (names == null) { + return; + } + while (names.hasMoreElements()) { + consumer.accept(names.nextElement()); + } + } catch (Throwable ignored) { + // best-effort + } + } + @Override protected int status(final Integer status) { return status; diff --git a/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-3.0/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Decorator.java b/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-3.0/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Decorator.java index 61a8d22a2b8..5733815135f 100644 --- a/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-3.0/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Decorator.java +++ b/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-3.0/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Decorator.java @@ -7,6 +7,8 @@ import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import datadog.trace.bootstrap.instrumentation.decorator.HttpServerDecorator; +import java.util.Enumeration; +import java.util.function.Consumer; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -82,6 +84,26 @@ protected String getRequestHeader(final HttpServletRequest request, String key) return request.getHeader(key); } + @Override + protected void forEachRequestHeaderName( + final HttpServletRequest request, final Consumer consumer) { + if (request == null) { + return; + } + try { + Enumeration names = request.getHeaderNames(); + if (names == null) { + return; + } + while (names.hasMoreElements()) { + consumer.accept(names.nextElement()); + } + } catch (Throwable ignored) { + // some containers throw if headers are accessed at the wrong lifecycle stage; + // silently skip — the tt extraction-sources tag is best-effort. + } + } + @Override public AgentSpan onRequest( final AgentSpan span, diff --git a/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/src/test/groovy/test/tt/TtExtractionSpringBootTest.groovy b/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/src/test/groovy/test/tt/TtExtractionSpringBootTest.groovy new file mode 100644 index 00000000000..97b6513a5ff --- /dev/null +++ b/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/src/test/groovy/test/tt/TtExtractionSpringBootTest.groovy @@ -0,0 +1,79 @@ +package test.tt + +import datadog.trace.agent.test.InstrumentationSpecification +import datadog.trace.api.DDSpanTypes +import datadog.trace.api.tt.TransactionTrackingPatterns +import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Controller +import org.springframework.test.web.servlet.MockMvc +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestMethod +import org.springframework.web.servlet.config.annotation.EnableWebMvc + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get + +@SpringBootTest(classes = TtExtractionSpringBootTest.TtController) +@EnableWebMvc +@AutoConfigureMockMvc +class TtExtractionSpringBootTest extends InstrumentationSpecification { + + @Controller + static class TtController { + @RequestMapping(value = "/tt", method = [RequestMethod.GET]) + ResponseEntity tt() { + return new ResponseEntity<>("ok", HttpStatus.OK) + } + } + + @Autowired + private MockMvc mvc + + def cleanup() { + TransactionTrackingPatterns.resetForTest() + } + + def 'sets _dd.tt.extraction_sources for mixed header and qs matches'() { + setup: + TransactionTrackingPatterns.update(["x-trace-*", "tenant", "*-id"]) + + when: + mvc.perform( + get("/tt?tenant=42&debug=1&request-id=abc") + .header("X-Trace-Id", "t1") + .header("X-Trace-Source", "test") + .header("Authorization", "secret") + ).andExpect({ res -> assert res.response.status == 200 }) + + TEST_WRITER.waitForTraces(1) + def serverSpan = TEST_WRITER.firstTrace().find { it.spanType == DDSpanTypes.HTTP_SERVER } + + then: + serverSpan != null + serverSpan.getTag(InstrumentationTags.TT_EXTRACTION_SOURCES) == + "header:x-trace-id,header:x-trace-source,qs:request-id,qs:tenant" + } + + def 'does not set the tag when the pattern list is empty'() { + setup: + TransactionTrackingPatterns.resetForTest() + assert TransactionTrackingPatterns.isEmpty() + + when: + mvc.perform( + get("/tt?tenant=42") + .header("X-Trace-Id", "t1") + ).andExpect({ res -> assert res.response.status == 200 }) + + TEST_WRITER.waitForTraces(1) + def serverSpan = TEST_WRITER.firstTrace().find { it.spanType == DDSpanTypes.HTTP_SERVER } + + then: + serverSpan != null + serverSpan.getTag(InstrumentationTags.TT_EXTRACTION_SOURCES) == null + } +} From 083f15fe0474516eb4c6d3fe040692eae9adbce5 Mon Sep 17 00:00:00 2001 From: labbati Date: Fri, 15 May 2026 15:40:23 +0200 Subject: [PATCH 4/8] docs(tt): document tt_extraction_patterns and the new span tag tag: ai generated tag: no release note --- docs/transaction_tracking_extraction.md | 50 +++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/transaction_tracking_extraction.md diff --git a/docs/transaction_tracking_extraction.md b/docs/transaction_tracking_extraction.md new file mode 100644 index 00000000000..3ce30f4ddc2 --- /dev/null +++ b/docs/transaction_tracking_extraction.md @@ -0,0 +1,50 @@ +# Transaction Tracking — extraction sources + +The tracer reads an optional list of `*`-glob patterns from the existing +`APM_TRACING` remote-config product under the field `tt_extraction_patterns`. + +When that list is non-empty, every server span gets a single tag +`_dd.tt.extraction_sources` whose value is a CSV of matching inbound HTTP +header names and query-string parameter names. + +## Wire format + +```json +{ + "lib_config": { + "tt_extraction_patterns": ["x-trace-*", "tenant", "*-id"] + } +} +``` + +- Patterns support only `*` (zero-or-more). Matching is case-insensitive on + the candidate name. Values are never inspected. +- Empty or missing list disables the feature; the next request is back to + a single volatile read + `isEmpty()` check (no allocation). + +## Tag shape + +- Tag key: `_dd.tt.extraction_sources` (constant + `InstrumentationTags.TT_EXTRACTION_SOURCES`). +- Value: deterministic CSV. `header:` entries are emitted + first in alphabetical order, followed by `qs:` entries + in alphabetical order. Duplicates within a bucket are collapsed. +- The tag is set only when at least one match is found. + +Example: with patterns `["x-trace-*", "tenant", "*-id"]` and an inbound +request bearing the headers `X-Trace-Id`, `X-Trace-Source`, `Authorization` +and the query string `?tenant=42&debug=1&request-id=abc`, the tag value is: + +``` +header:x-trace-id,header:x-trace-source,qs:request-id,qs:tenant +``` + +## Coverage + +The feature is implemented at the `HttpServerDecorator` layer, with the +`forEachRequestHeaderName` extension point overridden in the `javax-servlet` +2.2 and 3.0 decorators. Stacks built on top of those (Spring WebMVC, the +typical Tomcat / Jetty servlet path) get the tag transparently. Stacks +whose decorator does not override `forEachRequestHeaderName` (Netty, +Vert.x, WebFlux, …) fall back to the no-op default and silently produce +no tag until someone wires the override for that stack. From 425a40853762df21e190288a5975fea91cb38572 Mon Sep 17 00:00:00 2001 From: Luca Abbati Date: Sat, 16 May 2026 10:36:48 +0200 Subject: [PATCH 5/8] docs: remove transaction_tracking_extraction.md --- docs/transaction_tracking_extraction.md | 50 ------------------------- 1 file changed, 50 deletions(-) delete mode 100644 docs/transaction_tracking_extraction.md diff --git a/docs/transaction_tracking_extraction.md b/docs/transaction_tracking_extraction.md deleted file mode 100644 index 3ce30f4ddc2..00000000000 --- a/docs/transaction_tracking_extraction.md +++ /dev/null @@ -1,50 +0,0 @@ -# Transaction Tracking — extraction sources - -The tracer reads an optional list of `*`-glob patterns from the existing -`APM_TRACING` remote-config product under the field `tt_extraction_patterns`. - -When that list is non-empty, every server span gets a single tag -`_dd.tt.extraction_sources` whose value is a CSV of matching inbound HTTP -header names and query-string parameter names. - -## Wire format - -```json -{ - "lib_config": { - "tt_extraction_patterns": ["x-trace-*", "tenant", "*-id"] - } -} -``` - -- Patterns support only `*` (zero-or-more). Matching is case-insensitive on - the candidate name. Values are never inspected. -- Empty or missing list disables the feature; the next request is back to - a single volatile read + `isEmpty()` check (no allocation). - -## Tag shape - -- Tag key: `_dd.tt.extraction_sources` (constant - `InstrumentationTags.TT_EXTRACTION_SOURCES`). -- Value: deterministic CSV. `header:` entries are emitted - first in alphabetical order, followed by `qs:` entries - in alphabetical order. Duplicates within a bucket are collapsed. -- The tag is set only when at least one match is found. - -Example: with patterns `["x-trace-*", "tenant", "*-id"]` and an inbound -request bearing the headers `X-Trace-Id`, `X-Trace-Source`, `Authorization` -and the query string `?tenant=42&debug=1&request-id=abc`, the tag value is: - -``` -header:x-trace-id,header:x-trace-source,qs:request-id,qs:tenant -``` - -## Coverage - -The feature is implemented at the `HttpServerDecorator` layer, with the -`forEachRequestHeaderName` extension point overridden in the `javax-servlet` -2.2 and 3.0 decorators. Stacks built on top of those (Spring WebMVC, the -typical Tomcat / Jetty servlet path) get the tag transparently. Stacks -whose decorator does not override `forEachRequestHeaderName` (Netty, -Vert.x, WebFlux, …) fall back to the no-op default and silently produce -no tag until someone wires the override for that stack. From 31d7554435f3b46b985d4d097b37c22e248ea3e3 Mon Sep 17 00:00:00 2001 From: Luca Abbati Date: Sat, 16 May 2026 14:25:51 +0200 Subject: [PATCH 6/8] perf(tt): O(1) literal lookup and zero-alloc disabled path for tt patterns --- .../api/tt/TransactionTrackingPatterns.java | 193 ++++++++++++++++-- .../tt/TransactionTrackingPatternsTest.java | 78 +++++++ 2 files changed, 250 insertions(+), 21 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/tt/TransactionTrackingPatterns.java b/internal-api/src/main/java/datadog/trace/api/tt/TransactionTrackingPatterns.java index edc101ee085..ca1379f33ef 100644 --- a/internal-api/src/main/java/datadog/trace/api/tt/TransactionTrackingPatterns.java +++ b/internal-api/src/main/java/datadog/trace/api/tt/TransactionTrackingPatterns.java @@ -1,6 +1,7 @@ package datadog.trace.api.tt; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -12,8 +13,17 @@ * remote-config under the {@code APM_TRACING} product (field {@code tt_extraction_patterns}). * *

The hot path on every server request only does a single volatile read followed by an {@link - * List#isEmpty()} check when no patterns are configured, so this class is zero-allocation in the - * disabled case. + * Snapshot#isEmpty()} check when no patterns are configured, so this class is zero-allocation in + * the disabled case. + * + *

Patterns are partitioned at update time: + * + *

    + *
  • literal patterns (no {@code *}) go into a custom case-insensitive open-addressed hash set + * so that {@link #matchesAny(String)} is O(1) and does not allocate; + *
  • patterns containing at least one {@code *} go through {@link CompiledPattern#compile} and + * are matched linearly only when the literal lookup misses. + *
* *

Matching is case-insensitive on the candidate name and the supported wildcard alphabet is * limited to {@code *} (zero-or-more characters). The matcher is hand-rolled to avoid {@link @@ -23,10 +33,7 @@ public final class TransactionTrackingPatterns { private static final Logger log = LoggerFactory.getLogger(TransactionTrackingPatterns.class); - /** Shared empty snapshot — referenced when no patterns are configured. */ - private static final List EMPTY = Collections.emptyList(); - - private static volatile List snapshot = EMPTY; + private static volatile Snapshot snapshot = Snapshot.EMPTY; private TransactionTrackingPatterns() {} @@ -37,10 +44,11 @@ private TransactionTrackingPatterns() {} */ public static void update(List rawPatterns) { if (rawPatterns == null || rawPatterns.isEmpty()) { - snapshot = EMPTY; + snapshot = Snapshot.EMPTY; return; } - List compiled = new ArrayList<>(rawPatterns.size()); + List literalsLower = new ArrayList<>(rawPatterns.size()); + List wildcards = new ArrayList<>(); for (String raw : rawPatterns) { if (raw == null) { log.debug("Ignoring null tt_extraction_pattern entry"); @@ -51,13 +59,23 @@ public static void update(List rawPatterns) { log.debug("Ignoring blank tt_extraction_pattern entry"); continue; } - compiled.add(CompiledPattern.compile(trimmed)); + if (trimmed.indexOf('*') < 0) { + literalsLower.add(trimmed.toLowerCase(Locale.ROOT)); + } else { + wildcards.add(CompiledPattern.compile(trimmed)); + } } - if (compiled.isEmpty()) { - snapshot = EMPTY; - } else { - snapshot = Collections.unmodifiableList(compiled); + if (literalsLower.isEmpty() && wildcards.isEmpty()) { + snapshot = Snapshot.EMPTY; + return; } + CaseInsensitiveStringSet literalSet = + literalsLower.isEmpty() ? null : new CaseInsensitiveStringSet(literalsLower); + List wildcardList = + wildcards.isEmpty() + ? Collections.emptyList() + : Collections.unmodifiableList(wildcards); + snapshot = new Snapshot(literalSet, wildcardList); } /** Fast no-allocation check used as the hot-path guard. */ @@ -65,9 +83,29 @@ public static boolean isEmpty() { return snapshot.isEmpty(); } - /** Returns the current immutable snapshot. */ + /** + * Returns a snapshot view of the currently configured patterns. The returned list concatenates + * the literal patterns (rebuilt as {@link CompiledPattern} instances) and the wildcard patterns. + * This is intended for diagnostics/tests only; production code uses {@link #matchesAny(String)} + * and {@link #isEmpty()}. + */ public static List currentSnapshot() { - return snapshot; + Snapshot local = snapshot; + if (local.isEmpty()) { + return Collections.emptyList(); + } + List view = new ArrayList<>(); + if (local.literals != null) { + String[] table = local.literals.table; + for (int i = 0; i < table.length; i++) { + String entry = table[i]; + if (entry != null) { + view.add(CompiledPattern.compile(entry)); + } + } + } + view.addAll(local.wildcards); + return Collections.unmodifiableList(view); } /** True if {@code candidate} matches any compiled pattern in the current snapshot. */ @@ -75,14 +113,18 @@ public static boolean matchesAny(String candidate) { if (candidate == null) { return false; } - List local = snapshot; - if (local.isEmpty()) { + Snapshot local = snapshot; + if (local.literals != null && local.literals.contains(candidate)) { + return true; + } + List wildcards = local.wildcards; + int n = wildcards.size(); + if (n == 0) { return false; } String lowered = candidate.toLowerCase(Locale.ROOT); - // noinspection ForLoopReplaceableByForEach -- avoid iterator allocation on the hot path - for (int i = 0; i < local.size(); i++) { - if (local.get(i).matchesLowercased(lowered)) { + for (int i = 0; i < n; i++) { + if (wildcards.get(i).matchesLowercased(lowered)) { return true; } } @@ -91,7 +133,116 @@ public static boolean matchesAny(String candidate) { /** Test-only: replaces the snapshot atomically. */ public static void resetForTest() { - snapshot = EMPTY; + snapshot = Snapshot.EMPTY; + } + + /** Test-only: number of literal patterns in the current snapshot. */ + static int literalCountForTest() { + Snapshot local = snapshot; + return local.literals == null ? 0 : local.literals.size; + } + + /** Test-only: number of wildcard patterns in the current snapshot. */ + static int wildcardCountForTest() { + return snapshot.wildcards.size(); + } + + /** Immutable holder for the partitioned pattern set. */ + private static final class Snapshot { + static final Snapshot EMPTY = new Snapshot(null, Collections.emptyList()); + + /** Null when there are no literal patterns; otherwise non-empty. */ + final CaseInsensitiveStringSet literals; + + /** Possibly-empty immutable list of wildcard-bearing patterns. */ + final List wildcards; + + Snapshot(CaseInsensitiveStringSet literals, List wildcards) { + this.literals = literals; + this.wildcards = wildcards; + } + + boolean isEmpty() { + return (literals == null || literals.isEmpty()) && wildcards.isEmpty(); + } + } + + /** + * Open-addressed, power-of-two-sized, linearly probed case-insensitive string set. + * + *

Keys are stored lowercased once at construction; {@link #contains(String)} does NOT allocate + * and does NOT lowercase the candidate. The case-insensitive hash treats ASCII {@code A-Z} as + * their lowercase counterparts; non-ASCII characters are hashed verbatim and rely on {@link + * String#equalsIgnoreCase(String)} for correctness at the equality probe. + */ + static final class CaseInsensitiveStringSet { + final String[] table; + final int mask; + final int size; + + CaseInsensitiveStringSet(Collection lowercasedKeys) { + int n = lowercasedKeys.size(); + // capacity >= 8 and >= next-power-of-two of (2*n) to keep load factor <= 0.5 + int cap = 8; + int target = Math.max(1, n) * 2; + while (cap < target) { + cap <<= 1; + } + String[] t = new String[cap]; + int m = cap - 1; + int inserted = 0; + for (String k : lowercasedKeys) { + int slot = ciHash(k) & m; + boolean duplicate = false; + while (t[slot] != null) { + if (t[slot].equalsIgnoreCase(k)) { + duplicate = true; + break; + } + slot = (slot + 1) & m; + } + if (!duplicate) { + t[slot] = k; + inserted++; + } + } + this.table = t; + this.mask = m; + this.size = inserted; + } + + boolean isEmpty() { + return size == 0; + } + + boolean contains(String candidate) { + if (candidate == null || size == 0) { + return false; + } + int slot = ciHash(candidate) & mask; + while (true) { + String entry = table[slot]; + if (entry == null) { + return false; + } + if (entry.equalsIgnoreCase(candidate)) { + return true; + } + slot = (slot + 1) & mask; + } + } + + private static int ciHash(String s) { + int h = 0; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c >= 'A' && c <= 'Z') { + c = (char) (c + 32); + } + h = 31 * h + c; + } + return h; + } } /** diff --git a/internal-api/src/test/java/datadog/trace/api/tt/TransactionTrackingPatternsTest.java b/internal-api/src/test/java/datadog/trace/api/tt/TransactionTrackingPatternsTest.java index 159d7add80b..802574b33d9 100644 --- a/internal-api/src/test/java/datadog/trace/api/tt/TransactionTrackingPatternsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/tt/TransactionTrackingPatternsTest.java @@ -4,8 +4,10 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.List; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -104,6 +106,82 @@ void allBlankClearsSnapshot() { assertTrue(TransactionTrackingPatterns.isEmpty()); } + @Test + void matchesAny_literalSetFastPath() { + List patterns = new ArrayList<>(); + patterns.add("X-Request-Id"); + patterns.add("X-Tenant-Id"); + patterns.add("X-Customer-Id"); + patterns.add("X-Correlation-Id"); + patterns.add("X-Session-Id"); + patterns.add("X-Trace-Id"); + patterns.add("X-Span-Id"); + patterns.add("X-Account-Id"); + patterns.add("X-Order-Id"); + patterns.add("X-User-Id"); + patterns.add("x-debug-*"); + TransactionTrackingPatterns.update(patterns); + + assertEquals(10, TransactionTrackingPatterns.literalCountForTest()); + assertEquals(1, TransactionTrackingPatterns.wildcardCountForTest()); + + // literal matches (mixed case in candidates) + assertTrue(TransactionTrackingPatterns.matchesAny("x-request-id")); + assertTrue(TransactionTrackingPatterns.matchesAny("X-TENANT-ID")); + assertTrue(TransactionTrackingPatterns.matchesAny("x-Customer-Id")); + assertTrue(TransactionTrackingPatterns.matchesAny("X-correlation-id")); + assertTrue(TransactionTrackingPatterns.matchesAny("X-Session-Id")); + assertTrue(TransactionTrackingPatterns.matchesAny("x-TRACE-id")); + assertTrue(TransactionTrackingPatterns.matchesAny("X-Span-Id")); + assertTrue(TransactionTrackingPatterns.matchesAny("x-account-ID")); + assertTrue(TransactionTrackingPatterns.matchesAny("X-ORDER-ID")); + assertTrue(TransactionTrackingPatterns.matchesAny("X-User-Id")); + // wildcard match + assertTrue(TransactionTrackingPatterns.matchesAny("X-Debug-Flag")); + // unrelated name + assertFalse(TransactionTrackingPatterns.matchesAny("Content-Type")); + } + + @Test + void matchesAny_allLiteralsNoWildcardListWalk() { + TransactionTrackingPatterns.update( + Arrays.asList("a-one", "b-two", "c-three", "d-four", "e-five")); + assertEquals(5, TransactionTrackingPatterns.literalCountForTest()); + assertEquals(0, TransactionTrackingPatterns.wildcardCountForTest()); + assertTrue(TransactionTrackingPatterns.matchesAny("A-ONE")); + assertFalse(TransactionTrackingPatterns.matchesAny("missing")); + } + + @Test + void matchesAny_nullAndEmptyInputsSafe() { + // No patterns configured: null and empty candidates must not throw. + assertFalse(TransactionTrackingPatterns.matchesAny(null)); + assertFalse(TransactionTrackingPatterns.matchesAny("")); + // With patterns configured (literal + wildcard): same expectations. + TransactionTrackingPatterns.update(Arrays.asList("x-keep", "x-*")); + assertFalse(TransactionTrackingPatterns.matchesAny(null)); + // "x-*" matches empty-prefix; "" doesn't start with "x-" so should not match. + assertFalse(TransactionTrackingPatterns.matchesAny("")); + } + + @Test + void matchesAny_nonAsciiName() { + TransactionTrackingPatterns.update(Collections.singletonList("X-\u0422\u0435\u0441\u0442")); + assertTrue(TransactionTrackingPatterns.matchesAny("x-\u0422\u0435\u0441\u0442")); + assertFalse(TransactionTrackingPatterns.matchesAny("x-other")); + } + + @Test + void matchesAny_caseInsensitiveSetCollisions() { + // "x-a" and "x-i" both have (String.hashCode() & 7) == 4, so on a cap=8 table + // (the size used for 2 literals) they collide and exercise linear probing. + TransactionTrackingPatterns.update(Arrays.asList("x-a", "x-i")); + assertEquals(2, TransactionTrackingPatterns.literalCountForTest()); + assertTrue(TransactionTrackingPatterns.matchesAny("X-A")); + assertTrue(TransactionTrackingPatterns.matchesAny("X-I")); + assertFalse(TransactionTrackingPatterns.matchesAny("X-B")); + } + @Test void overlappingSegmentsDoNotMatch() { TransactionTrackingPatterns.update(Collections.singletonList("ab*ab")); From 7d6cbf62febb9cb8c66edd588e060194a154dec8 Mon Sep 17 00:00:00 2001 From: Luca Abbati Date: Sat, 16 May 2026 14:52:01 +0200 Subject: [PATCH 7/8] feat(rc): advertise APM_TRACING_TT_EXTRACTION_PATTERNS capability Set bit 49 of the tracer's RC client_capabilities so the agent's RC proxy knows this tracer understands the tt_extraction_patterns field on APM_TRACING configurations. --- .../src/main/java/datadog/trace/core/TracingConfigPoller.java | 4 +++- .../src/main/java/datadog/remoteconfig/Capabilities.java | 1 + .../DefaultConfigurationPollerSpecification.groovy | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java b/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java index 0e712a88cf2..9268db58054 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java @@ -12,6 +12,7 @@ import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_SAMPLE_RATE; import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_SAMPLE_RULES; import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_TRACING_ENABLED; +import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_TT_EXTRACTION_PATTERNS; import static datadog.trace.api.sampling.SamplingRule.normalizeGlob; import com.squareup.moshi.FromJson; @@ -79,7 +80,8 @@ public void start(Config config, SharedCommunicationObjects sco) { | CAPABILITY_APM_TRACING_ENABLE_EXCEPTION_REPLAY | CAPABILITY_APM_TRACING_ENABLE_CODE_ORIGIN | CAPABILITY_APM_TRACING_ENABLE_LIVE_DEBUGGING - | CAPABILITY_APM_TRACING_MULTICONFIG); + | CAPABILITY_APM_TRACING_MULTICONFIG + | CAPABILITY_APM_TRACING_TT_EXTRACTION_PATTERNS); } stopPolling = new Updater().register(config, configPoller); } diff --git a/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java b/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java index ce76e59000a..78642306d04 100644 --- a/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java +++ b/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java @@ -47,4 +47,5 @@ public interface Capabilities { long CAPABILITY_ASM_EXTENDED_DATA_COLLECTION = 1L << 44; long CAPABILITY_APM_TRACING_MULTICONFIG = 1L << 45; long CAPABILITY_FFE_FLAG_CONFIGURATION_RULES = 1L << 46; + long CAPABILITY_APM_TRACING_TT_EXTRACTION_PATTERNS = 1L << 49; } diff --git a/remote-config/remote-config-core/src/test/groovy/datadog/remoteconfig/DefaultConfigurationPollerSpecification.groovy b/remote-config/remote-config-core/src/test/groovy/datadog/remoteconfig/DefaultConfigurationPollerSpecification.groovy index ae18bd5f839..0446f6a4cad 100644 --- a/remote-config/remote-config-core/src/test/groovy/datadog/remoteconfig/DefaultConfigurationPollerSpecification.groovy +++ b/remote-config/remote-config-core/src/test/groovy/datadog/remoteconfig/DefaultConfigurationPollerSpecification.groovy @@ -1554,6 +1554,7 @@ class DefaultConfigurationPollerSpecification extends DDSpecification { 14L | [14] as byte[] 1 << 8 | [1, 0] as byte[] 1 << 9 | [2, 0] as byte[] + 1L << 49 | [2, 0, 0, 0, 0, 0, 0] as byte[] -9223372036854775807L | [128, 0, 0, 0, 0, 0, 0, 1] as byte[] } From 22e2e262cd1725f8e298002c85bece9c23ccb33d Mon Sep 17 00:00:00 2001 From: Luca Abbati Date: Mon, 18 May 2026 21:25:27 -0400 Subject: [PATCH 8/8] refactor(tt): rename discovery-phase symbols to candidate-source terminology Phase 1 of Transaction Tracking is candidate-source *discovery*; phase 2 (future PR) will be the actual value *extraction*. Free the word "extraction" from the discovery-phase code so it stays available for phase 2. Mechanical rename across the discovery-phase code added on this branch: - TransactionTrackingPatterns -> TransactionTrackingCandidateSources - TT_EXTRACTION_SOURCES tag -> TT_CANDIDATE_SOURCES (_dd.tt.candidate_sources) - tt_extraction_patterns field -> tt_candidate_source_patterns - ttExtractionPatterns (Moshi DTO) -> ttCandidateSourcePatterns - Builder/Config/TraceConfig getters/setters renamed accordingly - CAPABILITY_APM_TRACING_TT_EXTRACTION_PATTERNS -> CAPABILITY_APM_TRACING_TT_CANDIDATE_SOURCE_PATTERNS - HttpServerDecorator.tagTransactionTrackingExtractionSources -> tagCandidateSources - Spock specs HttpServerDecoratorTtExtractionTest / TtExtractionSpringBootTest renamed RC capability bit index stays 49. No behaviour change. tag: ai generated tag: no release note --- .../decorator/HttpServerDecorator.java | 28 +-- ...verDecoratorTtCandidateSourcesTest.groovy} | 30 +-- .../test/InstrumentationSpecification.groovy | 2 +- .../servlet3/Servlet3Decorator.java | 2 +- ...> TtCandidateSourcesSpringBootTest.groovy} | 20 +- .../trace/core/TracingConfigPoller.java | 15 +- .../trace/core/TracingConfigPollerTest.java | 38 ++-- .../main/java/datadog/trace/api/Config.java | 8 +- .../java/datadog/trace/api/DynamicConfig.java | 26 +-- .../java/datadog/trace/api/TraceConfig.java | 2 +- ... TransactionTrackingCandidateSources.java} | 15 +- .../instrumentation/api/AgentTracer.java | 2 +- .../api/InstrumentationTags.java | 4 +- ...ansactionTrackingCandidateSourcesTest.java | 194 ++++++++++++++++++ .../tt/TransactionTrackingPatternsTest.java | 193 ----------------- .../datadog/remoteconfig/Capabilities.java | 2 +- 16 files changed, 292 insertions(+), 289 deletions(-) rename dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/{HttpServerDecoratorTtExtractionTest.groovy => HttpServerDecoratorTtCandidateSourcesTest.groovy} (82%) rename dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/src/test/groovy/test/tt/{TtExtractionSpringBootTest.groovy => TtCandidateSourcesSpringBootTest.groovy} (75%) rename internal-api/src/main/java/datadog/trace/api/tt/{TransactionTrackingPatterns.java => TransactionTrackingCandidateSources.java} (95%) create mode 100644 internal-api/src/test/java/datadog/trace/api/tt/TransactionTrackingCandidateSourcesTest.java delete mode 100644 internal-api/src/test/java/datadog/trace/api/tt/TransactionTrackingPatternsTest.java diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java index 346423f5f1e..d7cfa87f9c1 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java @@ -26,7 +26,7 @@ import datadog.trace.api.gateway.RequestContext; import datadog.trace.api.gateway.RequestContextSlot; import datadog.trace.api.naming.SpanNaming; -import datadog.trace.api.tt.TransactionTrackingPatterns; +import datadog.trace.api.tt.TransactionTrackingCandidateSources; import datadog.trace.bootstrap.instrumentation.api.AgentPropagation; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; @@ -126,8 +126,8 @@ protected String getRequestHeader(REQUEST request, String key) { /** * Iterates the names of every inbound HTTP request header, invoking {@code consumer} once per * name. Default no-op implementation: subclasses with cheap access to the underlying request's - * header enumeration should override this so the Transaction Tracking extraction-sources tag - * works for that stack. Used only when {@link TransactionTrackingPatterns#isEmpty()} returns + * header enumeration should override this so the Transaction Tracking candidate-sources tag works + * for that stack. Used only when {@link TransactionTrackingCandidateSources#isEmpty()} returns * false. */ protected void forEachRequestHeaderName(REQUEST request, Consumer consumer) { @@ -361,11 +361,11 @@ public AgentSpan onRequest( } // Transaction Tracking: tag span with matching header / query-param names when the // remote-config snapshot is non-empty. Fast path is a single volatile read + isEmpty(). - if (!TransactionTrackingPatterns.isEmpty()) { + if (!TransactionTrackingCandidateSources.isEmpty()) { try { - tagTransactionTrackingExtractionSources(span, request); + tagCandidateSources(span, request); } catch (Exception e) { - log.debug("Error tagging tt extraction sources", e); + log.debug("Error tagging tt candidate sources", e); } } } @@ -445,15 +445,15 @@ public AgentSpan onRequest( } /** - * Adds the {@code _dd.tt.extraction_sources} tag based on the currently active {@link - * TransactionTrackingPatterns} snapshot. Caller must have already verified that the snapshot is - * non-empty. + * Adds the {@code _dd.tt.candidate_sources} tag based on the currently active {@link + * TransactionTrackingCandidateSources} snapshot. Caller must have already verified that the + * snapshot is non-empty. * *

The tag value is a CSV with deterministic ordering: {@code header:} entries (sorted), then * {@code qs:} entries (sorted). Names are lowercased and de-duplicated within each bucket. The * tag is only set if at least one match is found. */ - private void tagTransactionTrackingExtractionSources(AgentSpan span, REQUEST request) { + private void tagCandidateSources(AgentSpan span, REQUEST request) { if (request == null) { return; } @@ -476,7 +476,7 @@ private void tagTransactionTrackingExtractionSources(AgentSpan span, REQUEST req qsHits = collectQueryParameterMatches(rawQuery); } } catch (Exception e) { - log.debug("Error resolving URL for tt extraction sources", e); + log.debug("Error resolving URL for tt candidate sources", e); } if (headerHits == null && qsHits == null) { @@ -500,7 +500,7 @@ private void tagTransactionTrackingExtractionSources(AgentSpan span, REQUEST req } } if (sb.length() > 0) { - span.setTag(InstrumentationTags.TT_EXTRACTION_SOURCES, sb.toString()); + span.setTag(InstrumentationTags.TT_CANDIDATE_SOURCES, sb.toString()); } } @@ -516,7 +516,7 @@ private static TreeSet collectQueryParameterMatches(String rawQuery) { int nameEnd = (eq < 0 || eq > end) ? end : eq; if (nameEnd > start) { String name = rawQuery.substring(start, nameEnd); - if (TransactionTrackingPatterns.matchesAny(name)) { + if (TransactionTrackingCandidateSources.matchesAny(name)) { if (hits == null) { hits = new TreeSet<>(); } @@ -540,7 +540,7 @@ public void accept(String name) { if (name == null) { return; } - if (TransactionTrackingPatterns.matchesAny(name)) { + if (TransactionTrackingCandidateSources.matchesAny(name)) { if (matches == null) { matches = new TreeSet<>(); } diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecoratorTtExtractionTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecoratorTtCandidateSourcesTest.groovy similarity index 82% rename from dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecoratorTtExtractionTest.groovy rename to dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecoratorTtCandidateSourcesTest.groovy index 87575d34152..1bed585420d 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecoratorTtExtractionTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecoratorTtCandidateSourcesTest.groovy @@ -1,6 +1,6 @@ package datadog.trace.bootstrap.instrumentation.decorator -import datadog.trace.api.tt.TransactionTrackingPatterns +import datadog.trace.api.tt.TransactionTrackingCandidateSources import datadog.trace.bootstrap.instrumentation.api.AgentPropagation import datadog.trace.bootstrap.instrumentation.api.AgentSpan import datadog.trace.bootstrap.instrumentation.api.AgentTracer @@ -14,7 +14,7 @@ import datadog.trace.test.util.DDSpecification import java.util.function.Consumer -class HttpServerDecoratorTtExtractionTest extends DDSpecification { +class HttpServerDecoratorTtCandidateSourcesTest extends DDSpecification { def setupSpec() { ConfigHelper.get().setConfigInversionStrict(ConfigHelper.StrictnessPolicy.TEST) @@ -24,13 +24,13 @@ class HttpServerDecoratorTtExtractionTest extends DDSpecification { Map setTags = [:] void setup() { - TransactionTrackingPatterns.resetForTest() + TransactionTrackingCandidateSources.resetForTest() span.setTag(_, _) >> { String k, Object v -> setTags[k] = v; null } span.getTag(_) >> { String k -> setTags[k] } } void cleanup() { - TransactionTrackingPatterns.resetForTest() + TransactionTrackingCandidateSources.resetForTest() } def "no tag when pattern list is empty regardless of headers / qs"() { @@ -42,12 +42,12 @@ class HttpServerDecoratorTtExtractionTest extends DDSpecification { then: // Fast path: no allocation, no tag. - setTags[InstrumentationTags.TT_EXTRACTION_SOURCES] == null + setTags[InstrumentationTags.TT_CANDIDATE_SOURCES] == null } def "tags matching headers and qs with deterministic order and lowercasing"() { setup: - TransactionTrackingPatterns.update(["x-trace-*", "tenant", "*-id"]) + TransactionTrackingCandidateSources.update(["x-trace-*", "tenant", "*-id"]) def decorator = newDecorator( ["X-Trace-Id", "X-Trace-Source", "Authorization", "USER-ID"], URI.create("http://h/p?tenant=42&debug=1&request-id=abc")) @@ -56,7 +56,7 @@ class HttpServerDecoratorTtExtractionTest extends DDSpecification { decorator.onRequest(span, null, [marker: "anything"], datadog.context.Context.root()) then: - def csv = setTags[InstrumentationTags.TT_EXTRACTION_SOURCES] + def csv = setTags[InstrumentationTags.TT_CANDIDATE_SOURCES] csv != null // headers first (sorted), then qs (sorted), all lowercased + deduped per bucket csv == "header:user-id,header:x-trace-id,header:x-trace-source,qs:request-id,qs:tenant" @@ -64,50 +64,50 @@ class HttpServerDecoratorTtExtractionTest extends DDSpecification { def "headers only (no query string)"() { setup: - TransactionTrackingPatterns.update(["x-foo"]) + TransactionTrackingCandidateSources.update(["x-foo"]) def decorator = newDecorator(["X-FOO", "X-Bar"], URI.create("http://h/p")) when: decorator.onRequest(span, null, [:], datadog.context.Context.root()) then: - setTags[InstrumentationTags.TT_EXTRACTION_SOURCES] == "header:x-foo" + setTags[InstrumentationTags.TT_CANDIDATE_SOURCES] == "header:x-foo" } def "qs only (no header overrides)"() { setup: - TransactionTrackingPatterns.update(["tenant*"]) + TransactionTrackingCandidateSources.update(["tenant*"]) def decorator = newDecorator([], URI.create("http://h/p?tenantId=7&other=x")) when: decorator.onRequest(span, null, [:], datadog.context.Context.root()) then: - setTags[InstrumentationTags.TT_EXTRACTION_SOURCES] == "qs:tenantid" + setTags[InstrumentationTags.TT_CANDIDATE_SOURCES] == "qs:tenantid" } def "no match means no tag even with non-empty patterns"() { setup: - TransactionTrackingPatterns.update(["nope-*"]) + TransactionTrackingCandidateSources.update(["nope-*"]) def decorator = newDecorator(["X-Foo"], URI.create("http://h/p?a=1")) when: decorator.onRequest(span, null, [:], datadog.context.Context.root()) then: - setTags[InstrumentationTags.TT_EXTRACTION_SOURCES] == null + setTags[InstrumentationTags.TT_CANDIDATE_SOURCES] == null } def "duplicates within a bucket collapse to one entry"() { setup: - TransactionTrackingPatterns.update(["x-trace-*"]) + TransactionTrackingCandidateSources.update(["x-trace-*"]) def decorator = newDecorator(["X-Trace-Id", "x-trace-id", "X-TRACE-ID"], URI.create("http://h/p")) when: decorator.onRequest(span, null, [:], datadog.context.Context.root()) then: - setTags[InstrumentationTags.TT_EXTRACTION_SOURCES] == "header:x-trace-id" + setTags[InstrumentationTags.TT_CANDIDATE_SOURCES] == "header:x-trace-id" } def newDecorator(List headerNames, URI uri) { diff --git a/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/InstrumentationSpecification.groovy b/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/InstrumentationSpecification.groovy index 320407969fe..cf96371e9cc 100644 --- a/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/InstrumentationSpecification.groovy +++ b/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/InstrumentationSpecification.groovy @@ -262,7 +262,7 @@ abstract class InstrumentationSpecification extends DDSpecification implements A } @Override - List getTransactionTrackingExtractionPatterns() { + List getTransactionTrackingCandidateSourcePatterns() { return Collections.emptyList() } } diff --git a/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-3.0/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Decorator.java b/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-3.0/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Decorator.java index 5733815135f..2cd69380aca 100644 --- a/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-3.0/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Decorator.java +++ b/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-3.0/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Decorator.java @@ -100,7 +100,7 @@ protected void forEachRequestHeaderName( } } catch (Throwable ignored) { // some containers throw if headers are accessed at the wrong lifecycle stage; - // silently skip — the tt extraction-sources tag is best-effort. + // silently skip — the tt candidate-sources tag is best-effort. } } diff --git a/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/src/test/groovy/test/tt/TtExtractionSpringBootTest.groovy b/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/src/test/groovy/test/tt/TtCandidateSourcesSpringBootTest.groovy similarity index 75% rename from dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/src/test/groovy/test/tt/TtExtractionSpringBootTest.groovy rename to dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/src/test/groovy/test/tt/TtCandidateSourcesSpringBootTest.groovy index 97b6513a5ff..e3da7262f5a 100644 --- a/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/src/test/groovy/test/tt/TtExtractionSpringBootTest.groovy +++ b/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/src/test/groovy/test/tt/TtCandidateSourcesSpringBootTest.groovy @@ -2,7 +2,7 @@ package test.tt import datadog.trace.agent.test.InstrumentationSpecification import datadog.trace.api.DDSpanTypes -import datadog.trace.api.tt.TransactionTrackingPatterns +import datadog.trace.api.tt.TransactionTrackingCandidateSources import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc @@ -17,10 +17,10 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get -@SpringBootTest(classes = TtExtractionSpringBootTest.TtController) +@SpringBootTest(classes = TtCandidateSourcesSpringBootTest.TtController) @EnableWebMvc @AutoConfigureMockMvc -class TtExtractionSpringBootTest extends InstrumentationSpecification { +class TtCandidateSourcesSpringBootTest extends InstrumentationSpecification { @Controller static class TtController { @@ -34,12 +34,12 @@ class TtExtractionSpringBootTest extends InstrumentationSpecification { private MockMvc mvc def cleanup() { - TransactionTrackingPatterns.resetForTest() + TransactionTrackingCandidateSources.resetForTest() } - def 'sets _dd.tt.extraction_sources for mixed header and qs matches'() { + def 'sets _dd.tt.candidate_sources for mixed header and qs matches'() { setup: - TransactionTrackingPatterns.update(["x-trace-*", "tenant", "*-id"]) + TransactionTrackingCandidateSources.update(["x-trace-*", "tenant", "*-id"]) when: mvc.perform( @@ -54,14 +54,14 @@ class TtExtractionSpringBootTest extends InstrumentationSpecification { then: serverSpan != null - serverSpan.getTag(InstrumentationTags.TT_EXTRACTION_SOURCES) == + serverSpan.getTag(InstrumentationTags.TT_CANDIDATE_SOURCES) == "header:x-trace-id,header:x-trace-source,qs:request-id,qs:tenant" } def 'does not set the tag when the pattern list is empty'() { setup: - TransactionTrackingPatterns.resetForTest() - assert TransactionTrackingPatterns.isEmpty() + TransactionTrackingCandidateSources.resetForTest() + assert TransactionTrackingCandidateSources.isEmpty() when: mvc.perform( @@ -74,6 +74,6 @@ class TtExtractionSpringBootTest extends InstrumentationSpecification { then: serverSpan != null - serverSpan.getTag(InstrumentationTags.TT_EXTRACTION_SOURCES) == null + serverSpan.getTag(InstrumentationTags.TT_CANDIDATE_SOURCES) == null } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java b/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java index 9268db58054..48df4f8e41c 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java @@ -12,7 +12,7 @@ import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_SAMPLE_RATE; import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_SAMPLE_RULES; import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_TRACING_ENABLED; -import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_TT_EXTRACTION_PATTERNS; +import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_TT_CANDIDATE_SOURCE_PATTERNS; import static datadog.trace.api.sampling.SamplingRule.normalizeGlob; import com.squareup.moshi.FromJson; @@ -81,7 +81,7 @@ public void start(Config config, SharedCommunicationObjects sco) { | CAPABILITY_APM_TRACING_ENABLE_CODE_ORIGIN | CAPABILITY_APM_TRACING_ENABLE_LIVE_DEBUGGING | CAPABILITY_APM_TRACING_MULTICONFIG - | CAPABILITY_APM_TRACING_TT_EXTRACTION_PATTERNS); + | CAPABILITY_APM_TRACING_TT_CANDIDATE_SOURCE_PATTERNS); } stopPolling = new Updater().register(config, configPoller); } @@ -259,7 +259,8 @@ void applyConfigOverrides(LibConfig libConfig) { maybeOverride(builder::setTracingTags, parseTagListToMap(libConfig.tracingTags)); maybeOverride( - builder::setTransactionTrackingExtractionPatterns, libConfig.ttExtractionPatterns); + builder::setTransactionTrackingCandidateSourcePatterns, + libConfig.ttCandidateSourcePatterns); DebuggerConfigBridge.updateConfig( new DebuggerConfigUpdate( libConfig.dynamicInstrumentationEnabled, @@ -423,8 +424,8 @@ static final class LibConfig { @Json(name = "data_streams_transaction_extractors") public DataStreamsTransactionExtractors dataStreamsTransactionExtractors; - @Json(name = "tt_extraction_patterns") - public List ttExtractionPatterns; + @Json(name = "tt_candidate_source_patterns") + public List ttCandidateSourcePatterns; /** * Merges a list of LibConfig objects by taking the first non-null value for each field. @@ -489,8 +490,8 @@ public static LibConfig mergeLibConfigs(List configs) { if (merged.liveDebuggingEnabled == null) { merged.liveDebuggingEnabled = config.liveDebuggingEnabled; } - if (merged.ttExtractionPatterns == null) { - merged.ttExtractionPatterns = config.ttExtractionPatterns; + if (merged.ttCandidateSourcePatterns == null) { + merged.ttCandidateSourcePatterns = config.ttCandidateSourcePatterns; } } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/TracingConfigPollerTest.java b/dd-trace-core/src/test/java/datadog/trace/core/TracingConfigPollerTest.java index d45773f103e..619a8a01e7f 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/TracingConfigPollerTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/TracingConfigPollerTest.java @@ -19,7 +19,7 @@ import datadog.remoteconfig.state.ParsedConfigKey; import datadog.remoteconfig.state.ProductListener; import datadog.trace.api.datastreams.DataStreamsTransactionExtractor; -import datadog.trace.api.tt.TransactionTrackingPatterns; +import datadog.trace.api.tt.TransactionTrackingCandidateSources; import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -221,7 +221,7 @@ void actualConfigCommitWithServiceAndOrgLevelConfigs() throws Exception { } @Test - void ttExtractionPatternsArePropagatedAndPublished() throws Exception { + void ttCandidateSourcePatternsArePropagatedAndPublished() throws Exception { ParsedConfigKey key = ParsedConfigKey.parse("datadog/2/APM_TRACING/org_config/config"); ConfigurationPoller poller = mock(ConfigurationPoller.class); SharedCommunicationObjects sco = createScoWithPoller(poller); @@ -240,11 +240,11 @@ void ttExtractionPatternsArePropagatedAndPublished() throws Exception { unclosedTracers.add(tracer); try { - TransactionTrackingPatterns.resetForTest(); + TransactionTrackingCandidateSources.resetForTest(); assertEquals( Collections.emptyList(), - tracer.captureTraceConfig().getTransactionTrackingExtractionPatterns()); - assertTrue(TransactionTrackingPatterns.isEmpty()); + tracer.captureTraceConfig().getTransactionTrackingCandidateSourcePatterns()); + assertTrue(TransactionTrackingCandidateSources.isEmpty()); ProductListener updater = capturedUpdater[0]; updater.accept( @@ -252,7 +252,7 @@ void ttExtractionPatternsArePropagatedAndPublished() throws Exception { ("{\n" + " \"service_target\": {\"service\": \"*\", \"env\": \"*\"},\n" + " \"lib_config\": {\n" - + " \"tt_extraction_patterns\": [\"x-trace-*\", \"*-tenant\"]\n" + + " \"tt_candidate_source_patterns\": [\"x-trace-*\", \"*-tenant\"]\n" + " }\n" + "}") .getBytes(StandardCharsets.UTF_8), @@ -261,27 +261,27 @@ void ttExtractionPatternsArePropagatedAndPublished() throws Exception { assertEquals( Arrays.asList("x-trace-*", "*-tenant"), - tracer.captureTraceConfig().getTransactionTrackingExtractionPatterns()); - assertFalse(TransactionTrackingPatterns.isEmpty()); - assertTrue(TransactionTrackingPatterns.matchesAny("X-Trace-Id")); - assertTrue(TransactionTrackingPatterns.matchesAny("customer-tenant")); - assertFalse(TransactionTrackingPatterns.matchesAny("unrelated")); + tracer.captureTraceConfig().getTransactionTrackingCandidateSourcePatterns()); + assertFalse(TransactionTrackingCandidateSources.isEmpty()); + assertTrue(TransactionTrackingCandidateSources.matchesAny("X-Trace-Id")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("customer-tenant")); + assertFalse(TransactionTrackingCandidateSources.matchesAny("unrelated")); // Removing the config should clear the static snapshot back to empty. updater.remove(key, null); updater.commit(null); assertEquals( Collections.emptyList(), - tracer.captureTraceConfig().getTransactionTrackingExtractionPatterns()); - assertTrue(TransactionTrackingPatterns.isEmpty()); + tracer.captureTraceConfig().getTransactionTrackingCandidateSourcePatterns()); + assertTrue(TransactionTrackingCandidateSources.isEmpty()); } finally { - TransactionTrackingPatterns.resetForTest(); + TransactionTrackingCandidateSources.resetForTest(); tracer.close(); } } @Test - void absentTtExtractionPatternsFieldKeepsSnapshotEmpty() throws Exception { + void absentTtCandidateSourcePatternsFieldKeepsSnapshotEmpty() throws Exception { ParsedConfigKey key = ParsedConfigKey.parse("datadog/2/APM_TRACING/org_config/config"); ConfigurationPoller poller = mock(ConfigurationPoller.class); SharedCommunicationObjects sco = createScoWithPoller(poller); @@ -300,7 +300,7 @@ void absentTtExtractionPatternsFieldKeepsSnapshotEmpty() throws Exception { unclosedTracers.add(tracer); try { - TransactionTrackingPatterns.resetForTest(); + TransactionTrackingCandidateSources.resetForTest(); ProductListener updater = capturedUpdater[0]; updater.accept( key, @@ -314,10 +314,10 @@ void absentTtExtractionPatternsFieldKeepsSnapshotEmpty() throws Exception { assertEquals( Collections.emptyList(), - tracer.captureTraceConfig().getTransactionTrackingExtractionPatterns()); - assertTrue(TransactionTrackingPatterns.isEmpty()); + tracer.captureTraceConfig().getTransactionTrackingCandidateSourcePatterns()); + assertTrue(TransactionTrackingCandidateSources.isEmpty()); } finally { - TransactionTrackingPatterns.resetForTest(); + TransactionTrackingCandidateSources.resetForTest(); tracer.close(); } } diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index 094518eddab..33727fb7dd8 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -4873,11 +4873,11 @@ public String getDataStreamsTransactionExtractors() { } /** - * Static fallback for Transaction Tracking extraction patterns. Always returns an empty list; - * non-empty values are only delivered through {@code APM_TRACING} remote-config (field {@code - * tt_extraction_patterns}). + * Static fallback for Transaction Tracking candidate-source patterns. Always returns an empty + * list; non-empty values are only delivered through {@code APM_TRACING} remote-config (field + * {@code tt_candidate_source_patterns}). */ - public java.util.List getTransactionTrackingExtractionPatterns() { + public java.util.List getTransactionTrackingCandidateSourcePatterns() { return java.util.Collections.emptyList(); } diff --git a/internal-api/src/main/java/datadog/trace/api/DynamicConfig.java b/internal-api/src/main/java/datadog/trace/api/DynamicConfig.java index 1e314dc129b..34efbc56a6c 100644 --- a/internal-api/src/main/java/datadog/trace/api/DynamicConfig.java +++ b/internal-api/src/main/java/datadog/trace/api/DynamicConfig.java @@ -17,7 +17,7 @@ import datadog.trace.api.datastreams.DataStreamsTransactionExtractor; import datadog.trace.api.sampling.SamplingRule.SpanSamplingRule; import datadog.trace.api.sampling.SamplingRule.TraceSamplingRule; -import datadog.trace.api.tt.TransactionTrackingPatterns; +import datadog.trace.api.tt.TransactionTrackingCandidateSources; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -88,7 +88,7 @@ public Builder current() { public void resetTraceConfig() { currentSnapshot = initialSnapshot; reportConfigChange(initialSnapshot); - TransactionTrackingPatterns.update(initialSnapshot.ttExtractionPatterns); + TransactionTrackingCandidateSources.update(initialSnapshot.ttCandidateSourcePatterns); } @Override @@ -115,7 +115,7 @@ public final class Builder { Pair preferredServiceNameAndSource; List dataStreamsTransactionExtractors; - List ttExtractionPatterns; + List ttCandidateSourcePatterns; Builder() {} @@ -140,7 +140,7 @@ public final class Builder { this.preferredServiceNameAndSource = snapshot.preferredServiceNameAndSource; this.dataStreamsTransactionExtractors = snapshot.dataStreamsTransactionExtractors; - this.ttExtractionPatterns = snapshot.ttExtractionPatterns; + this.ttCandidateSourcePatterns = snapshot.ttCandidateSourcePatterns; } public Builder setRuntimeMetricsEnabled(boolean runtimeMetricsEnabled) { @@ -240,8 +240,8 @@ public Builder setPreferredServiceNameAndSource( * Sets the list of {@code *}-glob patterns used by Transaction Tracking to flag inbound HTTP * header / query-string parameter names. A {@code null} or empty list disables the feature. */ - public Builder setTransactionTrackingExtractionPatterns(List patterns) { - this.ttExtractionPatterns = patterns; + public Builder setTransactionTrackingCandidateSourcePatterns(List patterns) { + this.ttCandidateSourcePatterns = patterns; return this; } @@ -257,7 +257,7 @@ public DynamicConfig apply() { reportConfigChange(newSnapshot); } // Publish the compiled snapshot to the static holder used on the request hot path. - TransactionTrackingPatterns.update(newSnapshot.ttExtractionPatterns); + TransactionTrackingCandidateSources.update(newSnapshot.ttCandidateSourcePatterns); return DynamicConfig.this; } } @@ -350,7 +350,7 @@ public static class Snapshot implements TraceConfig { final Pair preferredServiceNameAndSource; final List dataStreamsTransactionExtractors; - final List ttExtractionPatterns; + final List ttCandidateSourcePatterns; protected Snapshot(DynamicConfig.Builder builder, Snapshot oldSnapshot) { @@ -373,7 +373,7 @@ protected Snapshot(DynamicConfig.Builder builder, Snapshot oldSnapshot) { this.preferredServiceNameAndSource = builder.preferredServiceNameAndSource; this.dataStreamsTransactionExtractors = builder.dataStreamsTransactionExtractors; - this.ttExtractionPatterns = nullToEmpty(builder.ttExtractionPatterns); + this.ttCandidateSourcePatterns = nullToEmpty(builder.ttCandidateSourcePatterns); } private static Map nullToEmpty(Map mapping) { @@ -450,8 +450,8 @@ public List getDataStreamsTransactionExtractors } @Override - public List getTransactionTrackingExtractionPatterns() { - return ttExtractionPatterns; + public List getTransactionTrackingCandidateSourcePatterns() { + return ttCandidateSourcePatterns; } @Override @@ -488,8 +488,8 @@ public String toString() { + tracingTags + ", preferredServiceNameAndSource=" + preferredServiceNameAndSource - + ", ttExtractionPatterns=" - + ttExtractionPatterns + + ", ttCandidateSourcePatterns=" + + ttCandidateSourcePatterns + '}'; } } diff --git a/internal-api/src/main/java/datadog/trace/api/TraceConfig.java b/internal-api/src/main/java/datadog/trace/api/TraceConfig.java index ab91ae401fb..11d43e9eb46 100644 --- a/internal-api/src/main/java/datadog/trace/api/TraceConfig.java +++ b/internal-api/src/main/java/datadog/trace/api/TraceConfig.java @@ -60,5 +60,5 @@ public interface TraceConfig { * Glob patterns used by Transaction Tracking to flag inbound HTTP header / query-string parameter * names. An empty list disables the feature. */ - List getTransactionTrackingExtractionPatterns(); + List getTransactionTrackingCandidateSourcePatterns(); } diff --git a/internal-api/src/main/java/datadog/trace/api/tt/TransactionTrackingPatterns.java b/internal-api/src/main/java/datadog/trace/api/tt/TransactionTrackingCandidateSources.java similarity index 95% rename from internal-api/src/main/java/datadog/trace/api/tt/TransactionTrackingPatterns.java rename to internal-api/src/main/java/datadog/trace/api/tt/TransactionTrackingCandidateSources.java index ca1379f33ef..abfee77da6a 100644 --- a/internal-api/src/main/java/datadog/trace/api/tt/TransactionTrackingPatterns.java +++ b/internal-api/src/main/java/datadog/trace/api/tt/TransactionTrackingCandidateSources.java @@ -9,8 +9,8 @@ import org.slf4j.LoggerFactory; /** - * Snapshot of compiled "transaction tracking" extraction glob patterns delivered through - * remote-config under the {@code APM_TRACING} product (field {@code tt_extraction_patterns}). + * Snapshot of compiled "transaction tracking" candidate-source glob patterns delivered through + * remote-config under the {@code APM_TRACING} product (field {@code tt_candidate_source_patterns}). * *

The hot path on every server request only does a single volatile read followed by an {@link * Snapshot#isEmpty()} check when no patterns are configured, so this class is zero-allocation in @@ -29,13 +29,14 @@ * limited to {@code *} (zero-or-more characters). The matcher is hand-rolled to avoid {@link * java.util.regex.Pattern} compilation on the request hot path. */ -public final class TransactionTrackingPatterns { +public final class TransactionTrackingCandidateSources { - private static final Logger log = LoggerFactory.getLogger(TransactionTrackingPatterns.class); + private static final Logger log = + LoggerFactory.getLogger(TransactionTrackingCandidateSources.class); private static volatile Snapshot snapshot = Snapshot.EMPTY; - private TransactionTrackingPatterns() {} + private TransactionTrackingCandidateSources() {} /** * Re-compile the raw pattern list and atomically publish a new snapshot. A {@code null} or empty @@ -51,12 +52,12 @@ public static void update(List rawPatterns) { List wildcards = new ArrayList<>(); for (String raw : rawPatterns) { if (raw == null) { - log.debug("Ignoring null tt_extraction_pattern entry"); + log.debug("Ignoring null tt_candidate_source_pattern entry"); continue; } String trimmed = raw.trim(); if (trimmed.isEmpty()) { - log.debug("Ignoring blank tt_extraction_pattern entry"); + log.debug("Ignoring blank tt_candidate_source_pattern entry"); continue; } if (trimmed.indexOf('*') < 0) { diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentTracer.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentTracer.java index dd3fca04482..f9c6c388c15 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentTracer.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentTracer.java @@ -737,7 +737,7 @@ public List getDataStreamsTransactionExtractors } @Override - public List getTransactionTrackingExtractionPatterns() { + public List getTransactionTrackingCandidateSourcePatterns() { return Collections.emptyList(); } } diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java index 92b59663c34..b4b33ab6c09 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java @@ -8,9 +8,9 @@ public class InstrumentationTags { // start looking at generating constants based on the // enabled instrumentations. - // Transaction tracking — extraction sources tag (set by HttpServerDecorator when a configured + // Transaction tracking — candidate sources tag (set by HttpServerDecorator when a configured // glob pattern matches an inbound HTTP header name or query-string parameter name). - public static final String TT_EXTRACTION_SOURCES = "_dd.tt.extraction_sources"; + public static final String TT_CANDIDATE_SOURCES = "_dd.tt.candidate_sources"; public static final String PARTITION = "partition"; public static final String OFFSET = "offset"; diff --git a/internal-api/src/test/java/datadog/trace/api/tt/TransactionTrackingCandidateSourcesTest.java b/internal-api/src/test/java/datadog/trace/api/tt/TransactionTrackingCandidateSourcesTest.java new file mode 100644 index 00000000000..a51ebd0c2f8 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/tt/TransactionTrackingCandidateSourcesTest.java @@ -0,0 +1,194 @@ +package datadog.trace.api.tt; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +class TransactionTrackingCandidateSourcesTest { + + @AfterEach + void reset() { + TransactionTrackingCandidateSources.resetForTest(); + } + + @Test + void emptyByDefault() { + assertTrue(TransactionTrackingCandidateSources.isEmpty()); + assertFalse(TransactionTrackingCandidateSources.matchesAny("x-foo")); + } + + @Test + void nullOrEmptyUpdateKeepsEmpty() { + TransactionTrackingCandidateSources.update(null); + assertTrue(TransactionTrackingCandidateSources.isEmpty()); + TransactionTrackingCandidateSources.update(Collections.emptyList()); + assertTrue(TransactionTrackingCandidateSources.isEmpty()); + } + + @Test + void literalPatternIsExactCaseInsensitive() { + TransactionTrackingCandidateSources.update(Collections.singletonList("X-Request-Id")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("x-request-id")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("X-REQUEST-ID")); + assertFalse(TransactionTrackingCandidateSources.matchesAny("x-request-id-2")); + assertFalse(TransactionTrackingCandidateSources.matchesAny("yx-request-id")); + } + + @Test + void prefixWildcard() { + TransactionTrackingCandidateSources.update(Collections.singletonList("*-id")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("X-Request-Id")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("-id")); + assertFalse(TransactionTrackingCandidateSources.matchesAny("id")); + assertFalse(TransactionTrackingCandidateSources.matchesAny("X-Request-Token")); + } + + @Test + void suffixWildcard() { + TransactionTrackingCandidateSources.update(Collections.singletonList("x-trace-*")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("X-Trace-Id")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("x-trace-")); + assertFalse(TransactionTrackingCandidateSources.matchesAny("x-trace")); + assertFalse(TransactionTrackingCandidateSources.matchesAny("y-trace-id")); + } + + @Test + void middleWildcard() { + TransactionTrackingCandidateSources.update(Collections.singletonList("x-*-id")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("X-Request-Id")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("x--id")); + assertFalse(TransactionTrackingCandidateSources.matchesAny("x-request")); + assertFalse(TransactionTrackingCandidateSources.matchesAny("y-request-id")); + } + + @Test + void multipleWildcards() { + TransactionTrackingCandidateSources.update(Collections.singletonList("*foo*bar*")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("xxfooyybarzz")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("foobar")); + assertFalse(TransactionTrackingCandidateSources.matchesAny("barfoo")); + assertFalse(TransactionTrackingCandidateSources.matchesAny("fooxx")); + } + + @Test + void starOnlyMatchesAnything() { + TransactionTrackingCandidateSources.update(Collections.singletonList("*")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("anything")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("")); + } + + @Test + void multiplePatternsAnyMatch() { + TransactionTrackingCandidateSources.update(Arrays.asList("x-foo-*", "*-trace")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("x-foo-1")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("dd-trace")); + assertFalse(TransactionTrackingCandidateSources.matchesAny("dd-other")); + } + + @Test + void blankAndNullEntriesAreSkipped() { + TransactionTrackingCandidateSources.update(Arrays.asList(null, "", " ", "x-keep")); + assertFalse(TransactionTrackingCandidateSources.isEmpty()); + assertTrue(TransactionTrackingCandidateSources.matchesAny("x-keep")); + assertEquals(1, TransactionTrackingCandidateSources.currentSnapshot().size()); + } + + @Test + void allBlankClearsSnapshot() { + TransactionTrackingCandidateSources.update(Arrays.asList(null, "", " ")); + assertTrue(TransactionTrackingCandidateSources.isEmpty()); + } + + @Test + void matchesAny_literalSetFastPath() { + List patterns = new ArrayList<>(); + patterns.add("X-Request-Id"); + patterns.add("X-Tenant-Id"); + patterns.add("X-Customer-Id"); + patterns.add("X-Correlation-Id"); + patterns.add("X-Session-Id"); + patterns.add("X-Trace-Id"); + patterns.add("X-Span-Id"); + patterns.add("X-Account-Id"); + patterns.add("X-Order-Id"); + patterns.add("X-User-Id"); + patterns.add("x-debug-*"); + TransactionTrackingCandidateSources.update(patterns); + + assertEquals(10, TransactionTrackingCandidateSources.literalCountForTest()); + assertEquals(1, TransactionTrackingCandidateSources.wildcardCountForTest()); + + // literal matches (mixed case in candidates) + assertTrue(TransactionTrackingCandidateSources.matchesAny("x-request-id")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("X-TENANT-ID")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("x-Customer-Id")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("X-correlation-id")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("X-Session-Id")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("x-TRACE-id")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("X-Span-Id")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("x-account-ID")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("X-ORDER-ID")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("X-User-Id")); + // wildcard match + assertTrue(TransactionTrackingCandidateSources.matchesAny("X-Debug-Flag")); + // unrelated name + assertFalse(TransactionTrackingCandidateSources.matchesAny("Content-Type")); + } + + @Test + void matchesAny_allLiteralsNoWildcardListWalk() { + TransactionTrackingCandidateSources.update( + Arrays.asList("a-one", "b-two", "c-three", "d-four", "e-five")); + assertEquals(5, TransactionTrackingCandidateSources.literalCountForTest()); + assertEquals(0, TransactionTrackingCandidateSources.wildcardCountForTest()); + assertTrue(TransactionTrackingCandidateSources.matchesAny("A-ONE")); + assertFalse(TransactionTrackingCandidateSources.matchesAny("missing")); + } + + @Test + void matchesAny_nullAndEmptyInputsSafe() { + // No patterns configured: null and empty candidates must not throw. + assertFalse(TransactionTrackingCandidateSources.matchesAny(null)); + assertFalse(TransactionTrackingCandidateSources.matchesAny("")); + // With patterns configured (literal + wildcard): same expectations. + TransactionTrackingCandidateSources.update(Arrays.asList("x-keep", "x-*")); + assertFalse(TransactionTrackingCandidateSources.matchesAny(null)); + // "x-*" matches empty-prefix; "" doesn't start with "x-" so should not match. + assertFalse(TransactionTrackingCandidateSources.matchesAny("")); + } + + @Test + void matchesAny_nonAsciiName() { + TransactionTrackingCandidateSources.update( + Collections.singletonList("X-\u0422\u0435\u0441\u0442")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("x-\u0422\u0435\u0441\u0442")); + assertFalse(TransactionTrackingCandidateSources.matchesAny("x-other")); + } + + @Test + void matchesAny_caseInsensitiveSetCollisions() { + // "x-a" and "x-i" both have (String.hashCode() & 7) == 4, so on a cap=8 table + // (the size used for 2 literals) they collide and exercise linear probing. + TransactionTrackingCandidateSources.update(Arrays.asList("x-a", "x-i")); + assertEquals(2, TransactionTrackingCandidateSources.literalCountForTest()); + assertTrue(TransactionTrackingCandidateSources.matchesAny("X-A")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("X-I")); + assertFalse(TransactionTrackingCandidateSources.matchesAny("X-B")); + } + + @Test + void overlappingSegmentsDoNotMatch() { + TransactionTrackingCandidateSources.update(Collections.singletonList("ab*ab")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("abxab")); + assertTrue(TransactionTrackingCandidateSources.matchesAny("abab")); + assertFalse(TransactionTrackingCandidateSources.matchesAny("aba")); + assertFalse(TransactionTrackingCandidateSources.matchesAny("xab")); + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/tt/TransactionTrackingPatternsTest.java b/internal-api/src/test/java/datadog/trace/api/tt/TransactionTrackingPatternsTest.java deleted file mode 100644 index 802574b33d9..00000000000 --- a/internal-api/src/test/java/datadog/trace/api/tt/TransactionTrackingPatternsTest.java +++ /dev/null @@ -1,193 +0,0 @@ -package datadog.trace.api.tt; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; - -class TransactionTrackingPatternsTest { - - @AfterEach - void reset() { - TransactionTrackingPatterns.resetForTest(); - } - - @Test - void emptyByDefault() { - assertTrue(TransactionTrackingPatterns.isEmpty()); - assertFalse(TransactionTrackingPatterns.matchesAny("x-foo")); - } - - @Test - void nullOrEmptyUpdateKeepsEmpty() { - TransactionTrackingPatterns.update(null); - assertTrue(TransactionTrackingPatterns.isEmpty()); - TransactionTrackingPatterns.update(Collections.emptyList()); - assertTrue(TransactionTrackingPatterns.isEmpty()); - } - - @Test - void literalPatternIsExactCaseInsensitive() { - TransactionTrackingPatterns.update(Collections.singletonList("X-Request-Id")); - assertTrue(TransactionTrackingPatterns.matchesAny("x-request-id")); - assertTrue(TransactionTrackingPatterns.matchesAny("X-REQUEST-ID")); - assertFalse(TransactionTrackingPatterns.matchesAny("x-request-id-2")); - assertFalse(TransactionTrackingPatterns.matchesAny("yx-request-id")); - } - - @Test - void prefixWildcard() { - TransactionTrackingPatterns.update(Collections.singletonList("*-id")); - assertTrue(TransactionTrackingPatterns.matchesAny("X-Request-Id")); - assertTrue(TransactionTrackingPatterns.matchesAny("-id")); - assertFalse(TransactionTrackingPatterns.matchesAny("id")); - assertFalse(TransactionTrackingPatterns.matchesAny("X-Request-Token")); - } - - @Test - void suffixWildcard() { - TransactionTrackingPatterns.update(Collections.singletonList("x-trace-*")); - assertTrue(TransactionTrackingPatterns.matchesAny("X-Trace-Id")); - assertTrue(TransactionTrackingPatterns.matchesAny("x-trace-")); - assertFalse(TransactionTrackingPatterns.matchesAny("x-trace")); - assertFalse(TransactionTrackingPatterns.matchesAny("y-trace-id")); - } - - @Test - void middleWildcard() { - TransactionTrackingPatterns.update(Collections.singletonList("x-*-id")); - assertTrue(TransactionTrackingPatterns.matchesAny("X-Request-Id")); - assertTrue(TransactionTrackingPatterns.matchesAny("x--id")); - assertFalse(TransactionTrackingPatterns.matchesAny("x-request")); - assertFalse(TransactionTrackingPatterns.matchesAny("y-request-id")); - } - - @Test - void multipleWildcards() { - TransactionTrackingPatterns.update(Collections.singletonList("*foo*bar*")); - assertTrue(TransactionTrackingPatterns.matchesAny("xxfooyybarzz")); - assertTrue(TransactionTrackingPatterns.matchesAny("foobar")); - assertFalse(TransactionTrackingPatterns.matchesAny("barfoo")); - assertFalse(TransactionTrackingPatterns.matchesAny("fooxx")); - } - - @Test - void starOnlyMatchesAnything() { - TransactionTrackingPatterns.update(Collections.singletonList("*")); - assertTrue(TransactionTrackingPatterns.matchesAny("anything")); - assertTrue(TransactionTrackingPatterns.matchesAny("")); - } - - @Test - void multiplePatternsAnyMatch() { - TransactionTrackingPatterns.update(Arrays.asList("x-foo-*", "*-trace")); - assertTrue(TransactionTrackingPatterns.matchesAny("x-foo-1")); - assertTrue(TransactionTrackingPatterns.matchesAny("dd-trace")); - assertFalse(TransactionTrackingPatterns.matchesAny("dd-other")); - } - - @Test - void blankAndNullEntriesAreSkipped() { - TransactionTrackingPatterns.update(Arrays.asList(null, "", " ", "x-keep")); - assertFalse(TransactionTrackingPatterns.isEmpty()); - assertTrue(TransactionTrackingPatterns.matchesAny("x-keep")); - assertEquals(1, TransactionTrackingPatterns.currentSnapshot().size()); - } - - @Test - void allBlankClearsSnapshot() { - TransactionTrackingPatterns.update(Arrays.asList(null, "", " ")); - assertTrue(TransactionTrackingPatterns.isEmpty()); - } - - @Test - void matchesAny_literalSetFastPath() { - List patterns = new ArrayList<>(); - patterns.add("X-Request-Id"); - patterns.add("X-Tenant-Id"); - patterns.add("X-Customer-Id"); - patterns.add("X-Correlation-Id"); - patterns.add("X-Session-Id"); - patterns.add("X-Trace-Id"); - patterns.add("X-Span-Id"); - patterns.add("X-Account-Id"); - patterns.add("X-Order-Id"); - patterns.add("X-User-Id"); - patterns.add("x-debug-*"); - TransactionTrackingPatterns.update(patterns); - - assertEquals(10, TransactionTrackingPatterns.literalCountForTest()); - assertEquals(1, TransactionTrackingPatterns.wildcardCountForTest()); - - // literal matches (mixed case in candidates) - assertTrue(TransactionTrackingPatterns.matchesAny("x-request-id")); - assertTrue(TransactionTrackingPatterns.matchesAny("X-TENANT-ID")); - assertTrue(TransactionTrackingPatterns.matchesAny("x-Customer-Id")); - assertTrue(TransactionTrackingPatterns.matchesAny("X-correlation-id")); - assertTrue(TransactionTrackingPatterns.matchesAny("X-Session-Id")); - assertTrue(TransactionTrackingPatterns.matchesAny("x-TRACE-id")); - assertTrue(TransactionTrackingPatterns.matchesAny("X-Span-Id")); - assertTrue(TransactionTrackingPatterns.matchesAny("x-account-ID")); - assertTrue(TransactionTrackingPatterns.matchesAny("X-ORDER-ID")); - assertTrue(TransactionTrackingPatterns.matchesAny("X-User-Id")); - // wildcard match - assertTrue(TransactionTrackingPatterns.matchesAny("X-Debug-Flag")); - // unrelated name - assertFalse(TransactionTrackingPatterns.matchesAny("Content-Type")); - } - - @Test - void matchesAny_allLiteralsNoWildcardListWalk() { - TransactionTrackingPatterns.update( - Arrays.asList("a-one", "b-two", "c-three", "d-four", "e-five")); - assertEquals(5, TransactionTrackingPatterns.literalCountForTest()); - assertEquals(0, TransactionTrackingPatterns.wildcardCountForTest()); - assertTrue(TransactionTrackingPatterns.matchesAny("A-ONE")); - assertFalse(TransactionTrackingPatterns.matchesAny("missing")); - } - - @Test - void matchesAny_nullAndEmptyInputsSafe() { - // No patterns configured: null and empty candidates must not throw. - assertFalse(TransactionTrackingPatterns.matchesAny(null)); - assertFalse(TransactionTrackingPatterns.matchesAny("")); - // With patterns configured (literal + wildcard): same expectations. - TransactionTrackingPatterns.update(Arrays.asList("x-keep", "x-*")); - assertFalse(TransactionTrackingPatterns.matchesAny(null)); - // "x-*" matches empty-prefix; "" doesn't start with "x-" so should not match. - assertFalse(TransactionTrackingPatterns.matchesAny("")); - } - - @Test - void matchesAny_nonAsciiName() { - TransactionTrackingPatterns.update(Collections.singletonList("X-\u0422\u0435\u0441\u0442")); - assertTrue(TransactionTrackingPatterns.matchesAny("x-\u0422\u0435\u0441\u0442")); - assertFalse(TransactionTrackingPatterns.matchesAny("x-other")); - } - - @Test - void matchesAny_caseInsensitiveSetCollisions() { - // "x-a" and "x-i" both have (String.hashCode() & 7) == 4, so on a cap=8 table - // (the size used for 2 literals) they collide and exercise linear probing. - TransactionTrackingPatterns.update(Arrays.asList("x-a", "x-i")); - assertEquals(2, TransactionTrackingPatterns.literalCountForTest()); - assertTrue(TransactionTrackingPatterns.matchesAny("X-A")); - assertTrue(TransactionTrackingPatterns.matchesAny("X-I")); - assertFalse(TransactionTrackingPatterns.matchesAny("X-B")); - } - - @Test - void overlappingSegmentsDoNotMatch() { - TransactionTrackingPatterns.update(Collections.singletonList("ab*ab")); - assertTrue(TransactionTrackingPatterns.matchesAny("abxab")); - assertTrue(TransactionTrackingPatterns.matchesAny("abab")); - assertFalse(TransactionTrackingPatterns.matchesAny("aba")); - assertFalse(TransactionTrackingPatterns.matchesAny("xab")); - } -} diff --git a/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java b/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java index 78642306d04..7c2491a8bd6 100644 --- a/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java +++ b/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java @@ -47,5 +47,5 @@ public interface Capabilities { long CAPABILITY_ASM_EXTENDED_DATA_COLLECTION = 1L << 44; long CAPABILITY_APM_TRACING_MULTICONFIG = 1L << 45; long CAPABILITY_FFE_FLAG_CONFIGURATION_RULES = 1L << 46; - long CAPABILITY_APM_TRACING_TT_EXTRACTION_PATTERNS = 1L << 49; + long CAPABILITY_APM_TRACING_TT_CANDIDATE_SOURCE_PATTERNS = 1L << 49; }