diff --git a/dd-java-agent/agent-bootstrap/build.gradle b/dd-java-agent/agent-bootstrap/build.gradle index 8bdf1a944f3..10be27679f6 100644 --- a/dd-java-agent/agent-bootstrap/build.gradle +++ b/dd-java-agent/agent-bootstrap/build.gradle @@ -23,6 +23,7 @@ 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-bootstrap') api project(':products:feature-flagging:feature-flagging-config') api project(':products:metrics:metrics-agent') api libs.instrument.java 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 a611a422d7b..bae635b59fc 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 @@ -44,6 +44,7 @@ import datadog.trace.api.config.TraceInstrumentationConfig; import datadog.trace.api.config.TracerConfig; import datadog.trace.api.config.UsmConfig; +import datadog.trace.api.featureflag.FeatureFlaggingGateway; import datadog.trace.api.featureflag.config.FeatureFlaggingConfig; import datadog.trace.api.gateway.RequestContextSlot; import datadog.trace.api.gateway.SubscriptionService; @@ -285,6 +286,7 @@ public static void start( appLogsCollectionEnabled = isFeatureEnabled(AgentFeature.APP_LOGS_COLLECTION); llmObsEnabled = isFeatureEnabled(AgentFeature.LLMOBS); featureFlaggingEnabled = isFeatureFlaggingEnabled(); + FeatureFlaggingGateway.setProviderInjectionEnabled(featureFlaggingEnabled); // setup writers when llmobs is enabled to accomodate apm and llmobs if (llmObsEnabled) { @@ -531,6 +533,7 @@ public static void shutdown(final boolean sync) { stopFlarePoller(); } if (featureFlaggingEnabled) { + FeatureFlaggingGateway.setProviderInjectionEnabled(false); shutdownFeatureFlagging(AGENT_CLASSLOADER); } diff --git a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/AgentInstaller.java b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/AgentInstaller.java index 3a8c7065362..ef4a00c2618 100644 --- a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/AgentInstaller.java +++ b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/AgentInstaller.java @@ -16,6 +16,7 @@ import datadog.trace.api.InstrumenterConfig; import datadog.trace.api.Platform; import datadog.trace.api.ProductActivation; +import datadog.trace.api.featureflag.FeatureFlaggingGateway; import datadog.trace.api.telemetry.IntegrationsCollector; import datadog.trace.bootstrap.FieldBackedContextAccessor; import datadog.trace.bootstrap.instrumentation.java.concurrent.ExcludeFilter; @@ -328,6 +329,9 @@ public static Set getEnabledSystems() { if (cfg.isCiVisibilityEnabled()) { enabledSystems.add(InstrumenterModule.TargetSystem.CIVISIBILITY); } + if (FeatureFlaggingGateway.isProviderInjectionEnabled()) { + enabledSystems.add(InstrumenterModule.TargetSystem.FEATURE_FLAGS); + } if (cfg.isUsmEnabled()) { enabledSystems.add(InstrumenterModule.TargetSystem.USM); } diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/InstrumenterModule.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/InstrumenterModule.java index d2abbc265e5..1e10304b73a 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/InstrumenterModule.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/InstrumenterModule.java @@ -40,6 +40,7 @@ public abstract class InstrumenterModule implements Instrumenter { *
  • {@link TargetSystem#APPSEC appsec} *
  • {@link TargetSystem#IAST iast} *
  • {@link TargetSystem#CIVISIBILITY ci-visibility} + *
  • {@link TargetSystem#FEATURE_FLAGS feature-flags} *
  • {@link TargetSystem#USM usm} *
  • {@link TargetSystem#CONTEXT_TRACKING context-tracking} *
  • {@link TargetSystem#RASP rasp} @@ -51,6 +52,7 @@ public enum TargetSystem { APPSEC, IAST, CIVISIBILITY, + FEATURE_FLAGS, USM, LLMOBS, CONTEXT_TRACKING, @@ -249,6 +251,18 @@ public final boolean isApplicable(Set enabledSystems) { } } + /** Parent class for all Feature Flags related instrumentations. */ + public abstract static class FeatureFlags extends InstrumenterModule { + public FeatureFlags(String instrumentationName, String... additionalNames) { + super(instrumentationName, additionalNames); + } + + @Override + public final boolean isApplicable(Set enabledSystems) { + return enabledSystems.contains(TargetSystem.FEATURE_FLAGS); + } + } + /** Parent class for all profiling related instrumentations */ public abstract static class Profiling extends InstrumenterModule { public Profiling(String instrumentationName, String... additionalNames) { diff --git a/dd-java-agent/instrumentation/openfeature/openfeature-1.20/build.gradle b/dd-java-agent/instrumentation/openfeature/openfeature-1.20/build.gradle new file mode 100644 index 00000000000..afbc940ad2c --- /dev/null +++ b/dd-java-agent/instrumentation/openfeature/openfeature-1.20/build.gradle @@ -0,0 +1,20 @@ +muzzle { + pass { + group = 'dev.openfeature' + module = 'sdk' + versions = '[1.20.1,1.21)' + } +} + +apply from: "$rootDir/gradle/java.gradle" + +dependencies { + compileOnly group: 'dev.openfeature', name: 'sdk', version: '1.20.1' + + implementation(project(':products:feature-flagging:feature-flagging-api')) { + transitive = false + } + + testImplementation project(':products:feature-flagging:feature-flagging-bootstrap') + testImplementation group: 'dev.openfeature', name: 'sdk', version: '1.20.1' +} diff --git a/dd-java-agent/instrumentation/openfeature/openfeature-1.20/src/main/java/datadog/trace/instrumentation/openfeature/OpenFeatureAPIInstrumentation.java b/dd-java-agent/instrumentation/openfeature/openfeature-1.20/src/main/java/datadog/trace/instrumentation/openfeature/OpenFeatureAPIInstrumentation.java new file mode 100644 index 00000000000..62b17f5f6d9 --- /dev/null +++ b/dd-java-agent/instrumentation/openfeature/openfeature-1.20/src/main/java/datadog/trace/instrumentation/openfeature/OpenFeatureAPIInstrumentation.java @@ -0,0 +1,64 @@ +package datadog.trace.instrumentation.openfeature; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import dev.openfeature.sdk.OpenFeatureAPI; +import net.bytebuddy.asm.Advice; + +@AutoService(InstrumenterModule.class) +public class OpenFeatureAPIInstrumentation extends InstrumenterModule.FeatureFlags + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + + public OpenFeatureAPIInstrumentation() { + super("openfeature"); + } + + @Override + public String instrumentedType() { + return "dev.openfeature.sdk.OpenFeatureAPI"; + } + + @Override + public String[] helperClassNames() { + return new String[] { + "datadog.trace.api.openfeature.Evaluator", + "datadog.trace.api.openfeature.DDEvaluator$1", + "datadog.trace.api.openfeature.DDEvaluator$FlattenEntry", + "datadog.trace.api.openfeature.DDEvaluator$NumberComparator", + "datadog.trace.api.openfeature.DDEvaluator", + "datadog.trace.api.openfeature.FlagEvalMetrics", + "datadog.trace.api.openfeature.FlagEvalHook", + "datadog.trace.api.openfeature.SpanEnrichmentGate", + "datadog.trace.api.openfeature.SpanEnrichmentHook", + "datadog.trace.api.openfeature.Provider$InitializationState", + "datadog.trace.api.openfeature.Provider$Options", + "datadog.trace.api.openfeature.Provider", + packageName + ".OpenFeatureProviderInstaller", + }; + } + + @Override + public void methodAdvice(final MethodTransformer transformer) { + transformer.applyAdvice( + isMethod() + .and(isStatic()) + .and(named("getInstance")) + .and(takesNoArguments()) + .and(returns(named("dev.openfeature.sdk.OpenFeatureAPI"))), + OpenFeatureAPIInstrumentation.class.getName() + "$GetInstanceAdvice"); + } + + public static class GetInstanceAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void installProvider(@Advice.Return final OpenFeatureAPI api) { + OpenFeatureProviderInstaller.install(api); + } + } +} diff --git a/dd-java-agent/instrumentation/openfeature/openfeature-1.20/src/main/java/datadog/trace/instrumentation/openfeature/OpenFeatureProviderInstaller.java b/dd-java-agent/instrumentation/openfeature/openfeature-1.20/src/main/java/datadog/trace/instrumentation/openfeature/OpenFeatureProviderInstaller.java new file mode 100644 index 00000000000..e0da93b6d4e --- /dev/null +++ b/dd-java-agent/instrumentation/openfeature/openfeature-1.20/src/main/java/datadog/trace/instrumentation/openfeature/OpenFeatureProviderInstaller.java @@ -0,0 +1,31 @@ +package datadog.trace.instrumentation.openfeature; + +import datadog.trace.api.featureflag.FeatureFlaggingGateway; +import datadog.trace.api.openfeature.Provider; +import dev.openfeature.sdk.FeatureProvider; +import dev.openfeature.sdk.NoOpProvider; +import dev.openfeature.sdk.OpenFeatureAPI; + +public final class OpenFeatureProviderInstaller { + + private static boolean installationComplete; + + private OpenFeatureProviderInstaller() {} + + public static synchronized void install(final OpenFeatureAPI api) { + if (installationComplete + || api == null + || !FeatureFlaggingGateway.isProviderInjectionEnabled()) { + return; + } + + final FeatureProvider currentProvider = api.getProvider(); + if (currentProvider == null || currentProvider.getClass() != NoOpProvider.class) { + installationComplete = true; + return; + } + + api.setProvider(new Provider()); + installationComplete = true; + } +} diff --git a/dd-java-agent/instrumentation/openfeature/openfeature-1.20/src/test/groovy/OpenFeatureProviderInjectionTest.groovy b/dd-java-agent/instrumentation/openfeature/openfeature-1.20/src/test/groovy/OpenFeatureProviderInjectionTest.groovy new file mode 100644 index 00000000000..31ef1a4bc6a --- /dev/null +++ b/dd-java-agent/instrumentation/openfeature/openfeature-1.20/src/test/groovy/OpenFeatureProviderInjectionTest.groovy @@ -0,0 +1,60 @@ +import datadog.trace.agent.test.InstrumentationSpecification +import datadog.trace.api.featureflag.FeatureFlaggingGateway +import datadog.trace.api.featureflag.ufc.v1.ServerConfiguration +import dev.openfeature.sdk.Metadata +import dev.openfeature.sdk.NoOpProvider +import dev.openfeature.sdk.OpenFeatureAPI + +import static java.util.Collections.emptyMap + +class OpenFeatureProviderInjectionTest extends InstrumentationSpecification { + + @Override + protected void configurePreAgent() { + super.configurePreAgent() + injectSysConfig("trace.enabled", "false") + injectSysConfig("trace.openfeature.enabled", "true") + FeatureFlaggingGateway.setProviderInjectionEnabled(true) + } + + def cleanup() { + FeatureFlaggingGateway.setProviderInjectionEnabled(false) + FeatureFlaggingGateway.dispatch((ServerConfiguration) null) + OpenFeatureAPI.getInstance().shutdown() + } + + def "injects once only after explicit activation"() { + given: "the instrumentation loaded for Feature Flags but provider installation is disabled" + FeatureFlaggingGateway.setProviderInjectionEnabled(false) + + when: "OpenFeature loads without explicit Feature Flags activation" + def api = OpenFeatureAPI.getInstance() + + then: + TRANSFORMED_CLASSES_NAMES.contains("dev.openfeature.sdk.OpenFeatureAPI") + api.provider.class == NoOpProvider + + when: "the Java agent enables provider injection" + FeatureFlaggingGateway.dispatch(new ServerConfiguration(null, null, null, emptyMap())) + FeatureFlaggingGateway.setProviderInjectionEnabled(true) + api = OpenFeatureAPI.getInstance() + + then: + api.provider.metadata.name == "datadog-openfeature-provider" + + when: "application code selects another provider" + def customerProvider = new CustomerProvider() + api.setProvider(customerProvider) + OpenFeatureAPI.getInstance() + + then: + api.provider.is(customerProvider) + } + + private static final class CustomerProvider extends NoOpProvider { + @Override + Metadata getMetadata() { + return { "customer-provider" } + } + } +} diff --git a/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy index ef95d5e902c..ac66d7dbf4a 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy @@ -3520,9 +3520,9 @@ class ConfigTest extends DDSpecification { where: value | expected - null | "agentless" - "" | "agentless" - " " | "agentless" + null | null + "" | null + " " | null " ReMoTe_ConFiG " | "remote_config" "not-a-real-source" | "not-a-real-source" " OFFLINE " | "offline" @@ -3550,8 +3550,8 @@ class ConfigTest extends DDSpecification { where: providerEnabled | source | legacyProviderEnabled | expectedEnabled | expectedSource - null | null | null | true | "agentless" - true | null | null | true | "agentless" + null | null | null | false | null + true | null | null | false | null null | null | true | true | "remote_config" null | null | false | false | null null | "agentless" | true | true | "agentless" diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index aaad660aae9..8973dddca77 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -8553,6 +8553,14 @@ "aliases": ["DD_TRACE_INTEGRATION_OPENAI_JAVA_ENABLED", "DD_INTEGRATION_OPENAI_JAVA_ENABLED"] } ], + "DD_TRACE_OPENFEATURE_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "true", + "aliases": ["DD_TRACE_INTEGRATION_OPENFEATURE_ENABLED", "DD_INTEGRATION_OPENFEATURE_ENABLED"] + } + ], "DD_TRACE_OPENSEARCH_ANALYTICS_ENABLED": [ { "version": "A", diff --git a/products/feature-flagging/feature-flagging-agent/src/main/java/com/datadog/featureflag/FeatureFlaggingSystem.java b/products/feature-flagging/feature-flagging-agent/src/main/java/com/datadog/featureflag/FeatureFlaggingSystem.java index 91b32ee1d64..acf70dc2ce0 100644 --- a/products/feature-flagging/feature-flagging-agent/src/main/java/com/datadog/featureflag/FeatureFlaggingSystem.java +++ b/products/feature-flagging/feature-flagging-agent/src/main/java/com/datadog/featureflag/FeatureFlaggingSystem.java @@ -5,7 +5,6 @@ import datadog.communication.ddagent.SharedCommunicationObjects; import datadog.trace.api.Config; -import datadog.trace.api.featureflag.FeatureFlaggingGateway; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,7 +16,6 @@ public class FeatureFlaggingSystem { private static volatile ConfigurationSourceService CONFIG_SERVICE; private static volatile ExposureWriter EXPOSURE_WRITER; private static volatile SpanEnrichmentWriter SPAN_ENRICHMENT_WRITER; - private static volatile FeatureFlaggingGateway.ActivationListener ACTIVATION_LISTENER; private static volatile boolean STARTED; private FeatureFlaggingSystem() {} @@ -40,31 +38,6 @@ public static synchronized void start(final SharedCommunicationObjects sco) { return; } - if (CONFIGURATION_SOURCE_AGENTLESS.equals(config.getFeatureFlaggingConfigurationSource())) { - final FeatureFlaggingGateway.ActivationListener activationListener = - () -> activateAgentless(sco, config); - ACTIVATION_LISTENER = activationListener; - FeatureFlaggingGateway.addActivationListener(activationListener); - LOGGER.debug("Feature Flagging system awaiting application provider activation"); - return; - } - - try { - initializeSystem(sco, config); - } catch (final RuntimeException | Error e) { - STARTED = false; - throw e; - } - } - - private static synchronized void activateAgentless( - final SharedCommunicationObjects sco, final Config config) { - final FeatureFlaggingGateway.ActivationListener activationListener = ACTIVATION_LISTENER; - if (!STARTED || activationListener == null) { - return; - } - ACTIVATION_LISTENER = null; - FeatureFlaggingGateway.removeActivationListener(activationListener); try { initializeSystem(sco, config); } catch (final RuntimeException | Error e) { @@ -134,18 +107,13 @@ static ConfigurationSourceService createConfigurationSourceService( justification = "Agent-internal class; Class object does not escape to app code and lock only guards the subsystem lifecycle.") public static synchronized void stop() { - final FeatureFlaggingGateway.ActivationListener activationListener = ACTIVATION_LISTENER; final SpanEnrichmentWriter spanEnrichmentWriter = SPAN_ENRICHMENT_WRITER; final ExposureWriter exposureWriter = EXPOSURE_WRITER; final ConfigurationSourceService configService = CONFIG_SERVICE; STARTED = false; - ACTIVATION_LISTENER = null; SPAN_ENRICHMENT_WRITER = null; EXPOSURE_WRITER = null; CONFIG_SERVICE = null; - if (activationListener != null) { - FeatureFlaggingGateway.removeActivationListener(activationListener); - } try { if (spanEnrichmentWriter != null) { spanEnrichmentWriter.close(); @@ -165,6 +133,6 @@ public static synchronized void stop() { } static boolean isAwaitingApplicationActivation() { - return ACTIVATION_LISTENER != null; + return false; } } 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 d408d91da4e..d7e136158f1 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 @@ -8,10 +8,8 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -25,7 +23,6 @@ import datadog.remoteconfig.ConfigurationPoller; import datadog.remoteconfig.Product; import datadog.trace.api.Config; -import datadog.trace.api.featureflag.FeatureFlaggingGateway; import datadog.trace.test.junit.utils.config.WithConfig; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; @@ -38,18 +35,12 @@ class FeatureFlaggingSystemTest { @WithConfig( key = FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL, value = "http://127.0.0.1:1") - void agentlessStartWaitsForApplicationProviderActivation() { + void agentlessStartDoesNotWaitForApplicationProviderActivation() { SharedCommunicationObjects sharedCommunicationObjects = sharedCommunicationObjects(); - clearInvocations(sharedCommunicationObjects); try { FeatureFlaggingSystem.start(sharedCommunicationObjects); - assertTrue(FeatureFlaggingSystem.isAwaitingApplicationActivation()); - verifyNoInteractions(sharedCommunicationObjects); - - FeatureFlaggingGateway.activate(); - assertFalse(FeatureFlaggingSystem.isAwaitingApplicationActivation()); } finally { FeatureFlaggingSystem.stop(); @@ -58,29 +49,6 @@ void agentlessStartWaitsForApplicationProviderActivation() { assertFalse(FeatureFlaggingSystem.isAwaitingApplicationActivation()); } - @Test - @WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "agentless") - @WithConfig( - key = FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL, - value = "http://127.0.0.1:1") - void agentlessStopRemovesPendingApplicationProviderActivation() { - SharedCommunicationObjects sharedCommunicationObjects = sharedCommunicationObjects(); - clearInvocations(sharedCommunicationObjects); - - try { - FeatureFlaggingSystem.start(sharedCommunicationObjects); - assertTrue(FeatureFlaggingSystem.isAwaitingApplicationActivation()); - - FeatureFlaggingSystem.stop(); - FeatureFlaggingGateway.activate(); - - assertFalse(FeatureFlaggingSystem.isAwaitingApplicationActivation()); - verifyNoInteractions(sharedCommunicationObjects); - } finally { - FeatureFlaggingSystem.stop(); - } - } - @Test @WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "remote_config") @WithConfig(key = REMOTE_CONFIGURATION_ENABLED, value = "true") diff --git a/products/feature-flagging/feature-flagging-bootstrap/src/main/java/datadog/trace/api/featureflag/FeatureFlaggingGateway.java b/products/feature-flagging/feature-flagging-bootstrap/src/main/java/datadog/trace/api/featureflag/FeatureFlaggingGateway.java index c8f5625c855..77e398b76e2 100644 --- a/products/feature-flagging/feature-flagging-bootstrap/src/main/java/datadog/trace/api/featureflag/FeatureFlaggingGateway.java +++ b/products/feature-flagging/feature-flagging-bootstrap/src/main/java/datadog/trace/api/featureflag/FeatureFlaggingGateway.java @@ -27,6 +27,7 @@ public interface SpanEnrichmentListener extends Consumer {} private static final AtomicReference CURRENT_CONFIG = new AtomicReference<>(); + private static volatile boolean providerInjectionEnabled; private FeatureFlaggingGateway() {} @@ -60,6 +61,16 @@ public static void activate() { ACTIVATION_LISTENERS.forEach(ActivationListener::activate); } + /** Enables agent instrumentation to install the Datadog provider into OpenFeature. */ + public static void setProviderInjectionEnabled(final boolean enabled) { + providerInjectionEnabled = enabled; + } + + /** Returns whether agent instrumentation can install the Datadog OpenFeature provider. */ + public static boolean isProviderInjectionEnabled() { + return providerInjectionEnabled; + } + public static void addExposureListener(final ExposureListener listener) { EXPOSURE_LISTENERS.add(listener); } diff --git a/products/feature-flagging/feature-flagging-bootstrap/src/test/java/datadog/trace/api/featureflag/FeatureFlaggingGatewayTest.java b/products/feature-flagging/feature-flagging-bootstrap/src/test/java/datadog/trace/api/featureflag/FeatureFlaggingGatewayTest.java index daaaf8d7001..0bdf11150a5 100644 --- a/products/feature-flagging/feature-flagging-bootstrap/src/test/java/datadog/trace/api/featureflag/FeatureFlaggingGatewayTest.java +++ b/products/feature-flagging/feature-flagging-bootstrap/src/test/java/datadog/trace/api/featureflag/FeatureFlaggingGatewayTest.java @@ -1,5 +1,7 @@ package datadog.trace.api.featureflag; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -35,12 +37,22 @@ void setUp() { @AfterEach void tearDown() { + FeatureFlaggingGateway.setProviderInjectionEnabled(false); FeatureFlaggingGateway.removeConfigListener(configListener); FeatureFlaggingGateway.removeActivationListener(activationListener); FeatureFlaggingGateway.removeExposureListener(exposureListener); FeatureFlaggingGateway.removeSpanEnrichmentListener(spanEnrichmentListener); } + @Test + void testProviderInjectionGate() { + assertFalse(FeatureFlaggingGateway.isProviderInjectionEnabled()); + + FeatureFlaggingGateway.setProviderInjectionEnabled(true); + + assertTrue(FeatureFlaggingGateway.isProviderInjectionEnabled()); + } + @Test void testProviderActivationListener() { FeatureFlaggingGateway.addActivationListener(activationListener); 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 index bae6fda1890..4a8dc1e86fe 100644 --- 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 @@ -52,7 +52,7 @@ public static Resolution resolveConfiguration( if (legacyProviderEnabled != null) { return legacyProviderEnabled ? REMOTE_CONFIG_CONFIGURATION : DISABLED_RESOLUTION; } - return AGENTLESS_CONFIGURATION; + return DISABLED_RESOLUTION; } public static boolean isSupportedConfigurationSource(final String source) { diff --git a/products/feature-flagging/feature-flagging-config/src/test/java/datadog/trace/api/featureflag/config/FeatureFlaggingConfigTest.java b/products/feature-flagging/feature-flagging-config/src/test/java/datadog/trace/api/featureflag/config/FeatureFlaggingConfigTest.java index 16283869d71..d7f551214bd 100644 --- a/products/feature-flagging/feature-flagging-config/src/test/java/datadog/trace/api/featureflag/config/FeatureFlaggingConfigTest.java +++ b/products/feature-flagging/feature-flagging-config/src/test/java/datadog/trace/api/featureflag/config/FeatureFlaggingConfigTest.java @@ -15,8 +15,9 @@ class FeatureFlaggingConfigTest { @Test void appliesConfigurationPrecedence() { - assertResolution(true, CONFIGURATION_SOURCE_AGENTLESS, null, null, null); - assertResolution(true, CONFIGURATION_SOURCE_AGENTLESS, null, " ", null); + assertResolution(false, null, null, null, null); + assertResolution(false, null, null, " ", null); + assertResolution(false, null, true, null, null); assertResolution(true, CONFIGURATION_SOURCE_REMOTE_CONFIG, null, null, true); assertResolution(false, null, null, null, false); assertResolution(true, CONFIGURATION_SOURCE_AGENTLESS, null, "agentless", true); diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessConfigurationSource.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessConfigurationSource.java index f4fc35cce30..77c6e46073c 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessConfigurationSource.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessConfigurationSource.java @@ -123,9 +123,9 @@ public void init() { started = true; } - // Complete the first poll cycle on the activation thread. This lets OpenFeature provider - // initialization observe a successful retry before it checks whether configuration is ready. - // No request occurs before application code activates the provider. + // Complete the first poll cycle during subsystem startup. This lets an injected OpenFeature + // provider observe configuration as soon as it initializes. Explicit configuration-source + // selection is the request and billing gate. pollOnceSafely(); synchronized (lifecycleLock) { diff --git a/settings.gradle.kts b/settings.gradle.kts index 14e988af3fe..b913ee0ff75 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -507,6 +507,7 @@ include( ":dd-java-agent:instrumentation:okhttp:okhttp-2.2", ":dd-java-agent:instrumentation:okhttp:okhttp-3.0", ":dd-java-agent:instrumentation:openai-java:openai-java-3.0", + ":dd-java-agent:instrumentation:openfeature:openfeature-1.20", ":dd-java-agent:instrumentation:opensearch:opensearch-rest-1.0", ":dd-java-agent:instrumentation:opensearch:opensearch-transport-1.0", ":dd-java-agent:instrumentation:opensearch:opensearch-common",