Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
d404f0d
feat(ffe): Restore the agent Feature Flags behaviors through the bridge
PerfectSlayer Oct 21, 2026
47e5c87
fix(ffe): Guard OpenFeature agent bridge against incompatible SDK int…
sarahchen6 Oct 22, 2026
14909de
fix(ffe): Report rejected Remote Configuration flag documents as appl…
PerfectSlayer Oct 23, 2026
18b7704
fix(ffe): Release the Remote Configuration poller when closing Featur…
PerfectSlayer Oct 26, 2026
b8030d1
fix(ffe): Flush Feature Flags at exit when the shutdown instrumentati…
PerfectSlayer Oct 27, 2026
23554d9
test(ffe): Drop the agent bridge class loader test
PerfectSlayer Oct 27, 2026
652cd42
chore(ffe): Mark the runtime bridge SPI check for removal after 1.69.0
PerfectSlayer Oct 27, 2026
6b1ea1f
chore(ffe): Track the checks to enable after the dd-openfeature 1.69.…
PerfectSlayer Oct 27, 2026
1919f32
chore(ffe): Drop the TODO about Remote Configuration without the agent
PerfectSlayer Oct 27, 2026
8408956
fix(ffe): Keep event proxy available with blank API key
sarahchen6 Oct 27, 2026
8442221
fix(ffe): Defer RC until an SDK subscriber is validated
sarahchen6 Oct 27, 2026
d1eaee5
fix(ffe): Restore RC before SDK subscriber is validated
sarahchen6 Oct 27, 2026
9817b27
fix(ffe): Report a rejected Remote Configuration document received be…
PerfectSlayer Oct 28, 2026
25f652f
test(ffe): Check a malformed first Remote Configuration document is r…
PerfectSlayer Oct 28, 2026
3b8f420
docs(ffe): Qualify when the agent shutdown notifies the SDK
PerfectSlayer Oct 29, 2026
de707ac
test(ffe): Check the shutdown flush with a single smoke test
PerfectSlayer Oct 29, 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 @@ -2,7 +2,15 @@ plugins {
id 'dd-trace-java.module.instrumentation'
}

// TODO Add a muzzle pass directive on com.datadoghq:dd-openfeature once the SDK is published.
// The SDK releases with the agent bridge.
// TODO APMLP-1512 Enable once dd-openfeature 1.69.0 is published.
// muzzle {
// pass {
// group = 'com.datadoghq'
// module = 'dd-openfeature'
// versions = '[1.69.0,)'
// }
// }

tracerJava {
addSourceSetFor(JavaVersion.VERSION_11)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public String[] helperClassNames() {
packageName + ".AgentConfigurationSource",
packageName + ".AgentEventProxy",
packageName + ".AgentHealthMetrics",
packageName + ".AgentShutdownSource",
packageName + ".AgentSpanEnricher",
packageName + ".DisabledAgentBridge",
packageName + ".JavaAgentBridge",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package datadog.trace.instrumentation.openfeature;

import com.datadog.openfeature.internal.bridge.v1.ShutdownSource;
import datadog.trace.api.featureflag.FeatureFlaggingGateway;

/**
* Notifies the agent shutdown, before the agent releases its Feature Flags resources: before the
* JVM shutdown hooks when the tracing shutdown instrumentation runs, otherwise from a JVM shutdown
* hook.
*/
public final class AgentShutdownSource implements ShutdownSource {
private final FeatureFlaggingGateway.Backend backend;

AgentShutdownSource(final FeatureFlaggingGateway.Backend backend) {
this.backend = backend;
}

@Override
public AutoCloseable subscribe(final Runnable listener) {
return this.backend.subscribeShutdown(listener);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package datadog.trace.instrumentation.openfeature;

import static java.lang.reflect.Modifier.isAbstract;
import static java.lang.reflect.Modifier.isStatic;

import com.datadog.openfeature.internal.bridge.v1.AgentBridge;
import com.datadog.openfeature.internal.bridge.v1.ConfigurationSource;
import com.datadog.openfeature.internal.bridge.v1.EventTransport;
Expand All @@ -8,10 +11,23 @@
import com.datadog.openfeature.internal.bridge.v1.SpanEnricher;
import datadog.trace.api.InstrumenterConfig;
import datadog.trace.api.featureflag.FeatureFlaggingGateway;
import java.lang.reflect.Method;
import javax.annotation.Nullable;

/** The bridge backed by the agent Feature Flags backend. */
public final class JavaAgentBridge implements AgentBridge {
// Muzzle checks calls, but not the SPI methods implemented by these helpers.
// TODO APMLP-1512 Drop this check once dd-openfeature 1.69.0 is released: the japicmpBridgeV1
// task then keeps the frozen bridge.v1 SPI compatible with the helpers implementing it.
private static final boolean SPI_COMPATIBLE =
implementsSpi(AgentBridge.class, JavaAgentBridge.class)
&& implementsSpi(ConfigurationSource.class, AgentConfigurationSource.class)
&& implementsSpi(EventTransport.class, AgentEventProxy.class)
&& implementsSpi(HealthMetrics.class, AgentHealthMetrics.class)
&& implementsSpi(SpanEnricher.class, AgentSpanEnricher.class)
&& implementsSpi(ShutdownSource.class, AgentShutdownSource.class)
&& implementsSpi(AgentBridge.class, DisabledAgentBridge.class);

private final FeatureFlaggingGateway.Backend backend;
private final HealthMetrics healthMetrics;
private final SpanEnricher spanEnricher;
Expand All @@ -25,18 +41,41 @@ public final class JavaAgentBridge implements AgentBridge {
/**
* Connects the SDK to the agent backend.
*
* @param fallback the bridge to use if the agent backend is not started.
* @param fallback the bridge to use if the agent backend is not started or the SDK SPI is
* incompatible.
* @return the agent bridge, the disabled one if the agent configuration disables Feature Flags,
* or the fallback one.
*/
public static AgentBridge connect(final AgentBridge fallback) {
if (!SPI_COMPATIBLE) {
return fallback;
}
if (!InstrumenterConfig.get().isFeatureFlaggingEnabled()) {
return DisabledAgentBridge.INSTANCE;
}
final FeatureFlaggingGateway.Backend backend = FeatureFlaggingGateway.backend();
return backend == null ? fallback : new JavaAgentBridge(backend);
}

private static boolean implementsSpi(final Class<?> spi, final Class<?> implementation) {
try {
for (final Method method : spi.getMethods()) {
if (!isStatic(method.getModifiers())) {
final Method implemented =
implementation.getMethod(method.getName(), method.getParameterTypes());
if (isAbstract(implemented.getModifiers())
|| isStatic(implemented.getModifiers())
|| implemented.getReturnType() != method.getReturnType()) {
return false;
}
}
}
return true;
} catch (final NoSuchMethodException e) {
return false;
}
}

@Nullable
@Override
public String setting(final String key) {
Expand Down Expand Up @@ -69,7 +108,6 @@ public SpanEnricher spanEnricher() {

@Override
public ShutdownSource shutdown() {
// TODO Flush the SDK through the agent shutdown. Until then, the SDK uses a JVM shutdown hook.
return null;
return new AgentShutdownSource(this.backend);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package datadog.trace.instrumentation.openfeature;

import datadog.trace.test.junit.utils.config.WithConfig;

@WithConfig(key = "trace.enabled", value = "false")
class DDOpenFeatureInstrumentationNoTracingForkedTest extends DDOpenFeatureInstrumentationTest {}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ void postsThroughTheEventProxyOnlyWhenAvailable() throws Exception {
assertThrows(EventProxyUnavailableException.class, () -> proxy.post("exposures", payload));
}

@Test
void subscribesToTheAgentShutdown() throws Exception {
final FeatureFlaggingGateway.Backend backend = mock(FeatureFlaggingGateway.Backend.class);
final AutoCloseable subscription = mock(AutoCloseable.class);
final Runnable listener = mock(Runnable.class);
when(backend.subscribeShutdown(listener)).thenReturn(subscription);
FeatureFlaggingGateway.register(backend);

assertSame(subscription, AgentBridges.detect().shutdown().subscribe(listener));
}

@Test
void delegatesSpanEnrichment() {
final FeatureFlaggingGateway.Backend backend = mock(FeatureFlaggingGateway.Backend.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package datadog.trace.instrumentation.openfeature;

import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.Mockito.mock;

import com.datadog.openfeature.internal.bridge.v1.AgentBridge;
import com.datadog.openfeature.internal.bridge.v1.AgentBridges;
import datadog.trace.agent.test.AbstractInstrumentationTest;
import datadog.trace.api.featureflag.FeatureFlaggingGateway;
import datadog.trace.test.junit.utils.config.WithConfig;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

@WithConfig(key = "trace.openfeature.enabled", value = "false")
class DDOpenFeatureIntegrationDisabledForkedTest extends AbstractInstrumentationTest {
@AfterEach
void unregisterBackend() {
FeatureFlaggingGateway.register(null);
}

@Test
void doesNotInstrumentWhenTheIntegrationIsDisabled() {
FeatureFlaggingGateway.register(mock(FeatureFlaggingGateway.Backend.class));
assertSame(AgentBridge.NONE, AgentBridges.detect());
}
}

This file was deleted.

6 changes: 4 additions & 2 deletions dd-openfeature/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ tasks.named<Jar>("jar") {

// The agent bridge SPI versions, with the first SDK release shipping each of them. A released
// version is frozen (only default methods can be added): check it against that release.
// TODO Set the v1 baseline once the first SDK release with the agent bridge is published.
// TODO APMLP-1512 Set the v1 baseline to 1.69.0, the first SDK release with the agent bridge,
// once it is published.
val bridgeSpiBaselines = mapOf<String, String?>(
"v1" to null,
)
Expand Down Expand Up @@ -101,7 +102,8 @@ val japicmpPublicApi = tasks.register<JapicmpTask>("japicmpPublicApi") {
packageIncludes.addAll("com.datadog.openfeature", "datadog.trace.api.openfeature")
failOnModification.set(true)
failOnSourceIncompatibility.set(true)
// TODO Remove once the baseline is 1.69.0 or later: earlier releases link the agent bootstrap API.
// TODO APMLP-1512 Remove once the baseline is 1.69.0 or later: earlier releases link the agent
// bootstrap API.
ignoreMissingClasses.set(true)
txtOutputFile.set(layout.buildDirectory.file("reports/japicmp/public-api.txt"))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
/**
* The Feature Flags runtime: configuration delivery and event pipelines. A single runtime is shared
* by all the providers of a class loader, and stopped when the last one releases it, or when the
* application shuts down: by the Datadog Java agent before the JVM shutdown hooks run when
* attached, or by a JVM shutdown hook otherwise.
* application shuts down: by the Datadog Java agent shutdown when attached, or by a JVM shutdown
* hook otherwise.
*/
public final class FeatureFlagsRuntime {
private static final Logger LOGGER = LoggerFactory.getLogger(FeatureFlagsRuntime.class);
Expand Down Expand Up @@ -105,7 +105,6 @@ static FeatureFlagsRuntime create(final AgentBridge bridge, final Settings setti
final ConfigurationService source;
final EventTransport transport;
if (remoteConfig) {
// TODO Support Remote Configuration without the Datadog Java agent.
final ConfigurationSource remoteConfiguration;
try {
remoteConfiguration = bridge.remoteConfiguration();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ public interface AgentBridge {
SpanEnricher spanEnricher();

/**
* @return the agent shutdown notifications, sent before the JVM shutdown hooks run, or {@code
* null} if not available, so the SDK must flush from its own JVM shutdown hook.
* @return the agent shutdown notifications, sent before the agent releases its Feature Flags
* resources, or {@code null} if not available, so the SDK must flush from its own JVM
* shutdown hook.
*/
@Nullable
ShutdownSource shutdown();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ public interface ConfigurationSource {
* Subscribes to configuration updates.
*
* @param listener the listener receiving raw UFC JSON documents, or {@code null} when the
* configuration is removed.
* configuration is removed. It throws a {@link RuntimeException} to reject a malformed
* document, which the source reports as not applied.
* @return the handle to close to unsubscribe.
*/
AutoCloseable subscribe(Consumer<byte[]> listener);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static java.net.http.HttpRequest.BodyPublishers.ofByteArray;

import com.datadog.openfeature.internal.bridge.v1.EventTransport;
import com.datadog.openfeature.internal.config.SdkVersion;
import com.datadog.openfeature.internal.config.Settings;
import java.io.IOException;
import java.net.URI;
Expand All @@ -22,6 +23,9 @@
public final class DirectIntakeTransport implements EventTransport {
private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(10);

/** Identifies the events like the agent event proxy does. */
static final String ORIGIN = "dd-trace-java";

private final URI intake;
private final String apiKey;
private final String traceId;
Expand Down Expand Up @@ -62,7 +66,9 @@ public void post(final String route, final byte[] json) throws IOException {
.header("Content-Type", "application/json")
.header("dd-api-key", this.apiKey)
.header("x-datadog-trace-id", this.traceId)
.header("x-datadog-parent-id", this.traceId);
.header("x-datadog-parent-id", this.traceId)
.header("DD-EVP-ORIGIN", ORIGIN)
.header("DD-EVP-ORIGIN-VERSION", SdkVersion.VERSION);
Headers.addMetadata(builder);
final HttpRequest request = builder.POST(ofByteArray(json)).build();
final int status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ public synchronized void start(final Consumer<ServerConfiguration> listener) {
"Remote Configuration returned a malformed flag configuration ({}); retaining the current configuration",
e.getClass().getSimpleName(),
e);
return;
// Rejects the document so the agent reports it as not applied.
throw new IllegalArgumentException(
"Malformed flag configuration: " + e.getMessage(), e);
}
if (configuration != null) {
listener.accept(configuration);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ void remoteConfigurationDeliversParsedConfigurationsAndReplaysToLateListeners()
runtime.start();
try {
source.listener.accept(EMPTY_UFC);
source.listener.accept("{not json".getBytes(StandardCharsets.UTF_8));
assertThrows(
IllegalArgumentException.class,
() -> source.listener.accept("{not json".getBytes(StandardCharsets.UTF_8)));
final List<ServerConfiguration> late = new CopyOnWriteArrayList<>();
runtime.addConfigurationListener(late::add);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,9 @@ void remoteConfigurationReportsItsSubscriptionAndMalformedDocuments() {
service.start(configuration -> {});

connector.listener.accept("{\"environment\":{\"name\":\"Test\"},\"flags\":{}}".getBytes(UTF_8));
connector.listener.accept("{not json".getBytes(UTF_8));
assertThrows(
IllegalArgumentException.class,
() -> connector.listener.accept("{not json".getBytes(UTF_8)));
connector.listener.accept(null);

assertLogged("subscribing to Remote Configuration");
Expand Down
Loading
Loading