From 8a5137fbac82342c4c4e620386dd21d61ad26223 Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Wed, 21 Oct 2026 13:47:21 +0200 Subject: [PATCH 1/3] refactor(ffe): Resolve the Feature Flags settings like the SDK Move the Feature Flags settings to dd-trace-api, remove the feature-flagging-config module, and expose the configuration source as an enum parsed like the SDK does. Check the agent and the SDK resolve the settings alike. Co-Authored-By: Claude Opus 5.5 --- dd-java-agent/agent-bootstrap/build.gradle | 1 - .../java/datadog/trace/bootstrap/Agent.java | 2 +- .../ConfigurationResolutionParityTest.java | 90 +++++++++++++++++++ .../api/config/FeatureFlaggingConfig.java | 17 ++++ gradle/dependencies.gradle | 1 - internal-api/build.gradle.kts | 1 - .../main/java/datadog/trace/api/Config.java | 11 ++- .../datadog/trace/api/InstrumenterConfig.java | 6 +- .../java/datadog/trace/api/ConfigTest.java | 45 +++++----- ...InstrumenterConfigFeatureFlaggingTest.java | 6 +- .../feature-flagging-agent/build.gradle.kts | 2 - .../FeatureFlaggingSystemTest.java | 4 +- .../feature-flagging-config/build.gradle.kts | 10 --- .../feature-flagging-config/gradle.lockfile | 75 ---------------- .../config/FeatureFlaggingConfig.java | 21 ----- .../feature-flagging-lib/build.gradle.kts | 2 - .../featureflag/FeatureFlagsBackend.java | 9 +- .../featureflag/FeatureFlagsBackendTest.java | 18 ++-- settings.gradle.kts | 1 - 19 files changed, 158 insertions(+), 164 deletions(-) create mode 100644 dd-java-agent/instrumentation/datadog/openfeature/dd-openfeature-1.69/src/test/java/datadog/trace/instrumentation/openfeature/ConfigurationResolutionParityTest.java create mode 100644 dd-trace-api/src/main/java/datadog/trace/api/config/FeatureFlaggingConfig.java delete mode 100644 products/feature-flagging/feature-flagging-config/build.gradle.kts delete mode 100644 products/feature-flagging/feature-flagging-config/gradle.lockfile delete mode 100644 products/feature-flagging/feature-flagging-config/src/main/java/datadog/trace/api/featureflag/config/FeatureFlaggingConfig.java diff --git a/dd-java-agent/agent-bootstrap/build.gradle b/dd-java-agent/agent-bootstrap/build.gradle index 03ba77796f6..17fee084562 100644 --- a/dd-java-agent/agent-bootstrap/build.gradle +++ b/dd-java-agent/agent-bootstrap/build.gradle @@ -22,7 +22,6 @@ dependencies { api project(':dd-java-agent:agent-debugger:debugger-bootstrap') api project(':components:environment') api project(':components:json') - api project(':products:feature-flagging:feature-flagging-config') api project(':products:metrics:metrics-agent') api libs.instrument.java api libs.slf4j diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java index d9888f23df4..8ca910dde7c 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java @@ -35,6 +35,7 @@ import datadog.trace.api.config.CrashTrackingConfig; import datadog.trace.api.config.CwsConfig; import datadog.trace.api.config.DebuggerConfig; +import datadog.trace.api.config.FeatureFlaggingConfig; import datadog.trace.api.config.GeneralConfig; import datadog.trace.api.config.IastConfig; import datadog.trace.api.config.JmxFetchConfig; @@ -44,7 +45,6 @@ import datadog.trace.api.config.TraceInstrumentationConfig; import datadog.trace.api.config.TracerConfig; import datadog.trace.api.config.UsmConfig; -import datadog.trace.api.featureflag.config.FeatureFlaggingConfig; import datadog.trace.api.gateway.RequestContextSlot; import datadog.trace.api.gateway.SubscriptionService; import datadog.trace.api.git.EmbeddedGitInfoBuilder; diff --git a/dd-java-agent/instrumentation/datadog/openfeature/dd-openfeature-1.69/src/test/java/datadog/trace/instrumentation/openfeature/ConfigurationResolutionParityTest.java b/dd-java-agent/instrumentation/datadog/openfeature/dd-openfeature-1.69/src/test/java/datadog/trace/instrumentation/openfeature/ConfigurationResolutionParityTest.java new file mode 100644 index 00000000000..8f8e55f716d --- /dev/null +++ b/dd-java-agent/instrumentation/datadog/openfeature/dd-openfeature-1.69/src/test/java/datadog/trace/instrumentation/openfeature/ConfigurationResolutionParityTest.java @@ -0,0 +1,90 @@ +package datadog.trace.instrumentation.openfeature; + +import static datadog.trace.api.config.FeatureFlaggingConfig.EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED; +import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE; +import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_ENABLED; +import static java.util.Locale.ROOT; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.datadog.openfeature.internal.bridge.v1.AgentBridge; +import com.datadog.openfeature.internal.config.Settings; +import datadog.trace.api.InstrumenterConfig; +import datadog.trace.test.junit.utils.config.WithConfigExtension; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.extension.ExtendWith; +import org.tabletest.junit.TableTest; + +/** + * Guards against drift between the agent ({@link InstrumenterConfig}) and the SDK ({@link + * Settings}) resolution of the Feature Flags product switch and configuration source. Both sides + * read the same raw settings, keyed by the agent setting names. + */ +@ExtendWith(WithConfigExtension.class) +class ConfigurationResolutionParityTest { + @TableTest({ + "Scenario | Enabled | Source | Legacy | Expected ", + "defaults | | | | agentless ", + "enabled | true | | | agentless ", + "enabled as number | 1 | | | agentless ", + "disabled | false | | | ", + "disabled as number | 0 | | | ", + "disabled wins over source | false | remote_config | | ", + "remote config source | | remote_config | | remote_config", + "agentless source | | agentless | | agentless ", + "source is case insensitive | | Remote_Config | | remote_config", + "padded source is unsupported | | ' agentless ' | | ", + "unsupported source | | offline | | ", + "legacy enabled | | | true | remote_config", + "legacy disabled | | | false | ", + "source wins over legacy | | agentless | true | agentless ", + "source wins over legacy disabled | | remote_config | false | remote_config", + "enabled with legacy disabled | true | | false | ", + "uppercase boolean | TRUE | | TRUE | remote_config", + "invalid boolean reads as false | maybe | | | ", + "invalid legacy reads as false | | | maybe | ", + "padded enabled reads as false | ' true ' | | | ", + "padded number reads as false | ' 1 ' | | | ", + "padded legacy reads as false | | | ' true ' | " + }) + void agentAndSdkResolveTheSameConfiguration( + final String enabled, final String source, final String legacy, final String expected) { + final Map settings = new HashMap<>(); + putIfNotNull(settings, FEATURE_FLAGS_ENABLED, enabled); + putIfNotNull(settings, FEATURE_FLAGS_CONFIGURATION_SOURCE, source); + putIfNotNull(settings, EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED, legacy); + + assertEquals(expected, agentSource(settings), "agent resolution"); + assertEquals(expected, sdkSource(settings), "SDK resolution"); + } + + /** Resolves the source like the agent, through {@link InstrumenterConfig}. */ + private static String agentSource(final Map settings) { + settings.forEach(WithConfigExtension::injectSysConfig); + final InstrumenterConfig config = InstrumenterConfig.get(); + return config.isFeatureFlaggingEnabled() + ? config.getFeatureFlaggingConfigurationSource().name().toLowerCase(ROOT) + : null; + } + + /** + * Resolves the source like the SDK. Missing settings fall back to the system properties injected + * for the agent, which hold the same values. + */ + private static String sdkSource(final Map settings) { + final AgentBridge bridge = mock(AgentBridge.class); + when(bridge.setting(anyString())) + .thenAnswer(invocation -> settings.get(invocation.getArgument(0))); + return Settings.load(bridge).configurationSource(); + } + + private static void putIfNotNull( + final Map map, final String key, final String value) { + if (value != null) { + map.put(key, value); + } + } +} diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/FeatureFlaggingConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/FeatureFlaggingConfig.java new file mode 100644 index 00000000000..9a232111344 --- /dev/null +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/FeatureFlaggingConfig.java @@ -0,0 +1,17 @@ +package datadog.trace.api.config; + +/** Constant with names of configuration options for Feature Flags. */ +public final class FeatureFlaggingConfig { + + public static final String FEATURE_FLAGS_ENABLED = "feature.flags.enabled"; + + /** Use {@link #FEATURE_FLAGS_ENABLED} and {@link #FEATURE_FLAGS_CONFIGURATION_SOURCE}. */ + @Deprecated + public static final String EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED = + "experimental.flagging.provider.enabled"; + + public static final String FEATURE_FLAGS_CONFIGURATION_SOURCE = + "feature.flags.configuration.source"; + + private FeatureFlaggingConfig() {} +} diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index f781cbc134a..606459f9d04 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -24,7 +24,6 @@ final class CachedData { filter.exclude(filter.project(':components:environment')) filter.exclude(filter.project(':components:json')) filter.exclude(filter.project(':products:feature-flagging:feature-flagging-bootstrap')) - filter.exclude(filter.project(':products:feature-flagging:feature-flagging-config')) filter.exclude(filter.project(':products:metrics:metrics-agent')) filter.exclude(filter.project(':products:metrics:metrics-api')) filter.exclude(filter.project(':products:metrics:metrics-lib')) diff --git a/internal-api/build.gradle.kts b/internal-api/build.gradle.kts index 9c877c4562d..2ce54fd0c15 100644 --- a/internal-api/build.gradle.kts +++ b/internal-api/build.gradle.kts @@ -277,7 +277,6 @@ dependencies { api(project(":components:context")) api(project(":components:environment")) api(project(":components:json")) - implementation(project(":products:feature-flagging:feature-flagging-config")) api(project(":utils:config-utils")) api(project(":utils:time-utils")) diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index 38738cf1447..3497fe88477 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -373,6 +373,9 @@ import static datadog.trace.api.config.DebuggerConfig.THIRD_PARTY_EXCLUDES; import static datadog.trace.api.config.DebuggerConfig.THIRD_PARTY_INCLUDES; import static datadog.trace.api.config.DebuggerConfig.THIRD_PARTY_SHADING_IDENTIFIERS; +import static datadog.trace.api.config.FeatureFlaggingConfig.EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED; +import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE; +import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_ENABLED; import static datadog.trace.api.config.GeneralConfig.AGENTLESS_LOG_SUBMISSION_LEVEL; import static datadog.trace.api.config.GeneralConfig.AGENTLESS_LOG_SUBMISSION_QUEUE_SIZE; import static datadog.trace.api.config.GeneralConfig.AGENTLESS_LOG_SUBMISSION_URL; @@ -742,9 +745,6 @@ import static datadog.trace.api.config.TracerConfig.WRITER_BAGGAGE_INJECT; import static datadog.trace.api.config.TracerConfig.WRITER_LINKS_INJECT; import static datadog.trace.api.config.TracerConfig.WRITER_TYPE; -import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED; -import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE; -import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.FEATURE_FLAGS_ENABLED; import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY; import static datadog.trace.bootstrap.instrumentation.api.WriterConstants.MULTI_WRITER_TYPE; import static datadog.trace.bootstrap.instrumentation.api.WriterConstants.OTLP_WRITER_TYPE; @@ -4888,9 +4888,8 @@ public boolean isFeatureFlaggingProviderEnabled() { return instrumenterConfig.isFeatureFlaggingEnabled(); } - public String getFeatureFlaggingConfigurationSource() { - final FeatureFlaggingSource source = instrumenterConfig.getFeatureFlaggingConfigurationSource(); - return source == FeatureFlaggingSource.UNSET ? null : source.name().toLowerCase(Locale.ROOT); + public FeatureFlaggingSource getFeatureFlaggingConfigurationSource() { + return instrumenterConfig.getFeatureFlaggingConfigurationSource(); } public boolean isDynamicInstrumentationEnabled() { diff --git a/internal-api/src/main/java/datadog/trace/api/InstrumenterConfig.java b/internal-api/src/main/java/datadog/trace/api/InstrumenterConfig.java index 84304973dd8..21aa2a87173 100644 --- a/internal-api/src/main/java/datadog/trace/api/InstrumenterConfig.java +++ b/internal-api/src/main/java/datadog/trace/api/InstrumenterConfig.java @@ -38,6 +38,9 @@ import static datadog.trace.api.config.AppSecConfig.APPSEC_ENABLED; import static datadog.trace.api.config.AppSecConfig.APPSEC_RASP_ENABLED; import static datadog.trace.api.config.CiVisibilityConfig.CIVISIBILITY_ENABLED; +import static datadog.trace.api.config.FeatureFlaggingConfig.EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED; +import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE; +import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_ENABLED; import static datadog.trace.api.config.GeneralConfig.AGENTLESS_LOG_SUBMISSION_ENABLED; import static datadog.trace.api.config.GeneralConfig.APP_LOGS_COLLECTION_ENABLED; import static datadog.trace.api.config.GeneralConfig.DATA_JOBS_ENABLED; @@ -103,9 +106,6 @@ import static datadog.trace.api.config.TraceInstrumentationConfig.UNSAFE_CLASS_INJECTION; import static datadog.trace.api.config.TraceInstrumentationConfig.VISITOR_CLASS_PARSING; import static datadog.trace.api.config.UsmConfig.USM_ENABLED; -import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED; -import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE; -import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.FEATURE_FLAGS_ENABLED; import static datadog.trace.util.CollectionUtils.tryMakeImmutableList; import static datadog.trace.util.CollectionUtils.tryMakeImmutableSet; import static java.lang.Boolean.FALSE; diff --git a/internal-api/src/test/java/datadog/trace/api/ConfigTest.java b/internal-api/src/test/java/datadog/trace/api/ConfigTest.java index 88a71f14565..990b7f1fd4c 100644 --- a/internal-api/src/test/java/datadog/trace/api/ConfigTest.java +++ b/internal-api/src/test/java/datadog/trace/api/ConfigTest.java @@ -42,6 +42,9 @@ import static datadog.trace.api.config.DebuggerConfig.DYNAMIC_INSTRUMENTATION_UPLOAD_TIMEOUT; import static datadog.trace.api.config.DebuggerConfig.DYNAMIC_INSTRUMENTATION_VERIFY_BYTECODE; import static datadog.trace.api.config.DebuggerConfig.EXCEPTION_REPLAY_ENABLED; +import static datadog.trace.api.config.FeatureFlaggingConfig.EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED; +import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE; +import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_ENABLED; import static datadog.trace.api.config.GeneralConfig.API_KEY; import static datadog.trace.api.config.GeneralConfig.API_KEY_FILE; import static datadog.trace.api.config.GeneralConfig.CONFIGURATION_FILE; @@ -166,9 +169,6 @@ import static datadog.trace.api.config.TracerConfig.TRACE_X_DATADOG_TAGS_MAX_LENGTH; import static datadog.trace.api.config.TracerConfig.WRITER_TYPE; import static datadog.trace.api.env.FixedCapturedEnvironment.useFixedEnv; -import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED; -import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE; -import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.FEATURE_FLAGS_ENABLED; import static datadog.trace.bootstrap.config.provider.ConfigProvider.withPropertiesOverride; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; @@ -3679,15 +3679,16 @@ void traceResourceRenamingAlwaysSimplifiedEndpointWithValue(String value, boolea @TableTest({ "scenario | value | expected ", - "not set | | agentless ", - "empty | '' | agentless ", - "blank | ' ' | agentless ", - "case insensitive | ReMoTe_ConFiG | remote_config", - "padded | ' remote_config ' | invalid ", - "unknown source | not-a-real-source | invalid ", - "unset is not a value | unset | invalid " + "not set | | AGENTLESS ", + "empty | '' | AGENTLESS ", + "blank | ' ' | AGENTLESS ", + "case insensitive | ReMoTe_ConFiG | REMOTE_CONFIG", + "padded | ' remote_config ' | INVALID ", + "unknown source | not-a-real-source | INVALID ", + "unset is not a value | unset | INVALID " }) - void featureFlagConfigurationSourceParsesValue(String value, String expected) throws Exception { + void featureFlagConfigurationSourceParsesValue(String value, FeatureFlaggingSource expected) + throws Exception { Properties properties = new Properties(); if (value != null) { properties.setProperty(FEATURE_FLAGS_CONFIGURATION_SOURCE, value); @@ -3700,23 +3701,23 @@ void featureFlagConfigurationSourceParsesValue(String value, String expected) th @TableTest({ "scenario | providerEnabled | source | legacyProviderEnabled | expectedEnabled | expectedSource", - "nothing set | | | | true | agentless ", - "provider enabled | true | | | true | agentless ", - "legacy enabled | | | true | true | remote_config ", - "legacy disabled | | | false | false | ", - "source wins over legacy enabled | | agentless | true | true | agentless ", - "source wins over legacy disabled | | remote_config | false | true | remote_config ", - "provider disabled wins over legacy | false | agentless | true | false | agentless ", - "legacy disabled wins over provider | true | | false | false | ", - "unknown source | | not-a-source | | false | invalid ", - "offline source | | offline | true | false | invalid " + "nothing set | | | | true | AGENTLESS ", + "provider enabled | true | | | true | AGENTLESS ", + "legacy enabled | | | true | true | REMOTE_CONFIG ", + "legacy disabled | | | false | false | UNSET ", + "source wins over legacy enabled | | agentless | true | true | AGENTLESS ", + "source wins over legacy disabled | | remote_config | false | true | REMOTE_CONFIG ", + "provider disabled wins over legacy | false | agentless | true | false | AGENTLESS ", + "legacy disabled wins over provider | true | | false | false | UNSET ", + "unknown source | | not-a-source | | false | INVALID ", + "offline source | | offline | true | false | INVALID " }) void featureFlagConfigurationAppliesMigrationPrecedence( Boolean providerEnabled, String source, Boolean legacyProviderEnabled, boolean expectedEnabled, - String expectedSource) + FeatureFlaggingSource expectedSource) throws Exception { Properties properties = new Properties(); if (providerEnabled != null) { diff --git a/internal-api/src/test/java/datadog/trace/api/InstrumenterConfigFeatureFlaggingTest.java b/internal-api/src/test/java/datadog/trace/api/InstrumenterConfigFeatureFlaggingTest.java index 5a10341d0dc..5e5af37b723 100644 --- a/internal-api/src/test/java/datadog/trace/api/InstrumenterConfigFeatureFlaggingTest.java +++ b/internal-api/src/test/java/datadog/trace/api/InstrumenterConfigFeatureFlaggingTest.java @@ -1,8 +1,8 @@ package datadog.trace.api; -import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED; -import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE; -import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.FEATURE_FLAGS_ENABLED; +import static datadog.trace.api.config.FeatureFlaggingConfig.EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED; +import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE; +import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_ENABLED; import static org.junit.jupiter.api.Assertions.assertEquals; import datadog.trace.bootstrap.config.provider.ConfigProvider; diff --git a/products/feature-flagging/feature-flagging-agent/build.gradle.kts b/products/feature-flagging/feature-flagging-agent/build.gradle.kts index b15250c5c0b..8962b2f5a35 100644 --- a/products/feature-flagging/feature-flagging-agent/build.gradle.kts +++ b/products/feature-flagging/feature-flagging-agent/build.gradle.kts @@ -14,11 +14,9 @@ dependencies { api(libs.slf4j) api(project(":products:feature-flagging:feature-flagging-lib")) api(project(":internal-api")) - compileOnly(project(":products:feature-flagging:feature-flagging-config")) testImplementation(libs.bundles.junit5) testImplementation(libs.bundles.mockito) - testImplementation(project(":products:feature-flagging:feature-flagging-config")) testImplementation(project(":utils:test-utils")) testRuntimeOnly(project(":dd-trace-core")) } diff --git a/products/feature-flagging/feature-flagging-agent/src/test/java/com/datadog/featureflag/FeatureFlaggingSystemTest.java b/products/feature-flagging/feature-flagging-agent/src/test/java/com/datadog/featureflag/FeatureFlaggingSystemTest.java index 5e6cbfb5d48..0935d1914cb 100644 --- a/products/feature-flagging/feature-flagging-agent/src/test/java/com/datadog/featureflag/FeatureFlaggingSystemTest.java +++ b/products/feature-flagging/feature-flagging-agent/src/test/java/com/datadog/featureflag/FeatureFlaggingSystemTest.java @@ -1,8 +1,8 @@ package com.datadog.featureflag; +import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE; +import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_ENABLED; import static datadog.trace.api.config.RemoteConfigConfig.REMOTE_CONFIGURATION_ENABLED; -import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE; -import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.FEATURE_FLAGS_ENABLED; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; diff --git a/products/feature-flagging/feature-flagging-config/build.gradle.kts b/products/feature-flagging/feature-flagging-config/build.gradle.kts deleted file mode 100644 index c7a1f989611..00000000000 --- a/products/feature-flagging/feature-flagging-config/build.gradle.kts +++ /dev/null @@ -1,10 +0,0 @@ -plugins { - `java-library` - id("dd-trace-java.module.internal-api") -} - -description = "Feature flagging configuration keys and source resolution" - -dependencies { - testImplementation(libs.bundles.junit5) -} diff --git a/products/feature-flagging/feature-flagging-config/gradle.lockfile b/products/feature-flagging/feature-flagging-config/gradle.lockfile deleted file mode 100644 index 58285597cfb..00000000000 --- a/products/feature-flagging/feature-flagging-config/gradle.lockfile +++ /dev/null @@ -1,75 +0,0 @@ -# This is a Gradle generated file for dependency locking. -# Manual edits can break the build and are not advised. -# This file is expected to be part of source control. -# To regenerate this file, run: ./gradlew :products:feature-flagging:feature-flagging-config:dependencies --write-locks -ch.qos.logback:logback-classic:1.2.13=testCompileClasspath,testRuntimeClasspath -ch.qos.logback:logback-core:1.2.13=testCompileClasspath,testRuntimeClasspath -com.github.javaparser:javaparser-core:3.25.6=codenarc -com.github.spotbugs:spotbugs-annotations:4.10.4=compileClasspath,spotbugs -com.github.spotbugs:spotbugs:4.10.4=spotbugs -com.github.stephenc.jcip:jcip-annotations:1.0-1=spotbugs -com.google.code.findbugs:jsr305:3.0.2=compileClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath -com.google.code.gson:gson:2.14.0=spotbugs -com.google.errorprone:error_prone_annotations:2.48.0=spotbugs -com.thoughtworks.qdox:qdox:1.12.1=codenarc -commons-io:commons-io:2.21.0=spotbugs -de.thetaphi:forbiddenapis:3.11=compileClasspath -io.leangen.geantyref:geantyref:1.3.16=testRuntimeClasspath -jaxen:jaxen:2.0.6=spotbugs -net.bytebuddy:byte-buddy-agent:1.12.8=testRuntimeClasspath -net.bytebuddy:byte-buddy:1.12.8=testRuntimeClasspath -net.sf.saxon:Saxon-HE:12.10=spotbugs -org.apache.ant:ant-antlr:1.10.14=codenarc -org.apache.ant:ant-junit:1.10.14=codenarc -org.apache.bcel:bcel:6.12.0=spotbugs -org.apache.commons:commons-lang3:3.20.0=spotbugs -org.apache.commons:commons-text:1.15.0=spotbugs -org.apache.logging.log4j:log4j-api:2.26.1=spotbugs -org.apache.logging.log4j:log4j-core:2.26.1=spotbugs -org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testRuntimeClasspath -org.codehaus.groovy:groovy-ant:3.0.23=codenarc -org.codehaus.groovy:groovy-docgenerator:3.0.23=codenarc -org.codehaus.groovy:groovy-groovydoc:3.0.23=codenarc -org.codehaus.groovy:groovy-json:3.0.23=codenarc -org.codehaus.groovy:groovy-json:3.0.25=testCompileClasspath,testRuntimeClasspath -org.codehaus.groovy:groovy-templates:3.0.23=codenarc -org.codehaus.groovy:groovy-xml:3.0.23=codenarc -org.codehaus.groovy:groovy:3.0.23=codenarc -org.codehaus.groovy:groovy:3.0.25=testCompileClasspath,testRuntimeClasspath -org.codenarc:CodeNarc:3.7.0=codenarc -org.dom4j:dom4j:2.2.0=spotbugs -org.gmetrics:GMetrics:2.1.0=codenarc -org.hamcrest:hamcrest:3.0=testCompileClasspath,testRuntimeClasspath -org.jacoco:org.jacoco.agent:0.8.15=jacocoAgent,jacocoAnt -org.jacoco:org.jacoco.ant:0.8.15=jacocoAnt -org.jacoco:org.jacoco.core:0.8.15=jacocoAnt -org.jacoco:org.jacoco.report:0.8.15=jacocoAnt -org.junit.jupiter:junit-jupiter-api:5.14.1=testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-engine:5.14.1=testRuntimeClasspath -org.junit.jupiter:junit-jupiter-params:5.14.1=testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter:5.14.1=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.14.1=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-engine:1.14.1=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-launcher:1.14.1=testRuntimeClasspath -org.junit:junit-bom:5.14.1=testCompileClasspath,testRuntimeClasspath -org.mockito:mockito-core:4.4.0=testRuntimeClasspath -org.objenesis:objenesis:3.3=testCompileClasspath,testRuntimeClasspath -org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath -org.ow2.asm:asm-analysis:9.10.1=spotbugs -org.ow2.asm:asm-commons:9.10.1=jacocoAnt,spotbugs -org.ow2.asm:asm-tree:9.10.1=jacocoAnt,spotbugs -org.ow2.asm:asm-util:9.10.1=spotbugs -org.ow2.asm:asm:9.10.1=jacocoAnt,spotbugs -org.slf4j:jcl-over-slf4j:1.7.30=testCompileClasspath,testRuntimeClasspath -org.slf4j:jul-to-slf4j:1.7.30=testCompileClasspath,testRuntimeClasspath -org.slf4j:log4j-over-slf4j:1.7.30=testCompileClasspath,testRuntimeClasspath -org.slf4j:slf4j-api:1.7.32=testCompileClasspath,testRuntimeClasspath -org.slf4j:slf4j-api:2.0.17=spotbugsSlf4j -org.slf4j:slf4j-api:2.0.18=spotbugs -org.slf4j:slf4j-simple:2.0.17=spotbugsSlf4j -org.spockframework:spock-bom:2.4-groovy-3.0=testCompileClasspath,testRuntimeClasspath -org.spockframework:spock-core:2.4-groovy-3.0=testCompileClasspath,testRuntimeClasspath -org.tabletest:tabletest-junit:1.2.2=testCompileClasspath,testRuntimeClasspath -org.tabletest:tabletest-parser:1.2.1=testCompileClasspath,testRuntimeClasspath -org.xmlresolver:xmlresolver:5.3.3=spotbugs -empty=annotationProcessor,runtimeClasspath,spotbugsPlugins,testAnnotationProcessor diff --git a/products/feature-flagging/feature-flagging-config/src/main/java/datadog/trace/api/featureflag/config/FeatureFlaggingConfig.java b/products/feature-flagging/feature-flagging-config/src/main/java/datadog/trace/api/featureflag/config/FeatureFlaggingConfig.java deleted file mode 100644 index 785412e787d..00000000000 --- a/products/feature-flagging/feature-flagging-config/src/main/java/datadog/trace/api/featureflag/config/FeatureFlaggingConfig.java +++ /dev/null @@ -1,21 +0,0 @@ -package datadog.trace.api.featureflag.config; - -/** - * The Feature Flags settings the agent needs: the product switch and the configuration source. The - * {@code dd-openfeature} SDK resolves them with the same semantics, see {@code - * ConfigurationResolutionParityTest} in the {@code dd-openfeature-connector} instrumentation. - */ -public class FeatureFlaggingConfig { - - public static final String CONFIGURATION_SOURCE_AGENTLESS = "agentless"; - public static final String CONFIGURATION_SOURCE_REMOTE_CONFIG = "remote_config"; - - public static final String FEATURE_FLAGS_ENABLED = "feature.flags.enabled"; - public static final String EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED = - "experimental.flagging.provider.enabled"; - - public static final String FEATURE_FLAGS_CONFIGURATION_SOURCE = - "feature.flags.configuration.source"; - - private FeatureFlaggingConfig() {} -} diff --git a/products/feature-flagging/feature-flagging-lib/build.gradle.kts b/products/feature-flagging/feature-flagging-lib/build.gradle.kts index 5539b495e70..4a355fa19be 100644 --- a/products/feature-flagging/feature-flagging-lib/build.gradle.kts +++ b/products/feature-flagging/feature-flagging-lib/build.gradle.kts @@ -11,14 +11,12 @@ dependencies { api(project(":communication")) implementation(project(":internal-api")) api(project(":products:feature-flagging:feature-flagging-bootstrap")) - compileOnly(project(":products:feature-flagging:feature-flagging-config")) // Platform JSON writer for the ffe_* tag values. compileOnly(project(":components:json")) testImplementation(libs.bundles.junit5) testImplementation(libs.bundles.mockito) - testImplementation(project(":products:feature-flagging:feature-flagging-config")) testImplementation(project(":utils:test-utils")) testImplementation(project(":dd-java-agent:testing")) } diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagsBackend.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagsBackend.java index fdb03ee214e..c8fc97bf8b3 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagsBackend.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagsBackend.java @@ -1,7 +1,7 @@ package com.datadog.featureflag; -import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.CONFIGURATION_SOURCE_AGENTLESS; -import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.CONFIGURATION_SOURCE_REMOTE_CONFIG; +import static datadog.trace.api.FeatureFlaggingSource.AGENTLESS; +import static datadog.trace.api.FeatureFlaggingSource.REMOTE_CONFIG; import datadog.communication.BackendApiFactory; import datadog.communication.ddagent.SharedCommunicationObjects; @@ -34,7 +34,7 @@ public FeatureFlagsBackend(final SharedCommunicationObjects sco, final Config co config, new EventProxy( new BackendApiFactory(config, sco), - CONFIGURATION_SOURCE_AGENTLESS.equals(config.getFeatureFlaggingConfigurationSource()) + config.getFeatureFlaggingConfigurationSource() == AGENTLESS && config.getApiKey() != null), SpanEnrichmentWriter.getInstance(), CoreMetricCollector.getInstance()); @@ -55,8 +55,7 @@ public FeatureFlagsBackend(final SharedCommunicationObjects sco, final Config co /** Registers the Remote Configuration product early when it is the configuration source. */ public void start() { - if (CONFIGURATION_SOURCE_REMOTE_CONFIG.equals( - this.config.getFeatureFlaggingConfigurationSource()) + if (this.config.getFeatureFlaggingConfigurationSource() == REMOTE_CONFIG && isRemoteConfigAvailable()) { remoteConfig().start(); } diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagsBackendTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagsBackendTest.java index c7a101ef2ff..50331965f96 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagsBackendTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagsBackendTest.java @@ -17,6 +17,7 @@ import datadog.remoteconfig.ConfigurationPoller; import datadog.remoteconfig.Product; import datadog.trace.api.Config; +import datadog.trace.api.FeatureFlaggingSource; import datadog.trace.api.telemetry.CoreMetricCollector; import datadog.trace.bootstrap.config.provider.ConfigProvider; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; @@ -65,12 +66,12 @@ void resolvesSettingsWithAgentDefaultsFromTheAgentConfiguration() { @TableTest({ "Scenario | Source | RC enabled | Registers", - "remote config source | remote_config | true | true ", - "remote config disabled | remote_config | false | false ", - "agentless source | agentless | true | false " + "remote config source | REMOTE_CONFIG | true | true ", + "remote config disabled | REMOTE_CONFIG | false | false ", + "agentless source | AGENTLESS | true | false " }) void startRegistersRemoteConfigOnlyWhenItIsTheSource( - final String source, final boolean rcEnabled, final boolean registers) { + final FeatureFlaggingSource source, final boolean rcEnabled, final boolean registers) { final ConfigurationPoller poller = mock(ConfigurationPoller.class); when(this.sco.configurationPoller(this.config)).thenReturn(poller); when(this.config.getFeatureFlaggingConfigurationSource()).thenReturn(source); @@ -83,11 +84,12 @@ void startRegistersRemoteConfigOnlyWhenItIsTheSource( @TableTest({ "Scenario | Source | API key", - "agentless with API key | agentless | key ", - "agentless without API key | agentless | ", - "remote config | remote_config | key " + "agentless with API key | AGENTLESS | key ", + "agentless without API key | AGENTLESS | ", + "remote config | REMOTE_CONFIG | key " }) - void createsTheBackendFromTheAgentConfiguration(final String source, final String apiKey) { + void createsTheBackendFromTheAgentConfiguration( + final FeatureFlaggingSource source, final String apiKey) { when(this.config.getFeatureFlaggingConfigurationSource()).thenReturn(source); when(this.config.getApiKey()).thenReturn(apiKey); diff --git a/settings.gradle.kts b/settings.gradle.kts index b05284e5d42..ec7ae964640 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -165,7 +165,6 @@ include(":dd-java-agent:agent-aiguard") include( ":products:feature-flagging:feature-flagging-agent", ":products:feature-flagging:feature-flagging-bootstrap", - ":products:feature-flagging:feature-flagging-config", ":products:feature-flagging:feature-flagging-lib" ) From 21015ea0e7ab53c472871d10993493e73577a282 Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Thu, 22 Oct 2026 13:38:23 +0200 Subject: [PATCH 2/3] chore(ffe): Own the Feature Flags settings in dd-trace-api Co-Authored-By: Claude Opus 5.5 --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a7ce61e0f94..4861b5a7bbc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -403,6 +403,7 @@ /dd-openfeature/ @DataDog/feature-flagging-and-experimentation-sdk /dd-smoke-tests/openfeature/ @DataDog/feature-flagging-and-experimentation-sdk /dd-smoke-tests/openfeature-legacy/ @DataDog/feature-flagging-and-experimentation-sdk +/dd-trace-api/**/config/FeatureFlaggingConfig* @DataDog/feature-flagging-and-experimentation-sdk /products/feature-flagging/ @DataDog/feature-flagging-and-experimentation-sdk # @DataDog/profiling-java From c888b07f41997ea50dfeb57fc3589a318948cd50 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Thu, 22 Oct 2026 18:01:48 -0500 Subject: [PATCH 3/3] fix(ffe): Expose resolved Feature Flags settings through the agent bridge --- .../featureflag/FeatureFlagsBackend.java | 7 ++++ .../featureflag/FeatureFlagsBackendTest.java | 37 ++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagsBackend.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagsBackend.java index c8fc97bf8b3..0fdc3e3d712 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagsBackend.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagsBackend.java @@ -2,6 +2,9 @@ import static datadog.trace.api.FeatureFlaggingSource.AGENTLESS; import static datadog.trace.api.FeatureFlaggingSource.REMOTE_CONFIG; +import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE; +import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_ENABLED; +import static java.util.Locale.ROOT; import datadog.communication.BackendApiFactory; import datadog.communication.ddagent.SharedCommunicationObjects; @@ -66,6 +69,10 @@ && isRemoteConfigAvailable()) { public String setting(final String key) { // Settings with agent defaults or derived values resolve through the agent configuration. switch (key) { + case FEATURE_FLAGS_ENABLED: + return Boolean.toString(this.config.isFeatureFlaggingProviderEnabled()); + case FEATURE_FLAGS_CONFIGURATION_SOURCE: + return this.config.getFeatureFlaggingConfigurationSource().name().toLowerCase(ROOT); case API_KEY: return this.config.getApiKey(); case SITE: diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagsBackendTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagsBackendTest.java index 50331965f96..0ba9c1a50e4 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagsBackendTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagsBackendTest.java @@ -1,5 +1,7 @@ package com.datadog.featureflag; +import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE; +import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_ENABLED; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -21,10 +23,13 @@ import datadog.trace.api.telemetry.CoreMetricCollector; import datadog.trace.bootstrap.config.provider.ConfigProvider; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.test.junit.utils.config.WithConfigExtension; import java.util.Properties; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.tabletest.junit.TableTest; +@ExtendWith(WithConfigExtension.class) class FeatureFlagsBackendTest { private final SharedCommunicationObjects sco = mock(SharedCommunicationObjects.class); private final Config config = mock(Config.class); @@ -51,8 +56,11 @@ void resolvesSettingsWithAgentDefaultsFromTheAgentConfiguration() { when(this.config.getEnv()).thenReturn("prod"); when(this.config.getServiceName()).thenReturn("service"); when(this.config.getVersion()).thenReturn("1.0"); + when(this.config.isFeatureFlaggingProviderEnabled()).thenReturn(true); + when(this.config.getFeatureFlaggingConfigurationSource()) + .thenReturn(FeatureFlaggingSource.AGENTLESS); final Properties properties = new Properties(); - properties.setProperty("feature.flags.configuration.source", "agentless"); + properties.setProperty("flagging.evaluation.counts.enabled", "false"); when(this.config.configProvider()) .thenReturn(ConfigProvider.withPropertiesOverride(properties)); @@ -61,7 +69,32 @@ void resolvesSettingsWithAgentDefaultsFromTheAgentConfiguration() { assertEquals("prod", this.backend.setting("env")); assertEquals("service", this.backend.setting("service")); assertEquals("1.0", this.backend.setting("version")); - assertEquals("agentless", this.backend.setting("feature.flags.configuration.source")); + assertEquals("true", this.backend.setting(FEATURE_FLAGS_ENABLED)); + assertEquals("agentless", this.backend.setting(FEATURE_FLAGS_CONFIGURATION_SOURCE)); + assertEquals("false", this.backend.setting("flagging.evaluation.counts.enabled")); + } + + @TableTest({ + "Scenario | Property | Environment | Expected ", + "blank property masks env | ' ' | remote_config | agentless ", + "property overrides environment | agentless | remote_config | agentless ", + "environment selects source | | remote_config | remote_config" + }) + void exposesTheResolvedConfigurationSource( + final String property, final String environment, final String expected) { + if (property != null) { + WithConfigExtension.injectSysConfig(FEATURE_FLAGS_CONFIGURATION_SOURCE, property); + } + WithConfigExtension.injectEnvConfig("FEATURE_FLAGS_CONFIGURATION_SOURCE", environment); + final Config config = Config.get(); + when(this.config.getFeatureFlaggingConfigurationSource()) + .thenReturn(config.getFeatureFlaggingConfigurationSource()); + when(this.config.isFeatureFlaggingProviderEnabled()) + .thenReturn(config.isFeatureFlaggingProviderEnabled()); + when(this.config.configProvider()).thenReturn(config.configProvider()); + + assertEquals(expected, this.backend.setting(FEATURE_FLAGS_CONFIGURATION_SOURCE)); + assertEquals("true", this.backend.setting(FEATURE_FLAGS_ENABLED)); } @TableTest({