From 32d91a01eec24e742d4f0bbe1f913fe9044eddd4 Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Fri, 10 Apr 2026 19:04:36 +0200 Subject: [PATCH 1/4] Migrate dd-trace-core groovy files to java part 3 we migrate 3 small tests: - BlackholeSpanTest - LongRunningTracesTrackerTest - TraceCorrelationTest using new SpanConfigExtension for injecting config --- .../skills/migrate-groovy-to-java/SKILL.md | 1 + dd-trace-core/build.gradle | 1 + .../trace/core/LongRunningTracesTracker.java | 10 + .../java/datadog/trace/core/PendingTrace.java | 10 + .../trace/core/PendingTraceBuffer.java | 7 +- .../trace/core/BlackholeSpanTest.groovy | 51 ---- .../core/LongRunningTracesTrackerTest.groovy | 204 --------------- .../trace/core/TraceCorrelationTest.groovy | 89 ------- .../datadog/trace/core/BlackholeSpanTest.java | 59 +++++ .../core/LongRunningTracesTrackerTest.java | 243 ++++++++++++++++++ .../trace/core/TraceCorrelationTest.java | 88 +++++++ 11 files changed, 418 insertions(+), 345 deletions(-) delete mode 100644 dd-trace-core/src/test/groovy/datadog/trace/core/BlackholeSpanTest.groovy delete mode 100644 dd-trace-core/src/test/groovy/datadog/trace/core/LongRunningTracesTrackerTest.groovy delete mode 100644 dd-trace-core/src/test/groovy/datadog/trace/core/TraceCorrelationTest.groovy create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/BlackholeSpanTest.java create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/LongRunningTracesTrackerTest.java create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/TraceCorrelationTest.java diff --git a/.claude/skills/migrate-groovy-to-java/SKILL.md b/.claude/skills/migrate-groovy-to-java/SKILL.md index c603bf227b6..617df592cfa 100644 --- a/.claude/skills/migrate-groovy-to-java/SKILL.md +++ b/.claude/skills/migrate-groovy-to-java/SKILL.md @@ -20,6 +20,7 @@ When converting Groovy code to Java code, make sure that: - `@TableTest` and `@MethodSource` may be combined on the same `@ParameterizedTest` when most cases are tabular but a few cases require programmatic setup. - In combined mode, keep table-friendly cases in `@TableTest`, and put only non-tabular/complex cases in `@MethodSource`. - If `@TableTest` is not viable for the test at all, use `@MethodSource` only. +- If `@TableTest` was successfully used, `@ParameterizedTest` can then be removed as `@TableTest` replace it fully - For `@MethodSource`, name the arguments method `Arguments` (camelCase, e.g. `testMethodArguments`) and return `Stream` using `Stream.of(...)` and `arguments(...)` with static import. - Ensure parameterized test names are human-readable (i.e. no hashcodes); instead add a description string as the first `Arguments.arguments(...)` value or index the test case - When converting tuples, create a light dedicated structure instead to keep the typing system diff --git a/dd-trace-core/build.gradle b/dd-trace-core/build.gradle index d626672dee4..48544e2984f 100644 --- a/dd-trace-core/build.gradle +++ b/dd-trace-core/build.gradle @@ -107,6 +107,7 @@ dependencies { testImplementation group: 'com.amazonaws', name: 'aws-lambda-java-events', version:'3.11.0' testImplementation group: 'com.google.protobuf', name: 'protobuf-java', version: '3.14.0' testImplementation libs.testcontainers + testImplementation project(':utils:junit-utils') traceAgentTestImplementation libs.testcontainers } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/LongRunningTracesTracker.java b/dd-trace-core/src/main/java/datadog/trace/core/LongRunningTracesTracker.java index 5d2e32fecf6..dbbd57b0c48 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/LongRunningTracesTracker.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/LongRunningTracesTracker.java @@ -139,4 +139,14 @@ private void flushStats() { write = 0; expired = 0; } + + // @VisibleForTesting + int trackedCount() { + return traceArray.size(); + } + + // @VisibleForTesting + int getDropped() { + return dropped; + } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/PendingTrace.java b/dd-trace-core/src/main/java/datadog/trace/core/PendingTrace.java index ad872164718..47e60f6310a 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/PendingTrace.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/PendingTrace.java @@ -223,6 +223,16 @@ boolean compareAndSetLongRunningState(int expected, int newState) { return LONG_RUNNING_STATE.compareAndSet(this, expected, newState); } + // @VisibleForTesting + int getLongRunningTrackedState() { + return longRunningTrackedState; + } + + // @VisibleForTesting + void setLongRunningTrackedState(int state) { + LONG_RUNNING_STATE.set(this, state); + } + boolean empty() { return 0 >= COMPLETED_SPAN_COUNT.get(this) + PENDING_REFERENCE_COUNT.get(this); } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/PendingTraceBuffer.java b/dd-trace-core/src/main/java/datadog/trace/core/PendingTraceBuffer.java index 1eed33d5232..ad89c996d90 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/PendingTraceBuffer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/PendingTraceBuffer.java @@ -54,7 +54,7 @@ public interface Element { boolean writeOnBufferFull(); } - private static class DelayingPendingTraceBuffer extends PendingTraceBuffer { + static class DelayingPendingTraceBuffer extends PendingTraceBuffer { private static final long FORCE_SEND_DELAY_MS = TimeUnit.SECONDS.toMillis(5); private static final long SEND_DELAY_NS = TimeUnit.MILLISECONDS.toNanos(500); private static final long SLEEP_TIME_MS = 100; @@ -303,6 +303,11 @@ public DelayingPendingTraceBuffer( config, bufferSize, sharedCommunicationObjects, healthMetrics) : null; } + + // @VisibleForTesting + LongRunningTracesTracker getRunningTracesTracker() { + return runningTracesTracker; + } } static class DiscardingPendingTraceBuffer extends PendingTraceBuffer { diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/BlackholeSpanTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/BlackholeSpanTest.groovy deleted file mode 100644 index 908cee8f23e..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/BlackholeSpanTest.groovy +++ /dev/null @@ -1,51 +0,0 @@ -package datadog.trace.core - - -import datadog.trace.common.writer.ListWriter -import datadog.trace.core.test.DDCoreSpecification - -class BlackholeSpanTest extends DDCoreSpecification { - def "should mute tracing"() { - setup: - injectSysConfig("trace.128.bit.traceid.logging.enabled", moreBits) - def writer = new ListWriter() - def props = new Properties() - def tracer = tracerBuilder().withProperties(props).writer(writer).build() - when: - def child = null - def bh = null - def ignored = null - def root = tracer.startSpan("test", "root") - def scope1 = tracer.activateSpan(root) - try { - bh = tracer.blackholeSpan() - def scope2 = tracer.activateSpan(bh) - try { - ignored = tracer.startSpan("test", "ignored") - ignored.finish() - } finally { - bh.finish() - scope2.close() - } - child = tracer.startSpan("test", "child") - child.finish() - } finally { - root.finish() - scope1.close() - } - then: - writer.waitForTraces(1) - assert writer.firstTrace().size() == 2 - assert writer.firstTrace().containsAll([root, child]) - assert !writer.firstTrace().contains(bh) - assert !writer.firstTrace().contains(ignored) - - cleanup: - writer.close() - tracer.close() - where: - moreBits | _ - "true" | _ - "false" | _ - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/LongRunningTracesTrackerTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/LongRunningTracesTrackerTest.groovy deleted file mode 100644 index 07313ee4282..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/LongRunningTracesTrackerTest.groovy +++ /dev/null @@ -1,204 +0,0 @@ -package datadog.trace.core - -import datadog.communication.ddagent.DDAgentFeaturesDiscovery -import datadog.communication.ddagent.SharedCommunicationObjects -import datadog.environment.JavaVirtualMachine -import datadog.trace.api.Config -import datadog.trace.api.DDTraceId -import datadog.trace.api.sampling.PrioritySampling -import datadog.trace.api.time.ControllableTimeSource -import datadog.trace.core.monitor.HealthMetrics -import datadog.trace.test.util.DDSpecification - -import java.util.concurrent.TimeUnit -import spock.lang.IgnoreIf - -@IgnoreIf(reason = """ -Oracle JDK 1.8 did not merge the fix in JDK-8058322, leading to the JVM failing to correctly -extract method parameters without args, when the code is compiled on a later JDK (targeting 8). -This can manifest when creating mocks. -""", value = { - JavaVirtualMachine.isOracleJDK8() -}) -class LongRunningTracesTrackerTest extends DDSpecification { - Config config = Mock(Config) - int maxTrackedTraces = 10 - def sharedCommunicationObjects = Mock(SharedCommunicationObjects) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - LongRunningTracesTracker tracker - def tracer = Mock(CoreTracer) - def traceConfig = Stub(CoreTracer.ConfigSnapshot) - PendingTraceBuffer.DelayingPendingTraceBuffer buffer - PendingTrace.Factory factory = null - def timeSource = new ControllableTimeSource() - - def setup() { - timeSource.set(0L) - features.supportsLongRunning() >> true - tracer.captureTraceConfig() >> traceConfig - tracer.getTimeWithNanoTicks(_) >> { Long x -> x } - traceConfig.getServiceMapping() >> [:] - config.getLongRunningTraceInitialFlushInterval() >> 10 - config.getLongRunningTraceFlushInterval() >> 20 - config.longRunningTraceEnabled >> true - sharedCommunicationObjects.featuresDiscovery(_) >> features - buffer = new PendingTraceBuffer.DelayingPendingTraceBuffer(maxTrackedTraces, timeSource, config, sharedCommunicationObjects, HealthMetrics.NO_OP) - tracker = buffer.runningTracesTracker - factory = new PendingTrace.Factory(tracer, buffer, timeSource, false, HealthMetrics.NO_OP) - } - - def "null is not added"() { - when: - tracker.add(null) - - then: - tracker.traceArray.size() == 0 - } - - def "trace with no span is not added"() { - when: - tracker.add(factory.create(DDTraceId.ONE)) - - then: - tracker.traceArray.size() == 0 - } - - def "trace without the right state are not tracked"() { - given: - def statesToTest = Arrays.asList( - LongRunningTracesTracker.NOT_TRACKED, - LongRunningTracesTracker.UNDEFINED, - LongRunningTracesTracker.TRACKED, - LongRunningTracesTracker.WRITE_RUNNING_SPANS, - LongRunningTracesTracker.EXPIRED - ) - when: - statesToTest.each { stateToTest -> - def trace = newTraceToTrack() - trace.longRunningTrackedState = stateToTest - tracker.add(trace) - } - then: - tracker.traceArray.size() == 0 - - when: - tracker.add(newTraceToTrack()) - then: - tracker.traceArray.size() == 1 - } - - - def "maxTrackedTraces is enforced"() { - given: - (1..maxTrackedTraces).each { - tracker.add(newTraceToTrack()) - } - - when: - tracker.add(newTraceToTrack()) - - then: - tracker.traceArray.size() == maxTrackedTraces - tracker.dropped == 1 - } - - def "expired traces"() { - given: - def trace = newTraceToTrack() - tracker.add(trace) - - when: - tracker.flushAndCompact(tracker.maxTrackedDurationMilli - 1000) - - then: - tracker.traceArray.size() == 1 - trace.longRunningTrackedState == LongRunningTracesTracker.WRITE_RUNNING_SPANS - - when: - tracker.flushAndCompact(1 + tracker.maxTrackedDurationMilli) - - then: - tracker.traceArray.size() == 0 - - trace.longRunningTrackedState == LongRunningTracesTracker.EXPIRED - } - - def "agent disabled feature"() { - given: - def trace = newTraceToTrack() - tracker.add(trace) - - when: - tracker.flushAndCompact(tracker.flushPeriodMilli - 1000) - - then: - 1 * features.supportsLongRunning() >> false - tracker.traceArray.size() == 0 - } - - def flushAt(long timeMilli) { - timeSource.set(TimeUnit.MILLISECONDS.toNanos(timeMilli)) - tracker.flushAndCompact(timeMilli) - } - - def "flush logic with initial flush"() { - given: - def trace = newTraceToTrack() - tracker.add(trace) - - when: // Before the initial flush - flushAt(tracker.initialFlushPeriodMilli - 1000) - - then: - 0 * tracer.write(_) - - when: // After the initial flush - flushAt(tracker.initialFlushPeriodMilli + 1000) - - then: - 1 * tracer.write(_) - trace.getLastWriteTime() == TimeUnit.MILLISECONDS.toNanos(tracker.initialFlushPeriodMilli + 1000) - - when: // Before the regular flush - flushAt(tracker.initialFlushPeriodMilli + tracker.flushPeriodMilli - 1000) - - then: - 0 * tracer.write(_) - trace.getLastWriteTime() == TimeUnit.MILLISECONDS.toNanos(tracker.initialFlushPeriodMilli + 1000) - - when: // After the first regular flush - flushAt(tracker.initialFlushPeriodMilli + tracker.flushPeriodMilli + 2000) - - then: - 1 * tracer.write(_) - trace.getLastWriteTime() == TimeUnit.MILLISECONDS.toNanos(tracker.initialFlushPeriodMilli + tracker.flushPeriodMilli + 2000) - } - - PendingTrace newTraceToTrack() { - PendingTrace trace = factory.create(DDTraceId.ONE) - PendingTraceBufferTest::newSpanOf(trace, PrioritySampling.SAMPLER_KEEP, 0) - return trace - } - - def "priority evaluation: #priority"() { - given: - def trace = newTraceToTrack() - def span = trace.spans.peek() - span.context().samplingPriority = priority - tracker.add(trace) - - when: - tracker.flushAndCompact(tracker.maxTrackedDurationMilli - 1000) - - then: - tracker.traceArray.size() == trackerExpectedSize - trace.longRunningTrackedState == traceExpectedState - - where: - priority | trackerExpectedSize | traceExpectedState - PrioritySampling.SAMPLER_DROP | 0 | LongRunningTracesTracker.NOT_TRACKED - PrioritySampling.USER_DROP | 0 | LongRunningTracesTracker.NOT_TRACKED - PrioritySampling.USER_KEEP | 1 | LongRunningTracesTracker.WRITE_RUNNING_SPANS - PrioritySampling.SAMPLER_KEEP | 1 | LongRunningTracesTracker.WRITE_RUNNING_SPANS - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/TraceCorrelationTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/TraceCorrelationTest.groovy deleted file mode 100644 index ee7894be954..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/TraceCorrelationTest.groovy +++ /dev/null @@ -1,89 +0,0 @@ -package datadog.trace.core - -import datadog.trace.common.writer.ListWriter -import datadog.trace.core.test.DDCoreSpecification - -import static datadog.trace.api.config.TraceInstrumentationConfig.TRACE_128_BIT_TRACEID_LOGGING_ENABLED -import static datadog.trace.api.config.TracerConfig.TRACE_128_BIT_TRACEID_GENERATION_ENABLED - -class TraceCorrelationTest extends DDCoreSpecification { - def "get trace id without trace (128b: #log128bTraceId)"() { - setup: - injectSysConfig(TRACE_128_BIT_TRACEID_GENERATION_ENABLED, log128bTraceIdConfigValue) - injectSysConfig(TRACE_128_BIT_TRACEID_LOGGING_ENABLED, log128bTraceIdConfigValue) - - when: - def tracer = tracerBuilder().writer(new ListWriter()).build() - def span = tracer.buildSpan("test").start() - def scope = tracer.activateSpan(span) - scope.close() - - then: - "0" == tracer.getTraceId() - - cleanup: - scope.close() - span.finish() - tracer.close() - - where: - log128bTraceId << [true, false] - log128bTraceIdConfigValue = log128bTraceId.toString() - } - - def "get trace id with trace (128b: #log128bTraceId)"() { - setup: - injectSysConfig(TRACE_128_BIT_TRACEID_GENERATION_ENABLED, log128bTraceIdConfigValue) - injectSysConfig(TRACE_128_BIT_TRACEID_LOGGING_ENABLED, log128bTraceIdConfigValue) - - when: - def tracer = tracerBuilder().writer(new ListWriter()).build() - def span = tracer.buildSpan("test").start() - def scope = tracer.activateSpan(span) - - then: - def traceId = ((DDSpan) scope.span()).traceId - def formattedTraceId = log128bTraceId ? traceId.toHexString() : traceId.toString() - formattedTraceId == tracer.getTraceId() - - cleanup: - scope.close() - span.finish() - tracer.close() - - where: - log128bTraceId << [true, false] - log128bTraceIdConfigValue = log128bTraceId.toString() - } - - def "get span id without span"() { - when: - def tracer = tracerBuilder().writer(new ListWriter()).build() - def span = tracer.buildSpan("test").start() - def scope = tracer.activateSpan(span) - scope.close() - - then: - "0" == tracer.getSpanId() - - cleanup: - scope.close() - span.finish() - tracer.close() - } - - def "get span id with trace"() { - when: - def tracer = tracerBuilder().writer(new ListWriter()).build() - def span = tracer.buildSpan("test").start() - def scope = tracer.activateSpan(span) - - then: - ((DDSpan) scope.span()).spanId.toString() == tracer.getSpanId() - - cleanup: - scope.close() - span.finish() - tracer.close() - } -} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/BlackholeSpanTest.java b/dd-trace-core/src/test/java/datadog/trace/core/BlackholeSpanTest.java new file mode 100644 index 00000000000..176d7d977c2 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/BlackholeSpanTest.java @@ -0,0 +1,59 @@ +package datadog.trace.core; + +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 datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.common.writer.ListWriter; +import datadog.trace.junit.utils.config.WithConfigExtension; +import java.util.Arrays; +import java.util.Properties; +import org.tabletest.junit.TableTest; + +public class BlackholeSpanTest extends DDCoreJavaSpecification { + + @TableTest({ + "scenario | moreBits", + "128 bit traceid logging enabled | true ", + "128 bit traceid logging disabled | false " + }) + void shouldMuteTracing(String moreBits) throws Exception { + WithConfigExtension.injectSysConfig("trace.128.bit.traceid.logging.enabled", moreBits); + ListWriter writer = new ListWriter(); + Properties props = new Properties(); + CoreTracer tracer = tracerBuilder().withProperties(props).writer(writer).build(); + try { + AgentSpan root = tracer.startSpan("test", "root"); + AgentScope scope1 = tracer.activateSpan(root); + AgentSpan bh; + AgentSpan ignored; + AgentSpan child; + try { + bh = tracer.blackholeSpan(); + AgentScope scope2 = tracer.activateSpan(bh); + try { + ignored = tracer.startSpan("test", "ignored"); + ignored.finish(); + } finally { + bh.finish(); + scope2.close(); + } + child = tracer.startSpan("test", "child"); + child.finish(); + } finally { + root.finish(); + scope1.close(); + } + writer.waitForTraces(1); + assertEquals(2, writer.firstTrace().size()); + assertTrue(writer.firstTrace().containsAll(Arrays.asList(root, child))); + assertFalse(writer.firstTrace().contains(bh)); + assertFalse(writer.firstTrace().contains(ignored)); + } finally { + writer.close(); + tracer.close(); + } + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/LongRunningTracesTrackerTest.java b/dd-trace-core/src/test/java/datadog/trace/core/LongRunningTracesTrackerTest.java new file mode 100644 index 00000000000..0067a51f772 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/LongRunningTracesTrackerTest.java @@ -0,0 +1,243 @@ +package datadog.trace.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import datadog.communication.ddagent.DDAgentFeaturesDiscovery; +import datadog.communication.ddagent.SharedCommunicationObjects; +import datadog.environment.JavaVirtualMachine; +import datadog.trace.api.Config; +import datadog.trace.api.DDSpanId; +import datadog.trace.api.DDTraceId; +import datadog.trace.api.datastreams.NoopPathwayContext; +import datadog.trace.api.sampling.PrioritySampling; +import datadog.trace.api.time.ControllableTimeSource; +import datadog.trace.core.monitor.HealthMetrics; +import datadog.trace.core.propagation.PropagationTags; +import datadog.trace.junit.utils.config.WithConfigExtension; +import datadog.trace.test.util.DDJavaSpecification; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.tabletest.junit.TableTest; + +public class LongRunningTracesTrackerTest extends DDJavaSpecification { + + private static final long INITIAL_FLUSH_PERIOD_MILLI = TimeUnit.SECONDS.toMillis(10); + private static final long FLUSH_PERIOD_MILLI = TimeUnit.SECONDS.toMillis(20); + private static final long MAX_TRACKED_DURATION_MILLI = TimeUnit.HOURS.toMillis(12); + private static final int MAX_TRACKED_TRACES = 10; + + private CoreTracer tracer; + private CoreTracer.ConfigSnapshot traceConfig; + private DDAgentFeaturesDiscovery features; + private SharedCommunicationObjects sharedCommunicationObjects; + private ControllableTimeSource timeSource; + private PendingTraceBuffer.DelayingPendingTraceBuffer buffer; + private LongRunningTracesTracker tracker; + private PendingTrace.Factory factory; + + @BeforeAll + static void checkJvm() { + Assumptions.assumeFalse( + JavaVirtualMachine.isOracleJDK8(), + "Oracle JDK 1.8 did not merge the fix in JDK-8058322, leading to the JVM failing to" + + " correctly extract method parameters without args, when the code is compiled on a" + + " later JDK (targeting 8). This can manifest when creating mocks."); + } + + @BeforeEach + void setup() { + WithConfigExtension.injectSysConfig("trace.experimental.long-running.enabled", "true"); + WithConfigExtension.injectSysConfig( + "trace.experimental.long-running.initial.flush.interval", "10"); + WithConfigExtension.injectSysConfig("trace.experimental.long-running.flush.interval", "20"); + + tracer = mock(CoreTracer.class); + traceConfig = mock(CoreTracer.ConfigSnapshot.class); + features = mock(DDAgentFeaturesDiscovery.class); + sharedCommunicationObjects = mock(SharedCommunicationObjects.class); + timeSource = new ControllableTimeSource(); + timeSource.set(0L); + + when(features.supportsLongRunning()).thenReturn(true); + when(tracer.captureTraceConfig()).thenReturn(traceConfig); + when(tracer.getTimeWithNanoTicks(anyLong())).thenAnswer(inv -> inv.getArgument(0)); + when(traceConfig.getServiceMapping()).thenReturn(Collections.emptyMap()); + when(sharedCommunicationObjects.featuresDiscovery(any())).thenReturn(features); + + buffer = + new PendingTraceBuffer.DelayingPendingTraceBuffer( + MAX_TRACKED_TRACES, + timeSource, + Config.get(), + sharedCommunicationObjects, + HealthMetrics.NO_OP); + tracker = buffer.getRunningTracesTracker(); + factory = new PendingTrace.Factory(tracer, buffer, timeSource, false, HealthMetrics.NO_OP); + } + + @Test + void nullIsNotAdded() { + tracker.add(null); + assertEquals(0, tracker.trackedCount()); + } + + @Test + void traceWithNoSpanIsNotAdded() { + tracker.add(factory.create(DDTraceId.ONE)); + assertEquals(0, tracker.trackedCount()); + } + + @Test + void traceWithoutRightStateAreNotTracked() { + List statesToTest = + Arrays.asList( + LongRunningTracesTracker.NOT_TRACKED, + LongRunningTracesTracker.UNDEFINED, + LongRunningTracesTracker.TRACKED, + LongRunningTracesTracker.WRITE_RUNNING_SPANS, + LongRunningTracesTracker.EXPIRED); + for (int stateToTest : statesToTest) { + PendingTrace trace = newTraceToTrack(); + trace.setLongRunningTrackedState(stateToTest); + tracker.add(trace); + } + assertEquals(0, tracker.trackedCount()); + + tracker.add(newTraceToTrack()); + assertEquals(1, tracker.trackedCount()); + } + + @Test + void maxTrackedTracesIsEnforced() { + for (int i = 0; i < MAX_TRACKED_TRACES; i++) { + tracker.add(newTraceToTrack()); + } + tracker.add(newTraceToTrack()); + assertEquals(MAX_TRACKED_TRACES, tracker.trackedCount()); + assertEquals(1, tracker.getDropped()); + } + + @Test + void expiredTraces() { + PendingTrace trace = newTraceToTrack(); + tracker.add(trace); + + tracker.flushAndCompact(MAX_TRACKED_DURATION_MILLI - 1000); + assertEquals(1, tracker.trackedCount()); + assertEquals(LongRunningTracesTracker.WRITE_RUNNING_SPANS, trace.getLongRunningTrackedState()); + + tracker.flushAndCompact(1 + MAX_TRACKED_DURATION_MILLI); + assertEquals(0, tracker.trackedCount()); + assertEquals(LongRunningTracesTracker.EXPIRED, trace.getLongRunningTrackedState()); + } + + @Test + void agentDisabledFeature() { + PendingTrace trace = newTraceToTrack(); + tracker.add(trace); + + when(features.supportsLongRunning()).thenReturn(false); + tracker.flushAndCompact(FLUSH_PERIOD_MILLI - 1000); + assertEquals(0, tracker.trackedCount()); + } + + @Test + void flushLogicWithInitialFlush() { + PendingTrace trace = newTraceToTrack(); + tracker.add(trace); + + // Before the initial flush + flushAt(INITIAL_FLUSH_PERIOD_MILLI - 1000); + verify(tracer, never()).write(any()); + clearInvocations(tracer); + + // After the initial flush + flushAt(INITIAL_FLUSH_PERIOD_MILLI + 1000); + verify(tracer, times(1)).write(any()); + assertEquals( + TimeUnit.MILLISECONDS.toNanos(INITIAL_FLUSH_PERIOD_MILLI + 1000), trace.getLastWriteTime()); + clearInvocations(tracer); + + // Before the regular flush + flushAt(INITIAL_FLUSH_PERIOD_MILLI + FLUSH_PERIOD_MILLI - 1000); + verify(tracer, never()).write(any()); + assertEquals( + TimeUnit.MILLISECONDS.toNanos(INITIAL_FLUSH_PERIOD_MILLI + 1000), trace.getLastWriteTime()); + clearInvocations(tracer); + + // After the first regular flush + flushAt(INITIAL_FLUSH_PERIOD_MILLI + FLUSH_PERIOD_MILLI + 2000); + verify(tracer, times(1)).write(any()); + assertEquals( + TimeUnit.MILLISECONDS.toNanos(INITIAL_FLUSH_PERIOD_MILLI + FLUSH_PERIOD_MILLI + 2000), + trace.getLastWriteTime()); + } + + @TableTest({ + "scenario | priority | trackerExpectedSize | traceExpectedState", + "sampler drop | 0 | 0 | -1 ", + "user drop | -1 | 0 | -1 ", + "user keep | 2 | 1 | 3 ", + "sampler keep | 1 | 1 | 3 " + }) + void priorityEvaluation(int priority, int trackerExpectedSize, int traceExpectedState) { + PendingTrace trace = factory.create(DDTraceId.ONE); + newSpanOf(trace, priority, 0); + tracker.add(trace); + + tracker.flushAndCompact(MAX_TRACKED_DURATION_MILLI - 1000); + + assertEquals(trackerExpectedSize, tracker.trackedCount()); + assertEquals(traceExpectedState, trace.getLongRunningTrackedState()); + } + + private void flushAt(long timeMilli) { + timeSource.set(TimeUnit.MILLISECONDS.toNanos(timeMilli)); + tracker.flushAndCompact(timeMilli); + } + + private PendingTrace newTraceToTrack() { + PendingTrace trace = factory.create(DDTraceId.ONE); + newSpanOf(trace, PrioritySampling.SAMPLER_KEEP, 0); + return trace; + } + + private static DDSpan newSpanOf(PendingTrace trace, int samplingPriority, long timestampMicro) { + DDSpanContext context = + new DDSpanContext( + DDTraceId.ONE, + 1, + DDSpanId.ZERO, + null, + "fakeService", + "fakeOperation", + "fakeResource", + samplingPriority, + null, + Collections.emptyMap(), + false, + "fakeType", + 0, + trace, + null, + null, + NoopPathwayContext.INSTANCE, + false, + PropagationTags.factory().empty()); + return DDSpan.create("test", timestampMicro, context, null); + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/TraceCorrelationTest.java b/dd-trace-core/src/test/java/datadog/trace/core/TraceCorrelationTest.java new file mode 100644 index 00000000000..30db3c679d4 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/TraceCorrelationTest.java @@ -0,0 +1,88 @@ +package datadog.trace.core; + +import static datadog.trace.api.config.TraceInstrumentationConfig.TRACE_128_BIT_TRACEID_LOGGING_ENABLED; +import static datadog.trace.api.config.TracerConfig.TRACE_128_BIT_TRACEID_GENERATION_ENABLED; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import datadog.trace.api.DDTraceId; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.common.writer.ListWriter; +import datadog.trace.junit.utils.config.WithConfigExtension; +import org.junit.jupiter.api.Test; +import org.tabletest.junit.TableTest; + +public class TraceCorrelationTest extends DDCoreJavaSpecification { + + @TableTest({ + "scenario | log128bTraceId", + "128-bit enabled | true ", + "128-bit disabled | false " + }) + void getTraceIdWithoutTrace(boolean log128bTraceId) { + WithConfigExtension.injectSysConfig( + TRACE_128_BIT_TRACEID_GENERATION_ENABLED, String.valueOf(log128bTraceId)); + WithConfigExtension.injectSysConfig( + TRACE_128_BIT_TRACEID_LOGGING_ENABLED, String.valueOf(log128bTraceId)); + + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + AgentSpan span = tracer.buildSpan("test").start(); + AgentScope scope = tracer.activateSpan(span); + scope.close(); + + assertEquals("0", tracer.getTraceId()); + + span.finish(); + tracer.close(); + } + + @TableTest({ + "scenario | log128bTraceId", + "128-bit enabled | true ", + "128-bit disabled | false " + }) + void getTraceIdWithTrace(boolean log128bTraceId) { + WithConfigExtension.injectSysConfig( + TRACE_128_BIT_TRACEID_GENERATION_ENABLED, String.valueOf(log128bTraceId)); + WithConfigExtension.injectSysConfig( + TRACE_128_BIT_TRACEID_LOGGING_ENABLED, String.valueOf(log128bTraceId)); + + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + AgentSpan span = tracer.buildSpan("test").start(); + AgentScope scope = tracer.activateSpan(span); + + DDTraceId traceId = ((DDSpan) scope.span()).getTraceId(); + String formattedTraceId = log128bTraceId ? traceId.toHexString() : traceId.toString(); + assertEquals(formattedTraceId, tracer.getTraceId()); + + scope.close(); + span.finish(); + tracer.close(); + } + + @Test + void getSpanIdWithoutSpan() { + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + AgentSpan span = tracer.buildSpan("test").start(); + AgentScope scope = tracer.activateSpan(span); + scope.close(); + + assertEquals("0", tracer.getSpanId()); + + span.finish(); + tracer.close(); + } + + @Test + void getSpanIdWithTrace() { + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + AgentSpan span = tracer.buildSpan("test").start(); + AgentScope scope = tracer.activateSpan(span); + + assertEquals(Long.toString(((DDSpan) scope.span()).getSpanId()), tracer.getSpanId()); + + scope.close(); + span.finish(); + tracer.close(); + } +} From 5556cb33f56e13b50b2fa8b0e38309e6727c8de7 Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Mon, 13 Apr 2026 17:25:29 +0200 Subject: [PATCH 2/4] replace injectSysConfig by @WithConfig --- .../datadog/trace/core/LongRunningTracesTrackerTest.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/dd-trace-core/src/test/java/datadog/trace/core/LongRunningTracesTrackerTest.java b/dd-trace-core/src/test/java/datadog/trace/core/LongRunningTracesTrackerTest.java index 0067a51f772..725f4daaa84 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/LongRunningTracesTrackerTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/LongRunningTracesTrackerTest.java @@ -21,6 +21,7 @@ import datadog.trace.api.time.ControllableTimeSource; import datadog.trace.core.monitor.HealthMetrics; import datadog.trace.core.propagation.PropagationTags; +import datadog.trace.junit.utils.config.WithConfig; import datadog.trace.junit.utils.config.WithConfigExtension; import datadog.trace.test.util.DDJavaSpecification; import java.util.Arrays; @@ -33,6 +34,9 @@ import org.junit.jupiter.api.Test; import org.tabletest.junit.TableTest; +@WithConfig(key = "trace.experimental.long-running.enabled", value = "true") +@WithConfig(key = "trace.experimental.long-running.initial.flush.interval", value = "10") +@WithConfig(key = "trace.experimental.long-running.flush.interval", value = "20") public class LongRunningTracesTrackerTest extends DDJavaSpecification { private static final long INITIAL_FLUSH_PERIOD_MILLI = TimeUnit.SECONDS.toMillis(10); @@ -60,11 +64,6 @@ static void checkJvm() { @BeforeEach void setup() { - WithConfigExtension.injectSysConfig("trace.experimental.long-running.enabled", "true"); - WithConfigExtension.injectSysConfig( - "trace.experimental.long-running.initial.flush.interval", "10"); - WithConfigExtension.injectSysConfig("trace.experimental.long-running.flush.interval", "20"); - tracer = mock(CoreTracer.class); traceConfig = mock(CoreTracer.ConfigSnapshot.class); features = mock(DDAgentFeaturesDiscovery.class); From 91df5cdb33fd774c467f075be9f6a6ed390376df Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Mon, 13 Apr 2026 17:59:35 +0200 Subject: [PATCH 3/4] spotless --- .../java/datadog/trace/core/LongRunningTracesTrackerTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/dd-trace-core/src/test/java/datadog/trace/core/LongRunningTracesTrackerTest.java b/dd-trace-core/src/test/java/datadog/trace/core/LongRunningTracesTrackerTest.java index 725f4daaa84..a8cc00a6869 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/LongRunningTracesTrackerTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/LongRunningTracesTrackerTest.java @@ -22,7 +22,6 @@ import datadog.trace.core.monitor.HealthMetrics; import datadog.trace.core.propagation.PropagationTags; import datadog.trace.junit.utils.config.WithConfig; -import datadog.trace.junit.utils.config.WithConfigExtension; import datadog.trace.test.util.DDJavaSpecification; import java.util.Arrays; import java.util.Collections; From 15c6cdb2dcb6927320629a52286600b27f03f920 Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Tue, 14 Apr 2026 14:34:18 +0200 Subject: [PATCH 4/4] replace tabletest by valuesource for single param --- .claude/skills/migrate-groovy-to-java/SKILL.md | 2 +- .../datadog/trace/core/BlackholeSpanTest.java | 16 +++++++--------- .../core/LongRunningTracesTrackerTest.java | 3 +-- .../trace/core/TraceCorrelationTest.java | 17 ++++++----------- 4 files changed, 15 insertions(+), 23 deletions(-) diff --git a/.claude/skills/migrate-groovy-to-java/SKILL.md b/.claude/skills/migrate-groovy-to-java/SKILL.md index 617df592cfa..c08c85ba296 100644 --- a/.claude/skills/migrate-groovy-to-java/SKILL.md +++ b/.claude/skills/migrate-groovy-to-java/SKILL.md @@ -20,7 +20,7 @@ When converting Groovy code to Java code, make sure that: - `@TableTest` and `@MethodSource` may be combined on the same `@ParameterizedTest` when most cases are tabular but a few cases require programmatic setup. - In combined mode, keep table-friendly cases in `@TableTest`, and put only non-tabular/complex cases in `@MethodSource`. - If `@TableTest` is not viable for the test at all, use `@MethodSource` only. -- If `@TableTest` was successfully used, `@ParameterizedTest` can then be removed as `@TableTest` replace it fully +- If `@TableTest` was successfully used and if the `@ParameterizedTest` is not used to specify the test name, `@ParameterizedTest` can then be removed as `@TableTest` replace it fully. - For `@MethodSource`, name the arguments method `Arguments` (camelCase, e.g. `testMethodArguments`) and return `Stream` using `Stream.of(...)` and `arguments(...)` with static import. - Ensure parameterized test names are human-readable (i.e. no hashcodes); instead add a description string as the first `Arguments.arguments(...)` value or index the test case - When converting tuples, create a light dedicated structure instead to keep the typing system diff --git a/dd-trace-core/src/test/java/datadog/trace/core/BlackholeSpanTest.java b/dd-trace-core/src/test/java/datadog/trace/core/BlackholeSpanTest.java index 176d7d977c2..b13389ee32f 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/BlackholeSpanTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/BlackholeSpanTest.java @@ -1,5 +1,6 @@ package datadog.trace.core; +import static datadog.trace.junit.utils.config.WithConfigExtension.injectSysConfig; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -7,20 +8,17 @@ import datadog.trace.bootstrap.instrumentation.api.AgentScope; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.common.writer.ListWriter; -import datadog.trace.junit.utils.config.WithConfigExtension; import java.util.Arrays; import java.util.Properties; -import org.tabletest.junit.TableTest; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; public class BlackholeSpanTest extends DDCoreJavaSpecification { - @TableTest({ - "scenario | moreBits", - "128 bit traceid logging enabled | true ", - "128 bit traceid logging disabled | false " - }) - void shouldMuteTracing(String moreBits) throws Exception { - WithConfigExtension.injectSysConfig("trace.128.bit.traceid.logging.enabled", moreBits); + @ValueSource(strings = {"true", "false"}) + @ParameterizedTest + void shouldMuteTracing(String use128bitTraceId) throws Exception { + injectSysConfig("trace.128.bit.traceid.logging.enabled", use128bitTraceId); ListWriter writer = new ListWriter(); Properties props = new Properties(); CoreTracer tracer = tracerBuilder().withProperties(props).writer(writer).build(); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/LongRunningTracesTrackerTest.java b/dd-trace-core/src/test/java/datadog/trace/core/LongRunningTracesTrackerTest.java index a8cc00a6869..40e21d97f7b 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/LongRunningTracesTrackerTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/LongRunningTracesTrackerTest.java @@ -121,10 +121,9 @@ void traceWithoutRightStateAreNotTracked() { @Test void maxTrackedTracesIsEnforced() { - for (int i = 0; i < MAX_TRACKED_TRACES; i++) { + for (int i = 0; i < MAX_TRACKED_TRACES + 1; i++) { tracker.add(newTraceToTrack()); } - tracker.add(newTraceToTrack()); assertEquals(MAX_TRACKED_TRACES, tracker.trackedCount()); assertEquals(1, tracker.getDropped()); } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/TraceCorrelationTest.java b/dd-trace-core/src/test/java/datadog/trace/core/TraceCorrelationTest.java index 30db3c679d4..b4edeed98de 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/TraceCorrelationTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/TraceCorrelationTest.java @@ -10,15 +10,13 @@ import datadog.trace.common.writer.ListWriter; import datadog.trace.junit.utils.config.WithConfigExtension; import org.junit.jupiter.api.Test; -import org.tabletest.junit.TableTest; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; public class TraceCorrelationTest extends DDCoreJavaSpecification { - @TableTest({ - "scenario | log128bTraceId", - "128-bit enabled | true ", - "128-bit disabled | false " - }) + @ValueSource(booleans = {true, false}) + @ParameterizedTest void getTraceIdWithoutTrace(boolean log128bTraceId) { WithConfigExtension.injectSysConfig( TRACE_128_BIT_TRACEID_GENERATION_ENABLED, String.valueOf(log128bTraceId)); @@ -36,11 +34,8 @@ void getTraceIdWithoutTrace(boolean log128bTraceId) { tracer.close(); } - @TableTest({ - "scenario | log128bTraceId", - "128-bit enabled | true ", - "128-bit disabled | false " - }) + @ValueSource(booleans = {true, false}) + @ParameterizedTest void getTraceIdWithTrace(boolean log128bTraceId) { WithConfigExtension.injectSysConfig( TRACE_128_BIT_TRACEID_GENERATION_ENABLED, String.valueOf(log128bTraceId));