From 4ed6bb010992d3d4d6b283776f43d3b690e7d00a Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Tue, 2 Jun 2026 19:12:06 +0200 Subject: [PATCH 1/2] Migrate dd-trace-core groovy files to java part 10 we migrate 11 tests: - IterationSpansForkedTest - ScopeAndContinuationLayoutTest - ScopeManagerDepthTest - ScopeManagerTest - IntegrationAdderTest - InternalTagsAdderTest - PayloadTagsProcessorTest - PeerServiceCalculatorTest - PostProcessorChainTest - QueryObfuscatorTest - SpanPointersProcessorTest --- .../IterationSpansForkedTest.groovy | 225 --- .../ScopeAndContinuationLayoutTest.groovy | 25 - .../scopemanager/ScopeManagerDepthTest.groovy | 130 -- .../core/scopemanager/ScopeManagerTest.groovy | 1289 ----------------- .../tagprocessor/IntegrationAdderTest.groovy | 26 - .../tagprocessor/InternalTagsAdderTest.groovy | 61 - .../PayloadTagsProcessorTest.groovy | 413 ------ .../PeerServiceCalculatorTest.groovy | 136 -- .../PostProcessorChainTest.groovy | 68 - .../tagprocessor/QueryObfuscatorTest.groovy | 54 - .../SpanPointersProcessorTest.groovy | 101 -- .../trace/core/ScopeManagerTestBridge.java | 10 + .../IterationSpansForkedTest.java | 199 +++ .../ScopeAndContinuationLayoutTest.java | 34 + .../scopemanager/ScopeManagerDepthTest.java | 130 ++ .../core/scopemanager/ScopeManagerTest.java | 1167 +++++++++++++++ .../tagprocessor/IntegrationAdderTest.java | 38 + .../tagprocessor/InternalTagsAdderTest.java | 70 + .../PayloadTagsProcessorTest.java | 491 +++++++ .../PeerServiceCalculatorTest.java | 146 ++ .../tagprocessor/PostProcessorChainTest.java | 91 ++ .../tagprocessor/QueryObfuscatorTest.java | 58 + .../SpanPointersProcessorTest.java | 59 + .../datadog/trace/test/util/ThreadUtils.java | 26 + 24 files changed, 2519 insertions(+), 2528 deletions(-) delete mode 100644 dd-trace-core/src/test/groovy/datadog/trace/core/scopemanager/IterationSpansForkedTest.groovy delete mode 100644 dd-trace-core/src/test/groovy/datadog/trace/core/scopemanager/ScopeAndContinuationLayoutTest.groovy delete mode 100644 dd-trace-core/src/test/groovy/datadog/trace/core/scopemanager/ScopeManagerDepthTest.groovy delete mode 100644 dd-trace-core/src/test/groovy/datadog/trace/core/scopemanager/ScopeManagerTest.groovy delete mode 100644 dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/IntegrationAdderTest.groovy delete mode 100644 dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/InternalTagsAdderTest.groovy delete mode 100644 dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PayloadTagsProcessorTest.groovy delete mode 100644 dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PeerServiceCalculatorTest.groovy delete mode 100644 dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PostProcessorChainTest.groovy delete mode 100644 dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/QueryObfuscatorTest.groovy delete mode 100644 dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/SpanPointersProcessorTest.groovy create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/ScopeManagerTestBridge.java create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/scopemanager/IterationSpansForkedTest.java create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/scopemanager/ScopeAndContinuationLayoutTest.java create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/scopemanager/ScopeManagerDepthTest.java create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/scopemanager/ScopeManagerTest.java create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/IntegrationAdderTest.java create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/InternalTagsAdderTest.java create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/PayloadTagsProcessorTest.java create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/PeerServiceCalculatorTest.java create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/PostProcessorChainTest.java create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/QueryObfuscatorTest.java create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/SpanPointersProcessorTest.java diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/scopemanager/IterationSpansForkedTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/scopemanager/IterationSpansForkedTest.groovy deleted file mode 100644 index 7002f8efd67..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/scopemanager/IterationSpansForkedTest.groovy +++ /dev/null @@ -1,225 +0,0 @@ -package datadog.trace.core.scopemanager - -import datadog.metrics.api.statsd.StatsDClient -import datadog.trace.bootstrap.instrumentation.api.AgentSpan -import datadog.trace.common.writer.ListWriter -import datadog.trace.core.CoreTracer -import datadog.trace.core.DDSpan -import datadog.trace.core.test.DDCoreSpecification - -class IterationSpansForkedTest extends DDCoreSpecification { - - ListWriter writer - CoreTracer tracer - ContinuableScopeManager scopeManager - StatsDClient statsDClient - - def setup() { - injectSysConfig("dd.trace.scope.iteration.keep.alive", "1") - - writer = new ListWriter() - statsDClient = Mock() - tracer = tracerBuilder().writer(writer).statsDClient(statsDClient).build() - scopeManager = tracer.scopeManager - } - - def cleanup() { - tracer.close() - } - - def "root iteration scope lifecycle"() { - when: - tracer.closePrevious(true) - def span1 = tracer.buildSpan("datadog", "next1").start() - def scope1 = tracer.activateNext(span1) - - then: - writer.empty - - and: - scope1.span() == span1 - scopeManager.active() == scope1 - !spanFinished(span1) - - when: - tracer.closePrevious(true) - def span2 = tracer.buildSpan("datadog", "next2").start() - def scope2 = tracer.activateNext(span2) - - then: - spanFinished(span1) - writer == [[span1]] - - and: - scope2.span() == span2 - scopeManager.active() == scope2 - !spanFinished(span2) - - when: - tracer.closePrevious(true) - def span3 = tracer.buildSpan("datadog", "next3").start() - def scope3 = tracer.activateNext(span3) - - then: - spanFinished(span2) - writer == [[span1], [span2]] - - and: - scope3.span() == span3 - scopeManager.active() == scope3 - !spanFinished(span3) - - when: - // 'next3' should time out & finish after 1s - writer.waitForTraces(3) - - then: - spanFinished(span3) - writer == [[span1], [span2], [span3]] - - and: - scopeManager.active() == null - } - - def "non-root iteration scope lifecycle"() { - setup: - def span0 = tracer.buildSpan("datadog", "parent").start() - def scope0 = tracer.activateSpan(span0) - - when: - tracer.closePrevious(true) - def span1 = tracer.buildSpan("datadog", "next1").start() - def scope1 = tracer.activateNext(span1) - - then: - writer.empty - - and: - scope1.span() == span1 - scopeManager.active() == scope1 - !spanFinished(span1) - - when: - tracer.closePrevious(true) - def span2 = tracer.buildSpan("datadog", "next2").start() - def scope2 = tracer.activateNext(span2) - - then: - spanFinished(span1) - writer.empty - - and: - scope2.span() == span2 - scopeManager.active() == scope2 - !spanFinished(span2) - - when: - tracer.closePrevious(true) - def span3 = tracer.buildSpan("datadog", "next3").start() - def scope3 = tracer.activateNext(span3) - - then: - spanFinished(span2) - writer.empty - - and: - scope3.span() == span3 - scopeManager.active() == scope3 - !spanFinished(span3) - - when: - scope0.close() - span0.finish() - // closing the parent scope will close & finish 'next3' - writer.waitForTraces(1) - - then: - spanFinished(span3) - spanFinished(span0) - sortSpansByStart() - writer == [[span0, span1, span2, span3]] - - and: - scopeManager.active() == null - } - - def "nested iteration scope lifecycle"() { - when: - tracer.closePrevious(true) - def span1 = tracer.buildSpan("datadog", "next").start() - def scope1 = tracer.activateNext(span1) - - then: - writer.empty - - and: - scope1.span() == span1 - scopeManager.active() == scope1 - !spanFinished(span1) - - when: - def span1A = tracer.buildSpan("datadog", "method").start() - def scope1A = tracer.activateSpan(span1A) - - and: - tracer.closePrevious(true) - def span1A1 = tracer.buildSpan("datadog", "next").start() - def scope1A1 = tracer.activateNext(span1A1) - - then: - !spanFinished(span1) - writer.empty - - and: - scope1A1.span() == span1A1 - scopeManager.active() == scope1A1 - !spanFinished(span1A1) - - when: - tracer.closePrevious(true) - def span1A2 = tracer.buildSpan("datadog", "next").start() - def scope1A2 = tracer.activateNext(span1A2) - - then: - spanFinished(span1A1) - writer.empty - - and: - scope1A2.span() == span1A2 - scopeManager.active() == scope1A2 - !spanFinished(span1A2) - - when: - scope1A.close() - span1A.finish() - // closing the intervening scope will close & finish 'next1_2' - - then: - spanFinished(span1A2) - spanFinished(span1A) - !spanFinished(span1) - writer.empty - - when: - // 'next1' should time out & finish after 1s to complete the trace - writer.waitForTraces(1) - - then: - spanFinished(span1) - sortSpansByStart() - writer == [[span1, span1A, span1A1, span1A2]] - - and: - scopeManager.active() == null - } - - boolean spanFinished(AgentSpan span) { - return ((DDSpan) span)?.isFinished() - } - - private List sortSpansByStart() { - writer.firstTrace().sort { a, b -> - return a.startTimeNano <=> b.startTimeNano - } - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/scopemanager/ScopeAndContinuationLayoutTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/scopemanager/ScopeAndContinuationLayoutTest.groovy deleted file mode 100644 index bba8f7d0028..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/scopemanager/ScopeAndContinuationLayoutTest.groovy +++ /dev/null @@ -1,25 +0,0 @@ -package datadog.trace.core.scopemanager - -import datadog.trace.test.util.DDSpecification -import org.openjdk.jol.info.ClassLayout -import spock.lang.Requires - -@Requires({ - !System.getProperty("java.vendor").toUpperCase().contains("IBM") -}) -class ScopeAndContinuationLayoutTest extends DDSpecification { - - def "continuable scope layout"() { - expect: layoutAcceptable(ContinuableScope, 32) - } - - def "single continuation layout"() { - expect: layoutAcceptable(ScopeContinuation, 32) - } - - def layoutAcceptable(Class klass, int acceptableSize) { - def layout = ClassLayout.parseClass(klass) - System.err.println(layout.toPrintable()) - return layout.instanceSize() <= acceptableSize - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/scopemanager/ScopeManagerDepthTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/scopemanager/ScopeManagerDepthTest.groovy deleted file mode 100644 index 4c7a1176ee7..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/scopemanager/ScopeManagerDepthTest.groovy +++ /dev/null @@ -1,130 +0,0 @@ -package datadog.trace.core.scopemanager - -import datadog.trace.api.config.TracerConfig -import datadog.trace.bootstrap.instrumentation.api.AgentScope -import datadog.trace.bootstrap.instrumentation.api.AgentSpan -import datadog.trace.common.writer.ListWriter -import datadog.trace.core.test.DDCoreSpecification - -import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.noopScope -import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.noopSpan - -class ScopeManagerDepthTest extends DDCoreSpecification { - def "scopemanager returns noop scope if depth exceeded"() { - given: - def tracer = tracerBuilder().writer(new ListWriter()).build() - def scopeManager = tracer.scopeManager - - when: "fill up the scope stack" - AgentScope scope - for (int i = 0; i < depth; i++) { - def testSpan = tracer.buildSpan("test", "test").start() - scope = tracer.activateSpan(testSpan) - assert scope instanceof ContinuableScope - } - - then: "last scope is still valid" - scopeManager.scopeStack().depth() == depth - - when: "activate span over limit" - def span = tracer.buildSpan("test", "test").start() - scope = tracer.activateSpan(span) - - then: "a noop instance is returned" - scope == noopScope() - - when: "activate a noop scope over the limit" - scope = scopeManager.activateManualSpan(noopSpan()) - - then: "still have a noop instance" - scope == noopScope() - - and: "scope stack not effected." - scopeManager.scopeStack().depth() == depth - - cleanup: - scopeManager.scopeStack().clear() - tracer.close() - - where: - depth = 100 // Using ConfigDefaults here causes classloading issues - } - - def "scopemanager ignores depth limit when 0"() { - given: - injectSysConfig(TracerConfig.SCOPE_DEPTH_LIMIT, "0") - def tracer = tracerBuilder().writer(new ListWriter()).build() - def scopeManager = tracer.scopeManager - - when: "fill up the scope stack" - AgentScope scope - for (int i = 0; i < defaultLimit; i++) { - def testSpan = tracer.buildSpan("test", "test").start() - scope = tracer.activateSpan(testSpan) - assert scope instanceof ContinuableScope - } - - then: "last scope is still valid" - scopeManager.scopeStack().depth() == defaultLimit - - when: "activate a scope" - def span = tracer.buildSpan("test", "test").start() - scope = tracer.activateSpan(span) - - then: "a real scope is returned" - scope != noopScope() - scopeManager.scopeStack().depth() == defaultLimit + 1 - - when: "activate a noop span" - scope = scopeManager.activateManualSpan(noopSpan()) - - then: "a real instance is still returned" - scope != noopScope() - - and: "scope stack not effected." - scopeManager.scopeStack().depth() == defaultLimit + 2 - - cleanup: - scopeManager.scopeStack().clear() - tracer.close() - - where: - defaultLimit = 100 // Using ConfigDefaults here causes classloading issues - } - - def "depth is correctly updated with out of order closing"() { - // The decision here is that depth is the top-most open scope - // Closed scopes that are not on top still count for depth - - given: - def tracer = tracerBuilder().writer(new ListWriter()).build() - def scopeManager = tracer.scopeManager - - when: - AgentSpan firstSpan = tracer.buildSpan("test", "foo").start() - AgentScope firstScope = tracer.activateSpan(firstSpan) - - AgentSpan secondSpan = tracer.buildSpan("test", "foo").start() - AgentScope secondScope = tracer.activateSpan(secondSpan) - - then: - scopeManager.scopeStack().depth() == 2 - - when: - firstSpan.finish() - firstScope.close() - - then: - scopeManager.scopeStack().depth() == 2 - - when: - secondSpan.finish() - secondScope.close() - - then: - scopeManager.scopeStack().depth() == 0 - - cleanup: - tracer.close() - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/scopemanager/ScopeManagerTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/scopemanager/ScopeManagerTest.groovy deleted file mode 100644 index be70ceec92c..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/scopemanager/ScopeManagerTest.groovy +++ /dev/null @@ -1,1289 +0,0 @@ -package datadog.trace.core.scopemanager - -import datadog.context.Context -import datadog.context.ContextKey -import datadog.trace.test.util.ThreadUtils -import datadog.trace.api.DDTraceId -import datadog.trace.api.Stateful -import datadog.trace.api.interceptor.MutableSpan -import datadog.trace.api.interceptor.TraceInterceptor -import datadog.trace.api.scopemanager.ExtendedScopeListener -import datadog.trace.bootstrap.instrumentation.api.AgentScope -import datadog.trace.bootstrap.instrumentation.api.AgentSpan -import datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration -import datadog.trace.common.writer.ListWriter -import datadog.trace.api.scopemanager.ScopeListener -import datadog.trace.context.TraceScope -import datadog.trace.core.CoreTracer -import datadog.trace.core.DDSpan -import datadog.trace.core.test.DDCoreSpecification -import spock.lang.Shared - -import java.lang.ref.WeakReference -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors -import java.util.concurrent.Future -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicInteger -import java.util.concurrent.atomic.AtomicReference - -import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.noopContinuation -import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.noopSpan -import static datadog.trace.core.scopemanager.EVENT.ACTIVATE -import static datadog.trace.core.scopemanager.EVENT.CLOSE -import static datadog.trace.test.util.GCUtils.awaitGC - -enum EVENT { - ACTIVATE, CLOSE -} - -class ScopeManagerTest extends DDCoreSpecification { - @Override - protected boolean useStrictTraceWrites() { - // This tests the behavior of the relaxed pending trace implementation - return false - } - - ListWriter writer - CoreTracer tracer - ContinuableScopeManager scopeManager - EventCountingListener eventCountingListener - EventCountingExtendedListener eventCountingExtendedListener - ProfilingContextIntegration profilingContext - - def setup() { - def state = Stub(Stateful) - profilingContext = Mock(ProfilingContextIntegration, { - newScopeState(_) >> state - name() >> "mock" - }) - writer = new ListWriter() - tracer = tracerBuilder().writer(writer).profilingContextIntegration(profilingContext).build() - scopeManager = tracer.scopeManager - eventCountingListener = new EventCountingListener() - scopeManager.addScopeListener(eventCountingListener) - eventCountingExtendedListener = new EventCountingExtendedListener() - scopeManager.addScopeListener(eventCountingExtendedListener) - } - - def cleanup() { - tracer.close() - } - - def "non-ddspan activation results in a continuable scope"() { - when: - def scope = scopeManager.activateSpan(noopSpan()) - - then: - scopeManager.active() == scope - scope instanceof ContinuableScope - - when: - scope.close() - - then: - scopeManager.active() == null - } - - def "no scope is active before activation"() { - setup: - def builder = tracer.buildSpan("test", "test") - builder.start() - - expect: - scopeManager.active() == null - writer.empty - } - - def "simple scope and span lifecycle"() { - when: - def span = tracer.buildSpan("test", "test").start() - def scope = tracer.activateSpan(span) - - then: - scope.span() == span - !spanFinished(scope.span()) - scopeManager.active() == scope - scope instanceof ContinuableScope - writer.empty - - when: - scope.close() - - then: - !spanFinished(scope.span()) - writer == [] - scopeManager.active() == null - - when: - span.finish() - writer.waitForTraces(1) - - then: - spanFinished(scope.span()) - writer == [[scope.span()]] - scopeManager.active() == null - } - - def "sets parent as current upon close"() { - when: - def parentSpan = tracer.buildSpan("test", "parent").start() - def parentScope = tracer.activateSpan(parentSpan) - def childSpan = tracer.buildSpan("test", "child").start() - def childScope = tracer.activateSpan(childSpan) - - then: - scopeManager.active() == childScope - childScope.span().context().parentId == parentScope.span().context().spanId - childScope.span().context().traceCollector == parentScope.span().context().traceCollector - - when: - childScope.close() - - then: - scopeManager.active() == parentScope - !spanFinished(childScope.span()) - !spanFinished(parentScope.span()) - writer == [] - } - - def "sets parent as current upon close with noop child"() { - when: - def parentSpan = tracer.buildSpan("test", "parent").start() - def parentScope = tracer.activateSpan(parentSpan) - def childSpan = noopSpan() - def childScope = tracer.activateSpan(childSpan) - - then: - scopeManager.active() == childScope - - when: - childScope.close() - - then: - scopeManager.active() == parentScope - !spanFinished(parentScope.span()) - writer == [] - } - - def "DDScope creates no-op continuations when propagation is not set"() { - when: - def span = tracer.buildSpan("test", "test").start() - tracer.activateSpan(span) - tracer.setAsyncPropagationEnabled(false) - def continuation = tracer.captureActiveSpan() - - then: - continuation == noopContinuation() - - when: - tracer.setAsyncPropagationEnabled(true) - continuation = tracer.captureActiveSpan() - - then: - continuation != noopContinuation() && continuation != null - - cleanup: - continuation.cancel() - } - - def "Continuation.cancel doesn't close parent scope"() { - when: - def span = tracer.buildSpan("test", "test").start() - def scope = tracer.activateSpan(span) - def continuation = tracer.captureActiveSpan() - - then: - continuation != null - - when: - continuation.cancel() - - then: - scopeManager.active() == scope - } - - // @Flaky("awaitGC is flaky") - def "test continuation doesn't have hard reference on scope"() { - when: - def span = tracer.buildSpan("test", "test").start() - def scopeRef = new AtomicReference(tracer.activateSpan(span)) - def continuation = tracer.captureActiveSpan() - - then: - continuation != null - - when: - scopeRef.get().close() - - then: - scopeManager.active() == null - - when: - def ref = new WeakReference(scopeRef.get()) - scopeRef.set(null) - awaitGC(ref) - - then: - continuation != null - ref.get() == null - !spanFinished(span) - writer == [] - } - - def "hard reference on continuation does not prevent trace from reporting"() { - when: - def span = tracer.buildSpan("test", "test").start() - def scope = tracer.activateSpan(span) - def continuation = tracer.captureActiveSpan() - - then: - continuation != null - - when: - scope.close() - span.finish() - if (autoClose) { - continuation.cancel() - } - - then: - scopeManager.active() == null - spanFinished(span) - - when: - writer.waitForTraces(1) - - then: - writer == [[span]] - - where: - autoClose << [true, false] - } - - def "continuation restores trace"() { - when: - def parentSpan = tracer.buildSpan("test", "parent").start() - def parentScope = tracer.activateSpan(parentSpan) - def childSpan = tracer.buildSpan("test", "child").start() - def childScope = tracer.activateSpan(childSpan) - - def continuation = tracer.captureActiveSpan() - childScope.close() - - then: - continuation != null - scopeManager.active() == parentScope - !spanFinished(childSpan) - !spanFinished(parentSpan) - - when: - parentScope.close() - parentSpan.finish() - - then: "parent span is finished, but trace is not reported" - scopeManager.active() == null - !spanFinished(childSpan) - spanFinished(parentSpan) - writer == [] - - when: "activating the continuation" - def newScope = continuation.activate() - - then: "the continued scope becomes active and span state doesnt change" - newScope instanceof ContinuableScope - tracer.isAsyncPropagationEnabled() - scopeManager.active() == newScope - newScope != childScope - newScope != parentScope - newScope.span() == childSpan - !spanFinished(childSpan) - spanFinished(parentSpan) - writer == [] - - when: "creating and activating a second continuation" - def newContinuation = tracer.captureActiveSpan() - newScope.close() - def secondContinuedScope = newContinuation.activate() - secondContinuedScope.close() - childSpan.finish() - writer.waitForTraces(1) - - then: "spans are all finished and trace is reported" - scopeManager.active() == null - spanFinished(childSpan) - spanFinished(parentSpan) - writer == [[childSpan, parentSpan]] - } - - def "continuation allows adding spans even after other spans were completed"() { - when: "creating and activating a continuation" - def span = tracer.buildSpan("test", "test").start() - def scope = tracer.activateSpan(span) - def continuation = tracer.captureActiveSpan() - scope.close() - span.finish() - - def newScope = continuation.activate() - - then: "the continuation sets the active scope" - newScope instanceof ContinuableScope - newScope != scope - scopeManager.active() == newScope - spanFinished(span) - writer == [] - - when: "creating a new child span under a continued scope" - def childSpan = tracer.buildSpan("test", "child").start() - def childScope = tracer.activateSpan(childSpan) - childScope.close() - childSpan.finish() - - then: - scopeManager.active() == newScope - - when: - scopeManager.active().close() - writer.waitForTraces(1) - - then: "the child has the correct parent" - scopeManager.active() == null - spanFinished(childSpan) - childSpan.context().parentId == span.context().spanId - writer == [[childSpan, span]] - } - - def "test activating same span multiple times"() { - setup: - def span = tracer.buildSpan("test", "test").start() - def state = Mock(Stateful) - - when: - AgentScope scope1 = scopeManager.activateSpan(span) - - then: - assertEvents([ACTIVATE]) - 1 * profilingContext.newScopeState(_) >> state - - when: - AgentScope scope2 = scopeManager.activateSpan(span) - - then: 'Activating the same span multiple times does not create a new scope' - assertEvents([ACTIVATE]) - 0 * profilingContext.newScopeState(_) - - when: - scope2.close() - - then: 'Closing a scope once that has been activated multiple times does not close' - assertEvents([ACTIVATE]) - 0 * state.close() - - when: - scope1.close() - - then: - assertEvents([ACTIVATE, CLOSE]) - 1 * state.close() - } - - def "opening and closing multiple scopes"() { - when: - AgentSpan span = tracer.buildSpan("test", "foo").start() - AgentScope continuableScope = tracer.activateSpan(span) - - then: - continuableScope instanceof ContinuableScope - assertEvents([ACTIVATE]) - - when: - AgentSpan childSpan = tracer.buildSpan("test", "foo").start() - AgentScope childDDScope = tracer.activateSpan(childSpan) - - then: - childDDScope instanceof ContinuableScope - assertEvents([ACTIVATE, ACTIVATE]) - - when: - childDDScope.close() - childSpan.finish() - - then: - assertEvents([ACTIVATE, ACTIVATE, CLOSE, ACTIVATE]) - - when: - continuableScope.close() - span.finish() - - then: - assertEvents([ACTIVATE, ACTIVATE, CLOSE, ACTIVATE, CLOSE]) - } - - def "closing scope out of order - simple"() { - when: - AgentSpan firstSpan = tracer.buildSpan("test", "foo").start() - AgentScope firstScope = tracer.activateSpan(firstSpan) - - AgentSpan secondSpan = tracer.buildSpan("test", "bar").start() - AgentScope secondScope = tracer.activateSpan(secondSpan) - - firstSpan.finish() - firstScope.close() - - then: - assertEvents([ACTIVATE, ACTIVATE]) - 1 * profilingContext.onRootSpanStarted(_) - 1 * profilingContext.onAttach() - 1 * profilingContext.encodeOperationName("foo") - 1 * profilingContext.encodeOperationName("bar") - 2 * profilingContext.newScopeState(_) >> Stub(Stateful) - 0 * _ - - when: - secondSpan.finish() - secondScope.close() - - then: - 1 * profilingContext.onRootSpanFinished(_, _) - 1 * profilingContext.onDetach() - assertEvents([ACTIVATE, ACTIVATE, CLOSE, CLOSE]) - 0 * _ - - when: - firstScope.close() - - then: - assertEvents([ACTIVATE, ACTIVATE, CLOSE, CLOSE]) - } - - def "closing scope out of order - complex"() { - // Events are checked twice in each case to ensure a call to - // scopeManager.active() or tracer.activeSpan() doesn't change the count - - when: - AgentSpan firstSpan = tracer.buildSpan("test", "foo").start() - AgentScope firstScope = tracer.activateSpan(firstSpan) - - then: - assertEvents([ACTIVATE]) - tracer.activeSpan() == firstSpan - scopeManager.active() == firstScope - assertEvents([ACTIVATE]) - 1 * profilingContext.onRootSpanStarted(_) - 1 * profilingContext.onAttach() - 1 * profilingContext.encodeOperationName("foo") - 1 * profilingContext.newScopeState(_) >> Stub(Stateful) - 0 * _ - - when: - AgentSpan secondSpan = tracer.buildSpan("test", "bar").start() - AgentScope secondScope = tracer.activateSpan(secondSpan) - - then: - assertEvents([ACTIVATE, ACTIVATE]) - tracer.activeSpan() == secondSpan - scopeManager.active() == secondScope - assertEvents([ACTIVATE, ACTIVATE]) - 1 * profilingContext.encodeOperationName("bar") - 1 * profilingContext.newScopeState(_) >> Stub(Stateful) - 0 * _ - - when: - AgentSpan thirdSpan = tracer.buildSpan("test", "quux").start() - AgentScope thirdScope = tracer.activateSpan(thirdSpan) - - then: - assertEvents([ACTIVATE, ACTIVATE, ACTIVATE]) - tracer.activeSpan() == thirdSpan - scopeManager.active() == thirdScope - assertEvents([ACTIVATE, ACTIVATE, ACTIVATE]) - 1 * profilingContext.encodeOperationName("quux") - 1 * profilingContext.newScopeState(_) >> Stub(Stateful) - 0 * _ - - when: - secondScope.close() - - then: - assertEvents([ACTIVATE, ACTIVATE, ACTIVATE]) - tracer.activeSpan() == thirdSpan - scopeManager.active() == thirdScope - assertEvents([ACTIVATE, ACTIVATE, ACTIVATE]) - 0 * _ - - when: - thirdScope.close() - - then: - assertEvents([ACTIVATE, ACTIVATE, ACTIVATE, CLOSE, CLOSE, ACTIVATE]) - tracer.activeSpan() == firstSpan - scopeManager.active() == firstScope - - assertEvents([ACTIVATE, ACTIVATE, ACTIVATE, CLOSE, CLOSE, ACTIVATE]) - 0 * _ - - when: - firstScope.close() - - then: - assertEvents([ - ACTIVATE, - ACTIVATE, - ACTIVATE, - CLOSE, - CLOSE, - ACTIVATE, - CLOSE - ]) - scopeManager.active() == null - assertEvents([ - ACTIVATE, - ACTIVATE, - ACTIVATE, - CLOSE, - CLOSE, - ACTIVATE, - CLOSE - ]) - 1 * profilingContext.onDetach() - 0 * _ - } - - def "closing scope out of order - multiple activations"() { - setup: - def span = tracer.buildSpan("test", "test").start() - - when: - AgentScope scope1 = scopeManager.activateSpan(span) - - then: - assertEvents([ACTIVATE]) - - when: - AgentScope scope2 = scopeManager.activateSpan(span) - - then: 'Activating the same span multiple times does not create a new scope' - assertEvents([ACTIVATE]) - - when: - AgentSpan thirdSpan = tracer.buildSpan("test", "quux").start() - AgentScope thirdScope = tracer.activateSpan(thirdSpan) - 0 * _ - - then: - assertEvents([ACTIVATE, ACTIVATE]) - tracer.activeSpan() == thirdSpan - scopeManager.active() == thirdScope - assertEvents([ACTIVATE, ACTIVATE]) - 1 * profilingContext.encodeOperationName("quux") - 1 * profilingContext.newScopeState(_) >> Stub(Stateful) - 0 * _ - - when: - scope2.close() - - then: 'Closing a scope once that has been activated multiple times does not close' - assertEvents([ACTIVATE, ACTIVATE]) - 0 * _ - - when: - thirdScope.close() - thirdSpan.finish() - - then: 'Closing scope above multiple activated scope does not close it' - assertEvents([ACTIVATE, ACTIVATE, CLOSE, ACTIVATE]) - 0 * _ - - when: - scope1.close() - - then: - assertEvents([ACTIVATE, ACTIVATE, CLOSE, ACTIVATE, CLOSE]) - } - - def "Closing a continued scope out of order cancels the continuation"() { - when: - def span = tracer.buildSpan("test", "test").start() - def scope = tracer.activateSpan(span) - def continuation = tracer.captureActiveSpan() - scope.close() - span.finish() - - then: - scopeManager.active() == null - spanFinished(span) - writer == [] - - when: - def continuedScope = continuation.activate() - - AgentSpan secondSpan = tracer.buildSpan("test", "test2").start() - AgentScope secondScope = (ContinuableScope) tracer.activateSpan(secondSpan) - - then: - scopeManager.active() == secondScope - - when: - continuedScope.close() - - then: - scopeManager.active() == secondScope - writer == [] - - when: - secondScope.close() - secondSpan.finish() - writer.waitForTraces(1) - - then: - writer == [[secondSpan, span]] - } - - def "exception thrown in TraceInterceptor does not leave scope manager in bad state "() { - setup: - def interceptor = new ExceptionThrowingInterceptor() - tracer.addTraceInterceptor(interceptor) - - when: - def span = tracer.buildSpan("test", "test").start() - def scope = tracer.activateSpan(span) - scope.close() - span.finish() - - then: "exception is thrown in same thread" - interceptor.lastTrace == [span] - - and: "scopeManager in good state" - scopeManager.active() == null - spanFinished(span) - scopeManager.scopeStack().depth() == 0 - writer == [[span]] - - when: "completing another scope lifecycle" - def span2 = tracer.buildSpan("test", "test").start() - def scope2 = tracer.activateSpan(span2) - - then: - scopeManager.active() == scope2 - - when: - interceptor.shouldThrowException = false - scope2.close() - span2.finish() - writer.waitForTraces(1) - - then: "second lifecycle gets reported" - scopeManager.active() == null - spanFinished(span2) - scopeManager.scopeStack().depth() == 0 - writer == [[span], [span2]] - } - - def "exception thrown in TraceInterceptor does not leave scope manager in bad state when reporting through PendingTraceBuffer"() { - setup: - def interceptor = new ExceptionThrowingInterceptor() - tracer.addTraceInterceptor(interceptor) - - when: - def span = tracer.buildSpan("test", "test").start() - def scope = tracer.activateSpan(span) - def continuation = tracer.captureActiveSpan() - scope.close() - span.finish() - - then: - continuation != null - scopeManager.active() == null - spanFinished(span) - scopeManager.scopeStack().depth() == 0 - writer == [] - - when: "wait for root span to be reported from PendingTraceBuffer" - writer.waitForTraces(1) - - then: - interceptor.lastTrace == [span] - - and: "scopeManager in good state" - scopeManager.active() == null - spanFinished(span) - scopeManager.scopeStack().depth() == 0 - writer == [[span]] - - when: "completing another async scope lifecycle" - def span2 = tracer.buildSpan("test", "test").start() - def scope2 = tracer.activateSpan(span2) - def continuation2 = tracer.captureActiveSpan() - - then: - continuation2 != null - scopeManager.active() == scope2 - - when: - interceptor.shouldThrowException = false - scope2.close() - span2.finish() - - writer.waitForTraces(2) - - then: "second lifecycle gets reported as well" - scopeManager.active() == null - spanFinished(span2) - scopeManager.scopeStack().depth() == 0 - writer == [[span], [span2]] - } - - @Shared - TraceScope.Continuation continuation = null - - @Shared - AtomicInteger iteration = new AtomicInteger(0) - - def "continuation can be activated and closed in multiple threads"() { - setup: - long sendDelayNanos = TimeUnit.MILLISECONDS.toNanos(500 - 100) - - when: - def span = tracer.buildSpan("test", "test").start() - def start = System.nanoTime() - def scope = (ContinuableScope) tracer.activateSpan(span) - continuation = tracer.captureActiveSpan() - scope.close() - span.finish() - - continuation.hold() - - then: - ThreadUtils.runConcurrently(8, 512) { - int iter = iteration.incrementAndGet() - if (iter & 1) { - Thread.sleep(1) - } - TraceScope s = continuation.activate() - assert scopeManager.active() == s - if (iter & 2) { - Thread.sleep(1) - } - s.close() - } - - when: - def written = writer - def duration = System.nanoTime() - start - - then: - // Since we can't rely on that nothing gets written to the tracer for verification, - // we only check for empty if we are faster than the flush interval - if (duration < sendDelayNanos) { - assert written == [] - } - - when: - continuation.cancel() - - then: - writer == [[span]] - } - - def "scope listener should be notified about the currently active scope"() { - setup: - def span = tracer.buildSpan("test", "test").start() - - when: - AgentScope scope = scopeManager.activateSpan(span) - - then: - assertEvents([ACTIVATE]) - - when: - def listener = new EventCountingListener() - - then: - listener.events == [] - - when: - scopeManager.addScopeListener(listener) - - then: - listener.events == [ACTIVATE] - - when: - scope.close() - - then: - assertEvents([ACTIVATE, CLOSE]) - listener.events == [ACTIVATE, CLOSE] - } - - def "extended scope listener should be notified about the currently active scope"() { - setup: - def span = tracer.buildSpan("test", "test").start() - - when: - AgentScope scope = scopeManager.activateSpan(span) - - then: - assertEvents([ACTIVATE]) - - when: - def listener = new EventCountingExtendedListener() - - then: - listener.events == [] - - when: - scopeManager.addScopeListener(listener) - - then: - listener.events == [ACTIVATE] - - when: - scope.close() - - then: - assertEvents([ACTIVATE, CLOSE]) - listener.events == [ACTIVATE, CLOSE] - } - - def "scope listener should not be notified when there is no active scope"() { - when: - def listener = new EventCountingListener() - - then: - listener.events == [] - - when: - scopeManager.addScopeListener(listener) - - then: - listener.events == [] - } - - def "misbehaving ScopeListener should not affect others"() { - setup: - def exceptionThrowingScopeLister = new ExceptionThrowingScopeListener() - exceptionThrowingScopeLister.throwOnScopeActivated = activationException - exceptionThrowingScopeLister.throwOnScopeClosed = closeException - - def secondEventCountingListener = new EventCountingListener() - scopeManager.addScopeListener(exceptionThrowingScopeLister) - scopeManager.addScopeListener(secondEventCountingListener) - - when: - AgentSpan span = tracer.buildSpan("test", "foo").start() - AgentScope continuableScope = tracer.activateSpan(span) - - then: - assertEvents([ACTIVATE]) - secondEventCountingListener.events == [ACTIVATE] - - when: - AgentSpan childSpan = tracer.buildSpan("test", "foo").start() - AgentScope childDDScope = tracer.activateSpan(childSpan) - - then: - assertEvents([ACTIVATE, ACTIVATE]) - secondEventCountingListener.events == [ACTIVATE, ACTIVATE] - - when: - childDDScope.close() - childSpan.finish() - - then: - assertEvents([ACTIVATE, ACTIVATE, CLOSE, ACTIVATE]) - secondEventCountingListener.events == [ACTIVATE, ACTIVATE, CLOSE, ACTIVATE] - - when: - continuableScope.close() - span.finish() - - then: - assertEvents([ACTIVATE, ACTIVATE, CLOSE, ACTIVATE, CLOSE]) - secondEventCountingListener.events == [ACTIVATE, ACTIVATE, CLOSE, ACTIVATE, CLOSE] - - where: - activationException | closeException - false | false - false | true - true | false - true | true - } - - def "context thread listener notified when scope activated on thread for the first time"() { - setup: - def numThreads = 5 - def numTasks = 20 - ExecutorService executor = Executors.newFixedThreadPool(numThreads) - - when: "usage of an instrumented executor results in scopestack initialisation but not scope creation" - executor.submit({ - assert scopeManager.active() == null - }).get() - then: "the listener is not notified" - 0 * profilingContext.onAttach() - - when: "scopes activate on threads" - AgentSpan span = tracer.buildSpan("test", "foo").start() - def futures = new Future[numTasks] - for (int i = 0; i < numTasks; i++) { - futures[i] = executor.submit({ - AgentScope scope = tracer.activateSpan(span) - def child = tracer.buildSpan("test", "foo" + i).start() - def childScope = tracer.activateSpan(child) - try { - Thread.sleep(100) - } catch (InterruptedException ignored) { - Thread.currentThread().interrupt() - } - childScope.close() - scope.close() - }) - } - for (Future future : futures) { - future.get() - } - - then: "the activation notifies the listener whenever the stack becomes non-empty" - numTasks * profilingContext.onAttach() - - cleanup: - executor.shutdown() - } - - def "activating a span merges it with existing context"() { - when: - def span = tracer.buildSpan("test", "test").start() - def testKey = ContextKey.named("test") - def context = Context.root().with(testKey, "test-value") - def contextScope = scopeManager.attach(context) - - then: - scopeManager.active() == contextScope - scopeManager.current() == context - scopeManager.activeSpan() == null - scopeManager.current().get(testKey) == "test-value" - - when: - def scope = tracer.activateSpan(span) - - then: - scopeManager.active() == scope - scopeManager.current() != context - scopeManager.activeSpan() == span - scopeManager.current().get(testKey) == "test-value" - - when: - scope.close() - - then: - scopeManager.active() == contextScope - scopeManager.current() == context - scopeManager.activeSpan() == null - scopeManager.current().get(testKey) == "test-value" - - when: - contextScope.close() - - then: - scopeManager.active() == null - scopeManager.current() == Context.root() - scopeManager.activeSpan() == null - } - - def "capturing and continuing a span merges it with existing context"() { - when: - def span = tracer.buildSpan("test", "test").start() - def testKey = ContextKey.named("test") - def context = Context.root().with(testKey, "test-value") - def contextScope = scopeManager.attach(context) - - then: - scopeManager.active() == contextScope - scopeManager.current() == context - scopeManager.activeSpan() == null - scopeManager.current().get(testKey) == "test-value" - - when: - def scope = tracer.captureSpan(span).activate() - - then: - scopeManager.active() == scope - scopeManager.current() != context - scopeManager.activeSpan() == span - scopeManager.current().get(testKey) == "test-value" - - when: - scope.close() - - then: - scopeManager.active() == contextScope - scopeManager.current() == context - scopeManager.activeSpan() == null - scopeManager.current().get(testKey) == "test-value" - - when: - contextScope.close() - - then: - scopeManager.active() == null - scopeManager.current() == Context.root() - scopeManager.activeSpan() == null - } - - def "capturing and continuing the active span merges it with existing context"() { - when: - def span = tracer.buildSpan("test", "test").start() - def testKey = ContextKey.named("test") - def context = Context.root().with(testKey, "test-value") - def contextScope = scopeManager.attach(context) - - then: - scopeManager.active() == contextScope - scopeManager.current() == context - scopeManager.activeSpan() == null - scopeManager.current().get(testKey) == "test-value" - - when: - def scope = tracer.activateSpan(span).withCloseable { - tracer.captureActiveSpan().activate() - } - - then: - scopeManager.active() == scope - scopeManager.current() != context - scopeManager.activeSpan() == span - scopeManager.current().get(testKey) == "test-value" - - when: - scope.close() - - then: - scopeManager.active() == contextScope - scopeManager.current() == context - scopeManager.activeSpan() == null - scopeManager.current().get(testKey) == "test-value" - - when: - contextScope.close() - - then: - scopeManager.active() == null - scopeManager.current() == Context.root() - scopeManager.activeSpan() == null - } - - def "rollback stops at most recent checkpoint"() { - when: - def span1 = tracer.buildSpan("test1", "test1").start() - def span2 = tracer.buildSpan("test2", "test2").start() - def span3 = tracer.buildSpan("test3", "test3").start() - then: - scopeManager.activeSpan() == null - - when: - tracer.checkpointActiveForRollback() - tracer.activateSpan(span1) - tracer.checkpointActiveForRollback() - tracer.activateSpan(span2) - tracer.checkpointActiveForRollback() - tracer.activateSpan(span1) - tracer.checkpointActiveForRollback() - tracer.activateSpan(span2) - tracer.checkpointActiveForRollback() - tracer.activateSpan(span2) - tracer.checkpointActiveForRollback() - tracer.activateSpan(span1) - tracer.activateSpan(span2) - tracer.activateSpan(span3) - then: - scopeManager.activeSpan() == span3 - - when: - tracer.rollbackActiveToCheckpoint() - then: - scopeManager.activeSpan() == span2 - - when: - tracer.rollbackActiveToCheckpoint() - then: - scopeManager.activeSpan() == span2 - - when: - tracer.rollbackActiveToCheckpoint() - then: - scopeManager.activeSpan() == span1 - - when: - tracer.rollbackActiveToCheckpoint() - then: - scopeManager.activeSpan() == span2 - - when: - tracer.rollbackActiveToCheckpoint() - then: - scopeManager.activeSpan() == span1 - - when: - tracer.rollbackActiveToCheckpoint() - then: - scopeManager.activeSpan() == null - } - - def "contexts can be swapped out and back"() { - setup: - def testKey = ContextKey.named("test") - def context1 = Context.root().with(testKey, "first-value") - def context2 = context1.with(testKey, "second-value") - - when: - def swappedOut = scopeManager.swap(Context.root()) - - then: - scopeManager.active() == null - scopeManager.current() == Context.root() - - when: - scopeManager.swap(context1) - - then: - scopeManager.active() != null - scopeManager.current() == context1 - - when: - scopeManager.swap(swappedOut) - - then: - scopeManager.active() == null - scopeManager.current() == Context.root() - - when: - def contextScope = scopeManager.attach(context1) - - then: - scopeManager.active() == contextScope - scopeManager.current() == context1 - - when: - swappedOut = scopeManager.swap(context2) - - then: - scopeManager.active() != null - scopeManager.active() != contextScope - scopeManager.current() == context2 - swappedOut.get(testKey) == "first-value" - - when: - def context3 = swappedOut.with(testKey, "third-value") - scopeManager.swap(context3) - - then: - scopeManager.active() != null - scopeManager.active() != contextScope - scopeManager.current() == context3 - - when: - scopeManager.swap(swappedOut) - - then: - scopeManager.active() == contextScope - scopeManager.current() == context1 - - when: - contextScope.close() - - then: - scopeManager.active() == null - scopeManager.current() == Context.root() - } - - boolean spanFinished(AgentSpan span) { - return ((DDSpan) span)?.isFinished() - } - - def assertEvents(List events) { - assert eventCountingListener.events == events - assert eventCountingExtendedListener.events == events - return true - } -} - -class EventCountingListener implements ScopeListener { - public final List events = new ArrayList<>() - - @Override - void afterScopeActivated() { - synchronized (events) { - events.add(ACTIVATE) - } - } - - @Override - void afterScopeClosed() { - synchronized (events) { - events.add(CLOSE) - } - } -} - -class EventCountingExtendedListener implements ExtendedScopeListener { - public final List events = new ArrayList<>() - - @Override - void afterScopeActivated() { - throw new IllegalArgumentException("This should not be called") - } - - @Override - void afterScopeActivated(DDTraceId traceId, long spanId) { - synchronized (events) { - events.add(ACTIVATE) - } - } - - @Override - void afterScopeClosed() { - synchronized (events) { - events.add(CLOSE) - } - } -} - -class ExceptionThrowingScopeListener implements ScopeListener { - boolean throwOnScopeActivated = false - boolean throwOnScopeClosed = false - - @Override - void afterScopeActivated() { - if (throwOnScopeActivated) { - throw new RuntimeException("Exception on activated") - } - } - - @Override - void afterScopeClosed() { - if (throwOnScopeClosed) { - throw new RuntimeException("Exception on closed") - } - } -} - -class ExceptionThrowingInterceptor implements TraceInterceptor { - def shouldThrowException = true - - Collection lastTrace - - @Override - Collection onTraceComplete(Collection trace) { - lastTrace = trace - if (shouldThrowException) { - throw new RuntimeException("Always throws exception") - } else { - return trace - } - } - - @Override - int priority() { - return 55 - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/IntegrationAdderTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/IntegrationAdderTest.groovy deleted file mode 100644 index e8a17de339f..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/IntegrationAdderTest.groovy +++ /dev/null @@ -1,26 +0,0 @@ -package datadog.trace.core.tagprocessor - -import datadog.trace.api.TagMap -import datadog.trace.core.DDSpanContext -import datadog.trace.test.util.DDSpecification - -class IntegrationAdderTest extends DDSpecification { - def "should add or remove _dd.integration when set (#isSet) on the span context"() { - setup: - def calculator = new IntegrationAdder() - def spanContext = Mock(DDSpanContext) - - when: - def unsafeTags = TagMap.fromMap(["_dd.integration": "bad"]) - calculator.processTags(unsafeTags, spanContext, {link -> }) - - then: - 1 * spanContext.getIntegrationName() >> (isSet ? "test" : null) - - and: - assert unsafeTags == (isSet ? ["_dd.integration": "test"] : [:]) - - where: - isSet << [true, false] - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/InternalTagsAdderTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/InternalTagsAdderTest.groovy deleted file mode 100644 index 5429a9e8b24..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/InternalTagsAdderTest.groovy +++ /dev/null @@ -1,61 +0,0 @@ -package datadog.trace.core.tagprocessor - -import static datadog.trace.bootstrap.instrumentation.api.Tags.VERSION - -import datadog.trace.api.TagMap -import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks -import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString -import datadog.trace.core.DDSpanContext -import datadog.trace.test.util.DDSpecification - -class InternalTagsAdderTest extends DDSpecification { - def "should add _dd.base_service when service differs to ddService"() { - setup: - def calculator = new InternalTagsAdder("test", null) - def spanContext = Mock(DDSpanContext) - def links = Mock(AppendableSpanLinks) - - when: - def unsafeTags = TagMap.fromMap([:]) - calculator.processTags(unsafeTags, spanContext, links) - - then: - 1 * spanContext.getServiceName() >> serviceName - - and: - assert unsafeTags == expectedTags - - where: - serviceName | expectedTags - "anotherOne" | ["_dd.base_service": UTF8BytesString.create("test")] - "test" | [:] - "TeSt" | [:] - } - - def "should add version when DD_SERVICE = #serviceName and version = #ddVersion"() { - setup: - def calculator = new InternalTagsAdder("same", ddVersion) - def spanContext = Mock(DDSpanContext) - def links = Mock(AppendableSpanLinks) - - when: - def unsafeTags = TagMap.fromMap(tags) - calculator.processTags(unsafeTags, spanContext, links) - - then: - 1 * spanContext.getServiceName() >> serviceName - - and: - assert unsafeTags?.get(VERSION)?.toString() == expected - - - where: - serviceName | ddVersion | tags | expected - "same" | null | [:] | null - "different" | "1.0" | [:] | null - "different" | "1.0" | ["version": "2.0"] | "2.0" - "same" | null | ["version": "2.0"] | "2.0" - "same" | "1.0" | ["version": "2.0"] | "2.0" - "same" | "1.0" | [:] | "1.0" - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PayloadTagsProcessorTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PayloadTagsProcessorTest.groovy deleted file mode 100644 index 5b871052d99..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PayloadTagsProcessorTest.groovy +++ /dev/null @@ -1,413 +0,0 @@ -package datadog.trace.core.tagprocessor - -import com.squareup.moshi.JsonWriter -import datadog.trace.payloadtags.PayloadTagsData -import datadog.trace.payloadtags.PayloadTagsData.PathAndValue -import datadog.trace.util.json.PathCursor -import datadog.trace.test.util.DDSpecification -import datadog.trace.api.Config -import datadog.trace.api.TagMap -import okio.Buffer -import java.time.Instant - -class PayloadTagsProcessorTest extends DDSpecification { - - PathCursor pc() { - new PathCursor(10) - } - - def "disabled by default"() { - expect: - !PayloadTagsProcessor.create(Config.get()) - } - - def "enabled with defaults when configured req #requestPayloadTagging resp #responsePayloadTagging"() { - setup: - requestPayloadTagging && injectSysConfig("trace.cloud.request.payload.tagging", requestPayloadTagging) - responsePayloadTagging && injectSysConfig("trace.cloud.response.payload.tagging", responsePayloadTagging) - - when: - def ptp = PayloadTagsProcessor.create(Config.get()) - - then: - ptp != null - ptp.maxDepth == 10 - ptp.maxTags == 758 - ptp.redactionRulesByTagPrefix.get(tagPrefix).findMatching(pathMatchingDefaultRules) != null - ptp.redactionRulesByTagPrefix.get(tagPrefix).findMatching(pc().push("non-matching-path")) == null - - where: - requestPayloadTagging | responsePayloadTagging | tagPrefix | pathMatchingDefaultRules - "all" | "all" | "aws.request.body" | pc().push("phoneNumber") - "all" | "\$[33].baz" | "aws.request.body" | pc().push("AWSAccountId") - "\$.bar" | "all" | "aws.response.body" | pc().push("Endpoints").push("foobar").push("Token") - "\$.foo.bar" | "\$..bar.*" | "aws.request.body" | pc().push("phoneNumber") - null | "all" | "aws.response.body" | pc().push("phoneNumbers").push(5) - "all" | null | "aws.request.body" | pc().push("Attributes").push("KmsMasterKeyId") - } - - def "enabled with custom limits"() { - setup: - requestPayloadTagging && injectSysConfig("trace.cloud.request.payload.tagging", requestPayloadTagging) - responsePayloadTagging && injectSysConfig("trace.cloud.response.payload.tagging", responsePayloadTagging) - injectSysConfig("trace.cloud.payload.tagging.max-depth", "$maxDepth") - injectSysConfig("trace.cloud.payload.tagging.max-tags", "$maxTags") - - when: - def ptp = PayloadTagsProcessor.create(Config.get()) - - then: - ptp.maxDepth == maxDepth - ptp.maxTags == maxTags - - where: - requestPayloadTagging | responsePayloadTagging | maxDepth | maxTags - "all" | "all" | 10 | 10 - "\$.bar" | null | 7 | 42 - "all" | "\$.*" | 12 | 50 - null | "all" | 8 | 33 - } - - static PayloadTagsProcessor tagsProcessor(String tagPrefix, List redactionRules, int maxDepth, int maxTags) { - def rules = new PayloadTagsProcessor.RedactionRules.Builder().addRedactionJsonPaths(redactionRules).build() - new PayloadTagsProcessor([(tagPrefix): rules], maxDepth, maxTags) - } - - def "preserve all span tags except for payloadData"() { - setup: - def ptp = tagsProcessor("payload", [], 10, 758) - def spanTags = [ - "foo": "bar", - "tag1": 1, - "payload": new PayloadTagsData([] as PathAndValue[]) - ] - - when: - def unsafeTags = TagMap.fromMap(spanTags) - ptp.processTags(unsafeTags, null, {link -> }) - - then: - unsafeTags == ["foo": "bar", "tag1": 1] - } - - static PathAndValue pv(PathCursor path, Object value) { - new PathAndValue(path.toPath(), value) - } - - static PayloadTagsData payloadData(List pathAndValues) { - new PayloadTagsData(pathAndValues as PathAndValue[]) - } - - static LinkedHashMap spanTags(String tagPrefix, List pathAndValues) { - [(tagPrefix): payloadData(pathAndValues)] - } - - def "expand payload to tags"() { - setup: - def ptp = tagsProcessor("payload", [], 10, 758) - def spanTags = [ - "foo": "bar", - "tag1": 1, - "payload": payloadData([pv(pc().push("tag1"), 0)]) - ] - - when: - def unsafeTags = TagMap.fromMap(spanTags) - ptp.processTags(unsafeTags, null, {link -> }) - - then: - unsafeTags == ["foo": "bar", "tag1": 1, "payload.tag1": 0] - } - - def "expand preserving tag types"() { - setup: - def ptp = tagsProcessor("payload", [], 10, 758) - - def st = spanTags("payload", [ - pv(pc().push("tag1"), 11), - pv(pc().push("tag2").push("Value"), 2342l), - pv(pc().push("tag3").push(0), 3.14d), - pv(pc().push("tag4").push("Value").push(0), "string"), - pv(pc().push("tag5"), null), - pv(pc().push("tag6"), false), - ]) - - when: - def unsafeTags = TagMap.fromMap(st) - ptp.processTags(unsafeTags, null, {link -> }) - - then: - unsafeTags == [ - "payload.tag1": 11, - "payload.tag2.Value": 2342l, - "payload.tag3.0": 3.14d, - "payload.tag4.Value.0": "string", - "payload.tag5": null, - "payload.tag6": false, - ] - } - - def "expand unknown tag values to string"() { - setup: - def ptp = tagsProcessor("payload", [], 10, 758) - def unknownValue = Instant.now() - - def st = spanTags("payload", [pv(pc().push("tag7"), unknownValue),]) - - when: - def unsafeTags = TagMap.fromMap(st) - ptp.processTags(unsafeTags, null, {link ->}) - - then: - unsafeTags == [ - "payload.tag7": unknownValue.toString(), - ] - } - - def "expand stringified JSON tags"() { - setup: - def ptp = tagsProcessor("p", [], 10, 758) - - def st = spanTags("p", [ - pv(pc().push("j1"), "{}"), - pv(pc().push("j2"), "[]"), - pv(pc().push("j3"), "['1', 2, 3.14, null, true]"), - pv(pc().push("j4"), "{'foo': 'bar', 'baz': 42}"), - ]) - - when: - def unsafeTags = TagMap.fromMap(st) - ptp.processTags(unsafeTags, null, {link ->}) - - then: - unsafeTags == [ - "p.j3.0": "1", - "p.j3.1": 2, - "p.j3.2": 3.14d, - "p.j3.3": null, - "p.j3.4": true, - "p.j4.foo": "bar", - "p.j4.baz": 42, - ] - } - - def "expand serialized escaped inner json within inner json"() { - Buffer b0 = new Buffer() - JsonWriter.of(b0) - .beginObject() - .name("a").value(1.15) - .name("password").value("my-secret-password") - .endObject() - .close() - - Buffer b1 = new Buffer() - JsonWriter.of(b1) - .beginObject() - .name("id").value(45) - .name("user").value(b0.readUtf8()) - .endObject() - .close() - - Buffer b2 = new Buffer() - JsonWriter.of(b2) - .beginObject() - .name("a").value(33) - .name("Message").value(b1.readUtf8()) - .name("b").value(true) - .endObject() - .close() - - String json = b2.readUtf8() - - setup: - def ptp = tagsProcessor("dd", [], 10, 758) - - def st = spanTags("dd", [pv(pc(), json),]) - - when: - def unsafeTags = TagMap.fromMap(st) - ptp.processTags(unsafeTags, null, {link ->}) - - then: - unsafeTags == [ - 'dd.a' : 33, - 'dd.Message.id' : 45, - 'dd.Message.user.a' : 1.15d, - 'dd.Message.user.password': 'my-secret-password', - 'dd.b' : true - ] - } - - def "keep failed to parse JSON as-is"() { - setup: - def ptp = tagsProcessor("p", [], 10, 758) - - def st = spanTags("p", [pv(pc().push("key"), invalidJson),]) - - when: - def unsafeTags = TagMap.fromMap(st) - ptp.processTags(unsafeTags, null, {link ->}) - - then: - unsafeTags == [ - "p.key": invalidJson, - ] - - where: - invalidJson << [ - "{'foo: 'bar'", - "[1, 2", - "[1, 2] ", - " [1, 2]", - "{'foo: 'bar'} ", - " {'foo: 'bar'}", - ] - } - - def "expand binary if JSON"() { - setup: - def ptp = tagsProcessor("p", [], 10, 758) - - def st = spanTags("p", [ - pv(pc().push("j0"), new ByteArrayInputStream("{}".bytes)), - pv(pc().push("j1"), new ByteArrayInputStream("{'foo': 'bar'}".bytes)), - pv(pc().push("j2"), new ByteArrayInputStream("[1, true]".bytes)), - ]) - - when: - def unsafeTags = TagMap.fromMap(st) - ptp.processTags(unsafeTags, null, {link ->}) - - then: - unsafeTags == [ - "p.j1.foo": "bar", - "p.j2.0": 1, - "p.j2.1": true, - ] - } - - def "expand binary escaped JSON tags"() { - setup: - def ptp = tagsProcessor("p", [], 10, 758) - - when: - def st = spanTags("p", [pv(pc().push("v"), new ByteArrayInputStream("""{ "inner": $innerJson}""".bytes))]) - - def unsafeTags = TagMap.fromMap(st) - ptp.processTags(unsafeTags, null, {link ->}) - - then: - unsafeTags == [ - "p.v.inner.a": 1.15d, - "p.v.inner.password": "my-secret-password", - ] - - where: - innerJson << [ - "{ \"a\": 1.15, \"password\": \"my-secret-password\" }", - "\"{ 'a': 1.15, 'password': 'my-secret-password' }\"", - '"{ \\"a\\": 1.15, \\"password\\": \\"my-secret-password\\" }"', - "'{ \"a\": 1.15, \"password\": \"my-secret-password\" }'", - '''"{ \\"a\\": 1.15, \\"password\\": \\"my-secret-password\\" }"''' - ] - } - - def "use value if not JSON or couldn't be parsed"() { - setup: - def ptp = tagsProcessor("p", [], 10, 758) - - def st = spanTags("p", [pv(pc().push("key"), new ByteArrayInputStream(invalidJson.bytes)),]) - - when: - def unsafeTags = TagMap.fromMap(st) - ptp.processTags(unsafeTags, null, {link ->}) - - then: - unsafeTags == [ - "p.key": "", - ] - - where: - invalidJson << [" [1]", " {'foo': 'bar'}", "invalid:"] - } - - def "apply redaction rules"() { - setup: - def ptp = tagsProcessor("p", ["\$.j3[0]", "\$.j4.baz"], 10, 758) - - def st = spanTags("p", [ - pv(pc().push("j1"), "{}"), - pv(pc().push("j2"), "[]"), - pv(pc().push("j3"), "['1', 2, 3.14, null, true]"), - pv(pc().push("j4"), "{'foo': 'bar', 'baz': 42}"), - ]) - - when: - def unsafeTags = TagMap.fromMap(st) - ptp.processTags(unsafeTags, null, {link ->}) - - then: - unsafeTags == [ - "p.j3.0": "redacted", - "p.j3.1": 2, - "p.j3.2": 3.14d, - "p.j3.3": null, - "p.j3.4": true, - "p.j4.foo": "bar", - "p.j4.baz": "redacted", - ] - } - - def "respect max tags limit"() { - setup: - def ptp = tagsProcessor("p", ["\$.j3[0]", "\$.j4.baz"], 10, 4) - - def st = spanTags("p", [ - pv(pc().push("j1"), "{}"), - pv(pc().push("j2"), "[]"), - pv(pc().push("j3"), "['1', 2, 3.14, null, true]"), - pv(pc().push("j4"), "{'foo': 'bar', 'baz': 42}"), - ]) - - when: - def unsafeTags = TagMap.fromMap(st) - ptp.processTags(unsafeTags, null, {link ->}) - - then: - unsafeTags == [ - "p.j3.0": "redacted", - "p.j3.1": 2, - "p.j3.2": 3.14d, - "p.j3.3": null, - "_dd.payload_tags_incomplete": true - ] - } - - def "respect max depth limit"() { - setup: - def ptp = tagsProcessor("p", ["\$.j3[0]", "\$.j4.baz"], 3, 800) - - def st = spanTags("p", [ - pv(pc().push("j3"), "['1', 2, 3.14, null, true, [ 1, [ 2, 3 ] ]]"), - pv(pc().push("j4"), "{'foo': 'bar', 'baz': 42, 'nested': { 'a': 1, 'b': { 'c': 2 } } }"), - ]) - - when: - def unsafeTags = TagMap.fromMap(st) - ptp.processTags(unsafeTags, null, {link -> }) - - then: - unsafeTags == [ - "p.j3.0": "redacted", - "p.j3.1": 2, - "p.j3.2": 3.14d, - "p.j3.3": null, - "p.j3.4": true, - "p.j3.5.0": 1, - "p.j4.foo": "bar", - "p.j4.baz": "redacted", - "p.j4.nested.a": 1, - ] - } -} - diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PeerServiceCalculatorTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PeerServiceCalculatorTest.groovy deleted file mode 100644 index 2ddf8770d1e..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PeerServiceCalculatorTest.groovy +++ /dev/null @@ -1,136 +0,0 @@ -package datadog.trace.core.tagprocessor - -import datadog.trace.api.Config -import datadog.trace.api.DDTags -import datadog.trace.api.TagMap -import datadog.trace.api.config.TracerConfig -import datadog.trace.api.naming.v0.NamingSchemaV0 -import datadog.trace.api.naming.v1.NamingSchemaV1 -import datadog.trace.bootstrap.instrumentation.api.Tags -import datadog.trace.test.util.DDSpecification - -class PeerServiceCalculatorTest extends DDSpecification { - def "schema v0 : peer service is not calculated by default"() { - setup: - def calculator = new PeerServiceCalculator(new NamingSchemaV0().peerService(), Collections.emptyMap()) - when: - def unsafeTags = TagMap.fromMap(tags) - calculator.processTags(unsafeTags, null, {link ->}) - - then: - // tags are not modified - assert unsafeTags == tags - - where: - tags | _ - [:] | _ - ["peer.hostname": "test"] | _ - ["peer.hostname": "test", "db.instance": "instance"] | _ - ["db.instance": "instance", "peer.hostname": "test"] | _ - ["peer.hostname": "test", "rpc.service": "svc"] | _ - ["rpc.service": "svc", "peer.hostname": "test"] | _ - } - - def "schema v1: test peer service default logic and precursors"() { - setup: - def calculator = new PeerServiceCalculator(new NamingSchemaV1().peerService(), Collections.emptyMap()) - - when: - tags.put(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT) - - def unsafeTags = TagMap.fromMap(tags) - calculator.processTags(unsafeTags, null, {link ->}) - - then: - unsafeTags.get(DDTags.PEER_SERVICE_SOURCE) == provenance - unsafeTags.get(Tags.PEER_SERVICE) == peerService - - where: - tags | provenance | peerService - [:] | null | null - ["peer.hostname": "test"] | Tags.PEER_HOSTNAME | "test" - ["peer.hostname": "test"] | Tags.PEER_HOSTNAME | "test" - ["peer.hostname": "test", "db.instance": "instance"] | Tags.DB_INSTANCE | "instance" - ["db.instance": "instance", "peer.hostname": "test"] | Tags.DB_INSTANCE | "instance" - ["peer.hostname": "test", "rpc.service": "svc", "component": "grpc-client"] | Tags.RPC_SERVICE | "svc" - ["rpc.service": "svc", "peer.hostname": "test", "component": "grpc-client"] | Tags.RPC_SERVICE | "svc" - ["peer.hostname": "test", "peer.service": "userService"] | null | "userService" - } - - def "schema v0: should calculate defaults if enabled"() { - setup: - injectSysConfig(TracerConfig.TRACE_PEER_SERVICE_DEFAULTS_ENABLED, "true") - def calculator = new PeerServiceCalculator(new NamingSchemaV0().peerService(), Collections.emptyMap()) - - when: - def unsafeTags = TagMap.fromMap(["span.kind": "client", "peer.hostname": "test"]) - calculator.processTags(unsafeTags, null, {link ->}) - - then: - assert unsafeTags.get(Tags.PEER_SERVICE) == "test" - } - - - def "calculate only for span kind client or producer"() { - setup: - def calculator = new PeerServiceCalculator(new NamingSchemaV1().peerService(), Collections.emptyMap()) - - when: - def tags = ["span.kind": kind, "peer.hostname": "test"] - def unsafeTags = TagMap.fromMap(tags) - - calculator.processTags(unsafeTags, null, {link -> }) - - then: - assert unsafeTags.containsKey(Tags.PEER_SERVICE) == calculate - - where: - kind | calculate - "client" | true - "producer" | true - "server" | false - } - - def "should apply peer service mappings if configured"() { - setup: - injectSysConfig(TracerConfig.TRACE_PEER_SERVICE_MAPPING, "service1:best_service,userService:my_service") - injectSysConfig(TracerConfig.TRACE_PEER_SERVICE_DEFAULTS_ENABLED, "true") - - def calculator = new PeerServiceCalculator(new NamingSchemaV0().peerService(), Config.get().getPeerServiceMapping()) - - when: - def unsafeTags = TagMap.fromMap(tags) - calculator.processTags(unsafeTags, null, {link ->}) - - then: - assert unsafeTags.get(Tags.PEER_SERVICE) == expected - assert unsafeTags.get(DDTags.PEER_SERVICE_REMAPPED_FROM) == original - - where: - tags | expected | original - ["peer.service": "userService"] | "my_service" | "userService" - ["peer.hostname": "test", "span.kind": "client"] | "test" | null - ["peer.hostname": "service1", "span.kind": "producer"] | "best_service" | "service1" - } - - def "should override peer service values if configured"() { - setup: - injectSysConfig(TracerConfig.TRACE_PEER_SERVICE_COMPONENT_OVERRIDES, "java-couchbase:couchbase") - injectSysConfig(TracerConfig.TRACE_PEER_SERVICE_DEFAULTS_ENABLED, "true") - - def calculator = new PeerServiceCalculator(new NamingSchemaV0().peerService(), Config.get().getPeerServiceComponentOverrides()) - - when: - def unsafeTags = TagMap.fromMap(tags) - calculator.processTags(unsafeTags, null, {link -> }) - - then: - assert unsafeTags.get(Tags.PEER_SERVICE) == expected - assert unsafeTags.get(DDTags.PEER_SERVICE_SOURCE) == source - - where: - tags | expected | source - ["component": "java-couchbase", "span.kind": "client"] | "couchbase" | "_component_override" - ["peer.hostname": "host1", "span.kind": "client", "component" : "my-http-client"] | "host1" | "peer.hostname" - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PostProcessorChainTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PostProcessorChainTest.groovy deleted file mode 100644 index 3d62f0b29da..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PostProcessorChainTest.groovy +++ /dev/null @@ -1,68 +0,0 @@ -package datadog.trace.core.tagprocessor - -import datadog.trace.api.TagMap -import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks -import datadog.trace.core.DDSpanContext -import datadog.trace.test.util.DDSpecification - -class PostProcessorChainTest extends DDSpecification { - def "chain works"() { - setup: - def processor1 = new TagsPostProcessor() { - @Override - void processTags(TagMap unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { - unsafeTags.put("key1", "processor1") - unsafeTags.put("key2", "processor1") - } - } - def processor2 = new TagsPostProcessor() { - @Override - void processTags(TagMap unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { - unsafeTags.put("key1", "processor2") - } - } - - def chain = new PostProcessorChain(processor1, processor2) - - def links = [] - def tags = TagMap.fromMap(["key1": "overwrite", "key3": "unchanged"]) - - when: - chain.processTags(tags, null, {link -> links.add(link)}) - - then: - assert tags == ["key1": "processor2", "key2": "processor1", "key3": "unchanged"] - assert links == [] - } - - def "processor can hide tags to next one()"() { - setup: - def processor1 = new TagsPostProcessor() { - @Override - void processTags(TagMap unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { - unsafeTags.clear() - unsafeTags.put("my", "tag") - } - } - def processor2 = new TagsPostProcessor() { - @Override - void processTags(TagMap unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { - if (unsafeTags.containsKey("test")) { - unsafeTags.put("found", "true") - } - } - } - - def chain = new PostProcessorChain(processor1, processor2) - - def links = [] - def tags = TagMap.fromMap(["test": "test"]) - - when: - chain.processTags(tags, null, {link -> links.add(link)}) - - then: - assert tags == ["my": "tag"] - assert links == [] - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/QueryObfuscatorTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/QueryObfuscatorTest.groovy deleted file mode 100644 index 1e4332477ed..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/QueryObfuscatorTest.groovy +++ /dev/null @@ -1,54 +0,0 @@ -package datadog.trace.core.tagprocessor - -import datadog.trace.api.DDTags -import datadog.trace.api.TagMap -import datadog.trace.bootstrap.instrumentation.api.Tags -import datadog.trace.test.util.DDSpecification - -class QueryObfuscatorTest extends DDSpecification { - def "tags processing"() { - setup: - def obfuscator = new QueryObfuscator() - def tags = [ - (Tags.HTTP_URL): 'http://site.com/index', - (DDTags.HTTP_QUERY): query - ] - - when: - def unsafeTags = TagMap.fromMap(tags) - obfuscator.processTags(unsafeTags, null, {link ->}) - - then: - assert unsafeTags.get(DDTags.HTTP_QUERY) == expectedQuery - assert unsafeTags.get(Tags.HTTP_URL) == 'http://site.com/index?' + expectedQuery - - where: - query | expectedQuery - 'key1=val1&token=a0b21ce2-006f-4cc6-95d5-d7b550698482&key2=val2' | 'key1=val1&&key2=val2' - 'app_key=1111&application_key=2222' | '&' - 'email=foo@bar.com' | 'email=foo@bar.com' - } - - def "tags processing with custom regexp for email"() { - setup: - def obfuscator = new QueryObfuscator("(?i)(?:(?:\"|%22)?)(?:(?:old[-_]?|new[-_]?)?p(?:ass)?w(?:or)?d(?:1|2)?|pass(?:[-_]?phrase)?|email|secret|(?:api[-_]?|private[-_]?|public[-_]?|access[-_]?|secret[-_]?|app(?:lication)?[-_]?)key(?:[-_]?id)?|token|consumer[-_]?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)(?:(?:\\s|%20)*(?:=|%3D)[^&]+|(?:\"|%22)(?:\\s|%20)*(?::|%3A)(?:\\s|%20)*(?:\"|%22)(?:%2[^2]|%[^2]|[^\"%])+(?:\"|%22))|(?:bearer(?:\\s|%20)+[a-z0-9._\\-]+|token(?::|%3A)[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L](?:[\\w=-]|%3D)+\\.ey[I-L](?:[\\w=-]|%3D)+(?:\\.(?:[\\w.+/=-]|%3D|%2F|%2B)+)?|-{5}BEGIN(?:[a-z\\s]|%20)+PRIVATE(?:\\s|%20)KEY-{5}[^\\-]+-{5}END(?:[a-z\\s]|%20)+PRIVATE(?:\\s|%20)KEY(?:-{5})?(?:\\n|%0A)?|(?:ssh-(?:rsa|dss)|ecdsa-[a-z0-9]+-[a-z0-9]+)(?:\\s|%20|%09)+(?:[a-z0-9/.+]|%2F|%5C|%2B){100,}(?:=|%3D)*(?:(?:\\s|%20|%09)+[a-z0-9._-]+)?)") - def tags = [ - (Tags.HTTP_URL): 'http://site.com/index', - (DDTags.HTTP_QUERY): query - ] - - when: - def unsafeTags = TagMap.fromMap(tags) - obfuscator.processTags(unsafeTags, null, {link ->}) - - then: - assert unsafeTags.get(DDTags.HTTP_QUERY) == expectedQuery - assert unsafeTags.get(Tags.HTTP_URL) == 'http://site.com/index?' + expectedQuery - - where: - query | expectedQuery - 'key1=val1&token=a0b21ce2-006f-4cc6-95d5-d7b550698482&key2=val2' | 'key1=val1&&key2=val2' - 'app_key=1111&application_key=2222' | '&' - 'email=foo@bar.com' | '' - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/SpanPointersProcessorTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/SpanPointersProcessorTest.groovy deleted file mode 100644 index a3407e30a02..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/SpanPointersProcessorTest.groovy +++ /dev/null @@ -1,101 +0,0 @@ -package datadog.trace.core.tagprocessor - -import datadog.trace.api.DDSpanId -import datadog.trace.api.DDTraceId -import datadog.trace.api.TagMap -import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags -import datadog.trace.bootstrap.instrumentation.api.SpanLink -import datadog.trace.core.DDSpanContext -import datadog.trace.test.util.DDSpecification - -class SpanPointersProcessorTest extends DDSpecification{ - def "SpanPointersProcessor adds correct link with basic values"() { - given: - def processor = new SpanPointersProcessor() - def unsafeTags = TagMap.fromMap([ - (InstrumentationTags.AWS_BUCKET_NAME): "some-bucket", - (InstrumentationTags.AWS_OBJECT_KEY) : "some-key.data", - "s3.eTag" : "ab12ef34" - ]) - def spanContext = Mock(DDSpanContext) - def spanLinks = [] - def expectedHash = "e721375466d4116ab551213fdea08413" - - when: - // Process the tags; the processor should remove 's3.eTag' and add one link - processor.processTags(unsafeTags, spanContext, {link -> spanLinks.add(link)}) - - then: - // 1. s3.eTag was removed - !unsafeTags.containsKey("s3.eTag") - // 2. Exactly one link was added - spanLinks.size() == 1 - // 3. Check link - def link = spanLinks[0] - link instanceof SpanLink - link.traceId() == DDTraceId.ZERO - link.spanId() == DDSpanId.ZERO - link.attributes.asMap().get("ptr.kind") == SpanPointersProcessor.S3_PTR_KIND - link.attributes.asMap().get("ptr.dir") == SpanPointersProcessor.DOWN_DIRECTION - link.attributes.asMap().get("ptr.hash") == expectedHash - link.attributes.asMap().get("link.kind") == SpanPointersProcessor.LINK_KIND - } - - def "SpanPointersProcessor adds correct link with non-ascii key"() { - given: - def processor = new SpanPointersProcessor() - def unsafeTags = TagMap.fromMap([ - (InstrumentationTags.AWS_BUCKET_NAME): "some-bucket", - (InstrumentationTags.AWS_OBJECT_KEY) : "some-key.你好", - "s3.eTag" : "ab12ef34" - ]) - def spanContext = Mock(DDSpanContext) - def spanLinks = [] - - // From the original test, expected hash for these components - def expectedHash = "d1333a04b9928ab462b5c6cadfa401f4" - - when: - processor.processTags(unsafeTags, spanContext, {link -> spanLinks.add(link)}) - - then: - !unsafeTags.containsKey("s3.eTag") - spanLinks.size() == 1 - def link = spanLinks[0] - link.traceId() == DDTraceId.ZERO - link.spanId() == DDSpanId.ZERO - link.attributes.asMap().get("ptr.kind") == SpanPointersProcessor.S3_PTR_KIND - link.attributes.asMap().get("ptr.dir") == SpanPointersProcessor.DOWN_DIRECTION - link.attributes.asMap().get("ptr.hash") == expectedHash - link.attributes.asMap().get("link.kind") == SpanPointersProcessor.LINK_KIND - } - - def "SpanPointersProcessor adds correct link with multipart-upload ETag"() { - given: - def processor = new SpanPointersProcessor() - def unsafeTags = TagMap.fromMap([ - (InstrumentationTags.AWS_BUCKET_NAME): "some-bucket", - (InstrumentationTags.AWS_OBJECT_KEY) : "some-key.data", - "s3.eTag" : "ab12ef34-5" - ]) - def spanContext = Mock(DDSpanContext) - def spanLinks = [] - - // From the original test, expected hash for these components - def expectedHash = "2b90dffc37ebc7bc610152c3dc72af9f" - - when: - processor.processTags(unsafeTags, spanContext, {link -> spanLinks.add(link)}) - - then: - !unsafeTags.containsKey("s3.eTag") - spanLinks.size() == 1 - def link = spanLinks[0] - link.traceId() == DDTraceId.ZERO - link.spanId() == DDSpanId.ZERO - link.attributes.asMap().get("ptr.kind") == SpanPointersProcessor.S3_PTR_KIND - link.attributes.asMap().get("ptr.dir") == SpanPointersProcessor.DOWN_DIRECTION - link.attributes.asMap().get("ptr.hash") == expectedHash - link.attributes.asMap().get("link.kind") == SpanPointersProcessor.LINK_KIND - } -} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/ScopeManagerTestBridge.java b/dd-trace-core/src/test/java/datadog/trace/core/ScopeManagerTestBridge.java new file mode 100644 index 00000000000..4c0682b4023 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/ScopeManagerTestBridge.java @@ -0,0 +1,10 @@ +package datadog.trace.core; + +import datadog.trace.core.scopemanager.ContinuableScopeManager; + +/** Bridge to expose package-private {@code CoreTracer.scopeManager} to tests in other packages. */ +public class ScopeManagerTestBridge { + public static ContinuableScopeManager getScopeManager(CoreTracer tracer) { + return tracer.scopeManager; + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/scopemanager/IterationSpansForkedTest.java b/dd-trace-core/src/test/java/datadog/trace/core/scopemanager/IterationSpansForkedTest.java new file mode 100644 index 00000000000..3ca5cd85e1f --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/scopemanager/IterationSpansForkedTest.java @@ -0,0 +1,199 @@ +package datadog.trace.core.scopemanager; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +import datadog.metrics.api.statsd.StatsDClient; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.common.writer.ListWriter; +import datadog.trace.core.CoreTracer; +import datadog.trace.core.DDCoreJavaSpecification; +import datadog.trace.core.DDSpan; +import datadog.trace.junit.utils.config.WithConfig; +import java.util.Comparator; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@WithConfig(key = "trace.scope.iteration.keep.alive", value = "1") +class IterationSpansForkedTest extends DDCoreJavaSpecification { + + ListWriter writer; + CoreTracer tracer; + StatsDClient statsDClient; + + @BeforeEach + void setup() { + writer = new ListWriter(); + statsDClient = mock(StatsDClient.class); + tracer = tracerBuilder().writer(writer).statsDClient(statsDClient).build(); + } + + @AfterEach + void cleanup() throws Exception { + tracer.close(); + } + + @Test + void rootIterationScopeLifecycle() throws Exception { + tracer.closePrevious(true); + AgentSpan span1 = tracer.buildSpan("datadog", "next1").start(); + AgentScope scope1 = tracer.activateNext(span1); + + assertTrue(writer.isEmpty()); + assertSame(span1, scope1.span()); + assertSame(span1, tracer.activeSpan()); + assertFalse(spanFinished(span1)); + + tracer.closePrevious(true); + AgentSpan span2 = tracer.buildSpan("datadog", "next2").start(); + AgentScope scope2 = tracer.activateNext(span2); + + assertTrue(spanFinished(span1)); + assertEquals(1, writer.size()); + assertSame(span1, writer.get(0).get(0)); + assertSame(span2, scope2.span()); + assertSame(span2, tracer.activeSpan()); + assertFalse(spanFinished(span2)); + + tracer.closePrevious(true); + AgentSpan span3 = tracer.buildSpan("datadog", "next3").start(); + AgentScope scope3 = tracer.activateNext(span3); + writer.waitForTraces(2); + + assertTrue(spanFinished(span2)); + assertEquals(2, writer.size()); + assertSame(span3, scope3.span()); + assertSame(span3, tracer.activeSpan()); + assertFalse(spanFinished(span3)); + + // 'next3' should time out & finish after 1s + writer.waitForTraces(3); + + assertTrue(spanFinished(span3)); + assertEquals(3, writer.size()); + assertNull(tracer.activeSpan()); + } + + @Test + void nonRootIterationScopeLifecycle() throws Exception { + AgentSpan span0 = tracer.buildSpan("datadog", "parent").start(); + AgentScope scope0 = tracer.activateSpan(span0); + + tracer.closePrevious(true); + AgentSpan span1 = tracer.buildSpan("datadog", "next1").start(); + AgentScope scope1 = tracer.activateNext(span1); + + assertTrue(writer.isEmpty()); + assertSame(span1, scope1.span()); + assertSame(span1, tracer.activeSpan()); + assertFalse(spanFinished(span1)); + + tracer.closePrevious(true); + AgentSpan span2 = tracer.buildSpan("datadog", "next2").start(); + AgentScope scope2 = tracer.activateNext(span2); + + assertTrue(spanFinished(span1)); + assertTrue(writer.isEmpty()); + assertSame(span2, scope2.span()); + assertSame(span2, tracer.activeSpan()); + assertFalse(spanFinished(span2)); + + tracer.closePrevious(true); + AgentSpan span3 = tracer.buildSpan("datadog", "next3").start(); + AgentScope scope3 = tracer.activateNext(span3); + + assertTrue(spanFinished(span2)); + assertTrue(writer.isEmpty()); + assertSame(span3, scope3.span()); + assertSame(span3, tracer.activeSpan()); + assertFalse(spanFinished(span3)); + + scope0.close(); + span0.finish(); + // closing the parent scope will close & finish 'next3' + writer.waitForTraces(1); + + assertTrue(spanFinished(span3)); + assertTrue(spanFinished(span0)); + sortSpansByStart(); + List trace = writer.get(0); + assertEquals(4, trace.size()); + assertSame(span0, trace.get(0)); + assertSame(span1, trace.get(1)); + assertSame(span2, trace.get(2)); + assertSame(span3, trace.get(3)); + assertNull(tracer.activeSpan()); + } + + @Test + void nestedIterationScopeLifecycle() throws Exception { + tracer.closePrevious(true); + AgentSpan span1 = tracer.buildSpan("datadog", "next").start(); + AgentScope scope1 = tracer.activateNext(span1); + + assertTrue(writer.isEmpty()); + assertSame(span1, scope1.span()); + assertSame(span1, tracer.activeSpan()); + assertFalse(spanFinished(span1)); + + AgentSpan span1A = tracer.buildSpan("datadog", "method").start(); + AgentScope scope1A = tracer.activateSpan(span1A); + + tracer.closePrevious(true); + AgentSpan span1A1 = tracer.buildSpan("datadog", "next").start(); + AgentScope scope1A1 = tracer.activateNext(span1A1); + + assertFalse(spanFinished(span1)); + assertTrue(writer.isEmpty()); + assertSame(span1A1, scope1A1.span()); + assertSame(span1A1, tracer.activeSpan()); + assertFalse(spanFinished(span1A1)); + + tracer.closePrevious(true); + AgentSpan span1A2 = tracer.buildSpan("datadog", "next").start(); + AgentScope scope1A2 = tracer.activateNext(span1A2); + + assertTrue(spanFinished(span1A1)); + assertTrue(writer.isEmpty()); + assertSame(span1A2, scope1A2.span()); + assertSame(span1A2, tracer.activeSpan()); + assertFalse(spanFinished(span1A2)); + + // closing the intervening scope will close & finish 'next1A2' + scope1A.close(); + span1A.finish(); + + assertTrue(spanFinished(span1A2)); + assertTrue(spanFinished(span1A)); + assertFalse(spanFinished(span1)); + assertTrue(writer.isEmpty()); + + // 'next1' should time out & finish after 1s to complete the trace + writer.waitForTraces(1); + + assertTrue(spanFinished(span1)); + sortSpansByStart(); + List trace = writer.get(0); + assertEquals(4, trace.size()); + assertSame(span1, trace.get(0)); + assertSame(span1A, trace.get(1)); + assertSame(span1A1, trace.get(2)); + assertSame(span1A2, trace.get(3)); + assertNull(tracer.activeSpan()); + } + + private boolean spanFinished(AgentSpan span) { + return span instanceof DDSpan && ((DDSpan) span).isFinished(); + } + + private void sortSpansByStart() { + writer.firstTrace().sort(Comparator.comparingLong(DDSpan::getStartTimeNano)); + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/scopemanager/ScopeAndContinuationLayoutTest.java b/dd-trace-core/src/test/java/datadog/trace/core/scopemanager/ScopeAndContinuationLayoutTest.java new file mode 100644 index 00000000000..57c01bdc056 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/scopemanager/ScopeAndContinuationLayoutTest.java @@ -0,0 +1,34 @@ +package datadog.trace.core.scopemanager; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeFalse; + +import datadog.environment.JavaVirtualMachine; +import datadog.trace.test.util.DDJavaSpecification; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.openjdk.jol.info.ClassLayout; + +class ScopeAndContinuationLayoutTest extends DDJavaSpecification { + + @BeforeAll + static void assumeNotIbmJvm() { + assumeFalse(JavaVirtualMachine.isIbm()); + } + + @Test + void continuableScopeLayout() { + assertTrue(layoutAcceptable(ContinuableScope.class, 32)); + } + + @Test + void singleContinuationLayout() { + assertTrue(layoutAcceptable(ScopeContinuation.class, 32)); + } + + private boolean layoutAcceptable(Class klass, int acceptableSize) { + ClassLayout layout = ClassLayout.parseClass(klass); + System.err.println(layout.toPrintable()); + return layout.instanceSize() <= acceptableSize; + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/scopemanager/ScopeManagerDepthTest.java b/dd-trace-core/src/test/java/datadog/trace/core/scopemanager/ScopeManagerDepthTest.java new file mode 100644 index 00000000000..25608bec9fd --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/scopemanager/ScopeManagerDepthTest.java @@ -0,0 +1,130 @@ +package datadog.trace.core.scopemanager; + +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.noopScope; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.noopSpan; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +import datadog.trace.api.config.TracerConfig; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.common.writer.ListWriter; +import datadog.trace.core.CoreTracer; +import datadog.trace.core.DDCoreJavaSpecification; +import datadog.trace.core.ScopeManagerTestBridge; +import datadog.trace.junit.utils.config.WithConfig; +import org.junit.jupiter.api.Test; + +class ScopeManagerDepthTest extends DDCoreJavaSpecification { + + @Test + void scopeManagerReturnsNoopScopeIfDepthExceeded() { + // Using a local constant here to avoid classloading issues with ConfigDefaults + int depth = 100; + + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + ContinuableScopeManager scopeManager = ScopeManagerTestBridge.getScopeManager(tracer); + + // fill up the scope stack + AgentScope scope = null; + for (int i = 0; i < depth; i++) { + AgentSpan testSpan = tracer.buildSpan("test", "test").start(); + scope = tracer.activateSpan(testSpan); + assertInstanceOf(ContinuableScope.class, scope); + } + + // last scope is still valid + assertEquals(depth, scopeManager.scopeStack().depth()); + + // activate span over limit + AgentSpan span = tracer.buildSpan("test", "test").start(); + scope = tracer.activateSpan(span); + + // a noop instance is returned + assertSame(noopScope(), scope); + + // activate a noop scope over the limit + scope = scopeManager.activateManualSpan(noopSpan()); + + // still have a noop instance + assertSame(noopScope(), scope); + + // scope stack not effected + assertEquals(depth, scopeManager.scopeStack().depth()); + + scopeManager.scopeStack().clear(); + tracer.close(); + } + + @Test + @WithConfig(key = TracerConfig.SCOPE_DEPTH_LIMIT, value = "0") + void scopeManagerIgnoresDepthLimitWhenZero() { + // Using a local constant here to avoid classloading issues with ConfigDefaults + int defaultLimit = 100; + + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + ContinuableScopeManager scopeManager = ScopeManagerTestBridge.getScopeManager(tracer); + + // fill up the scope stack + AgentScope scope = null; + for (int i = 0; i < defaultLimit; i++) { + AgentSpan testSpan = tracer.buildSpan("test", "test").start(); + scope = tracer.activateSpan(testSpan); + assertInstanceOf(ContinuableScope.class, scope); + } + + // last scope is still valid + assertEquals(defaultLimit, scopeManager.scopeStack().depth()); + + // activate a scope + AgentSpan span = tracer.buildSpan("test", "test").start(); + scope = tracer.activateSpan(span); + + // a real scope is returned + assertNotSame(noopScope(), scope); + assertEquals(defaultLimit + 1, scopeManager.scopeStack().depth()); + + // activate a noop span + scope = scopeManager.activateManualSpan(noopSpan()); + + // a real instance is still returned + assertNotSame(noopScope(), scope); + + // scope stack not effected + assertEquals(defaultLimit + 2, scopeManager.scopeStack().depth()); + + scopeManager.scopeStack().clear(); + tracer.close(); + } + + @Test + void depthIsCorrectlyUpdatedWithOutOfOrderClosing() { + // The decision here is that depth is the top-most open scope + // Closed scopes that are not on top still count for depth + + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + ContinuableScopeManager scopeManager = ScopeManagerTestBridge.getScopeManager(tracer); + + AgentSpan firstSpan = tracer.buildSpan("test", "foo").start(); + AgentScope firstScope = tracer.activateSpan(firstSpan); + + AgentSpan secondSpan = tracer.buildSpan("test", "foo").start(); + AgentScope secondScope = tracer.activateSpan(secondSpan); + + assertEquals(2, scopeManager.scopeStack().depth()); + + firstSpan.finish(); + firstScope.close(); + + assertEquals(2, scopeManager.scopeStack().depth()); + + secondSpan.finish(); + secondScope.close(); + + assertEquals(0, scopeManager.scopeStack().depth()); + + tracer.close(); + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/scopemanager/ScopeManagerTest.java b/dd-trace-core/src/test/java/datadog/trace/core/scopemanager/ScopeManagerTest.java new file mode 100644 index 00000000000..11ca5fb7c01 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/scopemanager/ScopeManagerTest.java @@ -0,0 +1,1167 @@ +package datadog.trace.core.scopemanager; + +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.noopContinuation; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.noopSpan; +import static datadog.trace.core.scopemanager.ScopeManagerTest.EVENT.ACTIVATE; +import static datadog.trace.core.scopemanager.ScopeManagerTest.EVENT.CLOSE; +import static datadog.trace.test.util.GCUtils.awaitGC; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +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.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import datadog.context.Context; +import datadog.context.ContextKey; +import datadog.context.ContextScope; +import datadog.trace.api.DDTraceId; +import datadog.trace.api.Stateful; +import datadog.trace.api.interceptor.MutableSpan; +import datadog.trace.api.interceptor.TraceInterceptor; +import datadog.trace.api.scopemanager.ExtendedScopeListener; +import datadog.trace.api.scopemanager.ScopeListener; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration; +import datadog.trace.common.writer.ListWriter; +import datadog.trace.context.TraceScope; +import datadog.trace.core.CoreTracer; +import datadog.trace.core.DDCoreJavaSpecification; +import datadog.trace.core.DDSpan; +import datadog.trace.core.ScopeManagerTestBridge; +import datadog.trace.test.util.ThreadUtils; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.tabletest.junit.TableTest; + +class ScopeManagerTest extends DDCoreJavaSpecification { + + enum EVENT { + ACTIVATE, + CLOSE + } + + @Override + protected boolean useStrictTraceWrites() { + // This tests the behavior of the relaxed pending trace implementation + return false; + } + + ListWriter writer; + CoreTracer tracer; + ContinuableScopeManager scopeManager; + EventCountingListener eventCountingListener; + EventCountingExtendedListener eventCountingExtendedListener; + ProfilingContextIntegration profilingContext; + Stateful state; + + @BeforeEach + void setup() { + state = mock(Stateful.class); + profilingContext = mock(ProfilingContextIntegration.class); + when(profilingContext.newScopeState(any())).thenReturn(state); + when(profilingContext.name()).thenReturn("mock"); + writer = new ListWriter(); + tracer = tracerBuilder().writer(writer).profilingContextIntegration(profilingContext).build(); + scopeManager = ScopeManagerTestBridge.getScopeManager(tracer); + eventCountingListener = new EventCountingListener(); + scopeManager.addScopeListener(eventCountingListener); + eventCountingExtendedListener = new EventCountingExtendedListener(); + scopeManager.addScopeListener(eventCountingExtendedListener); + // Clear interactions recorded during tracer initialization so each test starts clean + clearInvocations(profilingContext); + } + + @AfterEach + void cleanup() { + tracer.close(); + } + + @Test + void nonDdspanActivationResultsInAContinuableScope() { + AgentScope scope = scopeManager.activateSpan(noopSpan()); + + assertSame(scope, scopeManager.active()); + assertInstanceOf(ContinuableScope.class, scope); + + scope.close(); + + assertNull(scopeManager.active()); + } + + @Test + void noScopeIsActiveBeforeActivation() throws Exception { + tracer.buildSpan("test", "test").start(); + + assertNull(scopeManager.active()); + assertTrue(writer.isEmpty()); + } + + @Test + void simpleScopeAndSpanLifecycle() throws Exception { + AgentSpan span = tracer.buildSpan("test", "test").start(); + AgentScope scope = tracer.activateSpan(span); + + assertSame(span, scope.span()); + assertFalse(spanFinished(scope.span())); + assertSame(scope, scopeManager.active()); + assertInstanceOf(ContinuableScope.class, scope); + assertTrue(writer.isEmpty()); + + scope.close(); + + assertFalse(spanFinished(scope.span())); + assertTrue(writer.isEmpty()); + assertNull(scopeManager.active()); + + span.finish(); + writer.waitForTraces(1); + + assertTrue(spanFinished(scope.span())); + assertEquals(1, writer.size()); + assertSame(scope.span(), writer.get(0).get(0)); + assertNull(scopeManager.active()); + } + + @Test + void setsParentAsCurrentUponClose() { + AgentSpan parentSpan = tracer.buildSpan("test", "parent").start(); + AgentScope parentScope = tracer.activateSpan(parentSpan); + AgentSpan childSpan = tracer.buildSpan("test", "child").start(); + AgentScope childScope = tracer.activateSpan(childSpan); + + assertSame(childScope, scopeManager.active()); + assertEquals( + parentScope.span().context().getSpanId(), + ((DDSpan) childScope.span()).context().getParentId()); + assertSame( + parentScope.span().context().getTraceCollector(), + childScope.span().context().getTraceCollector()); + + childScope.close(); + + assertSame(parentScope, scopeManager.active()); + assertFalse(spanFinished(childScope.span())); + assertFalse(spanFinished(parentScope.span())); + assertTrue(writer.isEmpty()); + } + + @Test + void setsParentAsCurrentUponCloseWithNoopChild() { + AgentSpan parentSpan = tracer.buildSpan("test", "parent").start(); + AgentScope parentScope = tracer.activateSpan(parentSpan); + AgentSpan childSpan = noopSpan(); + AgentScope childScope = tracer.activateSpan(childSpan); + + assertSame(childScope, scopeManager.active()); + + childScope.close(); + + assertSame(parentScope, scopeManager.active()); + assertFalse(spanFinished(parentScope.span())); + assertTrue(writer.isEmpty()); + } + + @Test + void ddScopeCreatesNoOpContinuationsWhenPropagationIsNotSet() { + AgentSpan span = tracer.buildSpan("test", "test").start(); + tracer.activateSpan(span); + tracer.setAsyncPropagationEnabled(false); + AgentScope.Continuation continuation = tracer.captureActiveSpan(); + + assertSame(noopContinuation(), continuation); + + tracer.setAsyncPropagationEnabled(true); + continuation = tracer.captureActiveSpan(); + + assertNotSame(noopContinuation(), continuation); + assertNotNull(continuation); + + continuation.cancel(); + } + + @Test + void continuationCancelDoesNotCloseParentScope() { + AgentSpan span = tracer.buildSpan("test", "test").start(); + AgentScope scope = tracer.activateSpan(span); + AgentScope.Continuation continuation = tracer.captureActiveSpan(); + + assertNotNull(continuation); + + continuation.cancel(); + + assertSame(scope, scopeManager.active()); + } + + // @Flaky("awaitGC is flaky") + @Test + void testContinuationDoesNotHaveHardReferenceOnScope() throws InterruptedException { + AgentSpan span = tracer.buildSpan("test", "test").start(); + AtomicReference scopeRef = new AtomicReference<>(tracer.activateSpan(span)); + AgentScope.Continuation continuation = tracer.captureActiveSpan(); + + assertNotNull(continuation); + + scopeRef.get().close(); + + assertNull(scopeManager.active()); + + WeakReference ref = new WeakReference<>(scopeRef.get()); + scopeRef.set(null); + awaitGC(ref); + + assertNotNull(continuation); + assertNull(ref.get()); + assertFalse(spanFinished(span)); + assertTrue(writer.isEmpty()); + } + + @ValueSource(booleans = {true, false}) + @ParameterizedTest + void hardReferenceOnContinuationDoesNotPreventTraceFromReporting(boolean autoClose) + throws Exception { + AgentSpan span = tracer.buildSpan("test", "test").start(); + AgentScope scope = tracer.activateSpan(span); + AgentScope.Continuation continuation = tracer.captureActiveSpan(); + + assertNotNull(continuation); + + scope.close(); + span.finish(); + if (autoClose) { + continuation.cancel(); + } + + assertNull(scopeManager.active()); + assertTrue(spanFinished(span)); + + writer.waitForTraces(1); + + assertEquals(1, writer.size()); + assertSame(span, writer.get(0).get(0)); + } + + @Test + void continuationRestoresTrace() throws Exception { + AgentSpan parentSpan = tracer.buildSpan("test", "parent").start(); + AgentScope parentScope = tracer.activateSpan(parentSpan); + AgentSpan childSpan = tracer.buildSpan("test", "child").start(); + AgentScope childScope = tracer.activateSpan(childSpan); + + AgentScope.Continuation continuation = tracer.captureActiveSpan(); + childScope.close(); + + assertNotNull(continuation); + assertSame(parentScope, scopeManager.active()); + assertFalse(spanFinished(childSpan)); + assertFalse(spanFinished(parentSpan)); + + parentScope.close(); + parentSpan.finish(); + + // parent span is finished, but trace is not reported + assertNull(scopeManager.active()); + assertFalse(spanFinished(childSpan)); + assertTrue(spanFinished(parentSpan)); + assertTrue(writer.isEmpty()); + + // activating the continuation + AgentScope newScope = continuation.activate(); + + // the continued scope becomes active and span state doesn't change + assertInstanceOf(ContinuableScope.class, newScope); + assertTrue(tracer.isAsyncPropagationEnabled()); + assertSame(newScope, scopeManager.active()); + assertNotSame(childScope, newScope); + assertNotSame(parentScope, newScope); + assertSame(childSpan, newScope.span()); + assertFalse(spanFinished(childSpan)); + assertTrue(spanFinished(parentSpan)); + assertTrue(writer.isEmpty()); + + // creating and activating a second continuation + AgentScope.Continuation newContinuation = tracer.captureActiveSpan(); + newScope.close(); + AgentScope secondContinuedScope = newContinuation.activate(); + secondContinuedScope.close(); + childSpan.finish(); + writer.waitForTraces(1); + + // spans are all finished and trace is reported + assertNull(scopeManager.active()); + assertTrue(spanFinished(childSpan)); + assertTrue(spanFinished(parentSpan)); + assertEquals(1, writer.size()); + assertTrue(writer.get(0).containsAll(Arrays.asList(childSpan, parentSpan))); + } + + @Test + void continuationAllowsAddingSpansEvenAfterOtherSpansWereCompleted() throws Exception { + // creating and activating a continuation + AgentSpan span = tracer.buildSpan("test", "test").start(); + AgentScope scope = tracer.activateSpan(span); + AgentScope.Continuation continuation = tracer.captureActiveSpan(); + scope.close(); + span.finish(); + + AgentScope newScope = continuation.activate(); + + // the continuation sets the active scope + assertInstanceOf(ContinuableScope.class, newScope); + assertNotSame(scope, newScope); + assertSame(newScope, scopeManager.active()); + assertTrue(spanFinished(span)); + assertTrue(writer.isEmpty()); + + // creating a new child span under a continued scope + AgentSpan childSpan = tracer.buildSpan("test", "child").start(); + AgentScope childScope = tracer.activateSpan(childSpan); + childScope.close(); + childSpan.finish(); + + assertSame(newScope, scopeManager.active()); + + scopeManager.active().close(); + writer.waitForTraces(1); + + // the child has the correct parent + assertNull(scopeManager.active()); + assertTrue(spanFinished(childSpan)); + assertEquals(span.context().getSpanId(), ((DDSpan) childSpan).context().getParentId()); + assertEquals(1, writer.size()); + assertTrue(writer.get(0).containsAll(Arrays.asList(childSpan, span))); + } + + @Test + void testActivatingSameSpanMultipleTimes() { + AgentSpan span = tracer.buildSpan("test", "test").start(); + Stateful localState = mock(Stateful.class); + when(profilingContext.newScopeState(any())).thenReturn(localState); + clearInvocations(profilingContext); + + AgentScope scope1 = scopeManager.activateSpan(span); + + assertEvents(Arrays.asList(ACTIVATE)); + verify(profilingContext, times(1)).newScopeState(any()); + clearInvocations(profilingContext); + + AgentScope scope2 = scopeManager.activateSpan(span); + + // Activating the same span multiple times does not create a new scope + assertEvents(Arrays.asList(ACTIVATE)); + verify(profilingContext, never()).newScopeState(any()); + clearInvocations(profilingContext); + + scope2.close(); + + // Closing a scope once that has been activated multiple times does not close + assertEvents(Arrays.asList(ACTIVATE)); + verify(localState, never()).close(); + clearInvocations(localState); + + scope1.close(); + + assertEvents(Arrays.asList(ACTIVATE, CLOSE)); + verify(localState, times(1)).close(); + } + + @Test + void openingAndClosingMultipleScopes() { + AgentSpan span = tracer.buildSpan("test", "foo").start(); + AgentScope continuableScope = tracer.activateSpan(span); + + assertInstanceOf(ContinuableScope.class, continuableScope); + assertEvents(Arrays.asList(ACTIVATE)); + + AgentSpan childSpan = tracer.buildSpan("test", "foo").start(); + AgentScope childDDScope = tracer.activateSpan(childSpan); + + assertInstanceOf(ContinuableScope.class, childDDScope); + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE)); + + childDDScope.close(); + childSpan.finish(); + + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE, CLOSE, ACTIVATE)); + + continuableScope.close(); + span.finish(); + + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE, CLOSE, ACTIVATE, CLOSE)); + } + + @Test + void closingScopeOutOfOrderSimple() { + AgentSpan firstSpan = tracer.buildSpan("test", "foo").start(); + AgentScope firstScope = tracer.activateSpan(firstSpan); + + AgentSpan secondSpan = tracer.buildSpan("test", "bar").start(); + AgentScope secondScope = tracer.activateSpan(secondSpan); + + firstSpan.finish(); + firstScope.close(); + + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE)); + verify(profilingContext, times(1)).onRootSpanStarted(any()); + verify(profilingContext, times(1)).onAttach(); + verify(profilingContext, times(1)).encodeOperationName("foo"); + verify(profilingContext, times(1)).encodeOperationName("bar"); + verify(profilingContext, times(2)).newScopeState(any()); + verifyNoMoreInteractions(profilingContext); + clearInvocations(profilingContext); + + secondSpan.finish(); + secondScope.close(); + + verify(profilingContext, times(1)).onRootSpanFinished(any(), any()); + verify(profilingContext, times(1)).onDetach(); + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE, CLOSE, CLOSE)); + verifyNoMoreInteractions(profilingContext); + clearInvocations(profilingContext); + + firstScope.close(); + + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE, CLOSE, CLOSE)); + } + + @Test + void closingScopeOutOfOrderComplex() { + // Events are checked twice in each case to ensure a call to + // scopeManager.active() or tracer.activeSpan() doesn't change the count + + AgentSpan firstSpan = tracer.buildSpan("test", "foo").start(); + AgentScope firstScope = tracer.activateSpan(firstSpan); + + assertEvents(Arrays.asList(ACTIVATE)); + assertSame(firstSpan, tracer.activeSpan()); + assertSame(firstScope, scopeManager.active()); + assertEvents(Arrays.asList(ACTIVATE)); + verify(profilingContext, times(1)).onRootSpanStarted(any()); + verify(profilingContext, times(1)).onAttach(); + verify(profilingContext, times(1)).encodeOperationName("foo"); + verify(profilingContext, times(1)).newScopeState(any()); + verifyNoMoreInteractions(profilingContext); + clearInvocations(profilingContext); + + AgentSpan secondSpan = tracer.buildSpan("test", "bar").start(); + AgentScope secondScope = tracer.activateSpan(secondSpan); + + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE)); + assertSame(secondSpan, tracer.activeSpan()); + assertSame(secondScope, scopeManager.active()); + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE)); + verify(profilingContext, times(1)).encodeOperationName("bar"); + verify(profilingContext, times(1)).newScopeState(any()); + verifyNoMoreInteractions(profilingContext); + clearInvocations(profilingContext); + + AgentSpan thirdSpan = tracer.buildSpan("test", "quux").start(); + AgentScope thirdScope = tracer.activateSpan(thirdSpan); + + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE, ACTIVATE)); + assertSame(thirdSpan, tracer.activeSpan()); + assertSame(thirdScope, scopeManager.active()); + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE, ACTIVATE)); + verify(profilingContext, times(1)).encodeOperationName("quux"); + verify(profilingContext, times(1)).newScopeState(any()); + verifyNoMoreInteractions(profilingContext); + clearInvocations(profilingContext); + + secondScope.close(); + + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE, ACTIVATE)); + assertSame(thirdSpan, tracer.activeSpan()); + assertSame(thirdScope, scopeManager.active()); + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE, ACTIVATE)); + verifyNoMoreInteractions(profilingContext); + clearInvocations(profilingContext); + + thirdScope.close(); + + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE, ACTIVATE, CLOSE, CLOSE, ACTIVATE)); + assertSame(firstSpan, tracer.activeSpan()); + assertSame(firstScope, scopeManager.active()); + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE, ACTIVATE, CLOSE, CLOSE, ACTIVATE)); + verifyNoMoreInteractions(profilingContext); + clearInvocations(profilingContext); + + firstScope.close(); + + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE, ACTIVATE, CLOSE, CLOSE, ACTIVATE, CLOSE)); + assertNull(scopeManager.active()); + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE, ACTIVATE, CLOSE, CLOSE, ACTIVATE, CLOSE)); + verify(profilingContext, times(1)).onDetach(); + verifyNoMoreInteractions(profilingContext); + } + + @Test + void closingScopeOutOfOrderMultipleActivations() { + AgentSpan span = tracer.buildSpan("test", "test").start(); + clearInvocations(profilingContext); + + AgentScope scope1 = scopeManager.activateSpan(span); + + assertEvents(Arrays.asList(ACTIVATE)); + + AgentScope scope2 = scopeManager.activateSpan(span); + + // Activating the same span multiple times does not create a new scope + assertEvents(Arrays.asList(ACTIVATE)); + clearInvocations(profilingContext); + + AgentSpan thirdSpan = tracer.buildSpan("test", "quux").start(); + AgentScope thirdScope = tracer.activateSpan(thirdSpan); + + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE)); + assertSame(thirdSpan, tracer.activeSpan()); + assertSame(thirdScope, scopeManager.active()); + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE)); + verify(profilingContext, times(1)).encodeOperationName("quux"); + verify(profilingContext, times(1)).newScopeState(any()); + verifyNoMoreInteractions(profilingContext); + clearInvocations(profilingContext); + + scope2.close(); + + // Closing a scope once that has been activated multiple times does not close + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE)); + verifyNoMoreInteractions(profilingContext); + clearInvocations(profilingContext); + + thirdScope.close(); + thirdSpan.finish(); + + // Closing scope above multiple activated scope does not close it + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE, CLOSE, ACTIVATE)); + verifyNoMoreInteractions(profilingContext); + clearInvocations(profilingContext); + + scope1.close(); + + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE, CLOSE, ACTIVATE, CLOSE)); + } + + @Test + void closingAContinuedScopeOutOfOrderCancelsTheContinuation() throws Exception { + AgentSpan span = tracer.buildSpan("test", "test").start(); + AgentScope scope = tracer.activateSpan(span); + AgentScope.Continuation continuation = tracer.captureActiveSpan(); + scope.close(); + span.finish(); + + assertNull(scopeManager.active()); + assertTrue(spanFinished(span)); + assertTrue(writer.isEmpty()); + + AgentScope continuedScope = continuation.activate(); + + AgentSpan secondSpan = tracer.buildSpan("test", "test2").start(); + AgentScope secondScope = (ContinuableScope) tracer.activateSpan(secondSpan); + + assertSame(secondScope, scopeManager.active()); + + continuedScope.close(); + + assertSame(secondScope, scopeManager.active()); + assertTrue(writer.isEmpty()); + + secondScope.close(); + secondSpan.finish(); + writer.waitForTraces(1); + + assertEquals(1, writer.size()); + assertTrue(writer.get(0).containsAll(Arrays.asList(secondSpan, span))); + } + + @Test + void exceptionThrownInTraceInterceptorDoesNotLeaveScopeManagerInBadState() throws Exception { + ExceptionThrowingInterceptor interceptor = new ExceptionThrowingInterceptor(); + tracer.addTraceInterceptor(interceptor); + + AgentSpan span = tracer.buildSpan("test", "test").start(); + AgentScope scope = tracer.activateSpan(span); + scope.close(); + span.finish(); + + // exception is thrown in same thread + assertTrue(interceptor.lastTrace.contains(span)); + + // scopeManager in good state + assertNull(scopeManager.active()); + assertTrue(spanFinished(span)); + assertEquals(0, scopeManager.scopeStack().depth()); + assertEquals(1, writer.size()); + assertSame(span, writer.get(0).get(0)); + + // completing another scope lifecycle + AgentSpan span2 = tracer.buildSpan("test", "test").start(); + AgentScope scope2 = tracer.activateSpan(span2); + + assertSame(scope2, scopeManager.active()); + + interceptor.shouldThrowException = false; + scope2.close(); + span2.finish(); + writer.waitForTraces(1); + + // second lifecycle gets reported + assertNull(scopeManager.active()); + assertTrue(spanFinished(span2)); + assertEquals(0, scopeManager.scopeStack().depth()); + assertEquals(2, writer.size()); + assertSame(span2, writer.get(1).get(0)); + } + + @Test + void + exceptionThrownInTraceInterceptorDoesNotLeaveScopeManagerInBadStateWhenReportingThroughPendingTraceBuffer() + throws Exception { + ExceptionThrowingInterceptor interceptor = new ExceptionThrowingInterceptor(); + tracer.addTraceInterceptor(interceptor); + + AgentSpan span = tracer.buildSpan("test", "test").start(); + AgentScope scope = tracer.activateSpan(span); + AgentScope.Continuation continuation = tracer.captureActiveSpan(); + scope.close(); + span.finish(); + + assertNotNull(continuation); + assertNull(scopeManager.active()); + assertTrue(spanFinished(span)); + assertEquals(0, scopeManager.scopeStack().depth()); + assertTrue(writer.isEmpty()); + + // wait for root span to be reported from PendingTraceBuffer + writer.waitForTraces(1); + + assertTrue(interceptor.lastTrace.contains(span)); + + // scopeManager in good state + assertNull(scopeManager.active()); + assertTrue(spanFinished(span)); + assertEquals(0, scopeManager.scopeStack().depth()); + assertEquals(1, writer.size()); + assertSame(span, writer.get(0).get(0)); + + // completing another async scope lifecycle + AgentSpan span2 = tracer.buildSpan("test", "test").start(); + AgentScope scope2 = tracer.activateSpan(span2); + AgentScope.Continuation continuation2 = tracer.captureActiveSpan(); + + assertNotNull(continuation2); + assertSame(scope2, scopeManager.active()); + + interceptor.shouldThrowException = false; + scope2.close(); + span2.finish(); + + writer.waitForTraces(2); + + // second lifecycle gets reported as well + assertNull(scopeManager.active()); + assertTrue(spanFinished(span2)); + assertEquals(0, scopeManager.scopeStack().depth()); + assertEquals(2, writer.size()); + assertSame(span2, writer.get(1).get(0)); + } + + @Test + void continuationCanBeActivatedAndClosedInMultipleThreads() throws Throwable { + long sendDelayNanos = TimeUnit.MILLISECONDS.toNanos(500 - 100); + + AgentSpan span = tracer.buildSpan("test", "test").start(); + long start = System.nanoTime(); + AgentScope scope = tracer.activateSpan(span); + AtomicReference continuation = + new AtomicReference<>(tracer.captureActiveSpan()); + scope.close(); + span.finish(); + + continuation.get().hold(); + + AtomicInteger iteration = new AtomicInteger(0); + ThreadUtils.runConcurrently( + 8, + 512, + () -> { + int iter = iteration.incrementAndGet(); + if ((iter & 1) != 0) { + Thread.sleep(1); + } + TraceScope s = continuation.get().activate(); + assertSame(s, scopeManager.active()); + if ((iter & 2) != 0) { + Thread.sleep(1); + } + s.close(); + }); + + long duration = System.nanoTime() - start; + + // Since we can't rely on that nothing gets written to the tracer for verification, + // we only check for empty if we are faster than the flush interval + if (duration < sendDelayNanos) { + assertTrue(writer.isEmpty()); + } + + continuation.get().cancel(); + + assertEquals(1, writer.size()); + assertSame(span, writer.get(0).get(0)); + } + + @Test + void scopeListenerShouldBeNotifiedAboutTheCurrentlyActiveScope() { + AgentSpan span = tracer.buildSpan("test", "test").start(); + + AgentScope scope = scopeManager.activateSpan(span); + + assertEvents(Arrays.asList(ACTIVATE)); + + EventCountingListener listener = new EventCountingListener(); + + assertEquals(0, listener.events.size()); + + scopeManager.addScopeListener(listener); + + assertEquals(Arrays.asList(ACTIVATE), listener.events); + + scope.close(); + + assertEvents(Arrays.asList(ACTIVATE, CLOSE)); + assertEquals(Arrays.asList(ACTIVATE, CLOSE), listener.events); + } + + @Test + void extendedScopeListenerShouldBeNotifiedAboutTheCurrentlyActiveScope() { + AgentSpan span = tracer.buildSpan("test", "test").start(); + + AgentScope scope = scopeManager.activateSpan(span); + + assertEvents(Arrays.asList(ACTIVATE)); + + EventCountingExtendedListener listener = new EventCountingExtendedListener(); + + assertEquals(0, listener.events.size()); + + scopeManager.addScopeListener(listener); + + assertEquals(Arrays.asList(ACTIVATE), listener.events); + + scope.close(); + + assertEvents(Arrays.asList(ACTIVATE, CLOSE)); + assertEquals(Arrays.asList(ACTIVATE, CLOSE), listener.events); + } + + @Test + void scopeListenerShouldNotBeNotifiedWhenThereIsNoActiveScope() { + EventCountingListener listener = new EventCountingListener(); + + assertEquals(0, listener.events.size()); + + scopeManager.addScopeListener(listener); + + assertEquals(0, listener.events.size()); + } + + @TableTest({ + "scenario | activationException | closeException", + "no exceptions | false | false ", + "close exception | false | true ", + "activate exception | true | false ", + "both exceptions | true | true " + }) + void misbehavingScopeListenerShouldNotAffectOthers( + boolean activationException, boolean closeException) { + ExceptionThrowingScopeListener exceptionThrowingScopeListener = + new ExceptionThrowingScopeListener(); + exceptionThrowingScopeListener.throwOnScopeActivated = activationException; + exceptionThrowingScopeListener.throwOnScopeClosed = closeException; + + EventCountingListener secondEventCountingListener = new EventCountingListener(); + scopeManager.addScopeListener(exceptionThrowingScopeListener); + scopeManager.addScopeListener(secondEventCountingListener); + + AgentSpan span = tracer.buildSpan("test", "foo").start(); + AgentScope continuableScope = tracer.activateSpan(span); + + assertEvents(Arrays.asList(ACTIVATE)); + assertEquals(Arrays.asList(ACTIVATE), secondEventCountingListener.events); + + AgentSpan childSpan = tracer.buildSpan("test", "foo").start(); + AgentScope childDDScope = tracer.activateSpan(childSpan); + + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE)); + assertEquals(Arrays.asList(ACTIVATE, ACTIVATE), secondEventCountingListener.events); + + childDDScope.close(); + childSpan.finish(); + + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE, CLOSE, ACTIVATE)); + assertEquals( + Arrays.asList(ACTIVATE, ACTIVATE, CLOSE, ACTIVATE), secondEventCountingListener.events); + + continuableScope.close(); + span.finish(); + + assertEvents(Arrays.asList(ACTIVATE, ACTIVATE, CLOSE, ACTIVATE, CLOSE)); + assertEquals( + Arrays.asList(ACTIVATE, ACTIVATE, CLOSE, ACTIVATE, CLOSE), + secondEventCountingListener.events); + } + + @Test + void contextThreadListenerNotifiedWhenScopeActivatedOnThreadForTheFirstTime() throws Exception { + int numThreads = 5; + int numTasks = 20; + ExecutorService executor = Executors.newFixedThreadPool(numThreads); + + // usage of an instrumented executor results in scopestack initialisation but not scope creation + executor.submit(() -> assertNull(scopeManager.active())).get(); + // the listener is not notified + verify(profilingContext, never()).onAttach(); + clearInvocations(profilingContext); + + // scopes activate on threads + AgentSpan span = tracer.buildSpan("test", "foo").start(); + Future[] futures = new Future[numTasks]; + for (int i = 0; i < numTasks; i++) { + final int taskIndex = i; + futures[i] = + executor.submit( + () -> { + AgentScope scope = tracer.activateSpan(span); + AgentSpan child = tracer.buildSpan("test", "foo" + taskIndex).start(); + AgentScope childScope = tracer.activateSpan(child); + try { + Thread.sleep(100); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + childScope.close(); + scope.close(); + }); + } + for (Future future : futures) { + future.get(); + } + + // the activation notifies the listener whenever the stack becomes non-empty + verify(profilingContext, times(numTasks)).onAttach(); + + executor.shutdown(); + } + + @Test + void activatingASpanMergesItWithExistingContext() { + AgentSpan span = tracer.buildSpan("test", "test").start(); + ContextKey testKey = ContextKey.named("test"); + Context context = Context.root().with(testKey, "test-value"); + ContextScope contextScope = scopeManager.attach(context); + + assertSame(contextScope, scopeManager.active()); + assertEquals(context, scopeManager.current()); + assertNull(scopeManager.activeSpan()); + assertEquals("test-value", scopeManager.current().get(testKey)); + + AgentScope scope = tracer.activateSpan(span); + + assertSame(scope, scopeManager.active()); + assertNotEquals(context, scopeManager.current()); + assertSame(span, scopeManager.activeSpan()); + assertEquals("test-value", scopeManager.current().get(testKey)); + + scope.close(); + + assertSame(contextScope, scopeManager.active()); + assertEquals(context, scopeManager.current()); + assertNull(scopeManager.activeSpan()); + assertEquals("test-value", scopeManager.current().get(testKey)); + + contextScope.close(); + + assertNull(scopeManager.active()); + assertEquals(Context.root(), scopeManager.current()); + assertNull(scopeManager.activeSpan()); + } + + @Test + void capturingAndContinuingASpanMergesItWithExistingContext() { + AgentSpan span = tracer.buildSpan("test", "test").start(); + ContextKey testKey = ContextKey.named("test"); + Context context = Context.root().with(testKey, "test-value"); + ContextScope contextScope = scopeManager.attach(context); + + assertSame(contextScope, scopeManager.active()); + assertEquals(context, scopeManager.current()); + assertNull(scopeManager.activeSpan()); + assertEquals("test-value", scopeManager.current().get(testKey)); + + AgentScope scope = tracer.captureSpan(span).activate(); + + assertSame(scope, scopeManager.active()); + assertNotEquals(context, scopeManager.current()); + assertSame(span, scopeManager.activeSpan()); + assertEquals("test-value", scopeManager.current().get(testKey)); + + scope.close(); + + assertSame(contextScope, scopeManager.active()); + assertEquals(context, scopeManager.current()); + assertNull(scopeManager.activeSpan()); + assertEquals("test-value", scopeManager.current().get(testKey)); + + contextScope.close(); + + assertNull(scopeManager.active()); + assertEquals(Context.root(), scopeManager.current()); + assertNull(scopeManager.activeSpan()); + } + + @Test + void capturingAndContinuingTheActiveSpanMergesItWithExistingContext() { + AgentSpan span = tracer.buildSpan("test", "test").start(); + ContextKey testKey = ContextKey.named("test"); + Context context = Context.root().with(testKey, "test-value"); + ContextScope contextScope = scopeManager.attach(context); + + assertSame(contextScope, scopeManager.active()); + assertEquals(context, scopeManager.current()); + assertNull(scopeManager.activeSpan()); + assertEquals("test-value", scopeManager.current().get(testKey)); + + AgentScope innerScope = tracer.activateSpan(span); + AgentScope scope = tracer.captureActiveSpan().activate(); + innerScope.close(); + + assertSame(scope, scopeManager.active()); + assertNotEquals(context, scopeManager.current()); + assertSame(span, scopeManager.activeSpan()); + assertEquals("test-value", scopeManager.current().get(testKey)); + + scope.close(); + + assertSame(contextScope, scopeManager.active()); + assertEquals(context, scopeManager.current()); + assertNull(scopeManager.activeSpan()); + assertEquals("test-value", scopeManager.current().get(testKey)); + + contextScope.close(); + + assertNull(scopeManager.active()); + assertEquals(Context.root(), scopeManager.current()); + assertNull(scopeManager.activeSpan()); + } + + @Test + void rollbackStopsAtMostRecentCheckpoint() { + AgentSpan span1 = tracer.buildSpan("test1", "test1").start(); + AgentSpan span2 = tracer.buildSpan("test2", "test2").start(); + AgentSpan span3 = tracer.buildSpan("test3", "test3").start(); + + assertNull(scopeManager.activeSpan()); + + tracer.checkpointActiveForRollback(); + tracer.activateSpan(span1); + tracer.checkpointActiveForRollback(); + tracer.activateSpan(span2); + tracer.checkpointActiveForRollback(); + tracer.activateSpan(span1); + tracer.checkpointActiveForRollback(); + tracer.activateSpan(span2); + tracer.checkpointActiveForRollback(); + tracer.activateSpan(span2); + tracer.checkpointActiveForRollback(); + tracer.activateSpan(span1); + tracer.activateSpan(span2); + tracer.activateSpan(span3); + + assertSame(span3, scopeManager.activeSpan()); + + tracer.rollbackActiveToCheckpoint(); + assertSame(span2, scopeManager.activeSpan()); + + tracer.rollbackActiveToCheckpoint(); + assertSame(span2, scopeManager.activeSpan()); + + tracer.rollbackActiveToCheckpoint(); + assertSame(span1, scopeManager.activeSpan()); + + tracer.rollbackActiveToCheckpoint(); + assertSame(span2, scopeManager.activeSpan()); + + tracer.rollbackActiveToCheckpoint(); + assertSame(span1, scopeManager.activeSpan()); + + tracer.rollbackActiveToCheckpoint(); + assertNull(scopeManager.activeSpan()); + } + + @Test + void contextsCanBeSwappedOutAndBack() { + ContextKey testKey = ContextKey.named("test"); + Context context1 = Context.root().with(testKey, "first-value"); + Context context2 = context1.with(testKey, "second-value"); + + Context swappedOut = scopeManager.swap(Context.root()); + + assertNull(scopeManager.active()); + assertEquals(Context.root(), scopeManager.current()); + + scopeManager.swap(context1); + + assertNotNull(scopeManager.active()); + assertEquals(context1, scopeManager.current()); + + scopeManager.swap(swappedOut); + + assertNull(scopeManager.active()); + assertEquals(Context.root(), scopeManager.current()); + + ContextScope contextScope = scopeManager.attach(context1); + + assertSame(contextScope, scopeManager.active()); + assertEquals(context1, scopeManager.current()); + + swappedOut = scopeManager.swap(context2); + + assertNotNull(scopeManager.active()); + assertNotSame(contextScope, scopeManager.active()); + assertEquals(context2, scopeManager.current()); + assertEquals("first-value", swappedOut.get(testKey)); + + Context context3 = swappedOut.with(testKey, "third-value"); + scopeManager.swap(context3); + + assertNotNull(scopeManager.active()); + assertNotSame(contextScope, scopeManager.active()); + assertEquals(context3, scopeManager.current()); + + scopeManager.swap(swappedOut); + + assertSame(contextScope, scopeManager.active()); + assertEquals(context1, scopeManager.current()); + + contextScope.close(); + + assertNull(scopeManager.active()); + assertEquals(Context.root(), scopeManager.current()); + } + + private boolean spanFinished(AgentSpan span) { + return span instanceof DDSpan && ((DDSpan) span).isFinished(); + } + + private void assertEvents(List events) { + assertEquals(events, eventCountingListener.events); + assertEquals(events, eventCountingExtendedListener.events); + } + + static class EventCountingListener implements ScopeListener { + public final List events = new ArrayList<>(); + + @Override + public void afterScopeActivated() { + synchronized (events) { + events.add(ACTIVATE); + } + } + + @Override + public void afterScopeClosed() { + synchronized (events) { + events.add(CLOSE); + } + } + } + + static class EventCountingExtendedListener implements ExtendedScopeListener { + public final List events = new ArrayList<>(); + + @Override + public void afterScopeActivated() { + throw new IllegalArgumentException("This should not be called"); + } + + @Override + public void afterScopeActivated(DDTraceId traceId, long spanId) { + synchronized (events) { + events.add(ACTIVATE); + } + } + + @Override + public void afterScopeClosed() { + synchronized (events) { + events.add(CLOSE); + } + } + } + + static class ExceptionThrowingScopeListener implements ScopeListener { + boolean throwOnScopeActivated = false; + boolean throwOnScopeClosed = false; + + @Override + public void afterScopeActivated() { + if (throwOnScopeActivated) { + throw new RuntimeException("Exception on activated"); + } + } + + @Override + public void afterScopeClosed() { + if (throwOnScopeClosed) { + throw new RuntimeException("Exception on closed"); + } + } + } + + static class ExceptionThrowingInterceptor implements TraceInterceptor { + boolean shouldThrowException = true; + Collection lastTrace; + + @Override + public Collection onTraceComplete( + Collection trace) { + lastTrace = trace; + if (shouldThrowException) { + throw new RuntimeException("Always throws exception"); + } else { + return trace; + } + } + + @Override + public int priority() { + return 55; + } + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/IntegrationAdderTest.java b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/IntegrationAdderTest.java new file mode 100644 index 00000000000..8b6aa9cedae --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/IntegrationAdderTest.java @@ -0,0 +1,38 @@ +package datadog.trace.core.tagprocessor; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import datadog.trace.api.TagMap; +import datadog.trace.core.DDSpanContext; +import datadog.trace.test.util.DDJavaSpecification; +import java.util.Collections; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class IntegrationAdderTest extends DDJavaSpecification { + + @ValueSource(booleans = {true, false}) + @ParameterizedTest( + name = "should add or remove _dd.integration when set ({0}) on the span context") + void shouldAddOrRemoveDdIntegrationWhenSetOnTheSpanContext(boolean isSet) { + IntegrationAdder calculator = new IntegrationAdder(); + DDSpanContext spanContext = mock(DDSpanContext.class); + when(spanContext.getIntegrationName()).thenReturn(isSet ? "test" : null); + + TagMap unsafeTags = TagMap.fromMap(Collections.singletonMap("_dd.integration", "bad")); + calculator.processTags(unsafeTags, spanContext, link -> {}); + + verify(spanContext, times(1)).getIntegrationName(); + + if (isSet) { + assertEquals(Collections.singletonMap("_dd.integration", "test"), unsafeTags); + } else { + assertTrue(unsafeTags.isEmpty()); + } + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/InternalTagsAdderTest.java b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/InternalTagsAdderTest.java new file mode 100644 index 00000000000..ea3798a4427 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/InternalTagsAdderTest.java @@ -0,0 +1,70 @@ +package datadog.trace.core.tagprocessor; + +import static datadog.trace.bootstrap.instrumentation.api.Tags.VERSION; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import datadog.trace.api.TagMap; +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import datadog.trace.core.DDSpanContext; +import datadog.trace.test.util.DDJavaSpecification; +import java.util.Collections; +import java.util.Objects; +import org.tabletest.junit.TableTest; + +class InternalTagsAdderTest extends DDJavaSpecification { + + @TableTest({ + "scenario | serviceName | expectsBaseService", + "different service | anotherOne | true ", + "exact match | test | false ", + "case insensitive | TeSt | false " + }) + void shouldAddBaseServiceWhenServiceDiffersToDdService( + String serviceName, boolean expectsBaseService) { + InternalTagsAdder calculator = new InternalTagsAdder("test", null); + DDSpanContext spanContext = mock(DDSpanContext.class); + when(spanContext.getServiceName()).thenReturn(serviceName); + + TagMap unsafeTags = TagMap.fromMap(Collections.emptyMap()); + calculator.processTags(unsafeTags, spanContext, link -> {}); + + verify(spanContext, times(1)).getServiceName(); + + if (expectsBaseService) { + assertEquals(UTF8BytesString.create("test"), unsafeTags.get("_dd.base_service")); + } else { + assertTrue(unsafeTags.isEmpty()); + } + } + + @TableTest({ + "scenario | serviceName | ddVersion | initialVersion | expected", + "same service, no version | same | | | ", + "different service with ddVersion | different | 1.0 | | ", + "different service, manual version | different | 1.0 | 2.0 | 2.0 ", + "same service, no ddVersion | same | | 2.0 | 2.0 ", + "same service, both versions | same | 1.0 | 2.0 | 2.0 ", + "same service, only ddVersion | same | 1.0 | | 1.0 " + }) + void shouldAddVersionWhenDdServiceEqualsServiceNameAndVersionSet( + String serviceName, String ddVersion, String initialVersion, String expected) { + InternalTagsAdder calculator = new InternalTagsAdder("same", ddVersion); + DDSpanContext spanContext = mock(DDSpanContext.class); + when(spanContext.getServiceName()).thenReturn(serviceName); + + TagMap unsafeTags = + TagMap.fromMap( + initialVersion != null + ? Collections.singletonMap("version", initialVersion) + : Collections.emptyMap()); + calculator.processTags(unsafeTags, spanContext, link -> {}); + + verify(spanContext, times(1)).getServiceName(); + assertEquals(expected, Objects.toString(unsafeTags.get(VERSION), null)); + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/PayloadTagsProcessorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/PayloadTagsProcessorTest.java new file mode 100644 index 00000000000..5edcf825a30 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/PayloadTagsProcessorTest.java @@ -0,0 +1,491 @@ +package datadog.trace.core.tagprocessor; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.squareup.moshi.JsonWriter; +import datadog.trace.api.Config; +import datadog.trace.api.TagMap; +import datadog.trace.junit.utils.config.WithConfigExtension; +import datadog.trace.payloadtags.PayloadTagsData; +import datadog.trace.payloadtags.PayloadTagsData.PathAndValue; +import datadog.trace.test.util.DDJavaSpecification; +import datadog.trace.util.json.PathCursor; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import okio.Buffer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.tabletest.junit.TableTest; + +class PayloadTagsProcessorTest extends DDJavaSpecification { + + private static PathCursor pc() { + return new PathCursor(10); + } + + private static PathAndValue pv(PathCursor path, Object value) { + return new PathAndValue(path.toPath(), value); + } + + private static PayloadTagsData payloadData(PathAndValue... pvs) { + return new PayloadTagsData(pvs); + } + + private static Map spanTags(String tagPrefix, PathAndValue... pvs) { + Map map = new LinkedHashMap<>(); + map.put(tagPrefix, payloadData(pvs)); + return map; + } + + private static PayloadTagsProcessor tagsProcessor( + String tagPrefix, List redactionRules, int maxDepth, int maxTags) { + PayloadTagsProcessor.RedactionRules rules = + new PayloadTagsProcessor.RedactionRules.Builder() + .addRedactionJsonPaths(redactionRules) + .build(); + Map rulesMap = new HashMap<>(); + rulesMap.put(tagPrefix, rules); + return new PayloadTagsProcessor(rulesMap, maxDepth, maxTags); + } + + /** Builds a LinkedHashMap from alternating key-value pairs; supports null values. */ + @SafeVarargs + private static Map mapOf(Object... pairs) { + Map result = new LinkedHashMap<>(); + for (int i = 0; i < pairs.length; i += 2) { + @SuppressWarnings("unchecked") + V val = (V) pairs[i + 1]; + result.put((String) pairs[i], val); + } + return result; + } + + @Test + void disabledByDefault() { + assertNull(PayloadTagsProcessor.create(Config.get())); + } + + static Stream enabledWithDefaultsWhenConfiguredArguments() { + return Stream.of( + Arguments.arguments("all", "all", "aws.request.body", pc().push("phoneNumber")), + Arguments.arguments("all", "$[33].baz", "aws.request.body", pc().push("AWSAccountId")), + Arguments.arguments( + "$.bar", + "all", + "aws.response.body", + pc().push("Endpoints").push("foobar").push("Token")), + Arguments.arguments("$.foo.bar", "$..bar.*", "aws.request.body", pc().push("phoneNumber")), + Arguments.arguments(null, "all", "aws.response.body", pc().push("phoneNumbers").push(5)), + Arguments.arguments( + "all", null, "aws.request.body", pc().push("Attributes").push("KmsMasterKeyId"))); + } + + @ParameterizedTest(name = "enabled with defaults when configured req {0} resp {1}") + @MethodSource("enabledWithDefaultsWhenConfiguredArguments") + void enabledWithDefaultsWhenConfigured( + String requestPayloadTagging, + String responsePayloadTagging, + String tagPrefix, + PathCursor pathMatchingDefaultRules) { + if (requestPayloadTagging != null) { + WithConfigExtension.injectSysConfig( + "trace.cloud.request.payload.tagging", requestPayloadTagging); + } + if (responsePayloadTagging != null) { + WithConfigExtension.injectSysConfig( + "trace.cloud.response.payload.tagging", responsePayloadTagging); + } + + PayloadTagsProcessor ptp = PayloadTagsProcessor.create(Config.get()); + + assertNotNull(ptp); + assertEquals(10, ptp.maxDepth); + assertEquals(758, ptp.maxTags); + assertNotNull( + ptp.redactionRulesByTagPrefix.get(tagPrefix).findMatching(pathMatchingDefaultRules)); + assertNull( + ptp.redactionRulesByTagPrefix.get(tagPrefix).findMatching(pc().push("non-matching-path"))); + } + + @TableTest({ + "scenario | requestPayloadTagging | responsePayloadTagging | maxDepth | maxTags", + "both req and resp all | all | all | 10 | 10 ", + "req filter, no resp | $.bar | | 7 | 42 ", + "req all, resp wildcard | all | $.* | 12 | 50 ", + "no req, resp all | | all | 8 | 33 " + }) + void enabledWithCustomLimits( + String requestPayloadTagging, String responsePayloadTagging, int maxDepth, int maxTags) { + if (requestPayloadTagging != null) { + WithConfigExtension.injectSysConfig( + "trace.cloud.request.payload.tagging", requestPayloadTagging); + } + if (responsePayloadTagging != null) { + WithConfigExtension.injectSysConfig( + "trace.cloud.response.payload.tagging", responsePayloadTagging); + } + WithConfigExtension.injectSysConfig( + "trace.cloud.payload.tagging.max-depth", String.valueOf(maxDepth)); + WithConfigExtension.injectSysConfig( + "trace.cloud.payload.tagging.max-tags", String.valueOf(maxTags)); + + PayloadTagsProcessor ptp = PayloadTagsProcessor.create(Config.get()); + + assertEquals(maxDepth, ptp.maxDepth); + assertEquals(maxTags, ptp.maxTags); + } + + @Test + void preserveAllSpanTagsExceptForPayloadData() { + PayloadTagsProcessor ptp = tagsProcessor("payload", Collections.emptyList(), 10, 758); + Map spanTags = new LinkedHashMap<>(); + spanTags.put("foo", "bar"); + spanTags.put("tag1", 1); + spanTags.put("payload", new PayloadTagsData(new PathAndValue[0])); + + TagMap unsafeTags = TagMap.fromMap(spanTags); + ptp.processTags(unsafeTags, null, link -> {}); + + assertEquals(mapOf("foo", "bar", "tag1", 1), unsafeTags); + } + + @Test + void expandPayloadToTags() { + PayloadTagsProcessor ptp = tagsProcessor("payload", Collections.emptyList(), 10, 758); + Map spanTags = new LinkedHashMap<>(); + spanTags.put("foo", "bar"); + spanTags.put("tag1", 1); + spanTags.put("payload", payloadData(pv(pc().push("tag1"), 0))); + + TagMap unsafeTags = TagMap.fromMap(spanTags); + ptp.processTags(unsafeTags, null, link -> {}); + + assertEquals(mapOf("foo", "bar", "tag1", 1, "payload.tag1", 0), unsafeTags); + } + + @Test + void expandPreservingTagTypes() { + PayloadTagsProcessor ptp = tagsProcessor("payload", Collections.emptyList(), 10, 758); + + Map st = + spanTags( + "payload", + pv(pc().push("tag1"), 11), + pv(pc().push("tag2").push("Value"), 2342L), + pv(pc().push("tag3").push(0), 3.14d), + pv(pc().push("tag4").push("Value").push(0), "string"), + pv(pc().push("tag5"), null), + pv(pc().push("tag6"), false)); + + TagMap unsafeTags = TagMap.fromMap(st); + ptp.processTags(unsafeTags, null, link -> {}); + + Map expected = + mapOf( + "payload.tag1", + 11, + "payload.tag2.Value", + 2342L, + "payload.tag3.0", + 3.14d, + "payload.tag4.Value.0", + "string", + "payload.tag5", + null, + "payload.tag6", + false); + assertEquals(expected, unsafeTags); + } + + @Test + void expandUnknownTagValuesToString() { + PayloadTagsProcessor ptp = tagsProcessor("payload", Collections.emptyList(), 10, 758); + Instant unknownValue = Instant.now(); + + Map st = spanTags("payload", pv(pc().push("tag7"), unknownValue)); + + TagMap unsafeTags = TagMap.fromMap(st); + ptp.processTags(unsafeTags, null, link -> {}); + + assertEquals(mapOf("payload.tag7", unknownValue.toString()), unsafeTags); + } + + @Test + void expandStringifiedJsonTags() { + PayloadTagsProcessor ptp = tagsProcessor("p", Collections.emptyList(), 10, 758); + + Map st = + spanTags( + "p", + pv(pc().push("j1"), "{}"), + pv(pc().push("j2"), "[]"), + pv(pc().push("j3"), "['1', 2, 3.14, null, true]"), + pv(pc().push("j4"), "{'foo': 'bar', 'baz': 42}")); + + TagMap unsafeTags = TagMap.fromMap(st); + ptp.processTags(unsafeTags, null, link -> {}); + + Map expected = + mapOf( + "p.j3.0", + "1", + "p.j3.1", + 2, + "p.j3.2", + 3.14d, + "p.j3.3", + null, + "p.j3.4", + true, + "p.j4.foo", + "bar", + "p.j4.baz", + 42); + assertEquals(expected, unsafeTags); + } + + @Test + void expandSerializedEscapedInnerJsonWithinInnerJson() throws IOException { + Buffer b0 = new Buffer(); + JsonWriter.of(b0) + .beginObject() + .name("a") + .value(1.15) + .name("password") + .value("my-secret-password") + .endObject() + .close(); + + Buffer b1 = new Buffer(); + JsonWriter.of(b1) + .beginObject() + .name("id") + .value(45) + .name("user") + .value(b0.readUtf8()) + .endObject() + .close(); + + Buffer b2 = new Buffer(); + JsonWriter.of(b2) + .beginObject() + .name("a") + .value(33) + .name("Message") + .value(b1.readUtf8()) + .name("b") + .value(true) + .endObject() + .close(); + + String json = b2.readUtf8(); + + PayloadTagsProcessor ptp = tagsProcessor("dd", Collections.emptyList(), 10, 758); + Map st = spanTags("dd", pv(pc(), json)); + + TagMap unsafeTags = TagMap.fromMap(st); + ptp.processTags(unsafeTags, null, link -> {}); + + assertEquals( + mapOf( + "dd.a", + 33, + "dd.Message.id", + 45, + "dd.Message.user.a", + 1.15d, + "dd.Message.user.password", + "my-secret-password", + "dd.b", + true), + unsafeTags); + } + + @ValueSource( + strings = {"{'foo: 'bar'", "[1, 2", "[1, 2] ", " [1, 2]", "{'foo: 'bar'} ", " {'foo: 'bar'}"}) + @ParameterizedTest + void keepFailedToParseJsonAsIs(String invalidJson) { + PayloadTagsProcessor ptp = tagsProcessor("p", Collections.emptyList(), 10, 758); + Map st = spanTags("p", pv(pc().push("key"), invalidJson)); + + TagMap unsafeTags = TagMap.fromMap(st); + ptp.processTags(unsafeTags, null, link -> {}); + + assertEquals(mapOf("p.key", invalidJson), unsafeTags); + } + + @Test + void expandBinaryIfJson() { + PayloadTagsProcessor ptp = tagsProcessor("p", Collections.emptyList(), 10, 758); + + Map st = + spanTags( + "p", + pv(pc().push("j0"), new ByteArrayInputStream("{}".getBytes())), + pv(pc().push("j1"), new ByteArrayInputStream("{'foo': 'bar'}".getBytes())), + pv(pc().push("j2"), new ByteArrayInputStream("[1, true]".getBytes()))); + + TagMap unsafeTags = TagMap.fromMap(st); + ptp.processTags(unsafeTags, null, link -> {}); + + assertEquals(mapOf("p.j1.foo", "bar", "p.j2.0", 1, "p.j2.1", true), unsafeTags); + } + + @ParameterizedTest + @ValueSource( + strings = { + // standard JSON + "{ \"a\": 1.15, \"password\": \"my-secret-password\" }", + // JSON wrapped in double quotes (string-quoted) + "\"{ 'a': 1.15, 'password': 'my-secret-password' }\"", + // JSON with escaped quotes, wrapped in double quotes + "\"{ \\\"a\\\": 1.15, \\\"password\\\": \\\"my-secret-password\\\" }\"", + // JSON wrapped in single quotes + "'{ \"a\": 1.15, \"password\": \"my-secret-password\" }'", + // same as case 3, alternate source representation + "\"{ \\\"a\\\": 1.15, \\\"password\\\": \\\"my-secret-password\\\" }\"" + }) + void expandBinaryEscapedJsonTags(String innerJson) { + PayloadTagsProcessor ptp = tagsProcessor("p", Collections.emptyList(), 10, 758); + Map st = + spanTags( + "p", + pv( + pc().push("v"), + new ByteArrayInputStream(("{ \"inner\": " + innerJson + "}").getBytes()))); + + TagMap unsafeTags = TagMap.fromMap(st); + ptp.processTags(unsafeTags, null, link -> {}); + + assertEquals( + mapOf("p.v.inner.a", 1.15d, "p.v.inner.password", "my-secret-password"), unsafeTags); + } + + @ValueSource(strings = {" [1]", " {'foo': 'bar'}", "invalid:"}) + @ParameterizedTest + void useBinaryValueIfNotJsonOrCouldntBeParsed(String invalidJson) { + PayloadTagsProcessor ptp = tagsProcessor("p", Collections.emptyList(), 10, 758); + Map st = + spanTags("p", pv(pc().push("key"), new ByteArrayInputStream(invalidJson.getBytes()))); + + TagMap unsafeTags = TagMap.fromMap(st); + ptp.processTags(unsafeTags, null, link -> {}); + + assertEquals(mapOf("p.key", ""), unsafeTags); + } + + @Test + void applyRedactionRules() { + PayloadTagsProcessor ptp = tagsProcessor("p", Arrays.asList("$.j3[0]", "$.j4.baz"), 10, 758); + + Map st = + spanTags( + "p", + pv(pc().push("j1"), "{}"), + pv(pc().push("j2"), "[]"), + pv(pc().push("j3"), "['1', 2, 3.14, null, true]"), + pv(pc().push("j4"), "{'foo': 'bar', 'baz': 42}")); + + TagMap unsafeTags = TagMap.fromMap(st); + ptp.processTags(unsafeTags, null, link -> {}); + + Map expected = + mapOf( + "p.j3.0", + "redacted", + "p.j3.1", + 2, + "p.j3.2", + 3.14d, + "p.j3.3", + null, + "p.j3.4", + true, + "p.j4.foo", + "bar", + "p.j4.baz", + "redacted"); + assertEquals(expected, unsafeTags); + } + + @Test + void respectMaxTagsLimit() { + PayloadTagsProcessor ptp = tagsProcessor("p", Arrays.asList("$.j3[0]", "$.j4.baz"), 10, 4); + + Map st = + spanTags( + "p", + pv(pc().push("j1"), "{}"), + pv(pc().push("j2"), "[]"), + pv(pc().push("j3"), "['1', 2, 3.14, null, true]"), + pv(pc().push("j4"), "{'foo': 'bar', 'baz': 42}")); + + TagMap unsafeTags = TagMap.fromMap(st); + ptp.processTags(unsafeTags, null, link -> {}); + + assertEquals( + mapOf( + "p.j3.0", + "redacted", + "p.j3.1", + 2, + "p.j3.2", + 3.14d, + "p.j3.3", + null, + "_dd.payload_tags_incomplete", + true), + unsafeTags); + } + + @Test + void respectMaxDepthLimit() { + PayloadTagsProcessor ptp = tagsProcessor("p", Arrays.asList("$.j3[0]", "$.j4.baz"), 3, 800); + + Map st = + spanTags( + "p", + pv(pc().push("j3"), "['1', 2, 3.14, null, true, [ 1, [ 2, 3 ] ]]"), + pv( + pc().push("j4"), + "{'foo': 'bar', 'baz': 42, 'nested': { 'a': 1, 'b': { 'c': 2 } } }")); + + TagMap unsafeTags = TagMap.fromMap(st); + ptp.processTags(unsafeTags, null, link -> {}); + + assertEquals( + mapOf( + "p.j3.0", + "redacted", + "p.j3.1", + 2, + "p.j3.2", + 3.14d, + "p.j3.3", + null, + "p.j3.4", + true, + "p.j3.5.0", + 1, + "p.j4.foo", + "bar", + "p.j4.baz", + "redacted", + "p.j4.nested.a", + 1), + unsafeTags); + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/PeerServiceCalculatorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/PeerServiceCalculatorTest.java new file mode 100644 index 00000000000..1c6008b46c5 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/PeerServiceCalculatorTest.java @@ -0,0 +1,146 @@ +package datadog.trace.core.tagprocessor; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import datadog.trace.api.Config; +import datadog.trace.api.DDTags; +import datadog.trace.api.TagMap; +import datadog.trace.api.naming.v0.NamingSchemaV0; +import datadog.trace.api.naming.v1.NamingSchemaV1; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.junit.utils.config.WithConfig; +import datadog.trace.test.util.DDJavaSpecification; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.tabletest.junit.TableTest; + +class PeerServiceCalculatorTest extends DDJavaSpecification { + + private static LinkedHashMap linkedMap(Object... pairs) { + LinkedHashMap map = new LinkedHashMap<>(); + for (int i = 0; i < pairs.length; i += 2) { + map.put((String) pairs[i], pairs[i + 1]); + } + return map; + } + + @TableTest({ + "scenario | tags ", + "empty | [:] ", + "hostname only | ['peer.hostname': 'test'] ", + "hostname and db instance | ['peer.hostname': 'test', 'db.instance': 'instance']", + "db instance before hostname | ['db.instance': 'instance', 'peer.hostname': 'test']", + "hostname and rpc service | ['peer.hostname': 'test', 'rpc.service': 'svc'] ", + "rpc service before hostname | ['rpc.service': 'svc', 'peer.hostname': 'test'] " + }) + void schemaV0PeerServiceIsNotCalculatedByDefault(Map tags) { + PeerServiceCalculator calculator = + new PeerServiceCalculator(new NamingSchemaV0().peerService(), Collections.emptyMap()); + + TagMap unsafeTags = TagMap.fromMap(tags); + calculator.processTags(unsafeTags, null, link -> {}); + + // tags are not modified + assertEquals(tags, unsafeTags); + } + + @TableTest({ + "scenario | tags | provenance | peerService", + "empty | [:] | | ", + "hostname only (1) | ['peer.hostname': 'test'] | peer.hostname | test ", + "hostname only (2) | ['peer.hostname': 'test'] | peer.hostname | test ", + "hostname and db instance | ['peer.hostname': 'test', 'db.instance': 'instance'] | db.instance | instance ", + "db instance before hostname | ['db.instance': 'instance', 'peer.hostname': 'test'] | db.instance | instance ", + "hostname, rpc service, grpc component | ['peer.hostname': 'test', 'rpc.service': 'svc', 'component': 'grpc-client'] | rpc.service | svc ", + "rpc service before hostname | ['rpc.service': 'svc', 'peer.hostname': 'test', 'component': 'grpc-client'] | rpc.service | svc ", + "hostname and peer service | ['peer.hostname': 'test', 'peer.service': 'userService'] | | userService" + }) + void schemaV1TestPeerServiceDefaultLogicAndPrecursors( + Map tags, String provenance, String peerService) { + PeerServiceCalculator calculator = + new PeerServiceCalculator(new NamingSchemaV1().peerService(), Collections.emptyMap()); + + Map tagsWithSpanKind = new LinkedHashMap<>(tags); + tagsWithSpanKind.put(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + + TagMap unsafeTags = TagMap.fromMap(tagsWithSpanKind); + calculator.processTags(unsafeTags, null, link -> {}); + + assertEquals(provenance, unsafeTags.get(DDTags.PEER_SERVICE_SOURCE)); + assertEquals(peerService, unsafeTags.get(Tags.PEER_SERVICE)); + } + + @WithConfig(key = "trace.peer.service.defaults.enabled", value = "true") + @Test + void schemaV0ShouldCalculateDefaultsIfEnabled() { + PeerServiceCalculator calculator = + new PeerServiceCalculator(new NamingSchemaV0().peerService(), Collections.emptyMap()); + + TagMap unsafeTags = TagMap.fromMap(linkedMap("span.kind", "client", "peer.hostname", "test")); + calculator.processTags(unsafeTags, null, link -> {}); + + assertEquals("test", unsafeTags.get(Tags.PEER_SERVICE)); + } + + @TableTest({ + "scenario | kind | calculate", + "client | client | true ", + "producer | producer | true ", + "server | server | false " + }) + void calculateOnlyForSpanKindClientOrProducer(String kind, boolean calculate) { + PeerServiceCalculator calculator = + new PeerServiceCalculator(new NamingSchemaV1().peerService(), Collections.emptyMap()); + + Map tags = linkedMap("span.kind", kind, "peer.hostname", "test"); + TagMap unsafeTags = TagMap.fromMap(tags); + calculator.processTags(unsafeTags, null, link -> {}); + + assertEquals(calculate, unsafeTags.containsKey(Tags.PEER_SERVICE)); + } + + @WithConfig( + key = "trace.peer.service.mapping", + value = "service1:best_service,userService:my_service") + @WithConfig(key = "trace.peer.service.defaults.enabled", value = "true") + @TableTest({ + "scenario | tags | expected | original ", + "peer service remapped | ['peer.service': 'userService'] | my_service | userService", + "hostname client, no remap | ['peer.hostname': 'test', 'span.kind': 'client'] | test | ", + "hostname producer, remap service | ['peer.hostname': 'service1', 'span.kind': 'producer'] | best_service | service1 " + }) + void shouldApplyPeerServiceMappingsIfConfigured( + Map tags, String expected, String original) { + PeerServiceCalculator calculator = + new PeerServiceCalculator( + new NamingSchemaV0().peerService(), Config.get().getPeerServiceMapping()); + + TagMap unsafeTags = TagMap.fromMap(tags); + calculator.processTags(unsafeTags, null, link -> {}); + + assertEquals(expected, unsafeTags.get(Tags.PEER_SERVICE)); + assertEquals(original, unsafeTags.get(DDTags.PEER_SERVICE_REMAPPED_FROM)); + } + + @WithConfig(key = "trace.peer.service.component.overrides", value = "java-couchbase:couchbase") + @WithConfig(key = "trace.peer.service.defaults.enabled", value = "true") + @TableTest({ + "scenario | tags | expected | source ", + "component override applies | ['component': 'java-couchbase', 'span.kind': 'client'] | couchbase | _component_override", + "hostname wins over override | ['peer.hostname': 'host1', 'span.kind': 'client', 'component': 'my-http-client'] | host1 | peer.hostname " + }) + void shouldOverridePeerServiceValuesIfConfigured( + Map tags, String expected, String source) { + PeerServiceCalculator calculator = + new PeerServiceCalculator( + new NamingSchemaV0().peerService(), Config.get().getPeerServiceComponentOverrides()); + + TagMap unsafeTags = TagMap.fromMap(tags); + calculator.processTags(unsafeTags, null, link -> {}); + + assertEquals(expected, unsafeTags.get(Tags.PEER_SERVICE)); + assertEquals(source, unsafeTags.get(DDTags.PEER_SERVICE_SOURCE)); + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/PostProcessorChainTest.java b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/PostProcessorChainTest.java new file mode 100644 index 00000000000..e1b49cfd5b8 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/PostProcessorChainTest.java @@ -0,0 +1,91 @@ +package datadog.trace.core.tagprocessor; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.api.TagMap; +import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks; +import datadog.trace.core.DDSpanContext; +import datadog.trace.test.util.DDJavaSpecification; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class PostProcessorChainTest extends DDJavaSpecification { + + @Test + void chainWorks() { + TagsPostProcessor processor1 = + new TagsPostProcessor() { + @Override + public void processTags( + TagMap unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { + unsafeTags.put("key1", "processor1"); + unsafeTags.put("key2", "processor1"); + } + }; + TagsPostProcessor processor2 = + new TagsPostProcessor() { + @Override + public void processTags( + TagMap unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { + unsafeTags.put("key1", "processor2"); + } + }; + + PostProcessorChain chain = new PostProcessorChain(processor1, processor2); + + List links = new ArrayList<>(); + TagMap tags = TagMap.fromMap(linkedMap("key1", "overwrite", "key3", "unchanged")); + + chain.processTags(tags, null, link -> links.add(link)); + + Map expected = + linkedMap("key1", "processor2", "key2", "processor1", "key3", "unchanged"); + assertEquals(expected, tags); + assertTrue(links.isEmpty()); + } + + @Test + void processorCanHideTagsToNextOne() { + TagsPostProcessor processor1 = + new TagsPostProcessor() { + @Override + public void processTags( + TagMap unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { + unsafeTags.clear(); + unsafeTags.put("my", "tag"); + } + }; + TagsPostProcessor processor2 = + new TagsPostProcessor() { + @Override + public void processTags( + TagMap unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { + if (unsafeTags.containsKey("test")) { + unsafeTags.put("found", "true"); + } + } + }; + + PostProcessorChain chain = new PostProcessorChain(processor1, processor2); + + List links = new ArrayList<>(); + TagMap tags = TagMap.fromMap(linkedMap("test", "test")); + + chain.processTags(tags, null, link -> links.add(link)); + + assertEquals(linkedMap("my", "tag"), tags); + assertTrue(links.isEmpty()); + } + + private static LinkedHashMap linkedMap(Object... pairs) { + LinkedHashMap map = new LinkedHashMap<>(); + for (int i = 0; i < pairs.length; i += 2) { + map.put((String) pairs[i], pairs[i + 1]); + } + return map; + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/QueryObfuscatorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/QueryObfuscatorTest.java new file mode 100644 index 00000000000..59ce3696844 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/QueryObfuscatorTest.java @@ -0,0 +1,58 @@ +package datadog.trace.core.tagprocessor; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import datadog.trace.api.DDTags; +import datadog.trace.api.TagMap; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.test.util.DDJavaSpecification; +import java.util.LinkedHashMap; +import java.util.Map; +import org.tabletest.junit.TableTest; + +class QueryObfuscatorTest extends DDJavaSpecification { + + // Default pattern extended with 'email' to match the custom regexp test + private static final String CUSTOM_OBFUSCATION_PATTERN = + "(?i)(?:(?:\"|%22)?)(?:(?:old[-_]?|new[-_]?)?p(?:ass)?w(?:or)?d(?:1|2)?|pass(?:[-_]?phrase)?|email|secret|(?:api[-_]?|private[-_]?|public[-_]?|access[-_]?|secret[-_]?|app(?:lication)?[-_]?)key(?:[-_]?id)?|token|consumer[-_]?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)(?:(?:\\s|%20)*(?:=|%3D)[^&]+|(?:\"|%22)(?:\\s|%20)*(?::|%3A)(?:\\s|%20)*(?:\"|%22)(?:%2[^2]|%[^2]|[^\"%])+(?:\"|%22))|(?:bearer(?:\\s|%20)+[a-z0-9._\\-]+|token(?::|%3A)[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L](?:[\\w=-]|%3D)+\\.ey[I-L](?:[\\w=-]|%3D)+(?:\\.(?:[\\w.+/=-]|%3D|%2F|%2B)+)?|-{5}BEGIN(?:[a-z\\s]|%20)+PRIVATE(?:\\s|%20)KEY-{5}[^\\-]+-{5}END(?:[a-z\\s]|%20)+PRIVATE(?:\\s|%20)KEY(?:-{5})?(?:\\n|%0A)?|(?:ssh-(?:rsa|dss)|ecdsa-[a-z0-9]+-[a-z0-9]+)(?:\\s|%20|%09)+(?:[a-z0-9/.+]|%2F|%5C|%2B){100,}(?:=|%3D)*(?:(?:\\s|%20|%09)+[a-z0-9._-]+)?)"; + + @TableTest({ + "scenario | query | expectedQuery ", + "token | key1=val1&token=a0b21ce2-006f-4cc6-95d5-d7b550698482&key2=val2 | 'key1=val1&&key2=val2'", + "app keys | app_key=1111&application_key=2222 | '&' ", + "email | email=foo@bar.com | email=foo@bar.com " + }) + void tagsProcessing(String query, String expectedQuery) { + QueryObfuscator obfuscator = new QueryObfuscator(null); + + Map tags = new LinkedHashMap<>(); + tags.put(Tags.HTTP_URL, "http://site.com/index"); + tags.put(DDTags.HTTP_QUERY, query); + + TagMap unsafeTags = TagMap.fromMap(tags); + obfuscator.processTags(unsafeTags, null, link -> {}); + + assertEquals(expectedQuery, unsafeTags.get(DDTags.HTTP_QUERY)); + assertEquals("http://site.com/index?" + expectedQuery, unsafeTags.get(Tags.HTTP_URL)); + } + + @TableTest({ + "scenario | query | expectedQuery ", + "token | key1=val1&token=a0b21ce2-006f-4cc6-95d5-d7b550698482&key2=val2 | 'key1=val1&&key2=val2'", + "app keys | app_key=1111&application_key=2222 | '&' ", + "email | email=foo@bar.com | '' " + }) + void tagsProcessingWithCustomRegexpForEmail(String query, String expectedQuery) { + QueryObfuscator obfuscator = new QueryObfuscator(CUSTOM_OBFUSCATION_PATTERN); + + Map tags = new LinkedHashMap<>(); + tags.put(Tags.HTTP_URL, "http://site.com/index"); + tags.put(DDTags.HTTP_QUERY, query); + + TagMap unsafeTags = TagMap.fromMap(tags); + obfuscator.processTags(unsafeTags, null, link -> {}); + + assertEquals(expectedQuery, unsafeTags.get(DDTags.HTTP_QUERY)); + assertEquals("http://site.com/index?" + expectedQuery, unsafeTags.get(Tags.HTTP_URL)); + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/SpanPointersProcessorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/SpanPointersProcessorTest.java new file mode 100644 index 00000000000..0697f27d4f2 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/SpanPointersProcessorTest.java @@ -0,0 +1,59 @@ +package datadog.trace.core.tagprocessor; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.mockito.Mockito.mock; + +import datadog.trace.api.DDSpanId; +import datadog.trace.api.DDTraceId; +import datadog.trace.api.TagMap; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; +import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags; +import datadog.trace.bootstrap.instrumentation.api.SpanLink; +import datadog.trace.core.DDSpanContext; +import datadog.trace.test.util.DDJavaSpecification; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.tabletest.junit.TableTest; + +class SpanPointersProcessorTest extends DDJavaSpecification { + + @TableTest({ + "scenario | objectKey | eTag | expectedHash ", + "basic values | some-key.data | ab12ef34 | e721375466d4116ab551213fdea08413", + "non-ascii key | some-key.你好 | ab12ef34 | d1333a04b9928ab462b5c6cadfa401f4 ", + "multipart etag | some-key.data | ab12ef34-5 | 2b90dffc37ebc7bc610152c3dc72af9f" + }) + void spanPointersProcessorAddsCorrectLink(String objectKey, String eTag, String expectedHash) { + SpanPointersProcessor processor = new SpanPointersProcessor(); + + Map tagMap = new LinkedHashMap<>(); + tagMap.put(InstrumentationTags.AWS_BUCKET_NAME, "some-bucket"); + tagMap.put(InstrumentationTags.AWS_OBJECT_KEY, objectKey); + tagMap.put("s3.eTag", eTag); + + TagMap unsafeTags = TagMap.fromMap(tagMap); + DDSpanContext spanContext = mock(DDSpanContext.class); + List spanLinks = new ArrayList<>(); + + // Process the tags; the processor should remove 's3.eTag' and add one link + processor.processTags(unsafeTags, spanContext, link -> spanLinks.add(link)); + + // 1. s3.eTag was removed + assertFalse(unsafeTags.containsKey("s3.eTag")); + // 2. Exactly one link was added + assertEquals(1, spanLinks.size()); + // 3. Check link + AgentSpanLink link = spanLinks.get(0); + assertInstanceOf(SpanLink.class, link); + assertEquals(DDTraceId.ZERO, link.traceId()); + assertEquals(DDSpanId.ZERO, link.spanId()); + assertEquals(SpanPointersProcessor.S3_PTR_KIND, link.attributes().asMap().get("ptr.kind")); + assertEquals(SpanPointersProcessor.DOWN_DIRECTION, link.attributes().asMap().get("ptr.dir")); + assertEquals(expectedHash, link.attributes().asMap().get("ptr.hash")); + assertEquals(SpanPointersProcessor.LINK_KIND, link.attributes().asMap().get("link.kind")); + } +} diff --git a/utils/test-utils/src/main/java/datadog/trace/test/util/ThreadUtils.java b/utils/test-utils/src/main/java/datadog/trace/test/util/ThreadUtils.java index 1f22d9794f5..3dde0459573 100644 --- a/utils/test-utils/src/main/java/datadog/trace/test/util/ThreadUtils.java +++ b/utils/test-utils/src/main/java/datadog/trace/test/util/ThreadUtils.java @@ -33,6 +33,32 @@ public class ThreadUtils { * @return true if everything went well * @throws Throwable if anything went wrong */ + @FunctionalInterface + public interface ThrowingRunnable { + void run() throws Throwable; + } + + public static boolean runConcurrently( + final int concurrency, final int totalInvocations, final ThrowingRunnable runnable) + throws Throwable { + return runConcurrently( + concurrency, + totalInvocations, + new Closure(null) { + @Override + public Void call() { + try { + runnable.run(); + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException(t); + } + return null; + } + }); + } + public static boolean runConcurrently( final int concurrency, final int totalInvocations, final Closure closure) throws Throwable { From ea4927946c7347a8f90eb81131ff2c6cc60927c7 Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Thu, 4 Jun 2026 14:49:57 +0200 Subject: [PATCH 2/2] fix IBM exclusion --- .../trace/core/scopemanager/ScopeAndContinuationLayoutTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dd-trace-core/src/test/java/datadog/trace/core/scopemanager/ScopeAndContinuationLayoutTest.java b/dd-trace-core/src/test/java/datadog/trace/core/scopemanager/ScopeAndContinuationLayoutTest.java index 57c01bdc056..53d40ce391d 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/scopemanager/ScopeAndContinuationLayoutTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/scopemanager/ScopeAndContinuationLayoutTest.java @@ -13,7 +13,7 @@ class ScopeAndContinuationLayoutTest extends DDJavaSpecification { @BeforeAll static void assumeNotIbmJvm() { - assumeFalse(JavaVirtualMachine.isIbm()); + assumeFalse(JavaVirtualMachine.isJ9()); } @Test