diff --git a/internal-api/src/test/groovy/datadog/trace/bootstrap/config/provider/ConfigProviderTest.groovy b/internal-api/src/test/groovy/datadog/trace/bootstrap/config/provider/ConfigProviderTest.groovy deleted file mode 100644 index eced33f3049..00000000000 --- a/internal-api/src/test/groovy/datadog/trace/bootstrap/config/provider/ConfigProviderTest.groovy +++ /dev/null @@ -1,576 +0,0 @@ -package datadog.trace.bootstrap.config.provider - -import datadog.trace.test.util.DDSpecification -import spock.lang.Shared -import datadog.trace.api.ConfigCollector -import datadog.trace.api.ConfigOrigin -import datadog.trace.api.ConfigSetting -import datadog.trace.config.inversion.ConfigHelper - - -import static datadog.trace.api.config.TracerConfig.TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING - -class ConfigProviderTest extends DDSpecification { - - @Shared - ConfigProvider configProvider = ConfigProvider.withoutCollector() - - def strictness - - def setup(){ - strictness = ConfigHelper.get().configInversionStrictFlag() - ConfigHelper.get().setConfigInversionStrict(ConfigHelper.StrictnessPolicy.TEST) - } - - def cleanup() { - ConfigHelper.get().setConfigInversionStrict(strictness) - } - - def "properties take precedence over env vars for ordered map"() { - setup: - injectEnvConfig("TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING", "/a:env,/b:env") - injectSysConfig(TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING, "/a:prop") - - when: - def config = configProvider.getOrderedMap(TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING) - - then: - config["/a"] == "prop" - config["/b"] == "env" - } - - - def "test config alias priority"() { - setup: - injectEnvConfig("CONFIG_NAME", configNameValue) - injectEnvConfig("CONFIG_ALIAS1", configAlias1Value) - injectEnvConfig("CONFIG_ALIAS2", configAlias2Value) - - when: - def config = configProvider.getString("CONFIG_NAME", null, "CONFIG_ALIAS1", "CONFIG_ALIAS2") - - then: - config == expected - - where: - configNameValue | configAlias1Value | configAlias2Value | expected - "default" | null | null | "default" - null | "alias1" | null | "alias1" - null | null | "alias2" | "alias2" - "default" | "alias1" | null | "default" - "default" | null | "alias2" | "default" - null | "alias1" | "alias2" | "alias1" - } - - def "ConfigProvider assigns correct seqId, origin, and value for each source and default"() { - setup: - ConfigCollector.get().collect() // clear previous state - - injectEnvConfig("DD_TEST_KEY", "envValue") - injectSysConfig("test.key", "jvmValue") - // Default ConfigProvider includes ENV and JVM_PROP - def provider = ConfigProvider.createDefault() - - when: - def value = provider.getString("test.key", "defaultValue") - def collected = ConfigCollector.get().collect() - - then: - // Check the default - def defaultSetting = collected.get(ConfigOrigin.DEFAULT).get("test.key") - defaultSetting.stringValue() == "defaultValue" - defaultSetting.origin == ConfigOrigin.DEFAULT - defaultSetting.seqId == ConfigSetting.DEFAULT_SEQ_ID - - def envSetting = collected.get(ConfigOrigin.ENV).get("test.key") - envSetting.stringValue() == "envValue" - envSetting.origin == ConfigOrigin.ENV - - def jvmSetting = collected.get(ConfigOrigin.JVM_PROP).get("test.key") - jvmSetting.stringValue() == "jvmValue" - jvmSetting.origin == ConfigOrigin.JVM_PROP - - // It doesn't matter what the seqId values are, so long as they increase with source precedence - jvmSetting.seqId > envSetting.seqId - envSetting.seqId > defaultSetting.seqId - - // The value returned by ConfigProvider should be the highest precedence value - value == jvmSetting.stringValue() - } - - def "ConfigProvider reports highest seqId for chosen value and origin regardless of conversion errors for #methodType"() { - setup: - ConfigCollector.get().collect() // clear previous state - - // Set up: default, env (valid), jvm (invalid for the specific type) - injectEnvConfig(envKey, validValue) - injectSysConfig(configKey, invalidValue) - def provider = ConfigProvider.createDefault() - - when: - def value = methodCall(provider, configKey, defaultValue) - def collected = ConfigCollector.get().collect() - - then: - // Default - def defaultSetting = collected.get(ConfigOrigin.DEFAULT).get(configKey) - defaultSetting.stringValue() == String.valueOf(defaultValue) - defaultSetting.origin == ConfigOrigin.DEFAULT - defaultSetting.seqId == ConfigSetting.DEFAULT_SEQ_ID - - // ENV (valid) - def envSetting = collected.get(ConfigOrigin.ENV).get(configKey) - envSetting.stringValue() == validValue - envSetting.origin == ConfigOrigin.ENV - - // JVM_PROP (invalid, should still be reported) - def jvmSetting = collected.get(ConfigOrigin.JVM_PROP).get(configKey) - jvmSetting.stringValue() == invalidValue - jvmSetting.origin == ConfigOrigin.JVM_PROP - - // The chosen value (from ENV) should have been re-reported with the highest seqId - def maxSeqId = [defaultSetting.seqId, envSetting.seqId, jvmSetting.seqId].max() - def chosenSetting = [defaultSetting, envSetting, jvmSetting].find { it.seqId == maxSeqId } - chosenSetting.stringValue() == validValue - chosenSetting.origin == ConfigOrigin.ENV - - // The value returned by provider should be the valid one - value == expectedResult - - where: - // getBoolean is purposefully excluded; see getBoolean test below - methodType | configKey | envKey | validValue | invalidValue | defaultValue | expectedResult | methodCall - "getInteger" | "test.int" | "DD_TEST_INT" | "42" | "notAnInt" | 7 | 42 | { configProvider, key, defVal -> configProvider.getInteger(key, defVal) } - "getLong" | "test.long" | "DD_TEST_LONG" | "123" | "notALong" | 5L | 123L | { configProvider, key, defVal -> configProvider.getLong(key, defVal) } - "getFloat" | "test.float" | "DD_TEST_FLOAT" | "42.5" | "notAFloat" | 3.14f | 42.5f | { configProvider, key, defVal -> configProvider.getFloat(key, defVal) } - "getDouble" | "test.double" | "DD_TEST_DOUBLE"| "42.75" | "notADouble" | 2.71 | 42.75 | { configProvider, key, defVal -> configProvider.getDouble(key, defVal) } - } - - def "ConfigProvider transforms invalid values for getBoolean to false with CALCULATED origin"() { - // Booleans are a special case; we currently treat all invalid boolean configurations as false rather than falling back to a lower precedence setting. - setup: - ConfigCollector.get().collect() // clear previous state - - def envKey = "DD_TEST_BOOL" - def envValue = "true" - def configKey = "test.bool" - def propValue = "notABool" - def defaultValue = true - - injectEnvConfig(envKey, envValue) - injectSysConfig(configKey, propValue) - def provider = ConfigProvider.createDefault() - - when: - def value = provider.getBoolean(configKey, defaultValue) - def collected = ConfigCollector.get().collect() - - then: - // Default - def defaultSetting = collected.get(ConfigOrigin.DEFAULT).get(configKey) - defaultSetting.stringValue() == String.valueOf(defaultValue) - defaultSetting.origin == ConfigOrigin.DEFAULT - defaultSetting.seqId == ConfigSetting.DEFAULT_SEQ_ID - - // ENV (valid) - def envSetting = collected.get(ConfigOrigin.ENV).get(configKey) - envSetting.stringValue() == envValue - envSetting.origin == ConfigOrigin.ENV - - // JVM_PROP (invalid, should still be reported) - def jvmSetting = collected.get(ConfigOrigin.JVM_PROP).get(configKey) - jvmSetting.stringValue() == propValue - jvmSetting.origin == ConfigOrigin.JVM_PROP - - // Config was evaluated to false and reported with CALCULATED origin - def calcSetting = collected.get(ConfigOrigin.CALCULATED).get(configKey) - calcSetting.stringValue() == "false" - calcSetting.origin == ConfigOrigin.CALCULATED - - // The highest seqId should be the CALCULATED origin - def maxSeqId = [defaultSetting.seqId, envSetting.seqId, jvmSetting.seqId, calcSetting.seqId].max() - def chosenSetting = [defaultSetting, envSetting, jvmSetting, calcSetting].find { it.seqId == maxSeqId } - chosenSetting.origin == ConfigOrigin.CALCULATED - chosenSetting.stringValue() == "false" - - // The value returned by provider should be false - value == false - } - - def "ConfigProvider getEnum returns default when conversion fails"() { - setup: - ConfigCollector.get().collect() // clear previous state - - // Set up: only invalid enum values from all sources - injectEnvConfig("DD_TEST_ENUM2", "NOT_A_VALID_ENUM") - injectSysConfig("test.enum2", "ALSO_INVALID") - def provider = ConfigProvider.createDefault() - - when: - def value = provider.getEnum("test.enum2", ConfigOrigin, ConfigOrigin.CODE) - def collected = ConfigCollector.get().collect() - - then: - // Should have attempted to use the highest precedence value (JVM_PROP) - def jvmSetting = collected.get(ConfigOrigin.JVM_PROP).get("test.enum2") - jvmSetting.stringValue() == "ALSO_INVALID" - - // But since conversion failed, should return the default - value == ConfigOrigin.CODE - } - - def "ConfigProvider getString reports all sources and respects precedence"() { - setup: - ConfigCollector.get().collect() // clear previous state - - // Set up: default, env, jvm (all valid strings) - injectEnvConfig("DD_TEST_STRING", "envValue") - injectSysConfig("test.string", "jvmValue") - def provider = ConfigProvider.createDefault() - - when: - def value = provider.getString("test.string", "defaultValue") - def collected = ConfigCollector.get().collect() - - then: - // Default - def defaultSetting = collected.get(ConfigOrigin.DEFAULT).get("test.string") - defaultSetting.stringValue() == "defaultValue" - defaultSetting.origin == ConfigOrigin.DEFAULT - defaultSetting.seqId == ConfigSetting.DEFAULT_SEQ_ID - - // ENV - def envSetting = collected.get(ConfigOrigin.ENV).get("test.string") - envSetting.stringValue() == "envValue" - envSetting.origin == ConfigOrigin.ENV - - // JVM_PROP (highest precedence) - def jvmSetting = collected.get(ConfigOrigin.JVM_PROP).get("test.string") - jvmSetting.stringValue() == "jvmValue" - jvmSetting.origin == ConfigOrigin.JVM_PROP - - // JVM should have highest seqId and be the returned value - jvmSetting.seqId > envSetting.seqId - envSetting.seqId > defaultSetting.seqId - value == "jvmValue" - } - - def "ConfigProvider getStringNotEmpty reports all values even if they are empty, but returns the non-empty value"() { - setup: - ConfigCollector.get().collect() // clear previous state - - // Set up: env (empty/blank), jvm (valid but with whitespace) - injectEnvConfig("DD_TEST_STRING_NOT_EMPTY", " ") // blank string - injectSysConfig("test.string.not.empty", " jvmValue ") // valid but with whitespace - def provider = ConfigProvider.createDefault() - - when: - def value = provider.getStringNotEmpty("test.string.not.empty", "defaultValue") - def collected = ConfigCollector.get().collect() - - then: - // Default - def defaultSetting = collected.get(ConfigOrigin.DEFAULT).get("test.string.not.empty") - defaultSetting.stringValue() == "defaultValue" - - // ENV (blank, should be skipped for return value but still reported) - def envSetting = collected.get(ConfigOrigin.ENV).get("test.string.not.empty") - envSetting.stringValue() == " " - envSetting.origin == ConfigOrigin.ENV - - // JVM_PROP setting - should have highest seqId - def jvmSetting = collected.get(ConfigOrigin.JVM_PROP).get("test.string.not.empty") - jvmSetting.stringValue() == " jvmValue " - jvmSetting.origin == ConfigOrigin.JVM_PROP - - def maxSeqId = [defaultSetting.seqId, envSetting.seqId, jvmSetting.seqId].max() - jvmSetting.seqId == maxSeqId - - value == " jvmValue " - } - - def "ConfigProvider getStringExcludingSource excludes specified source type"() { - setup: - ConfigCollector.get().collect() // clear previous state - - // Set up: env, jvm (both valid) - injectEnvConfig("DD_TEST_STRING_EXCLUDE", "envValue") - injectSysConfig("test.string.exclude", "jvmValue") - def provider = ConfigProvider.createDefault() - - when: - // Exclude JVM_PROP source, should fall back to ENV - def value = provider.getStringExcludingSource("test.string.exclude", "defaultValue", - SystemPropertiesConfigSource) - def collected = ConfigCollector.get().collect() - - then: - // Default - def defaultSetting = collected.get(ConfigOrigin.DEFAULT).get("test.string.exclude") - defaultSetting.stringValue() == "defaultValue" - - // ENV (should be used since JVM is excluded) - def envSetting = collected.get(ConfigOrigin.ENV).get("test.string.exclude") - envSetting.stringValue() == "envValue" - envSetting.origin == ConfigOrigin.ENV - - // Should return ENV value since JVM source was excluded - value == "envValue" - } - - def "ConfigProvider getMergedMap merges maps from multiple sources with correct precedence"() { - setup: - ConfigCollector.get().collect() // clear previous state - - // Set up: env (partial map), jvm (partial map with overlap) - injectEnvConfig("DD_TEST_MAP", "env_key:env_value,shared:from_env") - injectSysConfig("test.map", "jvm_key:jvm_value,shared:from_jvm") - def provider = ConfigProvider.createDefault() - - when: - def result = provider.getMergedMap("test.map") - def collected = ConfigCollector.get().collect() - - then: - // Result should be merged with JVM taking precedence - result == [ - "env_key": "env_value", // from ENV - "jvm_key": "jvm_value", // from JVM - "shared": "from_jvm" // JVM overrides ENV - ] - - // Default should be reported as empty - def defaultSetting = collected.get(ConfigOrigin.DEFAULT).get("test.map") - defaultSetting.value == [:] - defaultSetting.origin == ConfigOrigin.DEFAULT - - // ENV map should be reported - def envSetting = collected.get(ConfigOrigin.ENV).get("test.map") - envSetting.value == ["env_key": "env_value", "shared": "from_env"] - envSetting.origin == ConfigOrigin.ENV - - // JVM map should be reported - def jvmSetting = collected.get(ConfigOrigin.JVM_PROP).get("test.map") - jvmSetting.value == ["jvm_key": "jvm_value", "shared": "from_jvm"] - jvmSetting.origin == ConfigOrigin.JVM_PROP - - // Final calculated result should be reported with highest seqId - def calculatedSetting = collected.get(ConfigOrigin.CALCULATED).get("test.map") - calculatedSetting.value == result - calculatedSetting.origin == ConfigOrigin.CALCULATED - - // Calculated should have highest seqId - def maxSeqId = [defaultSetting.seqId, envSetting.seqId, jvmSetting.seqId, calculatedSetting.seqId].max() - calculatedSetting.seqId == maxSeqId - } - - def "ConfigProvider getMergedTagsMap handles trace tags format"() { - setup: - ConfigCollector.get().collect() // clear previous state - - // Set up trace tags format (can use comma or space separators) - injectEnvConfig("DD_TEST_TAGS", "service:web,version:1.0") - injectSysConfig("test.tags", "env:prod team:backend") - def provider = ConfigProvider.createDefault() - - when: - def result = provider.getMergedTagsMap("test.tags") - def collected = ConfigCollector.get().collect() - - then: - // Should merge both tag formats - result == [ - "service": "web", // from ENV - "version": "1.0", // from ENV - "env": "prod", // from JVM - "team": "backend" // from JVM - ] - - // Should report individual sources and calculated result - def envSetting = collected.get(ConfigOrigin.ENV).get("test.tags") - envSetting.value == ["service": "web", "version": "1.0"] - - def jvmSetting = collected.get(ConfigOrigin.JVM_PROP).get("test.tags") - jvmSetting.value == ["env": "prod", "team": "backend"] - - def calculatedSetting = collected.get(ConfigOrigin.CALCULATED).get("test.tags") - calculatedSetting.value == result - } - - def "ConfigProvider getMergedMapWithOptionalMappings handles multiple keys and transformations"() { - setup: - ConfigCollector.get().collect() // clear previous state - - // Set up multiple keys with optional mappings - injectEnvConfig("DD_HEADER_1", "X-Custom-Header:custom.tag") - injectSysConfig("header.1", "X-Auth:auth.tag") - injectEnvConfig("DD_HEADER_2", "X-Request-ID") // key only, should get prefix - def provider = ConfigProvider.createDefault() - - when: - def result = provider.getMergedMapWithOptionalMappings("trace.http", true, "header.1", "header.2") - def collected = ConfigCollector.get().collect() - - then: - // Should merge with transformations - result.size() >= 2 - result["x-custom-header"] == "custom.tag" // from ENV header.1 - result["x-auth"] == "auth.tag" // from JVM header.1 - result["x-request-id"] != null // from ENV header.2, should get prefix - - // Should report sources and calculated result - def calculatedSetting = collected.get(ConfigOrigin.CALCULATED).get("header.2") // Last key processed - calculatedSetting != null - calculatedSetting.origin == ConfigOrigin.CALCULATED - } - - def "ConfigProvider getOrderedMap preserves insertion order and merges sources"() { - setup: - ConfigCollector.get().collect() // clear previous state - - // Set up ordered maps from multiple sources - injectEnvConfig("DD_TEST_ORDERED_MAP", "first:env_first,second:env_second,third:env_third") - injectSysConfig("test.ordered.map", "second:jvm_second,fourth:jvm_fourth,first:jvm_first") - def provider = ConfigProvider.createDefault() - - when: - def result = provider.getOrderedMap("test.ordered.map") - def collected = ConfigCollector.get().collect() - - then: - // Result should be a LinkedHashMap with preserved order and JVM precedence - result instanceof LinkedHashMap - result == [ - "first": "jvm_first", // JVM overrides ENV, appears first due to ENV order - "second": "jvm_second", // JVM overrides ENV, appears second due to ENV order - "third": "env_third", // only in ENV, appears third due to ENV order - "fourth": "jvm_fourth" // only in JVM, appears last due to JVM addition - ] - - // Verify order is preserved (LinkedHashMap maintains insertion order) - def keys = result.keySet() as List - keys == ["first", "second", "third", "fourth"] - - // Default should be reported as empty - def defaultSetting = collected.get(ConfigOrigin.DEFAULT).get("test.ordered.map") - defaultSetting.value == [:] - defaultSetting.origin == ConfigOrigin.DEFAULT - - // ENV ordered map should be reported - def envSetting = collected.get(ConfigOrigin.ENV).get("test.ordered.map") - envSetting.value == ["first": "env_first", "second": "env_second", "third": "env_third"] - envSetting.origin == ConfigOrigin.ENV - - // JVM ordered map should be reported - def jvmSetting = collected.get(ConfigOrigin.JVM_PROP).get("test.ordered.map") - jvmSetting.value == ["second": "jvm_second", "fourth": "jvm_fourth", "first": "jvm_first"] - jvmSetting.origin == ConfigOrigin.JVM_PROP - - // Final calculated result should be reported with highest seqId - def calculatedSetting = collected.get(ConfigOrigin.CALCULATED).get("test.ordered.map") - calculatedSetting.value == result - calculatedSetting.origin == ConfigOrigin.CALCULATED - - // Calculated should have highest seqId - def maxSeqId = [defaultSetting.seqId, envSetting.seqId, jvmSetting.seqId, calculatedSetting.seqId].max() - calculatedSetting.seqId == maxSeqId - } - - def "ConfigProvider methods that call getString internally report their own defaults before getString's null default"() { - setup: - ConfigCollector.get().collect() // clear previous state - // No environment or system property values set, so methods should fall back to their defaults - def provider = ConfigProvider.createDefault() - - when: - def enumResult = provider.getEnum("test.enum", ConfigOrigin, ConfigOrigin.CODE) - def listResult = provider.getList("test.list", ["default", "list"]) - def setResult = provider.getSet("test.set", ["default", "set"] as Set) - def rangeResult = provider.getIntegerRange("test.range", new BitSet()) - def collected = ConfigCollector.get().collect() - - then: - // Each method should have reported its own default, not getString's null default - - def enumDefault = collected.get(ConfigOrigin.DEFAULT).get("test.enum") - enumDefault.stringValue() == "CODE" // ConfigOrigin.CODE.name() - enumDefault.origin == ConfigOrigin.DEFAULT - enumDefault.seqId == ConfigSetting.DEFAULT_SEQ_ID - - def listDefault = collected.get(ConfigOrigin.DEFAULT).get("test.list") - listDefault.value == ["default", "list"] - listDefault.origin == ConfigOrigin.DEFAULT - listDefault.seqId == ConfigSetting.DEFAULT_SEQ_ID - - def setDefault = collected.get(ConfigOrigin.DEFAULT).get("test.set") - setDefault.value == ["default", "set"] as Set - setDefault.origin == ConfigOrigin.DEFAULT - setDefault.seqId == ConfigSetting.DEFAULT_SEQ_ID - - def rangeDefault = collected.get(ConfigOrigin.DEFAULT).get("test.range") - rangeDefault.value == new BitSet() - rangeDefault.origin == ConfigOrigin.DEFAULT - rangeDefault.seqId == ConfigSetting.DEFAULT_SEQ_ID - - // Verify the methods returned their default values (not null) - enumResult == ConfigOrigin.CODE - listResult == ["default", "list"] - setResult == ["default", "set"] as Set - rangeResult == new BitSet() - } - - // NOTE: This is a case that SHOULD never occur. #reReportToCollector(String, int) should only be called with valid origins - def "ConfigValueResolver reReportToCollector handles null origin gracefully"() { - setup: - ConfigCollector.get().collect() // clear previous state - ConfigProvider.ConfigValueResolver resolver = ConfigProvider.ConfigValueResolver.of("1") - - when: - resolver.reReportToCollector("test.key", 5) - - then: - 0 * ConfigCollector.get().put(_, _, _, _, _) - } - - def "ConfigMergeResolver reports correct origin for single vs multiple source contributions"() { - setup: - ConfigCollector.get().collect() // clear previous state - def provider = ConfigProvider.createDefault() - - when: "Only ENV source contributes to merged map" - injectEnvConfig("DD_SINGLE_SOURCE_MAP", "key1:value1,key2:value2") - // No JVM prop set, so only ENV contributes - def singleSourceResult = provider.getMergedMap("single.source.map") - def singleSourceCollected = ConfigCollector.get().collect() - - then: "Should report with ENV origin, not CALCULATED" - singleSourceResult == ["key1": "value1", "key2": "value2"] - - // Should have DEFAULT for default value - def singleDefault = singleSourceCollected.get(ConfigOrigin.DEFAULT)?.get("single.source.map") - singleDefault?.value == [:] - - // Should have ENV for the actual value (not CALCULATED) - def singleEnv = singleSourceCollected.get(ConfigOrigin.ENV)?.get("single.source.map") - singleEnv?.value == ["key1": "value1", "key2": "value2"] - singleEnv?.origin == ConfigOrigin.ENV - - // Should NOT have CALCULATED entry since only one source contributed - singleSourceCollected.get(ConfigOrigin.CALCULATED)?.get("single.source.map") == null - - when: "Multiple sources contribute to merged map" - ConfigCollector.get().collect() // clear for next test - injectEnvConfig("DD_MULTI_SOURCE_MAP", "env_key:env_value,shared:from_env") - injectSysConfig("multi.source.map", "jvm_key:jvm_value,shared:from_jvm") - def multiSourceResult = provider.getMergedMap("multi.source.map") - def multiSourceCollected = ConfigCollector.get().collect() - - then: "Should report with CALCULATED origin when multiple sources contribute" - multiSourceResult == ["env_key": "env_value", "jvm_key": "jvm_value", "shared": "from_jvm"] - - // Should have CALCULATED for the final merged result - def multiCalculated = multiSourceCollected.get(ConfigOrigin.CALCULATED)?.get("multi.source.map") - multiCalculated?.value == multiSourceResult - multiCalculated?.origin == ConfigOrigin.CALCULATED - } -} diff --git a/internal-api/src/test/groovy/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSourceTest.groovy b/internal-api/src/test/groovy/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSourceTest.groovy deleted file mode 100644 index a6fa9a917d5..00000000000 --- a/internal-api/src/test/groovy/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSourceTest.groovy +++ /dev/null @@ -1,394 +0,0 @@ -package datadog.trace.bootstrap.config.provider - -import datadog.trace.test.util.DDSpecification -import spock.lang.Ignore - -import static datadog.trace.api.config.GeneralConfig.ENV -import static datadog.trace.api.config.GeneralConfig.LOG_LEVEL -import static datadog.trace.api.config.GeneralConfig.RUNTIME_METRICS_ENABLED -import static datadog.trace.api.config.GeneralConfig.SERVICE_NAME -import static datadog.trace.api.config.GeneralConfig.TAGS -import static datadog.trace.api.config.GeneralConfig.VERSION -import static datadog.trace.api.config.OtlpConfig.OTEL_TRACES_SPAN_METRICS_ENABLED -import static datadog.trace.api.config.OtlpConfig.TRACE_OTEL_ENABLED -import static datadog.trace.api.config.OtlpConfig.TRACE_OTEL_EXPORTER -import static datadog.trace.api.config.TraceInstrumentationConfig.TRACE_ENABLED -import static datadog.trace.api.config.TraceInstrumentationConfig.TRACE_EXTENSIONS_PATH -import static datadog.trace.api.config.TracerConfig.REQUEST_HEADER_TAGS -import static datadog.trace.api.config.TracerConfig.RESPONSE_HEADER_TAGS -import static datadog.trace.api.config.TracerConfig.TRACE_PROPAGATION_STYLE -import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLE_RATE - -class OtelEnvironmentConfigSourceTest extends DDSpecification { - - // ignore this test when we enable the OpenTelemetry integration by default - def "no otel system properties are mapped by default"() { - setup: - injectSysConfig('otel.service.name', 'TEST_SERVICE', false) - injectSysConfig('otel.propagators', 'xray,b3,datadog', false) - injectSysConfig('otel.traces.sampler', 'parentbased_traceidratio', false) - injectSysConfig('otel.traces.sampler.arg', '0.5', false) - injectSysConfig('otel.traces.exporter', 'none', false) - injectSysConfig('otel.metrics.exporter', 'none', false) - injectSysConfig('otel.logs.exporter', 'none', false) - injectSysConfig('otel.resource.attributes', 'service.name=DEV_SERVICE', false) - injectSysConfig('otel.instrumentation.http.client.capture-request-headers', 'content-type', false) - injectSysConfig('otel.instrumentation.http.client.capture-response-headers', 'content-length', false) - injectSysConfig('otel.instrumentation.http.server.capture-request-headers', 'custom-header', false) - injectSysConfig('otel.instrumentation.http.server.capture-response-headers', 'another-header', false) - injectSysConfig('otel.javaagent.extensions', '/opt/opentelemetry/extensions', false) - - when: - def source = new OtelEnvironmentConfigSource() - - then: - source.get(TRACE_OTEL_ENABLED) == null - source.get(LOG_LEVEL) == null - source.get(SERVICE_NAME) == null - source.get(VERSION) == null - source.get(ENV) == null - source.get(TAGS) == null - source.get(TRACE_PROPAGATION_STYLE) == null - source.get(TRACE_SAMPLE_RATE) == null - source.get(TRACE_ENABLED) == null - source.get(RUNTIME_METRICS_ENABLED) == null - source.get(REQUEST_HEADER_TAGS) == null - source.get(RESPONSE_HEADER_TAGS) == null - source.get(TRACE_EXTENSIONS_PATH) == null - } - - // ignore this test when we enable the OpenTelemetry integration by default - def "no otel environment variables are mapped by default"() { - setup: - injectEnvConfig('OTEL_LOG_LEVEL', 'debug', false) - injectEnvConfig('OTEL_SERVICE_NAME', 'TEST_SERVICE', false) - injectEnvConfig('OTEL_PROPAGATORS', 'xray,b3,datadog', false) - injectEnvConfig('OTEL_TRACES_SAMPLER', 'parentbased_traceidratio', false) - injectEnvConfig('OTEL_TRACES_SAMPLER_ARG', '0.5', false) - injectEnvConfig('OTEL_TRACES_EXPORTER', 'none', false) - injectEnvConfig('OTEL_METRICS_EXPORTER', 'none', false) - injectEnvConfig('OTEL_LOGS_EXPORTER', 'none', false) - injectEnvConfig('OTEL_RESOURCE_ATTRIBUTES', 'service.name=DEV_SERVICE', false) - injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_CLIENT_CAPTURE_REQUEST_HEADERS', 'content-type', false) - injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_CLIENT_CAPTURE_RESPONSE_HEADERS', 'content-length', false) - injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_SERVER_CAPTURE_REQUEST_HEADERS', 'custom-header', false) - injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_SERVER_CAPTURE_RESPONSE_HEADERS', 'another-header', false) - injectEnvConfig('OTEL_JAVAAGENT_EXTENSIONS', '/opt/opentelemetry/extensions', false) - - when: - def source = new OtelEnvironmentConfigSource() - - then: - source.get(TRACE_OTEL_ENABLED) == null - source.get(LOG_LEVEL) == null - source.get(SERVICE_NAME) == null - source.get(VERSION) == null - source.get(ENV) == null - source.get(TAGS) == null - source.get(TRACE_PROPAGATION_STYLE) == null - source.get(TRACE_SAMPLE_RATE) == null - source.get(TRACE_ENABLED) == null - source.get(RUNTIME_METRICS_ENABLED) == null - source.get(REQUEST_HEADER_TAGS) == null - source.get(RESPONSE_HEADER_TAGS) == null - source.get(TRACE_EXTENSIONS_PATH) == null - } - - @Ignore // enable this test when we enable the OpenTelemetry integration by default - def "disabling otel with system property disables otel integration"() { - setup: - injectSysConfig('otel.sdk.disabled', 'true', false) - injectSysConfig('otel.service.name', 'TEST_SERVICE', false) - injectSysConfig('otel.propagators', 'xray,b3,datadog', false) - injectSysConfig('otel.traces.sampler', 'parentbased_traceidratio', false) - injectSysConfig('otel.traces.sampler.arg', '0.5', false) - injectSysConfig('otel.traces.exporter', 'none', false) - injectSysConfig('otel.metrics.exporter', 'none', false) - injectSysConfig('otel.logs.exporter', 'none', false) - injectSysConfig('otel.resource.attributes', 'service.name=DEV_SERVICE', false) - injectSysConfig('otel.instrumentation.http.client.capture-request-headers', 'content-type', false) - injectSysConfig('otel.instrumentation.http.client.capture-response-headers', 'content-length', false) - injectSysConfig('otel.instrumentation.http.server.capture-request-headers', 'custom-header', false) - injectSysConfig('otel.instrumentation.http.server.capture-response-headers', 'another-header', false) - injectSysConfig('otel.javaagent.extensions', '/opt/opentelemetry/extensions', false) - - when: - def source = new OtelEnvironmentConfigSource() - - then: - source.get(TRACE_OTEL_ENABLED) == 'false' - source.get(LOG_LEVEL) == null - source.get(SERVICE_NAME) == null - source.get(VERSION) == null - source.get(ENV) == null - source.get(TAGS) == null - source.get(TRACE_PROPAGATION_STYLE) == null - source.get(TRACE_SAMPLE_RATE) == null - source.get(TRACE_ENABLED) == null - source.get(RUNTIME_METRICS_ENABLED) == null - source.get(REQUEST_HEADER_TAGS) == null - source.get(RESPONSE_HEADER_TAGS) == null - source.get(TRACE_EXTENSIONS_PATH) == null - } - - @Ignore // enable this test when we enable the OpenTelemetry integration by default - def "disabling otel with environment variable disables otel integration"() { - setup: - injectEnvConfig('OTEL_SDK_DISABLED', 'true', false) - injectEnvConfig('OTEL_LOG_LEVEL', 'debug', false) - injectEnvConfig('OTEL_SERVICE_NAME', 'TEST_SERVICE', false) - injectEnvConfig('OTEL_PROPAGATORS', 'xray,b3,datadog', false) - injectEnvConfig('OTEL_TRACES_SAMPLER', 'parentbased_traceidratio', false) - injectEnvConfig('OTEL_TRACES_SAMPLER_ARG', '0.5', false) - injectEnvConfig('OTEL_TRACES_EXPORTER', 'none', false) - injectEnvConfig('OTEL_METRICS_EXPORTER', 'none', false) - injectEnvConfig('OTEL_LOGS_EXPORTER', 'none', false) - injectEnvConfig('OTEL_RESOURCE_ATTRIBUTES', 'service.name=DEV_SERVICE', false) - injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_CLIENT_CAPTURE_REQUEST_HEADERS', 'content-type', false) - injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_CLIENT_CAPTURE_RESPONSE_HEADERS', 'content-length', false) - injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_SERVER_CAPTURE_REQUEST_HEADERS', 'custom-header', false) - injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_SERVER_CAPTURE_RESPONSE_HEADERS', 'another-header', false) - injectEnvConfig('OTEL_JAVAAGENT_EXTENSIONS', '/opt/opentelemetry/extensions', false) - - when: - def source = new OtelEnvironmentConfigSource() - - then: - source.get(TRACE_OTEL_ENABLED) == 'false' - source.get(LOG_LEVEL) == null - source.get(SERVICE_NAME) == null - source.get(VERSION) == null - source.get(ENV) == null - source.get(TAGS) == null - source.get(TRACE_PROPAGATION_STYLE) == null - source.get(TRACE_SAMPLE_RATE) == null - source.get(TRACE_ENABLED) == null - source.get(RUNTIME_METRICS_ENABLED) == null - source.get(REQUEST_HEADER_TAGS) == null - source.get(RESPONSE_HEADER_TAGS) == null - source.get(TRACE_EXTENSIONS_PATH) == null - } - - def "otel system properties are mapped when otel is enabled"() { - setup: - injectSysConfig('dd.trace.otel.enabled', 'true', false) - injectSysConfig('otel.service.name', 'TEST_SERVICE', false) - injectSysConfig('otel.propagators', 'xray,b3,datadog', false) - injectSysConfig('otel.traces.sampler', 'parentbased_traceidratio', false) - injectSysConfig('otel.traces.sampler.arg', '0.5', false) - injectSysConfig('otel.traces.exporter', 'none', false) - injectSysConfig('otel.metrics.exporter', 'none', false) - injectSysConfig('otel.logs.exporter', 'none', false) - injectSysConfig('otel.resource.attributes', 'service.name=DEV_SERVICE', false) - injectSysConfig('otel.instrumentation.http.client.capture-request-headers', 'content-type', false) - injectSysConfig('otel.instrumentation.http.client.capture-response-headers', 'content-length', false) - injectSysConfig('otel.instrumentation.http.server.capture-request-headers', 'custom-header', false) - injectSysConfig('otel.instrumentation.http.server.capture-response-headers', 'another-header', false) - injectSysConfig('otel.javaagent.extensions', '/opt/opentelemetry/extensions', false) - - when: - def source = new OtelEnvironmentConfigSource() - - then: - source.get(SERVICE_NAME) == 'TEST_SERVICE' // value from otel.service.name overrides the one from otel.resource.attributes - source.get(TRACE_PROPAGATION_STYLE) == 'xray,b3single,datadog' - source.get(TRACE_SAMPLE_RATE) == '0.5' - source.get(TRACE_ENABLED) == 'false' - source.get(RUNTIME_METRICS_ENABLED) == 'false' - source.get(REQUEST_HEADER_TAGS) == 'content-type:http.request.header.content-type,custom-header:http.request.header.custom-header' - source.get(RESPONSE_HEADER_TAGS) == 'content-length:http.response.header.content-length,another-header:http.response.header.another-header' - source.get(TRACE_EXTENSIONS_PATH) == '/opt/opentelemetry/extensions' - } - - def "otel environment variables are mapped when otel is enabled"() { - setup: - injectEnvConfig('DD_TRACE_OTEL_ENABLED', 'true', false) - injectEnvConfig('OTEL_LOG_LEVEL', 'debug', false) - injectEnvConfig('OTEL_SERVICE_NAME', 'TEST_SERVICE', false) - injectEnvConfig('OTEL_PROPAGATORS', 'xray,b3,datadog', false) - injectEnvConfig('OTEL_TRACES_SAMPLER', 'parentbased_traceidratio', false) - injectEnvConfig('OTEL_TRACES_SAMPLER_ARG', '0.5', false) - injectEnvConfig('OTEL_TRACES_EXPORTER', 'none', false) - injectEnvConfig('OTEL_METRICS_EXPORTER', 'none', false) - injectEnvConfig('OTEL_LOGS_EXPORTER', 'none', false) - injectEnvConfig('OTEL_RESOURCE_ATTRIBUTES', 'service.name=DEV_SERVICE', false) - injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_CLIENT_CAPTURE_REQUEST_HEADERS', 'content-type', false) - injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_CLIENT_CAPTURE_RESPONSE_HEADERS', 'content-length', false) - injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_SERVER_CAPTURE_REQUEST_HEADERS', 'custom-header', false) - injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_SERVER_CAPTURE_RESPONSE_HEADERS', 'another-header', false) - injectEnvConfig('OTEL_JAVAAGENT_EXTENSIONS', '/opt/opentelemetry/extensions', false) - - when: - def source = new OtelEnvironmentConfigSource() - - then: - source.get(LOG_LEVEL) == 'debug' - source.get(SERVICE_NAME) == 'TEST_SERVICE' // value from OTEL_SERVICE_NAME overrides the one from OTEL_RESOURCE_ATTRIBUTES - source.get(TRACE_PROPAGATION_STYLE) == 'xray,b3single,datadog' - source.get(TRACE_SAMPLE_RATE) == '0.5' - source.get(TRACE_ENABLED) == 'false' - source.get(RUNTIME_METRICS_ENABLED) == 'false' - source.get(REQUEST_HEADER_TAGS) == 'content-type:http.request.header.content-type,custom-header:http.request.header.custom-header' - source.get(RESPONSE_HEADER_TAGS) == 'content-length:http.response.header.content-length,another-header:http.response.header.another-header' - source.get(TRACE_EXTENSIONS_PATH) == '/opt/opentelemetry/extensions' - } - - def "otel traces exporter otlp system property is mapped"() { - setup: - injectSysConfig('dd.trace.otel.enabled', 'true', false) - injectSysConfig('otel.traces.exporter', 'otlp', false) - - when: - def source = new OtelEnvironmentConfigSource() - - then: - source.get(TRACE_ENABLED) == null - source.get(TRACE_OTEL_EXPORTER) == 'otlp' - } - - def "otel traces exporter otlp environment variable is mapped"() { - setup: - injectEnvConfig('DD_TRACE_OTEL_ENABLED', 'true', false) - injectEnvConfig('OTEL_TRACES_EXPORTER', 'otlp', false) - - when: - def source = new OtelEnvironmentConfigSource() - - then: - source.get(TRACE_ENABLED) == null - source.get(TRACE_OTEL_EXPORTER) == 'otlp' - } - - def "otel traces span metrics enabled system property is mapped when otel is enabled"() { - setup: - injectSysConfig('dd.trace.otel.enabled', 'true', false) - injectSysConfig('otel.traces.span.metrics.enabled', value, false) - - when: - def source = new OtelEnvironmentConfigSource() - - then: - source.get(OTEL_TRACES_SPAN_METRICS_ENABLED) == value - - where: - value << ['true', 'false'] - } - - def "otel traces span metrics enabled environment variable is mapped when otel is enabled"() { - setup: - injectEnvConfig('DD_TRACE_OTEL_ENABLED', 'true', false) - injectEnvConfig('OTEL_TRACES_SPAN_METRICS_ENABLED', value, false) - - when: - def source = new OtelEnvironmentConfigSource() - - then: - source.get(OTEL_TRACES_SPAN_METRICS_ENABLED) == value - - where: - value << ['true', 'false'] - } - - def "otel traces span metrics enabled is not mapped when otel is disabled"() { - setup: - // Without dd.trace.otel.enabled, setupTraceOtelEnvironment() does not run. - injectSysConfig('otel.traces.span.metrics.enabled', 'true', false) - - when: - def source = new OtelEnvironmentConfigSource() - - then: - source.get(OTEL_TRACES_SPAN_METRICS_ENABLED) == null - } - - def "otel traces exporter none still disables tracing"() { - setup: - injectEnvConfig('DD_TRACE_OTEL_ENABLED', 'true', false) - injectEnvConfig('OTEL_TRACES_EXPORTER', 'none', false) - - when: - def source = new OtelEnvironmentConfigSource() - - then: - source.get(TRACE_ENABLED) == 'false' - } - - def "otel resource attributes system property is mapped"() { - setup: - injectSysConfig('dd.trace.otel.enabled', 'true', false) - injectSysConfig('otel.resource.attributes', - 'key1=one,' + - 'key2=two,' + - 'key3=three,' + - 'service.name=DEV_SERVICE,' + - 'key4=four,' + - 'key5=five,' + - 'key6=six,' + - 'deployment.environment.name=staging,' + - 'key7=seven,' + - 'key8=eight,' + - 'key9=nine,' + - 'service.version=42,'+ - 'key10=ten,'+ - 'key11=eleven,'+ - 'key12=twelve', false) - - when: - def source = new OtelEnvironmentConfigSource() - - then: - source.get(SERVICE_NAME) == 'DEV_SERVICE' - source.get(ENV) == 'staging' - source.get(VERSION) == '42' - source.get(TAGS) == 'key1:one,key2:two,key3:three,key4:four,key5:five,key6:six,key7:seven,key8:eight,key9:nine,key10:ten,key11:eleven,key12:twelve' - } - - def "otel resource attributes environment variable is mapped"() { - setup: - injectEnvConfig('DD_TRACE_OTEL_ENABLED', 'true', false) - injectEnvConfig('OTEL_RESOURCE_ATTRIBUTES', - 'key1=one,' + - 'key2=two,' + - 'key3=three,' + - 'service.name=DEV_SERVICE,' + - 'key4=four,' + - 'key5=five,' + - 'key6=six,' + - 'deployment.environment=staging,' + - 'key7=seven,' + - 'key8=eight,' + - 'key9=nine,' + - 'service.version=42,'+ - 'key10=ten,'+ - 'key11=eleven,'+ - 'key12=twelve', false) - - when: - def source = new OtelEnvironmentConfigSource() - - then: - source.get(SERVICE_NAME) == 'DEV_SERVICE' - source.get(ENV) == 'staging' - source.get(VERSION) == '42' - source.get(TAGS) == 'key1:one,key2:two,key3:three,key4:four,key5:five,key6:six,key7:seven,key8:eight,key9:nine,key10:ten,key11:eleven,key12:twelve' - } - - def "named deployment environment takes precedence over legacy attribute"() { - setup: - injectSysConfig('dd.trace.otel.enabled', 'true', false) - injectSysConfig('otel.resource.attributes', resourceAttributes, false) - - when: - def source = new OtelEnvironmentConfigSource() - - then: - source.get(ENV) == 'production' - source.get(TAGS) == null - - where: - resourceAttributes << [ - 'deployment.environment.name=production,deployment.environment=staging', - 'deployment.environment=staging,deployment.environment.name=production' - ] - } -} diff --git a/internal-api/src/test/groovy/datadog/trace/bootstrap/config/provider/StableConfigParserTest.groovy b/internal-api/src/test/groovy/datadog/trace/bootstrap/config/provider/StableConfigParserTest.groovy deleted file mode 100644 index bd814902e9d..00000000000 --- a/internal-api/src/test/groovy/datadog/trace/bootstrap/config/provider/StableConfigParserTest.groovy +++ /dev/null @@ -1,354 +0,0 @@ -package datadog.trace.bootstrap.config.provider -import datadog.trace.test.util.DDSpecification -import java.nio.file.Files -import java.nio.file.Path -import datadog.trace.config.inversion.ConfigHelper - -class StableConfigParserTest extends DDSpecification { - - def strictness - - def setup(){ - strictness = ConfigHelper.get().configInversionStrictFlag() - ConfigHelper.get().setConfigInversionStrict(ConfigHelper.StrictnessPolicy.TEST) - } - - def cleanup(){ - ConfigHelper.get().setConfigInversionStrict(strictness) - } - def "test parse valid"() { - when: - Path filePath = Files.createTempFile("testFile_", ".yaml") - then: - if (filePath == null) { - throw new AssertionError("Failed to create: " + filePath) - } - - when: - injectEnvConfig("DD_SERVICE", "mysvc") - // From the below yaml, only apm_configuration_default and the second selector should be applied: We use the first matching rule and discard the rest - String yaml = """ -config_id: 12345 -apm_configuration_default: - KEY_ONE: "default" - KEY_TWO: true -apm_configuration_rules: - - selectors: - - origin: language - matches: ["golang"] - operator: equals - configuration: - KEY_ONE: "ignored" - - selectors: - - origin: language - matches: ["Java"] - operator: equals - configuration: - KEY_ONE: "rules" - KEY_THREE: 1 - - selectors: - - origin: environment_variables - key: "DD_SERVICE" - operator: equals - matches: ["mysvc"] - configuration: - KEY_FOUR: "ignored" - - selectors: - - origin: process_arguments - key: "-Darg1" - operator: exists - configuration: - KEY_FIVE: "ignored" -""" - Files.write(filePath, yaml.getBytes()) - StableConfigSource.StableConfig cfg = StableConfigParser.parse(filePath.toString()) - - then: - def keys = cfg.getKeys() - keys.size() == 3 - cfg.getConfigId().trim() == ("12345") - cfg.get("KEY_ONE") == "rules" - cfg.get("KEY_TWO") == "true" - cfg.get("KEY_THREE") == "1" - Files.delete(filePath) - } - - def "test parse and template"() { - when: - Path filePath = Files.createTempFile("testFile_", ".yaml") - then: - if (filePath == null) { - throw new AssertionError("Failed to create: " + filePath) - } - - when: - String yaml = """ - apm_configuration_rules: - - selectors: - - origin: process_arguments - key: "-Dtest_parse_and_template" - operator: exists - configuration: - DD_SERVICE: {{process_arguments['-Dtest_parse_and_template']}} -""" - System.setProperty("test_parse_and_template", "myservice") - Files.write(filePath, yaml.getBytes()) - StableConfigSource.StableConfig cfg = StableConfigParser.parse(filePath.toString()) - - then: - cfg.get("DD_SERVICE") == "myservice" - } - - def "test selectorMatch"() { - when: - // Env vars - injectEnvConfig("DD_PROFILING_ENABLED", "true") - injectEnvConfig("DD_SERVICE", "mysvc") - injectEnvConfig("DD_TAGS", "team:apm,component:web") - System.setProperty("test_selectorMatch", "value1") - - def match = StableConfigParser.selectorMatch(origin, matches, operator, key) - - then: - match == expectMatch - - where: - origin | matches | operator | key | expectMatch - "language" | ["java"] | "equals" | "" | true - "LANGUAGE" | ["JaVa"] | "EQUALS" | "" | true // check case insensitivity - "language" | ["java", "golang"] | "equals" | "" | true - "language" | ["java"] | "starts_with" | "" | true - "language" | ["golang"] | "equals" | "" | false - "language" | ["java"] | "exists" | "" | false - "language" | ["java"] | "something unexpected" | "" | false - "environment_variables" | [] | "exists" | "DD_TAGS" | true - "environment_variables" | null | "exists" | "DD_TAGS" | true - "environment_variables" | ["team:apm"] | "contains" | "DD_TAGS" | true - "ENVIRONMENT_VARIABLES" | ["TeAm:ApM"] | "CoNtAiNs" | "Dd_TaGs" | true // check case insensitivity - "environment_variables" | ["team:apm"] | "equals" | "DD_TAGS" | false - "environment_variables" | ["team:apm"] | "starts_with" | "DD_TAGS" | true - "environment_variables" | ["true"] | "equals" | "DD_PROFILING_ENABLED" | true - "environment_variables" | ["abcdefg"] | "equals" | "DD_PROFILING_ENABLED" | false - "environment_variables" | ["true"] | "equals" | "DD_PROFILING_ENABLED" | true - "environment_variables" | ["mysvc", "othersvc"] | "equals" | "DD_SERVICE" | true - "environment_variables" | ["my"] | "starts_with" | "DD_SERVICE" | true - "environment_variables" | ["svc"] | "ends_with" | "DD_SERVICE" | true - "environment_variables" | ["svc"] | "contains" | "DD_SERVICE" | true - "environment_variables" | ["other"] | "contains" | "DD_SERVICE" | false - "environment_variables" | [null] | "contains" | "DD_SERVICE" | false - "environment_variables" | [] | "equals" | null | false - "environment_variables" | null | "equals" | "DD_SERVICE" | false - "language" | ["java"] | null | "" | false - "process_arguments" | null | "exists" | "-Dtest_selectorMatch" | true - "process_arguments" | null | "exists" | "-Darg2" | false - "process_arguments" | ["value1"] | "equals" | "-Dtest_selectorMatch" | true - "process_arguments" | ["value2"] | "equals" | "-Dtest_selectorMatch" | false - } - - def "test duplicate entries not allowed"() { - when: - Path filePath = Files.createTempFile("testFile_", ".yaml") - then: - if (filePath == null) { - throw new AssertionError("Failed to create: " + filePath) - } - - when: - String yaml = """ - config_id: 12345 - config_id: 67890 - """ - Files.write(filePath, yaml.getBytes()) - StableConfigParser.parse(filePath.toString()) - - then: - def ex = thrown(RuntimeException) - - and: - ex.message.contains "found duplicate key config_id" - } - - def "test config_id only"() { - when: - Path filePath = Files.createTempFile("testFile_", ".yaml") - then: - if (filePath == null) { - throw new AssertionError("Failed to create: " + filePath) - } - - when: - String yaml = """ - config_id: 12345 - """ - Files.write(filePath, yaml.getBytes()) - StableConfigSource.StableConfig cfg = StableConfigParser.parse(filePath.toString()) - - then: - cfg != null - cfg.getConfigId() == "12345" - cfg.getKeys().size() == 0 - } - - def "test parse invalid"() { - // If any piece of the file is invalid, the whole file is rendered invalid and an exception is thrown - when: - Path filePath = Files.createTempFile("testFile_", ".yaml") - then: - if (filePath == null) { - throw new AssertionError("Failed to create: " + filePath) - } - - when: - String yaml = """ - something-irrelevant: "" - config_id: 12345 - something : not : expected << and weird format - inufjka << - [a, - b, - c, - d] - apm_configuration_default: - KEY_ONE: value_one - KEY_TWO: "value_two" - KEY_THREE: 100 - KEY_FOUR: true - KEY_FIVE: [a,b,c,d] - something-else-irrelevant: value-irrelevant - """ - Files.write(filePath, yaml.getBytes()) - StableConfigSource.StableConfig cfg = null - Exception exception = null - try { - cfg = StableConfigParser.parse(filePath.toString()) - } catch (Exception e) { - exception = e - } - - then: - exception != null - cfg == null - Files.delete(filePath) - } - - def "test file over max size"() { - when: - Path filePath = Files.createTempFile("testFile_", ".yaml") - if (filePath == null) { - throw new AssertionError("Failed to create test file") - } - - // Create a file with valid contents, but bigger than MAX_FILE_SIZE_BYTES - String baseYaml = """ -config_id: 12345 -apm_configuration_default: - KEY_ONE: "value_one" -apm_configuration_rules: -""" - String builderYaml = """ - - selectors: - - origin: language - matches: ["Java"] - operator: equals - configuration: - KEY_TWO: "value_two" -""" - String bigYaml = baseYaml - while(bigYaml.size() < StableConfigParser.MAX_FILE_SIZE_BYTES) { - bigYaml += builderYaml - } - - try { - Files.write(filePath, bigYaml.getBytes()) - } catch (IOException e) { - throw new AssertionError("Failed to write to file: ${e.message}") - } - - StableConfigSource.StableConfig cfg - try { - cfg = StableConfigParser.parse(filePath.toString()) - } catch (Exception e) { - throw new AssertionError("Failed to parse the file: ${e.message}") - } - - then: - cfg == StableConfigSource.StableConfig.EMPTY - } - - def "test processTemplate valid cases"() { - when: - if (envKey != null) { - injectEnvConfig(envKey, envVal) - } - - then: - StableConfigParser.processTemplate(templateVar) == expect - - where: - templateVar | envKey | envVal | expect - "{{environment_variables['DD_KEY']}}" | "DD_KEY" | "value" | "value" - "{{environment_variables['DD_KEY']}}" | null | null | "" - "{{}}" | null | null | "" - "{}" | null | null | "{}" - "{{environment_variables['dd_key']}}" | "DD_KEY" | "value" | "value" - "{{environment_variables['DD_KEY}}" | "DD_KEY" | "value" | "" - "header-{{environment_variables['DD_KEY']}}-footer" | "DD_KEY" | "value" | "header-value-footer" - "{{environment_variables['HEADER']}}{{environment_variables['DD_KEY']}}{{environment_variables['FOOTER']}}" | "DD_KEY" | "value" | "value" - } - - def "test processTemplate error cases"() { - when: - StableConfigParser.processTemplate(templateVar) - - then: - def e = thrown(IOException) - e.message == expect - - where: - templateVar | expect - "{{environment_variables['']}}" | "Empty environment variable name in template" - "{{environment_variables['DD_KEY']}" | "Unterminated template in config" - } - - def "test null and empty values in YAML"() { - given: - Path filePath = Files.createTempFile("testFile_", ".yaml") - - when: - String yaml = """ -config_id: "12345" -apm_configuration_default: -apm_configuration_rules: -""" - Files.write(filePath, yaml.getBytes()) - StableConfigSource.StableConfig cfg = StableConfigParser.parse(filePath.toString()) - - then: - cfg.getConfigId() == "12345" - cfg.getKeys().isEmpty() - - cleanup: - Files.delete(filePath) - } - - def "test completely empty values in YAML"() { - given: - Path filePath = Files.createTempFile("testFile_", ".yaml") - - when: - String yaml = """ -config_id: "12345" -apm_configuration_default: -apm_configuration_rules: -""" - Files.write(filePath, yaml.getBytes()) - StableConfigSource.StableConfig cfg = StableConfigParser.parse(filePath.toString()) - - then: - cfg.getConfigId() == "12345" - cfg.getKeys().isEmpty() - - cleanup: - Files.delete(filePath) - } -} diff --git a/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/BaggageTest.groovy b/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/BaggageTest.groovy deleted file mode 100644 index 7611a87b407..00000000000 --- a/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/BaggageTest.groovy +++ /dev/null @@ -1,88 +0,0 @@ -package datadog.trace.bootstrap.instrumentation.api - -import datadog.context.Context -import spock.lang.Specification - -class BaggageTest extends Specification { - def 'test empty baggage'() { - when: - def baggage = Baggage.empty() - - then: - baggage.asMap().isEmpty() - baggage.w3cHeader != null - baggage.w3cHeader.isEmpty() - - when: - baggage.addItem('key', 'value') - - then: - baggage.w3cHeader == null - } - - def 'test baggage creation'() { - setup: - def items = ['key1': 'value1', 'key2': 'value2'] - def header = 'key1=value1,key2=value2' - - when: - def baggage = Baggage.create(items) - - then: - baggage.asMap() == items - baggage.w3cHeader == null - - when: - baggage = Baggage.create(items, header) - - then: - baggage.asMap() == items - baggage.w3cHeader == header - } - - def 'test baggage header'() { - setup: - def items = ['key1': 'value1', 'key2': 'value2'] - def header = 'key1=value1,key2=value2' - def baggage = Baggage.create(items, header) - - when: - baggage.removeItem('missingKey') - - then: 'header is preserved' - baggage.w3cHeader == header - - when: - baggage.removeItem('key2') - - then: 'header is out of sync' - baggage.w3cHeader == null - - when: - baggage.w3cHeader = 'key1=value1' - - then: 'header is forced' - baggage.w3cHeader == 'key1=value1' - - when: - baggage.addItem('key3', 'value3') - - then: 'header is out of sync' - baggage.w3cHeader == null - } - - def 'test context storage'() { - given: - def baggage = Baggage.empty() - def context = Context.root() - - expect: - Baggage.fromContext(context) == null - - when: - context = context.with(baggage) - - then: - Baggage.fromContext(context) == baggage - } -} diff --git a/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/SpanLinkTest.groovy b/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/SpanLinkTest.groovy deleted file mode 100644 index a8227194fc6..00000000000 --- a/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/SpanLinkTest.groovy +++ /dev/null @@ -1,159 +0,0 @@ -package datadog.trace.bootstrap.instrumentation.api - - -import datadog.trace.api.DDSpanId -import datadog.trace.api.DDTraceId -import spock.lang.Specification - -import static datadog.trace.bootstrap.instrumentation.api.AgentSpanLink.DEFAULT_FLAGS -import static datadog.trace.bootstrap.instrumentation.api.AgentSpanLink.SAMPLED_FLAG - -class SpanLinkTest extends Specification { - def "test span link from context"() { - setup: - def traceId = DDTraceId.fromHex("11223344556677889900aabbccddeeff") - def spanId = DDSpanId.fromHex("123456789abcdef0") - AgentSpanContext context = Stub { - getTraceId() >> traceId - getSpanId() >> spanId - getSamplingPriority() >> (sampled ? 1 : 0) - } - - when: - def link = SpanLink.from(context) - - then: - link.traceId() == traceId - link.spanId() == spanId - link.traceFlags() == (sampled ? SAMPLED_FLAG : DEFAULT_FLAGS) - link.traceState() == "" - link.attributes() == SpanAttributes.EMPTY - - when: - link.toString() - - then: - notThrown(Exception) - - where: - sampled << [true, false] - } - - def "test span links api"() { - when: - def link = new SpanLink(null, 0L, 0 as byte, null, null) - - then: - link.traceId() == DDTraceId.ZERO - link.traceState() != null - link.traceState().isEmpty() - link.attributes() != null - link.attributes().isEmpty() - } - - def "test span link attributes api"() { - when: - def attributes = SpanAttributes.builder().build() - - then: - attributes.isEmpty() - - when: - attributes = SpanAttributes.builder().put('key', 'value').build() - - then: - !attributes.isEmpty() - } - - def "test span link attributes encoding"() { - setup: - def builder = SpanAttributes.builder() - - when: - builder.put('string', 'value') - builder.put('string-empty', '') - builder.put('string-null', null) - builder.put('bool', true) - builder.put('bool-false', false) - builder.put('long', 12345L) - builder.put('long-negative', -12345L) - builder.put('double', 67.89) - builder.put('double-negative', -67.89) - builder.putStringArray('string-array', ['abc', '', null, 'def']) - builder.putStringArray('string-array-null', null) - builder.putBooleanArray('bool-array', [true, false, null, Boolean.TRUE, Boolean.FALSE]) - builder.putStringArray('bool-array-null', null) - builder.putLongArray('long-array', [123L, 456L, null, Long.MIN_VALUE, Long.MAX_VALUE]) - builder.putStringArray('long-array-null', null) - builder.putDoubleArray('double-array', [12.3D, 45.6D, null, Double.MIN_VALUE, Double.MAX_VALUE]) - builder.putStringArray('double-array-null', null) - def map = builder.build().asMap() - - then: - map['string'] == 'value' - map['string-empty'] == '' - !map.containsKey('string-null') - map['bool'] == 'true' - map['bool-false'] == 'false' - map['long'] == '12345' - map['long-negative'] == '-12345' - map['double'] == '67.89' - map['double-negative'] == '-67.89' - - map['string-array.0'] == 'abc' - map['string-array.1'] == '' - !map.containsKey('string-array.2') - map['string-array.3'] == 'def' - !map.containsKey('string-array-null') - - map['bool-array.0'] == 'true' - map['bool-array.1'] == 'false' - !map.containsKey('bool-array.2') - map['bool-array.3'] == 'true' - map['bool-array.4'] == 'false' - !map.containsKey('bool-array-null') - - map['long-array.0'] == '123' - map['long-array.1'] == '456' - !map.containsKey('long-array.2') - map['long-array.3'] == '-9223372036854775808' - map['long-array.4'] == '9223372036854775807' - !map.containsKey('long-array-null') - - map['double-array.0'] == '12.3' - map['double-array.1'] == '45.6' - !map.containsKey('double-array.2') - // Field should be encoded as String using JSON representation so the scientific notation is valid - map['double-array.3'] == '4.9E-324' - map['double-array.4'] == '1.7976931348623157E308' - !map.containsKey('double-array-null') - - when: - def attributes = SpanAttributes.fromMap(map) - - then: - attributes.asMap() == map - } - - def "test span link attributes toString"() { - when: - SpanAttributes.builder().build().toString() - - then: - notThrown(NullPointerException) - } - - def "test span link attributes equals and hashcode"() { - when: - def a = SpanAttributes.builder().put("test", "value").build() - def b = SpanAttributes.builder().put("test", "value").build() - def c = SpanAttributes.builder().build() - - then: - assert a == b - assert a != c - assert !c.equals(null) - assert a.hashCode() == b.hashCode() - assert a.hashCode() != c.hashCode() - } -} diff --git a/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/TaskWrapperTest.groovy b/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/TaskWrapperTest.groovy deleted file mode 100644 index 5b8edc7f080..00000000000 --- a/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/TaskWrapperTest.groovy +++ /dev/null @@ -1,73 +0,0 @@ -package datadog.trace.bootstrap.instrumentation.api - - -import datadog.trace.test.util.DDSpecification - -class TaskWrapperTest extends DDSpecification { - - def "test unwrapping"() { - when: "direct wrapping" - def directWrapper = new DirectWrapper() - then: - TaskWrapper.getUnwrappedType(directWrapper) == Integer - - when: "wrapped value is null" - def nullWrapper = new RecursiveWrapper() - then: - TaskWrapper.getUnwrappedType(nullWrapper) == null - - when: "recursive wrapped value within depth limit" - def shallowRecursiveWrapper = new RecursiveWrapper() - shallowRecursiveWrapper.wrapped = new DirectWrapper() - then: - TaskWrapper.getUnwrappedType(shallowRecursiveWrapper) == Integer - - when: "recursive wrapped value exceeds depth limit" - def l1 = new DirectWrapper() - def l2 = new RecursiveWrapper() - l2.wrapped = l1 - def l3 = new RecursiveWrapper() - l3.wrapped = l2 - def l4 = new RecursiveWrapper() - l4.wrapped = l3 - def l5 = new RecursiveWrapper() - l5.wrapped = l4 - def l6 = new RecursiveWrapper() - l6.wrapped = l5 - def deepRecursiveWrapper = l6 - then: "terminate at max depth" - TaskWrapper.getUnwrappedType(deepRecursiveWrapper) == DirectWrapper - - when: "cycle" - def outer = new RecursiveWrapper() - def inner = new RecursiveWrapper() - outer.wrapped = inner - inner.wrapped = outer - then: "terminate at max depth" - TaskWrapper.getUnwrappedType(outer) == RecursiveWrapper - TaskWrapper.getUnwrappedType(inner) == RecursiveWrapper - } - - - class RecursiveWrapper implements TaskWrapper { - - TaskWrapper wrapped - - @SuppressWarnings('MethodName') - @Override - Object $$DD$$__unwrap() { - return wrapped - } - } - - class DirectWrapper implements TaskWrapper { - - Integer field = 1 - - @SuppressWarnings('MethodName') - @Override - Object $$DD$$__unwrap() { - return field - } - } -} diff --git a/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/URIDataAdapterTest.groovy b/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/URIDataAdapterTest.groovy deleted file mode 100644 index 269b97a9fcc..00000000000 --- a/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/URIDataAdapterTest.groovy +++ /dev/null @@ -1,178 +0,0 @@ -package datadog.trace.bootstrap.instrumentation.api - -import datadog.trace.test.util.DDSpecification - -abstract class URIDataAdapterTest extends DDSpecification { - - abstract URIDataAdapter adapter(URI uri) - - boolean supportsRaw() { - return true - } - - def "test URI parts #input"() { - setup: - def adapter = URIDataAdapterBase.fromURI(input, {adapter(it)}) - - expect: - adapter.scheme() == scheme - adapter.host() == host - adapter.port() == port - adapter.path() == path - adapter.fragment() == fragment - adapter.query() == query - adapter.supportsRaw() == supportsRaw() - adapter.rawPath() == (supportsRaw() ? rawPath : null) - adapter.rawQuery() == (supportsRaw() ? rawQuery : null) - adapter.raw() == (supportsRaw() ? raw : null) - adapter.isValid() - - where: - // spotless:off - input | scheme | host | port | path | fragment | query | rawPath | rawQuery | raw - "http://host:17/path?query#fragment" | "http" | "host" | 17 | "/path" | "fragment" | "query" | "/path" | "query" | "/path?query" - "https://h0st" | "https" | "h0st" | -1 | "" | null | null | "" | null | "" - "http://host/v%C3%A4g?fr%C3%A5ga" | "http" | "host" | -1 | "/väg" | null | "fråga" | "/v%C3%A4g" | "fr%C3%A5ga" | "/v%C3%A4g?fr%C3%A5ga" - // spotless:on - } -} - -class URIDefaultDataAdapterTest extends URIDataAdapterTest { - @Override - URIDataAdapter adapter(URI uri) { - return new URIDefaultDataAdapter(uri) - } -} - -class URIRawDataAdapterTest extends URIDataAdapterTest { - - @Override - URIDataAdapter adapter(URI uri) { - return new RawTestAdapter(uri) - } - - static class RawTestAdapter extends URIRawDataAdapter { - private final URI uri - - RawTestAdapter(URI uri) { - this.uri = uri - } - - @Override - String scheme() { - return uri.scheme - } - - @Override - String host() { - return uri.host - } - - @Override - int port() { - return uri.port - } - - @Override - String fragment() { - return uri.fragment - } - - @Override - protected String innerRawPath() { - return uri.rawPath - } - - @Override - protected String innerRawQuery() { - return uri.rawQuery - } - } -} - -class URINoRawDataAdapterTest extends URIDataAdapterTest { - - @Override - URIDataAdapter adapter(URI uri) { - return new NoRawTestAdapter(uri) - } - - @Override - boolean supportsRaw() { - return false - } - - static class NoRawTestAdapter extends URIDataAdapterBase { - private final URI uri - - NoRawTestAdapter(URI uri) { - this.uri = uri - } - - @Override - String scheme() { - return uri.scheme - } - - @Override - String host() { - return uri.host - } - - @Override - int port() { - return uri.port - } - - @Override - String fragment() { - return uri.fragment - } - - @Override - String path() { - return uri.path - } - - @Override - String query() { - return uri.query - } - - @Override - boolean supportsRaw() { - return false - } - - @Override - String rawPath() { - return null - } - - @Override - String rawQuery() { - return null - } - } - - static class UnparseableURIAdapterTest extends DDSpecification { - def "should return raw URI only"() { - setup: - def uriStr = "http://myurl/path?query=value#fragment" - def uriAdapter = new UnparseableURIDataAdapter(uriStr) - - expect: - !uriAdapter.isValid() - uriAdapter.host() == null - uriAdapter.port() == 0 - uriAdapter.fragment() == null - uriAdapter.path() == null - uriAdapter.path() == null - uriAdapter.query() == null - uriAdapter.rawQuery() == null - uriAdapter.scheme() == null - uriAdapter.rawPath() == null - uriAdapter.raw() == uriStr - } - } -} diff --git a/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/URIUtilsTest.groovy b/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/URIUtilsTest.groovy deleted file mode 100644 index bdba202a22c..00000000000 --- a/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/URIUtilsTest.groovy +++ /dev/null @@ -1,134 +0,0 @@ -package datadog.trace.bootstrap.instrumentation.api - -import datadog.trace.test.util.DDSpecification - -import static datadog.environment.JavaVirtualMachine.isJavaVersion - -class URIUtilsTest extends DDSpecification { - static boolean java7 = isJavaVersion(7) - - def "should build urls \"#input\""() { - setup: - def uri = new URI(input) - def url = URIUtils.buildURL(uri.scheme, uri.host, uri.port, uri.path) - def lazyUrl = URIUtils.lazyValidURL(uri.scheme, uri.host, uri.port, uri.path) - - expect: - url == expected - lazyUrl.path() == uri.path - lazyUrl.get() == expected - lazyUrl.toString() == expected - - where: - input | expected - "https://host:0" | "https://host/" - "http://host/?query" | "http://host/" - "https://host/path" | "https://host/path" - "http://host:47/path?query" | "http://host:47/path" - "http://host:80/path?query" | "http://host/path" - "http://host:443/path?query" | "http://host:443/path" - "https://host:443/path?query" | "https://host/path" - "https://host:80/path?query" | "https://host:80/path" - } - - def "should build urls from corner cases \"#scheme\" \"#host\" #port \"#path\""() { - setup: - def url = URIUtils.buildURL(scheme, host, port, path) - def lazyUrl = URIUtils.lazyValidURL(scheme, host, port, path) - - expect: - url == expected - lazyUrl.path() == (path != null ? path : '') - lazyUrl.toString() == expected - lazyUrl.get() == expected - - where: - scheme | host | port | path | expected - null | null | -1 | null | "/" - "" | "" | -1 | "" | ":///" - "" | "" | -1 | "relative" | ":///relative" - "" | "" | -1 | "/absolute" | ":///absolute" - null | null | -1 | "relative" | "relative" - null | null | -1 | "/absolute" | "/absolute" - } - - def "should decode URL-encoded ignoring + \"#encoded\" -> \"#expected\""() { - setup: - def decoded = URIUtils.decode(encoded) - - expect: - decoded == expected - - where: - encoded | expected - null | null - "" | "" - "plain" | "plain" - "%C3%BEungur%20hn%C3%ADfur" | "þungur hnífur" - "tr%C3%A5kig%20str%C3%A4ng+med+%2B" | "tråkig sträng+med++" - "boring+string+with+only+plus" | "boring+string+with+only+plus" - "%" | "�" // invalid % sequence with too few characters - "%1" | "�" // invalid % sequence with too few characters - "%W1" | "�" // invalid % sequence with illegal 1st character - "%1X" | "�" // invalid % sequence with illegal 2nd character - "wh%Y1t" | "wh�t" // invalid % sequence with illegal 1st character - "wh%1Zt" | "wh�t" // invalid % sequence with illegal 2nd character - "wh%" | "wh�" // invalid % sequence with too few characters - "wh%1" | "wh�" // invalid % sequence with too few characters - "%C3%28" | "�(" // invalid 2 byte sequence - "%A0%A1" | "��" // invalid sequence identifier - "%E2%28%A1" | "�(�" // invalid 3 byte sequence in 2nd byte - "%E2%82%28" | "�(" // invalid 3 byte sequence in 3rd byte - "%F0%28%8C%BC" | "�(��" // invalid 4 byte sequence in 2nd byte - "%F0%90%28%BC" | "�(�" // invalid 4 byte sequence in 3rd byte - "%F0%28%8C%28" | "�(�(" // invalid 4 byte sequence in 4th byte - "%F8%A1%A1%A1%A1" | "${java7 ? "�" : "�����"}" // valid 5 byte sequence but not unicode - "%FC%A1%A1%A1%A1%A1" | "${java7 ? "�" : "������"}" // valid 6 byte sequence but not unicode - } - - def "should decode URL-encoded with + \"#encoded\" -> \"#expected\""() { - setup: - def decoded = URIUtils.decode(encoded, true) - - expect: - decoded == expected - - where: - encoded | expected - null | null - "plain" | "plain" - "%C3%BEungur%20hn%C3%ADfur" | "þungur hnífur" - "v%C3%A4ldigt+tr%C3%A5kig%20str%C3%A4ng+med+%2B" | "väldigt tråkig sträng med +" - "very+boring+string+with+only+plus" | "very boring string with only plus" - } - - def "test LazyUrl for code coverage"() { - when: - def raw = 'weird' - def invalid = URIUtils.lazyInvalidUrl(raw) - - then: - invalid.path() == null - invalid.toString() == raw - invalid.get() == raw - invalid.length() == raw.length() - invalid.hashCode() == raw.hashCode() - invalid.subSequence(1,3) == 'ei' - invalid.charAt(3) == 'r' as char - } - - def "test safeConcat for code coverage"() { - setup: - def concat = URIUtils.safeConcat(first, second) - expect: - (concat == null && expected == null) || concat.toString() == expected - where: - first | second | expected - "http://localhost/" | "test/me" | "http://localhost/test/me" - "http://localhost" | "/test/me" | "http://localhost/test/me" - "http://localhost/" | "http://localhost/test/me" | "http://localhost/test/me" - null | null | null - null | "/test" | "/test" - "http://localhost" | null | "http://localhost/" - } -} diff --git a/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/Utf8ByteStringTest.groovy b/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/Utf8ByteStringTest.groovy deleted file mode 100644 index 98a282b34df..00000000000 --- a/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/Utf8ByteStringTest.groovy +++ /dev/null @@ -1,134 +0,0 @@ -package datadog.trace.bootstrap.instrumentation.api - - -import datadog.trace.test.util.DDSpecification - -import java.nio.ByteBuffer -import java.nio.charset.StandardCharsets - -class Utf8ByteStringTest extends DDSpecification { - def "wrap String and produce the right bytes and String"() { - when: - final utf8String = UTF8BytesString.create((String) str) - - then: - if (utf8String != null) { - utf8String.toString() == str - utf8String.hashCode() == str.hashCode() - def bytes = str.getBytes(StandardCharsets.UTF_8) - def utf8Bytes = ByteBuffer.allocate(bytes.length) - utf8String.transferTo(utf8Bytes) - utf8Bytes.array() == bytes - // check that we get back the same byte array - !utf8String.equals(null) - utf8String != str - utf8String != UTF8BytesString.create("somethingcompletelydifferent") - } - - where: - str | _ - null | _ - "foo" | _ - "bar" | _ - "alongerstring" | _ - } - - def "wrap bytes and produce the right String and bytes"() { - when: - final utf8String = UTF8BytesString.create(bytes) - - then: - if (utf8String != null) { - utf8String.toString() == str - utf8String.hashCode() == str.hashCode() - def utf8Bytes = ByteBuffer.allocate(bytes.length) - utf8String.transferTo(utf8Bytes) - utf8Bytes.array() == bytes - !utf8String.equals(null) - utf8String != str - utf8String != UTF8BytesString.create("somethingcompletelydifferent") - } - - where: - str | bytes - null | null - "foo" | [0x66, 0x6f, 0x6f] as byte[] - "bar" | [0x62, 0x61, 0x72] as byte[] - "alongerstring" | [ - 0x61, - 0x6c, - 0x6f, - 0x6e, - 0x67, - 0x65, - 0x72, - 0x73, - 0x74, - 0x72, - 0x69, - 0x6e, - 0x67 - ] as byte[] - } - - def "behave like a proper CharSequence"() { - when: - final utf8String = UTF8BytesString.create((CharSequence) chars) - - then: - if (utf8String != null) { - utf8String.toString() == chars.toString() - utf8String.length() == chars.length() - for (int i = 0; i < chars.length(); i++) { - utf8String.charAt(i) == chars.charAt(i) - } - utf8String.subSequence(1, chars.length()).toString() == chars.subSequence(1, chars.length()).toString() - } - - where: - chars | _ - null | _ - "foo" | _ - new StringBuffer("bar") | _ - new StringBuffer("someotherlongstring") | _ - UTF8BytesString.create("utf8string") | _ - } - - def "wrap byte buffer and produce the right String and bytes"() { - when: - final utf8String = UTF8BytesString.create(bytes != null ? ByteBuffer.wrap(bytes) : (ByteBuffer)null) - - then: - if (utf8String != null) { - utf8String.toString() == str - utf8String.hashCode() == str.hashCode() - def utf8Bytes = ByteBuffer.allocate(bytes.length) - utf8String.transferTo(utf8Bytes) - utf8Bytes.array() == bytes - !utf8String.equals(null) - utf8String != str - utf8String != UTF8BytesString.create("somethingcompletelydifferent") - } - - where: - str | bytes - null | null - "foo" | [0x66, 0x6f, 0x6f] as byte[] - "bar" | [0x62, 0x61, 0x72] as byte[] - "alongerstring" | [ - 0x61, - 0x6c, - 0x6f, - 0x6e, - 0x67, - 0x65, - 0x72, - 0x73, - 0x74, - 0x72, - 0x69, - 0x6e, - 0x67 - ] as byte[] - } -} diff --git a/internal-api/src/test/java/datadog/trace/bootstrap/config/provider/ConfigProviderTest.java b/internal-api/src/test/java/datadog/trace/bootstrap/config/provider/ConfigProviderTest.java new file mode 100644 index 00000000000..6d7628763b3 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/bootstrap/config/provider/ConfigProviderTest.java @@ -0,0 +1,681 @@ +package datadog.trace.bootstrap.config.provider; + +import static datadog.trace.api.ConfigOrigin.CALCULATED; +import static datadog.trace.api.ConfigOrigin.CODE; +import static datadog.trace.api.ConfigOrigin.DEFAULT; +import static datadog.trace.api.ConfigOrigin.ENV; +import static datadog.trace.api.ConfigOrigin.JVM_PROP; +import static datadog.trace.api.ConfigSetting.DEFAULT_SEQ_ID; +import static datadog.trace.api.config.TracerConfig.TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING; +import static datadog.trace.test.junit.utils.config.WithConfigExtension.injectEnvConfig; +import static datadog.trace.test.junit.utils.config.WithConfigExtension.injectSysConfig; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyMap; +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.junit.jupiter.params.provider.Arguments.arguments; + +import datadog.trace.api.ConfigCollector; +import datadog.trace.api.ConfigOrigin; +import datadog.trace.api.ConfigSetting; +import datadog.trace.config.inversion.ConfigHelper; +import datadog.trace.config.inversion.ConfigHelper.StrictnessPolicy; +import datadog.trace.test.junit.utils.config.WithConfig; +import datadog.trace.test.junit.utils.config.WithConfigExtension; +import java.util.BitSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +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; + +@ExtendWith(WithConfigExtension.class) +class ConfigProviderTest { + + private static final ConfigProvider configProvider = ConfigProvider.withoutCollector(); + + private StrictnessPolicy strictness; + + @BeforeEach + void setup() { + strictness = ConfigHelper.get().configInversionStrictFlag(); + ConfigHelper.get().setConfigInversionStrict(StrictnessPolicy.TEST); + } + + @AfterEach + void cleanup() { + ConfigHelper.get().setConfigInversionStrict(strictness); + } + + @Test + @WithConfig( + key = "TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING", + value = "/a:env,/b:env", + env = true) + @WithConfig(key = TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING, value = "/a:prop") + void propertiesTakePrecedenceOverEnvVarsForOrderedMap() { + Map config = + configProvider.getOrderedMap(TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING); + + assertEquals("prop", config.get("/a")); + assertEquals("env", config.get("/b")); + } + + @TableTest({ + "scenario | configNameValue | configAlias1Value | configAlias2Value | expected", + "name only | default | | | default ", + "alias1 only | | alias1 | | alias1 ", + "alias2 only | | | alias2 | alias2 ", + "name over alias1 | default | alias1 | | default ", + "name over alias2 | default | | alias2 | default ", + "alias1 over alias2 | | alias1 | alias2 | alias1 " + }) + void testConfigAliasPriority( + String configNameValue, String configAlias1Value, String configAlias2Value, String expected) { + injectEnvConfig("CONFIG_NAME", configNameValue); + injectEnvConfig("CONFIG_ALIAS1", configAlias1Value); + injectEnvConfig("CONFIG_ALIAS2", configAlias2Value); + + String config = configProvider.getString("CONFIG_NAME", null, "CONFIG_ALIAS1", "CONFIG_ALIAS2"); + + assertEquals(expected, config); + } + + @Test + @WithConfig(key = "TEST_KEY", value = "envValue", env = true) + @WithConfig(key = "test.key", value = "jvmValue") + void configProviderAssignsCorrectSeqIdOriginAndValueForEachSourceAndDefault() { + ConfigCollector.get().collect(); // clear previous state + // Default ConfigProvider includes ENV and JVM_PROP + ConfigProvider provider = ConfigProvider.createDefault(); + + String value = provider.getString("test.key", "defaultValue"); + Map> collected = ConfigCollector.get().collect(); + + // Check the default + ConfigSetting defaultSetting = collected.get(DEFAULT).get("test.key"); + assertEquals("defaultValue", defaultSetting.stringValue()); + assertEquals(DEFAULT, defaultSetting.origin); + assertEquals(DEFAULT_SEQ_ID, defaultSetting.seqId); + + ConfigSetting envSetting = collected.get(ENV).get("test.key"); + assertEquals("envValue", envSetting.stringValue()); + assertEquals(ENV, envSetting.origin); + + ConfigSetting jvmSetting = collected.get(JVM_PROP).get("test.key"); + assertEquals("jvmValue", jvmSetting.stringValue()); + assertEquals(JVM_PROP, jvmSetting.origin); + + // It doesn't matter what the seqId values are, so long as they increase with source precedence + assertTrue(jvmSetting.seqId > envSetting.seqId); + assertTrue(envSetting.seqId > defaultSetting.seqId); + + // The value returned by ConfigProvider should be the highest precedence value + assertEquals(jvmSetting.stringValue(), value); + } + + @ParameterizedTest( + name = + "ConfigProvider reports highest seqId for chosen value and origin regardless of conversion errors for {0}") + @MethodSource("configProviderReportsHighestSeqIdForChosenValueArguments") + void configProviderReportsHighestSeqIdForChosenValue( + String methodType, + String configKey, + String envKey, + String validValue, + String invalidValue, + Object defaultValue, + Object expectedResult, + TypedGetter methodCall) { + ConfigCollector.get().collect(); // clear previous state + + // Set up: default, env (valid), jvm (invalid for the specific type) + injectEnvConfig(envKey, validValue); + injectSysConfig(configKey, invalidValue); + ConfigProvider provider = ConfigProvider.createDefault(); + + Object value = methodCall.get(provider, configKey, defaultValue); + Map> collected = ConfigCollector.get().collect(); + + // Default + ConfigSetting defaultSetting = collected.get(DEFAULT).get(configKey); + assertEquals(String.valueOf(defaultValue), defaultSetting.stringValue()); + assertEquals(DEFAULT, defaultSetting.origin); + assertEquals(DEFAULT_SEQ_ID, defaultSetting.seqId); + + // ENV (valid) + ConfigSetting envSetting = collected.get(ENV).get(configKey); + assertEquals(validValue, envSetting.stringValue()); + assertEquals(ENV, envSetting.origin); + + // JVM_PROP (invalid, should still be reported) + ConfigSetting jvmSetting = collected.get(JVM_PROP).get(configKey); + assertEquals(invalidValue, jvmSetting.stringValue()); + assertEquals(JVM_PROP, jvmSetting.origin); + + // The chosen value (from ENV) should have been re-reported with the highest seqId + ConfigSetting chosenSetting = highestSeqIdSetting(defaultSetting, envSetting, jvmSetting); + assertEquals(validValue, chosenSetting.stringValue()); + assertEquals(ENV, chosenSetting.origin); + + // The value returned by provider should be the valid one + assertEquals(expectedResult, value); + } + + static Stream configProviderReportsHighestSeqIdForChosenValueArguments() { + // getBoolean is purposefully excluded; see getBoolean test below + return Stream.of( + arguments( + "getInteger", + "test.int", + "DD_TEST_INT", + "42", + "notAnInt", + 7, + 42, + (TypedGetter) + (provider, key, defaultValue) -> provider.getInteger(key, (int) defaultValue)), + arguments( + "getLong", + "test.long", + "DD_TEST_LONG", + "123", + "notALong", + 5L, + 123L, + (TypedGetter) + (provider, key, defaultValue) -> provider.getLong(key, (long) defaultValue)), + arguments( + "getFloat", + "test.float", + "DD_TEST_FLOAT", + "42.5", + "notAFloat", + 3.14f, + 42.5f, + (TypedGetter) + (provider, key, defaultValue) -> provider.getFloat(key, (float) defaultValue)), + arguments( + "getDouble", + "test.double", + "DD_TEST_DOUBLE", + "42.75", + "notADouble", + 2.71, + 42.75, + (TypedGetter) + (provider, key, defaultValue) -> provider.getDouble(key, (double) defaultValue))); + } + + @Test + @WithConfig(key = "TEST_BOOL", value = "true", env = true) + @WithConfig(key = "test.bool", value = "notABool") + void configProviderTransformsInvalidValuesForGetBooleanToFalseWithCalculatedOrigin() { + // Booleans are a special case; we currently treat all invalid boolean configurations as false + // rather than falling back to a lower precedence setting. + ConfigCollector.get().collect(); // clear previous state + + String envValue = "true"; + String configKey = "test.bool"; + String propValue = "notABool"; + boolean defaultValue = true; + + ConfigProvider provider = ConfigProvider.createDefault(); + + boolean value = provider.getBoolean(configKey, defaultValue); + Map> collected = ConfigCollector.get().collect(); + + // Default + ConfigSetting defaultSetting = collected.get(DEFAULT).get(configKey); + assertEquals(String.valueOf(defaultValue), defaultSetting.stringValue()); + assertEquals(DEFAULT, defaultSetting.origin); + assertEquals(DEFAULT_SEQ_ID, defaultSetting.seqId); + + // ENV (valid) + ConfigSetting envSetting = collected.get(ENV).get(configKey); + assertEquals(envValue, envSetting.stringValue()); + assertEquals(ENV, envSetting.origin); + + // JVM_PROP (invalid, should still be reported) + ConfigSetting jvmSetting = collected.get(JVM_PROP).get(configKey); + assertEquals(propValue, jvmSetting.stringValue()); + assertEquals(JVM_PROP, jvmSetting.origin); + + // Config was evaluated to false and reported with CALCULATED origin + ConfigSetting calcSetting = collected.get(CALCULATED).get(configKey); + assertEquals("false", calcSetting.stringValue()); + assertEquals(CALCULATED, calcSetting.origin); + + // The highest seqId should be the CALCULATED origin + ConfigSetting chosenSetting = + highestSeqIdSetting(defaultSetting, envSetting, jvmSetting, calcSetting); + assertEquals(CALCULATED, chosenSetting.origin); + assertEquals("false", chosenSetting.stringValue()); + + // The value returned by provider should be false + assertFalse(value); + } + + @Test + // Set up: only invalid enum values from all sources + @WithConfig(key = "TEST_ENUM2", value = "NOT_A_VALID_ENUM", env = true) + @WithConfig(key = "test.enum2", value = "ALSO_INVALID") + void configProviderGetEnumReturnsDefaultWhenConversionFails() { + ConfigCollector.get().collect(); // clear previous state + ConfigProvider provider = ConfigProvider.createDefault(); + + ConfigOrigin value = provider.getEnum("test.enum2", ConfigOrigin.class, CODE); + Map> collected = ConfigCollector.get().collect(); + + // Should have attempted to use the highest precedence value (JVM_PROP) + ConfigSetting jvmSetting = collected.get(JVM_PROP).get("test.enum2"); + assertEquals("ALSO_INVALID", jvmSetting.stringValue()); + + // But since conversion failed, should return the default + assertEquals(CODE, value); + } + + @Test + // Set up: default, env, jvm (all valid strings) + @WithConfig(key = "TEST_STRING", value = "envValue", env = true) + @WithConfig(key = "test.string", value = "jvmValue") + void configProviderGetStringReportsAllSourcesAndRespectsPrecedence() { + ConfigCollector.get().collect(); // clear previous state + ConfigProvider provider = ConfigProvider.createDefault(); + + String value = provider.getString("test.string", "defaultValue"); + Map> collected = ConfigCollector.get().collect(); + + // Default + ConfigSetting defaultSetting = collected.get(DEFAULT).get("test.string"); + assertEquals("defaultValue", defaultSetting.stringValue()); + assertEquals(DEFAULT, defaultSetting.origin); + assertEquals(DEFAULT_SEQ_ID, defaultSetting.seqId); + + // ENV + ConfigSetting envSetting = collected.get(ENV).get("test.string"); + assertEquals("envValue", envSetting.stringValue()); + assertEquals(ENV, envSetting.origin); + + // JVM_PROP (highest precedence) + ConfigSetting jvmSetting = collected.get(JVM_PROP).get("test.string"); + assertEquals("jvmValue", jvmSetting.stringValue()); + assertEquals(JVM_PROP, jvmSetting.origin); + + // JVM should have highest seqId and be the returned value + assertTrue(jvmSetting.seqId > envSetting.seqId); + assertTrue(envSetting.seqId > defaultSetting.seqId); + assertEquals("jvmValue", value); + } + + @Test + // Set up: env (empty/blank), jvm (valid but with whitespace) + @WithConfig(key = "TEST_STRING_NOT_EMPTY", value = " ", env = true) // blank string + @WithConfig(key = "test.string.not.empty", value = " jvmValue ") // valid but with whitespace + void configProviderGetStringNotEmptyReportsAllValuesEvenIfEmptyButReturnsNonEmptyValue() { + ConfigCollector.get().collect(); // clear previous state + ConfigProvider provider = ConfigProvider.createDefault(); + + String value = provider.getStringNotEmpty("test.string.not.empty", "defaultValue"); + Map> collected = ConfigCollector.get().collect(); + + // Default + ConfigSetting defaultSetting = collected.get(DEFAULT).get("test.string.not.empty"); + assertEquals("defaultValue", defaultSetting.stringValue()); + + // ENV (blank, should be skipped for return value but still reported) + ConfigSetting envSetting = collected.get(ENV).get("test.string.not.empty"); + assertEquals(" ", envSetting.stringValue()); + assertEquals(ENV, envSetting.origin); + + // JVM_PROP setting - should have highest seqId + ConfigSetting jvmSetting = collected.get(JVM_PROP).get("test.string.not.empty"); + assertEquals(" jvmValue ", jvmSetting.stringValue()); + assertEquals(JVM_PROP, jvmSetting.origin); + + assertEquals(maxSeqId(defaultSetting, envSetting, jvmSetting), jvmSetting.seqId); + + assertEquals(" jvmValue ", value); + } + + @Test + // Set up: env, jvm (both valid) + @WithConfig(key = "TEST_STRING_EXCLUDE", value = "envValue", env = true) + @WithConfig(key = "test.string.exclude", value = "jvmValue") + void configProviderGetStringExcludingSourceExcludesSpecifiedSourceType() { + ConfigCollector.get().collect(); // clear previous state + ConfigProvider provider = ConfigProvider.createDefault(); + + // Exclude JVM_PROP source, should fall back to ENV + String value = + provider.getStringExcludingSource( + "test.string.exclude", "defaultValue", SystemPropertiesConfigSource.class); + Map> collected = ConfigCollector.get().collect(); + + // Default + ConfigSetting defaultSetting = collected.get(DEFAULT).get("test.string.exclude"); + assertEquals("defaultValue", defaultSetting.stringValue()); + + // ENV (should be used since JVM is excluded) + ConfigSetting envSetting = collected.get(ENV).get("test.string.exclude"); + assertEquals("envValue", envSetting.stringValue()); + assertEquals(ENV, envSetting.origin); + + // Should return ENV value since JVM source was excluded + assertEquals("envValue", value); + } + + @Test + // Set up: env (partial map), jvm (partial map with overlap) + @WithConfig(key = "TEST_MAP", value = "env_key:env_value,shared:from_env", env = true) + @WithConfig(key = "test.map", value = "jvm_key:jvm_value,shared:from_jvm") + void configProviderGetMergedMapMergesMapsFromMultipleSourcesWithCorrectPrecedence() { + ConfigCollector.get().collect(); // clear previous state + ConfigProvider provider = ConfigProvider.createDefault(); + + Map result = provider.getMergedMap("test.map"); + Map> collected = ConfigCollector.get().collect(); + + // Result should be merged with JVM taking precedence + assertEquals( + mapOf( + "env_key", "env_value", // from ENV + "jvm_key", "jvm_value", // from JVM + "shared", "from_jvm" // JVM overrides ENV + ), + result); + + // Default should be reported as empty + ConfigSetting defaultSetting = collected.get(DEFAULT).get("test.map"); + assertEquals(emptyMap(), defaultSetting.value); + assertEquals(DEFAULT, defaultSetting.origin); + + // ENV map should be reported + ConfigSetting envSetting = collected.get(ENV).get("test.map"); + assertEquals(mapOf("env_key", "env_value", "shared", "from_env"), envSetting.value); + assertEquals(ENV, envSetting.origin); + + // JVM map should be reported + ConfigSetting jvmSetting = collected.get(JVM_PROP).get("test.map"); + assertEquals(mapOf("jvm_key", "jvm_value", "shared", "from_jvm"), jvmSetting.value); + assertEquals(JVM_PROP, jvmSetting.origin); + + // Final calculated result should be reported with highest seqId + ConfigSetting calculatedSetting = collected.get(CALCULATED).get("test.map"); + assertEquals(result, calculatedSetting.value); + assertEquals(CALCULATED, calculatedSetting.origin); + + // Calculated should have highest seqId + assertEquals( + maxSeqId(defaultSetting, envSetting, jvmSetting, calculatedSetting), + calculatedSetting.seqId); + } + + @Test + // Set up trace tags format (can use comma or space separators) + @WithConfig(key = "TEST_TAGS", value = "service:web,version:1.0", env = true) + @WithConfig(key = "test.tags", value = "env:prod team:backend") + void configProviderGetMergedTagsMapHandlesTraceTagsFormat() { + ConfigCollector.get().collect(); // clear previous state + ConfigProvider provider = ConfigProvider.createDefault(); + + Map result = provider.getMergedTagsMap("test.tags"); + Map> collected = ConfigCollector.get().collect(); + + // Should merge both tag formats + assertEquals( + mapOf( + "service", "web", // from ENV + "version", "1.0", // from ENV + "env", "prod", // from JVM + "team", "backend" // from JVM + ), + result); + + // Should report individual sources and calculated result + ConfigSetting envSetting = collected.get(ENV).get("test.tags"); + assertEquals(mapOf("service", "web", "version", "1.0"), envSetting.value); + + ConfigSetting jvmSetting = collected.get(JVM_PROP).get("test.tags"); + assertEquals(mapOf("env", "prod", "team", "backend"), jvmSetting.value); + + ConfigSetting calculatedSetting = collected.get(CALCULATED).get("test.tags"); + assertEquals(result, calculatedSetting.value); + } + + @Test + // Set up multiple keys with optional mappings + @WithConfig(key = "HEADER_1", value = "X-Custom-Header:custom.tag", env = true) + @WithConfig(key = "header.1", value = "X-Auth:auth.tag") + @WithConfig(key = "HEADER_2", value = "X-Request-ID", env = true) // key only, should get prefix + void configProviderGetMergedMapWithOptionalMappingsHandlesMultipleKeysAndTransformations() { + ConfigCollector.get().collect(); // clear previous state + ConfigProvider provider = ConfigProvider.createDefault(); + + Map result = + provider.getMergedMapWithOptionalMappings("trace.http", true, "header.1", "header.2"); + Map> collected = ConfigCollector.get().collect(); + + // Should merge with transformations + assertTrue(result.size() >= 2); + assertEquals("custom.tag", result.get("x-custom-header")); // from ENV header.1 + assertEquals("auth.tag", result.get("x-auth")); // from JVM header.1 + assertNotNull(result.get("x-request-id")); // from ENV header.2, should get prefix + + // Should report sources and calculated result + ConfigSetting calculatedSetting = + collected.get(CALCULATED).get("header.2"); // Last key processed + assertNotNull(calculatedSetting); + assertEquals(CALCULATED, calculatedSetting.origin); + } + + @Test + // Set up ordered maps from multiple sources + @WithConfig( + key = "TEST_ORDERED_MAP", + value = "first:env_first,second:env_second,third:env_third", + env = true) + @WithConfig( + key = "test.ordered.map", + value = "second:jvm_second,fourth:jvm_fourth,first:jvm_first") + void configProviderGetOrderedMapPreservesInsertionOrderAndMergesSources() { + ConfigCollector.get().collect(); // clear previous state + ConfigProvider provider = ConfigProvider.createDefault(); + + Map result = provider.getOrderedMap("test.ordered.map"); + Map> collected = ConfigCollector.get().collect(); + + // Result should be a LinkedHashMap with preserved order and JVM precedence + assertInstanceOf(LinkedHashMap.class, result); + assertEquals( + mapOf( + "first", "jvm_first", // JVM overrides ENV, appears first due to ENV order + "second", "jvm_second", // JVM overrides ENV, appears second due to ENV order + "third", "env_third", // only in ENV, appears third due to ENV order + "fourth", "jvm_fourth" // only in JVM, appears last due to JVM addition + ), + result); + + // Verify order is preserved (LinkedHashMap maintains insertion order) + List keys = asList(result.keySet().toArray(new String[0])); + assertEquals(asList("first", "second", "third", "fourth"), keys); + + // Default should be reported as empty + ConfigSetting defaultSetting = collected.get(DEFAULT).get("test.ordered.map"); + assertEquals(emptyMap(), defaultSetting.value); + assertEquals(DEFAULT, defaultSetting.origin); + + // ENV ordered map should be reported + ConfigSetting envSetting = collected.get(ENV).get("test.ordered.map"); + assertEquals( + mapOf("first", "env_first", "second", "env_second", "third", "env_third"), + envSetting.value); + assertEquals(ENV, envSetting.origin); + + // JVM ordered map should be reported + ConfigSetting jvmSetting = collected.get(JVM_PROP).get("test.ordered.map"); + assertEquals( + mapOf("second", "jvm_second", "fourth", "jvm_fourth", "first", "jvm_first"), + jvmSetting.value); + assertEquals(JVM_PROP, jvmSetting.origin); + + // Final calculated result should be reported with highest seqId + ConfigSetting calculatedSetting = collected.get(CALCULATED).get("test.ordered.map"); + assertEquals(result, calculatedSetting.value); + assertEquals(CALCULATED, calculatedSetting.origin); + + // Calculated should have highest seqId + assertEquals( + maxSeqId(defaultSetting, envSetting, jvmSetting, calculatedSetting), + calculatedSetting.seqId); + } + + @Test + void + configProviderMethodsThatCallGetStringInternallyReportTheirOwnDefaultsBeforeGetStringsNullDefault() { + ConfigCollector.get().collect(); // clear previous state + // No environment or system property values set, so methods should fall back to their defaults + ConfigProvider provider = ConfigProvider.createDefault(); + List defaultList = asList("default", "list"); + Set defaultSet = new HashSet<>(asList("default", "set")); + + ConfigOrigin enumResult = provider.getEnum("test.enum", ConfigOrigin.class, CODE); + List listResult = provider.getList("test.list", defaultList); + Set setResult = provider.getSet("test.set", defaultSet); + BitSet rangeResult = provider.getIntegerRange("test.range", new BitSet()); + Map> collected = ConfigCollector.get().collect(); + + // Each method should have reported its own default, not getString's null default + + ConfigSetting enumDefault = collected.get(DEFAULT).get("test.enum"); + assertEquals("CODE", enumDefault.stringValue()); // ConfigOrigin.CODE.name() + assertEquals(DEFAULT, enumDefault.origin); + assertEquals(DEFAULT_SEQ_ID, enumDefault.seqId); + + ConfigSetting listDefault = collected.get(DEFAULT).get("test.list"); + assertEquals(defaultList, listDefault.value); + assertEquals(DEFAULT, listDefault.origin); + assertEquals(DEFAULT_SEQ_ID, listDefault.seqId); + + ConfigSetting setDefault = collected.get(DEFAULT).get("test.set"); + assertEquals(defaultSet, setDefault.value); + assertEquals(DEFAULT, setDefault.origin); + assertEquals(DEFAULT_SEQ_ID, setDefault.seqId); + + ConfigSetting rangeDefault = collected.get(DEFAULT).get("test.range"); + assertEquals(new BitSet(), rangeDefault.value); + assertEquals(DEFAULT, rangeDefault.origin); + assertEquals(DEFAULT_SEQ_ID, rangeDefault.seqId); + + // Verify the methods returned their default values (not null) + assertEquals(CODE, enumResult); + assertEquals(defaultList, listResult); + assertEquals(defaultSet, setResult); + assertEquals(new BitSet(), rangeResult); + } + + // NOTE: This is a case that SHOULD never occur. #reReportToCollector(String, int) should only be + // called with valid origins + @Test + void configValueResolverReReportToCollectorHandlesNullOriginGracefully() { + ConfigCollector.get().collect(); // clear previous state + ConfigProvider.ConfigValueResolver resolver = + ConfigProvider.ConfigValueResolver.of("1"); + + resolver.reReportToCollector("test.key", 5); + + for (Map settings : ConfigCollector.get().collect().values()) { + assertFalse(settings.containsKey("test.key")); + } + } + + @Test + void configMergeResolverReportsCorrectOriginForSingleVsMultipleSourceContributions() { + ConfigCollector.get().collect(); // clear previous state + ConfigProvider provider = ConfigProvider.createDefault(); + + // when: Only ENV source contributes to merged map + injectEnvConfig("DD_SINGLE_SOURCE_MAP", "key1:value1,key2:value2"); + // No JVM prop set, so only ENV contributes + Map singleSourceResult = provider.getMergedMap("single.source.map"); + Map> singleSourceCollected = + ConfigCollector.get().collect(); + + // then: Should report with ENV origin, not CALCULATED + assertEquals(mapOf("key1", "value1", "key2", "value2"), singleSourceResult); + + // Should have DEFAULT for default value + ConfigSetting singleDefault = singleSourceCollected.get(DEFAULT).get("single.source.map"); + assertEquals(emptyMap(), singleDefault.value); + + // Should have ENV for the actual value (not CALCULATED) + ConfigSetting singleEnv = singleSourceCollected.get(ENV).get("single.source.map"); + assertEquals(mapOf("key1", "value1", "key2", "value2"), singleEnv.value); + assertEquals(ENV, singleEnv.origin); + + // Should NOT have CALCULATED entry since only one source contributed + Map singleCalculated = singleSourceCollected.get(CALCULATED); + if (singleCalculated != null) { + assertNull(singleCalculated.get("single.source.map")); + } + + // when: Multiple sources contribute to merged map + ConfigCollector.get().collect(); // clear for next test + injectEnvConfig("DD_MULTI_SOURCE_MAP", "env_key:env_value,shared:from_env"); + injectSysConfig("multi.source.map", "jvm_key:jvm_value,shared:from_jvm"); + Map multiSourceResult = provider.getMergedMap("multi.source.map"); + Map> multiSourceCollected = + ConfigCollector.get().collect(); + + // then: Should report with CALCULATED origin when multiple sources contribute + assertEquals( + mapOf("env_key", "env_value", "jvm_key", "jvm_value", "shared", "from_jvm"), + multiSourceResult); + + // Should have CALCULATED for the final merged result + ConfigSetting multiCalculated = multiSourceCollected.get(CALCULATED).get("multi.source.map"); + assertEquals(multiSourceResult, multiCalculated.value); + assertEquals(CALCULATED, multiCalculated.origin); + } + + private static Map mapOf(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 int maxSeqId(ConfigSetting... settings) { + return highestSeqIdSetting(settings).seqId; + } + + private static ConfigSetting highestSeqIdSetting(ConfigSetting... settings) { + ConfigSetting highest = settings[0]; + for (ConfigSetting setting : settings) { + if (setting.seqId > highest.seqId) { + highest = setting; + } + } + return highest; + } + + @FunctionalInterface + interface TypedGetter { + Object get(ConfigProvider provider, String key, Object defaultValue); + } +} diff --git a/internal-api/src/test/java/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSourceTest.java b/internal-api/src/test/java/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSourceTest.java new file mode 100644 index 00000000000..b89ee1c061d --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSourceTest.java @@ -0,0 +1,482 @@ +package datadog.trace.bootstrap.config.provider; + +import static datadog.trace.api.config.GeneralConfig.ENV; +import static datadog.trace.api.config.GeneralConfig.LOG_LEVEL; +import static datadog.trace.api.config.GeneralConfig.RUNTIME_METRICS_ENABLED; +import static datadog.trace.api.config.GeneralConfig.SERVICE_NAME; +import static datadog.trace.api.config.GeneralConfig.TAGS; +import static datadog.trace.api.config.GeneralConfig.VERSION; +import static datadog.trace.api.config.OtlpConfig.OTEL_TRACES_SPAN_METRICS_ENABLED; +import static datadog.trace.api.config.OtlpConfig.TRACE_OTEL_ENABLED; +import static datadog.trace.api.config.OtlpConfig.TRACE_OTEL_EXPORTER; +import static datadog.trace.api.config.TraceInstrumentationConfig.TRACE_ENABLED; +import static datadog.trace.api.config.TraceInstrumentationConfig.TRACE_EXTENSIONS_PATH; +import static datadog.trace.api.config.TracerConfig.REQUEST_HEADER_TAGS; +import static datadog.trace.api.config.TracerConfig.RESPONSE_HEADER_TAGS; +import static datadog.trace.api.config.TracerConfig.TRACE_PROPAGATION_STYLE; +import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLE_RATE; +import static datadog.trace.test.junit.utils.config.WithConfigExtension.injectEnvConfig; +import static datadog.trace.test.junit.utils.config.WithConfigExtension.injectSysConfig; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import datadog.trace.test.junit.utils.config.WithConfig; +import datadog.trace.test.junit.utils.config.WithConfigExtension; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@ExtendWith(WithConfigExtension.class) +class OtelEnvironmentConfigSourceTest { + + private static final String REQUEST_HEADER_TAGS_VALUE = + "content-type:http.request.header.content-type,custom-header:http.request.header.custom-header"; + private static final String RESPONSE_HEADER_TAGS_VALUE = + "content-length:http.response.header.content-length,another-header:http.response.header.another-header"; + private static final String TAGS_VALUE = + "key1:one,key2:two,key3:three,key4:four,key5:five,key6:six,key7:seven,key8:eight,key9:nine,key10:ten,key11:eleven,key12:twelve"; + + // ignore this test when we enable the OpenTelemetry integration by default + @Test + @WithConfig(key = "otel.service.name", value = "TEST_SERVICE", addPrefix = false) + @WithConfig(key = "otel.propagators", value = "xray,b3,datadog", addPrefix = false) + @WithConfig(key = "otel.traces.sampler", value = "parentbased_traceidratio", addPrefix = false) + @WithConfig(key = "otel.traces.sampler.arg", value = "0.5", addPrefix = false) + @WithConfig(key = "otel.traces.exporter", value = "none", addPrefix = false) + @WithConfig(key = "otel.metrics.exporter", value = "none", addPrefix = false) + @WithConfig(key = "otel.logs.exporter", value = "none", addPrefix = false) + @WithConfig( + key = "otel.resource.attributes", + value = "service.name=DEV_SERVICE", + addPrefix = false) + @WithConfig( + key = "otel.instrumentation.http.client.capture-request-headers", + value = "content-type", + addPrefix = false) + @WithConfig( + key = "otel.instrumentation.http.client.capture-response-headers", + value = "content-length", + addPrefix = false) + @WithConfig( + key = "otel.instrumentation.http.server.capture-request-headers", + value = "custom-header", + addPrefix = false) + @WithConfig( + key = "otel.instrumentation.http.server.capture-response-headers", + value = "another-header", + addPrefix = false) + @WithConfig( + key = "otel.javaagent.extensions", + value = "/opt/opentelemetry/extensions", + addPrefix = false) + void noOtelSystemPropertiesAreMappedByDefault() { + OtelEnvironmentConfigSource source = new OtelEnvironmentConfigSource(); + + assertNull(source.get(TRACE_OTEL_ENABLED)); + assertNoOtelSettingsMapped(source); + } + + // ignore this test when we enable the OpenTelemetry integration by default + @Test + @WithConfig(key = "OTEL_LOG_LEVEL", value = "debug", env = true, addPrefix = false) + @WithConfig(key = "OTEL_SERVICE_NAME", value = "TEST_SERVICE", env = true, addPrefix = false) + @WithConfig(key = "OTEL_PROPAGATORS", value = "xray,b3,datadog", env = true, addPrefix = false) + @WithConfig( + key = "OTEL_TRACES_SAMPLER", + value = "parentbased_traceidratio", + env = true, + addPrefix = false) + @WithConfig(key = "OTEL_TRACES_SAMPLER_ARG", value = "0.5", env = true, addPrefix = false) + @WithConfig(key = "OTEL_TRACES_EXPORTER", value = "none", env = true, addPrefix = false) + @WithConfig(key = "OTEL_METRICS_EXPORTER", value = "none", env = true, addPrefix = false) + @WithConfig(key = "OTEL_LOGS_EXPORTER", value = "none", env = true, addPrefix = false) + @WithConfig( + key = "OTEL_RESOURCE_ATTRIBUTES", + value = "service.name=DEV_SERVICE", + env = true, + addPrefix = false) + @WithConfig( + key = "OTEL_INSTRUMENTATION_HTTP_CLIENT_CAPTURE_REQUEST_HEADERS", + value = "content-type", + env = true, + addPrefix = false) + @WithConfig( + key = "OTEL_INSTRUMENTATION_HTTP_CLIENT_CAPTURE_RESPONSE_HEADERS", + value = "content-length", + env = true, + addPrefix = false) + @WithConfig( + key = "OTEL_INSTRUMENTATION_HTTP_SERVER_CAPTURE_REQUEST_HEADERS", + value = "custom-header", + env = true, + addPrefix = false) + @WithConfig( + key = "OTEL_INSTRUMENTATION_HTTP_SERVER_CAPTURE_RESPONSE_HEADERS", + value = "another-header", + env = true, + addPrefix = false) + @WithConfig( + key = "OTEL_JAVAAGENT_EXTENSIONS", + value = "/opt/opentelemetry/extensions", + env = true, + addPrefix = false) + void noOtelEnvironmentVariablesAreMappedByDefault() { + OtelEnvironmentConfigSource source = new OtelEnvironmentConfigSource(); + + assertNull(source.get(TRACE_OTEL_ENABLED)); + assertNoOtelSettingsMapped(source); + } + + @Disabled("enable this test when we enable the OpenTelemetry integration by default") + @Test + @WithConfig(key = "otel.sdk.disabled", value = "true", addPrefix = false) + @WithConfig(key = "otel.service.name", value = "TEST_SERVICE", addPrefix = false) + @WithConfig(key = "otel.propagators", value = "xray,b3,datadog", addPrefix = false) + @WithConfig(key = "otel.traces.sampler", value = "parentbased_traceidratio", addPrefix = false) + @WithConfig(key = "otel.traces.sampler.arg", value = "0.5", addPrefix = false) + @WithConfig(key = "otel.traces.exporter", value = "none", addPrefix = false) + @WithConfig(key = "otel.metrics.exporter", value = "none", addPrefix = false) + @WithConfig(key = "otel.logs.exporter", value = "none", addPrefix = false) + @WithConfig( + key = "otel.resource.attributes", + value = "service.name=DEV_SERVICE", + addPrefix = false) + @WithConfig( + key = "otel.instrumentation.http.client.capture-request-headers", + value = "content-type", + addPrefix = false) + @WithConfig( + key = "otel.instrumentation.http.client.capture-response-headers", + value = "content-length", + addPrefix = false) + @WithConfig( + key = "otel.instrumentation.http.server.capture-request-headers", + value = "custom-header", + addPrefix = false) + @WithConfig( + key = "otel.instrumentation.http.server.capture-response-headers", + value = "another-header", + addPrefix = false) + @WithConfig( + key = "otel.javaagent.extensions", + value = "/opt/opentelemetry/extensions", + addPrefix = false) + void disablingOtelWithSystemPropertyDisablesOtelIntegration() { + OtelEnvironmentConfigSource source = new OtelEnvironmentConfigSource(); + + assertEquals("false", source.get(TRACE_OTEL_ENABLED)); + assertNoOtelSettingsMapped(source); + } + + @Disabled("enable this test when we enable the OpenTelemetry integration by default") + @Test + @WithConfig(key = "OTEL_SDK_DISABLED", value = "true", env = true, addPrefix = false) + @WithConfig(key = "OTEL_LOG_LEVEL", value = "debug", env = true, addPrefix = false) + @WithConfig(key = "OTEL_SERVICE_NAME", value = "TEST_SERVICE", env = true, addPrefix = false) + @WithConfig(key = "OTEL_PROPAGATORS", value = "xray,b3,datadog", env = true, addPrefix = false) + @WithConfig( + key = "OTEL_TRACES_SAMPLER", + value = "parentbased_traceidratio", + env = true, + addPrefix = false) + @WithConfig(key = "OTEL_TRACES_SAMPLER_ARG", value = "0.5", env = true, addPrefix = false) + @WithConfig(key = "OTEL_TRACES_EXPORTER", value = "none", env = true, addPrefix = false) + @WithConfig(key = "OTEL_METRICS_EXPORTER", value = "none", env = true, addPrefix = false) + @WithConfig(key = "OTEL_LOGS_EXPORTER", value = "none", env = true, addPrefix = false) + @WithConfig( + key = "OTEL_RESOURCE_ATTRIBUTES", + value = "service.name=DEV_SERVICE", + env = true, + addPrefix = false) + @WithConfig( + key = "OTEL_INSTRUMENTATION_HTTP_CLIENT_CAPTURE_REQUEST_HEADERS", + value = "content-type", + env = true, + addPrefix = false) + @WithConfig( + key = "OTEL_INSTRUMENTATION_HTTP_CLIENT_CAPTURE_RESPONSE_HEADERS", + value = "content-length", + env = true, + addPrefix = false) + @WithConfig( + key = "OTEL_INSTRUMENTATION_HTTP_SERVER_CAPTURE_REQUEST_HEADERS", + value = "custom-header", + env = true, + addPrefix = false) + @WithConfig( + key = "OTEL_INSTRUMENTATION_HTTP_SERVER_CAPTURE_RESPONSE_HEADERS", + value = "another-header", + env = true, + addPrefix = false) + @WithConfig( + key = "OTEL_JAVAAGENT_EXTENSIONS", + value = "/opt/opentelemetry/extensions", + env = true, + addPrefix = false) + void disablingOtelWithEnvironmentVariableDisablesOtelIntegration() { + OtelEnvironmentConfigSource source = new OtelEnvironmentConfigSource(); + + assertEquals("false", source.get(TRACE_OTEL_ENABLED)); + assertNoOtelSettingsMapped(source); + } + + @Test + @WithConfig(key = TRACE_OTEL_ENABLED, value = "true") + @WithConfig(key = "otel.service.name", value = "TEST_SERVICE", addPrefix = false) + @WithConfig(key = "otel.propagators", value = "xray,b3,datadog", addPrefix = false) + @WithConfig(key = "otel.traces.sampler", value = "parentbased_traceidratio", addPrefix = false) + @WithConfig(key = "otel.traces.sampler.arg", value = "0.5", addPrefix = false) + @WithConfig(key = "otel.traces.exporter", value = "none", addPrefix = false) + @WithConfig(key = "otel.metrics.exporter", value = "none", addPrefix = false) + @WithConfig(key = "otel.logs.exporter", value = "none", addPrefix = false) + @WithConfig( + key = "otel.resource.attributes", + value = "service.name=DEV_SERVICE", + addPrefix = false) + @WithConfig( + key = "otel.instrumentation.http.client.capture-request-headers", + value = "content-type", + addPrefix = false) + @WithConfig( + key = "otel.instrumentation.http.client.capture-response-headers", + value = "content-length", + addPrefix = false) + @WithConfig( + key = "otel.instrumentation.http.server.capture-request-headers", + value = "custom-header", + addPrefix = false) + @WithConfig( + key = "otel.instrumentation.http.server.capture-response-headers", + value = "another-header", + addPrefix = false) + @WithConfig( + key = "otel.javaagent.extensions", + value = "/opt/opentelemetry/extensions", + addPrefix = false) + void otelSystemPropertiesAreMappedWhenOtelIsEnabled() { + OtelEnvironmentConfigSource source = new OtelEnvironmentConfigSource(); + + // value from otel.service.name overrides the one from otel.resource.attributes + assertEquals("TEST_SERVICE", source.get(SERVICE_NAME)); + assertEquals("xray,b3single,datadog", source.get(TRACE_PROPAGATION_STYLE)); + assertEquals("0.5", source.get(TRACE_SAMPLE_RATE)); + assertEquals("false", source.get(TRACE_ENABLED)); + assertEquals("false", source.get(RUNTIME_METRICS_ENABLED)); + assertEquals(REQUEST_HEADER_TAGS_VALUE, source.get(REQUEST_HEADER_TAGS)); + assertEquals(RESPONSE_HEADER_TAGS_VALUE, source.get(RESPONSE_HEADER_TAGS)); + assertEquals("/opt/opentelemetry/extensions", source.get(TRACE_EXTENSIONS_PATH)); + } + + @Test + @WithConfig(key = "DD_TRACE_OTEL_ENABLED", value = "true", env = true, addPrefix = false) + @WithConfig(key = "OTEL_LOG_LEVEL", value = "debug", env = true, addPrefix = false) + @WithConfig(key = "OTEL_SERVICE_NAME", value = "TEST_SERVICE", env = true, addPrefix = false) + @WithConfig(key = "OTEL_PROPAGATORS", value = "xray,b3,datadog", env = true, addPrefix = false) + @WithConfig( + key = "OTEL_TRACES_SAMPLER", + value = "parentbased_traceidratio", + env = true, + addPrefix = false) + @WithConfig(key = "OTEL_TRACES_SAMPLER_ARG", value = "0.5", env = true, addPrefix = false) + @WithConfig(key = "OTEL_TRACES_EXPORTER", value = "none", env = true, addPrefix = false) + @WithConfig(key = "OTEL_METRICS_EXPORTER", value = "none", env = true, addPrefix = false) + @WithConfig(key = "OTEL_LOGS_EXPORTER", value = "none", env = true, addPrefix = false) + @WithConfig( + key = "OTEL_RESOURCE_ATTRIBUTES", + value = "service.name=DEV_SERVICE", + env = true, + addPrefix = false) + @WithConfig( + key = "OTEL_INSTRUMENTATION_HTTP_CLIENT_CAPTURE_REQUEST_HEADERS", + value = "content-type", + env = true, + addPrefix = false) + @WithConfig( + key = "OTEL_INSTRUMENTATION_HTTP_CLIENT_CAPTURE_RESPONSE_HEADERS", + value = "content-length", + env = true, + addPrefix = false) + @WithConfig( + key = "OTEL_INSTRUMENTATION_HTTP_SERVER_CAPTURE_REQUEST_HEADERS", + value = "custom-header", + env = true, + addPrefix = false) + @WithConfig( + key = "OTEL_INSTRUMENTATION_HTTP_SERVER_CAPTURE_RESPONSE_HEADERS", + value = "another-header", + env = true, + addPrefix = false) + @WithConfig( + key = "OTEL_JAVAAGENT_EXTENSIONS", + value = "/opt/opentelemetry/extensions", + env = true, + addPrefix = false) + void otelEnvironmentVariablesAreMappedWhenOtelIsEnabled() { + OtelEnvironmentConfigSource source = new OtelEnvironmentConfigSource(); + + assertEquals("debug", source.get(LOG_LEVEL)); + // value from OTEL_SERVICE_NAME overrides the one from OTEL_RESOURCE_ATTRIBUTES + assertEquals("TEST_SERVICE", source.get(SERVICE_NAME)); + assertEquals("xray,b3single,datadog", source.get(TRACE_PROPAGATION_STYLE)); + assertEquals("0.5", source.get(TRACE_SAMPLE_RATE)); + assertEquals("false", source.get(TRACE_ENABLED)); + assertEquals("false", source.get(RUNTIME_METRICS_ENABLED)); + assertEquals(REQUEST_HEADER_TAGS_VALUE, source.get(REQUEST_HEADER_TAGS)); + assertEquals(RESPONSE_HEADER_TAGS_VALUE, source.get(RESPONSE_HEADER_TAGS)); + assertEquals("/opt/opentelemetry/extensions", source.get(TRACE_EXTENSIONS_PATH)); + } + + @Test + @WithConfig(key = TRACE_OTEL_ENABLED, value = "true") + @WithConfig(key = "otel.traces.exporter", value = "otlp", addPrefix = false) + void otelTracesExporterOtlpSystemPropertyIsMapped() { + OtelEnvironmentConfigSource source = new OtelEnvironmentConfigSource(); + + assertNull(source.get(TRACE_ENABLED)); + assertEquals("otlp", source.get(TRACE_OTEL_EXPORTER)); + } + + @Test + @WithConfig(key = "DD_TRACE_OTEL_ENABLED", value = "true", env = true, addPrefix = false) + @WithConfig(key = "OTEL_TRACES_EXPORTER", value = "otlp", env = true, addPrefix = false) + void otelTracesExporterOtlpEnvironmentVariableIsMapped() { + OtelEnvironmentConfigSource source = new OtelEnvironmentConfigSource(); + + assertNull(source.get(TRACE_ENABLED)); + assertEquals("otlp", source.get(TRACE_OTEL_EXPORTER)); + } + + @ParameterizedTest + @ValueSource(strings = {"true", "false"}) + @WithConfig(key = TRACE_OTEL_ENABLED, value = "true") + void otelTracesSpanMetricsEnabledSystemPropertyIsMappedWhenOtelIsEnabled(String value) { + injectSysConfig(OTEL_TRACES_SPAN_METRICS_ENABLED, value, false); + + OtelEnvironmentConfigSource source = new OtelEnvironmentConfigSource(); + + assertEquals(value, source.get(OTEL_TRACES_SPAN_METRICS_ENABLED)); + } + + @ParameterizedTest + @ValueSource(strings = {"true", "false"}) + @WithConfig(key = "DD_TRACE_OTEL_ENABLED", value = "true", env = true, addPrefix = false) + void otelTracesSpanMetricsEnabledEnvironmentVariableIsMappedWhenOtelIsEnabled(String value) { + injectEnvConfig("OTEL_TRACES_SPAN_METRICS_ENABLED", value, false); + + OtelEnvironmentConfigSource source = new OtelEnvironmentConfigSource(); + + assertEquals(value, source.get(OTEL_TRACES_SPAN_METRICS_ENABLED)); + } + + @Test + // Without dd.trace.otel.enabled, setupTraceOtelEnvironment() does not run. + @WithConfig(key = OTEL_TRACES_SPAN_METRICS_ENABLED, value = "true", addPrefix = false) + void otelTracesSpanMetricsEnabledIsNotMappedWhenOtelIsDisabled() { + OtelEnvironmentConfigSource source = new OtelEnvironmentConfigSource(); + + assertNull(source.get(OTEL_TRACES_SPAN_METRICS_ENABLED)); + } + + @Test + @WithConfig(key = "DD_TRACE_OTEL_ENABLED", value = "true", env = true, addPrefix = false) + @WithConfig(key = "OTEL_TRACES_EXPORTER", value = "none", env = true, addPrefix = false) + void otelTracesExporterNoneStillDisablesTracing() { + OtelEnvironmentConfigSource source = new OtelEnvironmentConfigSource(); + + assertEquals("false", source.get(TRACE_ENABLED)); + } + + @Test + @WithConfig(key = TRACE_OTEL_ENABLED, value = "true") + @WithConfig( + key = "otel.resource.attributes", + value = + "key1=one," + + "key2=two," + + "key3=three," + + "service.name=DEV_SERVICE," + + "key4=four," + + "key5=five," + + "key6=six," + + "deployment.environment.name=staging," + + "key7=seven," + + "key8=eight," + + "key9=nine," + + "service.version=42," + + "key10=ten," + + "key11=eleven," + + "key12=twelve", + addPrefix = false) + void otelResourceAttributesSystemPropertyIsMapped() { + OtelEnvironmentConfigSource source = new OtelEnvironmentConfigSource(); + + assertEquals("DEV_SERVICE", source.get(SERVICE_NAME)); + assertEquals("staging", source.get(ENV)); + assertEquals("42", source.get(VERSION)); + assertEquals(TAGS_VALUE, source.get(TAGS)); + } + + @Test + @WithConfig(key = "DD_TRACE_OTEL_ENABLED", value = "true", env = true, addPrefix = false) + @WithConfig( + key = "OTEL_RESOURCE_ATTRIBUTES", + value = + "key1=one," + + "key2=two," + + "key3=three," + + "service.name=DEV_SERVICE," + + "key4=four," + + "key5=five," + + "key6=six," + + "deployment.environment=staging," + + "key7=seven," + + "key8=eight," + + "key9=nine," + + "service.version=42," + + "key10=ten," + + "key11=eleven," + + "key12=twelve", + env = true, + addPrefix = false) + void otelResourceAttributesEnvironmentVariableIsMapped() { + OtelEnvironmentConfigSource source = new OtelEnvironmentConfigSource(); + + assertEquals("DEV_SERVICE", source.get(SERVICE_NAME)); + assertEquals("staging", source.get(ENV)); + assertEquals("42", source.get(VERSION)); + assertEquals(TAGS_VALUE, source.get(TAGS)); + } + + @ParameterizedTest + @ValueSource( + strings = { + "deployment.environment.name=production,deployment.environment=staging", + "deployment.environment=staging,deployment.environment.name=production" + }) + @WithConfig(key = TRACE_OTEL_ENABLED, value = "true") + void namedDeploymentEnvironmentTakesPrecedenceOverLegacyAttribute(String resourceAttributes) { + injectSysConfig("otel.resource.attributes", resourceAttributes, false); + + OtelEnvironmentConfigSource source = new OtelEnvironmentConfigSource(); + + assertEquals("production", source.get(ENV)); + assertNull(source.get(TAGS)); + } + + private static void assertNoOtelSettingsMapped(OtelEnvironmentConfigSource source) { + assertNull(source.get(LOG_LEVEL)); + assertNull(source.get(SERVICE_NAME)); + assertNull(source.get(VERSION)); + assertNull(source.get(ENV)); + assertNull(source.get(TAGS)); + assertNull(source.get(TRACE_PROPAGATION_STYLE)); + assertNull(source.get(TRACE_SAMPLE_RATE)); + assertNull(source.get(TRACE_ENABLED)); + assertNull(source.get(RUNTIME_METRICS_ENABLED)); + assertNull(source.get(REQUEST_HEADER_TAGS)); + assertNull(source.get(RESPONSE_HEADER_TAGS)); + assertNull(source.get(TRACE_EXTENSIONS_PATH)); + } +} diff --git a/internal-api/src/test/java/datadog/trace/bootstrap/config/provider/StableConfigParserTest.java b/internal-api/src/test/java/datadog/trace/bootstrap/config/provider/StableConfigParserTest.java new file mode 100644 index 00000000000..c5b60ecae64 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/bootstrap/config/provider/StableConfigParserTest.java @@ -0,0 +1,308 @@ +package datadog.trace.bootstrap.config.provider; + +import static datadog.trace.bootstrap.config.provider.StableConfigParser.MAX_FILE_SIZE_BYTES; +import static datadog.trace.test.junit.utils.config.WithConfigExtension.injectEnvConfig; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import datadog.trace.bootstrap.config.provider.StableConfigSource.StableConfig; +import datadog.trace.config.inversion.ConfigHelper; +import datadog.trace.config.inversion.ConfigHelper.StrictnessPolicy; +import datadog.trace.test.junit.utils.config.WithConfig; +import datadog.trace.test.junit.utils.config.WithConfigExtension; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.tabletest.junit.TableTest; + +@ExtendWith(WithConfigExtension.class) +class StableConfigParserTest { + + @TempDir Path tempDir; + + private StrictnessPolicy strictness; + + @BeforeEach + void setup() { + strictness = ConfigHelper.get().configInversionStrictFlag(); + ConfigHelper.get().setConfigInversionStrict(StrictnessPolicy.TEST); + } + + @AfterEach + void cleanup() { + ConfigHelper.get().setConfigInversionStrict(strictness); + } + + @Test + @WithConfig(key = "DD_SERVICE", value = "mysvc", env = true, addPrefix = false) + void testParseValid() throws IOException { + // From the below yaml, only apm_configuration_default and the second selector should be + // applied: We use the first matching rule and discard the rest + String yaml = + "\n" + + "config_id: 12345\n" + + "apm_configuration_default:\n" + + " KEY_ONE: \"default\"\n" + + " KEY_TWO: true\n" + + "apm_configuration_rules:\n" + + " - selectors:\n" + + " - origin: language\n" + + " matches: [\"golang\"]\n" + + " operator: equals\n" + + " configuration:\n" + + " KEY_ONE: \"ignored\"\n" + + " - selectors:\n" + + " - origin: language\n" + + " matches: [\"Java\"]\n" + + " operator: equals\n" + + " configuration:\n" + + " KEY_ONE: \"rules\"\n" + + " KEY_THREE: 1\n" + + " - selectors:\n" + + " - origin: environment_variables\n" + + " key: \"DD_SERVICE\"\n" + + " operator: equals\n" + + " matches: [\"mysvc\"]\n" + + " configuration:\n" + + " KEY_FOUR: \"ignored\"\n" + + " - selectors:\n" + + " - origin: process_arguments\n" + + " key: \"-Darg1\"\n" + + " operator: exists\n" + + " configuration:\n" + + " KEY_FIVE: \"ignored\"\n"; + + StableConfig config = StableConfigParser.parse(writeYaml(yaml)); + + assertEquals(3, config.getKeys().size()); + assertEquals("12345", config.getConfigId().trim()); + assertEquals("rules", config.get("KEY_ONE")); + assertEquals("true", config.get("KEY_TWO")); + assertEquals("1", config.get("KEY_THREE")); + } + + @Test + @WithConfig(key = "test_parse_and_template", value = "myservice", addPrefix = false) + void testParseAndTemplate() throws IOException { + String yaml = + "\n" + + " apm_configuration_rules:\n" + + " - selectors:\n" + + " - origin: process_arguments\n" + + " key: \"-Dtest_parse_and_template\"\n" + + " operator: exists\n" + + " configuration:\n" + + " DD_SERVICE: {{process_arguments['-Dtest_parse_and_template']}}\n"; + + StableConfig config = StableConfigParser.parse(writeYaml(yaml)); + + assertEquals("myservice", config.get("DD_SERVICE")); + } + + @TableTest({ + "scenario | origin | matches | operator | key | expectMatch", + "language equals | language | [java] | equals | '' | true ", + "language case insensitivity | LANGUAGE | [JaVa] | EQUALS | '' | true ", + "language equals one of many | language | [java, golang] | equals | '' | true ", + "language starts_with | language | [java] | starts_with | '' | true ", + "language not equals | language | [golang] | equals | '' | false ", + "language exists | language | [java] | exists | '' | false ", + "language unexpected operator | language | [java] | something unexpected | '' | false ", + "env exists empty matches | environment_variables | [] | exists | DD_TAGS | true ", + "env exists null matches | environment_variables | | exists | DD_TAGS | true ", + "env contains | environment_variables | ['team:apm'] | contains | DD_TAGS | true ", + "env case insensitivity | ENVIRONMENT_VARIABLES | ['TeAm:ApM'] | CoNtAiNs | Dd_TaGs | true ", + "env not equals | environment_variables | ['team:apm'] | equals | DD_TAGS | false ", + "env starts_with | environment_variables | ['team:apm'] | starts_with | DD_TAGS | true ", + "env equals boolean | environment_variables | [true] | equals | DD_PROFILING_ENABLED | true ", + "env not equals boolean | environment_variables | [abcdefg] | equals | DD_PROFILING_ENABLED | false ", + "env equals boolean again | environment_variables | [true] | equals | DD_PROFILING_ENABLED | true ", + "env equals one of many | environment_variables | [mysvc, othersvc] | equals | DD_SERVICE | true ", + "env starts_with service | environment_variables | [my] | starts_with | DD_SERVICE | true ", + "env ends_with service | environment_variables | [svc] | ends_with | DD_SERVICE | true ", + "env contains service | environment_variables | [svc] | contains | DD_SERVICE | true ", + "env not contains service | environment_variables | [other] | contains | DD_SERVICE | false ", + "env equals null key | environment_variables | [] | equals | | false ", + "env equals null matches | environment_variables | | equals | DD_SERVICE | false ", + "language null operator | language | [java] | | '' | false ", + "process argument exists | process_arguments | | exists | -Dtest_selectorMatch | true ", + "process argument not exists | process_arguments | | exists | -Darg2 | false ", + "process argument equals | process_arguments | [value1] | equals | -Dtest_selectorMatch | true ", + "process argument not equals | process_arguments | [value2] | equals | -Dtest_selectorMatch | false " + }) + @MethodSource("testSelectorMatchArguments") + @WithConfig(key = "DD_PROFILING_ENABLED", value = "true", env = true, addPrefix = false) + @WithConfig(key = "DD_SERVICE", value = "mysvc", env = true, addPrefix = false) + @WithConfig(key = "DD_TAGS", value = "team:apm,component:web", env = true, addPrefix = false) + @WithConfig(key = "test_selectorMatch", value = "value1", addPrefix = false) + void testSelectorMatch( + String origin, List matches, String operator, String key, boolean expectMatch) { + boolean match = StableConfigParser.selectorMatch(origin, matches, operator, key); + + assertEquals(expectMatch, match); + } + + static Stream testSelectorMatchArguments() { + return Stream.of( + // env contains null match + arguments("environment_variables", singletonList(null), "contains", "DD_SERVICE", false)); + } + + @Test + void testDuplicateEntriesNotAllowed() throws IOException { + String yaml = "\n config_id: 12345\n config_id: 67890\n "; + String filePath = writeYaml(yaml); + + RuntimeException exception = + assertThrows(RuntimeException.class, () -> StableConfigParser.parse(filePath)); + + assertTrue( + exception.getMessage().contains("found duplicate key config_id"), exception.getMessage()); + } + + @Test + void testConfigIdOnly() throws IOException { + String yaml = "\n config_id: 12345\n "; + + StableConfig config = StableConfigParser.parse(writeYaml(yaml)); + + assertNotNull(config); + assertEquals("12345", config.getConfigId()); + assertEquals(0, config.getKeys().size()); + } + + @Test + void testParseInvalid() throws IOException { + // If any piece of the file is invalid, the whole file is rendered invalid and an exception is + // thrown + String yaml = + "\n" + + " something-irrelevant: \"\"\n" + + " config_id: 12345\n" + + " something : not : expected << and weird format\n" + + " inufjka <<\n" + + " [a,\n" + + " b,\n" + + " c,\n" + + " d]\n" + + " apm_configuration_default:\n" + + " KEY_ONE: value_one\n" + + " KEY_TWO: \"value_two\"\n" + + " KEY_THREE: 100\n" + + " KEY_FOUR: true\n" + + " KEY_FIVE: [a,b,c,d]\n" + + " something-else-irrelevant: value-irrelevant\n" + + " "; + String filePath = writeYaml(yaml); + + assertThrows(Exception.class, () -> StableConfigParser.parse(filePath)); + } + + @Test + void testFileOverMaxSize() throws IOException { + // Create a file with valid contents, but bigger than MAX_FILE_SIZE_BYTES + String baseYaml = + "\n" + + "config_id: 12345\n" + + "apm_configuration_default:\n" + + " KEY_ONE: \"value_one\"\n" + + "apm_configuration_rules:\n"; + String builderYaml = + "\n" + + " - selectors:\n" + + " - origin: language\n" + + " matches: [\"Java\"]\n" + + " operator: equals\n" + + " configuration:\n" + + " KEY_TWO: \"value_two\"\n"; + StringBuilder bigYaml = new StringBuilder(baseYaml); + while (bigYaml.length() < MAX_FILE_SIZE_BYTES) { + bigYaml.append(builderYaml); + } + + StableConfig config = StableConfigParser.parse(writeYaml(bigYaml.toString())); + + assertSame(StableConfig.EMPTY, config); + } + + @TableTest({ + "scenario | templateVar | envKey | envVal | expect ", + "env var | \"{{environment_variables['DD_KEY']}}\" | DD_KEY | value | value ", + "missing env var | \"{{environment_variables['DD_KEY']}}\" | | | '' ", + "empty template | '{{}}' | | | '' ", + "single braces | '{}' | | | '{}' ", + "lowercase env var | \"{{environment_variables['dd_key']}}\" | DD_KEY | value | value ", + "unterminated quote | \"{{environment_variables['DD_KEY}}\" | DD_KEY | value | '' ", + "header and footer | \"header-{{environment_variables['DD_KEY']}}-footer\" | DD_KEY | value | header-value-footer", + "multiple templates | \"{{environment_variables['HEADER']}}{{environment_variables['DD_KEY']}}{{environment_variables['FOOTER']}}\" | DD_KEY | value | value " + }) + void testProcessTemplateValidCases( + String templateVar, String envKey, String envVal, String expect) throws IOException { + if (envKey != null) { + injectEnvConfig(envKey, envVal); + } + + assertEquals(expect, StableConfigParser.processTemplate(templateVar)); + } + + @TableTest({ + "scenario | templateVar | expect ", + "empty variable name | \"{{environment_variables['']}}\" | Empty environment variable name in template", + "unterminated | \"{{environment_variables['DD_KEY']}\" | Unterminated template in config " + }) + void testProcessTemplateErrorCases(String templateVar, String expect) { + IOException exception = + assertThrows(IOException.class, () -> StableConfigParser.processTemplate(templateVar)); + + assertEquals(expect, exception.getMessage()); + } + + @Test + void testNullAndEmptyValuesInYaml() throws IOException { + String yaml = + "\n" + + "config_id: \"12345\"\n" + + "apm_configuration_default:\n" + + "apm_configuration_rules:\n"; + + StableConfig config = StableConfigParser.parse(writeYaml(yaml)); + + assertEquals("12345", config.getConfigId()); + assertTrue(config.getKeys().isEmpty()); + } + + @Test + void testCompletelyEmptyValuesInYaml() throws IOException { + String yaml = + "\n" + + "config_id: \"12345\"\n" + + "apm_configuration_default: \n" + + "apm_configuration_rules: \n"; + + StableConfig config = StableConfigParser.parse(writeYaml(yaml)); + + assertEquals("12345", config.getConfigId()); + assertTrue(config.getKeys().isEmpty()); + } + + private String writeYaml(String yaml) throws IOException { + Path filePath = Files.createTempFile(tempDir, "testFile_", ".yaml"); + Files.write(filePath, yaml.getBytes(UTF_8)); + return filePath.toString(); + } +} diff --git a/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/BaggageTest.java b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/BaggageTest.java new file mode 100644 index 00000000000..9f2b1dd5d7e --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/BaggageTest.java @@ -0,0 +1,89 @@ +package datadog.trace.bootstrap.instrumentation.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.context.Context; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class BaggageTest { + + private static final String HEADER = "key1=value1,key2=value2"; + + @Test + void testEmptyBaggage() { + Baggage baggage = Baggage.empty(); + + assertTrue(baggage.asMap().isEmpty()); + assertNotNull(baggage.getW3cHeader()); + assertTrue(baggage.getW3cHeader().isEmpty()); + + baggage.addItem("key", "value"); + + assertNull(baggage.getW3cHeader()); + } + + @Test + void testBaggageCreation() { + Map items = items(); + + Baggage baggage = Baggage.create(items); + + assertEquals(items, baggage.asMap()); + assertNull(baggage.getW3cHeader()); + + baggage = Baggage.create(items, HEADER); + + assertEquals(items, baggage.asMap()); + assertEquals(HEADER, baggage.getW3cHeader()); + } + + @Test + void testBaggageHeader() { + Baggage baggage = Baggage.create(items(), HEADER); + + baggage.removeItem("missingKey"); + + // header is preserved + assertEquals(HEADER, baggage.getW3cHeader()); + + baggage.removeItem("key2"); + + // header is out of sync + assertNull(baggage.getW3cHeader()); + + baggage.setW3cHeader("key1=value1"); + + // header is forced + assertEquals("key1=value1", baggage.getW3cHeader()); + + baggage.addItem("key3", "value3"); + + // header is out of sync + assertNull(baggage.getW3cHeader()); + } + + @Test + void testContextStorage() { + Baggage baggage = Baggage.empty(); + Context context = Context.root(); + + assertNull(Baggage.fromContext(context)); + + context = context.with(baggage); + + assertSame(baggage, Baggage.fromContext(context)); + } + + private static Map items() { + Map items = new HashMap<>(); + items.put("key1", "value1"); + items.put("key2", "value2"); + return items; + } +} diff --git a/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/SpanLinkTest.java b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/SpanLinkTest.java new file mode 100644 index 00000000000..a49ae658b90 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/SpanLinkTest.java @@ -0,0 +1,154 @@ +package datadog.trace.bootstrap.instrumentation.api; + +import static datadog.trace.bootstrap.instrumentation.api.AgentSpanLink.DEFAULT_FLAGS; +import static datadog.trace.bootstrap.instrumentation.api.AgentSpanLink.SAMPLED_FLAG; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import datadog.trace.api.DDSpanId; +import datadog.trace.api.DDTraceId; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class SpanLinkTest { + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testSpanLinkFromContext(boolean sampled) { + DDTraceId traceId = DDTraceId.fromHex("11223344556677889900aabbccddeeff"); + long spanId = DDSpanId.fromHex("123456789abcdef0"); + AgentSpanContext context = mock(AgentSpanContext.class); + when(context.getTraceId()).thenReturn(traceId); + when(context.getSpanId()).thenReturn(spanId); + when(context.getSamplingPriority()).thenReturn(sampled ? 1 : 0); + + SpanLink link = SpanLink.from(context); + + assertEquals(traceId, link.traceId()); + assertEquals(spanId, link.spanId()); + assertEquals(sampled ? SAMPLED_FLAG : DEFAULT_FLAGS, link.traceFlags()); + assertEquals("", link.traceState()); + assertEquals(SpanAttributes.EMPTY, link.attributes()); + + assertDoesNotThrow(link::toString); + } + + @Test + void testSpanLinksApi() { + SpanLink link = new SpanLink(null, 0L, (byte) 0, null, null); + + assertEquals(DDTraceId.ZERO, link.traceId()); + assertNotNull(link.traceState()); + assertTrue(link.traceState().isEmpty()); + assertNotNull(link.attributes()); + assertTrue(link.attributes().isEmpty()); + } + + @Test + void testSpanLinkAttributesApi() { + SpanAttributes attributes = SpanAttributes.builder().build(); + + assertTrue(attributes.isEmpty()); + + attributes = SpanAttributes.builder().put("key", "value").build(); + + assertFalse(attributes.isEmpty()); + } + + @Test + void testSpanLinkAttributesEncoding() { + Map map = + SpanAttributes.builder() + .put("string", "value") + .put("string-empty", "") + .put("string-null", (String) null) + .put("bool", true) + .put("bool-false", false) + .put("long", 12345L) + .put("long-negative", -12345L) + .put("double", 67.89) + .put("double-negative", -67.89) + .putStringArray("string-array", asList("abc", "", null, "def")) + .putStringArray("string-array-null", null) + .putBooleanArray("bool-array", asList(true, false, null, Boolean.TRUE, Boolean.FALSE)) + .putStringArray("bool-array-null", null) + .putLongArray("long-array", asList(123L, 456L, null, Long.MIN_VALUE, Long.MAX_VALUE)) + .putStringArray("long-array-null", null) + .putDoubleArray( + "double-array", asList(12.3D, 45.6D, null, Double.MIN_VALUE, Double.MAX_VALUE)) + .putStringArray("double-array-null", null) + .build() + .asMap(); + + assertEquals("value", map.get("string")); + assertEquals("", map.get("string-empty")); + assertFalse(map.containsKey("string-null")); + assertEquals("true", map.get("bool")); + assertEquals("false", map.get("bool-false")); + assertEquals("12345", map.get("long")); + assertEquals("-12345", map.get("long-negative")); + assertEquals("67.89", map.get("double")); + assertEquals("-67.89", map.get("double-negative")); + + assertEquals("abc", map.get("string-array.0")); + assertEquals("", map.get("string-array.1")); + assertFalse(map.containsKey("string-array.2")); + assertEquals("def", map.get("string-array.3")); + assertFalse(map.containsKey("string-array-null")); + + assertEquals("true", map.get("bool-array.0")); + assertEquals("false", map.get("bool-array.1")); + assertFalse(map.containsKey("bool-array.2")); + assertEquals("true", map.get("bool-array.3")); + assertEquals("false", map.get("bool-array.4")); + assertFalse(map.containsKey("bool-array-null")); + + assertEquals("123", map.get("long-array.0")); + assertEquals("456", map.get("long-array.1")); + assertFalse(map.containsKey("long-array.2")); + assertEquals("-9223372036854775808", map.get("long-array.3")); + assertEquals("9223372036854775807", map.get("long-array.4")); + assertFalse(map.containsKey("long-array-null")); + + assertEquals("12.3", map.get("double-array.0")); + assertEquals("45.6", map.get("double-array.1")); + assertFalse(map.containsKey("double-array.2")); + // Field should be encoded as String using JSON representation so the scientific notation is + // valid + assertEquals("4.9E-324", map.get("double-array.3")); + assertEquals("1.7976931348623157E308", map.get("double-array.4")); + assertFalse(map.containsKey("double-array-null")); + + SpanAttributes attributes = SpanAttributes.fromMap(map); + + assertEquals(map, attributes.asMap()); + } + + @Test + void testSpanLinkAttributesToString() { + assertDoesNotThrow(() -> SpanAttributes.builder().build().toString()); + } + + @Test + void testSpanLinkAttributesEqualsAndHashcode() { + SpanAttributes attributes = SpanAttributes.builder().put("test", "value").build(); + SpanAttributes sameAttributes = SpanAttributes.builder().put("test", "value").build(); + SpanAttributes emptyAttributes = SpanAttributes.builder().build(); + + assertEquals(attributes, sameAttributes); + assertNotEquals(attributes, emptyAttributes); + // argument order matters: exercises emptyAttributes.equals(null) + assertNotEquals(emptyAttributes, null); + assertEquals(attributes.hashCode(), sameAttributes.hashCode()); + assertNotEquals(attributes.hashCode(), emptyAttributes.hashCode()); + } +} diff --git a/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/TaskWrapperTest.java b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/TaskWrapperTest.java new file mode 100644 index 00000000000..44ddb69df27 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/TaskWrapperTest.java @@ -0,0 +1,70 @@ +package datadog.trace.bootstrap.instrumentation.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +class TaskWrapperTest { + + @Test + void testUnwrapping() { + // when: direct wrapping + DirectWrapper directWrapper = new DirectWrapper(); + assertEquals(Integer.class, TaskWrapper.getUnwrappedType(directWrapper)); + + // when: wrapped value is null + RecursiveWrapper nullWrapper = new RecursiveWrapper(); + assertNull(TaskWrapper.getUnwrappedType(nullWrapper)); + + // when: recursive wrapped value within depth limit + RecursiveWrapper shallowRecursiveWrapper = new RecursiveWrapper(); + shallowRecursiveWrapper.wrapped = new DirectWrapper(); + assertEquals(Integer.class, TaskWrapper.getUnwrappedType(shallowRecursiveWrapper)); + + // when: recursive wrapped value exceeds depth limit + DirectWrapper level1 = new DirectWrapper(); + RecursiveWrapper level2 = new RecursiveWrapper(); + level2.wrapped = level1; + RecursiveWrapper level3 = new RecursiveWrapper(); + level3.wrapped = level2; + RecursiveWrapper level4 = new RecursiveWrapper(); + level4.wrapped = level3; + RecursiveWrapper level5 = new RecursiveWrapper(); + level5.wrapped = level4; + RecursiveWrapper level6 = new RecursiveWrapper(); + level6.wrapped = level5; + RecursiveWrapper deepRecursiveWrapper = level6; + // then: terminate at max depth + assertEquals(DirectWrapper.class, TaskWrapper.getUnwrappedType(deepRecursiveWrapper)); + + // when: cycle + RecursiveWrapper outer = new RecursiveWrapper(); + RecursiveWrapper inner = new RecursiveWrapper(); + outer.wrapped = inner; + inner.wrapped = outer; + // then: terminate at max depth + assertEquals(RecursiveWrapper.class, TaskWrapper.getUnwrappedType(outer)); + assertEquals(RecursiveWrapper.class, TaskWrapper.getUnwrappedType(inner)); + } + + static class RecursiveWrapper implements TaskWrapper { + + TaskWrapper wrapped; + + @Override + public Object $$DD$$__unwrap() { + return wrapped; + } + } + + static class DirectWrapper implements TaskWrapper { + + Integer field = 1; + + @Override + public Object $$DD$$__unwrap() { + return field; + } + } +} diff --git a/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/URIDataAdapterTest.java b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/URIDataAdapterTest.java new file mode 100644 index 00000000000..6f1fe84edbc --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/URIDataAdapterTest.java @@ -0,0 +1,170 @@ +package datadog.trace.bootstrap.instrumentation.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import org.tabletest.junit.TableTest; + +public class URIDataAdapterTest { + abstract static class AbstractURIDataAdapterTest { + + abstract URIDataAdapter adapter(URI uri); + + boolean supportsRaw() { + return true; + } + + @TableTest({ + "scenario | input | scheme | host | port | path | fragment | query | rawPath | rawQuery | raw ", + "all parts | 'http://host:17/path?query#fragment' | http | host | 17 | /path | fragment | query | /path | query | /path?query ", + "host only | 'https://h0st' | https | h0st | -1 | '' | | | '' | | '' ", + "encoded parts | 'http://host/v%C3%A4g?fr%C3%A5ga' | http | host | -1 | /väg | | fråga | /v%C3%A4g | fr%C3%A5ga | '/v%C3%A4g?fr%C3%A5ga'" + }) + void testUriParts( + String input, + String scheme, + String host, + int port, + String path, + String fragment, + String query, + String rawPath, + String rawQuery, + String raw) { + URIDataAdapter adapter = URIDataAdapterBase.fromURI(input, this::adapter); + + assertEquals(scheme, adapter.scheme()); + assertEquals(host, adapter.host()); + assertEquals(port, adapter.port()); + assertEquals(path, adapter.path()); + assertEquals(fragment, adapter.fragment()); + assertEquals(query, adapter.query()); + assertEquals(supportsRaw(), adapter.supportsRaw()); + assertEquals(supportsRaw() ? rawPath : null, adapter.rawPath()); + assertEquals(supportsRaw() ? rawQuery : null, adapter.rawQuery()); + assertEquals(supportsRaw() ? raw : null, adapter.raw()); + assertTrue(adapter.isValid()); + } + } + + static class URIDefaultDataAdapterTest extends AbstractURIDataAdapterTest { + + @Override + URIDataAdapter adapter(URI uri) { + return new URIDefaultDataAdapter(uri); + } + } + + static class URINoRawDataAdapterTest extends AbstractURIDataAdapterTest { + + @Override + URIDataAdapter adapter(URI uri) { + return new NoRawTestAdapter(uri); + } + + @Override + boolean supportsRaw() { + return false; + } + + static class NoRawTestAdapter extends URIDataAdapterBase { + private final URI uri; + + NoRawTestAdapter(URI uri) { + this.uri = uri; + } + + @Override + public String scheme() { + return uri.getScheme(); + } + + @Override + public String host() { + return uri.getHost(); + } + + @Override + public int port() { + return uri.getPort(); + } + + @Override + public String fragment() { + return uri.getFragment(); + } + + @Override + public String path() { + return uri.getPath(); + } + + @Override + public String query() { + return uri.getQuery(); + } + + @Override + public boolean supportsRaw() { + return false; + } + + @Override + public String rawPath() { + return null; + } + + @Override + public String rawQuery() { + return null; + } + } + } + + static class URIRawDataAdapterTest extends AbstractURIDataAdapterTest { + + @Override + URIDataAdapter adapter(URI uri) { + return new RawTestAdapter(uri); + } + + static class RawTestAdapter extends URIRawDataAdapter { + private final URI uri; + + RawTestAdapter(URI uri) { + this.uri = uri; + } + + @Override + public String scheme() { + return uri.getScheme(); + } + + @Override + public String host() { + return uri.getHost(); + } + + @Override + public int port() { + return uri.getPort(); + } + + @Override + public String fragment() { + return uri.getFragment(); + } + + @Override + protected String innerRawPath() { + return uri.getRawPath(); + } + + @Override + protected String innerRawQuery() { + return uri.getRawQuery(); + } + } + } +} diff --git a/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/URIUtilsTest.java b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/URIUtilsTest.java new file mode 100644 index 00000000000..93da10be405 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/URIUtilsTest.java @@ -0,0 +1,131 @@ +package datadog.trace.bootstrap.instrumentation.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import datadog.trace.bootstrap.instrumentation.api.URIUtils.LazyUrl; +import java.net.URI; +import java.net.URISyntaxException; +import org.junit.jupiter.api.Test; +import org.tabletest.junit.TableTest; + +class URIUtilsTest { + + @TableTest({ + "scenario | input | expected ", + "port zero | 'https://host:0' | 'https://host/' ", + "root with query | 'http://host/?query' | 'http://host/' ", + "path | 'https://host/path' | 'https://host/path' ", + "custom port | 'http://host:47/path?query' | 'http://host:47/path' ", + "http default port | 'http://host:80/path?query' | 'http://host/path' ", + "http with https port | 'http://host:443/path?query' | 'http://host:443/path'", + "https default port | 'https://host:443/path?query' | 'https://host/path' ", + "https with http port | 'https://host:80/path?query' | 'https://host:80/path'" + }) + void shouldBuildUrls(String input, String expected) throws URISyntaxException { + URI uri = new URI(input); + String url = URIUtils.buildURL(uri.getScheme(), uri.getHost(), uri.getPort(), uri.getPath()); + LazyUrl lazyUrl = + URIUtils.lazyValidURL(uri.getScheme(), uri.getHost(), uri.getPort(), uri.getPath()); + + assertEquals(expected, url); + assertEquals(uri.getPath(), lazyUrl.path()); + assertEquals(expected, lazyUrl.get()); + assertEquals(expected, lazyUrl.toString()); + } + + @TableTest({ + "scenario | scheme | host | port | path | expected ", + "all null | | | -1 | | / ", + "all empty | '' | '' | -1 | '' | ':///' ", + "empty with relative path | '' | '' | -1 | relative | ':///relative'", + "empty with absolute path | '' | '' | -1 | /absolute | ':///absolute'", + "null with relative path | | | -1 | relative | relative ", + "null with absolute path | | | -1 | /absolute | /absolute " + }) + void shouldBuildUrlsFromCornerCases( + String scheme, String host, int port, String path, String expected) { + String url = URIUtils.buildURL(scheme, host, port, path); + LazyUrl lazyUrl = URIUtils.lazyValidURL(scheme, host, port, path); + + assertEquals(expected, url); + assertEquals(path != null ? path : "", lazyUrl.path()); + assertEquals(expected, lazyUrl.toString()); + assertEquals(expected, lazyUrl.get()); + } + + @TableTest({ + "scenario | encoded | expected ", + "null | | ", + "empty | '' | '' ", + "plain | plain | plain ", + "encoded spaces | '%C3%BEungur%20hn%C3%ADfur' | 'þungur hnífur' ", + "plus kept | 'tr%C3%A5kig%20str%C3%A4ng+med+%2B' | 'tråkig sträng+med++' ", + "only plus | 'boring+string+with+only+plus' | 'boring+string+with+only+plus'", + "too few characters | '%' | '�' ", + "too few characters after one digit | '%1' | '�' ", + "illegal 1st character | '%W1' | '�' ", + "illegal 2nd character | '%1X' | '�' ", + "illegal 1st character in word | 'wh%Y1t' | 'wh�t' ", + "illegal 2nd character in word | 'wh%1Zt' | 'wh�t' ", + "too few characters at end | 'wh%' | 'wh�' ", + "too few characters after one digit at end | 'wh%1' | 'wh�' ", + "invalid 2 byte sequence | '%C3%28' | '�(' ", + "invalid sequence identifier | '%A0%A1' | '��' ", + "invalid 3 byte sequence in 2nd byte | '%E2%28%A1' | '�(�' ", + "invalid 3 byte sequence in 3rd byte | '%E2%82%28' | '�(' ", + "invalid 4 byte sequence in 2nd byte | '%F0%28%8C%BC' | '�(��' ", + "invalid 4 byte sequence in 3rd byte | '%F0%90%28%BC' | '�(�' ", + "invalid 4 byte sequence in 4th byte | '%F0%28%8C%28' | '�(�(' ", + "valid 5 byte sequence but not unicode | '%F8%A1%A1%A1%A1' | '�����' ", + "valid 6 byte sequence but not unicode | '%FC%A1%A1%A1%A1%A1' | '������' " + }) + void shouldDecodeUrlEncodedIgnoringPlus(String encoded, String expected) { + String decoded = URIUtils.decode(encoded); + + assertEquals(expected, decoded); + } + + @TableTest({ + "scenario | encoded | expected ", + "null | | ", + "plain | plain | plain ", + "encoded spaces | '%C3%BEungur%20hn%C3%ADfur' | 'þungur hnífur' ", + "plus and %2B | 'v%C3%A4ldigt+tr%C3%A5kig%20str%C3%A4ng+med+%2B' | 'väldigt tråkig sträng med +' ", + "only plus | 'very+boring+string+with+only+plus' | 'very boring string with only plus'" + }) + void shouldDecodeUrlEncodedWithPlus(String encoded, String expected) { + String decoded = URIUtils.decode(encoded, true); + + assertEquals(expected, decoded); + } + + @Test + void testLazyUrlForCodeCoverage() { + String raw = "weird"; + LazyUrl invalid = URIUtils.lazyInvalidUrl(raw); + + assertNull(invalid.path()); + assertEquals(raw, invalid.toString()); + assertEquals(raw, invalid.get()); + assertEquals(raw.length(), invalid.length()); + assertEquals(raw.hashCode(), invalid.hashCode()); + assertEquals("ei", invalid.subSequence(1, 3).toString()); + assertEquals('r', invalid.charAt(3)); + } + + @TableTest({ + "scenario | first | second | expected ", + "trailing slash | 'http://localhost/' | 'test/me' | 'http://localhost/test/me'", + "leading slash | 'http://localhost' | '/test/me' | 'http://localhost/test/me'", + "absolute second | 'http://localhost/' | 'http://localhost/test/me' | 'http://localhost/test/me'", + "both null | | | ", + "null first | | '/test' | '/test' ", + "null second | 'http://localhost' | | 'http://localhost/' " + }) + void testSafeConcatForCodeCoverage(String first, String second, String expected) { + URI concat = URIUtils.safeConcat(first, second); + + assertEquals(expected, concat == null ? null : concat.toString()); + } +} diff --git a/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/UnparseableURIAdapterTest.java b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/UnparseableURIAdapterTest.java new file mode 100644 index 00000000000..da3b9f0d1a8 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/UnparseableURIAdapterTest.java @@ -0,0 +1,27 @@ +package datadog.trace.bootstrap.instrumentation.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +class UnparseableURIAdapterTest { + + @Test + void shouldReturnRawUriOnly() { + String uriStr = "http://myurl/path?query=value#fragment"; + UnparseableURIDataAdapter uriAdapter = new UnparseableURIDataAdapter(uriStr); + + assertFalse(uriAdapter.isValid()); + assertNull(uriAdapter.host()); + assertEquals(0, uriAdapter.port()); + assertNull(uriAdapter.fragment()); + assertNull(uriAdapter.path()); + assertNull(uriAdapter.query()); + assertNull(uriAdapter.rawQuery()); + assertNull(uriAdapter.scheme()); + assertNull(uriAdapter.rawPath()); + assertEquals(uriStr, uriAdapter.raw()); + } +} diff --git a/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/Utf8ByteStringTest.java b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/Utf8ByteStringTest.java new file mode 100644 index 00000000000..d48ffca4ef9 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/Utf8ByteStringTest.java @@ -0,0 +1,113 @@ +package datadog.trace.bootstrap.instrumentation.api; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +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.params.provider.Arguments.arguments; + +import java.nio.ByteBuffer; +import java.util.stream.Stream; +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; + +class Utf8ByteStringTest { + + @TableTest({ + "scenario | str ", + "null | ", + "foo | foo ", + "bar | bar ", + "a longer string | alongerstring" + }) + void wrapStringAndProduceTheRightBytesAndString(String str) { + UTF8BytesString utf8String = UTF8BytesString.create(str); + + if (str == null) { + assertNull(utf8String); + return; + } + assertWrapped(str, str.getBytes(UTF_8), utf8String); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("bytesArguments") + void wrapBytesAndProduceTheRightStringAndBytes(String str, byte[] bytes) { + UTF8BytesString utf8String = UTF8BytesString.create(bytes); + + if (str == null) { + assertNull(utf8String); + return; + } + assertWrapped(str, bytes, utf8String); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("bytesArguments") + void wrapByteBufferAndProduceTheRightStringAndBytes(String str, byte[] bytes) { + UTF8BytesString utf8String = + UTF8BytesString.create(bytes != null ? ByteBuffer.wrap(bytes) : (ByteBuffer) null); + + if (str == null) { + assertNull(utf8String); + return; + } + assertWrapped(str, bytes, utf8String); + } + + static Stream bytesArguments() { + return Stream.of( + arguments(null, null), + arguments("foo", new byte[] {0x66, 0x6f, 0x6f}), + arguments("bar", new byte[] {0x62, 0x61, 0x72}), + arguments( + "alongerstring", + new byte[] { + 0x61, 0x6c, 0x6f, 0x6e, 0x67, 0x65, 0x72, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67 + })); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("behaveLikeAProperCharSequenceArguments") + void behaveLikeAProperCharSequence(String description, CharSequence chars) { + UTF8BytesString utf8String = UTF8BytesString.create(chars); + + if (chars == null) { + assertNull(utf8String); + return; + } + assertEquals(chars.toString(), utf8String.toString()); + assertEquals(chars.length(), utf8String.length()); + for (int index = 0; index < chars.length(); index++) { + assertEquals(chars.charAt(index), utf8String.charAt(index)); + } + assertEquals( + chars.subSequence(1, chars.length()).toString(), + utf8String.subSequence(1, chars.length()).toString()); + } + + static Stream behaveLikeAProperCharSequenceArguments() { + return Stream.of( + arguments("null", null), + arguments("String", "foo"), + arguments("StringBuffer", new StringBuffer("bar")), + arguments("long StringBuffer", new StringBuffer("someotherlongstring")), + arguments("UTF8BytesString", UTF8BytesString.create("utf8string"))); + } + + private static void assertWrapped(String str, byte[] bytes, UTF8BytesString utf8String) { + assertEquals(str, utf8String.toString()); + assertEquals(str.hashCode(), utf8String.hashCode()); + ByteBuffer utf8Bytes = ByteBuffer.allocate(bytes.length); + utf8String.transferTo(utf8Bytes); + // check that we get back the same byte array + assertArrayEquals(bytes, utf8Bytes.array()); + // argument order matters: exercises UTF8BytesString.equals + assertNotEquals(utf8String, null); + assertNotEquals(utf8String, str); + assertNotEquals(utf8String, UTF8BytesString.create("somethingcompletelydifferent")); + } +}