Skip to content

Commit eaa99ce

Browse files
committed
fix(feature-flags): align configuration source semantics
1 parent 3ae9cf0 commit eaa99ce

8 files changed

Lines changed: 259 additions & 40 deletions

File tree

dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ private enum AgentFeature {
136136
APP_LOGS_COLLECTION(GeneralConfig.APP_LOGS_COLLECTION_ENABLED, false),
137137
LLMOBS(LlmObsConfig.LLMOBS_ENABLED, false),
138138
LLMOBS_AGENTLESS(LlmObsConfig.LLMOBS_AGENTLESS_ENABLED, false),
139-
FEATURE_FLAGGING(FeatureFlaggingConfig.FLAGGING_PROVIDER_ENABLED, false);
139+
FEATURE_FLAGGING(FeatureFlaggingConfig.FEATURE_FLAGS_ENABLED, true);
140140

141141
private final String configKey;
142142
private final String systemProp;
@@ -283,7 +283,7 @@ public static void start(
283283
agentlessLogSubmissionEnabled = isFeatureEnabled(AgentFeature.AGENTLESS_LOG_SUBMISSION);
284284
appLogsCollectionEnabled = isFeatureEnabled(AgentFeature.APP_LOGS_COLLECTION);
285285
llmObsEnabled = isFeatureEnabled(AgentFeature.LLMOBS);
286-
featureFlaggingEnabled = isFeatureEnabled(AgentFeature.FEATURE_FLAGGING);
286+
featureFlaggingEnabled = isFeatureFlaggingEnabled();
287287

288288
// setup writers when llmobs is enabled to accomodate apm and llmobs
289289
if (llmObsEnabled) {
@@ -1756,6 +1756,45 @@ private static boolean isFeatureEnabled(AgentFeature feature) {
17561756
}
17571757
}
17581758

1759+
private static boolean isFeatureFlaggingEnabled() {
1760+
final Boolean providerEnabled =
1761+
featureFlaggingBooleanSetting(FeatureFlaggingConfig.FEATURE_FLAGS_ENABLED);
1762+
final String configurationSource =
1763+
featureFlaggingSetting(FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE);
1764+
final Boolean legacyProviderEnabled =
1765+
featureFlaggingBooleanSetting(FeatureFlaggingConfig.EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED);
1766+
1767+
return FeatureFlaggingConfig.resolveConfiguration(
1768+
providerEnabled, configurationSource, legacyProviderEnabled)
1769+
.isEnabled();
1770+
}
1771+
1772+
@SuppressFBWarnings(
1773+
value = "NP_BOOLEAN_RETURN_NULL",
1774+
justification = "A null value preserves the distinction between absent and explicitly false")
1775+
private static Boolean featureFlaggingBooleanSetting(final String configKey) {
1776+
final String value = featureFlaggingSetting(configKey);
1777+
if (value == null) {
1778+
return null;
1779+
}
1780+
return Boolean.parseBoolean(value) || "1".equals(value);
1781+
}
1782+
1783+
private static String featureFlaggingSetting(final String configKey) {
1784+
final String systemProperty = propertyNameToSystemPropertyName(configKey);
1785+
String value = SystemProperties.get(systemProperty);
1786+
if (value == null) {
1787+
value = getStableConfig(FLEET, configKey);
1788+
}
1789+
if (value == null) {
1790+
value = ddGetEnv(systemProperty);
1791+
}
1792+
if (value == null) {
1793+
value = getStableConfig(LOCAL, configKey);
1794+
}
1795+
return value;
1796+
}
1797+
17591798
/**
17601799
* @see datadog.trace.api.ProductActivation#fromString(String)
17611800
*/

dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public final class ConfigDefaults {
5050
static final String DEFAULT_SITE = "datadoghq.com";
5151
static final String DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE = "agentless";
5252
static final int DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS = 30;
53-
static final int DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS = 2;
53+
static final int DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS = 5;
5454

5555
static final boolean DEFAULT_CODE_ORIGIN_FOR_SPANS_INTERFACE_SUPPORT = false;
5656
static final int DEFAULT_CODE_ORIGIN_MAX_USER_FRAMES = 8;

internal-api/src/main/java/datadog/trace/api/Config.java

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@
8787
import static datadog.trace.api.ConfigDefaults.DEFAULT_ELASTICSEARCH_BODY_ENABLED;
8888
import static datadog.trace.api.ConfigDefaults.DEFAULT_ELASTICSEARCH_PARAMS_ENABLED;
8989
import static datadog.trace.api.ConfigDefaults.DEFAULT_EXPERIMENTATAL_JEE_SPLIT_BY_DEPLOYMENT;
90-
import static datadog.trace.api.ConfigDefaults.DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE;
9190
import static datadog.trace.api.ConfigDefaults.DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS;
9291
import static datadog.trace.api.ConfigDefaults.DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS;
9392
import static datadog.trace.api.ConfigDefaults.DEFAULT_GRPC_CLIENT_ERROR_STATUSES;
@@ -729,10 +728,14 @@
729728
import static datadog.trace.api.config.TracerConfig.WRITER_BAGGAGE_INJECT;
730729
import static datadog.trace.api.config.TracerConfig.WRITER_LINKS_INJECT;
731730
import static datadog.trace.api.config.TracerConfig.WRITER_TYPE;
731+
import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED;
732732
import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE;
733733
import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL;
734734
import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS;
735735
import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS;
736+
import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.FEATURE_FLAGS_ENABLED;
737+
import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.isSupportedConfigurationSource;
738+
import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.resolveConfiguration;
736739
import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY;
737740
import static datadog.trace.bootstrap.instrumentation.api.WriterConstants.OTLP_WRITER_TYPE;
738741
import static datadog.trace.util.CollectionUtils.tryMakeImmutableList;
@@ -749,6 +752,7 @@
749752
import datadog.trace.api.config.OtlpConfig;
750753
import datadog.trace.api.config.ProfilingConfig;
751754
import datadog.trace.api.config.TracerConfig;
755+
import datadog.trace.api.featureflag.config.FeatureFlaggingConfig;
752756
import datadog.trace.api.iast.IastContext;
753757
import datadog.trace.api.iast.IastDetectionMode;
754758
import datadog.trace.api.iast.telemetry.Verbosity;
@@ -1227,6 +1231,7 @@ public static String getHostName() {
12271231

12281232
private final int remoteConfigMaxExtraServices;
12291233

1234+
private final boolean featureFlaggingProviderEnabled;
12301235
private final String featureFlaggingConfigurationSource;
12311236
private final String featureFlaggingConfigurationSourceAgentlessBaseUrl;
12321237
private final int featureFlaggingConfigurationSourcePollIntervalSeconds;
@@ -2862,10 +2867,33 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment())
28622867
configProvider.getInteger(
28632868
REMOTE_CONFIG_MAX_EXTRA_SERVICES, DEFAULT_REMOTE_CONFIG_MAX_EXTRA_SERVICES);
28642869

2865-
featureFlaggingConfigurationSource =
2866-
normalizeFeatureFlaggingConfigurationSource(
2867-
configProvider.getString(
2868-
FEATURE_FLAGS_CONFIGURATION_SOURCE, DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE));
2870+
final Boolean configuredFeatureFlaggingProviderEnabled =
2871+
configProvider.getBoolean(FEATURE_FLAGS_ENABLED);
2872+
final Boolean legacyFeatureFlaggingProviderEnabled =
2873+
configProvider.getBoolean(EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED);
2874+
final String configuredFeatureFlaggingConfigurationSource =
2875+
configProvider.isSet(FEATURE_FLAGS_CONFIGURATION_SOURCE)
2876+
? configProvider.getString(FEATURE_FLAGS_CONFIGURATION_SOURCE)
2877+
: null;
2878+
final FeatureFlaggingConfig.Resolution resolvedFeatureFlaggingConfiguration =
2879+
resolveConfiguration(
2880+
configuredFeatureFlaggingProviderEnabled,
2881+
configuredFeatureFlaggingConfigurationSource,
2882+
legacyFeatureFlaggingProviderEnabled);
2883+
if (legacyFeatureFlaggingProviderEnabled != null) {
2884+
log.warn(
2885+
"Setting {} is deprecated. Use {} and {} instead.",
2886+
EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED,
2887+
FEATURE_FLAGS_ENABLED,
2888+
FEATURE_FLAGS_CONFIGURATION_SOURCE);
2889+
}
2890+
if (!isSupportedConfigurationSource(configuredFeatureFlaggingConfigurationSource)) {
2891+
log.warn(
2892+
"Unsupported Feature Flagging configuration source; provider disabled: '{}'",
2893+
resolvedFeatureFlaggingConfiguration.getSource());
2894+
}
2895+
featureFlaggingProviderEnabled = resolvedFeatureFlaggingConfiguration.isEnabled();
2896+
featureFlaggingConfigurationSource = resolvedFeatureFlaggingConfiguration.getSource();
28692897
featureFlaggingConfigurationSourceAgentlessBaseUrl =
28702898
configProvider.getStringNotEmpty(
28712899
FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL, null);
@@ -3808,28 +3836,6 @@ public boolean isInferredProxyPropagationEnabled() {
38083836
return traceInferredProxyEnabled;
38093837
}
38103838

3811-
private static String normalizeFeatureFlaggingConfigurationSource(final String source) {
3812-
if (source == null) {
3813-
return DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE;
3814-
}
3815-
final String normalized = source.trim().toLowerCase(Locale.ROOT);
3816-
if (normalized.isEmpty()) {
3817-
return DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE;
3818-
}
3819-
switch (normalized) {
3820-
case "agentless":
3821-
case "remote_config":
3822-
case "offline":
3823-
return normalized;
3824-
default:
3825-
log.warn(
3826-
"Unsupported Feature Flagging configuration source: {}. Defaulting to {}",
3827-
source,
3828-
DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE);
3829-
return DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE;
3830-
}
3831-
}
3832-
38333839
public boolean isBaggageExtract() {
38343840
return tracePropagationStylesToExtract.contains(TracePropagationStyle.BAGGAGE);
38353841
}
@@ -4751,6 +4757,10 @@ public int getRemoteConfigMaxExtraServices() {
47514757
return remoteConfigMaxExtraServices;
47524758
}
47534759

4760+
public boolean isFeatureFlaggingProviderEnabled() {
4761+
return featureFlaggingProviderEnabled;
4762+
}
4763+
47544764
public String getFeatureFlaggingConfigurationSource() {
47554765
return featureFlaggingConfigurationSource;
47564766
}
@@ -6624,6 +6634,8 @@ public String toString() {
66246634
+ remoteConfigMaxPayloadSize
66256635
+ ", remoteConfigIntegrityCheckEnabled="
66266636
+ remoteConfigIntegrityCheckEnabled
6637+
+ ", featureFlaggingProviderEnabled="
6638+
+ featureFlaggingProviderEnabled
66276639
+ ", featureFlaggingConfigurationSource="
66286640
+ featureFlaggingConfigurationSource
66296641
+ ", featureFlaggingConfigurationSourceAgentlessBaseUrl="

internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,11 @@ import static datadog.trace.api.config.GeneralConfig.TAGS
5757
import static datadog.trace.api.config.GeneralConfig.TRACER_METRICS_IGNORED_RESOURCES
5858
import static datadog.trace.api.config.GeneralConfig.TRACE_OTEL_SEMANTICS_ENABLED
5959
import static datadog.trace.api.config.GeneralConfig.VERSION
60+
import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED
6061
import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE
6162
import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS
6263
import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS
64+
import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.FEATURE_FLAGS_ENABLED
6365
import static datadog.trace.api.config.JmxFetchConfig.JMX_FETCH_CHECK_PERIOD
6466
import static datadog.trace.api.config.JmxFetchConfig.JMX_FETCH_ENABLED
6567
import static datadog.trace.api.config.JmxFetchConfig.JMX_FETCH_METRICS_CONFIGS
@@ -3517,7 +3519,42 @@ class ConfigTest extends DDSpecification {
35173519
"" | "agentless"
35183520
" " | "agentless"
35193521
" ReMoTe_ConFiG " | "remote_config"
3520-
"not-a-real-source" | "agentless"
3522+
"not-a-real-source" | "not-a-real-source"
3523+
" OFFLINE " | "offline"
3524+
}
3525+
3526+
def "feature flag configuration applies migration precedence"() {
3527+
setup:
3528+
Properties properties = new Properties()
3529+
if (providerEnabled != null) {
3530+
properties.setProperty(FEATURE_FLAGS_ENABLED, providerEnabled.toString())
3531+
}
3532+
if (source != null) {
3533+
properties.setProperty(FEATURE_FLAGS_CONFIGURATION_SOURCE, source)
3534+
}
3535+
if (legacyProviderEnabled != null) {
3536+
properties.setProperty(EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED, legacyProviderEnabled.toString())
3537+
}
3538+
3539+
when:
3540+
def config = new Config(ConfigProvider.withPropertiesOverride(properties))
3541+
3542+
then:
3543+
config.featureFlaggingProviderEnabled == expectedEnabled
3544+
config.featureFlaggingConfigurationSource == expectedSource
3545+
3546+
where:
3547+
providerEnabled | source | legacyProviderEnabled | expectedEnabled | expectedSource
3548+
null | null | null | true | "agentless"
3549+
true | null | null | true | "agentless"
3550+
null | null | true | true | "remote_config"
3551+
null | null | false | false | null
3552+
null | "agentless" | true | true | "agentless"
3553+
null | "remote_config" | false | true | "remote_config"
3554+
false | "agentless" | true | false | "agentless"
3555+
true | null | false | false | null
3556+
null | "not-a-source" | null | false | "not-a-source"
3557+
null | "offline" | true | false | "offline"
35213558
}
35223559
35233560
def "agentless feature flag timing falls back for non-positive values"() {

metadata/supported-configurations.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1513,6 +1513,14 @@
15131513
"aliases": []
15141514
}
15151515
],
1516+
"DD_FEATURE_FLAGS_ENABLED": [
1517+
{
1518+
"version": "A",
1519+
"type": "boolean",
1520+
"default": "true",
1521+
"aliases": []
1522+
}
1523+
],
15161524
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE": [
15171525
{
15181526
"version": "A",
@@ -1541,7 +1549,7 @@
15411549
{
15421550
"version": "A",
15431551
"type": "int",
1544-
"default": "2",
1552+
"default": "5",
15451553
"aliases": []
15461554
}
15471555
],
@@ -12002,5 +12010,7 @@
1200212010
}
1200312011
]
1200412012
},
12005-
"deprecations": {}
12013+
"deprecations": {
12014+
"DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED": "DD_FEATURE_FLAGS_ENABLED"
12015+
}
1200612016
}

products/feature-flagging/feature-flagging-config/build.gradle.kts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ plugins {
44

55
apply(from = "$rootDir/gradle/java.gradle")
66

7-
description = "Feature flagging configuration keys (compile-time constants)"
7+
description = "Feature flagging configuration keys and source resolution"
88

9-
extra["excludedClassesCoverage"] = listOf(
10-
// Constants-only holder — no executable logic to cover.
11-
"datadog.trace.api.featureflag.config.FeatureFlaggingConfig",
12-
)
9+
dependencies {
10+
testImplementation(libs.bundles.junit5)
11+
}

products/feature-flagging/feature-flagging-config/src/main/java/datadog/trace/api/featureflag/config/FeatureFlaggingConfig.java

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,22 @@
22

33
public class FeatureFlaggingConfig {
44

5-
public static final String FLAGGING_PROVIDER_ENABLED = "experimental.flagging.provider.enabled";
5+
public static final String CONFIGURATION_SOURCE_AGENTLESS = "agentless";
6+
public static final String CONFIGURATION_SOURCE_REMOTE_CONFIG = "remote_config";
7+
8+
private static final Resolution DISABLED_RESOLUTION = new Resolution(false, null);
9+
private static final Resolution AGENTLESS_CONFIGURATION =
10+
new Resolution(true, CONFIGURATION_SOURCE_AGENTLESS);
11+
private static final Resolution REMOTE_CONFIG_CONFIGURATION =
12+
new Resolution(true, CONFIGURATION_SOURCE_REMOTE_CONFIG);
13+
14+
public static final String FEATURE_FLAGS_ENABLED = "feature.flags.enabled";
15+
public static final String EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED =
16+
"experimental.flagging.provider.enabled";
617

718
/**
819
* Opt-in gate for APM span enrichment with feature-flag evaluation metadata. DISTINCT from {@link
9-
* #FLAGGING_PROVIDER_ENABLED} and OFF by default — enabling the provider does not enable span
20+
* #FEATURE_FLAGS_ENABLED} and OFF by default — enabling the provider does not enable span
1021
* enrichment.
1122
*/
1223
public static final String EXPERIMENTAL_SPAN_ENRICHMENT_ENABLED =
@@ -21,5 +32,61 @@ public class FeatureFlaggingConfig {
2132
public static final String FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS =
2233
"feature.flags.configuration.source.agentless.request.timeout.seconds";
2334

35+
public static Resolution resolveConfiguration(
36+
final Boolean providerEnabled,
37+
final String explicitSource,
38+
final Boolean legacyProviderEnabled) {
39+
final String normalizedSource = normalizeConfigurationSource(explicitSource);
40+
if (Boolean.FALSE.equals(providerEnabled)) {
41+
return new Resolution(false, normalizedSource);
42+
}
43+
if (normalizedSource != null) {
44+
if (CONFIGURATION_SOURCE_AGENTLESS.equals(normalizedSource)) {
45+
return AGENTLESS_CONFIGURATION;
46+
}
47+
if (CONFIGURATION_SOURCE_REMOTE_CONFIG.equals(normalizedSource)) {
48+
return REMOTE_CONFIG_CONFIGURATION;
49+
}
50+
return new Resolution(false, normalizedSource);
51+
}
52+
if (legacyProviderEnabled != null) {
53+
return legacyProviderEnabled ? REMOTE_CONFIG_CONFIGURATION : DISABLED_RESOLUTION;
54+
}
55+
return AGENTLESS_CONFIGURATION;
56+
}
57+
58+
public static boolean isSupportedConfigurationSource(final String source) {
59+
final String normalizedSource = normalizeConfigurationSource(source);
60+
return normalizedSource == null
61+
|| CONFIGURATION_SOURCE_AGENTLESS.equals(normalizedSource)
62+
|| CONFIGURATION_SOURCE_REMOTE_CONFIG.equals(normalizedSource);
63+
}
64+
65+
private static String normalizeConfigurationSource(final String source) {
66+
if (source == null) {
67+
return null;
68+
}
69+
final String normalized = source.trim().toLowerCase(java.util.Locale.ROOT);
70+
return normalized.isEmpty() ? null : normalized;
71+
}
72+
73+
public static final class Resolution {
74+
private final boolean enabled;
75+
private final String source;
76+
77+
private Resolution(final boolean enabled, final String source) {
78+
this.enabled = enabled;
79+
this.source = source;
80+
}
81+
82+
public boolean isEnabled() {
83+
return enabled;
84+
}
85+
86+
public String getSource() {
87+
return source;
88+
}
89+
}
90+
2491
private FeatureFlaggingConfig() {}
2592
}

0 commit comments

Comments
 (0)