From 76ff0027cb63a5a14a78b6ecfcb914f241c9169e Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Thu, 16 Apr 2026 17:43:45 +0200 Subject: [PATCH 1/3] Migrate dd-trace-core groovy files to java part 4 we migrate 3 tests: - CoreSpanBuilderTest - CoreTracerTest - DDSpanContextPropagationTagsTest --- .../trace/core/CoreSpanBuilderTest.groovy | 507 ------------- .../datadog/trace/core/CoreTracerTest.groovy | 670 ----------------- .../DDSpanContextPropagationTagsTest.groovy | 134 ---- .../trace/core/ControllableSampler.java | 20 + .../trace/core/CoreSpanBuilderTest.java | 568 +++++++++++++++ .../datadog/trace/core/CoreTracerTest.java | 679 ++++++++++++++++++ .../DDSpanContextPropagationTagsTest.java | 277 +++++++ 7 files changed, 1544 insertions(+), 1311 deletions(-) delete mode 100644 dd-trace-core/src/test/groovy/datadog/trace/core/CoreSpanBuilderTest.groovy delete mode 100644 dd-trace-core/src/test/groovy/datadog/trace/core/CoreTracerTest.groovy delete mode 100644 dd-trace-core/src/test/groovy/datadog/trace/core/DDSpanContextPropagationTagsTest.groovy create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/ControllableSampler.java create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/CoreTracerTest.java create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/DDSpanContextPropagationTagsTest.java diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/CoreSpanBuilderTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/CoreSpanBuilderTest.groovy deleted file mode 100644 index 2e533583ea2..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/CoreSpanBuilderTest.groovy +++ /dev/null @@ -1,507 +0,0 @@ -package datadog.trace.core - -import static datadog.trace.api.DDTags.DJM_ENABLED -import static datadog.trace.api.DDTags.DSM_ENABLED -import static datadog.trace.api.DDTags.PROFILING_ENABLED -import static datadog.trace.api.DDTags.SCHEMA_VERSION_TAG_KEY - -import datadog.trace.api.Config -import datadog.trace.api.DDSpanId -import datadog.trace.api.DDTraceId -import datadog.trace.api.TagMap -import datadog.trace.api.gateway.RequestContextSlot -import datadog.trace.api.naming.SpanNaming -import datadog.trace.api.sampling.PrioritySampling -import datadog.trace.bootstrap.instrumentation.api.AgentScope -import datadog.trace.api.datastreams.NoopPathwayContext -import datadog.trace.bootstrap.instrumentation.api.TagContext -import datadog.trace.common.writer.ListWriter -import datadog.trace.core.propagation.PropagationTags -import datadog.trace.core.propagation.ExtractedContext -import datadog.trace.core.test.DDCoreSpecification - -import static datadog.trace.api.DDTags.LANGUAGE_TAG_KEY -import static datadog.trace.api.DDTags.LANGUAGE_TAG_VALUE -import static datadog.trace.api.DDTags.ORIGIN_KEY -import static datadog.trace.api.DDTags.PID_TAG -import static datadog.trace.api.DDTags.RUNTIME_ID_TAG -import static datadog.trace.api.DDTags.THREAD_ID -import static datadog.trace.api.DDTags.THREAD_NAME -import static datadog.trace.api.TracePropagationStyle.DATADOG -import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.noopSpan -import static java.util.concurrent.TimeUnit.MILLISECONDS - -class CoreSpanBuilderTest extends DDCoreSpecification { - - def writer = new ListWriter() - def tracer = tracerBuilder().writer(writer).build() - - def cleanup() { - tracer.close() - } - - def "build simple span"() { - setup: - final DDSpan span = tracer.buildSpan("test", "op name").withServiceName("foo").start() - - expect: - span.operationName == "op name" - } - - def "build complex span"() { - setup: - def expectedName = "fakeName" - def tags = [ - "1": true, - "2": "fakeString", - "3": 42.0, - ] - - CoreTracer.CoreSpanBuilder builder = tracer - .buildSpan(expectedName) - .withServiceName("foo") - tags.each { - builder = builder.withTag(it.key, it.value) - } - - when: - DDSpan span = builder.start() - - then: - span.getOperationName() == expectedName - span.tags.subMap(tags.keySet()) == tags - - - when: - span = tracer.buildSpan("test", expectedName).withServiceName("foo").start() - - then: - span.getTags() == [ - (THREAD_NAME) : Thread.currentThread().getName(), - (THREAD_ID) : Thread.currentThread().getId(), - (RUNTIME_ID_TAG) : Config.get().getRuntimeId(), - (LANGUAGE_TAG_KEY) : LANGUAGE_TAG_VALUE, - (PID_TAG) : Config.get().getProcessId(), - (SCHEMA_VERSION_TAG_KEY) : SpanNaming.instance().version() - ] + productTags() - - when: - // with all custom fields provided - final String expectedResource = "fakeResource" - final String expectedService = "fakeService" - final String expectedType = "fakeType" - - span = - tracer - .buildSpan("test", expectedName) - .withServiceName("foo") - .withResourceName(expectedResource) - .withServiceName(expectedService) - .withErrorFlag() - .withSpanType(expectedType) - .start() - - final DDSpanContext context = span.context() - - then: - context.getResourceName() == expectedResource - context.getErrorFlag() - context.getServiceName() == expectedService - context.getSpanType() == expectedType - - context.getTag(THREAD_NAME) == Thread.currentThread().getName() - context.getTag(THREAD_ID) == Thread.currentThread().getId() - } - - def "setting #name should remove"() { - setup: - final DDSpan span = tracer.buildSpan("test", "op name") - .withTag(name, "tag value") - .withTag(name, value) - .start() - - expect: - span.tags[name] == null - - when: - span.setTag(name, "a tag") - - then: - span.tags[name] == "a tag" - - when: - span.setTag(name, (String) value) - - then: - span.tags[name] == null - - where: - name | value - "null.tag" | null - "empty.tag" | "" - } - - def "should build span timestamp in nano"() { - setup: - // time in micro - final long expectedTimestamp = 487517802L * 1000 * 1000L - final String expectedName = "fakeName" - - DDSpan span = - tracer - .buildSpan("test", expectedName) - .withServiceName("foo") - .withStartTimestamp(expectedTimestamp) - .start() - - expect: - // get return nano time - span.getStartTime() == expectedTimestamp * 1000L - - when: - // auto-timestamp in nanoseconds - def start = System.currentTimeMillis() - span = tracer.buildSpan("test", expectedName).withServiceName("foo").start() - def stop = System.currentTimeMillis() - - then: - // Give a range of +/- 5 millis - span.getStartTime() >= MILLISECONDS.toNanos(start - 1) - span.getStartTime() <= MILLISECONDS.toNanos(stop + 1) - } - - def "should link to parent span"() { - setup: - final long spanId = 1 - final DDTraceId traceId = DDTraceId.ONE - final long expectedParentId = spanId - - final DDSpanContext mockedContext = Mock() - 1 * mockedContext.getTraceId() >> traceId - 1 * mockedContext.getSpanId() >> spanId - _ * mockedContext.getServiceName() >> "foo" - 1 * mockedContext.getBaggageItems() >> [:] - 1 * mockedContext.getTraceCollector() >> tracer.traceCollectorFactory.create(DDTraceId.ONE) - _ * mockedContext.getPathwayContext() >> NoopPathwayContext.INSTANCE - - final String expectedName = "fakeName" - - final DDSpan span = - tracer - .buildSpan("test", expectedName) - .withServiceName("foo") - .asChildOf(mockedContext) - .start() - - final DDSpanContext actualContext = span.context() - - expect: - actualContext.getParentId() == expectedParentId - actualContext.getTraceId() == traceId - } - - def "should link to parent span implicitly"() { - setup: - final AgentScope parent = tracer.activateSpan(noopParent ? - noopSpan() : tracer.buildSpan("test", "parent").withServiceName("service").start()) - - final long expectedParentId = noopParent ? DDSpanId.ZERO : parent.span().context().getSpanId() - - final String expectedName = "fakeName" - - final DDSpan span = tracer - .buildSpan("test", expectedName) - .withServiceName(serviceName) - .start() - - final DDSpanContext actualContext = span.context() - - expect: - actualContext.getParentId() == expectedParentId - span.isTopLevel() == expectTopLevel - - cleanup: - parent.close() - - where: - noopParent | serviceName | expectTopLevel - false | "service" | false - true | "service" | true - false | "another service" | true - true | "another service" | true - } - - def "should inherit the DD parent attributes"() { - setup: - def expectedName = "fakeName" - def expectedParentServiceName = "fakeServiceName" - def expectedParentResourceName = "fakeResourceName" - def expectedParentType = "fakeType" - def expectedChildServiceName = "fakeServiceName-child" - def expectedChildResourceName = "fakeResourceName-child" - def expectedChildType = "fakeType-child" - def expectedBaggageItemKey = "fakeKey" - def expectedBaggageItemValue = "fakeValue" - - final DDSpan parent = - tracer - .buildSpan("test", expectedName) - .withServiceName("foo") - .withResourceName(expectedParentResourceName) - .withSpanType(expectedParentType) - .start() - - parent.setBaggageItem(expectedBaggageItemKey, expectedBaggageItemValue) - - // ServiceName and SpanType are always set by the parent if they are not present in the child - DDSpan span = - tracer - .buildSpan("test", expectedName) - .withServiceName(expectedParentServiceName) - .asChildOf(parent) - .start() - - expect: - span.getOperationName() == expectedName - span.getBaggageItem(expectedBaggageItemKey) == expectedBaggageItemValue - span.context().getServiceName() == expectedParentServiceName - span.context().getResourceName() == expectedName - span.context().getSpanType() == null - span.isTopLevel() // service names differ between parent and child - - when: - // ServiceName and SpanType are always overwritten by the child if they are present - span = - tracer - .buildSpan("test", expectedName) - .withServiceName(expectedChildServiceName) - .withResourceName(expectedChildResourceName) - .withSpanType(expectedChildType) - .asChildOf(parent) - .start() - - then: - span.getOperationName() == expectedName - span.getBaggageItem(expectedBaggageItemKey) == expectedBaggageItemValue - span.context().getServiceName() == expectedChildServiceName - span.context().getResourceName() == expectedChildResourceName - span.context().getSpanType() == expectedChildType - } - - def "should track all spans in trace"() { - setup: - List spans = [] - final int nbSamples = 10 - - // root (aka spans[0]) is the parent - // others are just for fun - - def root = tracer.buildSpan("test", "fake_O").withServiceName("foo").start() - - def lastSpan = root - - for (int i = 1; i <= 10; i++) { - lastSpan = tracer - .buildSpan("test", "fake_" + i) - .withServiceName("foo") - .asChildOf(lastSpan) - .start() - spans.add(lastSpan) - lastSpan.finish() - } - - expect: - root.context().getTraceCollector().rootSpan == root - root.context().getTraceCollector().size() == nbSamples - root.context().getTraceCollector().spans.containsAll(spans) - spans[(int) (Math.random() * nbSamples)].context.traceCollector.spans.containsAll(spans) - } - - def "ExtractedContext should populate new span details"() { - setup: - def thread = Thread.currentThread() - final DDSpan span = tracer.buildSpan("test", "op name") - .asChildOf(extractedContext).start() - - expect: - span.traceId == extractedContext.traceId - span.parentId == extractedContext.spanId - span.samplingPriority == extractedContext.samplingPriority - span.context().origin == extractedContext.origin - span.context().baggageItems == extractedContext.baggage - // check the extracted context has been copied into the span tags - for (Map.Entry tag : extractedContext.tags) { - span.context().tags.get(tag.getKey()) == tag.getValue() - } - span.getTag(THREAD_ID) == thread.id - span.getTag(THREAD_NAME) == thread.name - span.context().propagationTags.headerValue(PropagationTags.HeaderType.DATADOG) == extractedContext.propagationTags.headerValue(PropagationTags.HeaderType.DATADOG) - - where: - extractedContext | _ - new ExtractedContext(DDTraceId.ONE, 2, PrioritySampling.SAMPLER_DROP, null, 0, [:], [:], null, PropagationTags.factory().fromHeaderValue(PropagationTags.HeaderType.DATADOG, "_dd.p.dm=934086a686-4,_dd.p.anytag=value"), null, DATADOG) | _ - new ExtractedContext(DDTraceId.from(3), 4, PrioritySampling.SAMPLER_KEEP, "some-origin", 0, ["asdf": "qwer"], [(ORIGIN_KEY): "some-origin", "zxcv": "1234"], null, PropagationTags.factory().empty(), null, DATADOG) | _ - } - - def "build context from ExtractedContext with TRACE_PROPAGATION_BEHAVIOR_EXTRACT=restart"() { - setup: - injectSysConfig("trace.propagation.behavior.extract", "restart") - def extractedContext = new ExtractedContext(DDTraceId.ONE, 2, PrioritySampling.SAMPLER_DROP, null, 0, [:], [:], null, PropagationTags.factory().fromHeaderValue(PropagationTags.HeaderType.DATADOG, "_dd.p.dm=934086a686-4,_dd.p.anytag=value"), null, DATADOG) - final DDSpan span = tracer.buildSpan("test", "op name") - .asChildOf(extractedContext).start() - - expect: - span.traceId != extractedContext.traceId - span.parentId != extractedContext.spanId - span.samplingPriority() == PrioritySampling.UNSET - - def spanLinks = span.links - - assert spanLinks.size() == 1 - def link = spanLinks[0] - link.traceId() == extractedContext.traceId - link.spanId() == extractedContext.spanId - link.traceState() == extractedContext.propagationTags.headerValue(PropagationTags.HeaderType.W3C) - } - - def "build context from ExtractedContext with TRACE_PROPAGATION_BEHAVIOR_EXTRACT=ignore"() { - setup: - injectSysConfig("trace.propagation.behavior.extract", "ignore") - def extractedContext = new ExtractedContext(DDTraceId.ONE, 2, PrioritySampling.SAMPLER_DROP, null, 0, [:], [:], null, PropagationTags.factory().fromHeaderValue(PropagationTags.HeaderType.DATADOG, "_dd.p.dm=934086a686-4,_dd.p.anytag=value"), null, DATADOG) - final DDSpan span = tracer.buildSpan("test", "op name") - .asChildOf(extractedContext).start() - - expect: - span.traceId != extractedContext.traceId - span.parentId != extractedContext.spanId - span.samplingPriority() == PrioritySampling.UNSET - span.links.empty - } - - def "TagContext should populate default span details"() { - setup: - def thread = Thread.currentThread() - final DDSpan span = tracer.buildSpan("test", "op name").asChildOf(tagContext).start() - - expect: - span.traceId != DDTraceId.ZERO - span.parentId == DDSpanId.ZERO - span.samplingPriority == null - span.context().origin == tagContext.origin - span.context().baggageItems == [:] - span.context().tags == tagContext.tags + [ - (RUNTIME_ID_TAG) : Config.get().getRuntimeId(), - (LANGUAGE_TAG_KEY) : LANGUAGE_TAG_VALUE, - (THREAD_NAME) : thread.name, (THREAD_ID): thread.id, (PID_TAG): Config.get().getProcessId(), - (SCHEMA_VERSION_TAG_KEY) : SpanNaming.instance().version() - ] + productTags() - - where: - tagContext | _ - new TagContext(null, TagMap.fromMap([:])) | _ - new TagContext("some-origin", TagMap.fromMap(["asdf": "qwer"])) | _ - } - - def "global span tags populated on each span"() { - setup: - injectSysConfig("dd.trace.span.tags", tagString) - def customTracer = tracerBuilder().writer(writer).build() - def span = customTracer.buildSpan("test", "op name").withServiceName("foo").start() - - expect: - span.tags == tags + [ - (THREAD_NAME) : Thread.currentThread().getName(), - (THREAD_ID) : Thread.currentThread().getId(), - (RUNTIME_ID_TAG) : Config.get().getRuntimeId(), - (LANGUAGE_TAG_KEY) : LANGUAGE_TAG_VALUE, - (PID_TAG) : Config.get().getProcessId(), - (SCHEMA_VERSION_TAG_KEY) : SpanNaming.instance().version() - ] + productTags() - - cleanup: - customTracer.close() - - where: - tagString | tags - "" | [:] - "is:val:id" | [is: "val:id"] - "a:x" | [a: "x"] - "a:a,a:b,a:c" | [a: "c"] - "a:1,b-c:d" | [a: "1", "b-c": "d"] - } - - def "can overwrite RequestContext data with builder from empty"() { - when: - def span1 = tracer.startSpan("test", "span1") - - then: - span1.getRequestContext().getData(RequestContextSlot.APPSEC) == null - span1.getRequestContext().getData(RequestContextSlot.CI_VISIBILITY) == null - span1.getRequestContext().getData(RequestContextSlot.IAST) == null - - when: - def span2 = tracer.buildSpan("test", "span2") - .asChildOf(span1.context()) - .withRequestContextData(RequestContextSlot.APPSEC, "override") - .withRequestContextData(RequestContextSlot.CI_VISIBILITY, "override") - .withRequestContextData(RequestContextSlot.IAST, "override") - .start() - - then: - span2.getRequestContext().getData(RequestContextSlot.APPSEC) == "override" - span2.getRequestContext().getData(RequestContextSlot.CI_VISIBILITY) == "override" - span2.getRequestContext().getData(RequestContextSlot.IAST) == "override" - - cleanup: - span2.finish() - span1.finish() - } - - def "can overwrite RequestContext data with builder"() { - setup: - TagContext context = new TagContext() - .withCiVisibilityContextData("value") - .withRequestContextDataIast("value") - .withRequestContextDataAppSec("value") - def span1 = tracer.buildSpan("test", "span1").asChildOf(context).start() - - when: - def span2 = tracer.buildSpan("test", "span2").asChildOf(span1.context()).start() - - then: - span2.getRequestContext().getData(RequestContextSlot.APPSEC) == "value" - span2.getRequestContext().getData(RequestContextSlot.CI_VISIBILITY) == "value" - span2.getRequestContext().getData(RequestContextSlot.IAST) == "value" - - when: - def span3 = tracer.buildSpan("test", "span3") - .asChildOf(span2.context()) - .withRequestContextData(RequestContextSlot.APPSEC, "override") - .withRequestContextData(RequestContextSlot.CI_VISIBILITY, "override") - .withRequestContextData(RequestContextSlot.IAST, "override") - .start() - - then: - span3.getRequestContext().getData(RequestContextSlot.APPSEC) == "override" - span3.getRequestContext().getData(RequestContextSlot.CI_VISIBILITY) == "override" - span3.getRequestContext().getData(RequestContextSlot.IAST) == "override" - - cleanup: - span3.finish() - span2.finish() - span1.finish() - } - - def productTags() { - def productTags = [ - (PROFILING_ENABLED) : Config.get().isProfilingEnabled() ? 1 : 0 - ] - if (Config.get().isDataStreamsEnabled()) { - productTags[DSM_ENABLED] = 1 - } - if (Config.get().isDataJobsEnabled()) { - productTags[DJM_ENABLED] = 1 - } - return productTags - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/CoreTracerTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/CoreTracerTest.groovy deleted file mode 100644 index 97eac1fce6a..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/CoreTracerTest.groovy +++ /dev/null @@ -1,670 +0,0 @@ -package datadog.trace.core - -import datadog.communication.ddagent.DDAgentFeaturesDiscovery -import datadog.communication.ddagent.SharedCommunicationObjects -import datadog.metrics.api.Monitoring -import datadog.remoteconfig.ConfigurationPoller -import datadog.remoteconfig.Product -import datadog.remoteconfig.state.ParsedConfigKey -import datadog.remoteconfig.state.ProductListener -import datadog.trace.api.Config -import datadog.trace.api.DDTags -import datadog.trace.api.remoteconfig.ServiceNameCollector -import datadog.trace.api.sampling.PrioritySampling -import datadog.trace.api.sampling.SamplingMechanism -import datadog.trace.bootstrap.instrumentation.api.ServiceNameSources -import datadog.trace.common.sampling.AllSampler -import datadog.trace.common.sampling.PrioritySampler -import datadog.trace.common.sampling.RateByServiceTraceSampler -import datadog.trace.common.sampling.Sampler -import datadog.trace.common.writer.DDAgentWriter -import datadog.trace.common.writer.ListWriter -import datadog.trace.common.writer.LoggingWriter -import datadog.trace.core.tagprocessor.TagsPostProcessorFactory -import datadog.trace.core.test.DDCoreSpecification -import okhttp3.HttpUrl -import okhttp3.OkHttpClient -import spock.lang.Timeout - -import java.nio.charset.StandardCharsets -import java.util.concurrent.CopyOnWriteArrayList - -import static datadog.trace.api.config.GeneralConfig.ENV -import static datadog.trace.api.config.GeneralConfig.SERVICE_NAME -import static datadog.trace.api.config.GeneralConfig.VERSION -import static datadog.trace.api.config.TracerConfig.AGENT_UNIX_DOMAIN_SOCKET -import static datadog.trace.api.config.TracerConfig.BAGGAGE_MAPPING -import static datadog.trace.api.config.TracerConfig.HEADER_TAGS -import static datadog.trace.api.config.TracerConfig.PRIORITY_SAMPLING -import static datadog.trace.api.config.TracerConfig.SERVICE_MAPPING -import static datadog.trace.api.config.TracerConfig.SPAN_TAGS -import static datadog.trace.api.config.TracerConfig.WRITER_TYPE - -@Timeout(10) -class CoreTracerTest extends DDCoreSpecification { - - def "verify defaults on tracer"() { - when: - def tracer = CoreTracer.builder().build() - - then: - tracer.serviceName != "" - tracer.initialSampler instanceof RateByServiceTraceSampler - tracer.writer instanceof DDAgentWriter - - cleanup: - tracer.close() - } - - - - def "verify overriding sampler"() { - setup: - injectSysConfig(PRIORITY_SAMPLING, "false") - - when: - def tracer = tracerBuilder().build() - - then: - tracer.initialSampler instanceof AllSampler - - cleanup: - tracer.close() - } - - def "verify overriding writer"() { - setup: - injectSysConfig(WRITER_TYPE, "LoggingWriter") - - when: - def tracer = tracerBuilder().build() - - then: - tracer.writer instanceof LoggingWriter - - cleanup: - tracer.close() - } - - def "verify uds+windows"() { - setup: - System.setProperty("os.name", "Windows ME") - - when: - injectSysConfig(AGENT_UNIX_DOMAIN_SOCKET, uds) - - then: - Config.get().getAgentUnixDomainSocket() == uds - - where: - uds = "asdf" - } - - def "verify mapping configs on tracer"() { - setup: - injectSysConfig(SERVICE_MAPPING, mapString) - injectSysConfig(SPAN_TAGS, mapString) - injectSysConfig(HEADER_TAGS, mapString) - - when: - def tracer = tracerBuilder().build() - - then: - tracer.defaultSpanTags == map - tracer.captureTraceConfig().serviceMapping == map - - cleanup: - tracer.close() - - where: - mapString | map - "a:one, a:two, a:three" | [a: "three"] - "a:b,c:d,e:" | [a: "b", c: "d"] - } - - def "verify baggage mapping configs on tracer"() { - setup: - injectSysConfig(BAGGAGE_MAPPING, mapString) - - when: - def tracer = tracerBuilder().build() - - then: - tracer.captureTraceConfig().baggageMapping == map - - cleanup: - tracer.close() - - where: - mapString | map - "a:one, a:two, a:three" | [a: "three"] - "a:b,c:d,e:" | [a: "b", c: "d"] - } - - def "verify overriding host"() { - when: - injectSysConfig(key, value) - - then: - Config.get().getAgentHost() == value - - where: - key | value - "agent.host" | "somethingelse" - } - - def "verify overriding port"() { - when: - injectSysConfig(key, value) - - then: - Config.get().getAgentPort() == Integer.valueOf(value) - - where: - key | value - "agent.port" | "777" - "trace.agent.port" | "9999" - } - - def "Writer is instance of LoggingWriter when property set"() { - when: - injectSysConfig("writer.type", "LoggingWriter") - def tracer = tracerBuilder().build() - - then: - tracer.writer instanceof LoggingWriter - - cleanup: - tracer.close() - } - - def "Shares TraceCount with DDApi with #key = #value"() { - setup: - injectSysConfig(key, value) - final CoreTracer tracer = tracerBuilder().build() - - expect: - tracer.writer instanceof DDAgentWriter - - cleanup: - tracer.close() - - where: - key | value - PRIORITY_SAMPLING | "true" - PRIORITY_SAMPLING | "false" - } - - def "root tags are applied only to root spans"() { - setup: - def tracer = tracerBuilder().localRootSpanTags(['only_root': 'value']).build() - def root = tracer.buildSpan('my_root').start() - def child = tracer.buildSpan('my_child').asChildOf(root).start() - - expect: - root.context().tags.containsKey('only_root') - !child.context().tags.containsKey('only_root') - - cleanup: - child.finish() - root.finish() - tracer.close() - } - - def "priority sampling when span finishes"() { - given: - def writer = new ListWriter() - def tracer = tracerBuilder().writer(writer).build() - - when: - def span = tracer.buildSpan("operation").start() - span.finish() - writer.waitForTraces(1) - - then: - span.getSamplingPriority() == PrioritySampling.SAMPLER_KEEP - - cleanup: - tracer.close() - } - - def "priority sampling set when child span complete"() { - given: - def writer = new ListWriter() - def tracer = tracerBuilder().writer(writer).build() - - when: - def root = tracer.buildSpan("operation").start() - def child = tracer.buildSpan('my_child').asChildOf(root).start() - root.finish() - - then: - root.getSamplingPriority() == null - - when: - child.finish() - writer.waitForTraces(1) - - then: - root.getSamplingPriority() == PrioritySampling.SAMPLER_KEEP - child.getSamplingPriority() == root.getSamplingPriority() - - cleanup: - tracer.close() - } - - def "verify configuration polling"() { - setup: - def key = ParsedConfigKey.parse("datadog/2/APM_TRACING/config_overrides/config") - def poller = Mock(ConfigurationPoller) - def sco = new SharedCommunicationObjects( - agentHttpClient: Mock(OkHttpClient), - monitoring: Mock(Monitoring), - agentUrl: HttpUrl.get('https://example.com'), - featuresDiscovery: Mock(DDAgentFeaturesDiscovery), - configurationPoller: poller - ) - - def updater - - when: - def tracer = CoreTracer.builder() - .sharedCommunicationObjects(sco) - .pollForTracingConfiguration() - .build() - - then: - 1 * poller.addListener(Product.APM_TRACING, _ as ProductListener) >> { - updater = it[1] // capture config updater for further testing - } - and: - tracer.captureTraceConfig().serviceMapping == [:] - tracer.captureTraceConfig().requestHeaderTags == [:] - tracer.captureTraceConfig().responseHeaderTags == [:] - tracer.captureTraceConfig().traceSampleRate == null - - when: - updater.accept(key, ''' - { - "lib_config": - { - "tracing_service_mapping": - [{ - "from_key": "foobar", - "to_name": "bar" - }, { - "from_key": "snafu", - "to_name": "foo" - }] - , - "tracing_header_tags": - [{ - "header": "Cookie", - "tag_name": "" - }, { - "header": "Referer", - "tag_name": "http.referer" - }, { - "header": " Some.Header ", - "tag_name": "" - }, { - "header": "C!!!ont_____ent----tYp!/!e", - "tag_name": "" - }, { - "header": "this.header", - "tag_name": "whatever.the.user.wants.this.header" - }] - , - "tracing_sampling_rate": 0.5 - } - } - '''.getBytes(StandardCharsets.UTF_8), null) - updater.commit() - - then: - tracer.captureTraceConfig().serviceMapping == ['foobar':'bar', 'snafu':'foo'] - tracer.captureTraceConfig().requestHeaderTags == [ - 'cookie':'http.request.headers.cookie', - 'referer':'http.referer', - 'some.header':'http.request.headers.some_header', - 'c!!!ont_____ent----typ!/!e':'http.request.headers.c___ont_____ent----typ_/_e', - 'this.header':'whatever.the.user.wants.this.header' - ] - tracer.captureTraceConfig().responseHeaderTags == [ - 'cookie':'http.response.headers.cookie', - 'referer':'http.referer', - 'some.header':'http.response.headers.some_header', - 'c!!!ont_____ent----typ!/!e':'http.response.headers.c___ont_____ent----typ_/_e', - 'this.header':'whatever.the.user.wants.this.header' - ] - tracer.captureTraceConfig().traceSampleRate == 0.5 - - when: - updater.remove(key, null) - updater.commit() - - then: - tracer.captureTraceConfig().serviceMapping == [:] - tracer.captureTraceConfig().requestHeaderTags == [:] - tracer.captureTraceConfig().responseHeaderTags == [:] - tracer.captureTraceConfig().traceSampleRate == null - - cleanup: - tracer?.close() - } - - def "verify configuration polling with custom tags"() { - setup: - def key = ParsedConfigKey.parse("datadog/2/APM_TRACING/config_overrides/config") - def poller = Mock(ConfigurationPoller) - def sco = new SharedCommunicationObjects( - agentHttpClient: Mock(OkHttpClient), - monitoring: Mock(Monitoring), - agentUrl: HttpUrl.get('https://example.com'), - featuresDiscovery: Mock(DDAgentFeaturesDiscovery), - configurationPoller: poller - ) - - def updater - - when: - def tracer = CoreTracer.builder() - .sharedCommunicationObjects(sco) - .pollForTracingConfiguration() - .build() - - then: - 1 * poller.addListener(Product.APM_TRACING, _ as ProductListener) >> { - updater = it[1] // capture config updater for further testing - } - and: - tracer.captureTraceConfig().tracingTags == [:] - - when: - updater.accept(key, value.getBytes(StandardCharsets.UTF_8), null) - updater.commit() - - then: - tracer.captureTraceConfig().tracingTags == expectedValue - tracer.captureTraceConfig().mergedTracerTags == expectedValue - when: - updater.remove(key, null) - updater.commit() - - then: - tracer.captureTraceConfig().tracingTags == [:] - - cleanup: - tracer?.close() - - where: - value | expectedValue - """{"lib_config":{"tracing_tags": ["a:b", "c:d", "e:f"]}}""" | ["a":"b", "c":"d", "e":"f"] - """{"lib_config":{"tracing_tags": ["", "c:d", ""]}}""" | [ "c":"d"] - """{"lib_config":{"tracing_tags": [":b", "c:", "e:f"]}}""" | ["e":"f"] - """{"lib_config":{"tracing_tags": [":", "c:", "e:f"]}}""" | ["e":"f"] - """{"lib_config":{"tracing_tags": [":", "c:", ""]}}""" | [:] - """{"lib_config":{"tracing_tags": []}}""" | [:] - } - - def "verify configuration polling with tracing_enabled"() { - setup: - def key = ParsedConfigKey.parse("datadog/2/APM_TRACING/config_overrides/config") - def poller = Mock(ConfigurationPoller) - def sco = new SharedCommunicationObjects( - agentHttpClient: Mock(OkHttpClient), - monitoring: Mock(Monitoring), - agentUrl: HttpUrl.get('https://example.com'), - featuresDiscovery: Mock(DDAgentFeaturesDiscovery), - configurationPoller: poller - ) - - def updater - - when: - def tracer = CoreTracer.builder() - .sharedCommunicationObjects(sco) - .pollForTracingConfiguration() - .build() - - then: - - 1 * poller.addListener(Product.APM_TRACING, _ as ProductListener) >> { - updater = it[1] // capture config updater for further testing - } - and: - tracer.captureTraceConfig().traceEnabled == true - - when: - updater.accept(key, value.getBytes(StandardCharsets.UTF_8), null) - updater.commit() - - then: - tracer.captureTraceConfig().traceEnabled == expectedValue - - cleanup: - tracer?.close() - - where: - value | expectedValue - """{"lib_config":{"tracing_enabled": false } } """ | false - """{"lib_config":{"tracing_enabled": true } } """ | true - """{"action": "enable", "lib_config": {"tracing_sampling_rate": null, "log_injection_enabled": null, "tracing_header_tags": null, "runtime_metrics_enabled": null, "tracing_debug": null, "tracing_service_mapping": null, "tracing_sampling_rules": null, "span_sampling_rules": null, "data_streams_enabled": null, "tracing_enabled": false}}""" | false - } - - def "test local root service name override"() { - setup: - def tracer = tracerBuilder().writer(new ListWriter()).serviceName("test").build() - tracer.updatePreferredServiceName(preferred, preferred) - when: - def span = tracer.startSpan("", "test") - span.finish() - then: - span.serviceName == expected - and: - if (preferred != null) { - ServiceNameCollector.get().getServices().contains(preferred) - } - cleanup: - tracer?.close() - where: - preferred | expected - null | "test" - "some" | "some" - } - - def "test dd_version exists only if service == dd_service"() { - setup: - injectSysConfig(SERVICE_NAME, "dd_service_name") - injectSysConfig(VERSION, "1.0.0") - TagsPostProcessorFactory.withAddInternalTags(true) - def tracer = tracerBuilder().writer(new ListWriter()).build() - - when: - def span = tracer.buildSpan("def").withTag(SERVICE_NAME,"foo").start() - span.finish() - then: - span.getServiceName() == "foo" - span.getTags().containsKey("version") == false - - when: - def span2 = tracer.buildSpan("abc").start() - span2.finish() - then: - span2.getServiceName() == "dd_service_name" - span2.getTags()["version"]?.toString() == "1.0.0" - - cleanup: - tracer?.close() - } - - def "flushes on tracer close if configured to do so"() { - given: - def writer = new WriterWithExplicitFlush() - def tracer = tracerBuilder().writer(writer).flushOnClose(true).build() - - when: - tracer.buildSpan('my_span').start().finish() - tracer.close() - - then: - !writer.flushedTraces.empty - } - - def "verify no filtering of service/env when mismatched with DD_SERVICE/DD_ENV"() { - setup: - injectSysConfig(SERVICE_NAME, service) - injectSysConfig(ENV, env) - - def key = ParsedConfigKey.parse("datadog/2/APM_TRACING/config_overrides/config") - def poller = Mock(ConfigurationPoller) - def sco = new SharedCommunicationObjects( - agentHttpClient: Mock(OkHttpClient), - monitoring: Mock(Monitoring), - agentUrl: HttpUrl.get('https://example.com'), - featuresDiscovery: Mock(DDAgentFeaturesDiscovery), - configurationPoller: poller - ) - - def updater - - when: - def tracer = CoreTracer.builder() - .sharedCommunicationObjects(sco) - .pollForTracingConfiguration() - .build() - - then: - 1 * poller.addListener(Product.APM_TRACING, _ as ProductListener) >> { - updater = it[1] // capture config updater for further testing - } - and: - tracer.captureTraceConfig().serviceMapping == [:] - - when: - updater.accept(key, """ - { - "service_target": { - "service": "${targetService}", - "env": "${targetEnv}" - }, - "lib_config": - { - "tracing_service_mapping": - [{ - "from_key": "foobar", - "to_name": "bar" - }] - } - } - """.getBytes(StandardCharsets.UTF_8), null) - updater.commit() - - then: "configuration should be applied" - tracer.captureTraceConfig().serviceMapping == ["foobar":"bar"] - - cleanup: - tracer?.close() - - where: - service | env | targetService | targetEnv - "service" | "env" | "service_1" | "env" - "service" | "env" | "service" | "env_1" - "service" | "env" | "service_2" | "env_2" - } - - def "service name source is recorded when using two-parameter setServiceName"() { - setup: - def tracer = tracerBuilder().writer(new ListWriter()).build() - - when: - def span = tracer.buildSpan("operation").start() - span.setServiceName("custom-service", "my-integration") - def child = tracer.buildSpan("child").start() - child.finish() - span.finish() - - then: - [span, child].each { - assert span.getServiceName() == "custom-service" - assert span.getTag(DDTags.DD_SVC_SRC) == "my-integration" - } - cleanup: - tracer?.close() - } - - def "service name source is marked as manual when using one-parameter setServiceName"() { - setup: - def tracer = tracerBuilder().writer(new ListWriter()).build() - - when: - def span = tracer.buildSpan("operation").start() - span.setServiceName("custom-service", "my-integration") - span.setServiceName("another") - span.finish() - - then: - span.getServiceName() == "another" - span.getTag(DDTags.DD_SVC_SRC) == ServiceNameSources.MANUAL - cleanup: - tracer?.close() - } - - def "service name source is missing when not explicitly setting the service name"() { - setup: - def tracer = tracerBuilder().writer(new ListWriter()).build() - - when: - def span = tracer.buildSpan("operation").start() - span.finish() - - then: - span.getServiceName() == tracer.serviceName - span.getTag(DDTags.DD_SVC_SRC) == null - cleanup: - tracer?.close() - } -} - -class WriterWithExplicitFlush implements datadog.trace.common.writer.Writer { - final List> writtenTraces = new CopyOnWriteArrayList<>() - final List> flushedTraces = new CopyOnWriteArrayList<>() - - @Override - void write(List trace) { - writtenTraces.add(trace) - } - - @Override - void start() { - } - - @Override - boolean flush() { - flushedTraces.addAll(writtenTraces) - writtenTraces.clear() - return true - } - - @Override - void close() { - } - - @Override - void incrementDropCounts(int spanCount) { - } -} - -class ControllableSampler implements Sampler, PrioritySampler { - protected int nextSamplingPriority = PrioritySampling.SAMPLER_KEEP - - @Override - > void setSamplingPriority(T span) { - span.setSamplingPriority(nextSamplingPriority, SamplingMechanism.DEFAULT) - } - - @Override - > boolean sample(T span) { - return true - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/DDSpanContextPropagationTagsTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/DDSpanContextPropagationTagsTest.groovy deleted file mode 100644 index 1fc5dc342c9..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/DDSpanContextPropagationTagsTest.groovy +++ /dev/null @@ -1,134 +0,0 @@ -package datadog.trace.core - -import datadog.trace.api.DDTraceId -import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext -import datadog.trace.common.writer.ListWriter -import datadog.trace.core.propagation.ExtractedContext -import datadog.trace.core.propagation.PropagationTags -import datadog.trace.core.test.DDCoreSpecification - -import static datadog.trace.api.TracePropagationStyle.DATADOG -import static datadog.trace.api.sampling.PrioritySampling.* -import static datadog.trace.api.sampling.SamplingMechanism.* - -class DDSpanContextPropagationTagsTest extends DDCoreSpecification { - - def writer = new ListWriter() - def tracer - - def cleanup() { - tracer.close() - } - - def "update span PropagationTags #priority #header"() { - setup: - tracer = tracerBuilder().writer(writer).build() - def propagationTags = tracer.propagationTagsFactory.fromHeaderValue(PropagationTags.HeaderType.DATADOG, header) - def extracted = new ExtractedContext(DDTraceId.from(123), 456, priority, "789", propagationTags, DATADOG) - .withRequestContextDataAppSec("dummy") - def span = (DDSpan) tracer.buildSpan("top") - .asChildOf((AgentSpanContext) extracted) - .start() - def dd = span.context().getPropagationTags() - - when: - span.setSamplingPriority(newPriority, newMechanism) - span.getSamplingPriority() == newPriority - - then: - dd.headerValue(PropagationTags.HeaderType.DATADOG) == newHeader - dd.createTagMap() == tagMap - - where: - priority | header | newPriority | newMechanism | newHeader | tagMap - UNSET | "_dd.p.usr=123" | USER_KEEP | MANUAL | "_dd.p.dm=-4,_dd.p.usr=123" | ["_dd.p.dm": "-4", "_dd.p.usr": "123"] - UNSET | "_dd.p.usr=123" | SAMPLER_DROP | DEFAULT | "_dd.p.usr=123" | ["_dd.p.usr": "123"] - // decision has already been made, propagate as-is - SAMPLER_KEEP | "_dd.p.dm=9bf3439f2f-1,_dd.p.usr=123" | USER_KEEP | MANUAL | "_dd.p.dm=9bf3439f2f-1,_dd.p.usr=123" | ["_dd.p.dm": "9bf3439f2f-1", "_dd.p.usr": "123"] - SAMPLER_KEEP | "_dd.p.dm=9bf3439f2f-1,_dd.p.usr=123" | USER_DROP | MANUAL | "_dd.p.dm=9bf3439f2f-1,_dd.p.usr=123" | ["_dd.p.dm": "9bf3439f2f-1", "_dd.p.usr": "123"] - SAMPLER_KEEP | "_dd.p.usr=123" | USER_KEEP | MANUAL | "_dd.p.usr=123" | ["_dd.p.usr": "123"] - } - - def "update trace PropagationTags #priority #header"() { - setup: - tracer = tracerBuilder().writer(writer).build() - def propagationTags = tracer.propagationTagsFactory.fromHeaderValue(PropagationTags.HeaderType.DATADOG, header) - def extracted = new ExtractedContext(DDTraceId.from(123), 456, priority, "789", propagationTags, DATADOG) - .withRequestContextDataAppSec("dummy") - def rootSpan = (DDSpan) tracer.buildSpan("top") - .asChildOf((AgentSpanContext) extracted) - .start() - def ddRoot = rootSpan.context().getPropagationTags() - def span = (DDSpan) tracer.buildSpan("current").asChildOf(rootSpan).start() - - when: - span.setSamplingPriority(newPriority, newMechanism) - span.getSamplingPriority() == newPriority - - then: - ddRoot.headerValue(PropagationTags.HeaderType.DATADOG) == rootHeader - ddRoot.createTagMap() == rootTagMap - - where: - priority | header | newPriority | newMechanism | rootHeader | rootTagMap - UNSET | "_dd.p.usr=123" | USER_KEEP | MANUAL | "_dd.p.dm=-4,_dd.p.usr=123" | ["_dd.p.dm": "-4", "_dd.p.usr": "123"] - UNSET | "_dd.p.usr=123" | SAMPLER_DROP | DEFAULT | "_dd.p.usr=123" | ["_dd.p.usr": "123"] - // decision has already been made, propagate as-is - SAMPLER_KEEP | "_dd.p.dm=9bf3439f2f-1,_dd.p.usr=123" | USER_KEEP | MANUAL | "_dd.p.dm=9bf3439f2f-1,_dd.p.usr=123" | ["_dd.p.dm": "9bf3439f2f-1", "_dd.p.usr": "123"] - SAMPLER_KEEP | "_dd.p.dm=9bf3439f2f-1,_dd.p.usr=123" | USER_DROP | MANUAL | "_dd.p.dm=9bf3439f2f-1,_dd.p.usr=123" | ["_dd.p.dm": "9bf3439f2f-1", "_dd.p.usr": "123"] - SAMPLER_KEEP | "_dd.p.usr=123" | USER_KEEP | MANUAL | "_dd.p.usr=123" | ["_dd.p.usr": "123"] - } - - def "forceKeep span PropagationTags #priority #header"() { - setup: - tracer = tracerBuilder().writer(writer).build() - def propagationTags = tracer.propagationTagsFactory.fromHeaderValue(PropagationTags.HeaderType.DATADOG, header) - def extracted = new ExtractedContext(DDTraceId.from(123), 456, priority, "789", propagationTags, DATADOG) - .withRequestContextDataAppSec("dummy") - def span = (DDSpan) tracer.buildSpan("top") - .asChildOf((AgentSpanContext) extracted) - .start() - def dd = span.context().getPropagationTags() - - when: - span.context().forceKeep() - span.getSamplingPriority() == USER_KEEP - - then: - dd.headerValue(PropagationTags.HeaderType.DATADOG) == newHeader - dd.createTagMap() == tagMap - - where: - priority | header | newHeader | tagMap - UNSET | "_dd.p.usr=123" | "_dd.p.dm=-4,_dd.p.usr=123" | ["_dd.p.dm": "-4", "_dd.p.usr": "123"] - SAMPLER_KEEP | "_dd.p.dm=9bf3439f2f-1,_dd.p.usr=123" | "_dd.p.dm=-4,_dd.p.usr=123" | ["_dd.p.dm": "-4", "_dd.p.usr": "123"] - SAMPLER_KEEP | "_dd.p.usr=123" | "_dd.p.dm=-4,_dd.p.usr=123" | ["_dd.p.dm": "-4", "_dd.p.usr": "123"] - } - - def "forceKeep trace PropagationTags #priority #header"() { - setup: - tracer = tracerBuilder().writer(writer).build() - def propagationTags = tracer.propagationTagsFactory.fromHeaderValue(PropagationTags.HeaderType.DATADOG, header) - def extracted = new ExtractedContext(DDTraceId.from(123), 456, priority, "789", propagationTags, DATADOG) - .withRequestContextDataAppSec("dummy") - def rootSpan = (DDSpan) tracer.buildSpan("top") - .asChildOf((AgentSpanContext) extracted) - .start() - def ddRoot = rootSpan.context().getPropagationTags() - def span = (DDSpan) tracer.buildSpan("current").asChildOf(rootSpan).start() - - when: - span.context().forceKeep() - span.getSamplingPriority() == USER_KEEP - - then: - ddRoot.headerValue(PropagationTags.HeaderType.DATADOG) == rootHeader - ddRoot.createTagMap() == rootTagMap - - where: - priority | header | rootHeader | rootTagMap - UNSET | "_dd.p.usr=123" | "_dd.p.dm=-4,_dd.p.usr=123" | ["_dd.p.dm": "-4", "_dd.p.usr": "123"] - SAMPLER_KEEP | "_dd.p.dm=9bf3439f2f-1,_dd.p.usr=123" | "_dd.p.dm=-4,_dd.p.usr=123" | ["_dd.p.dm": "-4", "_dd.p.usr": "123"] - SAMPLER_KEEP | "_dd.p.usr=123" | "_dd.p.dm=-4,_dd.p.usr=123" | ["_dd.p.dm": "-4", "_dd.p.usr": "123"] - } -} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/ControllableSampler.java b/dd-trace-core/src/test/java/datadog/trace/core/ControllableSampler.java new file mode 100644 index 00000000000..4941fc0894a --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/ControllableSampler.java @@ -0,0 +1,20 @@ +package datadog.trace.core; + +import datadog.trace.api.sampling.PrioritySampling; +import datadog.trace.api.sampling.SamplingMechanism; +import datadog.trace.common.sampling.PrioritySampler; +import datadog.trace.common.sampling.Sampler; + +public class ControllableSampler implements Sampler, PrioritySampler { + protected int nextSamplingPriority = PrioritySampling.SAMPLER_KEEP; + + @Override + public > void setSamplingPriority(T span) { + span.setSamplingPriority(nextSamplingPriority, SamplingMechanism.DEFAULT); + } + + @Override + public > boolean sample(T span) { + return true; + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java b/dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java new file mode 100644 index 00000000000..1b606941658 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java @@ -0,0 +1,568 @@ +package datadog.trace.core; + +import static datadog.trace.api.DDTags.DJM_ENABLED; +import static datadog.trace.api.DDTags.DSM_ENABLED; +import static datadog.trace.api.DDTags.LANGUAGE_TAG_KEY; +import static datadog.trace.api.DDTags.LANGUAGE_TAG_VALUE; +import static datadog.trace.api.DDTags.ORIGIN_KEY; +import static datadog.trace.api.DDTags.PID_TAG; +import static datadog.trace.api.DDTags.PROFILING_ENABLED; +import static datadog.trace.api.DDTags.RUNTIME_ID_TAG; +import static datadog.trace.api.DDTags.SCHEMA_VERSION_TAG_KEY; +import static datadog.trace.api.DDTags.THREAD_ID; +import static datadog.trace.api.DDTags.THREAD_NAME; +import static datadog.trace.api.TracePropagationStyle.DATADOG; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.noopSpan; +import static datadog.trace.junit.utils.config.WithConfigExtension.injectSysConfig; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import datadog.trace.api.Config; +import datadog.trace.api.DDSpanId; +import datadog.trace.api.DDTraceId; +import datadog.trace.api.TagMap; +import datadog.trace.api.datastreams.NoopPathwayContext; +import datadog.trace.api.gateway.RequestContextSlot; +import datadog.trace.api.naming.SpanNaming; +import datadog.trace.api.sampling.PrioritySampling; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import datadog.trace.bootstrap.instrumentation.api.TagContext; +import datadog.trace.common.writer.ListWriter; +import datadog.trace.core.propagation.ExtractedContext; +import datadog.trace.core.propagation.PropagationTags; +import datadog.trace.junit.utils.config.WithConfig; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +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.tabletest.junit.TableTest; + +public class CoreSpanBuilderTest extends DDCoreJavaSpecification { + + private ListWriter writer; + private CoreTracer tracer; + + @BeforeEach + void setup() { + writer = new ListWriter(); + tracer = tracerBuilder().writer(writer).build(); + } + + @Test + void buildSimpleSpan() { + DDSpan span = (DDSpan) tracer.buildSpan("test", "op name").withServiceName("foo").start(); + assertEquals("op name", span.getOperationName()); + } + + @Test + void buildComplexSpan() { + String expectedName = "fakeName"; + Map tags = new HashMap<>(); + tags.put("1", true); + tags.put("2", "fakeString"); + tags.put("3", 42.0); + + AgentTracer.SpanBuilder builder = tracer.buildSpan(expectedName).withServiceName("foo"); + for (Map.Entry entry : tags.entrySet()) { + builder = builder.withTag(entry.getKey(), entry.getValue()); + } + + DDSpan span = (DDSpan) builder.start(); + + assertEquals(expectedName, span.getOperationName()); + for (Map.Entry entry : tags.entrySet()) { + assertEquals(entry.getValue(), span.getTags().get(entry.getKey())); + } + + span = (DDSpan) tracer.buildSpan("test", expectedName).withServiceName("foo").start(); + + Map expectedTags = new HashMap<>(); + expectedTags.put(THREAD_NAME, Thread.currentThread().getName()); + expectedTags.put(THREAD_ID, Thread.currentThread().getId()); + expectedTags.put(RUNTIME_ID_TAG, Config.get().getRuntimeId()); + expectedTags.put(LANGUAGE_TAG_KEY, LANGUAGE_TAG_VALUE); + expectedTags.put(PID_TAG, Config.get().getProcessId()); + expectedTags.put(SCHEMA_VERSION_TAG_KEY, SpanNaming.instance().version()); + expectedTags.putAll(productTags()); + assertEquals(expectedTags, span.getTags()); + + String expectedResource = "fakeResource"; + String expectedService = "fakeService"; + String expectedType = "fakeType"; + + span = + (DDSpan) + tracer + .buildSpan("test", expectedName) + .withServiceName("foo") + .withResourceName(expectedResource) + .withServiceName(expectedService) + .withErrorFlag() + .withSpanType(expectedType) + .start(); + + DDSpanContext context = span.context(); + + assertEquals(expectedResource, context.getResourceName()); + assertTrue(context.getErrorFlag()); + assertEquals(expectedService, context.getServiceName()); + assertEquals(expectedType, context.getSpanType()); + assertEquals(Thread.currentThread().getName(), context.getTag(THREAD_NAME)); + assertEquals(Thread.currentThread().getId(), context.getTag(THREAD_ID)); + } + + @TableTest({ + "scenario | name | value", + "null tag | null.tag | ", + "empty tag | empty.tag | '' " + }) + void settingNameShouldRemove(String name, String value) { + DDSpan span = + (DDSpan) + tracer + .buildSpan("test", "op name") + .withTag(name, "tag value") + .withTag(name, value) + .start(); + + assertNull(span.getTags().get(name)); + + span.setTag(name, "a tag"); + assertEquals("a tag", span.getTags().get(name)); + + span.setTag(name, value); + assertNull(span.getTags().get(name)); + } + + @Test + void shouldBuildSpanTimestampInNano() { + long expectedTimestamp = 487517802L * 1000 * 1000L; + String expectedName = "fakeName"; + + DDSpan span = + (DDSpan) + tracer + .buildSpan("test", expectedName) + .withServiceName("foo") + .withStartTimestamp(expectedTimestamp) + .start(); + + assertEquals(expectedTimestamp * 1000L, span.getStartTime()); + + long start = System.currentTimeMillis(); + span = (DDSpan) tracer.buildSpan("test", expectedName).withServiceName("foo").start(); + long stop = System.currentTimeMillis(); + + assertTrue(span.getStartTime() >= MILLISECONDS.toNanos(start - 1)); + assertTrue(span.getStartTime() <= MILLISECONDS.toNanos(stop + 1)); + } + + @Test + void shouldLinkToParentSpan() { + long spanId = 1L; + DDTraceId traceId = DDTraceId.ONE; + long expectedParentId = spanId; + + DDSpanContext mockedContext = mock(DDSpanContext.class); + when(mockedContext.getTraceId()).thenReturn(traceId); + when(mockedContext.getSpanId()).thenReturn(spanId); + when(mockedContext.getServiceName()).thenReturn("foo"); + when(mockedContext.getBaggageItems()).thenReturn(Collections.emptyMap()); + when(mockedContext.getTraceCollector()).thenReturn(tracer.createTraceCollector(DDTraceId.ONE)); + when(mockedContext.getPathwayContext()).thenReturn(NoopPathwayContext.INSTANCE); + + DDSpan span = + (DDSpan) + tracer + .buildSpan("test", "fakeName") + .withServiceName("foo") + .asChildOf(mockedContext) + .start(); + + DDSpanContext actualContext = span.context(); + assertEquals(expectedParentId, actualContext.getParentId()); + assertEquals(traceId, actualContext.getTraceId()); + } + + @TableTest({ + "scenario | noopParent | serviceName | expectTopLevel", + "same service, no noop | false | service | false ", + "noop parent, same service | true | service | true ", + "diff service, no noop | false | another service | true ", + "noop parent, diff service | true | another service | true " + }) + void shouldLinkToParentSpanImplicitly( + boolean noopParent, String serviceName, boolean expectTopLevel) { + try (AgentScope parent = + tracer.activateSpan( + noopParent + ? noopSpan() + : tracer.buildSpan("test", "parent").withServiceName("service").start())) { + long expectedParentId = noopParent ? DDSpanId.ZERO : parent.span().context().getSpanId(); + + DDSpan span = + (DDSpan) tracer.buildSpan("test", "fakeName").withServiceName(serviceName).start(); + + DDSpanContext actualContext = span.context(); + assertEquals(expectedParentId, actualContext.getParentId()); + assertEquals(expectTopLevel, span.isTopLevel()); + } + } + + @Test + void shouldInheritTheDDParentAttributes() { + String expectedName = "fakeName"; + String expectedParentResourceName = "fakeResourceName"; + String expectedParentType = "fakeType"; + String expectedParentServiceName = "fakeServiceName"; + String expectedChildServiceName = "fakeServiceName-child"; + String expectedChildResourceName = "fakeResourceName-child"; + String expectedChildType = "fakeType-child"; + String expectedBaggageItemKey = "fakeKey"; + String expectedBaggageItemValue = "fakeValue"; + + DDSpan parent = + (DDSpan) + tracer + .buildSpan("test", expectedName) + .withServiceName("foo") + .withResourceName(expectedParentResourceName) + .withSpanType(expectedParentType) + .start(); + + parent.setBaggageItem(expectedBaggageItemKey, expectedBaggageItemValue); + + // ServiceName and SpanType are always set by the parent if they are not present in the child + DDSpan span = + (DDSpan) + tracer + .buildSpan("test", expectedName) + .withServiceName(expectedParentServiceName) + .asChildOf(parent) + .start(); + + assertEquals(expectedName, span.getOperationName()); + assertEquals(expectedBaggageItemValue, span.getBaggageItem(expectedBaggageItemKey)); + assertEquals(expectedParentServiceName, span.context().getServiceName()); + assertEquals(expectedName, span.context().getResourceName()); + assertNull(span.context().getSpanType()); + assertTrue(span.isTopLevel()); // service names differ between parent and child + + // ServiceName and SpanType are always overwritten by the child if they are present + span = + (DDSpan) + tracer + .buildSpan("test", expectedName) + .withServiceName(expectedChildServiceName) + .withResourceName(expectedChildResourceName) + .withSpanType(expectedChildType) + .asChildOf(parent) + .start(); + + assertEquals(expectedName, span.getOperationName()); + assertEquals(expectedBaggageItemValue, span.getBaggageItem(expectedBaggageItemKey)); + assertEquals(expectedChildServiceName, span.context().getServiceName()); + assertEquals(expectedChildResourceName, span.context().getResourceName()); + assertEquals(expectedChildType, span.context().getSpanType()); + } + + @Test + void shouldTrackAllSpansInTrace() { + int nbSamples = 10; + List spans = new ArrayList<>(); + + DDSpan root = (DDSpan) tracer.buildSpan("test", "fake_O").withServiceName("foo").start(); + DDSpan lastSpan = root; + + for (int i = 1; i <= 10; i++) { + lastSpan = + (DDSpan) + tracer + .buildSpan("test", "fake_" + i) + .withServiceName("foo") + .asChildOf(lastSpan) + .start(); + spans.add(lastSpan); + lastSpan.finish(); + } + + PendingTrace traceCollector = (PendingTrace) root.context().getTraceCollector(); + assertEquals(root, traceCollector.getRootSpan()); + assertEquals(nbSamples, traceCollector.size()); + assertTrue(traceCollector.getSpans().containsAll(spans)); + DDSpan randomSpan = spans.get((int) (Math.random() * nbSamples)); + assertTrue( + ((PendingTrace) randomSpan.context().getTraceCollector()).getSpans().containsAll(spans)); + } + + static Stream extractedContextShouldPopulateNewSpanDetailsArguments() { + return Stream.of( + new ExtractedContext( + DDTraceId.ONE, + 2, + PrioritySampling.SAMPLER_DROP, + null, + 0, + Collections.emptyMap(), + Collections.emptyMap(), + null, + PropagationTags.factory() + .fromHeaderValue( + PropagationTags.HeaderType.DATADOG, "_dd.p.dm=934086a686-4,_dd.p.anytag=value"), + null, + DATADOG), + new ExtractedContext( + DDTraceId.from(3), + 4, + PrioritySampling.SAMPLER_KEEP, + "some-origin", + 0, + Collections.singletonMap("asdf", "qwer"), + buildTagsMap(ORIGIN_KEY, "some-origin", "zxcv", "1234"), + null, + PropagationTags.factory().empty(), + null, + DATADOG)); + } + + @ParameterizedTest + @MethodSource("extractedContextShouldPopulateNewSpanDetailsArguments") + void extractedContextShouldPopulateNewSpanDetails(ExtractedContext extractedContext) { + Thread thread = Thread.currentThread(); + DDSpan span = (DDSpan) tracer.buildSpan("test", "op name").asChildOf(extractedContext).start(); + + assertEquals(extractedContext.getTraceId(), span.getTraceId()); + assertEquals(extractedContext.getSpanId(), span.getParentId()); + assertEquals(extractedContext.getSamplingPriority(), (int) span.getSamplingPriority()); + assertEquals(extractedContext.getOrigin(), span.context().getOrigin()); + assertEquals(extractedContext.getBaggage(), span.context().getBaggageItems()); + assertEquals(thread.getId(), span.getTag(THREAD_ID)); + assertEquals(thread.getName(), span.getTag(THREAD_NAME)); + assertEquals( + extractedContext.getPropagationTags().headerValue(PropagationTags.HeaderType.DATADOG), + span.context().getPropagationTags().headerValue(PropagationTags.HeaderType.DATADOG)); + } + + @Test + @WithConfig(key = "trace.propagation.behavior.extract", value = "restart") + void buildContextFromExtractedContextWithRestartBehavior() { + ExtractedContext extractedContext = + new ExtractedContext( + DDTraceId.ONE, + 2, + PrioritySampling.SAMPLER_DROP, + null, + 0, + Collections.emptyMap(), + Collections.emptyMap(), + null, + PropagationTags.factory() + .fromHeaderValue( + PropagationTags.HeaderType.DATADOG, "_dd.p.dm=934086a686-4,_dd.p.anytag=value"), + null, + DATADOG); + DDSpan span = (DDSpan) tracer.buildSpan("test", "op name").asChildOf(extractedContext).start(); + + assertNotEquals(extractedContext.getTraceId(), span.getTraceId()); + assertNotEquals(extractedContext.getSpanId(), span.getParentId()); + assertEquals(PrioritySampling.UNSET, span.samplingPriority()); + + List spanLinks = span.getLinks(); + assertEquals(1, spanLinks.size()); + AgentSpanLink link = spanLinks.get(0); + assertEquals(extractedContext.getTraceId(), link.traceId()); + assertEquals(extractedContext.getSpanId(), link.spanId()); + assertEquals( + extractedContext.getPropagationTags().headerValue(PropagationTags.HeaderType.W3C), + link.traceState()); + } + + @Test + @WithConfig(key = "trace.propagation.behavior.extract", value = "ignore") + void buildContextFromExtractedContextWithIgnoreBehavior() { + ExtractedContext extractedContext = + new ExtractedContext( + DDTraceId.ONE, + 2, + PrioritySampling.SAMPLER_DROP, + null, + 0, + Collections.emptyMap(), + Collections.emptyMap(), + null, + PropagationTags.factory() + .fromHeaderValue( + PropagationTags.HeaderType.DATADOG, "_dd.p.dm=934086a686-4,_dd.p.anytag=value"), + null, + DATADOG); + DDSpan span = (DDSpan) tracer.buildSpan("test", "op name").asChildOf(extractedContext).start(); + + assertNotEquals(extractedContext.getTraceId(), span.getTraceId()); + assertNotEquals(extractedContext.getSpanId(), span.getParentId()); + assertEquals(PrioritySampling.UNSET, span.samplingPriority()); + assertTrue(span.getLinks().isEmpty()); + } + + static Stream tagContextShouldPopulateDefaultSpanDetailsArguments() { + return Stream.of( + new TagContext(null, TagMap.fromMap(Collections.emptyMap())), + new TagContext( + "some-origin", + TagMap.fromMap(Collections.singletonMap("asdf", "qwer")))); + } + + @ParameterizedTest + @MethodSource("tagContextShouldPopulateDefaultSpanDetailsArguments") + void tagContextShouldPopulateDefaultSpanDetails(TagContext tagContext) { + Thread thread = Thread.currentThread(); + DDSpan span = (DDSpan) tracer.buildSpan("test", "op name").asChildOf(tagContext).start(); + + assertNotEquals(DDTraceId.ZERO, span.getTraceId()); + assertEquals(DDSpanId.ZERO, span.getParentId()); + assertNull(span.getSamplingPriority()); + assertEquals(tagContext.getOrigin(), span.context().getOrigin()); + assertEquals(Collections.emptyMap(), span.context().getBaggageItems()); + + Map expectedTags = new HashMap<>(); + if (tagContext.getTags() != null) { + expectedTags.putAll(tagContext.getTags()); + } + expectedTags.put(RUNTIME_ID_TAG, Config.get().getRuntimeId()); + expectedTags.put(LANGUAGE_TAG_KEY, LANGUAGE_TAG_VALUE); + expectedTags.put(THREAD_NAME, thread.getName()); + expectedTags.put(THREAD_ID, thread.getId()); + expectedTags.put(PID_TAG, Config.get().getProcessId()); + expectedTags.put(SCHEMA_VERSION_TAG_KEY, SpanNaming.instance().version()); + expectedTags.putAll(productTags()); + assertEquals(expectedTags, span.context().getTags()); + } + + static Stream globalSpanTagsPopulatedOnEachSpanArguments() { + return Stream.of( + arguments("", Collections.emptyMap()), + arguments("is:val:id", Collections.singletonMap("is", "val:id")), + arguments("a:x", Collections.singletonMap("a", "x")), + arguments("a:a,a:b,a:c", Collections.singletonMap("a", "c")), + arguments("a:1,b-c:d", buildStringMap("a", "1", "b-c", "d"))); + } + + @ParameterizedTest + @MethodSource("globalSpanTagsPopulatedOnEachSpanArguments") + void globalSpanTagsPopulatedOnEachSpan(String tagString, Map tags) { + injectSysConfig("dd.trace.span.tags", tagString); + CoreTracer customTracer = tracerBuilder().writer(writer).build(); + DDSpan span = (DDSpan) customTracer.buildSpan("test", "op name").withServiceName("foo").start(); + + Map expectedTags = new HashMap<>(tags); + expectedTags.put(THREAD_NAME, Thread.currentThread().getName()); + expectedTags.put(THREAD_ID, Thread.currentThread().getId()); + expectedTags.put(RUNTIME_ID_TAG, Config.get().getRuntimeId()); + expectedTags.put(LANGUAGE_TAG_KEY, LANGUAGE_TAG_VALUE); + expectedTags.put(PID_TAG, Config.get().getProcessId()); + expectedTags.put(SCHEMA_VERSION_TAG_KEY, SpanNaming.instance().version()); + expectedTags.putAll(productTags()); + assertEquals(expectedTags, span.getTags()); + } + + @Test + void canOverwriteRequestContextDataWithBuilderFromEmpty() { + AgentSpan span1 = tracer.startSpan("test", "span1"); + + assertNull(span1.getRequestContext().getData(RequestContextSlot.APPSEC)); + assertNull(span1.getRequestContext().getData(RequestContextSlot.CI_VISIBILITY)); + assertNull(span1.getRequestContext().getData(RequestContextSlot.IAST)); + + AgentSpan span2 = + tracer + .buildSpan("test", "span2") + .asChildOf(span1.context()) + .withRequestContextData(RequestContextSlot.APPSEC, "override") + .withRequestContextData(RequestContextSlot.CI_VISIBILITY, "override") + .withRequestContextData(RequestContextSlot.IAST, "override") + .start(); + + assertEquals("override", span2.getRequestContext().getData(RequestContextSlot.APPSEC)); + assertEquals("override", span2.getRequestContext().getData(RequestContextSlot.CI_VISIBILITY)); + assertEquals("override", span2.getRequestContext().getData(RequestContextSlot.IAST)); + + span2.finish(); + span1.finish(); + } + + @Test + void canOverwriteRequestContextDataWithBuilder() { + TagContext context = + new TagContext() + .withCiVisibilityContextData("value") + .withRequestContextDataIast("value") + .withRequestContextDataAppSec("value"); + AgentSpan span1 = tracer.buildSpan("test", "span1").asChildOf(context).start(); + + AgentSpan span2 = tracer.buildSpan("test", "span2").asChildOf(span1.context()).start(); + + assertEquals("value", span2.getRequestContext().getData(RequestContextSlot.APPSEC)); + assertEquals("value", span2.getRequestContext().getData(RequestContextSlot.CI_VISIBILITY)); + assertEquals("value", span2.getRequestContext().getData(RequestContextSlot.IAST)); + + AgentSpan span3 = + tracer + .buildSpan("test", "span3") + .asChildOf(span2.context()) + .withRequestContextData(RequestContextSlot.APPSEC, "override") + .withRequestContextData(RequestContextSlot.CI_VISIBILITY, "override") + .withRequestContextData(RequestContextSlot.IAST, "override") + .start(); + + assertEquals("override", span3.getRequestContext().getData(RequestContextSlot.APPSEC)); + assertEquals("override", span3.getRequestContext().getData(RequestContextSlot.CI_VISIBILITY)); + assertEquals("override", span3.getRequestContext().getData(RequestContextSlot.IAST)); + + span3.finish(); + span2.finish(); + span1.finish(); + } + + private Map productTags() { + Map productTags = new HashMap<>(); + productTags.put(PROFILING_ENABLED, Config.get().isProfilingEnabled() ? 1 : 0); + if (Config.get().isDataStreamsEnabled()) { + productTags.put(DSM_ENABLED, 1); + } + if (Config.get().isDataJobsEnabled()) { + productTags.put(DJM_ENABLED, 1); + } + return productTags; + } + + private static Map buildTagsMap(String... keyValues) { + Map map = new HashMap<>(); + for (int i = 0; i < keyValues.length; i += 2) { + map.put(keyValues[i], keyValues[i + 1]); + } + return map; + } + + private static Map buildStringMap(String... keyValues) { + Map map = new HashMap<>(); + for (int i = 0; i < keyValues.length; i += 2) { + map.put(keyValues[i], keyValues[i + 1]); + } + return map; + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/CoreTracerTest.java b/dd-trace-core/src/test/java/datadog/trace/core/CoreTracerTest.java new file mode 100644 index 00000000000..4cf5b230604 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/CoreTracerTest.java @@ -0,0 +1,679 @@ +package datadog.trace.core; + +import static datadog.trace.junit.utils.config.WithConfigExtension.injectSysConfig; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import datadog.communication.ddagent.DDAgentFeaturesDiscovery; +import datadog.communication.ddagent.SharedCommunicationObjects; +import datadog.environment.JavaVirtualMachine; +import datadog.metrics.api.Monitoring; +import datadog.remoteconfig.ConfigurationPoller; +import datadog.remoteconfig.Product; +import datadog.remoteconfig.state.ParsedConfigKey; +import datadog.remoteconfig.state.ProductListener; +import datadog.trace.api.Config; +import datadog.trace.api.DDTags; +import datadog.trace.api.config.GeneralConfig; +import datadog.trace.api.config.TracerConfig; +import datadog.trace.api.remoteconfig.ServiceNameCollector; +import datadog.trace.api.sampling.PrioritySampling; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.ServiceNameSources; +import datadog.trace.common.sampling.AllSampler; +import datadog.trace.common.sampling.RateByServiceTraceSampler; +import datadog.trace.common.writer.DDAgentWriter; +import datadog.trace.common.writer.ListWriter; +import datadog.trace.common.writer.LoggingWriter; +import datadog.trace.core.CoreTracer.ConfigSnapshot; +import datadog.trace.core.tagprocessor.TagsPostProcessorFactory; +import datadog.trace.junit.utils.config.WithConfig; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.tabletest.junit.TableTest; + +@Timeout(value = 10, unit = TimeUnit.SECONDS) +public class CoreTracerTest extends DDCoreJavaSpecification { + + @BeforeAll + static void checkJvm() { + Assumptions.assumeFalse( + JavaVirtualMachine.isOracleJDK8(), + "Oracle JDK 1.8 did not merge the fix in JDK-8058322, leading to the JVM failing to" + + " correctly extract method parameters without args, when the code is compiled on a" + + " later JDK (targeting 8). This can manifest when creating mocks."); + } + + @Test + void verifyDefaultsOnTracer() { + CoreTracer tracer = CoreTracer.builder().build(); + try { + assertFalse(tracer.serviceName.isEmpty()); + assertInstanceOf(RateByServiceTraceSampler.class, tracer.initialSampler); + assertInstanceOf(DDAgentWriter.class, tracer.writer); + } finally { + tracer.close(); + } + } + + @Test + @WithConfig(key = TracerConfig.PRIORITY_SAMPLING, value = "false") + void verifyOverridingSampler() { + CoreTracer tracer = tracerBuilder().build(); + try { + assertInstanceOf(AllSampler.class, tracer.initialSampler); + } finally { + tracer.close(); + } + } + + @Test + @WithConfig(key = TracerConfig.WRITER_TYPE, value = "LoggingWriter") + void verifyOverridingWriter() { + CoreTracer tracer = tracerBuilder().build(); + try { + assertInstanceOf(LoggingWriter.class, tracer.writer); + } finally { + tracer.close(); + } + } + + @Test + @WithConfig(key = TracerConfig.AGENT_UNIX_DOMAIN_SOCKET, value = "asdf") + void verifyUdsWindows() { + String originalOsName = System.getProperty("os.name"); + try { + System.setProperty("os.name", "Windows ME"); + assertEquals("asdf", Config.get().getAgentUnixDomainSocket()); + } finally { + if (originalOsName != null) { + System.setProperty("os.name", originalOsName); + } else { + System.clearProperty("os.name"); + } + } + } + + @ParameterizedTest + @MethodSource("verifyMappingConfigsOnTracerArguments") + void verifyMappingConfigsOnTracer(String scenario, String mapString, Map map) { + injectSysConfig(TracerConfig.SERVICE_MAPPING, mapString); + injectSysConfig(TracerConfig.SPAN_TAGS, mapString); + injectSysConfig(TracerConfig.HEADER_TAGS, mapString); + CoreTracer tracer = tracerBuilder().build(); + try { + ConfigSnapshot config = tracer.captureTraceConfig(); + assertEquals(map, config.mergedTracerTags); + assertEquals(map, config.getServiceMapping()); + } finally { + tracer.close(); + } + } + + static Stream verifyMappingConfigsOnTracerArguments() { + return Stream.of( + new Object[] {"duplicate keys", "a:one, a:two, a:three", buildStringMap("a", "three")}, + new Object[] {"empty value", "a:b,c:d,e:", buildStringMap("a", "b", "c", "d")}); + } + + @ParameterizedTest + @MethodSource("verifyBaggageMappingConfigsOnTracerArguments") + void verifyBaggageMappingConfigsOnTracer( + String scenario, String mapString, Map map) { + injectSysConfig(TracerConfig.BAGGAGE_MAPPING, mapString); + CoreTracer tracer = tracerBuilder().build(); + try { + assertEquals(map, tracer.captureTraceConfig().getBaggageMapping()); + } finally { + tracer.close(); + } + } + + static Stream verifyBaggageMappingConfigsOnTracerArguments() { + return Stream.of( + new Object[] {"duplicate keys", "a:one, a:two, a:three", buildStringMap("a", "three")}, + new Object[] {"empty value", "a:b,c:d,e:", buildStringMap("a", "b", "c", "d")}); + } + + @Test + @WithConfig(key = "agent.host", value = "somethingelse") + void verifyOverridingHost() { + assertEquals("somethingelse", Config.get().getAgentHost()); + } + + @TableTest({ + "scenario | key | value", + "agent port | agent.port | 777 ", + "trace port | trace.agent.port | 9999 " + }) + void verifyOverridingPort(String key, String value) { + injectSysConfig(key, value); + assertEquals(Integer.valueOf(value), Config.get().getAgentPort()); + } + + @Test + @WithConfig(key = "writer.type", value = "LoggingWriter") + void writerIsLoggingWriterWhenPropertySet() { + CoreTracer tracer = tracerBuilder().build(); + try { + assertInstanceOf(LoggingWriter.class, tracer.writer); + } finally { + tracer.close(); + } + } + + @TableTest({ + "scenario | key | value", + "priority true | priority.sampling | true ", + "priority false | priority.sampling | false" + }) + void sharesTraceCountWithDDApiWithKeyValue(String key, String value) { + injectSysConfig(key, value); + CoreTracer tracer = tracerBuilder().build(); + try { + assertInstanceOf(DDAgentWriter.class, tracer.writer); + } finally { + tracer.close(); + } + } + + @Test + void rootTagsAppliedOnlyToRootSpans() { + Map localRootSpanTags = new LinkedHashMap<>(); + localRootSpanTags.put("only_root", "value"); + CoreTracer tracer = tracerBuilder().localRootSpanTags(localRootSpanTags).build(); + AgentSpan root = tracer.buildSpan("my_root").start(); + AgentSpan child = tracer.buildSpan("my_child").asChildOf(root.context()).start(); + try { + assertTrue(root.getTags().containsKey("only_root")); + assertFalse(child.getTags().containsKey("only_root")); + } finally { + child.finish(); + root.finish(); + tracer.close(); + } + } + + @Test + void prioritySamplingWhenSpanFinishes() throws Exception { + ListWriter writer = new ListWriter(); + CoreTracer tracer = tracerBuilder().writer(writer).build(); + try { + DDSpan span = (DDSpan) tracer.buildSpan("operation").start(); + span.finish(); + writer.waitForTraces(1); + assertEquals(PrioritySampling.SAMPLER_KEEP, (int) span.getSamplingPriority()); + } finally { + tracer.close(); + } + } + + @Test + void prioritySamplingSetWhenChildSpanComplete() throws Exception { + ListWriter writer = new ListWriter(); + CoreTracer tracer = tracerBuilder().writer(writer).build(); + try { + DDSpan root = (DDSpan) tracer.buildSpan("operation").start(); + DDSpan child = (DDSpan) tracer.buildSpan("my_child").asChildOf(root.context()).start(); + root.finish(); + + assertNull(root.getSamplingPriority()); + + child.finish(); + writer.waitForTraces(1); + + assertEquals(PrioritySampling.SAMPLER_KEEP, (int) root.getSamplingPriority()); + assertEquals(root.getSamplingPriority(), child.getSamplingPriority()); + } finally { + tracer.close(); + } + } + + @Test + void verifyConfigurationPolling() throws Exception { + ParsedConfigKey key = ParsedConfigKey.parse("datadog/2/APM_TRACING/config_overrides/config"); + ConfigurationPoller poller = mock(ConfigurationPoller.class); + SharedCommunicationObjects sco = createScoWithPoller(poller); + + ProductListener[] capturedUpdater = {null}; + doAnswer( + inv -> { + capturedUpdater[0] = inv.getArgument(1, ProductListener.class); + return null; + }) + .when(poller) + .addListener(eq(Product.APM_TRACING), any(ProductListener.class)); + + CoreTracer tracer = + CoreTracer.builder().sharedCommunicationObjects(sco).pollForTracingConfiguration().build(); + unclosedTracers.add(tracer); + + try { + verify(poller).addListener(eq(Product.APM_TRACING), any(ProductListener.class)); + assertNotNull(capturedUpdater[0]); + assertEquals(Collections.emptyMap(), tracer.captureTraceConfig().getServiceMapping()); + assertEquals(Collections.emptyMap(), tracer.captureTraceConfig().getRequestHeaderTags()); + assertEquals(Collections.emptyMap(), tracer.captureTraceConfig().getResponseHeaderTags()); + assertNull(tracer.captureTraceConfig().getTraceSampleRate()); + + String json = + "{\n" + + " \"lib_config\":\n" + + " {\n" + + " \"tracing_service_mapping\":\n" + + " [{\n" + + " \"from_key\": \"foobar\",\n" + + " \"to_name\": \"bar\"\n" + + " }, {\n" + + " \"from_key\": \"snafu\",\n" + + " \"to_name\": \"foo\"\n" + + " }]\n" + + " ,\n" + + " \"tracing_header_tags\":\n" + + " [{\n" + + " \"header\": \"Cookie\",\n" + + " \"tag_name\": \"\"\n" + + " }, {\n" + + " \"header\": \"Referer\",\n" + + " \"tag_name\": \"http.referer\"\n" + + " }, {\n" + + " \"header\": \" Some.Header \",\n" + + " \"tag_name\": \"\"\n" + + " }, {\n" + + " \"header\": \"C!!!ont_____ent----tYp!/!e\",\n" + + " \"tag_name\": \"\"\n" + + " }, {\n" + + " \"header\": \"this.header\",\n" + + " \"tag_name\": \"whatever.the.user.wants.this.header\"\n" + + " }]\n" + + " ,\n" + + " \"tracing_sampling_rate\": 0.5\n" + + " }\n" + + "}"; + + capturedUpdater[0].accept(key, json.getBytes(StandardCharsets.UTF_8), null); + capturedUpdater[0].commit(null); + + Map expectedServiceMapping = buildStringMap("foobar", "bar", "snafu", "foo"); + assertEquals(expectedServiceMapping, tracer.captureTraceConfig().getServiceMapping()); + + Map expectedRequestHeaderTags = + buildStringMap( + "cookie", "http.request.headers.cookie", + "referer", "http.referer", + "some.header", "http.request.headers.some_header", + "c!!!ont_____ent----typ!/!e", "http.request.headers.c___ont_____ent----typ_/_e", + "this.header", "whatever.the.user.wants.this.header"); + assertEquals(expectedRequestHeaderTags, tracer.captureTraceConfig().getRequestHeaderTags()); + + Map expectedResponseHeaderTags = + buildStringMap( + "cookie", "http.response.headers.cookie", + "referer", "http.referer", + "some.header", "http.response.headers.some_header", + "c!!!ont_____ent----typ!/!e", "http.response.headers.c___ont_____ent----typ_/_e", + "this.header", "whatever.the.user.wants.this.header"); + assertEquals(expectedResponseHeaderTags, tracer.captureTraceConfig().getResponseHeaderTags()); + + assertEquals(0.5, tracer.captureTraceConfig().getTraceSampleRate(), 0.0001); + + capturedUpdater[0].remove(key, null); + capturedUpdater[0].commit(null); + + assertEquals(Collections.emptyMap(), tracer.captureTraceConfig().getServiceMapping()); + assertEquals(Collections.emptyMap(), tracer.captureTraceConfig().getRequestHeaderTags()); + assertEquals(Collections.emptyMap(), tracer.captureTraceConfig().getResponseHeaderTags()); + assertNull(tracer.captureTraceConfig().getTraceSampleRate()); + } finally { + tracer.close(); + } + } + + @ParameterizedTest + @MethodSource("verifyConfigurationPollingWithCustomTagsArguments") + void verifyConfigurationPollingWithCustomTags( + String scenario, String json, Map expectedValue) throws Exception { + ParsedConfigKey key = ParsedConfigKey.parse("datadog/2/APM_TRACING/config_overrides/config"); + ConfigurationPoller poller = mock(ConfigurationPoller.class); + SharedCommunicationObjects sco = createScoWithPoller(poller); + + ProductListener[] capturedUpdater = {null}; + doAnswer( + inv -> { + capturedUpdater[0] = inv.getArgument(1, ProductListener.class); + return null; + }) + .when(poller) + .addListener(eq(Product.APM_TRACING), any(ProductListener.class)); + + CoreTracer tracer = + CoreTracer.builder().sharedCommunicationObjects(sco).pollForTracingConfiguration().build(); + unclosedTracers.add(tracer); + + try { + verify(poller).addListener(eq(Product.APM_TRACING), any(ProductListener.class)); + assertNotNull(capturedUpdater[0]); + assertEquals(Collections.emptyMap(), tracer.captureTraceConfig().getTracingTags()); + + capturedUpdater[0].accept(key, json.getBytes(StandardCharsets.UTF_8), null); + capturedUpdater[0].commit(null); + + ConfigSnapshot config = tracer.captureTraceConfig(); + assertEquals(expectedValue, config.getTracingTags()); + assertEquals(expectedValue, config.mergedTracerTags); + + capturedUpdater[0].remove(key, null); + capturedUpdater[0].commit(null); + + assertEquals(Collections.emptyMap(), tracer.captureTraceConfig().getTracingTags()); + } finally { + tracer.close(); + } + } + + static Stream verifyConfigurationPollingWithCustomTagsArguments() { + return Stream.of( + new Object[] { + "a:b c:d e:f", + "{\"lib_config\":{\"tracing_tags\": [\"a:b\", \"c:d\", \"e:f\"]}}", + buildStringMap("a", "b", "c", "d", "e", "f") + }, + new Object[] { + "empty and c:d", + "{\"lib_config\":{\"tracing_tags\": [\"\", \"c:d\", \"\"]}}", + buildStringMap("c", "d") + }, + new Object[] { + ":b c: e:f", + "{\"lib_config\":{\"tracing_tags\": [\":b\", \"c:\", \"e:f\"]}}", + buildStringMap("e", "f") + }, + new Object[] { + ": c: e:f", + "{\"lib_config\":{\"tracing_tags\": [\":\", \"c:\", \"e:f\"]}}", + buildStringMap("e", "f") + }, + new Object[] { + ": c: empty", + "{\"lib_config\":{\"tracing_tags\": [\":\", \"c:\", \"\"]}}", + Collections.emptyMap() + }, + new Object[] { + "empty array", "{\"lib_config\":{\"tracing_tags\": []}}", Collections.emptyMap() + }); + } + + @ParameterizedTest + @MethodSource("verifyConfigurationPollingWithTracingEnabledArguments") + void verifyConfigurationPollingWithTracingEnabled( + String scenario, String json, boolean expectedValue) throws Exception { + ParsedConfigKey key = ParsedConfigKey.parse("datadog/2/APM_TRACING/config_overrides/config"); + ConfigurationPoller poller = mock(ConfigurationPoller.class); + SharedCommunicationObjects sco = createScoWithPoller(poller); + + ProductListener[] capturedUpdater = {null}; + doAnswer( + inv -> { + capturedUpdater[0] = inv.getArgument(1, ProductListener.class); + return null; + }) + .when(poller) + .addListener(eq(Product.APM_TRACING), any(ProductListener.class)); + + CoreTracer tracer = + CoreTracer.builder().sharedCommunicationObjects(sco).pollForTracingConfiguration().build(); + unclosedTracers.add(tracer); + + try { + verify(poller).addListener(eq(Product.APM_TRACING), any(ProductListener.class)); + assertNotNull(capturedUpdater[0]); + assertTrue(tracer.captureTraceConfig().isTraceEnabled()); + + capturedUpdater[0].accept(key, json.getBytes(StandardCharsets.UTF_8), null); + capturedUpdater[0].commit(null); + + assertEquals(expectedValue, tracer.captureTraceConfig().isTraceEnabled()); + } finally { + tracer.close(); + } + } + + static Stream verifyConfigurationPollingWithTracingEnabledArguments() { + return Stream.of( + new Object[] {"tracing disabled", "{\"lib_config\":{\"tracing_enabled\": false}}", false}, + new Object[] {"tracing enabled", "{\"lib_config\":{\"tracing_enabled\": true}}", true}, + new Object[] { + "action with tracing disabled", + "{\"action\": \"enable\", \"lib_config\": {\"tracing_sampling_rate\": null," + + " \"log_injection_enabled\": null, \"tracing_header_tags\": null," + + " \"runtime_metrics_enabled\": null, \"tracing_debug\": null," + + " \"tracing_service_mapping\": null, \"tracing_sampling_rules\": null," + + " \"span_sampling_rules\": null, \"data_streams_enabled\": null," + + " \"tracing_enabled\": false}}", + false + }); + } + + @TableTest({ + "scenario | preferred | expected", + "no pref | | test ", + "with pref | some | some " + }) + void testLocalRootServiceNameOverride(String preferred, String expected) { + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).serviceName("test").build(); + tracer.updatePreferredServiceName(preferred, preferred); + try { + DDSpan span = (DDSpan) tracer.startSpan("", "test"); + span.finish(); + assertEquals(expected, span.getServiceName()); + if (preferred != null) { + assertTrue(ServiceNameCollector.get().getServices().contains(preferred)); + } + } finally { + tracer.close(); + } + } + + @Test + @WithConfig(key = GeneralConfig.SERVICE_NAME, value = "dd_service_name") + @WithConfig(key = GeneralConfig.VERSION, value = "1.0.0") + void testDdVersionExistsOnlyIfServiceEqDdService() { + TagsPostProcessorFactory.withAddInternalTags(true); + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + DDSpan span = + (DDSpan) tracer.buildSpan("def").withTag(GeneralConfig.SERVICE_NAME, "foo").start(); + span.finish(); + assertEquals("foo", span.getServiceName()); + assertFalse(span.getTags().containsKey("version")); + + DDSpan span2 = (DDSpan) tracer.buildSpan("abc").start(); + span2.finish(); + assertEquals("dd_service_name", span2.getServiceName()); + assertEquals("1.0.0", String.valueOf(span2.getTags().get("version"))); + } finally { + tracer.close(); + } + } + + @Test + void flushesOnTracerCloseIfConfiguredToDoSo() { + WriterWithExplicitFlush writer = new WriterWithExplicitFlush(); + CoreTracer tracer = tracerBuilder().writer(writer).flushOnClose(true).build(); + tracer.buildSpan("my_span").start().finish(); + tracer.close(); + assertFalse(writer.flushedTraces.isEmpty()); + } + + @TableTest({ + "scenario | service | env | targetService | targetEnv", + "diff target service | service | env | service_1 | env ", + "diff target env | service | env | service | env_1 ", + "diff target service and env | service | env | service_2 | env_2 " + }) + void verifyNoFilteringOfServiceEnvWhenMismatchedWithDdServiceDdEnv( + String service, String env, String targetService, String targetEnv) throws Exception { + injectSysConfig(GeneralConfig.SERVICE_NAME, service); + injectSysConfig(GeneralConfig.ENV, env); + + ParsedConfigKey key = ParsedConfigKey.parse("datadog/2/APM_TRACING/config_overrides/config"); + ConfigurationPoller poller = mock(ConfigurationPoller.class); + SharedCommunicationObjects sco = createScoWithPoller(poller); + + ProductListener[] capturedUpdater = {null}; + doAnswer( + inv -> { + capturedUpdater[0] = inv.getArgument(1, ProductListener.class); + return null; + }) + .when(poller) + .addListener(eq(Product.APM_TRACING), any(ProductListener.class)); + + CoreTracer tracer = + CoreTracer.builder().sharedCommunicationObjects(sco).pollForTracingConfiguration().build(); + unclosedTracers.add(tracer); + + try { + verify(poller).addListener(eq(Product.APM_TRACING), any(ProductListener.class)); + assertNotNull(capturedUpdater[0]); + assertEquals(Collections.emptyMap(), tracer.captureTraceConfig().getServiceMapping()); + + String json = + String.format( + "{\"service_target\":{\"service\":\"%s\",\"env\":\"%s\"}," + + "\"lib_config\":{\"tracing_service_mapping\":" + + "[{\"from_key\":\"foobar\",\"to_name\":\"bar\"}]}}", + targetService, targetEnv); + + capturedUpdater[0].accept(key, json.getBytes(StandardCharsets.UTF_8), null); + capturedUpdater[0].commit(null); + + assertEquals( + buildStringMap("foobar", "bar"), tracer.captureTraceConfig().getServiceMapping()); + } finally { + tracer.close(); + } + } + + @Test + void serviceNameSourceIsRecordedWhenUsingTwoParameterSetServiceName() { + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + DDSpan span = (DDSpan) tracer.buildSpan("operation").start(); + span.setServiceName("custom-service", "my-integration"); + DDSpan child = (DDSpan) tracer.buildSpan("child").start(); + child.finish(); + span.finish(); + + assertEquals("custom-service", span.getServiceName()); + assertEquals("my-integration", span.getTag(DDTags.DD_SVC_SRC)); + } finally { + tracer.close(); + } + } + + @Test + void serviceNameSourceIsMarkedAsManualWhenUsingOneParameterSetServiceName() { + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + DDSpan span = (DDSpan) tracer.buildSpan("operation").start(); + span.setServiceName("custom-service", "my-integration"); + span.setServiceName("another"); + span.finish(); + + assertEquals("another", span.getServiceName()); + assertEquals(ServiceNameSources.MANUAL, span.getTag(DDTags.DD_SVC_SRC)); + } finally { + tracer.close(); + } + } + + @Test + void serviceNameSourceIsMissingWhenNotExplicitlySettingServiceName() { + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + DDSpan span = (DDSpan) tracer.buildSpan("operation").start(); + span.finish(); + + assertEquals(tracer.serviceName, span.getServiceName()); + assertNull(span.getTag(DDTags.DD_SVC_SRC)); + } finally { + tracer.close(); + } + } + + // --- helpers --- + + private SharedCommunicationObjects createScoWithPoller(ConfigurationPoller poller) + throws Exception { + SharedCommunicationObjects sco = new SharedCommunicationObjects(); + sco.agentHttpClient = mock(OkHttpClient.class); + sco.monitoring = mock(Monitoring.class); + sco.agentUrl = HttpUrl.get("https://example.com"); + sco.setFeaturesDiscovery(mock(DDAgentFeaturesDiscovery.class)); + Field pollerField = SharedCommunicationObjects.class.getDeclaredField("configurationPoller"); + pollerField.setAccessible(true); + pollerField.set(sco, poller); + return sco; + } + + private static Map buildStringMap(String... keyValues) { + Map map = new LinkedHashMap<>(); + for (int i = 0; i < keyValues.length; i += 2) { + map.put(keyValues[i], keyValues[i + 1]); + } + return map; + } + + // --- inner classes --- + + static class WriterWithExplicitFlush implements datadog.trace.common.writer.Writer { + final List> writtenTraces = new CopyOnWriteArrayList<>(); + final List> flushedTraces = new CopyOnWriteArrayList<>(); + + @Override + public void write(List trace) { + writtenTraces.add(trace); + } + + @Override + public void start() {} + + @Override + public boolean flush() { + flushedTraces.addAll(writtenTraces); + writtenTraces.clear(); + return true; + } + + @Override + public void close() {} + + @Override + public void incrementDropCounts(int spanCount) {} + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanContextPropagationTagsTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanContextPropagationTagsTest.java new file mode 100644 index 00000000000..ae2f424069f --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanContextPropagationTagsTest.java @@ -0,0 +1,277 @@ +package datadog.trace.core; + +import static datadog.trace.api.TracePropagationStyle.DATADOG; +import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_DROP; +import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP; +import static datadog.trace.api.sampling.PrioritySampling.UNSET; +import static datadog.trace.api.sampling.PrioritySampling.USER_DROP; +import static datadog.trace.api.sampling.PrioritySampling.USER_KEEP; +import static datadog.trace.api.sampling.SamplingMechanism.DEFAULT; +import static datadog.trace.api.sampling.SamplingMechanism.MANUAL; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import datadog.trace.api.DDTraceId; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; +import datadog.trace.common.writer.ListWriter; +import datadog.trace.core.propagation.ExtractedContext; +import datadog.trace.core.propagation.PropagationTags; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class DDSpanContextPropagationTagsTest extends DDCoreJavaSpecification { + + private ListWriter writer; + private CoreTracer tracer; + + @BeforeEach + void setup() { + writer = new ListWriter(); + tracer = tracerBuilder().writer(writer).build(); + } + + @ParameterizedTest() + @MethodSource("updateSpanPropagationTagsArguments") + void updateSpanPropagationTags( + String scenario, + int priority, + String header, + int newPriority, + int newMechanism, + String newHeader, + Map tagMap) { + PropagationTags propagationTags = + tracer + .getPropagationTagsFactory() + .fromHeaderValue(PropagationTags.HeaderType.DATADOG, header); + AgentSpanContext extracted = + new ExtractedContext(DDTraceId.from(123), 456, priority, "789", propagationTags, DATADOG) + .withRequestContextDataAppSec("dummy"); + DDSpan span = (DDSpan) tracer.buildSpan("top").asChildOf(extracted).start(); + PropagationTags dd = span.context().getPropagationTags(); + + span.setSamplingPriority(newPriority, newMechanism); + + assertEquals(newHeader, dd.headerValue(PropagationTags.HeaderType.DATADOG)); + assertEquals(tagMap, dd.createTagMap()); + } + + static Stream updateSpanPropagationTagsArguments() { + return Stream.of( + arguments( + "UNSET->USER_KEEP", + UNSET, + "_dd.p.usr=123", + USER_KEEP, + MANUAL, + "_dd.p.dm=-4,_dd.p.usr=123", + buildMap("_dd.p.dm", "-4", "_dd.p.usr", "123")), + arguments( + "UNSET->SAMPLER_DROP", + UNSET, + "_dd.p.usr=123", + SAMPLER_DROP, + DEFAULT, + "_dd.p.usr=123", + buildMap("_dd.p.usr", "123")), + // decision has already been made, propagate as-is + arguments( + "SAMPLER_KEEP->USER_KEEP with dm", + SAMPLER_KEEP, + "_dd.p.dm=9bf3439f2f-1,_dd.p.usr=123", + USER_KEEP, + MANUAL, + "_dd.p.dm=9bf3439f2f-1,_dd.p.usr=123", + buildMap("_dd.p.dm", "9bf3439f2f-1", "_dd.p.usr", "123")), + arguments( + "SAMPLER_KEEP->USER_DROP with dm", + SAMPLER_KEEP, + "_dd.p.dm=9bf3439f2f-1,_dd.p.usr=123", + USER_DROP, + MANUAL, + "_dd.p.dm=9bf3439f2f-1,_dd.p.usr=123", + buildMap("_dd.p.dm", "9bf3439f2f-1", "_dd.p.usr", "123")), + arguments( + "SAMPLER_KEEP->USER_KEEP no dm", + SAMPLER_KEEP, + "_dd.p.usr=123", + USER_KEEP, + MANUAL, + "_dd.p.usr=123", + buildMap("_dd.p.usr", "123"))); + } + + @ParameterizedTest + @MethodSource("updateTracePropagationTagsArguments") + void updateTracePropagationTags( + String scenario, + int priority, + String header, + int newPriority, + int newMechanism, + String rootHeader, + Map rootTagMap) { + PropagationTags propagationTags = + tracer + .getPropagationTagsFactory() + .fromHeaderValue(PropagationTags.HeaderType.DATADOG, header); + AgentSpanContext extracted = + new ExtractedContext(DDTraceId.from(123), 456, priority, "789", propagationTags, DATADOG) + .withRequestContextDataAppSec("dummy"); + DDSpan rootSpan = (DDSpan) tracer.buildSpan("top").asChildOf(extracted).start(); + PropagationTags ddRoot = rootSpan.context().getPropagationTags(); + DDSpan span = (DDSpan) tracer.buildSpan("current").asChildOf(rootSpan.context()).start(); + + span.setSamplingPriority(newPriority, newMechanism); + + assertEquals(rootHeader, ddRoot.headerValue(PropagationTags.HeaderType.DATADOG)); + assertEquals(rootTagMap, ddRoot.createTagMap()); + } + + static Stream updateTracePropagationTagsArguments() { + return Stream.of( + arguments( + "UNSET->USER_KEEP", + UNSET, + "_dd.p.usr=123", + USER_KEEP, + MANUAL, + "_dd.p.dm=-4,_dd.p.usr=123", + buildMap("_dd.p.dm", "-4", "_dd.p.usr", "123")), + arguments( + "UNSET->SAMPLER_DROP", + UNSET, + "_dd.p.usr=123", + SAMPLER_DROP, + DEFAULT, + "_dd.p.usr=123", + buildMap("_dd.p.usr", "123")), + // decision has already been made, propagate as-is + arguments( + "SAMPLER_KEEP->USER_KEEP with dm", + SAMPLER_KEEP, + "_dd.p.dm=9bf3439f2f-1,_dd.p.usr=123", + USER_KEEP, + MANUAL, + "_dd.p.dm=9bf3439f2f-1,_dd.p.usr=123", + buildMap("_dd.p.dm", "9bf3439f2f-1", "_dd.p.usr", "123")), + arguments( + "SAMPLER_KEEP->USER_DROP with dm", + SAMPLER_KEEP, + "_dd.p.dm=9bf3439f2f-1,_dd.p.usr=123", + USER_DROP, + MANUAL, + "_dd.p.dm=9bf3439f2f-1,_dd.p.usr=123", + buildMap("_dd.p.dm", "9bf3439f2f-1", "_dd.p.usr", "123")), + arguments( + "SAMPLER_KEEP->USER_KEEP no dm", + SAMPLER_KEEP, + "_dd.p.usr=123", + USER_KEEP, + MANUAL, + "_dd.p.usr=123", + buildMap("_dd.p.usr", "123"))); + } + + @ParameterizedTest + @MethodSource("forceKeepSpanPropagationTagsArguments") + void forceKeepSpanPropagationTags( + String scenario, int priority, String header, String newHeader, Map tagMap) { + PropagationTags propagationTags = + tracer + .getPropagationTagsFactory() + .fromHeaderValue(PropagationTags.HeaderType.DATADOG, header); + AgentSpanContext extracted = + new ExtractedContext(DDTraceId.from(123), 456, priority, "789", propagationTags, DATADOG) + .withRequestContextDataAppSec("dummy"); + DDSpan span = (DDSpan) tracer.buildSpan("top").asChildOf(extracted).start(); + PropagationTags dd = span.context().getPropagationTags(); + + span.context().forceKeep(); + + assertEquals(newHeader, dd.headerValue(PropagationTags.HeaderType.DATADOG)); + assertEquals(tagMap, dd.createTagMap()); + } + + static Stream forceKeepSpanPropagationTagsArguments() { + return Stream.of( + arguments( + "UNSET", + UNSET, + "_dd.p.usr=123", + "_dd.p.dm=-4,_dd.p.usr=123", + buildMap("_dd.p.dm", "-4", "_dd.p.usr", "123")), + arguments( + "SAMPLER_KEEP with dm", + SAMPLER_KEEP, + "_dd.p.dm=9bf3439f2f-1,_dd.p.usr=123", + "_dd.p.dm=-4,_dd.p.usr=123", + buildMap("_dd.p.dm", "-4", "_dd.p.usr", "123")), + arguments( + "SAMPLER_KEEP no dm", + SAMPLER_KEEP, + "_dd.p.usr=123", + "_dd.p.dm=-4,_dd.p.usr=123", + buildMap("_dd.p.dm", "-4", "_dd.p.usr", "123"))); + } + + @ParameterizedTest + @MethodSource("forceKeepTracePropagationTagsArguments") + void forceKeepTracePropagationTags( + String scenario, + int priority, + String header, + String rootHeader, + Map rootTagMap) { + PropagationTags propagationTags = + tracer + .getPropagationTagsFactory() + .fromHeaderValue(PropagationTags.HeaderType.DATADOG, header); + AgentSpanContext extracted = + new ExtractedContext(DDTraceId.from(123), 456, priority, "789", propagationTags, DATADOG) + .withRequestContextDataAppSec("dummy"); + DDSpan rootSpan = (DDSpan) tracer.buildSpan("top").asChildOf(extracted).start(); + PropagationTags ddRoot = rootSpan.context().getPropagationTags(); + DDSpan span = (DDSpan) tracer.buildSpan("current").asChildOf(rootSpan.context()).start(); + + span.context().forceKeep(); + + assertEquals(rootHeader, ddRoot.headerValue(PropagationTags.HeaderType.DATADOG)); + assertEquals(rootTagMap, ddRoot.createTagMap()); + } + + static Stream forceKeepTracePropagationTagsArguments() { + return Stream.of( + arguments( + "UNSET", + UNSET, + "_dd.p.usr=123", + "_dd.p.dm=-4,_dd.p.usr=123", + buildMap("_dd.p.dm", "-4", "_dd.p.usr", "123")), + arguments( + "SAMPLER_KEEP with dm", + SAMPLER_KEEP, + "_dd.p.dm=9bf3439f2f-1,_dd.p.usr=123", + "_dd.p.dm=-4,_dd.p.usr=123", + buildMap("_dd.p.dm", "-4", "_dd.p.usr", "123")), + arguments( + "SAMPLER_KEEP no dm", + SAMPLER_KEEP, + "_dd.p.usr=123", + "_dd.p.dm=-4,_dd.p.usr=123", + buildMap("_dd.p.dm", "-4", "_dd.p.usr", "123"))); + } + + private static Map buildMap(String... keyValues) { + Map map = new HashMap<>(); + for (int i = 0; i < keyValues.length; i += 2) { + map.put(keyValues[i], keyValues[i + 1]); + } + return map; + } +} From 9bce9c0a8448e3e189c64496ed926828839b82aa Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Fri, 17 Apr 2026 13:30:45 +0200 Subject: [PATCH 2/3] fix ServiceNameCollector singleton we save the original instance before replaced by mock to restore it Some tests rely on having. the original, and depending on the order the class my keep the mocks instead resulting in errors --- .../trace/core/taginterceptor/TagInterceptorTest.groovy | 8 ++++++++ .../naming/ExtraServiceProviderNamingV0ForkedTest.groovy | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/taginterceptor/TagInterceptorTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/taginterceptor/TagInterceptorTest.groovy index 075ac57a188..0bb65ac4919 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/taginterceptor/TagInterceptorTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/taginterceptor/TagInterceptorTest.groovy @@ -696,6 +696,7 @@ class TagInterceptorTest extends DDCoreSpecification { } void "when interceptServiceName extraServiceProvider is called"() { + def origServiceNameCollector = ServiceNameCollector.INSTANCE setup: final extraServiceProvider = Mock(ServiceNameCollector) ServiceNameCollector.INSTANCE = extraServiceProvider @@ -708,9 +709,13 @@ class TagInterceptorTest extends DDCoreSpecification { then: 1 * extraServiceProvider.addService("some-service") + + cleanup: + ServiceNameCollector.INSTANCE = origServiceNameCollector } void "when interceptServletContext extraServiceProvider is called"() { + def origServiceNameCollector = ServiceNameCollector.INSTANCE setup: final extraServiceProvider = Mock(ServiceNameCollector) ServiceNameCollector.INSTANCE = extraServiceProvider @@ -724,6 +729,9 @@ class TagInterceptorTest extends DDCoreSpecification { then: 1 * extraServiceProvider.addService(expected) + cleanup: + ServiceNameCollector.INSTANCE = origServiceNameCollector + where: value | expected "/" | "root-servlet" diff --git a/internal-api/src/test/groovy/datadog/trace/api/naming/ExtraServiceProviderNamingV0ForkedTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/naming/ExtraServiceProviderNamingV0ForkedTest.groovy index fe203df0b2e..1a936615591 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/naming/ExtraServiceProviderNamingV0ForkedTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/naming/ExtraServiceProviderNamingV0ForkedTest.groovy @@ -7,6 +7,7 @@ import datadog.trace.api.remoteconfig.ServiceNameCollector class ExtraServiceProviderNamingV0ForkedTest extends DDSpecification { void "Naming schema calls ExtraServicesProvider if provides a service name"() { + def origServiceNameCollector = ServiceNameCollector.INSTANCE setup: final extraServiceProvider = Mock(ServiceNameCollector) ServiceNameCollector.INSTANCE = extraServiceProvider @@ -67,5 +68,8 @@ class ExtraServiceProviderNamingV0ForkedTest extends DDSpecification { then: 1 * extraServiceProvider.addService("anything") + + cleanup: + ServiceNameCollector.INSTANCE = origServiceNameCollector } } From 3ec47ca86e16c239ea2d1497daef1da8f9b91339 Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Tue, 21 Apr 2026 10:39:41 +0200 Subject: [PATCH 3/3] convert to table test --- .../trace/core/CoreSpanBuilderTest.java | 29 ++--- .../datadog/trace/core/CoreTracerTest.java | 110 ++++++------------ 2 files changed, 53 insertions(+), 86 deletions(-) diff --git a/dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java b/dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java index 1b606941658..05c9cd87222 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/CoreSpanBuilderTest.java @@ -419,18 +419,15 @@ void buildContextFromExtractedContextWithIgnoreBehavior() { assertTrue(span.getLinks().isEmpty()); } - static Stream tagContextShouldPopulateDefaultSpanDetailsArguments() { - return Stream.of( - new TagContext(null, TagMap.fromMap(Collections.emptyMap())), - new TagContext( - "some-origin", - TagMap.fromMap(Collections.singletonMap("asdf", "qwer")))); - } - - @ParameterizedTest - @MethodSource("tagContextShouldPopulateDefaultSpanDetailsArguments") - void tagContextShouldPopulateDefaultSpanDetails(TagContext tagContext) { + @TableTest({ + "scenario | origin | tagMap ", + "empty tag map | | [:] ", + "some origin | some-origin | [asdf: qwer]" + }) + void tagContextShouldPopulateDefaultSpanDetails( + String scenario, String origin, Map tagMap) { Thread thread = Thread.currentThread(); + TagContext tagContext = new TagContext(origin, TagMap.fromMap(tagMap)); DDSpan span = (DDSpan) tracer.buildSpan("test", "op name").asChildOf(tagContext).start(); assertNotEquals(DDTraceId.ZERO, span.getTraceId()); @@ -462,8 +459,14 @@ static Stream globalSpanTagsPopulatedOnEachSpanArguments() { arguments("a:1,b-c:d", buildStringMap("a", "1", "b-c", "d"))); } - @ParameterizedTest - @MethodSource("globalSpanTagsPopulatedOnEachSpanArguments") + @TableTest({ + "scenario | tagString | tags ", + "empty | '' | [:] ", + "column | is:val:id | [is: 'val:id']", + "single | a:x | [a: x] ", + "same multi-values | a:a,a:b,a:c | [a: c] ", + "multi values | a:1,b-c:d | [a: 1, b-c: d]" + }) void globalSpanTagsPopulatedOnEachSpan(String tagString, Map tags) { injectSysConfig("dd.trace.span.tags", tagString); CoreTracer customTracer = tracerBuilder().writer(writer).build(); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/CoreTracerTest.java b/dd-trace-core/src/test/java/datadog/trace/core/CoreTracerTest.java index 4cf5b230604..451fab2e418 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/CoreTracerTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/CoreTracerTest.java @@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; @@ -45,15 +46,11 @@ import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; -import java.util.stream.Stream; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; -import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; import org.tabletest.junit.TableTest; @Timeout(value = 10, unit = TimeUnit.SECONDS) @@ -61,7 +58,7 @@ public class CoreTracerTest extends DDCoreJavaSpecification { @BeforeAll static void checkJvm() { - Assumptions.assumeFalse( + assumeFalse( JavaVirtualMachine.isOracleJDK8(), "Oracle JDK 1.8 did not merge the fix in JDK-8058322, leading to the JVM failing to" + " correctly extract method parameters without args, when the code is compiled on a" @@ -118,8 +115,11 @@ void verifyUdsWindows() { } } - @ParameterizedTest - @MethodSource("verifyMappingConfigsOnTracerArguments") + @TableTest({ + "scenario | mapString | map ", + "duplicate keys | a:one, a:two, a:three | [a: three] ", + "empty value | a:b,c:d,e: | [a: b, c: d]" + }) void verifyMappingConfigsOnTracer(String scenario, String mapString, Map map) { injectSysConfig(TracerConfig.SERVICE_MAPPING, mapString); injectSysConfig(TracerConfig.SPAN_TAGS, mapString); @@ -134,14 +134,11 @@ void verifyMappingConfigsOnTracer(String scenario, String mapString, Map verifyMappingConfigsOnTracerArguments() { - return Stream.of( - new Object[] {"duplicate keys", "a:one, a:two, a:three", buildStringMap("a", "three")}, - new Object[] {"empty value", "a:b,c:d,e:", buildStringMap("a", "b", "c", "d")}); - } - - @ParameterizedTest - @MethodSource("verifyBaggageMappingConfigsOnTracerArguments") + @TableTest({ + "scenario | mapString | map ", + "duplicate keys | a:one, a:two, a:three | [a: three] ", + "empty value | a:b,c:d,e: | [a: b, c: d]" + }) void verifyBaggageMappingConfigsOnTracer( String scenario, String mapString, Map map) { injectSysConfig(TracerConfig.BAGGAGE_MAPPING, mapString); @@ -153,12 +150,6 @@ void verifyBaggageMappingConfigsOnTracer( } } - static Stream verifyBaggageMappingConfigsOnTracerArguments() { - return Stream.of( - new Object[] {"duplicate keys", "a:one, a:two, a:three", buildStringMap("a", "three")}, - new Object[] {"empty value", "a:b,c:d,e:", buildStringMap("a", "b", "c", "d")}); - } - @Test @WithConfig(key = "agent.host", value = "somethingelse") void verifyOverridingHost() { @@ -353,8 +344,15 @@ void verifyConfigurationPolling() throws Exception { } } - @ParameterizedTest - @MethodSource("verifyConfigurationPollingWithCustomTagsArguments") + @TableTest({ + "scenario | json | expectedValue ", + "a:b c:d e:f | '{\"lib_config\":{\"tracing_tags\": [\"a:b\", \"c:d\", \"e:f\"]}}' | [a: b, c: d, e: f]", + "empty and c:d | '{\"lib_config\":{\"tracing_tags\": [\"\", \"c:d\", \"\"]}}' | [c: d] ", + ":b c: e:f | '{\"lib_config\":{\"tracing_tags\": [\":b\", \"c:\", \"e:f\"]}}' | [e: f] ", + ": c: e:f | '{\"lib_config\":{\"tracing_tags\": [\":\", \"c:\", \"e:f\"]}}' | [e: f] ", + ": c: empty | '{\"lib_config\":{\"tracing_tags\": [\":\", \"c:\", \"\"]}}' | [:] ", + "empty array | '{\"lib_config\":{\"tracing_tags\": []}}' | [:] " + }) void verifyConfigurationPollingWithCustomTags( String scenario, String json, Map expectedValue) throws Exception { ParsedConfigKey key = ParsedConfigKey.parse("datadog/2/APM_TRACING/config_overrides/config"); @@ -395,40 +393,22 @@ void verifyConfigurationPollingWithCustomTags( } } - static Stream verifyConfigurationPollingWithCustomTagsArguments() { - return Stream.of( - new Object[] { - "a:b c:d e:f", - "{\"lib_config\":{\"tracing_tags\": [\"a:b\", \"c:d\", \"e:f\"]}}", - buildStringMap("a", "b", "c", "d", "e", "f") - }, - new Object[] { - "empty and c:d", - "{\"lib_config\":{\"tracing_tags\": [\"\", \"c:d\", \"\"]}}", - buildStringMap("c", "d") - }, - new Object[] { - ":b c: e:f", - "{\"lib_config\":{\"tracing_tags\": [\":b\", \"c:\", \"e:f\"]}}", - buildStringMap("e", "f") - }, - new Object[] { - ": c: e:f", - "{\"lib_config\":{\"tracing_tags\": [\":\", \"c:\", \"e:f\"]}}", - buildStringMap("e", "f") - }, - new Object[] { - ": c: empty", - "{\"lib_config\":{\"tracing_tags\": [\":\", \"c:\", \"\"]}}", - Collections.emptyMap() - }, - new Object[] { - "empty array", "{\"lib_config\":{\"tracing_tags\": []}}", Collections.emptyMap() - }); - } - - @ParameterizedTest - @MethodSource("verifyConfigurationPollingWithTracingEnabledArguments") + @TableTest({ + "scenario | json | expectedValue ", + "tracing disabled | '{\"lib_config\":{\"tracing_enabled\": false}}' | false ", + "tracing enabled | '{\"lib_config\":{\"tracing_enabled\": true}}' | true ", + "action with tracing disabled | '{\"action\": \"enable\", \"lib_config\": ", + "{\"tracing_sampling_rate\": null, ", + " \"log_injection_enabled\": null, ", + "\"tracing_header_tags\": null, ", + " \"runtime_metrics_enabled\": null, ", + "\"tracing_debug\": null, ", + " \"tracing_service_mapping\": null, ", + "\"tracing_sampling_rules\": null, ", + " \"span_sampling_rules\": null, ", + "\"data_streams_enabled\": null, ", + " \"tracing_enabled\": false}}' | false " + }) void verifyConfigurationPollingWithTracingEnabled( String scenario, String json, boolean expectedValue) throws Exception { ParsedConfigKey key = ParsedConfigKey.parse("datadog/2/APM_TRACING/config_overrides/config"); @@ -462,22 +442,6 @@ void verifyConfigurationPollingWithTracingEnabled( } } - static Stream verifyConfigurationPollingWithTracingEnabledArguments() { - return Stream.of( - new Object[] {"tracing disabled", "{\"lib_config\":{\"tracing_enabled\": false}}", false}, - new Object[] {"tracing enabled", "{\"lib_config\":{\"tracing_enabled\": true}}", true}, - new Object[] { - "action with tracing disabled", - "{\"action\": \"enable\", \"lib_config\": {\"tracing_sampling_rate\": null," - + " \"log_injection_enabled\": null, \"tracing_header_tags\": null," - + " \"runtime_metrics_enabled\": null, \"tracing_debug\": null," - + " \"tracing_service_mapping\": null, \"tracing_sampling_rules\": null," - + " \"span_sampling_rules\": null, \"data_streams_enabled\": null," - + " \"tracing_enabled\": false}}", - false - }); - } - @TableTest({ "scenario | preferred | expected", "no pref | | test ",