Skip to content

Commit 060a55a

Browse files
committed
feat(ffe): Restore the agent Feature Flags behaviors through the bridge
Port the agent-side behaviors of the removed writers: send each event batch once with proxy discovery and recovery, and the Java SDK identity headers (#12477, #12575); flush the SDK pending events at agent shutdown, with or without tracing; declare the SDK settings the agent exposes (#12884, #12774). Move the Feature Flags shutdown tests from the agent tests to the OpenFeature smoke tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent bbdf441 commit 060a55a

19 files changed

Lines changed: 809 additions & 137 deletions

File tree

‎dd-java-agent/instrumentation/datadog/openfeature/dd-openfeature-1.69/src/main/java/datadog/trace/instrumentation/openfeature/DDOpenFeatureInstrumentation.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public String[] helperClassNames() {
4141
packageName + ".AgentConfigurationSource",
4242
packageName + ".AgentEventProxy",
4343
packageName + ".AgentHealthMetrics",
44+
packageName + ".AgentShutdownSource",
4445
packageName + ".AgentSpanEnricher",
4546
packageName + ".JavaAgentBridge",
4647
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package datadog.trace.instrumentation.openfeature;
2+
3+
import com.datadog.openfeature.internal.bridge.ShutdownSource;
4+
import datadog.trace.api.featureflag.FeatureFlaggingGateway;
5+
6+
/** Notifies the agent shutdown, before the JVM shutdown hooks run. */
7+
public final class AgentShutdownSource implements ShutdownSource {
8+
private final FeatureFlaggingGateway.Backend backend;
9+
10+
AgentShutdownSource(final FeatureFlaggingGateway.Backend backend) {
11+
this.backend = backend;
12+
}
13+
14+
@Override
15+
public AutoCloseable subscribe(final Runnable listener) {
16+
return this.backend.subscribeShutdown(listener);
17+
}
18+
}

‎dd-java-agent/instrumentation/datadog/openfeature/dd-openfeature-1.69/src/main/java11/datadog/trace/instrumentation/openfeature/JavaAgentBridge.java‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ public SpanEnricher spanEnricher() {
6464

6565
@Override
6666
public ShutdownSource shutdown() {
67-
// TODO Flush the SDK through the agent shutdown. Until then, the SDK uses a JVM shutdown hook.
68-
return null;
67+
return new AgentShutdownSource(this.backend);
6968
}
7069
}

‎dd-java-agent/instrumentation/datadog/openfeature/dd-openfeature-1.69/src/test/java/datadog/trace/instrumentation/openfeature/DDOpenFeatureInstrumentationTest.java‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,17 @@ void postsThroughTheEventProxyOnlyWhenAvailable() throws Exception {
8888
assertThrows(EventProxyUnavailableException.class, () -> proxy.post("exposures", payload));
8989
}
9090

91+
@Test
92+
void subscribesToTheAgentShutdown() throws Exception {
93+
final FeatureFlaggingGateway.Backend backend = mock(FeatureFlaggingGateway.Backend.class);
94+
final AutoCloseable subscription = mock(AutoCloseable.class);
95+
final Runnable listener = mock(Runnable.class);
96+
when(backend.subscribeShutdown(listener)).thenReturn(subscription);
97+
FeatureFlaggingGateway.register(backend);
98+
99+
assertSame(subscription, AgentBridges.detect().shutdown().subscribe(listener));
100+
}
101+
91102
@Test
92103
void delegatesSpanEnrichment() {
93104
final FeatureFlaggingGateway.Backend backend = mock(FeatureFlaggingGateway.Backend.class);

‎dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/datadog/trace/instrumentation/java/lang/ShutdownInstrumentationTest.java‎

Lines changed: 0 additions & 58 deletions
This file was deleted.

‎dd-openfeature/src/main/java/com/datadog/openfeature/internal/delivery/DirectIntakeTransport.java‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static java.net.http.HttpRequest.BodyPublishers.ofByteArray;
55

66
import com.datadog.openfeature.internal.bridge.EventTransport;
7+
import com.datadog.openfeature.internal.config.SdkVersion;
78
import com.datadog.openfeature.internal.config.Settings;
89
import java.io.IOException;
910
import java.net.URI;
@@ -22,6 +23,9 @@
2223
public final class DirectIntakeTransport implements EventTransport {
2324
private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(10);
2425

26+
/** Identifies the events like the agent event proxy does. */
27+
static final String ORIGIN = "dd-trace-java";
28+
2529
private final URI intake;
2630
private final String apiKey;
2731
private final String traceId;
@@ -62,7 +66,9 @@ public void post(final String route, final byte[] json) throws IOException {
6266
.header("Content-Type", "application/json")
6367
.header("dd-api-key", this.apiKey)
6468
.header("x-datadog-trace-id", this.traceId)
65-
.header("x-datadog-parent-id", this.traceId);
69+
.header("x-datadog-parent-id", this.traceId)
70+
.header("DD-EVP-ORIGIN", ORIGIN)
71+
.header("DD-EVP-ORIGIN-VERSION", SdkVersion.VERSION);
6672
Headers.addMetadata(builder);
6773
final HttpRequest request = builder.POST(ofByteArray(json)).build();
6874
final int status;

‎dd-openfeature/src/test/java/com/datadog/openfeature/internal/delivery/DirectIntakeTransportTest.java‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static java.nio.charset.StandardCharsets.UTF_8;
44
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
56
import static org.junit.jupiter.api.Assertions.assertNotNull;
67
import static org.junit.jupiter.api.Assertions.assertNull;
78
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -12,6 +13,7 @@
1213
import static org.mockito.Mockito.verify;
1314

1415
import com.datadog.openfeature.internal.LocalHttpServer;
16+
import com.datadog.openfeature.internal.config.SdkVersion;
1517
import com.datadog.openfeature.internal.config.TestSettings;
1618
import java.io.IOException;
1719
import java.io.InterruptedIOException;
@@ -56,7 +58,7 @@ void rejectsInvalidSite() {
5658
}
5759

5860
@Test
59-
void postsJsonWithApiKeyAndTraceHeaders() throws Exception {
61+
void postsJsonWithApiKeyTraceAndIdentityHeaders() throws Exception {
6062
this.server.enqueue(202, null);
6163

6264
transport().post("exposures", "{\"a\":1}".getBytes(UTF_8));
@@ -71,6 +73,9 @@ void postsJsonWithApiKeyAndTraceHeaders() throws Exception {
7173
assertEquals(request.header("x-datadog-trace-id"), request.header("x-datadog-parent-id"));
7274
assertEquals("java", request.header("Datadog-Meta-Lang"));
7375
assertNull(request.header("X-Datadog-EVP-Subdomain"));
76+
assertEquals("dd-trace-java", request.header("DD-EVP-ORIGIN"));
77+
assertEquals(SdkVersion.VERSION, request.header("DD-EVP-ORIGIN-VERSION"));
78+
assertNotEquals("unknown", request.header("DD-EVP-ORIGIN-VERSION"));
7479
}
7580

7681
@TableTest({
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package datadog.smoketest.springboot
2+
3+
import static datadog.trace.agent.test.server.http.TestHttpServer.httpServer
4+
import static java.util.concurrent.TimeUnit.SECONDS
5+
6+
import datadog.smoketest.AbstractSmokeTest
7+
import datadog.trace.agent.test.server.http.TestHttpServer
8+
import datadog.trace.agent.test.server.http.TestHttpServer.HandlerApi.RequestApi
9+
import datadog.trace.agent.test.utils.OkHttpUtils
10+
import datadog.trace.agent.test.utils.PortUtils
11+
import groovy.json.JsonOutput
12+
import groovy.json.JsonSlurper
13+
import okhttp3.MediaType
14+
import okhttp3.OkHttpClient
15+
import okhttp3.Request
16+
import okhttp3.RequestBody
17+
import spock.lang.AutoCleanup
18+
import spock.lang.Shared
19+
import spock.util.concurrent.PollingConditions
20+
21+
/** The SDK with the Datadog Java agent flushes its pending events when the application exits. */
22+
abstract class AbstractOpenFeatureShutdownSmokeTest extends AbstractSmokeTest {
23+
24+
@Shared
25+
protected int httpPort = PortUtils.randomOpenPort()
26+
27+
protected OkHttpClient client = OkHttpUtils.client()
28+
29+
@Shared
30+
protected final ufcConfig = new JsonSlurper().parse(fetchResource("ffe-system-test-data/ufc-config.json")) as Map<String, Object>
31+
32+
/** Serves the flag configuration as the Datadog managed CDN does. */
33+
@Shared
34+
@AutoCleanup
35+
protected TestHttpServer cdn = httpServer {
36+
handlers {
37+
prefix("/api/v2/feature-flagging/config/rules-based/server") {
38+
response.status(200).send(JsonOutput.toJson([
39+
data: [type: 'universal-flag-configuration', attributes: ufcConfig]
40+
]))
41+
}
42+
}
43+
}
44+
45+
/** @return whether the application runs with tracing enabled. */
46+
abstract boolean tracing()
47+
48+
def setupSpec() {
49+
PortUtils.waitForPortToOpen(httpPort, 240, SECONDS, testedProcess)
50+
}
51+
52+
@Override
53+
ProcessBuilder createProcessBuilder() {
54+
final springBootShadowJar = System.getProperty("datadog.smoketest.springboot.shadowJar.path")
55+
final command = [javaPath()]
56+
command.addAll(defaultJavaProperties)
57+
command.add("-Ddd.trace.enabled=${tracing()}".toString())
58+
command.addAll(['-jar', springBootShadowJar, "--server.port=${httpPort}".toString()])
59+
final builder = new ProcessBuilder(command).directory(new File(buildDirectory))
60+
builder.environment().put('DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL', "http://localhost:${cdn.address.port}".toString())
61+
return builder
62+
}
63+
64+
@Override
65+
boolean testTelemetry() {
66+
return false
67+
}
68+
69+
@Override
70+
Closure decodedTracesCallback() {
71+
return {}
72+
}
73+
74+
@Override
75+
Closure decodedEvpProxyMessageCallback() {
76+
return { String path, RequestApi request ->
77+
if (!path.contains('api/v2/flagevaluation')) {
78+
return null
79+
}
80+
return [
81+
origin: request.getHeader('DD-EVP-ORIGIN'),
82+
originVersion: request.getHeader('DD-EVP-ORIGIN-VERSION'),
83+
body: new JsonSlurper().parse(request.body)
84+
]
85+
}
86+
}
87+
88+
void 'test pending flag evaluations are flushed at application exit'() {
89+
when:
90+
// The first evaluation is flushed within the 10 seconds flush interval, which then restarts:
91+
// the next evaluation stays pending until the application exits.
92+
evaluate('test-case-boolean-false-assignment.json')
93+
new PollingConditions(timeout: 20).eventually {
94+
assert evaluatedFlags().contains('boolean-false-assignment')
95+
}
96+
evaluate('test-case-boolean-one-of-matches.json')
97+
98+
then:
99+
!evaluatedFlags().contains('boolean-one-of-matches')
100+
101+
when:
102+
testedProcess.destroy()
103+
104+
then:
105+
testedProcess.waitFor(30, SECONDS)
106+
evaluatedFlags().contains('boolean-one-of-matches')
107+
evaluationRequests().every { it.origin == 'dd-trace-java' && it.originVersion }
108+
}
109+
110+
protected void evaluate(final String testCaseFile) {
111+
final testCase = (new JsonSlurper().parse(fetchResource("ffe-system-test-data/evaluation-cases/${testCaseFile}")) as List<Map<String, Object>>).first()
112+
final request = new Request.Builder()
113+
.url("http://localhost:${httpPort}/openfeature/evaluate")
114+
.post(RequestBody.create(MediaType.parse('application/json'), JsonOutput.toJson(testCase)))
115+
.build()
116+
final response = client.newCall(request).execute()
117+
try {
118+
assert response.code() == 200
119+
} finally {
120+
response.close()
121+
}
122+
}
123+
124+
protected List<String> evaluatedFlags() {
125+
return evaluationRequests()*.body*.flagEvaluations.flatten()*.flag*.key as List<String>
126+
}
127+
128+
protected List<Map<String, Object>> evaluationRequests() {
129+
return evpProxyMessages*.getV2() as List<Map<String, Object>>
130+
}
131+
132+
protected static URL fetchResource(final String name) {
133+
return Thread.currentThread().getContextClassLoader().getResource(name)
134+
}
135+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package datadog.smoketest.springboot
2+
3+
/** The tracing shutdown hook flushes the SDK pending events. */
4+
class OpenFeatureShutdownSmokeTest extends AbstractOpenFeatureShutdownSmokeTest {
5+
6+
@Override
7+
boolean tracing() {
8+
return true
9+
}
10+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package datadog.smoketest.springboot
2+
3+
/**
4+
* Without tracing, the Feature Flagging system registers its own shutdown hook to flush the SDK
5+
* pending events.
6+
*/
7+
class OpenFeatureTraceDisabledShutdownSmokeTest extends AbstractOpenFeatureShutdownSmokeTest {
8+
9+
@Override
10+
boolean tracing() {
11+
return false
12+
}
13+
}

0 commit comments

Comments
 (0)