Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b8bbd26
Define Feature Flagging configuration source contract
leoromanovsky Jul 11, 2026
4baefdb
Add Datadog-managed agentless UFC polling
leoromanovsky Jul 11, 2026
d378350
Support custom agentless UFC endpoints
leoromanovsky Jul 11, 2026
f9e70e6
Select the Feature Flagging configuration source
leoromanovsky Jul 11, 2026
17a02ad
Warn on agentless authentication failures
leoromanovsky Jul 13, 2026
0f02a2a
Merge remote-tracking branch 'origin/master' into rewrite/java-agentl…
leoromanovsky Jul 15, 2026
5057539
fix(ffe): handle nullable agentless response bodies
leoromanovsky Jul 15, 2026
7aa2181
Merge branch 'master' into leo.romanovsky/ffl-2693-java-agentless-con…
leoromanovsky Jul 15, 2026
ffbf970
Address agentless poller review feedback
leoromanovsky Jul 15, 2026
3b9b97c
Fix feature flag config test imports
leoromanovsky Jul 15, 2026
eaaea27
Address additional agentless source review feedback
leoromanovsky Jul 15, 2026
76917d6
Support the UFC CDN response contract
leoromanovsky Jul 15, 2026
2cee183
Harden agentless configuration responses
leoromanovsky Jul 17, 2026
ebc28ca
Separate UFC transport parsers
leoromanovsky Jul 17, 2026
0984370
Fix feature flagging test formatting
leoromanovsky Jul 17, 2026
c146f09
Fix feature flagging parser coverage
leoromanovsky Jul 17, 2026
488b477
Address final feature flagging review feedback
leoromanovsky Jul 17, 2026
c0575ac
Merge branch 'master' into leo.romanovsky/ffl-2693-java-agentless-con…
vjfridge Jul 21, 2026
3ae9cf0
Merge branch 'master' into leo.romanovsky/ffl-2693-java-agentless-con…
leoromanovsky Jul 24, 2026
eaa99ce
fix(feature-flags): align configuration source semantics
leoromanovsky Jul 24, 2026
2c0ee6c
feat(feature-flags): delay agentless polling until provider use
leoromanovsky Jul 24, 2026
bd0e347
feat(communication): support mapped retried HTTP calls
leoromanovsky Jul 24, 2026
cfaca97
fix(feature-flags): align agentless HTTP behavior
leoromanovsky Jul 24, 2026
6df48d1
fix(feature-flags): complete initial poll during activation
leoromanovsky Jul 24, 2026
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 @@ -54,7 +54,13 @@ public class HttpRetryPolicy implements AutoCloseable {
private final double delayFactor;
private final boolean suppressInterrupts;

private HttpRetryPolicy(
/**
* Creates a retry policy.
*
* <p>Protected so products with a stricter cross-SDK retry contract can reuse the shared HTTP
* retry loop while supplying their own response, exception, and backoff rules.
*/
protected HttpRetryPolicy(
int retriesLeft, long delay, double delayFactor, boolean suppressInterrupts) {
this.retriesLeft = retriesLeft;
this.delay = delay;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import okhttp3.Call;
import okhttp3.ConnectionPool;
import okhttp3.ConnectionSpec;
import okhttp3.Credentials;
Expand Down Expand Up @@ -405,19 +406,39 @@ public void writeTo(BufferedSink sink) throws IOException {
public static Response sendWithRetries(
OkHttpClient httpClient, HttpRetryPolicy.Factory retryPolicyFactory, Request request)
throws IOException {
return sendWithRetries((Call.Factory) httpClient, retryPolicyFactory, request);
}

public static Response sendWithRetries(
Call.Factory callFactory, HttpRetryPolicy.Factory retryPolicyFactory, Request request)
throws IOException {
return sendWithRetries(callFactory, retryPolicyFactory, request, response -> response);
}

public static <T> T sendWithRetries(
Call.Factory callFactory,
HttpRetryPolicy.Factory retryPolicyFactory,
Request request,
ResponseMapper<T> responseMapper)
throws IOException {
try (HttpRetryPolicy retryPolicy = retryPolicyFactory.create()) {
while (true) {
Response response = null;
try {
Response response = httpClient.newCall(request).execute();
response = callFactory.newCall(request).execute();
if (response.isSuccessful()) {
return response;
return responseMapper.map(response);
}
if (!retryPolicy.shouldRetry(response)) {
return response;
return responseMapper.map(response);
} else {
closeQuietly(response);
response = null;
}
} catch (Exception ex) {
if (response != null) {
closeQuietly(response);
}
if (!retryPolicy.shouldRetry(ex)) {
throw ex;
}
Expand All @@ -428,6 +449,11 @@ public static Response sendWithRetries(
}
}

@FunctionalInterface
public interface ResponseMapper<T> {
T map(Response response) throws IOException;
}

private static void closeQuietly(Response response) {
try {
response.close();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package datadog.communication.http;

import static datadog.communication.http.OkHttpUtils.sendWithRetries;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.net.ConnectException;
import java.util.concurrent.atomic.AtomicInteger;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.ResponseBody;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.jupiter.api.Test;

class OkHttpUtilsRetryTest {

@Test
void retriesResponseMappingFailureThroughCallFactory() throws Exception {
final MockWebServer server = new MockWebServer();
final OkHttpClient client = new OkHttpClient();
final AtomicInteger mappingAttempts = new AtomicInteger();
server.enqueue(new MockResponse().setBody("first"));
server.enqueue(new MockResponse().setBody("second"));
server.start();

try {
final Request request = new Request.Builder().url(server.url("/configuration")).build();

final String body =
sendWithRetries(
(Call.Factory) client,
new HttpRetryPolicy.Factory(1, 0, 1),
request,
response -> {
try (ResponseBody responseBody = response.body()) {
if (mappingAttempts.getAndIncrement() == 0) {
throw new ConnectException("response body could not be mapped");
}
return responseBody.string();
}
});

assertEquals("second", body);
assertEquals(2, mappingAttempts.get());
assertEquals(2, server.getRequestCount());
} finally {
client.dispatcher().executorService().shutdownNow();
client.connectionPool().evictAll();
server.shutdown();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,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, true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this line be removed now that AgentFeature.FEATURE_FLAGGING is no longer referenced on line 286?


private final String configKey;
private final String systemProp;
Expand Down Expand Up @@ -283,7 +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);
featureFlaggingEnabled = isFeatureFlaggingEnabled();

// setup writers when llmobs is enabled to accomodate apm and llmobs
if (llmObsEnabled) {
Expand Down Expand Up @@ -531,6 +531,9 @@ public static void shutdown(final boolean sync) {
if (flareEnabled) {
stopFlarePoller();
}
if (featureFlaggingEnabled) {
shutdownFeatureFlagging(AGENT_CLASSLOADER);
}

if (agentlessLogSubmissionEnabled) {
shutdownLogsIntake();
Expand Down Expand Up @@ -1287,6 +1290,20 @@ private static void maybeStartFeatureFlagging(final Class<?> scoClass, final Obj
}
}

static void shutdownFeatureFlagging(final ClassLoader agentClassLoader) {
if (agentClassLoader == null) {
return;
}
try {
final Class<?> ffSysClass =
agentClassLoader.loadClass("com.datadog.featureflag.FeatureFlaggingSystem");
final Method stopMethod = ffSysClass.getMethod("stop");
stopMethod.invoke(null);
} catch (final Throwable e) {
log.warn("Unable to stop Feature Flagging subsystem", e);
}
}

private static void maybeInstallLogsIntake(Class<?> scoClass, Object sco) {
if (agentlessLogSubmissionEnabled || appLogsCollectionEnabled) {
StaticEventLogger.begin("Logs Intake");
Expand Down Expand Up @@ -1739,6 +1756,45 @@ private static boolean isFeatureEnabled(AgentFeature feature) {
}
}

private static boolean isFeatureFlaggingEnabled() {
final Boolean providerEnabled =
featureFlaggingBooleanSetting(FeatureFlaggingConfig.FEATURE_FLAGS_ENABLED);
final String configurationSource =
featureFlaggingSetting(FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE);
final Boolean legacyProviderEnabled =
featureFlaggingBooleanSetting(FeatureFlaggingConfig.EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED);

return FeatureFlaggingConfig.resolveConfiguration(
providerEnabled, configurationSource, legacyProviderEnabled)
.isEnabled();
}

@SuppressFBWarnings(
value = "NP_BOOLEAN_RETURN_NULL",
justification = "A null value preserves the distinction between absent and explicitly false")
private static Boolean featureFlaggingBooleanSetting(final String configKey) {
final String value = featureFlaggingSetting(configKey);
if (value == null) {
return null;
}
return Boolean.parseBoolean(value) || "1".equals(value);
}

private static String featureFlaggingSetting(final String configKey) {
final String systemProperty = propertyNameToSystemPropertyName(configKey);
String value = SystemProperties.get(systemProperty);
if (value == null) {
value = getStableConfig(FLEET, configKey);
}
if (value == null) {
value = ddGetEnv(systemProperty);
}
if (value == null) {
value = getStableConfig(LOCAL, configKey);
}
return value;
}

/**
* @see datadog.trace.api.ProductActivation#fromString(String)
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package datadog.trace.bootstrap;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class AgentFeatureFlaggingLifecycleTest {

@BeforeEach
void reset() {
FakeFeatureFlaggingSystem.stopCalls.set(0);
}

@Test
void shutdownInvokesFeatureFlaggingSystemStopThroughAgentClassLoader() {
final ClassLoader classLoader =
new ClassLoader(null) {
@Override
public Class<?> loadClass(final String name) throws ClassNotFoundException {
if ("com.datadog.featureflag.FeatureFlaggingSystem".equals(name)) {
return FakeFeatureFlaggingSystem.class;
}
return super.loadClass(name);
}
};

Agent.shutdownFeatureFlagging(classLoader);

assertEquals(1, FakeFeatureFlaggingSystem.stopCalls.get());
}

@Test
void shutdownIsNoopBeforeAgentClassLoaderExists() {
Agent.shutdownFeatureFlagging(null);

assertEquals(0, FakeFeatureFlaggingSystem.stopCalls.get());
}

public static final class FakeFeatureFlaggingSystem {
private static final AtomicInteger stopCalls = new AtomicInteger();

public static void stop() {
stopCalls.incrementAndGet();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class OpenFeatureProviderSmokeTest extends AbstractServerSmokeTest {
command.addAll(['-jar', springBootShadowJar, "--server.port=${httpPort}".toString()])
final builder = new ProcessBuilder(command).directory(new File(buildDirectory))
builder.environment().put('DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED', 'true')
builder.environment().put('DD_FEATURE_FLAGS_CONFIGURATION_SOURCE', 'remote_config')
return builder
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ public final class ConfigDefaults {

static final boolean DEFAULT_INJECT_DATADOG_ATTRIBUTE = true;
static final String DEFAULT_SITE = "datadoghq.com";
static final String DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE = "agentless";
static final int DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS = 30;
static final int DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS = 5;

static final boolean DEFAULT_CODE_ORIGIN_FOR_SPANS_INTERFACE_SUPPORT = false;
static final int DEFAULT_CODE_ORIGIN_MAX_USER_FRAMES = 8;
Expand Down
1 change: 1 addition & 0 deletions internal-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ dependencies {
api(project(":components:context"))
api(project(":components:environment"))
api(project(":components:json"))
implementation(project(":products:feature-flagging:feature-flagging-config"))
api(project(":utils:config-utils"))
api(project(":utils:time-utils"))

Expand Down
Loading