From 370109dd45c3dc9c7983dbdcea55941394290e67 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Wed, 8 Jul 2026 13:31:09 -0400 Subject: [PATCH 1/5] Add agentless feature flag configuration source --- .../java/datadog/trace/bootstrap/Agent.java | 7 +- .../datadog/trace/api/ConfigDefaults.java | 4 + .../api/config/FeatureFlaggingConfig.java | 16 + .../main/java/datadog/trace/api/Config.java | 69 ++++ metadata/supported-configurations.json | 48 +++ .../featureflag/FeatureFlaggingSystem.java | 47 ++- .../FeatureFlaggingSystemTest.java | 79 +++++ .../datadog/featureflag/CdnConfigService.java | 284 ++++++++++++++++ .../featureflag/CdnConfigServiceTest.java | 303 ++++++++++++++++++ 9 files changed, 851 insertions(+), 6 deletions(-) create mode 100644 products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/CdnConfigService.java create mode 100644 products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/CdnConfigServiceTest.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 876a07f1fd1..ea9793ce86d 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 @@ -135,7 +135,8 @@ private enum AgentFeature { APP_LOGS_COLLECTION(GeneralConfig.APP_LOGS_COLLECTION_ENABLED, false), LLMOBS(LlmObsConfig.LLMOBS_ENABLED, false), LLMOBS_AGENTLESS(LlmObsConfig.LLMOBS_AGENTLESS_ENABLED, false), - FEATURE_FLAGGING(FeatureFlaggingConfig.FLAGGING_PROVIDER_ENABLED, false); + FEATURE_FLAGGING(FeatureFlaggingConfig.FLAGGING_ENABLED, false), + DEPRECATED_FEATURE_FLAGGING(FeatureFlaggingConfig.FLAGGING_PROVIDER_ENABLED, false); private final String configKey; private final String systemProp; @@ -282,7 +283,9 @@ public static void start( agentlessLogSubmissionEnabled = isFeatureEnabled(AgentFeature.AGENTLESS_LOG_SUBMISSION); appLogsCollectionEnabled = isFeatureEnabled(AgentFeature.APP_LOGS_COLLECTION); llmObsEnabled = isFeatureEnabled(AgentFeature.LLMOBS); - featureFlaggingEnabled = isFeatureEnabled(AgentFeature.FEATURE_FLAGGING); + featureFlaggingEnabled = + isFeatureEnabled(AgentFeature.FEATURE_FLAGGING) + || isFeatureEnabled(AgentFeature.DEPRECATED_FEATURE_FLAGGING); // setup writers when llmobs is enabled to accomodate apm and llmobs if (llmObsEnabled) { diff --git a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java index 1aba2cb276b..42293cdea28 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java @@ -49,6 +49,10 @@ public final class ConfigDefaults { static final boolean DEFAULT_INJECT_DATADOG_ATTRIBUTE = true; static final String DEFAULT_SITE = "datadoghq.com"; + public static final String DEFAULT_FLAGGING_CONFIGURATION_SOURCE = "cdn"; + public static final double DEFAULT_FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS = 30.0D; + public static final double DEFAULT_FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS = 2.0D; + static final boolean DEFAULT_CODE_ORIGIN_FOR_SPANS_INTERFACE_SUPPORT = false; static final int DEFAULT_CODE_ORIGIN_MAX_USER_FRAMES = 8; static final boolean DEFAULT_TRACE_ENABLED = true; 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 index 28151f88864..25faf9da12f 100644 --- 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 @@ -2,5 +2,21 @@ public class FeatureFlaggingConfig { + public static final String FLAGGING_ENABLED = "flagging.enabled"; + + /** Replaced by {@link #FLAGGING_ENABLED}. */ + @Deprecated public static final String FLAGGING_PROVIDER_ENABLED = "experimental.flagging.provider.enabled"; + + public static final String FLAGGING_CONFIGURATION_SOURCE = "flagging.configuration.source"; + public static final String FLAGGING_CONFIGURATION_SOURCE_BASE_URL = + "flagging.configuration.source.base.url"; + public static final String FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS = + "flagging.configuration.source.poll.interval.seconds"; + public static final String FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS = + "flagging.configuration.source.request.timeout.seconds"; + public static final String FLAGGING_CONFIGURATION_SOURCE_EXTRA_HEADERS = + "flagging.configuration.source.extra.headers"; + + private FeatureFlaggingConfig() {} } 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 5ced61622ca..5a64fcf8aa6 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -86,6 +86,9 @@ import static datadog.trace.api.ConfigDefaults.DEFAULT_ELASTICSEARCH_BODY_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_ELASTICSEARCH_PARAMS_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_EXPERIMENTATAL_JEE_SPLIT_BY_DEPLOYMENT; +import static datadog.trace.api.ConfigDefaults.DEFAULT_FLAGGING_CONFIGURATION_SOURCE; +import static datadog.trace.api.ConfigDefaults.DEFAULT_FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS; +import static datadog.trace.api.ConfigDefaults.DEFAULT_FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS; import static datadog.trace.api.ConfigDefaults.DEFAULT_GRPC_CLIENT_ERROR_STATUSES; import static datadog.trace.api.ConfigDefaults.DEFAULT_GRPC_SERVER_ERROR_STATUSES; import static datadog.trace.api.ConfigDefaults.DEFAULT_HEALTH_METRICS_ENABLED; @@ -361,6 +364,11 @@ 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.FLAGGING_CONFIGURATION_SOURCE; +import static datadog.trace.api.config.FeatureFlaggingConfig.FLAGGING_CONFIGURATION_SOURCE_BASE_URL; +import static datadog.trace.api.config.FeatureFlaggingConfig.FLAGGING_CONFIGURATION_SOURCE_EXTRA_HEADERS; +import static datadog.trace.api.config.FeatureFlaggingConfig.FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS; +import static datadog.trace.api.config.FeatureFlaggingConfig.FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS; 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; @@ -1213,6 +1221,12 @@ public static String getHostName() { private final int remoteConfigMaxExtraServices; + private final String flaggingConfigurationSource; + private final String flaggingConfigurationSourceBaseUrl; + private final double flaggingConfigurationSourcePollIntervalSeconds; + private final double flaggingConfigurationSourceRequestTimeoutSeconds; + private final Map flaggingConfigurationSourceExtraHeaders; + private final boolean dbmInjectSqlBaseHash; private final String dbmPropagationMode; private final boolean dbmTracePreparedStatements; @@ -2835,6 +2849,23 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment()) configProvider.getInteger( REMOTE_CONFIG_MAX_EXTRA_SERVICES, DEFAULT_REMOTE_CONFIG_MAX_EXTRA_SERVICES); + flaggingConfigurationSource = + normalizeFlaggingConfigurationSource( + configProvider.getString( + FLAGGING_CONFIGURATION_SOURCE, DEFAULT_FLAGGING_CONFIGURATION_SOURCE)); + flaggingConfigurationSourceBaseUrl = + configProvider.getStringNotEmpty(FLAGGING_CONFIGURATION_SOURCE_BASE_URL, null); + flaggingConfigurationSourcePollIntervalSeconds = + configProvider.getDouble( + FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS, + DEFAULT_FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS); + flaggingConfigurationSourceRequestTimeoutSeconds = + configProvider.getDouble( + FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS, + DEFAULT_FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS); + flaggingConfigurationSourceExtraHeaders = + configProvider.getMergedMap(FLAGGING_CONFIGURATION_SOURCE_EXTRA_HEADERS, '='); + dynamicInstrumentationEnabled = configProvider.getBoolean( DYNAMIC_INSTRUMENTATION_ENABLED, DEFAULT_DYNAMIC_INSTRUMENTATION_ENABLED); @@ -3747,6 +3778,14 @@ public boolean isInferredProxyPropagationEnabled() { return traceInferredProxyEnabled; } + private static String normalizeFlaggingConfigurationSource(final String source) { + if (source == null) { + return DEFAULT_FLAGGING_CONFIGURATION_SOURCE; + } + final String normalized = source.trim().toLowerCase(Locale.ROOT); + return normalized.isEmpty() ? DEFAULT_FLAGGING_CONFIGURATION_SOURCE : normalized; + } + public boolean isBaggageExtract() { return tracePropagationStylesToExtract.contains(TracePropagationStyle.BAGGAGE); } @@ -4654,6 +4693,26 @@ public int getRemoteConfigMaxExtraServices() { return remoteConfigMaxExtraServices; } + public String getFlaggingConfigurationSource() { + return flaggingConfigurationSource; + } + + public String getFlaggingConfigurationSourceBaseUrl() { + return flaggingConfigurationSourceBaseUrl; + } + + public double getFlaggingConfigurationSourcePollIntervalSeconds() { + return flaggingConfigurationSourcePollIntervalSeconds; + } + + public double getFlaggingConfigurationSourceRequestTimeoutSeconds() { + return flaggingConfigurationSourceRequestTimeoutSeconds; + } + + public Map getFlaggingConfigurationSourceExtraHeaders() { + return flaggingConfigurationSourceExtraHeaders; + } + public boolean isDynamicInstrumentationEnabled() { return dynamicInstrumentationEnabled; } @@ -6486,6 +6545,16 @@ public String toString() { + remoteConfigMaxPayloadSize + ", remoteConfigIntegrityCheckEnabled=" + remoteConfigIntegrityCheckEnabled + + ", flaggingConfigurationSource=" + + flaggingConfigurationSource + + ", flaggingConfigurationSourceBaseUrl=" + + flaggingConfigurationSourceBaseUrl + + ", flaggingConfigurationSourcePollIntervalSeconds=" + + flaggingConfigurationSourcePollIntervalSeconds + + ", flaggingConfigurationSourceRequestTimeoutSeconds=" + + flaggingConfigurationSourceRequestTimeoutSeconds + + ", flaggingConfigurationSourceExtraHeaderNames=" + + flaggingConfigurationSourceExtraHeaders.keySet() + ", debuggerEnabled=" + dynamicInstrumentationEnabled + ", debuggerUploadTimeout=" diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 733f3736cbb..4c99c583841 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -1513,6 +1513,54 @@ "aliases": [] } ], + "DD_FLAGGING_CONFIGURATION_SOURCE": [ + { + "version": "A", + "type": "string", + "default": "cdn", + "aliases": [] + } + ], + "DD_FLAGGING_CONFIGURATION_SOURCE_BASE_URL": [ + { + "version": "A", + "type": "string", + "default": null, + "aliases": [] + } + ], + "DD_FLAGGING_CONFIGURATION_SOURCE_EXTRA_HEADERS": [ + { + "version": "A", + "type": "map", + "default": null, + "aliases": [] + } + ], + "DD_FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS": [ + { + "version": "A", + "type": "decimal", + "default": "30", + "aliases": [] + } + ], + "DD_FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS": [ + { + "version": "A", + "type": "decimal", + "default": "2", + "aliases": [] + } + ], + "DD_FLAGGING_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": ["DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED"] + } + ], "DD_FORCE_CLEAR_TEXT_HTTP_FOR_INTAKE_CLIENT": [ { "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 02689767bad..8d2ab24fb8f 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 @@ -8,21 +8,44 @@ public class FeatureFlaggingSystem { private static final Logger LOGGER = LoggerFactory.getLogger(FeatureFlaggingSystem.class); + private static final String SOURCE_CDN = "cdn"; + private static final String SOURCE_REMOTE_CONFIG = "remote_config"; + private static final String SOURCE_OFFLINE = "offline"; private static volatile RemoteConfigService CONFIG_SERVICE; private static volatile ExposureWriter EXPOSURE_WRITER; + private static final ConfigServiceFactory DEFAULT_CDN_CONFIG_SERVICE_FACTORY = + (sco, config) -> new CdnConfigService(config); + private static final ConfigServiceFactory DEFAULT_REMOTE_CONFIG_SERVICE_FACTORY = + RemoteConfigServiceImpl::new; + private static volatile ConfigServiceFactory CDN_CONFIG_SERVICE_FACTORY = + DEFAULT_CDN_CONFIG_SERVICE_FACTORY; + private static volatile ConfigServiceFactory REMOTE_CONFIG_SERVICE_FACTORY = + DEFAULT_REMOTE_CONFIG_SERVICE_FACTORY; private FeatureFlaggingSystem() {} public static void start(final SharedCommunicationObjects sco) { LOGGER.debug("Feature Flagging system starting"); final Config config = Config.get(); + final String configurationSource = config.getFlaggingConfigurationSource(); - if (!config.isRemoteConfigEnabled()) { - throw new IllegalStateException("Feature Flagging system started without RC"); + if (SOURCE_REMOTE_CONFIG.equals(configurationSource)) { + if (!config.isRemoteConfigEnabled()) { + throw new IllegalStateException("Feature Flagging system started without RC"); + } + CONFIG_SERVICE = REMOTE_CONFIG_SERVICE_FACTORY.create(sco, config); + CONFIG_SERVICE.init(); + } else if (SOURCE_CDN.equals(configurationSource)) { + CONFIG_SERVICE = CDN_CONFIG_SERVICE_FACTORY.create(sco, config); + CONFIG_SERVICE.init(); + } else if (SOURCE_OFFLINE.equals(configurationSource)) { + LOGGER.debug( + "Feature Flagging offline configuration source selected; no config service started"); + } else { + throw new IllegalArgumentException( + "Unsupported Feature Flagging configuration source: " + configurationSource); } - CONFIG_SERVICE = new RemoteConfigServiceImpl(sco, config); - CONFIG_SERVICE.init(); EXPOSURE_WRITER = new ExposureWriterImpl(sco, config); EXPOSURE_WRITER.init(); @@ -41,4 +64,20 @@ public static void stop() { } LOGGER.debug("Feature Flagging system stopped"); } + + public static void setServiceFactoriesForTest( + final ConfigServiceFactory cdnConfigServiceFactory, + final ConfigServiceFactory remoteConfigServiceFactory) { + CDN_CONFIG_SERVICE_FACTORY = cdnConfigServiceFactory; + REMOTE_CONFIG_SERVICE_FACTORY = remoteConfigServiceFactory; + } + + public static void resetServiceFactoriesForTest() { + CDN_CONFIG_SERVICE_FACTORY = DEFAULT_CDN_CONFIG_SERVICE_FACTORY; + REMOTE_CONFIG_SERVICE_FACTORY = DEFAULT_REMOTE_CONFIG_SERVICE_FACTORY; + } + + public interface ConfigServiceFactory { + RemoteConfigService create(SharedCommunicationObjects sco, Config config); + } } 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 970c2cf16e3..c98617fd8f1 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,11 +1,13 @@ package com.datadog.featureflag; +import static datadog.trace.api.config.FeatureFlaggingConfig.FLAGGING_CONFIGURATION_SOURCE; import static datadog.trace.api.config.RemoteConfigConfig.REMOTE_CONFIGURATION_ENABLED; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import datadog.communication.ddagent.DDAgentFeaturesDiscovery; @@ -23,6 +25,8 @@ class FeatureFlaggingSystemTest { @Test + @WithConfig(key = FLAGGING_CONFIGURATION_SOURCE, value = "remote_config") + @WithConfig(key = REMOTE_CONFIGURATION_ENABLED, value = "true") void testFeatureFlagSystemInitialization() { ConfigurationPoller poller = mock(ConfigurationPoller.class); DDAgentFeaturesDiscovery discovery = mock(DDAgentFeaturesDiscovery.class); @@ -48,6 +52,7 @@ void testFeatureFlagSystemInitialization() { } @Test + @WithConfig(key = FLAGGING_CONFIGURATION_SOURCE, value = "remote_config") @WithConfig(key = REMOTE_CONFIGURATION_ENABLED, value = "false") void testThatRemoteConfigIsRequired() { SharedCommunicationObjects sharedCommunicationObjects = mock(SharedCommunicationObjects.class); @@ -60,4 +65,78 @@ void testThatRemoteConfigIsRequired() { FeatureFlaggingSystem.stop(); } } + + @Test + @WithConfig(key = FLAGGING_CONFIGURATION_SOURCE, value = "cdn") + @WithConfig(key = REMOTE_CONFIGURATION_ENABLED, value = "false") + void cdnConfigurationSourceStartsCdnWithoutRemoteConfig() { + RemoteConfigService cdnService = mock(RemoteConfigService.class); + RemoteConfigService remoteConfigService = mock(RemoteConfigService.class); + FeatureFlaggingSystem.setServiceFactoriesForTest( + (ignoredSco, ignoredConfig) -> cdnService, + (ignoredSco, ignoredConfig) -> remoteConfigService); + + try { + FeatureFlaggingSystem.start(sharedCommunicationObjects()); + + verify(cdnService).init(); + verifyNoInteractions(remoteConfigService); + } finally { + FeatureFlaggingSystem.stop(); + FeatureFlaggingSystem.resetServiceFactoriesForTest(); + } + } + + @Test + @WithConfig(key = FLAGGING_CONFIGURATION_SOURCE, value = "remote_config") + @WithConfig(key = REMOTE_CONFIGURATION_ENABLED, value = "true") + void explicitRemoteConfigSuppressesCdnService() { + RemoteConfigService cdnService = mock(RemoteConfigService.class); + RemoteConfigService remoteConfigService = mock(RemoteConfigService.class); + FeatureFlaggingSystem.setServiceFactoriesForTest( + (ignoredSco, ignoredConfig) -> cdnService, + (ignoredSco, ignoredConfig) -> remoteConfigService); + + try { + FeatureFlaggingSystem.start(sharedCommunicationObjects()); + + verify(remoteConfigService).init(); + verifyNoInteractions(cdnService); + } finally { + FeatureFlaggingSystem.stop(); + FeatureFlaggingSystem.resetServiceFactoriesForTest(); + } + } + + @Test + @WithConfig(key = FLAGGING_CONFIGURATION_SOURCE, value = "invalid") + void invalidConfigurationSourceFailsBeforeStartingCdn() { + RemoteConfigService cdnService = mock(RemoteConfigService.class); + RemoteConfigService remoteConfigService = mock(RemoteConfigService.class); + FeatureFlaggingSystem.setServiceFactoriesForTest( + (ignoredSco, ignoredConfig) -> cdnService, + (ignoredSco, ignoredConfig) -> remoteConfigService); + + try { + assertThrows( + IllegalArgumentException.class, + () -> FeatureFlaggingSystem.start(sharedCommunicationObjects())); + verifyNoInteractions(cdnService); + verifyNoInteractions(remoteConfigService); + } finally { + FeatureFlaggingSystem.stop(); + FeatureFlaggingSystem.resetServiceFactoriesForTest(); + } + } + + private static SharedCommunicationObjects sharedCommunicationObjects() { + DDAgentFeaturesDiscovery discovery = mock(DDAgentFeaturesDiscovery.class); + when(discovery.supportsEvpProxy()).thenReturn(true); + when(discovery.getEvpProxyEndpoint()).thenReturn("/evp_proxy/"); + SharedCommunicationObjects sharedCommunicationObjects = mock(SharedCommunicationObjects.class); + when(sharedCommunicationObjects.featuresDiscovery(any(Config.class))).thenReturn(discovery); + sharedCommunicationObjects.agentUrl = HttpUrl.get("http://localhost"); + sharedCommunicationObjects.agentHttpClient = new OkHttpClient.Builder().build(); + return sharedCommunicationObjects; + } } diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/CdnConfigService.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/CdnConfigService.java new file mode 100644 index 00000000000..3857cfd8cd4 --- /dev/null +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/CdnConfigService.java @@ -0,0 +1,284 @@ +package com.datadog.featureflag; + +import static datadog.communication.http.OkHttpUtils.prepareRequest; + +import datadog.communication.http.OkHttpUtils; +import datadog.trace.api.Config; +import datadog.trace.api.featureflag.FeatureFlaggingGateway; +import datadog.trace.api.featureflag.ufc.v1.ServerConfiguration; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URLEncoder; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class CdnConfigService implements RemoteConfigService { + private static final Logger LOGGER = LoggerFactory.getLogger(CdnConfigService.class); + + private static final String SERVER_DISTRIBUTION_PATH = + "/api/v2/feature-flagging/config/server-distribution"; + private static final int MAX_ATTEMPTS = 3; + + private final HttpUrl endpoint; + private final Config config; + private final Map extraHeaders; + private final long pollIntervalMillis; + private final CdnClient client; + private final ScheduledExecutorService executor; + private final AtomicBoolean polling = new AtomicBoolean(); + private volatile boolean closed; + private volatile ScheduledFuture scheduledPoll; + private volatile String etag; + + CdnConfigService(final Config config) { + this(config, endpoint(config)); + } + + private CdnConfigService(final Config config, final HttpUrl endpoint) { + this( + endpoint, + config, + millis(config.getFlaggingConfigurationSourcePollIntervalSeconds()), + new OkHttpCdnClient( + OkHttpUtils.buildHttpClient( + endpoint, millis(config.getFlaggingConfigurationSourceRequestTimeoutSeconds()))), + Executors.newSingleThreadScheduledExecutor(new CdnThreadFactory())); + } + + CdnConfigService( + final HttpUrl endpoint, + final Config config, + final long pollIntervalMillis, + final CdnClient client, + final ScheduledExecutorService executor) { + this.endpoint = endpoint; + this.config = config; + final Map configuredExtraHeaders = + config.getFlaggingConfigurationSourceExtraHeaders(); + this.extraHeaders = + configuredExtraHeaders == null ? Collections.emptyMap() : configuredExtraHeaders; + this.pollIntervalMillis = pollIntervalMillis; + this.client = client; + this.executor = executor; + } + + @Override + public void init() { + if (closed) { + return; + } + scheduledPoll = + executor.scheduleWithFixedDelay( + this::pollOnceSafely, 0, pollIntervalMillis, TimeUnit.MILLISECONDS); + } + + boolean pollOnce() { + if (closed || !polling.compareAndSet(false, true)) { + return false; + } + try { + return fetchAndApply(); + } finally { + polling.set(false); + } + } + + @Override + public void close() { + closed = true; + if (scheduledPoll != null) { + scheduledPoll.cancel(true); + scheduledPoll = null; + } + executor.shutdownNow(); + } + + private void pollOnceSafely() { + try { + pollOnce(); + } catch (final RuntimeException e) { + LOGGER.debug("Unexpected error while polling Feature Flagging CDN", e); + } + } + + private boolean fetchAndApply() { + for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + try { + final CdnResponse response = client.fetch(endpoint, config, extraHeaders, etag); + if (isRetryableStatus(response.status) && attempt < MAX_ATTEMPTS) { + continue; + } + return apply(response); + } catch (final IOException e) { + if (attempt == MAX_ATTEMPTS) { + LOGGER.debug("Feature Flagging CDN request failed", e); + return false; + } + } + } + return false; + } + + private boolean apply(final CdnResponse response) { + if (response.status == HttpURLConnection.HTTP_NOT_MODIFIED) { + updateEtag(response.etag); + return true; + } + if (response.status == HttpURLConnection.HTTP_UNAUTHORIZED + || response.status == HttpURLConnection.HTTP_FORBIDDEN + || response.status != HttpURLConnection.HTTP_OK + || response.body == null) { + return false; + } + final ServerConfiguration configuration; + try { + configuration = + RemoteConfigServiceImpl.UniversalFlagConfigDeserializer.INSTANCE.deserialize( + response.body); + } catch (final IOException | RuntimeException e) { + LOGGER.debug("Feature Flagging CDN returned malformed UFC payload", e); + return false; + } + if (configuration == null) { + return false; + } + updateEtag(response.etag); + FeatureFlaggingGateway.dispatch(configuration); + return true; + } + + private static boolean isRetryableStatus(final int status) { + return status == HttpURLConnection.HTTP_CLIENT_TIMEOUT + || status == 429 + || (status >= 500 && status <= 599); + } + + private void updateEtag(final String nextEtag) { + if (nextEtag != null && !nextEtag.trim().isEmpty()) { + etag = nextEtag; + } + } + + static HttpUrl endpoint(final Config config) { + final String configuredBaseUrl = config.getFlaggingConfigurationSourceBaseUrl(); + final String endpoint = + configuredBaseUrl == null + ? defaultEndpoint(config) + : endpointFromConfiguredUrl(configuredBaseUrl); + final HttpUrl parsed = HttpUrl.parse(endpoint); + if (parsed == null) { + throw new IllegalArgumentException("Invalid Feature Flagging CDN URL: " + endpoint); + } + return parsed; + } + + private static String endpointFromConfiguredUrl(final String configuredUrl) { + final HttpUrl parsed = HttpUrl.parse(configuredUrl.trim()); + if (parsed == null) { + throw new IllegalArgumentException("Invalid Feature Flagging CDN URL: " + configuredUrl); + } + if (isRootPath(parsed)) { + return parsed.newBuilder().addPathSegments("mock/ufc/config").build().toString(); + } + return parsed.toString(); + } + + private static boolean isRootPath(final HttpUrl url) { + return "/".equals(url.encodedPath()) || url.encodedPath().isEmpty(); + } + + private static String defaultEndpoint(final Config config) { + final StringBuilder endpoint = + new StringBuilder("https://api.") + .append(config.getSite().toLowerCase(Locale.ROOT)) + .append(SERVER_DISTRIBUTION_PATH); + final String env = config.getEnv(); + if (env != null && !env.isEmpty()) { + endpoint.append("?dd_env=").append(urlEncode(env)); + } + return endpoint.toString(); + } + + private static String urlEncode(final String value) { + try { + return URLEncoder.encode(value, "UTF-8"); + } catch (final IOException e) { + throw new IllegalArgumentException("Unable to encode Feature Flagging environment", e); + } + } + + static long millis(final double seconds) { + return Math.max(1L, Math.round(seconds * 1000)); + } + + interface CdnClient { + CdnResponse fetch( + HttpUrl endpoint, Config config, Map extraHeaders, String etag) + throws IOException; + } + + static final class CdnResponse { + final int status; + final String etag; + final byte[] body; + + CdnResponse(final int status, final String etag, final byte[] body) { + this.status = status; + this.etag = etag; + this.body = body; + } + } + + private static final class OkHttpCdnClient implements CdnClient { + private final OkHttpClient httpClient; + + private OkHttpCdnClient(final OkHttpClient httpClient) { + this.httpClient = httpClient; + } + + @Override + public CdnResponse fetch( + final HttpUrl endpoint, + final Config config, + final Map extraHeaders, + final String etag) + throws IOException { + final Map headers = new HashMap<>(extraHeaders); + if (etag != null) { + headers.put("If-None-Match", etag); + } + final Request request = prepareRequest(endpoint, headers, config, true).get().build(); + try (Response response = httpClient.newCall(request).execute()) { + final ResponseBody responseBody = response.body(); + return new CdnResponse( + response.code(), + response.header("ETag"), + responseBody == null ? null : responseBody.bytes()); + } + } + } + + private static final class CdnThreadFactory implements ThreadFactory { + @Override + public Thread newThread(final Runnable runnable) { + final Thread thread = new Thread(runnable, "dd-feature-flagging-cdn-poller"); + thread.setDaemon(true); + return thread; + } + } +} diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/CdnConfigServiceTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/CdnConfigServiceTest.java new file mode 100644 index 00000000000..0bcdf1567c9 --- /dev/null +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/CdnConfigServiceTest.java @@ -0,0 +1,303 @@ +package com.datadog.featureflag; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import datadog.trace.api.Config; +import datadog.trace.api.featureflag.FeatureFlaggingGateway; +import datadog.trace.api.featureflag.ufc.v1.ServerConfiguration; +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import okhttp3.HttpUrl; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class CdnConfigServiceTest { + + @Mock private FeatureFlaggingGateway.ConfigListener listener; + + @AfterEach + void cleanup() { + FeatureFlaggingGateway.removeConfigListener(listener); + FeatureFlaggingGateway.dispatch((ServerConfiguration) null); + } + + @Test + void appendsMockConfigPathWhenConfiguredUrlIsRoot() { + final Config config = config("http://mock-cdn:8092", "datadoghq.com", ""); + + assertEquals( + "http://mock-cdn:8092/mock/ufc/config", CdnConfigService.endpoint(config).toString()); + } + + @Test + void preservesConfiguredFullEndpointUrl() { + final Config config = + config("http://mock-cdn:8092/mock/ufc/config?fixture=valid", "datadoghq.com", ""); + + assertEquals( + "http://mock-cdn:8092/mock/ufc/config?fixture=valid", + CdnConfigService.endpoint(config).toString()); + } + + @Test + void derivesDefaultEndpointFromSiteAndEnv() { + final Config config = config(null, "datad0g.com", "staging env"); + + assertEquals( + "https://api.datad0g.com/api/v2/feature-flagging/config/server-distribution?dd_env=staging+env", + CdnConfigService.endpoint(config).toString()); + } + + @Test + void appliesAcceptedUfcThroughGatewayAndSendsHeaders() throws Exception { + final FakeClient client = new FakeClient(response(200, "etag-a", emptyConfig())); + final CdnConfigService service = service(client, extraHeaders()); + FeatureFlaggingGateway.addConfigListener(listener); + + assertTrue(service.pollOnce()); + + verify(listener).accept(any(ServerConfiguration.class)); + assertEquals("test-api-key", client.requests.get(0).apiKey); + assertEquals("app-key", client.requests.get(0).extraHeaders.get("DD-APPLICATION-KEY")); + assertEquals("1", client.requests.get(0).extraHeaders.get("fastly-client")); + assertNull(client.requests.get(0).etag); + } + + @Test + void usesEtagAndSkipsDispatchOnUnchangedConfig() throws Exception { + final FakeClient client = + new FakeClient(response(200, "etag-a", emptyConfig()), response(304, null, null)); + final CdnConfigService service = service(client, extraHeaders()); + FeatureFlaggingGateway.addConfigListener(listener); + + assertTrue(service.pollOnce()); + assertTrue(service.pollOnce()); + + verify(listener).accept(any(ServerConfiguration.class)); + assertEquals("etag-a", client.requests.get(1).etag); + } + + @Test + void keepsLastKnownGoodOnAuthFailureAndMalformedPayload() throws Exception { + final FakeClient client = + new FakeClient( + response(401, null, null), + response(200, null, "{not-json}"), + response(200, null, "{\"flags\":[]}")); + final CdnConfigService service = service(client, extraHeaders()); + FeatureFlaggingGateway.addConfigListener(listener); + + assertFalse(service.pollOnce()); + assertFalse(service.pollOnce()); + assertFalse(service.pollOnce()); + + verifyNoInteractions(listener); + } + + @Test + void retriesTimeoutBeforeApplyingConfig() throws Exception { + final FakeClient client = + new FakeClient( + new SocketTimeoutException("slow CDN"), + new SocketTimeoutException("slow CDN"), + response(200, "etag-a", emptyConfig())); + final CdnConfigService service = service(client, extraHeaders()); + FeatureFlaggingGateway.addConfigListener(listener); + + assertTrue(service.pollOnce()); + + assertEquals(3, client.calls.get()); + verify(listener).accept(any(ServerConfiguration.class)); + } + + @Test + void retriesServerErrorThenKeepsColdStateOnNotModified() throws Exception { + final FakeClient client = new FakeClient(response(500, null, null), response(304, null, null)); + final CdnConfigService service = service(client, extraHeaders()); + FeatureFlaggingGateway.addConfigListener(listener); + + assertTrue(service.pollOnce()); + + assertEquals(2, client.calls.get()); + verifyNoInteractions(listener); + } + + @Test + void rejectsOverlappingPolls() throws Exception { + final CountDownLatch requestStarted = new CountDownLatch(1); + final CountDownLatch releaseRequest = new CountDownLatch(1); + final FakeClient client = new FakeClient(response(200, "etag-a", emptyConfig())); + client.block(requestStarted, releaseRequest); + final CdnConfigService service = service(client, extraHeaders()); + final ExecutorService runner = Executors.newFixedThreadPool(2); + + try { + final Future first = runner.submit(service::pollOnce); + assertTrue(requestStarted.await(1, TimeUnit.SECONDS)); + final Future second = runner.submit(service::pollOnce); + + assertFalse(second.get(1, TimeUnit.SECONDS)); + releaseRequest.countDown(); + assertTrue(first.get(1, TimeUnit.SECONDS)); + assertEquals(1, client.calls.get()); + } finally { + releaseRequest.countDown(); + runner.shutdownNow(); + } + } + + @Test + void closePreventsFurtherPolls() throws Exception { + final FakeClient client = new FakeClient(response(200, "etag-a", emptyConfig())); + final CdnConfigService service = service(client, extraHeaders()); + + service.close(); + + assertFalse(service.pollOnce()); + assertEquals(0, client.calls.get()); + } + + private static CdnConfigService service( + final FakeClient client, final Map extraHeaders) { + final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + return new CdnConfigService( + HttpUrl.get("http://localhost/mock/ufc/config"), + config("http://localhost", "datadoghq.com", "", extraHeaders), + 60_000, + client, + executor); + } + + private static Config config(final String baseUrl, final String site, final String env) { + return config(baseUrl, site, env, new HashMap<>()); + } + + private static Config config( + final String baseUrl, + final String site, + final String env, + final Map extraHeaders) { + final Config config = mock(Config.class); + lenient().when(config.getFlaggingConfigurationSourceBaseUrl()).thenReturn(baseUrl); + lenient().when(config.getFlaggingConfigurationSourceExtraHeaders()).thenReturn(extraHeaders); + lenient().when(config.getFlaggingConfigurationSourcePollIntervalSeconds()).thenReturn(30.0D); + lenient().when(config.getFlaggingConfigurationSourceRequestTimeoutSeconds()).thenReturn(2.0D); + lenient().when(config.getApiKey()).thenReturn("test-api-key"); + lenient().when(config.getSite()).thenReturn(site); + lenient().when(config.getEnv()).thenReturn(env); + return config; + } + + private static Map extraHeaders() { + final Map headers = new HashMap<>(); + headers.put("DD-APPLICATION-KEY", "app-key"); + headers.put("fastly-client", "1"); + return headers; + } + + private static CdnConfigService.CdnResponse response( + final int status, final String etag, final String body) { + return new CdnConfigService.CdnResponse( + status, etag, body == null ? null : body.getBytes(UTF_8)); + } + + private static String emptyConfig() { + return "{" + + "\"createdAt\":\"2024-04-17T19:40:53.716Z\"," + + "\"format\":\"SERVER\"," + + "\"environment\":{\"name\":\"Test\"}," + + "\"flags\":{}" + + "}"; + } + + private static final class FakeClient implements CdnConfigService.CdnClient { + private final AtomicInteger calls = new AtomicInteger(); + private final List requests = new ArrayList<>(); + private final BlockingQueue responses = new LinkedBlockingQueue<>(); + private CountDownLatch requestStarted; + private CountDownLatch releaseRequest; + + private FakeClient(final Object... responses) { + for (final Object response : responses) { + this.responses.add(response); + } + } + + private void block(final CountDownLatch requestStarted, final CountDownLatch releaseRequest) { + this.requestStarted = requestStarted; + this.releaseRequest = releaseRequest; + } + + @Override + public CdnConfigService.CdnResponse fetch( + final HttpUrl endpoint, + final Config config, + final Map extraHeaders, + final String etag) + throws IOException { + calls.incrementAndGet(); + requests.add(new Request(config.getApiKey(), etag, extraHeaders)); + if (requestStarted != null) { + requestStarted.countDown(); + } + if (releaseRequest != null) { + await(releaseRequest); + } + final Object response = responses.remove(); + if (response instanceof IOException) { + throw (IOException) response; + } + return (CdnConfigService.CdnResponse) response; + } + + private static void await(final CountDownLatch latch) throws IOException { + try { + if (!latch.await(1, TimeUnit.SECONDS)) { + throw new SocketTimeoutException("test request did not release"); + } + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException(e); + } + } + } + + private static final class Request { + private final String apiKey; + private final String etag; + private final Map extraHeaders; + + private Request( + final String apiKey, final String etag, final Map extraHeaders) { + this.apiKey = apiKey; + this.etag = etag; + this.extraHeaders = new HashMap<>(extraHeaders); + } + } +} From 1c8eef89066f8d1fe75f9a0967f37eab1352b459 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Wed, 8 Jul 2026 14:26:12 -0400 Subject: [PATCH 2/5] Clean up feature flagging configuration source naming --- .../featureflag/FeatureFlaggingSystem.java | 52 +++++-------- .../FeatureFlaggingSystemTest.java | 78 +++++++------------ ...e.java => ConfigurationSourceService.java} | 2 +- .../featureflag/RemoteConfigServiceImpl.java | 2 +- ...Service.java => UfcHttpConfigService.java} | 54 ++++++------- ...est.java => UfcHttpConfigServiceTest.java} | 40 +++++----- 6 files changed, 97 insertions(+), 131 deletions(-) rename products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/{RemoteConfigService.java => ConfigurationSourceService.java} (60%) rename products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/{CdnConfigService.java => UfcHttpConfigService.java} (82%) rename products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/{CdnConfigServiceTest.java => UfcHttpConfigServiceTest.java} (88%) 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 8d2ab24fb8f..6128d43cd34 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 @@ -12,45 +12,45 @@ public class FeatureFlaggingSystem { private static final String SOURCE_REMOTE_CONFIG = "remote_config"; private static final String SOURCE_OFFLINE = "offline"; - private static volatile RemoteConfigService CONFIG_SERVICE; + private static volatile ConfigurationSourceService CONFIG_SERVICE; private static volatile ExposureWriter EXPOSURE_WRITER; - private static final ConfigServiceFactory DEFAULT_CDN_CONFIG_SERVICE_FACTORY = - (sco, config) -> new CdnConfigService(config); - private static final ConfigServiceFactory DEFAULT_REMOTE_CONFIG_SERVICE_FACTORY = - RemoteConfigServiceImpl::new; - private static volatile ConfigServiceFactory CDN_CONFIG_SERVICE_FACTORY = - DEFAULT_CDN_CONFIG_SERVICE_FACTORY; - private static volatile ConfigServiceFactory REMOTE_CONFIG_SERVICE_FACTORY = - DEFAULT_REMOTE_CONFIG_SERVICE_FACTORY; private FeatureFlaggingSystem() {} public static void start(final SharedCommunicationObjects sco) { LOGGER.debug("Feature Flagging system starting"); final Config config = Config.get(); + CONFIG_SERVICE = createConfigurationSourceService(sco, config); + + if (CONFIG_SERVICE != null) { + CONFIG_SERVICE.init(); + } + + EXPOSURE_WRITER = new ExposureWriterImpl(sco, config); + EXPOSURE_WRITER.init(); + + LOGGER.debug("Feature Flagging system started"); + } + + static ConfigurationSourceService createConfigurationSourceService( + final SharedCommunicationObjects sco, final Config config) { final String configurationSource = config.getFlaggingConfigurationSource(); if (SOURCE_REMOTE_CONFIG.equals(configurationSource)) { if (!config.isRemoteConfigEnabled()) { throw new IllegalStateException("Feature Flagging system started without RC"); } - CONFIG_SERVICE = REMOTE_CONFIG_SERVICE_FACTORY.create(sco, config); - CONFIG_SERVICE.init(); + return new RemoteConfigServiceImpl(sco, config); } else if (SOURCE_CDN.equals(configurationSource)) { - CONFIG_SERVICE = CDN_CONFIG_SERVICE_FACTORY.create(sco, config); - CONFIG_SERVICE.init(); + return new UfcHttpConfigService(config); } else if (SOURCE_OFFLINE.equals(configurationSource)) { LOGGER.debug( "Feature Flagging offline configuration source selected; no config service started"); + return null; } else { throw new IllegalArgumentException( "Unsupported Feature Flagging configuration source: " + configurationSource); } - - EXPOSURE_WRITER = new ExposureWriterImpl(sco, config); - EXPOSURE_WRITER.init(); - - LOGGER.debug("Feature Flagging system started"); } public static void stop() { @@ -64,20 +64,4 @@ public static void stop() { } LOGGER.debug("Feature Flagging system stopped"); } - - public static void setServiceFactoriesForTest( - final ConfigServiceFactory cdnConfigServiceFactory, - final ConfigServiceFactory remoteConfigServiceFactory) { - CDN_CONFIG_SERVICE_FACTORY = cdnConfigServiceFactory; - REMOTE_CONFIG_SERVICE_FACTORY = remoteConfigServiceFactory; - } - - public static void resetServiceFactoriesForTest() { - CDN_CONFIG_SERVICE_FACTORY = DEFAULT_CDN_CONFIG_SERVICE_FACTORY; - REMOTE_CONFIG_SERVICE_FACTORY = DEFAULT_REMOTE_CONFIG_SERVICE_FACTORY; - } - - public interface ConfigServiceFactory { - RemoteConfigService create(SharedCommunicationObjects sco, Config config); - } } 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 c98617fd8f1..c70ec92bafd 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 @@ -2,12 +2,13 @@ import static datadog.trace.api.config.FeatureFlaggingConfig.FLAGGING_CONFIGURATION_SOURCE; import static datadog.trace.api.config.RemoteConfigConfig.REMOTE_CONFIGURATION_ENABLED; +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.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import datadog.communication.ddagent.DDAgentFeaturesDiscovery; @@ -69,64 +70,43 @@ void testThatRemoteConfigIsRequired() { @Test @WithConfig(key = FLAGGING_CONFIGURATION_SOURCE, value = "cdn") @WithConfig(key = REMOTE_CONFIGURATION_ENABLED, value = "false") - void cdnConfigurationSourceStartsCdnWithoutRemoteConfig() { - RemoteConfigService cdnService = mock(RemoteConfigService.class); - RemoteConfigService remoteConfigService = mock(RemoteConfigService.class); - FeatureFlaggingSystem.setServiceFactoriesForTest( - (ignoredSco, ignoredConfig) -> cdnService, - (ignoredSco, ignoredConfig) -> remoteConfigService); - - try { - FeatureFlaggingSystem.start(sharedCommunicationObjects()); - - verify(cdnService).init(); - verifyNoInteractions(remoteConfigService); - } finally { - FeatureFlaggingSystem.stop(); - FeatureFlaggingSystem.resetServiceFactoriesForTest(); - } + void cdnConfigurationSourceUsesHttpServiceWithoutRemoteConfig() { + assertInstanceOf( + UfcHttpConfigService.class, + FeatureFlaggingSystem.createConfigurationSourceService( + sharedCommunicationObjects(), Config.get())); } @Test @WithConfig(key = FLAGGING_CONFIGURATION_SOURCE, value = "remote_config") @WithConfig(key = REMOTE_CONFIGURATION_ENABLED, value = "true") - void explicitRemoteConfigSuppressesCdnService() { - RemoteConfigService cdnService = mock(RemoteConfigService.class); - RemoteConfigService remoteConfigService = mock(RemoteConfigService.class); - FeatureFlaggingSystem.setServiceFactoriesForTest( - (ignoredSco, ignoredConfig) -> cdnService, - (ignoredSco, ignoredConfig) -> remoteConfigService); - - try { - FeatureFlaggingSystem.start(sharedCommunicationObjects()); - - verify(remoteConfigService).init(); - verifyNoInteractions(cdnService); - } finally { - FeatureFlaggingSystem.stop(); - FeatureFlaggingSystem.resetServiceFactoriesForTest(); - } + void explicitRemoteConfigUsesRemoteConfigService() { + SharedCommunicationObjects sharedCommunicationObjects = sharedCommunicationObjects(); + when(sharedCommunicationObjects.configurationPoller(any(Config.class))) + .thenReturn(mock(ConfigurationPoller.class)); + + assertInstanceOf( + RemoteConfigServiceImpl.class, + FeatureFlaggingSystem.createConfigurationSourceService( + sharedCommunicationObjects, Config.get())); } @Test @WithConfig(key = FLAGGING_CONFIGURATION_SOURCE, value = "invalid") - void invalidConfigurationSourceFailsBeforeStartingCdn() { - RemoteConfigService cdnService = mock(RemoteConfigService.class); - RemoteConfigService remoteConfigService = mock(RemoteConfigService.class); - FeatureFlaggingSystem.setServiceFactoriesForTest( - (ignoredSco, ignoredConfig) -> cdnService, - (ignoredSco, ignoredConfig) -> remoteConfigService); + void invalidConfigurationSourceFailsBeforeStartingNetworkSource() { + assertThrows( + IllegalArgumentException.class, + () -> + FeatureFlaggingSystem.createConfigurationSourceService( + sharedCommunicationObjects(), Config.get())); + } - try { - assertThrows( - IllegalArgumentException.class, - () -> FeatureFlaggingSystem.start(sharedCommunicationObjects())); - verifyNoInteractions(cdnService); - verifyNoInteractions(remoteConfigService); - } finally { - FeatureFlaggingSystem.stop(); - FeatureFlaggingSystem.resetServiceFactoriesForTest(); - } + @Test + @WithConfig(key = FLAGGING_CONFIGURATION_SOURCE, value = "offline") + void offlineConfigurationSourceDoesNotStartNetworkSource() { + assertNull( + FeatureFlaggingSystem.createConfigurationSourceService( + sharedCommunicationObjects(), Config.get())); } private static SharedCommunicationObjects sharedCommunicationObjects() { diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/RemoteConfigService.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/ConfigurationSourceService.java similarity index 60% rename from products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/RemoteConfigService.java rename to products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/ConfigurationSourceService.java index 5f84a78f7d4..83495545717 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/RemoteConfigService.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/ConfigurationSourceService.java @@ -2,7 +2,7 @@ import java.io.Closeable; -public interface RemoteConfigService extends Closeable { +public interface ConfigurationSourceService extends Closeable { void init(); diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/RemoteConfigServiceImpl.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/RemoteConfigServiceImpl.java index ec28f684a96..a5a9b2d7f36 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/RemoteConfigServiceImpl.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/RemoteConfigServiceImpl.java @@ -32,7 +32,7 @@ import okio.Okio; public class RemoteConfigServiceImpl - implements RemoteConfigService, ConfigurationChangesTypedListener { + implements ConfigurationSourceService, ConfigurationChangesTypedListener { private final ConfigurationPoller configurationPoller; diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/CdnConfigService.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/UfcHttpConfigService.java similarity index 82% rename from products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/CdnConfigService.java rename to products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/UfcHttpConfigService.java index 3857cfd8cd4..f7b42909d36 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/CdnConfigService.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/UfcHttpConfigService.java @@ -27,8 +27,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -final class CdnConfigService implements RemoteConfigService { - private static final Logger LOGGER = LoggerFactory.getLogger(CdnConfigService.class); +final class UfcHttpConfigService implements ConfigurationSourceService { + private static final Logger LOGGER = LoggerFactory.getLogger(UfcHttpConfigService.class); private static final String SERVER_DISTRIBUTION_PATH = "/api/v2/feature-flagging/config/server-distribution"; @@ -38,33 +38,33 @@ final class CdnConfigService implements RemoteConfigService { private final Config config; private final Map extraHeaders; private final long pollIntervalMillis; - private final CdnClient client; + private final UfcHttpClient client; private final ScheduledExecutorService executor; private final AtomicBoolean polling = new AtomicBoolean(); private volatile boolean closed; private volatile ScheduledFuture scheduledPoll; private volatile String etag; - CdnConfigService(final Config config) { + UfcHttpConfigService(final Config config) { this(config, endpoint(config)); } - private CdnConfigService(final Config config, final HttpUrl endpoint) { + private UfcHttpConfigService(final Config config, final HttpUrl endpoint) { this( endpoint, config, millis(config.getFlaggingConfigurationSourcePollIntervalSeconds()), - new OkHttpCdnClient( + new OkHttpUfcHttpClient( OkHttpUtils.buildHttpClient( endpoint, millis(config.getFlaggingConfigurationSourceRequestTimeoutSeconds()))), - Executors.newSingleThreadScheduledExecutor(new CdnThreadFactory())); + Executors.newSingleThreadScheduledExecutor(new UfcHttpThreadFactory())); } - CdnConfigService( + UfcHttpConfigService( final HttpUrl endpoint, final Config config, final long pollIntervalMillis, - final CdnClient client, + final UfcHttpClient client, final ScheduledExecutorService executor) { this.endpoint = endpoint; this.config = config; @@ -112,21 +112,21 @@ private void pollOnceSafely() { try { pollOnce(); } catch (final RuntimeException e) { - LOGGER.debug("Unexpected error while polling Feature Flagging CDN", e); + LOGGER.debug("Unexpected error while polling Feature Flagging HTTP configuration source", e); } } private boolean fetchAndApply() { for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { try { - final CdnResponse response = client.fetch(endpoint, config, extraHeaders, etag); + final UfcHttpResponse response = client.fetch(endpoint, config, extraHeaders, etag); if (isRetryableStatus(response.status) && attempt < MAX_ATTEMPTS) { continue; } return apply(response); } catch (final IOException e) { if (attempt == MAX_ATTEMPTS) { - LOGGER.debug("Feature Flagging CDN request failed", e); + LOGGER.debug("Feature Flagging HTTP configuration source request failed", e); return false; } } @@ -134,7 +134,7 @@ private boolean fetchAndApply() { return false; } - private boolean apply(final CdnResponse response) { + private boolean apply(final UfcHttpResponse response) { if (response.status == HttpURLConnection.HTTP_NOT_MODIFIED) { updateEtag(response.etag); return true; @@ -151,7 +151,7 @@ private boolean apply(final CdnResponse response) { RemoteConfigServiceImpl.UniversalFlagConfigDeserializer.INSTANCE.deserialize( response.body); } catch (final IOException | RuntimeException e) { - LOGGER.debug("Feature Flagging CDN returned malformed UFC payload", e); + LOGGER.debug("Feature Flagging HTTP configuration source returned malformed UFC payload", e); return false; } if (configuration == null) { @@ -182,7 +182,8 @@ static HttpUrl endpoint(final Config config) { : endpointFromConfiguredUrl(configuredBaseUrl); final HttpUrl parsed = HttpUrl.parse(endpoint); if (parsed == null) { - throw new IllegalArgumentException("Invalid Feature Flagging CDN URL: " + endpoint); + throw new IllegalArgumentException( + "Invalid Feature Flagging HTTP configuration source URL: " + endpoint); } return parsed; } @@ -190,7 +191,8 @@ static HttpUrl endpoint(final Config config) { private static String endpointFromConfiguredUrl(final String configuredUrl) { final HttpUrl parsed = HttpUrl.parse(configuredUrl.trim()); if (parsed == null) { - throw new IllegalArgumentException("Invalid Feature Flagging CDN URL: " + configuredUrl); + throw new IllegalArgumentException( + "Invalid Feature Flagging HTTP configuration source URL: " + configuredUrl); } if (isRootPath(parsed)) { return parsed.newBuilder().addPathSegments("mock/ufc/config").build().toString(); @@ -226,33 +228,33 @@ static long millis(final double seconds) { return Math.max(1L, Math.round(seconds * 1000)); } - interface CdnClient { - CdnResponse fetch( + interface UfcHttpClient { + UfcHttpResponse fetch( HttpUrl endpoint, Config config, Map extraHeaders, String etag) throws IOException; } - static final class CdnResponse { + static final class UfcHttpResponse { final int status; final String etag; final byte[] body; - CdnResponse(final int status, final String etag, final byte[] body) { + UfcHttpResponse(final int status, final String etag, final byte[] body) { this.status = status; this.etag = etag; this.body = body; } } - private static final class OkHttpCdnClient implements CdnClient { + private static final class OkHttpUfcHttpClient implements UfcHttpClient { private final OkHttpClient httpClient; - private OkHttpCdnClient(final OkHttpClient httpClient) { + private OkHttpUfcHttpClient(final OkHttpClient httpClient) { this.httpClient = httpClient; } @Override - public CdnResponse fetch( + public UfcHttpResponse fetch( final HttpUrl endpoint, final Config config, final Map extraHeaders, @@ -265,7 +267,7 @@ public CdnResponse fetch( final Request request = prepareRequest(endpoint, headers, config, true).get().build(); try (Response response = httpClient.newCall(request).execute()) { final ResponseBody responseBody = response.body(); - return new CdnResponse( + return new UfcHttpResponse( response.code(), response.header("ETag"), responseBody == null ? null : responseBody.bytes()); @@ -273,10 +275,10 @@ public CdnResponse fetch( } } - private static final class CdnThreadFactory implements ThreadFactory { + private static final class UfcHttpThreadFactory implements ThreadFactory { @Override public Thread newThread(final Runnable runnable) { - final Thread thread = new Thread(runnable, "dd-feature-flagging-cdn-poller"); + final Thread thread = new Thread(runnable, "dd-feature-flagging-http-poller"); thread.setDaemon(true); return thread; } diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/CdnConfigServiceTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/UfcHttpConfigServiceTest.java similarity index 88% rename from products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/CdnConfigServiceTest.java rename to products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/UfcHttpConfigServiceTest.java index 0bcdf1567c9..6bdc7b408a6 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/CdnConfigServiceTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/UfcHttpConfigServiceTest.java @@ -37,7 +37,7 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -class CdnConfigServiceTest { +class UfcHttpConfigServiceTest { @Mock private FeatureFlaggingGateway.ConfigListener listener; @@ -52,7 +52,7 @@ void appendsMockConfigPathWhenConfiguredUrlIsRoot() { final Config config = config("http://mock-cdn:8092", "datadoghq.com", ""); assertEquals( - "http://mock-cdn:8092/mock/ufc/config", CdnConfigService.endpoint(config).toString()); + "http://mock-cdn:8092/mock/ufc/config", UfcHttpConfigService.endpoint(config).toString()); } @Test @@ -62,7 +62,7 @@ void preservesConfiguredFullEndpointUrl() { assertEquals( "http://mock-cdn:8092/mock/ufc/config?fixture=valid", - CdnConfigService.endpoint(config).toString()); + UfcHttpConfigService.endpoint(config).toString()); } @Test @@ -71,13 +71,13 @@ void derivesDefaultEndpointFromSiteAndEnv() { assertEquals( "https://api.datad0g.com/api/v2/feature-flagging/config/server-distribution?dd_env=staging+env", - CdnConfigService.endpoint(config).toString()); + UfcHttpConfigService.endpoint(config).toString()); } @Test void appliesAcceptedUfcThroughGatewayAndSendsHeaders() throws Exception { final FakeClient client = new FakeClient(response(200, "etag-a", emptyConfig())); - final CdnConfigService service = service(client, extraHeaders()); + final UfcHttpConfigService service = service(client, extraHeaders()); FeatureFlaggingGateway.addConfigListener(listener); assertTrue(service.pollOnce()); @@ -93,7 +93,7 @@ void appliesAcceptedUfcThroughGatewayAndSendsHeaders() throws Exception { void usesEtagAndSkipsDispatchOnUnchangedConfig() throws Exception { final FakeClient client = new FakeClient(response(200, "etag-a", emptyConfig()), response(304, null, null)); - final CdnConfigService service = service(client, extraHeaders()); + final UfcHttpConfigService service = service(client, extraHeaders()); FeatureFlaggingGateway.addConfigListener(listener); assertTrue(service.pollOnce()); @@ -110,7 +110,7 @@ void keepsLastKnownGoodOnAuthFailureAndMalformedPayload() throws Exception { response(401, null, null), response(200, null, "{not-json}"), response(200, null, "{\"flags\":[]}")); - final CdnConfigService service = service(client, extraHeaders()); + final UfcHttpConfigService service = service(client, extraHeaders()); FeatureFlaggingGateway.addConfigListener(listener); assertFalse(service.pollOnce()); @@ -124,10 +124,10 @@ void keepsLastKnownGoodOnAuthFailureAndMalformedPayload() throws Exception { void retriesTimeoutBeforeApplyingConfig() throws Exception { final FakeClient client = new FakeClient( - new SocketTimeoutException("slow CDN"), - new SocketTimeoutException("slow CDN"), + new SocketTimeoutException("slow HTTP configuration source"), + new SocketTimeoutException("slow HTTP configuration source"), response(200, "etag-a", emptyConfig())); - final CdnConfigService service = service(client, extraHeaders()); + final UfcHttpConfigService service = service(client, extraHeaders()); FeatureFlaggingGateway.addConfigListener(listener); assertTrue(service.pollOnce()); @@ -139,7 +139,7 @@ void retriesTimeoutBeforeApplyingConfig() throws Exception { @Test void retriesServerErrorThenKeepsColdStateOnNotModified() throws Exception { final FakeClient client = new FakeClient(response(500, null, null), response(304, null, null)); - final CdnConfigService service = service(client, extraHeaders()); + final UfcHttpConfigService service = service(client, extraHeaders()); FeatureFlaggingGateway.addConfigListener(listener); assertTrue(service.pollOnce()); @@ -154,7 +154,7 @@ void rejectsOverlappingPolls() throws Exception { final CountDownLatch releaseRequest = new CountDownLatch(1); final FakeClient client = new FakeClient(response(200, "etag-a", emptyConfig())); client.block(requestStarted, releaseRequest); - final CdnConfigService service = service(client, extraHeaders()); + final UfcHttpConfigService service = service(client, extraHeaders()); final ExecutorService runner = Executors.newFixedThreadPool(2); try { @@ -175,7 +175,7 @@ void rejectsOverlappingPolls() throws Exception { @Test void closePreventsFurtherPolls() throws Exception { final FakeClient client = new FakeClient(response(200, "etag-a", emptyConfig())); - final CdnConfigService service = service(client, extraHeaders()); + final UfcHttpConfigService service = service(client, extraHeaders()); service.close(); @@ -183,10 +183,10 @@ void closePreventsFurtherPolls() throws Exception { assertEquals(0, client.calls.get()); } - private static CdnConfigService service( + private static UfcHttpConfigService service( final FakeClient client, final Map extraHeaders) { final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); - return new CdnConfigService( + return new UfcHttpConfigService( HttpUrl.get("http://localhost/mock/ufc/config"), config("http://localhost", "datadoghq.com", "", extraHeaders), 60_000, @@ -221,9 +221,9 @@ private static Map extraHeaders() { return headers; } - private static CdnConfigService.CdnResponse response( + private static UfcHttpConfigService.UfcHttpResponse response( final int status, final String etag, final String body) { - return new CdnConfigService.CdnResponse( + return new UfcHttpConfigService.UfcHttpResponse( status, etag, body == null ? null : body.getBytes(UTF_8)); } @@ -236,7 +236,7 @@ private static String emptyConfig() { + "}"; } - private static final class FakeClient implements CdnConfigService.CdnClient { + private static final class FakeClient implements UfcHttpConfigService.UfcHttpClient { private final AtomicInteger calls = new AtomicInteger(); private final List requests = new ArrayList<>(); private final BlockingQueue responses = new LinkedBlockingQueue<>(); @@ -255,7 +255,7 @@ private void block(final CountDownLatch requestStarted, final CountDownLatch rel } @Override - public CdnConfigService.CdnResponse fetch( + public UfcHttpConfigService.UfcHttpResponse fetch( final HttpUrl endpoint, final Config config, final Map extraHeaders, @@ -273,7 +273,7 @@ public CdnConfigService.CdnResponse fetch( if (response instanceof IOException) { throw (IOException) response; } - return (CdnConfigService.CdnResponse) response; + return (UfcHttpConfigService.UfcHttpResponse) response; } private static void await(final CountDownLatch latch) throws IOException { From ce756567e57a6a49c3ba8d0d838c5ca22f081ce3 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Wed, 8 Jul 2026 14:48:08 -0400 Subject: [PATCH 3/5] Auto-enable feature flagging for explicit CDN source --- .../java/datadog/trace/bootstrap/Agent.java | 26 ++++++++++++++++--- .../api/config/FeatureFlaggingConfig.java | 4 --- metadata/supported-configurations.json | 8 ------ 3 files changed, 23 insertions(+), 15 deletions(-) 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 ea9793ce86d..45f9ecb3acf 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 @@ -135,8 +135,7 @@ private enum AgentFeature { APP_LOGS_COLLECTION(GeneralConfig.APP_LOGS_COLLECTION_ENABLED, false), LLMOBS(LlmObsConfig.LLMOBS_ENABLED, false), LLMOBS_AGENTLESS(LlmObsConfig.LLMOBS_AGENTLESS_ENABLED, false), - FEATURE_FLAGGING(FeatureFlaggingConfig.FLAGGING_ENABLED, false), - DEPRECATED_FEATURE_FLAGGING(FeatureFlaggingConfig.FLAGGING_PROVIDER_ENABLED, false); + FEATURE_FLAGGING(FeatureFlaggingConfig.FLAGGING_PROVIDER_ENABLED, false); private final String configKey; private final String systemProp; @@ -285,7 +284,7 @@ public static void start( llmObsEnabled = isFeatureEnabled(AgentFeature.LLMOBS); featureFlaggingEnabled = isFeatureEnabled(AgentFeature.FEATURE_FLAGGING) - || isFeatureEnabled(AgentFeature.DEPRECATED_FEATURE_FLAGGING); + || isFeatureFlaggingCdnConfigurationSourceConfigured(); // setup writers when llmobs is enabled to accomodate apm and llmobs if (llmObsEnabled) { @@ -1634,6 +1633,27 @@ private static boolean isFeatureEnabled(AgentFeature feature) { } } + private static boolean isFeatureFlaggingCdnConfigurationSourceConfigured() { + final String configurationSource = + getConfiguredValue(FeatureFlaggingConfig.FLAGGING_CONFIGURATION_SOURCE); + return configurationSource != null && "cdn".equalsIgnoreCase(configurationSource.trim()); + } + + private static String getConfiguredValue(final String configKey) { + final String systemProp = propertyNameToSystemPropertyName(configKey); + String settingValue = getNullIfEmpty(SystemProperties.get(systemProp)); + if (settingValue == null) { + settingValue = getNullIfEmpty(getStableConfig(FLEET, configKey)); + } + if (settingValue == null) { + settingValue = getNullIfEmpty(ddGetEnv(systemProp)); + } + if (settingValue == null) { + settingValue = getNullIfEmpty(getStableConfig(LOCAL, configKey)); + } + return settingValue; + } + /** * @see datadog.trace.api.ProductActivation#fromString(String) */ 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 index 25faf9da12f..99a96797041 100644 --- 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 @@ -2,10 +2,6 @@ public class FeatureFlaggingConfig { - public static final String FLAGGING_ENABLED = "flagging.enabled"; - - /** Replaced by {@link #FLAGGING_ENABLED}. */ - @Deprecated public static final String FLAGGING_PROVIDER_ENABLED = "experimental.flagging.provider.enabled"; public static final String FLAGGING_CONFIGURATION_SOURCE = "flagging.configuration.source"; diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 4c99c583841..425948da222 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -1553,14 +1553,6 @@ "aliases": [] } ], - "DD_FLAGGING_ENABLED": [ - { - "version": "A", - "type": "boolean", - "default": "false", - "aliases": ["DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED"] - } - ], "DD_FORCE_CLEAR_TEXT_HTTP_FOR_INTAKE_CLIENT": [ { "version": "A", From 23c0b229998f3c8e59fd0fe8e97b541ddb717872 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Wed, 8 Jul 2026 21:01:51 -0400 Subject: [PATCH 4/5] Add Java agentless feature flag source --- .../java/datadog/trace/bootstrap/Agent.java | 39 +++++++++++--- .../datadog/trace/api/ConfigDefaults.java | 2 +- .../api/config/FeatureFlaggingConfig.java | 20 +++---- .../main/java/datadog/trace/api/Config.java | 21 ++++---- metadata/supported-configurations.json | 52 +++++++++++-------- .../featureflag/FeatureFlaggingSystem.java | 4 +- .../FeatureFlaggingSystemTest.java | 16 +++--- .../feature-flagging-api/README.md | 4 +- .../featureflag/UfcHttpConfigServiceTest.java | 9 ++-- 9 files changed, 102 insertions(+), 65 deletions(-) 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 45f9ecb3acf..98ec5e57718 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 @@ -72,6 +72,7 @@ import java.net.URL; import java.security.CodeSource; import java.util.EnumSet; +import java.util.Locale; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.PatternSyntaxException; @@ -135,7 +136,7 @@ private enum AgentFeature { APP_LOGS_COLLECTION(GeneralConfig.APP_LOGS_COLLECTION_ENABLED, false), LLMOBS(LlmObsConfig.LLMOBS_ENABLED, false), LLMOBS_AGENTLESS(LlmObsConfig.LLMOBS_AGENTLESS_ENABLED, false), - FEATURE_FLAGGING(FeatureFlaggingConfig.FLAGGING_PROVIDER_ENABLED, false); + FEATURE_FLAGGING(FeatureFlaggingConfig.FEATURE_FLAGS_ENABLED, false); private final String configKey; private final String systemProp; @@ -282,9 +283,7 @@ public static void start( agentlessLogSubmissionEnabled = isFeatureEnabled(AgentFeature.AGENTLESS_LOG_SUBMISSION); appLogsCollectionEnabled = isFeatureEnabled(AgentFeature.APP_LOGS_COLLECTION); llmObsEnabled = isFeatureEnabled(AgentFeature.LLMOBS); - featureFlaggingEnabled = - isFeatureEnabled(AgentFeature.FEATURE_FLAGGING) - || isFeatureFlaggingCdnConfigurationSourceConfigured(); + featureFlaggingEnabled = isFeatureFlaggingEnabled(); // setup writers when llmobs is enabled to accomodate apm and llmobs if (llmObsEnabled) { @@ -1633,10 +1632,36 @@ private static boolean isFeatureEnabled(AgentFeature feature) { } } - private static boolean isFeatureFlaggingCdnConfigurationSourceConfigured() { + private static boolean isFeatureFlaggingEnabled() { + final String enabled = getConfiguredValue(FeatureFlaggingConfig.FEATURE_FLAGS_ENABLED); + if ("false".equalsIgnoreCase(enabled) || "0".equals(enabled)) { + return false; + } + if ("true".equalsIgnoreCase(enabled) || "1".equals(enabled)) { + return true; + } + + final String oldEnabled = getConfiguredValue(FeatureFlaggingConfig.FLAGGING_PROVIDER_ENABLED); + if ("false".equalsIgnoreCase(oldEnabled) || "0".equals(oldEnabled)) { + return false; + } + if ("true".equalsIgnoreCase(oldEnabled) || "1".equals(oldEnabled)) { + return true; + } + + return isFeatureFlaggingConfigurationSourceAutoEnabled(); + } + + private static boolean isFeatureFlaggingConfigurationSourceAutoEnabled() { final String configurationSource = - getConfiguredValue(FeatureFlaggingConfig.FLAGGING_CONFIGURATION_SOURCE); - return configurationSource != null && "cdn".equalsIgnoreCase(configurationSource.trim()); + getConfiguredValue(FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE); + if (configurationSource == null) { + return true; + } + final String normalized = configurationSource.trim().toLowerCase(Locale.ROOT); + return "agentless".equals(normalized) + || "remote_config".equals(normalized) + || "offline".equals(normalized); } private static String getConfiguredValue(final String configKey) { diff --git a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java index 42293cdea28..84f1428fe94 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java @@ -49,7 +49,7 @@ public final class ConfigDefaults { static final boolean DEFAULT_INJECT_DATADOG_ATTRIBUTE = true; static final String DEFAULT_SITE = "datadoghq.com"; - public static final String DEFAULT_FLAGGING_CONFIGURATION_SOURCE = "cdn"; + public static final String DEFAULT_FLAGGING_CONFIGURATION_SOURCE = "agentless"; public static final double DEFAULT_FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS = 30.0D; public static final double DEFAULT_FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS = 2.0D; 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 index 99a96797041..97d7c105caa 100644 --- 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 @@ -2,17 +2,19 @@ public class FeatureFlaggingConfig { + public static final String FEATURE_FLAGS_ENABLED = "feature.flags.enabled"; public static final String FLAGGING_PROVIDER_ENABLED = "experimental.flagging.provider.enabled"; - public static final String FLAGGING_CONFIGURATION_SOURCE = "flagging.configuration.source"; - public static final String FLAGGING_CONFIGURATION_SOURCE_BASE_URL = - "flagging.configuration.source.base.url"; - public static final String FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS = - "flagging.configuration.source.poll.interval.seconds"; - public static final String FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS = - "flagging.configuration.source.request.timeout.seconds"; - public static final String FLAGGING_CONFIGURATION_SOURCE_EXTRA_HEADERS = - "flagging.configuration.source.extra.headers"; + public static final String FEATURE_FLAGS_CONFIGURATION_SOURCE = + "feature.flags.configuration.source"; + public static final String FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL = + "feature.flags.configuration.source.agentless.base.url"; + public static final String FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS = + "feature.flags.configuration.source.agentless.poll.interval.seconds"; + public static final String FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS = + "feature.flags.configuration.source.agentless.request.timeout.seconds"; + public static final String FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_EXTRA_HEADERS = + "feature.flags.configuration.source.agentless.extra.headers"; private FeatureFlaggingConfig() {} } 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 5a64fcf8aa6..f691a415f8e 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -364,11 +364,11 @@ 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.FLAGGING_CONFIGURATION_SOURCE; -import static datadog.trace.api.config.FeatureFlaggingConfig.FLAGGING_CONFIGURATION_SOURCE_BASE_URL; -import static datadog.trace.api.config.FeatureFlaggingConfig.FLAGGING_CONFIGURATION_SOURCE_EXTRA_HEADERS; -import static datadog.trace.api.config.FeatureFlaggingConfig.FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS; -import static datadog.trace.api.config.FeatureFlaggingConfig.FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS; +import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE; +import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL; +import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_EXTRA_HEADERS; +import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS; +import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS; 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; @@ -2852,19 +2852,20 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment()) flaggingConfigurationSource = normalizeFlaggingConfigurationSource( configProvider.getString( - FLAGGING_CONFIGURATION_SOURCE, DEFAULT_FLAGGING_CONFIGURATION_SOURCE)); + FEATURE_FLAGS_CONFIGURATION_SOURCE, DEFAULT_FLAGGING_CONFIGURATION_SOURCE)); flaggingConfigurationSourceBaseUrl = - configProvider.getStringNotEmpty(FLAGGING_CONFIGURATION_SOURCE_BASE_URL, null); + configProvider.getStringNotEmpty( + FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL, null); flaggingConfigurationSourcePollIntervalSeconds = configProvider.getDouble( - FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS, + FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS, DEFAULT_FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS); flaggingConfigurationSourceRequestTimeoutSeconds = configProvider.getDouble( - FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS, + FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS, DEFAULT_FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS); flaggingConfigurationSourceExtraHeaders = - configProvider.getMergedMap(FLAGGING_CONFIGURATION_SOURCE_EXTRA_HEADERS, '='); + configProvider.getMergedMap(FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_EXTRA_HEADERS, '='); dynamicInstrumentationEnabled = configProvider.getBoolean( diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 425948da222..19c2b2fdce4 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -1497,31 +1497,15 @@ "aliases": [] } ], - "DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED": [ - { - "version": "A", - "type": "boolean", - "default": "false", - "aliases": [] - } - ], - "DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED": [ - { - "version": "B", - "type": "boolean", - "default": "true", - "aliases": [] - } - ], - "DD_FLAGGING_CONFIGURATION_SOURCE": [ + "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE": [ { "version": "A", "type": "string", - "default": "cdn", + "default": "agentless", "aliases": [] } ], - "DD_FLAGGING_CONFIGURATION_SOURCE_BASE_URL": [ + "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL": [ { "version": "A", "type": "string", @@ -1529,7 +1513,7 @@ "aliases": [] } ], - "DD_FLAGGING_CONFIGURATION_SOURCE_EXTRA_HEADERS": [ + "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_EXTRA_HEADERS": [ { "version": "A", "type": "map", @@ -1537,7 +1521,7 @@ "aliases": [] } ], - "DD_FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS": [ + "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS": [ { "version": "A", "type": "decimal", @@ -1545,7 +1529,7 @@ "aliases": [] } ], - "DD_FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS": [ + "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS": [ { "version": "A", "type": "decimal", @@ -1553,6 +1537,30 @@ "aliases": [] } ], + "DD_FEATURE_FLAGS_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "true", + "aliases": ["DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED"] + } + ], + "DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": [] + } + ], + "DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED": [ + { + "version": "B", + "type": "boolean", + "default": "true", + "aliases": [] + } + ], "DD_FORCE_CLEAR_TEXT_HTTP_FOR_INTAKE_CLIENT": [ { "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 6128d43cd34..10fc0bdbd5f 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 @@ -8,7 +8,7 @@ public class FeatureFlaggingSystem { private static final Logger LOGGER = LoggerFactory.getLogger(FeatureFlaggingSystem.class); - private static final String SOURCE_CDN = "cdn"; + private static final String SOURCE_AGENTLESS = "agentless"; private static final String SOURCE_REMOTE_CONFIG = "remote_config"; private static final String SOURCE_OFFLINE = "offline"; @@ -41,7 +41,7 @@ static ConfigurationSourceService createConfigurationSourceService( throw new IllegalStateException("Feature Flagging system started without RC"); } return new RemoteConfigServiceImpl(sco, config); - } else if (SOURCE_CDN.equals(configurationSource)) { + } else if (SOURCE_AGENTLESS.equals(configurationSource)) { return new UfcHttpConfigService(config); } else if (SOURCE_OFFLINE.equals(configurationSource)) { LOGGER.debug( 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 c70ec92bafd..df8511aa3a7 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,6 +1,6 @@ package com.datadog.featureflag; -import static datadog.trace.api.config.FeatureFlaggingConfig.FLAGGING_CONFIGURATION_SOURCE; +import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE; import static datadog.trace.api.config.RemoteConfigConfig.REMOTE_CONFIGURATION_ENABLED; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; @@ -26,7 +26,7 @@ class FeatureFlaggingSystemTest { @Test - @WithConfig(key = FLAGGING_CONFIGURATION_SOURCE, value = "remote_config") + @WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "remote_config") @WithConfig(key = REMOTE_CONFIGURATION_ENABLED, value = "true") void testFeatureFlagSystemInitialization() { ConfigurationPoller poller = mock(ConfigurationPoller.class); @@ -53,7 +53,7 @@ void testFeatureFlagSystemInitialization() { } @Test - @WithConfig(key = FLAGGING_CONFIGURATION_SOURCE, value = "remote_config") + @WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "remote_config") @WithConfig(key = REMOTE_CONFIGURATION_ENABLED, value = "false") void testThatRemoteConfigIsRequired() { SharedCommunicationObjects sharedCommunicationObjects = mock(SharedCommunicationObjects.class); @@ -68,9 +68,9 @@ void testThatRemoteConfigIsRequired() { } @Test - @WithConfig(key = FLAGGING_CONFIGURATION_SOURCE, value = "cdn") + @WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "agentless") @WithConfig(key = REMOTE_CONFIGURATION_ENABLED, value = "false") - void cdnConfigurationSourceUsesHttpServiceWithoutRemoteConfig() { + void agentlessConfigurationSourceUsesHttpServiceWithoutRemoteConfig() { assertInstanceOf( UfcHttpConfigService.class, FeatureFlaggingSystem.createConfigurationSourceService( @@ -78,7 +78,7 @@ void cdnConfigurationSourceUsesHttpServiceWithoutRemoteConfig() { } @Test - @WithConfig(key = FLAGGING_CONFIGURATION_SOURCE, value = "remote_config") + @WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "remote_config") @WithConfig(key = REMOTE_CONFIGURATION_ENABLED, value = "true") void explicitRemoteConfigUsesRemoteConfigService() { SharedCommunicationObjects sharedCommunicationObjects = sharedCommunicationObjects(); @@ -92,7 +92,7 @@ void explicitRemoteConfigUsesRemoteConfigService() { } @Test - @WithConfig(key = FLAGGING_CONFIGURATION_SOURCE, value = "invalid") + @WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "invalid") void invalidConfigurationSourceFailsBeforeStartingNetworkSource() { assertThrows( IllegalArgumentException.class, @@ -102,7 +102,7 @@ void invalidConfigurationSourceFailsBeforeStartingNetworkSource() { } @Test - @WithConfig(key = FLAGGING_CONFIGURATION_SOURCE, value = "offline") + @WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "offline") void offlineConfigurationSourceDoesNotStartNetworkSource() { assertNull( FeatureFlaggingSystem.createConfigurationSourceService( diff --git a/products/feature-flagging/feature-flagging-api/README.md b/products/feature-flagging/feature-flagging-api/README.md index 47733020559..b55f9ad988c 100644 --- a/products/feature-flagging/feature-flagging-api/README.md +++ b/products/feature-flagging/feature-flagging-api/README.md @@ -85,5 +85,5 @@ OTEL_EXPORTER_OTLP_PROTOCOL=grpc ## Requirements - Java 11+ -- Datadog Agent with Remote Configuration enabled -- `DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=true` +- Feature Flags provider enabled with `DD_FEATURE_FLAGS_ENABLED=true`, or left unset + for the default `agentless` configuration source. diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/UfcHttpConfigServiceTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/UfcHttpConfigServiceTest.java index 6bdc7b408a6..d90f85b92f4 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/UfcHttpConfigServiceTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/UfcHttpConfigServiceTest.java @@ -49,19 +49,20 @@ void cleanup() { @Test void appendsMockConfigPathWhenConfiguredUrlIsRoot() { - final Config config = config("http://mock-cdn:8092", "datadoghq.com", ""); + final Config config = config("http://mock-backend:8092", "datadoghq.com", ""); assertEquals( - "http://mock-cdn:8092/mock/ufc/config", UfcHttpConfigService.endpoint(config).toString()); + "http://mock-backend:8092/mock/ufc/config", + UfcHttpConfigService.endpoint(config).toString()); } @Test void preservesConfiguredFullEndpointUrl() { final Config config = - config("http://mock-cdn:8092/mock/ufc/config?fixture=valid", "datadoghq.com", ""); + config("http://mock-backend:8092/mock/ufc/config?fixture=valid", "datadoghq.com", ""); assertEquals( - "http://mock-cdn:8092/mock/ufc/config?fixture=valid", + "http://mock-backend:8092/mock/ufc/config?fixture=valid", UfcHttpConfigService.endpoint(config).toString()); } From 82e864f68be1c8059cbbd3b27ccec66fb11078f8 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Wed, 8 Jul 2026 21:24:55 -0400 Subject: [PATCH 5/5] Address agentless configuration source feedback --- .../datadog/trace/api/ConfigDefaults.java | 8 +- .../main/java/datadog/trace/api/Config.java | 83 ++++++++++--------- .../featureflag/FeatureFlaggingSystem.java | 58 +++++++++---- .../featureflag/UfcHttpConfigService.java | 27 ++++-- .../featureflag/UfcHttpConfigServiceTest.java | 29 ++++--- 5 files changed, 124 insertions(+), 81 deletions(-) diff --git a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java index 84f1428fe94..c3c46201c69 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java @@ -49,9 +49,11 @@ public final class ConfigDefaults { static final boolean DEFAULT_INJECT_DATADOG_ATTRIBUTE = true; static final String DEFAULT_SITE = "datadoghq.com"; - public static final String DEFAULT_FLAGGING_CONFIGURATION_SOURCE = "agentless"; - public static final double DEFAULT_FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS = 30.0D; - public static final double DEFAULT_FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS = 2.0D; + public static final String DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE = "agentless"; + public static final double DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS = + 30.0D; + public static final double DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS = + 2.0D; static final boolean DEFAULT_CODE_ORIGIN_FOR_SPANS_INTERFACE_SUPPORT = false; static final int DEFAULT_CODE_ORIGIN_MAX_USER_FRAMES = 8; 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 f691a415f8e..82c64929f4f 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -86,9 +86,9 @@ import static datadog.trace.api.ConfigDefaults.DEFAULT_ELASTICSEARCH_BODY_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_ELASTICSEARCH_PARAMS_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_EXPERIMENTATAL_JEE_SPLIT_BY_DEPLOYMENT; -import static datadog.trace.api.ConfigDefaults.DEFAULT_FLAGGING_CONFIGURATION_SOURCE; -import static datadog.trace.api.ConfigDefaults.DEFAULT_FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS; -import static datadog.trace.api.ConfigDefaults.DEFAULT_FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS; +import static datadog.trace.api.ConfigDefaults.DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE; +import static datadog.trace.api.ConfigDefaults.DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS; +import static datadog.trace.api.ConfigDefaults.DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS; import static datadog.trace.api.ConfigDefaults.DEFAULT_GRPC_CLIENT_ERROR_STATUSES; import static datadog.trace.api.ConfigDefaults.DEFAULT_GRPC_SERVER_ERROR_STATUSES; import static datadog.trace.api.ConfigDefaults.DEFAULT_HEALTH_METRICS_ENABLED; @@ -1221,11 +1221,11 @@ public static String getHostName() { private final int remoteConfigMaxExtraServices; - private final String flaggingConfigurationSource; - private final String flaggingConfigurationSourceBaseUrl; - private final double flaggingConfigurationSourcePollIntervalSeconds; - private final double flaggingConfigurationSourceRequestTimeoutSeconds; - private final Map flaggingConfigurationSourceExtraHeaders; + private final String featureFlaggingConfigurationSource; + private final String featureFlaggingConfigurationSourceBaseUrl; + private final double featureFlaggingConfigurationSourcePollIntervalSeconds; + private final double featureFlaggingConfigurationSourceRequestTimeoutSeconds; + private final Map featureFlaggingConfigurationSourceExtraHeaders; private final boolean dbmInjectSqlBaseHash; private final String dbmPropagationMode; @@ -2849,23 +2849,24 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment()) configProvider.getInteger( REMOTE_CONFIG_MAX_EXTRA_SERVICES, DEFAULT_REMOTE_CONFIG_MAX_EXTRA_SERVICES); - flaggingConfigurationSource = - normalizeFlaggingConfigurationSource( + featureFlaggingConfigurationSource = + normalizeFeatureFlaggingConfigurationSource( configProvider.getString( - FEATURE_FLAGS_CONFIGURATION_SOURCE, DEFAULT_FLAGGING_CONFIGURATION_SOURCE)); - flaggingConfigurationSourceBaseUrl = + FEATURE_FLAGS_CONFIGURATION_SOURCE, DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE)); + featureFlaggingConfigurationSourceBaseUrl = configProvider.getStringNotEmpty( FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL, null); - flaggingConfigurationSourcePollIntervalSeconds = + featureFlaggingConfigurationSourcePollIntervalSeconds = configProvider.getDouble( FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS, - DEFAULT_FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS); - flaggingConfigurationSourceRequestTimeoutSeconds = + DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS); + featureFlaggingConfigurationSourceRequestTimeoutSeconds = configProvider.getDouble( FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS, - DEFAULT_FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS); - flaggingConfigurationSourceExtraHeaders = - configProvider.getMergedMap(FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_EXTRA_HEADERS, '='); + DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS); + featureFlaggingConfigurationSourceExtraHeaders = + configProvider.getMergedMap( + FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_EXTRA_HEADERS, '='); dynamicInstrumentationEnabled = configProvider.getBoolean( @@ -3779,12 +3780,12 @@ public boolean isInferredProxyPropagationEnabled() { return traceInferredProxyEnabled; } - private static String normalizeFlaggingConfigurationSource(final String source) { + private static String normalizeFeatureFlaggingConfigurationSource(final String source) { if (source == null) { - return DEFAULT_FLAGGING_CONFIGURATION_SOURCE; + return DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE; } final String normalized = source.trim().toLowerCase(Locale.ROOT); - return normalized.isEmpty() ? DEFAULT_FLAGGING_CONFIGURATION_SOURCE : normalized; + return normalized.isEmpty() ? DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE : normalized; } public boolean isBaggageExtract() { @@ -4694,24 +4695,24 @@ public int getRemoteConfigMaxExtraServices() { return remoteConfigMaxExtraServices; } - public String getFlaggingConfigurationSource() { - return flaggingConfigurationSource; + public String getFeatureFlaggingConfigurationSource() { + return featureFlaggingConfigurationSource; } - public String getFlaggingConfigurationSourceBaseUrl() { - return flaggingConfigurationSourceBaseUrl; + public String getFeatureFlaggingConfigurationSourceBaseUrl() { + return featureFlaggingConfigurationSourceBaseUrl; } - public double getFlaggingConfigurationSourcePollIntervalSeconds() { - return flaggingConfigurationSourcePollIntervalSeconds; + public double getFeatureFlaggingConfigurationSourcePollIntervalSeconds() { + return featureFlaggingConfigurationSourcePollIntervalSeconds; } - public double getFlaggingConfigurationSourceRequestTimeoutSeconds() { - return flaggingConfigurationSourceRequestTimeoutSeconds; + public double getFeatureFlaggingConfigurationSourceRequestTimeoutSeconds() { + return featureFlaggingConfigurationSourceRequestTimeoutSeconds; } - public Map getFlaggingConfigurationSourceExtraHeaders() { - return flaggingConfigurationSourceExtraHeaders; + public Map getFeatureFlaggingConfigurationSourceExtraHeaders() { + return featureFlaggingConfigurationSourceExtraHeaders; } public boolean isDynamicInstrumentationEnabled() { @@ -6546,16 +6547,16 @@ public String toString() { + remoteConfigMaxPayloadSize + ", remoteConfigIntegrityCheckEnabled=" + remoteConfigIntegrityCheckEnabled - + ", flaggingConfigurationSource=" - + flaggingConfigurationSource - + ", flaggingConfigurationSourceBaseUrl=" - + flaggingConfigurationSourceBaseUrl - + ", flaggingConfigurationSourcePollIntervalSeconds=" - + flaggingConfigurationSourcePollIntervalSeconds - + ", flaggingConfigurationSourceRequestTimeoutSeconds=" - + flaggingConfigurationSourceRequestTimeoutSeconds - + ", flaggingConfigurationSourceExtraHeaderNames=" - + flaggingConfigurationSourceExtraHeaders.keySet() + + ", featureFlaggingConfigurationSource=" + + featureFlaggingConfigurationSource + + ", featureFlaggingConfigurationSourceBaseUrl=" + + featureFlaggingConfigurationSourceBaseUrl + + ", featureFlaggingConfigurationSourcePollIntervalSeconds=" + + featureFlaggingConfigurationSourcePollIntervalSeconds + + ", featureFlaggingConfigurationSourceRequestTimeoutSeconds=" + + featureFlaggingConfigurationSourceRequestTimeoutSeconds + + ", featureFlaggingConfigurationSourceExtraHeaderNames=" + + featureFlaggingConfigurationSourceExtraHeaders.keySet() + ", debuggerEnabled=" + dynamicInstrumentationEnabled + ", debuggerUploadTimeout=" 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 10fc0bdbd5f..f2db8754a27 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 @@ -8,9 +8,6 @@ public class FeatureFlaggingSystem { private static final Logger LOGGER = LoggerFactory.getLogger(FeatureFlaggingSystem.class); - private static final String SOURCE_AGENTLESS = "agentless"; - private static final String SOURCE_REMOTE_CONFIG = "remote_config"; - private static final String SOURCE_OFFLINE = "offline"; private static volatile ConfigurationSourceService CONFIG_SERVICE; private static volatile ExposureWriter EXPOSURE_WRITER; @@ -34,22 +31,25 @@ public static void start(final SharedCommunicationObjects sco) { static ConfigurationSourceService createConfigurationSourceService( final SharedCommunicationObjects sco, final Config config) { - final String configurationSource = config.getFlaggingConfigurationSource(); + final ConfigurationSource configurationSource = + ConfigurationSource.from(config.getFeatureFlaggingConfigurationSource()); - if (SOURCE_REMOTE_CONFIG.equals(configurationSource)) { - if (!config.isRemoteConfigEnabled()) { - throw new IllegalStateException("Feature Flagging system started without RC"); - } - return new RemoteConfigServiceImpl(sco, config); - } else if (SOURCE_AGENTLESS.equals(configurationSource)) { - return new UfcHttpConfigService(config); - } else if (SOURCE_OFFLINE.equals(configurationSource)) { - LOGGER.debug( - "Feature Flagging offline configuration source selected; no config service started"); - return null; - } else { - throw new IllegalArgumentException( - "Unsupported Feature Flagging configuration source: " + configurationSource); + switch (configurationSource) { + case REMOTE_CONFIG: + if (!config.isRemoteConfigEnabled()) { + throw new IllegalStateException("Feature Flagging system started without RC"); + } + return new RemoteConfigServiceImpl(sco, config); + case AGENTLESS: + return new UfcHttpConfigService(config); + case OFFLINE: + LOGGER.debug( + "Feature Flagging offline configuration source selected; no config service started"); + return null; + default: + throw new IllegalArgumentException( + "Unsupported Feature Flagging configuration source: " + + config.getFeatureFlaggingConfigurationSource()); } } @@ -64,4 +64,26 @@ public static void stop() { } LOGGER.debug("Feature Flagging system stopped"); } + + private enum ConfigurationSource { + AGENTLESS("agentless"), + REMOTE_CONFIG("remote_config"), + OFFLINE("offline"); + + private final String value; + + ConfigurationSource(final String value) { + this.value = value; + } + + private static ConfigurationSource from(final String value) { + for (final ConfigurationSource source : values()) { + if (source.value.equals(value)) { + return source; + } + } + throw new IllegalArgumentException( + "Unsupported Feature Flagging configuration source: " + value); + } + } } diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/UfcHttpConfigService.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/UfcHttpConfigService.java index f7b42909d36..f0ecfa049ab 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/UfcHttpConfigService.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/UfcHttpConfigService.java @@ -30,7 +30,7 @@ final class UfcHttpConfigService implements ConfigurationSourceService { private static final Logger LOGGER = LoggerFactory.getLogger(UfcHttpConfigService.class); - private static final String SERVER_DISTRIBUTION_PATH = + private static final String DATADOG_API_SERVER_DISTRIBUTION_PATH = "/api/v2/feature-flagging/config/server-distribution"; private static final int MAX_ATTEMPTS = 3; @@ -53,10 +53,11 @@ private UfcHttpConfigService(final Config config, final HttpUrl endpoint) { this( endpoint, config, - millis(config.getFlaggingConfigurationSourcePollIntervalSeconds()), + millis(config.getFeatureFlaggingConfigurationSourcePollIntervalSeconds()), new OkHttpUfcHttpClient( OkHttpUtils.buildHttpClient( - endpoint, millis(config.getFlaggingConfigurationSourceRequestTimeoutSeconds()))), + endpoint, + millis(config.getFeatureFlaggingConfigurationSourceRequestTimeoutSeconds()))), Executors.newSingleThreadScheduledExecutor(new UfcHttpThreadFactory())); } @@ -69,7 +70,7 @@ endpoint, millis(config.getFlaggingConfigurationSourceRequestTimeoutSeconds()))) this.endpoint = endpoint; this.config = config; final Map configuredExtraHeaders = - config.getFlaggingConfigurationSourceExtraHeaders(); + config.getFeatureFlaggingConfigurationSourceExtraHeaders(); this.extraHeaders = configuredExtraHeaders == null ? Collections.emptyMap() : configuredExtraHeaders; this.pollIntervalMillis = pollIntervalMillis; @@ -175,10 +176,10 @@ private void updateEtag(final String nextEtag) { } static HttpUrl endpoint(final Config config) { - final String configuredBaseUrl = config.getFlaggingConfigurationSourceBaseUrl(); + final String configuredBaseUrl = config.getFeatureFlaggingConfigurationSourceBaseUrl(); final String endpoint = configuredBaseUrl == null - ? defaultEndpoint(config) + ? datadogApiServerDistributionEndpoint(config) : endpointFromConfiguredUrl(configuredBaseUrl); final HttpUrl parsed = HttpUrl.parse(endpoint); if (parsed == null) { @@ -195,7 +196,11 @@ private static String endpointFromConfiguredUrl(final String configuredUrl) { "Invalid Feature Flagging HTTP configuration source URL: " + configuredUrl); } if (isRootPath(parsed)) { - return parsed.newBuilder().addPathSegments("mock/ufc/config").build().toString(); + return parsed + .newBuilder() + .addPathSegments(datadogApiServerDistributionPath()) + .build() + .toString(); } return parsed.toString(); } @@ -204,11 +209,11 @@ private static boolean isRootPath(final HttpUrl url) { return "/".equals(url.encodedPath()) || url.encodedPath().isEmpty(); } - private static String defaultEndpoint(final Config config) { + private static String datadogApiServerDistributionEndpoint(final Config config) { final StringBuilder endpoint = new StringBuilder("https://api.") .append(config.getSite().toLowerCase(Locale.ROOT)) - .append(SERVER_DISTRIBUTION_PATH); + .append(DATADOG_API_SERVER_DISTRIBUTION_PATH); final String env = config.getEnv(); if (env != null && !env.isEmpty()) { endpoint.append("?dd_env=").append(urlEncode(env)); @@ -216,6 +221,10 @@ private static String defaultEndpoint(final Config config) { return endpoint.toString(); } + private static String datadogApiServerDistributionPath() { + return DATADOG_API_SERVER_DISTRIBUTION_PATH.substring(1); + } + private static String urlEncode(final String value) { try { return URLEncoder.encode(value, "UTF-8"); diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/UfcHttpConfigServiceTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/UfcHttpConfigServiceTest.java index d90f85b92f4..107b06bf53f 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/UfcHttpConfigServiceTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/UfcHttpConfigServiceTest.java @@ -48,26 +48,29 @@ void cleanup() { } @Test - void appendsMockConfigPathWhenConfiguredUrlIsRoot() { + void appendsDatadogApiServerDistributionPathWhenConfiguredUrlIsRoot() { final Config config = config("http://mock-backend:8092", "datadoghq.com", ""); assertEquals( - "http://mock-backend:8092/mock/ufc/config", + "http://mock-backend:8092/api/v2/feature-flagging/config/server-distribution", UfcHttpConfigService.endpoint(config).toString()); } @Test void preservesConfiguredFullEndpointUrl() { final Config config = - config("http://mock-backend:8092/mock/ufc/config?fixture=valid", "datadoghq.com", ""); + config( + "http://mock-backend:8092/api/v2/feature-flagging/config/server-distribution?fixture=valid", + "datadoghq.com", + ""); assertEquals( - "http://mock-backend:8092/mock/ufc/config?fixture=valid", + "http://mock-backend:8092/api/v2/feature-flagging/config/server-distribution?fixture=valid", UfcHttpConfigService.endpoint(config).toString()); } @Test - void derivesDefaultEndpointFromSiteAndEnv() { + void derivesDatadogApiServerDistributionEndpointFromSiteAndEnv() { final Config config = config(null, "datad0g.com", "staging env"); assertEquals( @@ -188,7 +191,7 @@ private static UfcHttpConfigService service( final FakeClient client, final Map extraHeaders) { final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); return new UfcHttpConfigService( - HttpUrl.get("http://localhost/mock/ufc/config"), + HttpUrl.get("http://localhost/api/v2/feature-flagging/config/server-distribution"), config("http://localhost", "datadoghq.com", "", extraHeaders), 60_000, client, @@ -205,10 +208,16 @@ private static Config config( final String env, final Map extraHeaders) { final Config config = mock(Config.class); - lenient().when(config.getFlaggingConfigurationSourceBaseUrl()).thenReturn(baseUrl); - lenient().when(config.getFlaggingConfigurationSourceExtraHeaders()).thenReturn(extraHeaders); - lenient().when(config.getFlaggingConfigurationSourcePollIntervalSeconds()).thenReturn(30.0D); - lenient().when(config.getFlaggingConfigurationSourceRequestTimeoutSeconds()).thenReturn(2.0D); + lenient().when(config.getFeatureFlaggingConfigurationSourceBaseUrl()).thenReturn(baseUrl); + lenient() + .when(config.getFeatureFlaggingConfigurationSourceExtraHeaders()) + .thenReturn(extraHeaders); + lenient() + .when(config.getFeatureFlaggingConfigurationSourcePollIntervalSeconds()) + .thenReturn(30.0D); + lenient() + .when(config.getFeatureFlaggingConfigurationSourceRequestTimeoutSeconds()) + .thenReturn(2.0D); lenient().when(config.getApiKey()).thenReturn("test-api-key"); lenient().when(config.getSite()).thenReturn(site); lenient().when(config.getEnv()).thenReturn(env);