From 97b6e05d815ab1f71da73fabd13389dfeec378f8 Mon Sep 17 00:00:00 2001 From: Idan Shatz Date: Wed, 13 Aug 2025 14:45:53 -0400 Subject: [PATCH 1/7] add support for org level APM_TRACING configs --- .../trace/core/TracingConfigPoller.java | 178 +++++++++++- .../trace/core/TracingConfigPollerTest.groovy | 256 ++++++++++++++++++ .../datadog/remoteconfig/Capabilities.java | 1 + 3 files changed, 427 insertions(+), 8 deletions(-) create mode 100644 dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy diff --git a/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java b/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java index ad3a57c1505..e9a8638a4a2 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java @@ -8,6 +8,7 @@ import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_ENABLE_DYNAMIC_INSTRUMENTATION; import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_ENABLE_EXCEPTION_REPLAY; import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_ENABLE_LIVE_DEBUGGING; +import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_MERGE_CONFIG; import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_SAMPLE_RATE; import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_SAMPLE_RULES; import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_TRACING_ENABLED; @@ -34,6 +35,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -74,7 +76,8 @@ public void start(Config config, SharedCommunicationObjects sco) { | CAPABILITY_APM_TRACING_ENABLE_DYNAMIC_INSTRUMENTATION | CAPABILITY_APM_TRACING_ENABLE_EXCEPTION_REPLAY | CAPABILITY_APM_TRACING_ENABLE_CODE_ORIGIN - | CAPABILITY_APM_TRACING_ENABLE_LIVE_DEBUGGING); + | CAPABILITY_APM_TRACING_ENABLE_LIVE_DEBUGGING + | CAPABILITY_APM_TRACING_MERGE_CONFIG); } stopPolling = new Updater().register(config, configPoller); } @@ -95,6 +98,7 @@ final class Updater implements ProductListener { TRACE_SAMPLING_RULE = MOSHI.adapter(TracingSamplingRule.class); } + private final Map configs = new HashMap<>(); private boolean receivedOverrides = false; public Runnable register(Config config, ConfigurationPoller poller) { @@ -115,11 +119,12 @@ public void accept(ConfigKey configKey, byte[] content, PollingRateHinter hinter Okio.buffer(Okio.source(new ByteArrayInputStream(content)))); if (null != overrides && null != overrides.libConfig) { - receivedOverrides = true; - applyConfigOverrides(checkConfig(overrides.libConfig)); + configs.put(configKey.getConfigId(), overrides); if (log.isDebugEnabled()) { log.debug( - "Applied APM_TRACING overrides: {}", CONFIG_OVERRIDES_ADAPTER.toJson(overrides)); + "Applied APM_TRACING overrides: {} - priority: {}", + CONFIG_OVERRIDES_ADAPTER.toJson(overrides), + overrides.getOverridePriority()); } } else { log.debug("No APM_TRACING overrides"); @@ -127,15 +132,36 @@ public void accept(ConfigKey configKey, byte[] content, PollingRateHinter hinter } @Override - public void remove(ConfigKey configKey, PollingRateHinter hinter) {} + public void remove(ConfigKey configKey, PollingRateHinter hinter) { + configs.remove(configKey.getConfigId()); + } @Override public void commit(PollingRateHinter hinter) { - if (!receivedOverrides) { + // sort configs by override priority + List sortedConfigs = + configs.values().stream() + .sorted(Comparator.comparingInt(ConfigOverrides::getOverridePriority).reversed()) + .map(config -> config.libConfig) + .collect(Collectors.toList()); + + LibConfig mergedConfig = LibConfig.mergeLibConfigs(sortedConfigs); + + if (mergedConfig != null) { + // apply merged config + if (log.isDebugEnabled()) { + ConfigOverrides mergedConfigOverrides = new ConfigOverrides(); + mergedConfigOverrides.libConfig = mergedConfig; + log.debug( + "Applying merged APM_TRACING config: {}", + CONFIG_OVERRIDES_ADAPTER.toJson(mergedConfigOverrides)); + } + applyConfigOverrides(mergedConfig); + } + + if (sortedConfigs.isEmpty()) { removeConfigOverrides(); log.debug("Removed APM_TRACING overrides"); - } else { - receivedOverrides = false; } } @@ -263,6 +289,77 @@ private Map parseTagListToMap(List input) { static final class ConfigOverrides { @Json(name = "lib_config") public LibConfig libConfig; + + @Json(name = "service_target") + public ServiceTarget serviceTarget; + + @Json(name = "k8s_target_v2") + public K8sTargetV2 k8sTargetV2; + + public int getOverridePriority() { + boolean isSingleEnvironment = isSingleEnvironment(); + boolean isSingleService = isSingleService(); + boolean isClusterTarget = isClusterTarget(); + + // Service+ Environment level override - highest priority + if (isSingleEnvironment && isSingleService) { + return 5; + } + + if (isSingleService) { + return 4; + } + + if (isSingleEnvironment) { + return 3; + } + + if (isClusterTarget) { + return 2; + } + + // Org level override - lowest priority + return 1; + } + + // allEnvironments = serviceTarget is null or serviceTarget.env is null or '*' + public boolean isSingleEnvironment() { + return serviceTarget != null && serviceTarget.env != null && !"*".equals(serviceTarget.env); + } + + public boolean isSingleService() { + return serviceTarget != null + && serviceTarget.service != null + && !"*".equals(serviceTarget.service); + } + + public boolean isClusterTarget() { + return k8sTargetV2 != null; + } + } + + static final class ServiceTarget { + @Json(name = "service") + public String service; + + @Json(name = "env") + public String env; + } + + static final class K8sTargetV2 { + @Json(name = "cluster_targets") + public List clusterTargets; + } + + static final class ClusterTarget { + @Json(name = "cluster_name") + public String clusterName; + + @Json(name = "enabled") + public Boolean enabled; + + @Json(name = "enabled_namespaces") + public List enabledNamespaces; } static final class LibConfig { @@ -307,6 +404,71 @@ static final class LibConfig { @Json(name = "live_debugging_enabled") public Boolean liveDebuggingEnabled; + + /** + * Merges a list of LibConfig objects by taking the first non-null value for each field. + * + * @param configs the list of LibConfig objects to merge + * @return a merged LibConfig object, or null if the input list is null or empty + */ + public static LibConfig mergeLibConfigs(List configs) { + if (configs == null || configs.isEmpty()) { + return null; + } + + LibConfig merged = new LibConfig(); + + for (LibConfig config : configs) { + if (config == null) { + continue; + } + + if (merged.tracingEnabled == null) { + merged.tracingEnabled = config.tracingEnabled; + } + if (merged.debugEnabled == null) { + merged.debugEnabled = config.debugEnabled; + } + if (merged.runtimeMetricsEnabled == null) { + merged.runtimeMetricsEnabled = config.runtimeMetricsEnabled; + } + if (merged.logsInjectionEnabled == null) { + merged.logsInjectionEnabled = config.logsInjectionEnabled; + } + if (merged.dataStreamsEnabled == null) { + merged.dataStreamsEnabled = config.dataStreamsEnabled; + } + if (merged.serviceMapping == null) { + merged.serviceMapping = config.serviceMapping; + } + if (merged.headerTags == null) { + merged.headerTags = config.headerTags; + } + if (merged.traceSampleRate == null) { + merged.traceSampleRate = config.traceSampleRate; + } + if (merged.tracingTags == null) { + merged.tracingTags = config.tracingTags; + } + if (merged.tracingSamplingRules == null) { + merged.tracingSamplingRules = config.tracingSamplingRules; + } + if (merged.dynamicInstrumentationEnabled == null) { + merged.dynamicInstrumentationEnabled = config.dynamicInstrumentationEnabled; + } + if (merged.exceptionReplayEnabled == null) { + merged.exceptionReplayEnabled = config.exceptionReplayEnabled; + } + if (merged.codeOriginEnabled == null) { + merged.codeOriginEnabled = config.codeOriginEnabled; + } + if (merged.liveDebuggingEnabled == null) { + merged.liveDebuggingEnabled = config.liveDebuggingEnabled; + } + } + + return merged; + } } /** Holds the raw JSON string and the parsed rule data. */ diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy new file mode 100644 index 00000000000..6152909d1a5 --- /dev/null +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy @@ -0,0 +1,256 @@ +package datadog.trace.core + +import datadog.communication.ddagent.DDAgentFeaturesDiscovery +import datadog.communication.ddagent.SharedCommunicationObjects +import datadog.communication.monitor.Monitoring +import datadog.remoteconfig.ConfigurationPoller +import datadog.remoteconfig.Product +import datadog.remoteconfig.state.ParsedConfigKey +import datadog.remoteconfig.state.ProductListener +import datadog.trace.api.Config +import datadog.trace.api.DynamicConfig +import datadog.trace.core.test.DDCoreSpecification +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import spock.lang.Timeout + +import java.nio.charset.StandardCharsets + +@Timeout(10) +class TracingConfigPollerTest extends DDCoreSpecification { + + def "test mergeLibConfigs with null and non-null values"() { + setup: + def config1 = new TracingConfigPoller.LibConfig() // all nulls + def config2 = new TracingConfigPoller.LibConfig( + tracingEnabled: true, + debugEnabled: false, + runtimeMetricsEnabled: true, + logsInjectionEnabled: false, + dataStreamsEnabled: true, + traceSampleRate: 0.5, + dynamicInstrumentationEnabled: true, + exceptionReplayEnabled: false, + codeOriginEnabled: true, + liveDebuggingEnabled: false + ) + def config3 = new TracingConfigPoller.LibConfig( + tracingEnabled: false, + debugEnabled: true, + runtimeMetricsEnabled: false, + logsInjectionEnabled: true, + dataStreamsEnabled: false, + traceSampleRate: 0.8, + dynamicInstrumentationEnabled: false, + exceptionReplayEnabled: true, + codeOriginEnabled: false, + liveDebuggingEnabled: true + ) + + when: + def merged = TracingConfigPoller.LibConfig.mergeLibConfigs([config1, config2, config3]) + + then: + merged != null + // Should take first non-null values from config2 + merged.tracingEnabled == true + merged.debugEnabled == false + merged.runtimeMetricsEnabled == true + merged.logsInjectionEnabled == false + merged.dataStreamsEnabled == true + merged.traceSampleRate == 0.5 + merged.dynamicInstrumentationEnabled == true + merged.exceptionReplayEnabled == false + merged.codeOriginEnabled == true + merged.liveDebuggingEnabled == false + } + + def "test single service+env config priority calculation"() { + setup: + def configOverrides = new TracingConfigPoller.ConfigOverrides() + configOverrides.serviceTarget = new TracingConfigPoller.ServiceTarget( + service: "test-service", + env: "staging" + ) + configOverrides.libConfig = new TracingConfigPoller.LibConfig( + tracingEnabled: false, + debugEnabled: true, + runtimeMetricsEnabled: false, + logsInjectionEnabled: true, + dataStreamsEnabled: false, + traceSampleRate: 0.3, + dynamicInstrumentationEnabled: true, + exceptionReplayEnabled: false, + codeOriginEnabled: true, + liveDebuggingEnabled: false + ) + + when: + def priority = configOverrides.getOverridePriority() + + then: + priority == 5 // highest priority for service + environment + configOverrides.isSingleService() == true + configOverrides.isSingleEnvironment() == true + configOverrides.isClusterTarget() == false + } + + def "test org level + service level config priority comparison"() { + setup: + def orgConfig = new TracingConfigPoller.ConfigOverrides() + orgConfig.libConfig = new TracingConfigPoller.LibConfig( + tracingEnabled: true, + debugEnabled: false, + runtimeMetricsEnabled: true, + logsInjectionEnabled: false, + dataStreamsEnabled: true, + traceSampleRate: 0.7, + dynamicInstrumentationEnabled: false, + exceptionReplayEnabled: true, + codeOriginEnabled: false, + liveDebuggingEnabled: true + ) + + def serviceConfig = new TracingConfigPoller.ConfigOverrides() + serviceConfig.serviceTarget = new TracingConfigPoller.ServiceTarget( + service: "test-service", + env: "*" + ) + serviceConfig.libConfig = new TracingConfigPoller.LibConfig( + tracingEnabled: false, + debugEnabled: true, + runtimeMetricsEnabled: false, + logsInjectionEnabled: true, + dataStreamsEnabled: false, + traceSampleRate: 0.2, + dynamicInstrumentationEnabled: true, + exceptionReplayEnabled: false, + codeOriginEnabled: true, + liveDebuggingEnabled: false + ) + + when: + def orgPriority = orgConfig.getOverridePriority() + def servicePriority = serviceConfig.getOverridePriority() + + then: + orgPriority == 1 // lowest priority for org level + servicePriority == 4 // higher priority for service level + servicePriority > orgPriority + + and: + orgConfig.isSingleService() == false + orgConfig.isSingleEnvironment() == false + orgConfig.isClusterTarget() == false + + serviceConfig.isSingleService() == true + serviceConfig.isSingleEnvironment() == false // env is "*" + serviceConfig.isClusterTarget() == false + } + + def "test actual config commit with service and org level configs"() { + setup: + def orgKey = ParsedConfigKey.parse("datadog/2/APM_TRACING/org_config/config") + def serviceKey = ParsedConfigKey.parse("datadog/2/APM_TRACING/service_config/config") + def poller = Mock(ConfigurationPoller) + def sco = new SharedCommunicationObjects( + okHttpClient: Mock(OkHttpClient), + monitoring: Mock(Monitoring), + agentUrl: HttpUrl.get('https://example.com'), + featuresDiscovery: Mock(DDAgentFeaturesDiscovery), + configurationPoller: poller + ) + + def updater + + when: + def tracer = CoreTracer.builder() + .sharedCommunicationObjects(sco) + .pollForTracingConfiguration() + .build() + + then: + 1 * poller.addListener(Product.APM_TRACING, _ as ProductListener) >> { + updater = it[1] // capture config updater for further testing + } + and: + tracer.captureTraceConfig().serviceMapping == [:] + tracer.captureTraceConfig().traceSampleRate == null + + when: + // Add org level config (priority 1) - should set service mapping + updater.accept(orgKey, """ + { + "lib_config": { + "tracing_service_mapping": [ + { + "from_key": "org-service", + "to_name": "org-mapped" + } + ], + "tracing_sampling_rate": 0.7 + } + } + """.getBytes(StandardCharsets.UTF_8), null) + + // Add service level config (priority 4) - should override service mapping and add header tags + updater.accept(serviceKey, """ + { + "service_target": { + "service": "test-service", + "env": "*" + }, + "lib_config": { + "tracing_service_mapping": [ + { + "from_key": "service-specific", + "to_name": "service-mapped" + } + ], + "tracing_header_tags": [ + { + "header": "X-Custom-Header", + "tag_name": "custom.header" + } + ], + "tracing_sampling_rate": 0.3 + } + } + """.getBytes(StandardCharsets.UTF_8), null) + + // Commit both configs + updater.commit() + + then: + // Service level config should take precedence due to higher priority (4 vs 1) + tracer.captureTraceConfig().serviceMapping == ["service-specific":"service-mapped"] + tracer.captureTraceConfig().traceSampleRate == 0.3 + tracer.captureTraceConfig().requestHeaderTags == ["x-custom-header":"custom.header"] + tracer.captureTraceConfig().responseHeaderTags == ["x-custom-header":"custom.header"] + + when: + // Remove service level config + updater.remove(serviceKey, null) + updater.commit() + + then: + // Should fall back to org level config + tracer.captureTraceConfig().serviceMapping == ["org-service":"org-mapped"] + tracer.captureTraceConfig().traceSampleRate == 0.7 + tracer.captureTraceConfig().requestHeaderTags == [:] + tracer.captureTraceConfig().responseHeaderTags == [:] + + when: + // Remove org level config + updater.remove(orgKey, null) + updater.commit() + + then: + // Should have no configs + tracer.captureTraceConfig().serviceMapping == [:] + tracer.captureTraceConfig().traceSampleRate == null + + cleanup: + tracer?.close() + } +} diff --git a/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java b/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java index b8e1124a1b2..471b1bd7f20 100644 --- a/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java +++ b/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java @@ -42,4 +42,5 @@ public interface Capabilities { long CAPABILITY_APM_TRACING_ENABLE_EXCEPTION_REPLAY = 1L << 39; long CAPABILITY_APM_TRACING_ENABLE_CODE_ORIGIN = 1L << 40; long CAPABILITY_APM_TRACING_ENABLE_LIVE_DEBUGGING = 1L << 41; + long CAPABILITY_APM_TRACING_MERGE_CONFIG = 1L << 42; } From 69dff6d2c52a0f432e8be7d9f50c1cf28be4a599 Mon Sep 17 00:00:00 2001 From: Idan Shatz Date: Thu, 14 Aug 2025 09:12:42 -0400 Subject: [PATCH 2/7] fix coverage error by adding more tests --- .../trace/core/TracingConfigPollerTest.groovy | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy index 6152909d1a5..e2cd3a7a7ec 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy @@ -181,6 +181,10 @@ class TracingConfigPollerTest extends DDCoreSpecification { // Add org level config (priority 1) - should set service mapping updater.accept(orgKey, """ { + "service_target": { + "service": "*", + "env": "*" + }, "lib_config": { "tracing_service_mapping": [ { @@ -253,4 +257,111 @@ class TracingConfigPollerTest extends DDCoreSpecification { cleanup: tracer?.close() } + + def "test actual config commit with cluster and org level configs"() { + setup: + def orgKey = ParsedConfigKey.parse("datadog/2/APM_TRACING/org_config/config") + def clusterKey = ParsedConfigKey.parse("datadog/2/APM_TRACING/cluster_config/config") + def poller = Mock(ConfigurationPoller) + def sco = new SharedCommunicationObjects( + okHttpClient: Mock(OkHttpClient), + monitoring: Mock(Monitoring), + agentUrl: HttpUrl.get('https://example.com'), + featuresDiscovery: Mock(DDAgentFeaturesDiscovery), + configurationPoller: poller + ) + + def updater + + when: + def tracer = CoreTracer.builder() + .sharedCommunicationObjects(sco) + .pollForTracingConfiguration() + .build() + + then: + 1 * poller.addListener(Product.APM_TRACING, _ as ProductListener) >> { + updater = it[1] // capture config updater for further testing + } + and: + tracer.captureTraceConfig().serviceMapping == [:] + tracer.captureTraceConfig().traceSampleRate == null + tracer.captureTraceConfig().tracingTags == [:] + + when: + // Add org level config (priority 1) - should set service mapping and sampling rate + updater.accept(orgKey, """ + { + "lib_config": { + "tracing_service_mapping": [ + { + "from_key": "org-service", + "to_name": "org-mapped" + } + ], + "tracing_sampling_rate": 0.8, + "tracing_tags": ["org:tag", "level:org"] + } + } + """.getBytes(StandardCharsets.UTF_8), null) + + // Add cluster level config (priority 2) - should override org level configs + updater.accept(clusterKey, """ + { + "k8s_target_v2": { + "cluster_targets": [ + { + "cluster_name": "test-cluster", + "enabled": true, + "enabled_namespaces": ["default", "kube-system"] + } + ] + }, + "lib_config": { + "tracing_service_mapping": [ + { + "from_key": "cluster-service", + "to_name": "cluster-mapped" + } + ], + "tracing_sampling_rate": 0.4, + "tracing_tags": ["cluster:tag", "level:cluster"] + } + } + """.getBytes(StandardCharsets.UTF_8), null) + + // Commit both configs + updater.commit() + + then: + // Cluster level config should take precedence due to higher priority (2 vs 1) + tracer.captureTraceConfig().serviceMapping == ["cluster-service":"cluster-mapped"] + tracer.captureTraceConfig().traceSampleRate == 0.4 + tracer.captureTraceConfig().tracingTags == ["cluster":"tag", "level":"cluster"] + + when: + // Remove cluster level config + updater.remove(clusterKey, null) + updater.commit() + + then: + // Should fall back to org level config + tracer.captureTraceConfig().serviceMapping == ["org-service":"org-mapped"] + tracer.captureTraceConfig().traceSampleRate == 0.8 + tracer.captureTraceConfig().tracingTags == ["org":"tag", "level":"org"] + + when: + // Remove org level config + updater.remove(orgKey, null) + updater.commit() + + then: + // Should have no configs + tracer.captureTraceConfig().serviceMapping == [:] + tracer.captureTraceConfig().traceSampleRate == null + tracer.captureTraceConfig().tracingTags == [:] + + cleanup: + tracer?.close() + } } From 0bc1ad1ee827a5636836fc64925e093c533c6df5 Mon Sep 17 00:00:00 2001 From: Idan Shatz Date: Thu, 14 Aug 2025 10:47:09 -0400 Subject: [PATCH 3/7] fix capability name and index --- .../main/java/datadog/trace/core/TracingConfigPoller.java | 4 ++-- .../src/main/java/datadog/remoteconfig/Capabilities.java | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java b/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java index e9a8638a4a2..e69cc139c6e 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java @@ -8,7 +8,7 @@ import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_ENABLE_DYNAMIC_INSTRUMENTATION; import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_ENABLE_EXCEPTION_REPLAY; import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_ENABLE_LIVE_DEBUGGING; -import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_MERGE_CONFIG; +import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_MULTICONFIG; import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_SAMPLE_RATE; import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_SAMPLE_RULES; import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_TRACING_ENABLED; @@ -77,7 +77,7 @@ public void start(Config config, SharedCommunicationObjects sco) { | CAPABILITY_APM_TRACING_ENABLE_EXCEPTION_REPLAY | CAPABILITY_APM_TRACING_ENABLE_CODE_ORIGIN | CAPABILITY_APM_TRACING_ENABLE_LIVE_DEBUGGING - | CAPABILITY_APM_TRACING_MERGE_CONFIG); + | CAPABILITY_APM_TRACING_MULTICONFIG); } stopPolling = new Updater().register(config, configPoller); } diff --git a/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java b/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java index 471b1bd7f20..7ddd4e0c693 100644 --- a/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java +++ b/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java @@ -42,5 +42,8 @@ public interface Capabilities { long CAPABILITY_APM_TRACING_ENABLE_EXCEPTION_REPLAY = 1L << 39; long CAPABILITY_APM_TRACING_ENABLE_CODE_ORIGIN = 1L << 40; long CAPABILITY_APM_TRACING_ENABLE_LIVE_DEBUGGING = 1L << 41; - long CAPABILITY_APM_TRACING_MERGE_CONFIG = 1L << 42; + long CAPABILITY_ASM_DD_MULTICONFIG = 1L << 42; + long CAPABILITY_ASM_TRACE_TAGGING_RULES = 1L << 43; + long CAPABILITY_ASM_EXTENDED_DATA_COLLECTION = 1L << 44; + long CAPABILITY_APM_TRACING_MULTICONFIG = 1L << 45; } From 117096e75504ecf4b535c0052d2faa83d80481cc Mon Sep 17 00:00:00 2001 From: Idan Shatz Date: Thu, 14 Aug 2025 11:39:45 -0400 Subject: [PATCH 4/7] fix codenarc error --- .../groovy/datadog/trace/core/TracingConfigPollerTest.groovy | 2 -- 1 file changed, 2 deletions(-) diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy index e2cd3a7a7ec..514a5b46b72 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy @@ -7,8 +7,6 @@ import datadog.remoteconfig.ConfigurationPoller import datadog.remoteconfig.Product import datadog.remoteconfig.state.ParsedConfigKey import datadog.remoteconfig.state.ProductListener -import datadog.trace.api.Config -import datadog.trace.api.DynamicConfig import datadog.trace.core.test.DDCoreSpecification import okhttp3.HttpUrl import okhttp3.OkHttpClient From 8a80bee1494a0e9cc9de10ecfcfd416857bd169d Mon Sep 17 00:00:00 2001 From: Idan Shatz Date: Sun, 24 Aug 2025 22:39:08 -0400 Subject: [PATCH 5/7] address PR comments --- .../trace/core/TracingConfigPoller.java | 9 +- .../trace/core/TracingConfigPollerTest.groovy | 204 +++--------------- 2 files changed, 29 insertions(+), 184 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java b/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java index e69cc139c6e..94eb01b7694 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java @@ -90,11 +90,13 @@ public void stop() { final class Updater implements ProductListener { private final JsonAdapter CONFIG_OVERRIDES_ADAPTER; + private final JsonAdapter LIB_CONFIG_ADAPTER; private final JsonAdapter TRACE_SAMPLING_RULE; { Moshi MOSHI = new Moshi.Builder().add(new TracingSamplingRulesAdapter()).build(); CONFIG_OVERRIDES_ADAPTER = MOSHI.adapter(ConfigOverrides.class); + LIB_CONFIG_ADAPTER = MOSHI.adapter(LibConfig.class); TRACE_SAMPLING_RULE = MOSHI.adapter(TracingSamplingRule.class); } @@ -150,13 +152,10 @@ public void commit(PollingRateHinter hinter) { if (mergedConfig != null) { // apply merged config if (log.isDebugEnabled()) { - ConfigOverrides mergedConfigOverrides = new ConfigOverrides(); - mergedConfigOverrides.libConfig = mergedConfig; log.debug( - "Applying merged APM_TRACING config: {}", - CONFIG_OVERRIDES_ADAPTER.toJson(mergedConfigOverrides)); + "Applying merged APM_TRACING config: {}", LIB_CONFIG_ADAPTER.toJson(mergedConfig)); } - applyConfigOverrides(mergedConfig); + applyConfigOverrides(checkConfig(mergedConfig)); } if (sortedConfigs.isEmpty()) { diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy index 514a5b46b72..6695e76ac2c 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy @@ -63,88 +63,41 @@ class TracingConfigPollerTest extends DDCoreSpecification { merged.liveDebuggingEnabled == false } - def "test single service+env config priority calculation"() { + def "test config priority calculation"() { setup: def configOverrides = new TracingConfigPoller.ConfigOverrides() + if (service != null || env != null) { configOverrides.serviceTarget = new TracingConfigPoller.ServiceTarget( - service: "test-service", - env: "staging" + service: service, + env: env, ) - configOverrides.libConfig = new TracingConfigPoller.LibConfig( - tracingEnabled: false, - debugEnabled: true, - runtimeMetricsEnabled: false, - logsInjectionEnabled: true, - dataStreamsEnabled: false, - traceSampleRate: 0.3, - dynamicInstrumentationEnabled: true, - exceptionReplayEnabled: false, - codeOriginEnabled: true, - liveDebuggingEnabled: false + } + if (clusterName != null) { + configOverrides.k8sTargetV2 = new TracingConfigPoller.K8sTargetV2( + clusterTargets: [new TracingConfigPoller.ClusterTarget( + clusterName: clusterName, + enabled: true, + ) + ] ) + } + configOverrides.libConfig = new TracingConfigPoller.LibConfig() when: def priority = configOverrides.getOverridePriority() then: - priority == 5 // highest priority for service + environment - configOverrides.isSingleService() == true - configOverrides.isSingleEnvironment() == true - configOverrides.isClusterTarget() == false + priority == expectedPriority + + where: + service | env | clusterName | expectedPriority + "test-service" | "staging" | null | 5 + "test-service" | "*" | null | 4 + "*" | "staging" | null | 3 + null | null | "test-cluster" | 2 + "*" | "*" | null | 1 } - def "test org level + service level config priority comparison"() { - setup: - def orgConfig = new TracingConfigPoller.ConfigOverrides() - orgConfig.libConfig = new TracingConfigPoller.LibConfig( - tracingEnabled: true, - debugEnabled: false, - runtimeMetricsEnabled: true, - logsInjectionEnabled: false, - dataStreamsEnabled: true, - traceSampleRate: 0.7, - dynamicInstrumentationEnabled: false, - exceptionReplayEnabled: true, - codeOriginEnabled: false, - liveDebuggingEnabled: true - ) - - def serviceConfig = new TracingConfigPoller.ConfigOverrides() - serviceConfig.serviceTarget = new TracingConfigPoller.ServiceTarget( - service: "test-service", - env: "*" - ) - serviceConfig.libConfig = new TracingConfigPoller.LibConfig( - tracingEnabled: false, - debugEnabled: true, - runtimeMetricsEnabled: false, - logsInjectionEnabled: true, - dataStreamsEnabled: false, - traceSampleRate: 0.2, - dynamicInstrumentationEnabled: true, - exceptionReplayEnabled: false, - codeOriginEnabled: true, - liveDebuggingEnabled: false - ) - - when: - def orgPriority = orgConfig.getOverridePriority() - def servicePriority = serviceConfig.getOverridePriority() - - then: - orgPriority == 1 // lowest priority for org level - servicePriority == 4 // higher priority for service level - servicePriority > orgPriority - - and: - orgConfig.isSingleService() == false - orgConfig.isSingleEnvironment() == false - orgConfig.isClusterTarget() == false - - serviceConfig.isSingleService() == true - serviceConfig.isSingleEnvironment() == false // env is "*" - serviceConfig.isClusterTarget() == false - } def "test actual config commit with service and org level configs"() { setup: @@ -215,7 +168,7 @@ class TracingConfigPollerTest extends DDCoreSpecification { "tag_name": "custom.header" } ], - "tracing_sampling_rate": 0.3 + "tracing_sampling_rate": 1.3 } } """.getBytes(StandardCharsets.UTF_8), null) @@ -226,7 +179,7 @@ class TracingConfigPollerTest extends DDCoreSpecification { then: // Service level config should take precedence due to higher priority (4 vs 1) tracer.captureTraceConfig().serviceMapping == ["service-specific":"service-mapped"] - tracer.captureTraceConfig().traceSampleRate == 0.3 + tracer.captureTraceConfig().traceSampleRate == 1.0 // should be clamped to 1.0 tracer.captureTraceConfig().requestHeaderTags == ["x-custom-header":"custom.header"] tracer.captureTraceConfig().responseHeaderTags == ["x-custom-header":"custom.header"] @@ -255,111 +208,4 @@ class TracingConfigPollerTest extends DDCoreSpecification { cleanup: tracer?.close() } - - def "test actual config commit with cluster and org level configs"() { - setup: - def orgKey = ParsedConfigKey.parse("datadog/2/APM_TRACING/org_config/config") - def clusterKey = ParsedConfigKey.parse("datadog/2/APM_TRACING/cluster_config/config") - def poller = Mock(ConfigurationPoller) - def sco = new SharedCommunicationObjects( - okHttpClient: Mock(OkHttpClient), - monitoring: Mock(Monitoring), - agentUrl: HttpUrl.get('https://example.com'), - featuresDiscovery: Mock(DDAgentFeaturesDiscovery), - configurationPoller: poller - ) - - def updater - - when: - def tracer = CoreTracer.builder() - .sharedCommunicationObjects(sco) - .pollForTracingConfiguration() - .build() - - then: - 1 * poller.addListener(Product.APM_TRACING, _ as ProductListener) >> { - updater = it[1] // capture config updater for further testing - } - and: - tracer.captureTraceConfig().serviceMapping == [:] - tracer.captureTraceConfig().traceSampleRate == null - tracer.captureTraceConfig().tracingTags == [:] - - when: - // Add org level config (priority 1) - should set service mapping and sampling rate - updater.accept(orgKey, """ - { - "lib_config": { - "tracing_service_mapping": [ - { - "from_key": "org-service", - "to_name": "org-mapped" - } - ], - "tracing_sampling_rate": 0.8, - "tracing_tags": ["org:tag", "level:org"] - } - } - """.getBytes(StandardCharsets.UTF_8), null) - - // Add cluster level config (priority 2) - should override org level configs - updater.accept(clusterKey, """ - { - "k8s_target_v2": { - "cluster_targets": [ - { - "cluster_name": "test-cluster", - "enabled": true, - "enabled_namespaces": ["default", "kube-system"] - } - ] - }, - "lib_config": { - "tracing_service_mapping": [ - { - "from_key": "cluster-service", - "to_name": "cluster-mapped" - } - ], - "tracing_sampling_rate": 0.4, - "tracing_tags": ["cluster:tag", "level:cluster"] - } - } - """.getBytes(StandardCharsets.UTF_8), null) - - // Commit both configs - updater.commit() - - then: - // Cluster level config should take precedence due to higher priority (2 vs 1) - tracer.captureTraceConfig().serviceMapping == ["cluster-service":"cluster-mapped"] - tracer.captureTraceConfig().traceSampleRate == 0.4 - tracer.captureTraceConfig().tracingTags == ["cluster":"tag", "level":"cluster"] - - when: - // Remove cluster level config - updater.remove(clusterKey, null) - updater.commit() - - then: - // Should fall back to org level config - tracer.captureTraceConfig().serviceMapping == ["org-service":"org-mapped"] - tracer.captureTraceConfig().traceSampleRate == 0.8 - tracer.captureTraceConfig().tracingTags == ["org":"tag", "level":"org"] - - when: - // Remove org level config - updater.remove(orgKey, null) - updater.commit() - - then: - // Should have no configs - tracer.captureTraceConfig().serviceMapping == [:] - tracer.captureTraceConfig().traceSampleRate == null - tracer.captureTraceConfig().tracingTags == [:] - - cleanup: - tracer?.close() - } } From acee689647cfe61fe2bd129f9b6545938caa556a Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Fri, 29 Aug 2025 14:10:42 +0200 Subject: [PATCH 6/7] fix spotless --- .../trace/core/TracingConfigPollerTest.groovy | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy index 6695e76ac2c..7b2acd7cc9e 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy @@ -67,19 +67,20 @@ class TracingConfigPollerTest extends DDCoreSpecification { setup: def configOverrides = new TracingConfigPoller.ConfigOverrides() if (service != null || env != null) { - configOverrides.serviceTarget = new TracingConfigPoller.ServiceTarget( - service: service, - env: env, - ) + configOverrides.serviceTarget = new TracingConfigPoller.ServiceTarget( + service: service, + env: env, + ) } if (clusterName != null) { - configOverrides.k8sTargetV2 = new TracingConfigPoller.K8sTargetV2( - clusterTargets: [new TracingConfigPoller.ClusterTarget( - clusterName: clusterName, - enabled: true, + configOverrides.k8sTargetV2 = new TracingConfigPoller.K8sTargetV2( + clusterTargets: [ + new TracingConfigPoller.ClusterTarget( + clusterName: clusterName, + enabled: true, + ) + ] ) - ] - ) } configOverrides.libConfig = new TracingConfigPoller.LibConfig() From 694f35354c7ff17d59fee27e0744fae120e7fc42 Mon Sep 17 00:00:00 2001 From: Idan Shatz Date: Tue, 2 Sep 2025 22:15:46 -0400 Subject: [PATCH 7/7] add another test --- .../trace/core/TracingConfigPollerTest.groovy | 88 ++++++++++++++++--- 1 file changed, 78 insertions(+), 10 deletions(-) diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy index 7b2acd7cc9e..63d70911f47 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/TracingConfigPollerTest.groovy @@ -91,12 +91,12 @@ class TracingConfigPollerTest extends DDCoreSpecification { priority == expectedPriority where: - service | env | clusterName | expectedPriority - "test-service" | "staging" | null | 5 - "test-service" | "*" | null | 4 - "*" | "staging" | null | 3 - null | null | "test-cluster" | 2 - "*" | "*" | null | 1 + service | env | clusterName | expectedPriority + "test-service" | "staging" | null | 5 + "test-service" | "*" | null | 4 + "*" | "staging" | null | 3 + null | null | "test-cluster" | 2 + "*" | "*" | null | 1 } @@ -179,10 +179,10 @@ class TracingConfigPollerTest extends DDCoreSpecification { then: // Service level config should take precedence due to higher priority (4 vs 1) - tracer.captureTraceConfig().serviceMapping == ["service-specific":"service-mapped"] + tracer.captureTraceConfig().serviceMapping == ["service-specific": "service-mapped"] tracer.captureTraceConfig().traceSampleRate == 1.0 // should be clamped to 1.0 - tracer.captureTraceConfig().requestHeaderTags == ["x-custom-header":"custom.header"] - tracer.captureTraceConfig().responseHeaderTags == ["x-custom-header":"custom.header"] + tracer.captureTraceConfig().requestHeaderTags == ["x-custom-header": "custom.header"] + tracer.captureTraceConfig().responseHeaderTags == ["x-custom-header": "custom.header"] when: // Remove service level config @@ -191,7 +191,7 @@ class TracingConfigPollerTest extends DDCoreSpecification { then: // Should fall back to org level config - tracer.captureTraceConfig().serviceMapping == ["org-service":"org-mapped"] + tracer.captureTraceConfig().serviceMapping == ["org-service": "org-mapped"] tracer.captureTraceConfig().traceSampleRate == 0.7 tracer.captureTraceConfig().requestHeaderTags == [:] tracer.captureTraceConfig().responseHeaderTags == [:] @@ -209,4 +209,72 @@ class TracingConfigPollerTest extends DDCoreSpecification { cleanup: tracer?.close() } + + def "test two org levels config setting different flags works"() { + setup: + def orgConfig1Key = ParsedConfigKey.parse("datadog/2/APM_TRACING/org_config/config1") + def orgConfig2Key = ParsedConfigKey.parse("datadog/2/APM_TRACING/org_config/config2") + def poller = Mock(ConfigurationPoller) + def sco = new SharedCommunicationObjects( + okHttpClient: Mock(OkHttpClient), + monitoring: Mock(Monitoring), + agentUrl: HttpUrl.get('https://example.com'), + featuresDiscovery: Mock(DDAgentFeaturesDiscovery), + configurationPoller: poller + ) + + def updater + + when: + def tracer = CoreTracer.builder() + .sharedCommunicationObjects(sco) + .pollForTracingConfiguration() + .build() + + then: + 1 * poller.addListener(Product.APM_TRACING, _ as ProductListener) >> { + updater = it[1] // capture config updater for further testing + } + and: + tracer.captureTraceConfig().isTraceEnabled() == true + tracer.captureTraceConfig().isDataStreamsEnabled() == false + + when: + // Add org level config with ApmTracing enabled + updater.accept(orgConfig1Key, """ + { + "service_target": { + "service": "*", + "env": "*" + }, + "lib_config": { + "tracing_enabled": true + } + } + """.getBytes(StandardCharsets.UTF_8), null) + + // Add second org level config with DataStreams enabled + updater.accept(orgConfig2Key, """ + { + "service_target": { + "service": "*", + "env": "*" + }, + "lib_config": { + "data_streams_enabled": true + } + } + """.getBytes(StandardCharsets.UTF_8), null) + + // Commit both configs + updater.commit() + + then: + // Both org level configs should be merged, with data streams enabled + tracer.captureTraceConfig().isTraceEnabled() == true + tracer.captureTraceConfig().isDataStreamsEnabled() == true + + cleanup: + tracer?.close() + } }