Skip to content

Commit 29efc06

Browse files
leoromanovskyvjfridgedevflow.devflow-routing-intake
authored
Add agentless Feature Flagging configuration source (#11892)
Define Feature Flagging configuration source contract Add Datadog-managed agentless UFC polling Support custom agentless UFC endpoints Select the Feature Flagging configuration source Warn on agentless authentication failures Merge remote-tracking branch 'origin/master' into rewrite/java-agentless-narrative # Conflicts: # dd-trace-api/src/main/java/datadog/trace/api/config/FeatureFlaggingConfig.java # products/feature-flagging/feature-flagging-agent/src/main/java/com/datadog/featureflag/FeatureFlaggingSystem.java fix(ffe): handle nullable agentless response bodies Merge branch 'master' into leo.romanovsky/ffl-2693-java-agentless-configuration-source Address agentless poller review feedback Fix feature flag config test imports Address additional agentless source review feedback Support the UFC CDN response contract Harden agentless configuration responses Rely on OkHttp gzip negotiation and cover truncated response cache preservation. Require JSON:API envelopes for custom endpoints. Separate UFC transport parsers Keep Remote Configuration on raw UFC parsing and give agentless JSON:API its own streaming envelope parser. Share UFC adapters without materializing an intermediate response map. Fix feature flagging test formatting Fix feature flagging parser coverage Address final feature flagging review feedback Merge branch 'master' into leo.romanovsky/ffl-2693-java-agentless-configuration-source Merge branch 'master' into leo.romanovsky/ffl-2693-java-agentless-configuration-source fix(feature-flags): align configuration source semantics feat(feature-flags): delay agentless polling until provider use feat(communication): support mapped retried HTTP calls fix(feature-flags): align agentless HTTP behavior fix(feature-flags): complete initial poll during activation chore(feature-flags): remove unused agent feature Merge branch 'master' into leo.romanovsky/ffl-2693-java-agentless-configuration-source Fix race in agentless config source listener test scheduledPollContinuesAfterListenerRuntimeException waited on FakeClient.calls, which is incremented when a request starts rather than when it completes. The barrier therefore released as soon as the second poll began, letting the assertion race the poll thread that applies the configuration and notifies the listener. Wait on a CountDownLatch counted down by the listener itself, so the second notification is guaranteed to have happened before the assertions run. Environment: Datadog workspace Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: vickie.fridge <vickie.fridge@datadoghq.com> test(feature-flags): cover agentless lifecycle branches chore(agent): raise jar size budget to 34 MiB fix(feature-flags): document retry policy confinement Revert "chore(agent): raise jar size budget to 34 MiB" This reverts commit 29c1fd4. Merge remote-tracking branch 'origin/master' into agent/pr11892-cross-sdk-contract Co-authored-by: vjfridge <vickie.fridge@datadoghq.com> Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
1 parent c8ba6ce commit 29efc06

32 files changed

Lines changed: 3153 additions & 172 deletions

File tree

communication/src/main/java/datadog/communication/http/HttpRetryPolicy.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,13 @@ public class HttpRetryPolicy implements AutoCloseable {
5454
private final double delayFactor;
5555
private final boolean suppressInterrupts;
5656

57-
private HttpRetryPolicy(
57+
/**
58+
* Creates a retry policy.
59+
*
60+
* <p>Protected so products with a stricter cross-SDK retry contract can reuse the shared HTTP
61+
* retry loop while supplying their own response, exception, and backoff rules.
62+
*/
63+
protected HttpRetryPolicy(
5864
int retriesLeft, long delay, double delayFactor, boolean suppressInterrupts) {
5965
this.retriesLeft = retriesLeft;
6066
this.delay = delay;

communication/src/main/java/datadog/communication/http/OkHttpUtils.java

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.List;
2121
import java.util.Map;
2222
import javax.annotation.Nullable;
23+
import okhttp3.Call;
2324
import okhttp3.ConnectionPool;
2425
import okhttp3.ConnectionSpec;
2526
import okhttp3.Credentials;
@@ -405,19 +406,39 @@ public void writeTo(BufferedSink sink) throws IOException {
405406
public static Response sendWithRetries(
406407
OkHttpClient httpClient, HttpRetryPolicy.Factory retryPolicyFactory, Request request)
407408
throws IOException {
409+
return sendWithRetries((Call.Factory) httpClient, retryPolicyFactory, request);
410+
}
411+
412+
public static Response sendWithRetries(
413+
Call.Factory callFactory, HttpRetryPolicy.Factory retryPolicyFactory, Request request)
414+
throws IOException {
415+
return sendWithRetries(callFactory, retryPolicyFactory, request, response -> response);
416+
}
417+
418+
public static <T> T sendWithRetries(
419+
Call.Factory callFactory,
420+
HttpRetryPolicy.Factory retryPolicyFactory,
421+
Request request,
422+
ResponseMapper<T> responseMapper)
423+
throws IOException {
408424
try (HttpRetryPolicy retryPolicy = retryPolicyFactory.create()) {
409425
while (true) {
426+
Response response = null;
410427
try {
411-
Response response = httpClient.newCall(request).execute();
428+
response = callFactory.newCall(request).execute();
412429
if (response.isSuccessful()) {
413-
return response;
430+
return responseMapper.map(response);
414431
}
415432
if (!retryPolicy.shouldRetry(response)) {
416-
return response;
433+
return responseMapper.map(response);
417434
} else {
418435
closeQuietly(response);
436+
response = null;
419437
}
420438
} catch (Exception ex) {
439+
if (response != null) {
440+
closeQuietly(response);
441+
}
421442
if (!retryPolicy.shouldRetry(ex)) {
422443
throw ex;
423444
}
@@ -428,6 +449,11 @@ public static Response sendWithRetries(
428449
}
429450
}
430451

452+
@FunctionalInterface
453+
public interface ResponseMapper<T> {
454+
T map(Response response) throws IOException;
455+
}
456+
431457
private static void closeQuietly(Response response) {
432458
try {
433459
response.close();
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package datadog.communication.http;
2+
3+
import static datadog.communication.http.OkHttpUtils.sendWithRetries;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
6+
import java.net.ConnectException;
7+
import java.util.concurrent.atomic.AtomicInteger;
8+
import okhttp3.Call;
9+
import okhttp3.OkHttpClient;
10+
import okhttp3.Request;
11+
import okhttp3.ResponseBody;
12+
import okhttp3.mockwebserver.MockResponse;
13+
import okhttp3.mockwebserver.MockWebServer;
14+
import org.junit.jupiter.api.Test;
15+
16+
class OkHttpUtilsRetryTest {
17+
18+
@Test
19+
void retriesResponseMappingFailureThroughCallFactory() throws Exception {
20+
final MockWebServer server = new MockWebServer();
21+
final OkHttpClient client = new OkHttpClient();
22+
final AtomicInteger mappingAttempts = new AtomicInteger();
23+
server.enqueue(new MockResponse().setBody("first"));
24+
server.enqueue(new MockResponse().setBody("second"));
25+
server.start();
26+
27+
try {
28+
final Request request = new Request.Builder().url(server.url("/configuration")).build();
29+
30+
final String body =
31+
sendWithRetries(
32+
(Call.Factory) client,
33+
new HttpRetryPolicy.Factory(1, 0, 1),
34+
request,
35+
response -> {
36+
try (ResponseBody responseBody = response.body()) {
37+
if (mappingAttempts.getAndIncrement() == 0) {
38+
throw new ConnectException("response body could not be mapped");
39+
}
40+
return responseBody.string();
41+
}
42+
});
43+
44+
assertEquals("second", body);
45+
assertEquals(2, mappingAttempts.get());
46+
assertEquals(2, server.getRequestCount());
47+
} finally {
48+
client.dispatcher().executorService().shutdownNow();
49+
client.connectionPool().evictAll();
50+
server.shutdown();
51+
}
52+
}
53+
}

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

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,7 @@ private enum AgentFeature {
135135
AGENTLESS_LOG_SUBMISSION(GeneralConfig.AGENTLESS_LOG_SUBMISSION_ENABLED, false),
136136
APP_LOGS_COLLECTION(GeneralConfig.APP_LOGS_COLLECTION_ENABLED, false),
137137
LLMOBS(LlmObsConfig.LLMOBS_ENABLED, false),
138-
LLMOBS_AGENTLESS(LlmObsConfig.LLMOBS_AGENTLESS_ENABLED, false),
139-
FEATURE_FLAGGING(FeatureFlaggingConfig.FLAGGING_PROVIDER_ENABLED, false);
138+
LLMOBS_AGENTLESS(LlmObsConfig.LLMOBS_AGENTLESS_ENABLED, false);
140139

141140
private final String configKey;
142141
private final String systemProp;
@@ -285,7 +284,7 @@ public static void start(
285284
agentlessLogSubmissionEnabled = isFeatureEnabled(AgentFeature.AGENTLESS_LOG_SUBMISSION);
286285
appLogsCollectionEnabled = isFeatureEnabled(AgentFeature.APP_LOGS_COLLECTION);
287286
llmObsEnabled = isFeatureEnabled(AgentFeature.LLMOBS);
288-
featureFlaggingEnabled = isFeatureEnabled(AgentFeature.FEATURE_FLAGGING);
287+
featureFlaggingEnabled = isFeatureFlaggingEnabled();
289288

290289
// setup writers when llmobs is enabled to accomodate apm and llmobs
291290
if (llmObsEnabled) {
@@ -531,6 +530,9 @@ public static void shutdown(final boolean sync) {
531530
if (flareEnabled) {
532531
stopFlarePoller();
533532
}
533+
if (featureFlaggingEnabled) {
534+
shutdownFeatureFlagging(AGENT_CLASSLOADER);
535+
}
534536

535537
if (agentlessLogSubmissionEnabled) {
536538
shutdownLogsIntake();
@@ -1287,6 +1289,20 @@ private static void maybeStartFeatureFlagging(final Class<?> scoClass, final Obj
12871289
}
12881290
}
12891291

1292+
static void shutdownFeatureFlagging(final ClassLoader agentClassLoader) {
1293+
if (agentClassLoader == null) {
1294+
return;
1295+
}
1296+
try {
1297+
final Class<?> ffSysClass =
1298+
agentClassLoader.loadClass("com.datadog.featureflag.FeatureFlaggingSystem");
1299+
final Method stopMethod = ffSysClass.getMethod("stop");
1300+
stopMethod.invoke(null);
1301+
} catch (final Throwable e) {
1302+
log.warn("Unable to stop Feature Flagging subsystem", e);
1303+
}
1304+
}
1305+
12901306
private static void maybeInstallLogsIntake(Class<?> scoClass, Object sco) {
12911307
if (agentlessLogSubmissionEnabled || appLogsCollectionEnabled) {
12921308
StaticEventLogger.begin("Logs Intake");
@@ -1739,6 +1755,45 @@ private static boolean isFeatureEnabled(AgentFeature feature) {
17391755
}
17401756
}
17411757

1758+
private static boolean isFeatureFlaggingEnabled() {
1759+
final Boolean providerEnabled =
1760+
featureFlaggingBooleanSetting(FeatureFlaggingConfig.FEATURE_FLAGS_ENABLED);
1761+
final String configurationSource =
1762+
featureFlaggingSetting(FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE);
1763+
final Boolean legacyProviderEnabled =
1764+
featureFlaggingBooleanSetting(FeatureFlaggingConfig.EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED);
1765+
1766+
return FeatureFlaggingConfig.resolveConfiguration(
1767+
providerEnabled, configurationSource, legacyProviderEnabled)
1768+
.isEnabled();
1769+
}
1770+
1771+
@SuppressFBWarnings(
1772+
value = "NP_BOOLEAN_RETURN_NULL",
1773+
justification = "A null value preserves the distinction between absent and explicitly false")
1774+
private static Boolean featureFlaggingBooleanSetting(final String configKey) {
1775+
final String value = featureFlaggingSetting(configKey);
1776+
if (value == null) {
1777+
return null;
1778+
}
1779+
return Boolean.parseBoolean(value) || "1".equals(value);
1780+
}
1781+
1782+
private static String featureFlaggingSetting(final String configKey) {
1783+
final String systemProperty = propertyNameToSystemPropertyName(configKey);
1784+
String value = SystemProperties.get(systemProperty);
1785+
if (value == null) {
1786+
value = getStableConfig(FLEET, configKey);
1787+
}
1788+
if (value == null) {
1789+
value = ddGetEnv(systemProperty);
1790+
}
1791+
if (value == null) {
1792+
value = getStableConfig(LOCAL, configKey);
1793+
}
1794+
return value;
1795+
}
1796+
17421797
/**
17431798
* @see datadog.trace.api.ProductActivation#fromString(String)
17441799
*/
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package datadog.trace.bootstrap;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import java.util.concurrent.atomic.AtomicInteger;
6+
import org.junit.jupiter.api.BeforeEach;
7+
import org.junit.jupiter.api.Test;
8+
9+
class AgentFeatureFlaggingLifecycleTest {
10+
11+
@BeforeEach
12+
void reset() {
13+
FakeFeatureFlaggingSystem.stopCalls.set(0);
14+
}
15+
16+
@Test
17+
void shutdownInvokesFeatureFlaggingSystemStopThroughAgentClassLoader() {
18+
final ClassLoader classLoader =
19+
new ClassLoader(null) {
20+
@Override
21+
public Class<?> loadClass(final String name) throws ClassNotFoundException {
22+
if ("com.datadog.featureflag.FeatureFlaggingSystem".equals(name)) {
23+
return FakeFeatureFlaggingSystem.class;
24+
}
25+
return super.loadClass(name);
26+
}
27+
};
28+
29+
Agent.shutdownFeatureFlagging(classLoader);
30+
31+
assertEquals(1, FakeFeatureFlaggingSystem.stopCalls.get());
32+
}
33+
34+
@Test
35+
void shutdownIsNoopBeforeAgentClassLoaderExists() {
36+
Agent.shutdownFeatureFlagging(null);
37+
38+
assertEquals(0, FakeFeatureFlaggingSystem.stopCalls.get());
39+
}
40+
41+
public static final class FakeFeatureFlaggingSystem {
42+
private static final AtomicInteger stopCalls = new AtomicInteger();
43+
44+
public static void stop() {
45+
stopCalls.incrementAndGet();
46+
}
47+
}
48+
}

dd-smoke-tests/openfeature/src/test/groovy/datadog/smoketest/springboot/OpenFeatureProviderSmokeTest.groovy

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class OpenFeatureProviderSmokeTest extends AbstractServerSmokeTest {
4242
command.addAll(['-jar', springBootShadowJar, "--server.port=${httpPort}".toString()])
4343
final builder = new ProcessBuilder(command).directory(new File(buildDirectory))
4444
builder.environment().put('DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED', 'true')
45+
builder.environment().put('DD_FEATURE_FLAGS_CONFIGURATION_SOURCE', 'remote_config')
4546
return builder
4647
}
4748

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ public final class ConfigDefaults {
4848

4949
static final boolean DEFAULT_INJECT_DATADOG_ATTRIBUTE = true;
5050
static final String DEFAULT_SITE = "datadoghq.com";
51+
static final String DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE = "agentless";
52+
static final int DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS = 30;
53+
static final int DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS = 5;
5154

5255
static final boolean DEFAULT_CODE_ORIGIN_FOR_SPANS_INTERFACE_SUPPORT = false;
5356
static final int DEFAULT_CODE_ORIGIN_MAX_USER_FRAMES = 8;

internal-api/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ dependencies {
269269
api(project(":components:context"))
270270
api(project(":components:environment"))
271271
api(project(":components:json"))
272+
implementation(project(":products:feature-flagging:feature-flagging-config"))
272273
api(project(":utils:config-utils"))
273274
api(project(":utils:time-utils"))
274275

0 commit comments

Comments
 (0)