Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ public final class ConfigDefaults {
static final boolean DEFAULT_INJECT_DATADOG_ATTRIBUTE = true;
static final String DEFAULT_SITE = "datadoghq.com";

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;
static final boolean DEFAULT_TRACE_ENABLED = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,13 @@
public class FeatureFlaggingConfig {

public static final String FLAGGING_PROVIDER_ENABLED = "experimental.flagging.provider.enabled";

public static final String FEATURE_FLAGS_CONFIGURATION_SOURCE =
"feature.flags.configuration.source";
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";

private FeatureFlaggingConfig() {}
}
49 changes: 49 additions & 0 deletions internal-api/src/main/java/datadog/trace/api/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -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_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;
Expand Down Expand Up @@ -361,6 +364,9 @@
import static datadog.trace.api.config.DebuggerConfig.THIRD_PARTY_EXCLUDES;
import static datadog.trace.api.config.DebuggerConfig.THIRD_PARTY_INCLUDES;
import static datadog.trace.api.config.DebuggerConfig.THIRD_PARTY_SHADING_IDENTIFIERS;
import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE;
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;
Expand Down Expand Up @@ -1213,6 +1219,10 @@ public static String getHostName() {

private final int remoteConfigMaxExtraServices;

private final String featureFlaggingConfigurationSource;
private final double featureFlaggingConfigurationSourcePollIntervalSeconds;
private final double featureFlaggingConfigurationSourceRequestTimeoutSeconds;

private final boolean dbmInjectSqlBaseHash;
private final String dbmPropagationMode;
private final boolean dbmTracePreparedStatements;
Expand Down Expand Up @@ -2835,6 +2845,19 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment())
configProvider.getInteger(
REMOTE_CONFIG_MAX_EXTRA_SERVICES, DEFAULT_REMOTE_CONFIG_MAX_EXTRA_SERVICES);

featureFlaggingConfigurationSource =
normalizeFeatureFlaggingConfigurationSource(
configProvider.getString(
FEATURE_FLAGS_CONFIGURATION_SOURCE, DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE));
featureFlaggingConfigurationSourcePollIntervalSeconds =
configProvider.getDouble(
FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS,
DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS);
featureFlaggingConfigurationSourceRequestTimeoutSeconds =
configProvider.getDouble(
FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS,
DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS);

dynamicInstrumentationEnabled =
configProvider.getBoolean(
DYNAMIC_INSTRUMENTATION_ENABLED, DEFAULT_DYNAMIC_INSTRUMENTATION_ENABLED);
Expand Down Expand Up @@ -3747,6 +3770,14 @@ public boolean isInferredProxyPropagationEnabled() {
return traceInferredProxyEnabled;
}

private static String normalizeFeatureFlaggingConfigurationSource(final String source) {
if (source == null) {
return DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE;
}
final String normalized = source.trim().toLowerCase(Locale.ROOT);
return normalized.isEmpty() ? DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE : normalized;
}

public boolean isBaggageExtract() {
return tracePropagationStylesToExtract.contains(TracePropagationStyle.BAGGAGE);
}
Expand Down Expand Up @@ -4654,6 +4685,18 @@ public int getRemoteConfigMaxExtraServices() {
return remoteConfigMaxExtraServices;
}

public String getFeatureFlaggingConfigurationSource() {
return featureFlaggingConfigurationSource;
}

public double getFeatureFlaggingConfigurationSourcePollIntervalSeconds() {
return featureFlaggingConfigurationSourcePollIntervalSeconds;
}

public double getFeatureFlaggingConfigurationSourceRequestTimeoutSeconds() {
return featureFlaggingConfigurationSourceRequestTimeoutSeconds;
}

public boolean isDynamicInstrumentationEnabled() {
return dynamicInstrumentationEnabled;
}
Expand Down Expand Up @@ -6486,6 +6529,12 @@ public String toString() {
+ remoteConfigMaxPayloadSize
+ ", remoteConfigIntegrityCheckEnabled="
+ remoteConfigIntegrityCheckEnabled
+ ", featureFlaggingConfigurationSource="
+ featureFlaggingConfigurationSource
+ ", featureFlaggingConfigurationSourcePollIntervalSeconds="
+ featureFlaggingConfigurationSourcePollIntervalSeconds
+ ", featureFlaggingConfigurationSourceRequestTimeoutSeconds="
+ featureFlaggingConfigurationSourceRequestTimeoutSeconds
+ ", debuggerEnabled="
+ dynamicInstrumentationEnabled
+ ", debuggerUploadTimeout="
Expand Down
24 changes: 24 additions & 0 deletions metadata/supported-configurations.json
Original file line number Diff line number Diff line change
Expand Up @@ -1497,6 +1497,30 @@
"aliases": []
}
],
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE": [
{
"version": "A",
"type": "string",
"default": "agentless",
"aliases": []
}
],
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS": [
{
"version": "A",
"type": "decimal",
"default": "30",
"aliases": []
}
],
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS": [
{
"version": "A",
"type": "decimal",
"default": "2",
"aliases": []
}
],
"DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED": [
{
"version": "A",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,50 @@ public class FeatureFlaggingSystem {

private static final Logger LOGGER = LoggerFactory.getLogger(FeatureFlaggingSystem.class);

private static volatile RemoteConfigService CONFIG_SERVICE;
private static volatile ConfigurationSourceService CONFIG_SERVICE;
private static volatile ExposureWriter EXPOSURE_WRITER;

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.isRemoteConfigEnabled()) {
throw new IllegalStateException("Feature Flagging system started without RC");
if (CONFIG_SERVICE != null) {
CONFIG_SERVICE.init();
}
CONFIG_SERVICE = new RemoteConfigServiceImpl(sco, config);
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 ConfigurationSource configurationSource =
ConfigurationSource.from(config.getFeatureFlaggingConfigurationSource());

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 AgentlessConfigurationSource(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());
}
}

public static void stop() {
if (EXPOSURE_WRITER != null) {
EXPOSURE_WRITER.close();
Expand All @@ -41,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);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.datadog.featureflag;

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;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
Expand All @@ -23,6 +26,8 @@
class FeatureFlaggingSystemTest {

@Test
@WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "remote_config")
@WithConfig(key = REMOTE_CONFIGURATION_ENABLED, value = "true")
void testFeatureFlagSystemInitialization() {
ConfigurationPoller poller = mock(ConfigurationPoller.class);
DDAgentFeaturesDiscovery discovery = mock(DDAgentFeaturesDiscovery.class);
Expand All @@ -48,6 +53,7 @@ void testFeatureFlagSystemInitialization() {
}

@Test
@WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "remote_config")
@WithConfig(key = REMOTE_CONFIGURATION_ENABLED, value = "false")
void testThatRemoteConfigIsRequired() {
SharedCommunicationObjects sharedCommunicationObjects = mock(SharedCommunicationObjects.class);
Expand All @@ -60,4 +66,57 @@ void testThatRemoteConfigIsRequired() {
FeatureFlaggingSystem.stop();
}
}

@Test
@WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "agentless")
@WithConfig(key = REMOTE_CONFIGURATION_ENABLED, value = "false")
void agentlessConfigurationSourceUsesHttpServiceWithoutRemoteConfig() {
assertInstanceOf(
AgentlessConfigurationSource.class,
FeatureFlaggingSystem.createConfigurationSourceService(
sharedCommunicationObjects(), Config.get()));
}

@Test
@WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "remote_config")
@WithConfig(key = REMOTE_CONFIGURATION_ENABLED, value = "true")
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 = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "invalid")
void invalidConfigurationSourceFailsBeforeStartingNetworkSource() {
assertThrows(
IllegalArgumentException.class,
() ->
FeatureFlaggingSystem.createConfigurationSourceService(
sharedCommunicationObjects(), Config.get()));
}

@Test
@WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "offline")
void offlineConfigurationSourceDoesNotStartNetworkSource() {
assertNull(
FeatureFlaggingSystem.createConfigurationSourceService(
sharedCommunicationObjects(), Config.get()));
}

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;
}
}
6 changes: 4 additions & 2 deletions products/feature-flagging/feature-flagging-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,7 @@ OTEL_EXPORTER_OTLP_PROTOCOL=grpc
## Requirements

- Java 11+
- Datadog Agent with Remote Configuration enabled
- `DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=true`
- `DD_FEATURE_FLAGS_CONFIGURATION_SOURCE=agentless` uses the Datadog agentless
backend. `remote_config` uses the existing Agent Remote Configuration path.
`offline` is reserved for startup-provided UFC bytes; until those bytes are
implemented, no network source starts and evaluations use defaults.
Loading
Loading