From 44c48964769f483cf6aa833eacb5808ff45bb9ec Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 13 Aug 2026 11:06:38 -0400 Subject: [PATCH 1/3] feat(openfeature): add direct flagevaluation fallback --- ...va => AgentlessFeatureFlagBackendApi.java} | 15 ++-- .../ExposureBackendApiFactory.java | 72 --------------- .../featureflag/ExposureWriterImpl.java | 11 ++- .../FeatureFlagBackendApiFactory.java | 90 +++++++++++++++++++ .../featureflag/FlagEvaluationWriterImpl.java | 82 ++++++++++++++++- ...> AgentlessFeatureFlagBackendApiTest.java} | 19 ++-- .../featureflag/ExposureWriterTests.java | 7 +- ... => FeatureFlagBackendApiFactoryTest.java} | 47 ++++++---- .../FlagEvaluationWriterImplTest.java | 59 ++++++++++++ 9 files changed, 290 insertions(+), 112 deletions(-) rename products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/{AgentlessExposureBackendApi.java => AgentlessFeatureFlagBackendApi.java} (76%) delete mode 100644 products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/ExposureBackendApiFactory.java create mode 100644 products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagBackendApiFactory.java rename products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/{AgentlessExposureBackendApiTest.java => AgentlessFeatureFlagBackendApiTest.java} (82%) rename products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/{ExposureBackendApiFactoryTest.java => FeatureFlagBackendApiFactoryTest.java} (73%) diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessExposureBackendApi.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApi.java similarity index 76% rename from products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessExposureBackendApi.java rename to products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApi.java index 97c798f0e1d..663f99d49a5 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessExposureBackendApi.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApi.java @@ -12,18 +12,22 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** Sends exposures through a local EVP proxy, with a safe direct intake fallback. */ -final class AgentlessExposureBackendApi implements BackendApi { +/** Sends Feature Flag events through a local EVP proxy, with a safe direct intake fallback. */ +final class AgentlessFeatureFlagBackendApi implements BackendApi { - private static final Logger LOGGER = LoggerFactory.getLogger(AgentlessExposureBackendApi.class); + private static final Logger LOGGER = + LoggerFactory.getLogger(AgentlessFeatureFlagBackendApi.class); private final BackendApi localApi; private final BackendApi directApi; + private final String eventType; private volatile BackendApi activeApi; - AgentlessExposureBackendApi(final BackendApi localApi, final BackendApi directApi) { + AgentlessFeatureFlagBackendApi( + final BackendApi localApi, final BackendApi directApi, final String eventType) { this.localApi = localApi; this.directApi = directApi; + this.eventType = eventType; this.activeApi = localApi; } @@ -46,7 +50,8 @@ public T post( if (activeApi == localApi) { LOGGER.debug( - "Switching Feature Flagging exposure delivery from the local EVP proxy to direct intake"); + "Switching Feature Flagging {} delivery from the local EVP proxy to direct intake", + eventType); activeApi = directApi; } return directApi.post(uri, requestBody, responseParser, requestListener, requestCompression); diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/ExposureBackendApiFactory.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/ExposureBackendApiFactory.java deleted file mode 100644 index f0eb1fa0e3d..00000000000 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/ExposureBackendApiFactory.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.datadog.featureflag; - -import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.CONFIGURATION_SOURCE_AGENTLESS; - -import datadog.communication.BackendApi; -import datadog.communication.BackendApiFactory; -import datadog.communication.ddagent.SharedCommunicationObjects; -import datadog.trace.api.Config; -import datadog.trace.api.intake.Intake; -import javax.annotation.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Selects the transport for Feature Flagging exposure events. */ -final class ExposureBackendApiFactory { - - private static final Logger LOGGER = LoggerFactory.getLogger(ExposureBackendApiFactory.class); - - private final Config config; - private final BackendApiFactory backendApiFactory; - - ExposureBackendApiFactory( - final Config config, final SharedCommunicationObjects sharedCommunicationObjects) { - this(config, new BackendApiFactory(config, sharedCommunicationObjects)); - } - - ExposureBackendApiFactory(final Config config, final BackendApiFactory backendApiFactory) { - this.config = config; - this.backendApiFactory = backendApiFactory; - } - - @Nullable - BackendApi create() { - final BackendApi localApi = backendApiFactory.createEvpProxyApi(Intake.EVENT_PLATFORM); - if (!CONFIGURATION_SOURCE_AGENTLESS.equals(config.getFeatureFlaggingConfigurationSource())) { - if (localApi == null) { - LOGGER.warn( - "Feature Flagging exposure delivery is disabled because the local Agent does not support the EVP proxy"); - } - return localApi; - } - - final BackendApi directApi = createDirectApi(); - if (localApi != null && directApi != null) { - return new AgentlessExposureBackendApi(localApi, directApi); - } - if (localApi != null) { - return localApi; - } - if (directApi != null) { - return directApi; - } - - LOGGER.warn( - "Feature Flagging exposure delivery is disabled because no compatible local EVP proxy or direct intake credentials are available"); - return null; - } - - @Nullable - private BackendApi createDirectApi() { - final String apiKey = config.getApiKey(); - if (apiKey == null || apiKey.isEmpty()) { - return null; - } - try { - return backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM); - } catch (final IllegalArgumentException exception) { - LOGGER.debug("Cannot configure direct Feature Flagging exposure delivery", exception); - return null; - } - } -} diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/ExposureWriterImpl.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/ExposureWriterImpl.java index 928c2da6681..7b8ca052feb 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/ExposureWriterImpl.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/ExposureWriterImpl.java @@ -41,14 +41,19 @@ public ExposureWriterImpl(final SharedCommunicationObjects sco, final Config con final TimeUnit timeUnit, final SharedCommunicationObjects sco, final Config config) { - this(capacity, flushInterval, timeUnit, new ExposureBackendApiFactory(config, sco), config); + this( + capacity, + flushInterval, + timeUnit, + new FeatureFlagBackendApiFactory(config, sco, "exposure", true), + config); } ExposureWriterImpl( final int capacity, final long flushInterval, final TimeUnit timeUnit, - final ExposureBackendApiFactory backendApiFactory, + final FeatureFlagBackendApiFactory backendApiFactory, final Config config) { this.queue = Queues.mpscBlockingConsumerArrayQueue(capacity); final ExposureSerializingHandler serializer = @@ -104,7 +109,7 @@ private static class ExposureSerializingHandler implements Runnable { private final Runnable errorCallback; ExposureSerializingHandler( - final ExposureBackendApiFactory backendApiFactory, + final FeatureFlagBackendApiFactory backendApiFactory, final MessagePassingBlockingQueue queue, final long flushInterval, final TimeUnit timeUnit, diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagBackendApiFactory.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagBackendApiFactory.java new file mode 100644 index 00000000000..5c15198570e --- /dev/null +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagBackendApiFactory.java @@ -0,0 +1,90 @@ +package com.datadog.featureflag; + +import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.CONFIGURATION_SOURCE_AGENTLESS; + +import datadog.communication.BackendApi; +import datadog.communication.BackendApiFactory; +import datadog.communication.ddagent.SharedCommunicationObjects; +import datadog.trace.api.Config; +import datadog.trace.api.intake.Intake; +import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Selects the transport for Feature Flagging events. */ +final class FeatureFlagBackendApiFactory { + + private static final Logger LOGGER = LoggerFactory.getLogger(FeatureFlagBackendApiFactory.class); + + private final Config config; + private final BackendApiFactory backendApiFactory; + private final String eventType; + private final boolean responseCompression; + + FeatureFlagBackendApiFactory( + final Config config, + final SharedCommunicationObjects sharedCommunicationObjects, + final String eventType, + final boolean responseCompression) { + this( + config, + new BackendApiFactory(config, sharedCommunicationObjects), + eventType, + responseCompression); + } + + FeatureFlagBackendApiFactory( + final Config config, + final BackendApiFactory backendApiFactory, + final String eventType, + final boolean responseCompression) { + this.config = config; + this.backendApiFactory = backendApiFactory; + this.eventType = eventType; + this.responseCompression = responseCompression; + } + + @Nullable + BackendApi create() { + final BackendApi localApi = + backendApiFactory.createEvpProxyApi(Intake.EVENT_PLATFORM, responseCompression); + if (!CONFIGURATION_SOURCE_AGENTLESS.equals(config.getFeatureFlaggingConfigurationSource())) { + if (localApi == null) { + LOGGER.warn( + "Feature Flagging {} delivery is disabled because the local Agent does not support the EVP proxy", + eventType); + } + return localApi; + } + + final BackendApi directApi = createDirectApi(); + if (localApi != null && directApi != null) { + return new AgentlessFeatureFlagBackendApi(localApi, directApi, eventType); + } + if (localApi != null) { + return localApi; + } + if (directApi != null) { + return directApi; + } + + LOGGER.warn( + "Feature Flagging {} delivery is disabled because no compatible local EVP proxy or direct intake credentials are available", + eventType); + return null; + } + + @Nullable + private BackendApi createDirectApi() { + final String apiKey = config.getApiKey(); + if (apiKey == null || apiKey.isEmpty()) { + return null; + } + try { + return backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM, responseCompression); + } catch (final IllegalArgumentException exception) { + LOGGER.debug("Cannot configure direct Feature Flagging {} delivery", eventType, exception); + return null; + } + } +} diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FlagEvaluationWriterImpl.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FlagEvaluationWriterImpl.java index e15666aa10a..64884b78b8e 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FlagEvaluationWriterImpl.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FlagEvaluationWriterImpl.java @@ -6,6 +6,7 @@ import datadog.common.queue.MessagePassingBlockingQueue; import datadog.common.queue.Queues; +import datadog.communication.BackendApi; import datadog.communication.BackendApiFactory; import datadog.communication.EvpProxy; import datadog.communication.ddagent.SharedCommunicationObjects; @@ -13,6 +14,7 @@ import datadog.trace.api.featureflag.FeatureFlaggingGateway; import datadog.trace.api.featureflag.flagevaluation.FlagEvalEvent; import datadog.trace.api.featureflag.flagevaluation.FlagEvaluationWriter; +import datadog.trace.api.intake.Intake; import datadog.trace.api.telemetry.CoreMetricCollector; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.util.ArrayList; @@ -23,6 +25,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -108,7 +111,12 @@ public FlagEvaluationWriterImpl(final SharedCommunicationObjects sco, final Conf final TimeUnit timeUnit, final SharedCommunicationObjects sco, final Config config) { - this(capacity, flushInterval, timeUnit, new BackendApiFactory(config, sco), config); + this( + capacity, + flushInterval, + timeUnit, + new FeatureFlagBackendApiFactory(config, sco, "flag evaluation", false), + config); } /** Package-private constructor allowing a BackendApiFactory to be injected for tests. */ @@ -118,10 +126,33 @@ public FlagEvaluationWriterImpl(final SharedCommunicationObjects sco, final Conf final TimeUnit timeUnit, final BackendApiFactory backendApiFactory, final Config config) { + this( + capacity, + flushInterval, + timeUnit, + () -> backendApiFactory.createBackendApi(Intake.EVENT_PLATFORM, false), + config); + } + + FlagEvaluationWriterImpl( + final int capacity, + final long flushInterval, + final TimeUnit timeUnit, + final FeatureFlagBackendApiFactory backendApiFactory, + final Config config) { + this(capacity, flushInterval, timeUnit, backendApiFactory::create, config); + } + + private FlagEvaluationWriterImpl( + final int capacity, + final long flushInterval, + final TimeUnit timeUnit, + final Supplier backendApiSupplier, + final Config config) { this.queue = Queues.mpscBlockingConsumerArrayQueue(capacity); this.serializer = new FlagEvaluationSerializingHandler( - backendApiFactory, + backendApiSupplier, queue, flushInterval, timeUnit, @@ -318,7 +349,7 @@ static class FlagEvaluationSerializingHandler implements Runnable { final ConcurrentHashMap contextTruncatedCounts, final Runnable errorCallback) { this( - backendApiFactory, + () -> backendApiFactory.createBackendApi(Intake.EVENT_PLATFORM, false), queue, flushInterval, timeUnit, @@ -339,10 +370,53 @@ static class FlagEvaluationSerializingHandler implements Runnable { final ConcurrentHashMap contextTruncatedCounts, final Runnable errorCallback, final int payloadSizeLimitBytes) { + this( + () -> backendApiFactory.createBackendApi(Intake.EVENT_PLATFORM, false), + queue, + flushInterval, + timeUnit, + context, + droppedQueueOverflow, + contextTruncatedCounts, + errorCallback, + payloadSizeLimitBytes); + } + + FlagEvaluationSerializingHandler( + final Supplier backendApiSupplier, + final MessagePassingBlockingQueue queue, + final long flushInterval, + final TimeUnit timeUnit, + final Map context, + final AtomicLong droppedQueueOverflow, + final ConcurrentHashMap contextTruncatedCounts, + final Runnable errorCallback) { + this( + backendApiSupplier, + queue, + flushInterval, + timeUnit, + context, + droppedQueueOverflow, + contextTruncatedCounts, + errorCallback, + FLAG_EVALUATION_PAYLOAD_SIZE_LIMIT_BYTES); + } + + FlagEvaluationSerializingHandler( + final Supplier backendApiSupplier, + final MessagePassingBlockingQueue queue, + final long flushInterval, + final TimeUnit timeUnit, + final Map context, + final AtomicLong droppedQueueOverflow, + final ConcurrentHashMap contextTruncatedCounts, + final Runnable errorCallback, + final int payloadSizeLimitBytes) { this.queue = queue; this.evpPublisher = new FeatureFlagEvpPublisher<>( - backendApiFactory, FlagEvaluationPayloads.FlagEvaluationsRequest.class, false); + backendApiSupplier, FlagEvaluationPayloads.FlagEvaluationsRequest.class); this.context = context; this.droppedQueueOverflow = droppedQueueOverflow; this.contextTruncatedCounts = contextTruncatedCounts; diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessExposureBackendApiTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApiTest.java similarity index 82% rename from products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessExposureBackendApiTest.java rename to products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApiTest.java index 9455634ec45..0a2403ec25d 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessExposureBackendApiTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApiTest.java @@ -22,7 +22,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -class AgentlessExposureBackendApiTest { +class AgentlessFeatureFlagBackendApiTest { @ParameterizedTest @ValueSource(ints = {403, 404, 405}) @@ -30,12 +30,13 @@ void replaysRejectedBatchDirectlyAndKeepsDirectRoute(final int statusCode) throw final RecordingBackendApi local = new RecordingBackendApi(new HttpResponseException(statusCode, "rejected")); final RecordingBackendApi direct = new RecordingBackendApi(); - final AgentlessExposureBackendApi api = new AgentlessExposureBackendApi(local, direct); + final AgentlessFeatureFlagBackendApi api = + new AgentlessFeatureFlagBackendApi(local, direct, "flag evaluation"); final RequestBody firstBody = requestBody("first"); final RequestBody secondBody = requestBody("second"); - api.post("exposures", firstBody, stream -> null, null, false); - api.post("exposures", secondBody, stream -> null, null, false); + api.post("flagevaluation", firstBody, stream -> null, null, false); + api.post("flagevaluation", secondBody, stream -> null, null, false); assertEquals(1, local.calls); assertEquals(2, direct.calls); @@ -49,9 +50,10 @@ void fallsBackAfterConnectionRefusal() throws Exception { final RecordingBackendApi local = new RecordingBackendApi(new ConnectException("connection refused")); final RecordingBackendApi direct = new RecordingBackendApi(); - final AgentlessExposureBackendApi api = new AgentlessExposureBackendApi(local, direct); + final AgentlessFeatureFlagBackendApi api = + new AgentlessFeatureFlagBackendApi(local, direct, "flag evaluation"); - api.post("exposures", requestBody("exposure"), stream -> null, null, false); + api.post("flagevaluation", requestBody("evaluation"), stream -> null, null, false); assertEquals(1, local.calls); assertEquals(1, direct.calls); @@ -76,11 +78,12 @@ void doesNotReplayConnectionReset() { private static void assertNoDirectReplay(final IOException failure) { final RecordingBackendApi local = new RecordingBackendApi(failure); final RecordingBackendApi direct = new RecordingBackendApi(); - final AgentlessExposureBackendApi api = new AgentlessExposureBackendApi(local, direct); + final AgentlessFeatureFlagBackendApi api = + new AgentlessFeatureFlagBackendApi(local, direct, "flag evaluation"); assertThrows( IOException.class, - () -> api.post("exposures", requestBody("exposure"), stream -> null, null, false)); + () -> api.post("flagevaluation", requestBody("evaluation"), stream -> null, null, false)); assertEquals(1, local.calls); assertEquals(0, direct.calls); diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java index cfd65cceb2c..cc2eb5bb5c5 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java @@ -155,10 +155,11 @@ void testAgentlessExposureEventWritesDirectlyWithApiKey() throws Exception { HttpRetryPolicy.Factory.NEVER_RETRY, new OkHttpClient.Builder().build(), false); - when(backendApiFactory.createDirectIntakeApi(datadog.trace.api.intake.Intake.EVENT_PLATFORM)) + when(backendApiFactory.createDirectIntakeApi( + datadog.trace.api.intake.Intake.EVENT_PLATFORM, true)) .thenReturn(directApi); - ExposureBackendApiFactory exposureBackendApiFactory = - new ExposureBackendApiFactory(config, backendApiFactory); + FeatureFlagBackendApiFactory exposureBackendApiFactory = + new FeatureFlagBackendApiFactory(config, backendApiFactory, "exposure", true); List exposures = buildExposures(5); try (ExposureWriterImpl writer = diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureBackendApiFactoryTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagBackendApiFactoryTest.java similarity index 73% rename from products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureBackendApiFactoryTest.java rename to products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagBackendApiFactoryTest.java index 602a3219ef3..b5248fcfbda 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureBackendApiFactoryTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagBackendApiFactoryTest.java @@ -16,33 +16,37 @@ import datadog.trace.api.intake.Intake; import org.junit.jupiter.api.Test; -class ExposureBackendApiFactoryTest { +class FeatureFlagBackendApiFactoryTest { @Test void remoteConfigUsesOnlyLocalEvpProxy() { final Config config = config(CONFIGURATION_SOURCE_REMOTE_CONFIG, "api-key"); final BackendApiFactory backendApiFactory = mock(BackendApiFactory.class); final BackendApi localApi = mock(BackendApi.class); - when(backendApiFactory.createEvpProxyApi(Intake.EVENT_PLATFORM)).thenReturn(localApi); + when(backendApiFactory.createEvpProxyApi(Intake.EVENT_PLATFORM, false)).thenReturn(localApi); - final BackendApi selected = new ExposureBackendApiFactory(config, backendApiFactory).create(); + final BackendApi selected = + new FeatureFlagBackendApiFactory(config, backendApiFactory, "flag evaluation", false) + .create(); assertSame(localApi, selected); - verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM); + verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM, false); } @Test void agentlessPrefersLocalEvpProxyWithDirectFallback() { final Config config = config(CONFIGURATION_SOURCE_AGENTLESS, "api-key"); final BackendApiFactory backendApiFactory = mock(BackendApiFactory.class); - when(backendApiFactory.createEvpProxyApi(Intake.EVENT_PLATFORM)) + when(backendApiFactory.createEvpProxyApi(Intake.EVENT_PLATFORM, false)) .thenReturn(mock(BackendApi.class)); - when(backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM)) + when(backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM, false)) .thenReturn(mock(BackendApi.class)); - final BackendApi selected = new ExposureBackendApiFactory(config, backendApiFactory).create(); + final BackendApi selected = + new FeatureFlagBackendApiFactory(config, backendApiFactory, "flag evaluation", false) + .create(); - assertInstanceOf(AgentlessExposureBackendApi.class, selected); + assertInstanceOf(AgentlessFeatureFlagBackendApi.class, selected); } @Test @@ -50,9 +54,12 @@ void agentlessUsesDirectIntakeWhenLocalEvpProxyIsUnavailable() { final Config config = config(CONFIGURATION_SOURCE_AGENTLESS, "api-key"); final BackendApiFactory backendApiFactory = mock(BackendApiFactory.class); final BackendApi directApi = mock(BackendApi.class); - when(backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM)).thenReturn(directApi); + when(backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM, false)) + .thenReturn(directApi); - final BackendApi selected = new ExposureBackendApiFactory(config, backendApiFactory).create(); + final BackendApi selected = + new FeatureFlagBackendApiFactory(config, backendApiFactory, "flag evaluation", false) + .create(); assertSame(directApi, selected); } @@ -62,12 +69,14 @@ void agentlessUsesLocalEvpProxyWhenApiKeyIsUnavailable() { final Config config = config(CONFIGURATION_SOURCE_AGENTLESS, null); final BackendApiFactory backendApiFactory = mock(BackendApiFactory.class); final BackendApi localApi = mock(BackendApi.class); - when(backendApiFactory.createEvpProxyApi(Intake.EVENT_PLATFORM)).thenReturn(localApi); + when(backendApiFactory.createEvpProxyApi(Intake.EVENT_PLATFORM, false)).thenReturn(localApi); - final BackendApi selected = new ExposureBackendApiFactory(config, backendApiFactory).create(); + final BackendApi selected = + new FeatureFlagBackendApiFactory(config, backendApiFactory, "flag evaluation", false) + .create(); assertSame(localApi, selected); - verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM); + verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM, false); } @Test @@ -75,7 +84,9 @@ void agentlessDisablesDeliveryWhenNoRouteIsAvailable() { final Config config = config(CONFIGURATION_SOURCE_AGENTLESS, null); final BackendApiFactory backendApiFactory = mock(BackendApiFactory.class); - final BackendApi selected = new ExposureBackendApiFactory(config, backendApiFactory).create(); + final BackendApi selected = + new FeatureFlagBackendApiFactory(config, backendApiFactory, "flag evaluation", false) + .create(); assertNull(selected); } @@ -85,11 +96,13 @@ void agentlessKeepsLocalRouteWhenDirectUrlIsInvalid() { final Config config = config(CONFIGURATION_SOURCE_AGENTLESS, "api-key"); final BackendApiFactory backendApiFactory = mock(BackendApiFactory.class); final BackendApi localApi = mock(BackendApi.class); - when(backendApiFactory.createEvpProxyApi(Intake.EVENT_PLATFORM)).thenReturn(localApi); - when(backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM)) + when(backendApiFactory.createEvpProxyApi(Intake.EVENT_PLATFORM, false)).thenReturn(localApi); + when(backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM, false)) .thenThrow(new IllegalArgumentException("invalid URL")); - final BackendApi selected = new ExposureBackendApiFactory(config, backendApiFactory).create(); + final BackendApi selected = + new FeatureFlagBackendApiFactory(config, backendApiFactory, "flag evaluation", false) + .create(); assertSame(localApi, selected); } diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationWriterImplTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationWriterImplTest.java index b354fdbb6c4..a395162a8b8 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationWriterImplTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationWriterImplTest.java @@ -11,6 +11,7 @@ import static com.datadog.featureflag.FlagEvaluationTestSupport.metricSum; import static com.datadog.featureflag.FlagEvaluationTestSupport.repeat; import static com.datadog.featureflag.FlagEvaluationTestSupport.simpleEvent; +import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.CONFIGURATION_SOURCE_AGENTLESS; import static java.util.Collections.emptyMap; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -31,13 +32,18 @@ import datadog.common.queue.Queues; import datadog.communication.BackendApi; import datadog.communication.BackendApiFactory; +import datadog.communication.IntakeApi; import datadog.communication.ddagent.SharedCommunicationObjects; +import datadog.communication.http.HttpRetryPolicy; +import datadog.trace.agent.test.server.http.JavaTestHttpServer; +import datadog.trace.api.Config; import datadog.trace.api.featureflag.FeatureFlaggingGateway; import datadog.trace.api.featureflag.flagevaluation.FlagEvalEvent; import datadog.trace.api.featureflag.ufc.v1.ServerConfiguration; import datadog.trace.api.intake.Intake; import datadog.trace.api.telemetry.CoreMetricCollector; import datadog.trace.api.telemetry.MetricCollector; +import datadog.trace.test.util.PollingConditions; import java.io.IOException; import java.lang.reflect.Field; import java.util.Collection; @@ -46,6 +52,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; import okhttp3.RequestBody; import okio.Buffer; import org.junit.jupiter.api.AfterEach; @@ -54,6 +62,10 @@ class FlagEvaluationWriterImplTest { + private static final String DIRECT_FLAG_EVALUATION_ENDPOINT = "/api/v2/flagevaluation"; + private static final String API_KEY = "test-api-key"; + private static final double TIMEOUT_SECONDS = 5; + @BeforeEach void clearCoreMetricsBefore() { clearCoreMetrics(); @@ -650,6 +662,53 @@ void scoConstructorCreatesUsableWriter() { writer.close(); } + @Test + void agentlessWritesFlagEvaluationsDirectlyWhenLocalProxyIsUnavailable() throws Exception { + try (JavaTestHttpServer server = + JavaTestHttpServer.httpServer( + s -> + s.handlers( + h -> + h.prefix( + DIRECT_FLAG_EVALUATION_ENDPOINT, + api -> api.getResponse().status(200).send("OK"))))) { + final Config config = cfg(); + when(config.getFeatureFlaggingConfigurationSource()) + .thenReturn(CONFIGURATION_SOURCE_AGENTLESS); + when(config.getApiKey()).thenReturn(API_KEY); + final BackendApiFactory backendApiFactory = mock(BackendApiFactory.class); + final IntakeApi directApi = + new IntakeApi( + HttpUrl.get(server.getAddress()).resolve("/api/v2/"), + API_KEY, + "123", + HttpRetryPolicy.Factory.NEVER_RETRY, + new OkHttpClient.Builder().build(), + false); + when(backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM, false)) + .thenReturn(directApi); + final FeatureFlagBackendApiFactory featureFlagBackendApiFactory = + new FeatureFlagBackendApiFactory(config, backendApiFactory, "flag evaluation", false); + final PollingConditions poll = new PollingConditions(TIMEOUT_SECONDS); + + try (FlagEvaluationWriterImpl writer = + new FlagEvaluationWriterImpl( + 16, 1, TimeUnit.MILLISECONDS, featureFlagBackendApiFactory, config)) { + writer.startForTest(); + writer.enqueue(simpleEvent("direct-flag", "on")); + + poll.eventually( + () -> { + assertNotNull(server.getLastRequest()); + assertEquals(DIRECT_FLAG_EVALUATION_ENDPOINT, server.getLastRequest().getPath()); + assertEquals(API_KEY, server.getLastRequest().getHeader("dd-api-key")); + assertNull(server.getLastRequest().getHeader("X-Datadog-EVP-Subdomain")); + assertTrue(server.getLastRequest().getBody().length > 0); + }); + } + } + } + @Test void countContextTruncatedAccumulatesPerReason() { final BackendApi mockEvp = mock(BackendApi.class); From 59165392a0da76690b305e2c8facd656b14698a6 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 13 Aug 2026 11:18:33 -0400 Subject: [PATCH 2/3] test(openfeature): cover both direct EVP signals --- .../AgentlessFeatureFlagBackendApiTest.java | 38 ++++++++++++++++--- .../FeatureFlagBackendApiFactoryTest.java | 25 ++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApiTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApiTest.java index 0a2403ec25d..23226697fe7 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApiTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApiTest.java @@ -15,11 +15,14 @@ import java.net.SocketTimeoutException; import java.util.ArrayList; import java.util.List; +import java.util.stream.Stream; import javax.annotation.Nullable; import okhttp3.MediaType; import okhttp3.RequestBody; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; class AgentlessFeatureFlagBackendApiTest { @@ -45,20 +48,40 @@ void replaysRejectedBatchDirectlyAndKeepsDirectRoute(final int statusCode) throw assertSame(secondBody, direct.requestBodies.get(1)); } - @Test - void fallsBackAfterConnectionRefusal() throws Exception { + @ParameterizedTest + @MethodSource("featureFlagRoutes") + void fallsBackAfterConnectionRefusal(final String route, final String eventType) + throws Exception { final RecordingBackendApi local = new RecordingBackendApi(new ConnectException("connection refused")); final RecordingBackendApi direct = new RecordingBackendApi(); final AgentlessFeatureFlagBackendApi api = - new AgentlessFeatureFlagBackendApi(local, direct, "flag evaluation"); + new AgentlessFeatureFlagBackendApi(local, direct, eventType); - api.post("flagevaluation", requestBody("evaluation"), stream -> null, null, false); + api.post(route, requestBody(eventType), stream -> null, null, false); assertEquals(1, local.calls); assertEquals(1, direct.calls); } + @Test + void doesNotReturnToLocalRouteAfterSwitchingToDirectIntake() throws Exception { + final RecordingBackendApi local = + new RecordingBackendApi(new ConnectException("connection refused")); + final RecordingBackendApi direct = new RecordingBackendApi(); + final AgentlessFeatureFlagBackendApi api = + new AgentlessFeatureFlagBackendApi(local, direct, "exposure"); + + api.post("exposures", requestBody("first"), stream -> null, null, false); + direct.failure = new IOException("direct intake failed"); + + assertThrows( + IOException.class, + () -> api.post("exposures", requestBody("second"), stream -> null, null, false)); + assertEquals(1, local.calls); + assertEquals(2, direct.calls); + } + @ParameterizedTest @ValueSource(ints = {429, 500}) void doesNotReplayAmbiguousHttpFailure(final int statusCode) { @@ -93,8 +116,13 @@ private static RequestBody requestBody(final String value) { return RequestBody.create(MediaType.parse("application/json"), value); } + private static Stream featureFlagRoutes() { + return Stream.of( + Arguments.of("exposures", "exposure"), Arguments.of("flagevaluation", "flag evaluation")); + } + private static final class RecordingBackendApi implements BackendApi { - private final IOException failure; + private IOException failure; private final List requestBodies = new ArrayList<>(); private int calls; diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagBackendApiFactoryTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagBackendApiFactoryTest.java index b5248fcfbda..e9fc9962eb1 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagBackendApiFactoryTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagBackendApiFactoryTest.java @@ -33,6 +33,19 @@ void remoteConfigUsesOnlyLocalEvpProxy() { verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM, false); } + @Test + void remoteConfigDisablesDeliveryWhenLocalEvpProxyIsUnavailable() { + final Config config = config(CONFIGURATION_SOURCE_REMOTE_CONFIG, "api-key"); + final BackendApiFactory backendApiFactory = mock(BackendApiFactory.class); + + final BackendApi selected = + new FeatureFlagBackendApiFactory(config, backendApiFactory, "exposure", true).create(); + + assertNull(selected); + verify(backendApiFactory).createEvpProxyApi(Intake.EVENT_PLATFORM, true); + verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM, true); + } + @Test void agentlessPrefersLocalEvpProxyWithDirectFallback() { final Config config = config(CONFIGURATION_SOURCE_AGENTLESS, "api-key"); @@ -91,6 +104,18 @@ void agentlessDisablesDeliveryWhenNoRouteIsAvailable() { assertNull(selected); } + @Test + void agentlessDisablesDeliveryWhenApiKeyIsEmpty() { + final Config config = config(CONFIGURATION_SOURCE_AGENTLESS, ""); + final BackendApiFactory backendApiFactory = mock(BackendApiFactory.class); + + final BackendApi selected = + new FeatureFlagBackendApiFactory(config, backendApiFactory, "exposure", true).create(); + + assertNull(selected); + verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM, true); + } + @Test void agentlessKeepsLocalRouteWhenDirectUrlIsInvalid() { final Config config = config(CONFIGURATION_SOURCE_AGENTLESS, "api-key"); From 855b295fdf186f82b680d409f3c4306a3dd5aa81 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 13 Aug 2026 21:28:12 -0400 Subject: [PATCH 3/3] fix(openfeature): clarify feature flag transport policy --- .../java/datadog/communication/IntakeApi.java | 8 ++- .../datadog/communication/IntakeApiTest.java | 65 +++++++++++++++++++ .../AgentlessFeatureFlagBackendApi.java | 14 ++-- .../featureflag/ExposureWriterImpl.java | 2 +- .../FeatureFlagBackendApiFactory.java | 42 ++++++------ .../featureflag/FeatureFlagEventType.java | 26 ++++++++ .../featureflag/FlagEvaluationWriterImpl.java | 7 +- .../featureflag/ExposureWriterTests.java | 2 +- .../FeatureFlagBackendApiFactoryTest.java | 43 ++++++------ .../FlagEvaluationWriterImplTest.java | 3 +- 10 files changed, 150 insertions(+), 62 deletions(-) create mode 100644 communication/src/test/java/datadog/communication/IntakeApiTest.java create mode 100644 products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEventType.java diff --git a/communication/src/main/java/datadog/communication/IntakeApi.java b/communication/src/main/java/datadog/communication/IntakeApi.java index 3f1fe9df67f..1a6f3f91bc7 100644 --- a/communication/src/main/java/datadog/communication/IntakeApi.java +++ b/communication/src/main/java/datadog/communication/IntakeApi.java @@ -25,6 +25,7 @@ public class IntakeApi implements BackendApi { private static final String ACCEPT_ENCODING_HEADER = "Accept-Encoding"; private static final String CONTENT_ENCODING_HEADER = "Content-Encoding"; private static final String GZIP_ENCODING = "gzip"; + private static final String IDENTITY_ENCODING = "identity"; private final String apiKey; private final String traceId; @@ -73,9 +74,10 @@ public T post( requestBuilder.addHeader(CONTENT_ENCODING_HEADER, GZIP_ENCODING); } - if (responseCompression) { - requestBuilder.addHeader(ACCEPT_ENCODING_HEADER, GZIP_ENCODING); - } + // OkHttp adds Accept-Encoding: gzip when this header is absent. Always set the header so a + // caller can disable response compression on the wire. + requestBuilder.addHeader( + ACCEPT_ENCODING_HEADER, responseCompression ? GZIP_ENCODING : IDENTITY_ENCODING); Request request = requestBuilder.build(); try (okhttp3.Response response = diff --git a/communication/src/test/java/datadog/communication/IntakeApiTest.java b/communication/src/test/java/datadog/communication/IntakeApiTest.java new file mode 100644 index 00000000000..326cf21ca84 --- /dev/null +++ b/communication/src/test/java/datadog/communication/IntakeApiTest.java @@ -0,0 +1,65 @@ +package datadog.communication; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import datadog.communication.http.HttpRetryPolicy; +import java.io.IOException; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.RequestBody; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class IntakeApiTest { + + private static final MediaType JSON = MediaType.parse("application/json"); + + private MockWebServer server; + private OkHttpClient client; + + @BeforeEach + void setUp() throws IOException { + server = new MockWebServer(); + server.start(); + client = new OkHttpClient.Builder().build(); + } + + @AfterEach + void tearDown() throws IOException { + client.dispatcher().executorService().shutdownNow(); + client.connectionPool().evictAll(); + server.shutdown(); + } + + @Test + void requestsGzipResponseCompressionWhenEnabled() throws Exception { + assertEquals("gzip", postAndReadAcceptEncoding(true)); + } + + @Test + void requestsIdentityResponseEncodingWhenCompressionIsDisabled() throws Exception { + assertEquals("identity", postAndReadAcceptEncoding(false)); + } + + private String postAndReadAcceptEncoding(final boolean responseCompression) throws Exception { + server.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); + final IntakeApi api = + new IntakeApi( + server.url("/api/v2/"), + "api-key", + "123", + HttpRetryPolicy.Factory.NEVER_RETRY, + client, + responseCompression); + + api.post("flagevaluation", RequestBody.create(JSON, "{}"), responseBody -> null, null, false); + + final RecordedRequest request = server.takeRequest(); + assertEquals("/api/v2/flagevaluation", request.getPath()); + return request.getHeader("Accept-Encoding"); + } +} diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApi.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApi.java index 10327eee328..769ebfd1dd1 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApi.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApi.java @@ -19,20 +19,20 @@ final class AgentlessFeatureFlagBackendApi implements BackendApi { private static final Logger LOGGER = LoggerFactory.getLogger(AgentlessFeatureFlagBackendApi.class); - private final BackendApi localApi; + private final BackendApi proxyApi; private final Supplier directApiSupplier; private final String eventType; private volatile BackendApi activeApi; private volatile boolean directApiCreationAttempted; AgentlessFeatureFlagBackendApi( - final BackendApi localApi, + final BackendApi proxyApi, final Supplier directApiSupplier, final String eventType) { - this.localApi = localApi; + this.proxyApi = proxyApi; this.directApiSupplier = directApiSupplier; this.eventType = eventType; - this.activeApi = localApi; + this.activeApi = proxyApi; } @Override @@ -48,7 +48,7 @@ public T post( return selectedApi.post( uri, requestBody, responseParser, requestListener, requestCompression); } catch (final IOException exception) { - if (selectedApi != localApi || !isDefinitiveRejection(exception)) { + if (selectedApi != proxyApi || !isDefinitiveRejection(exception)) { throw exception; } @@ -63,13 +63,13 @@ public T post( @Nullable private BackendApi getOrCreateDirectApi() { final BackendApi selectedApi = activeApi; - if (selectedApi != localApi) { + if (selectedApi != proxyApi) { return selectedApi; } synchronized (this) { final BackendApi currentApi = activeApi; - if (currentApi != localApi) { + if (currentApi != proxyApi) { return currentApi; } if (directApiCreationAttempted) { diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/ExposureWriterImpl.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/ExposureWriterImpl.java index 7b8ca052feb..017616ed070 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/ExposureWriterImpl.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/ExposureWriterImpl.java @@ -45,7 +45,7 @@ public ExposureWriterImpl(final SharedCommunicationObjects sco, final Config con capacity, flushInterval, timeUnit, - new FeatureFlagBackendApiFactory(config, sco, "exposure", true), + new FeatureFlagBackendApiFactory(config, sco, FeatureFlagEventType.EXPOSURE), config); } diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagBackendApiFactory.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagBackendApiFactory.java index 0081cac3343..1727d156915 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagBackendApiFactory.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagBackendApiFactory.java @@ -18,50 +18,44 @@ final class FeatureFlagBackendApiFactory { private final Config config; private final BackendApiFactory backendApiFactory; - private final String eventType; - private final boolean responseCompression; + private final FeatureFlagEventType eventType; FeatureFlagBackendApiFactory( final Config config, final SharedCommunicationObjects sharedCommunicationObjects, - final String eventType, - final boolean responseCompression) { - this( - config, - new BackendApiFactory(config, sharedCommunicationObjects), - eventType, - responseCompression); + final FeatureFlagEventType eventType) { + this(config, new BackendApiFactory(config, sharedCommunicationObjects), eventType); } FeatureFlagBackendApiFactory( final Config config, final BackendApiFactory backendApiFactory, - final String eventType, - final boolean responseCompression) { + final FeatureFlagEventType eventType) { this.config = config; this.backendApiFactory = backendApiFactory; this.eventType = eventType; - this.responseCompression = responseCompression; } @Nullable BackendApi create() { - final BackendApi localApi = - backendApiFactory.createEvpProxyApi(Intake.EVENT_PLATFORM, responseCompression); + final BackendApi proxyApi = + backendApiFactory.createEvpProxyApi( + Intake.EVENT_PLATFORM, eventType.responseCompressionEnabled()); if (!CONFIGURATION_SOURCE_AGENTLESS.equals(config.getFeatureFlaggingConfigurationSource())) { - if (localApi == null) { + if (proxyApi == null) { LOGGER.warn( "Feature Flagging {} delivery is disabled because the local Agent does not support the EVP proxy", - eventType); + eventType.logName()); } - return localApi; + return proxyApi; } - if (localApi != null) { + if (proxyApi != null) { if (hasDirectCredentials()) { - return new AgentlessFeatureFlagBackendApi(localApi, this::createDirectApi, eventType); + return new AgentlessFeatureFlagBackendApi( + proxyApi, this::createDirectApi, eventType.logName()); } - return localApi; + return proxyApi; } final BackendApi directApi = createDirectApi(); @@ -71,7 +65,7 @@ BackendApi create() { LOGGER.warn( "Feature Flagging {} delivery is disabled because no compatible local EVP proxy or direct intake credentials are available", - eventType); + eventType.logName()); return null; } @@ -86,9 +80,11 @@ private BackendApi createDirectApi() { return null; } try { - return backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM, responseCompression); + return backendApiFactory.createDirectIntakeApi( + Intake.EVENT_PLATFORM, eventType.responseCompressionEnabled()); } catch (final IllegalArgumentException exception) { - LOGGER.debug("Cannot configure direct Feature Flagging {} delivery", eventType, exception); + LOGGER.debug( + "Cannot configure direct Feature Flagging {} delivery", eventType.logName(), exception); return null; } } diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEventType.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEventType.java new file mode 100644 index 00000000000..dee8b424fa3 --- /dev/null +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEventType.java @@ -0,0 +1,26 @@ +package com.datadog.featureflag; + +/** Defines event-specific transport behavior for Feature Flag delivery. */ +enum FeatureFlagEventType { + // Keep the established exposure transport behavior for compatibility. + EXPOSURE("exposure", true), + + // Flag evaluation writers ignore successful response bodies, so gzip negotiation adds no value. + FLAG_EVALUATION("flag evaluation", false); + + private final String logName; + private final boolean responseCompressionEnabled; + + FeatureFlagEventType(final String logName, final boolean responseCompressionEnabled) { + this.logName = logName; + this.responseCompressionEnabled = responseCompressionEnabled; + } + + String logName() { + return logName; + } + + boolean responseCompressionEnabled() { + return responseCompressionEnabled; + } +} diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FlagEvaluationWriterImpl.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FlagEvaluationWriterImpl.java index 64884b78b8e..1bc203022ac 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FlagEvaluationWriterImpl.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FlagEvaluationWriterImpl.java @@ -115,7 +115,7 @@ public FlagEvaluationWriterImpl(final SharedCommunicationObjects sco, final Conf capacity, flushInterval, timeUnit, - new FeatureFlagBackendApiFactory(config, sco, "flag evaluation", false), + new FeatureFlagBackendApiFactory(config, sco, FeatureFlagEventType.FLAG_EVALUATION), config); } @@ -130,7 +130,10 @@ public FlagEvaluationWriterImpl(final SharedCommunicationObjects sco, final Conf capacity, flushInterval, timeUnit, - () -> backendApiFactory.createBackendApi(Intake.EVENT_PLATFORM, false), + () -> + backendApiFactory.createBackendApi( + Intake.EVENT_PLATFORM, + FeatureFlagEventType.FLAG_EVALUATION.responseCompressionEnabled()), config); } diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java index cc2eb5bb5c5..78e4a72ebba 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java @@ -159,7 +159,7 @@ void testAgentlessExposureEventWritesDirectlyWithApiKey() throws Exception { datadog.trace.api.intake.Intake.EVENT_PLATFORM, true)) .thenReturn(directApi); FeatureFlagBackendApiFactory exposureBackendApiFactory = - new FeatureFlagBackendApiFactory(config, backendApiFactory, "exposure", true); + new FeatureFlagBackendApiFactory(config, backendApiFactory, FeatureFlagEventType.EXPOSURE); List exposures = buildExposures(5); try (ExposureWriterImpl writer = diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagBackendApiFactoryTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagBackendApiFactoryTest.java index c498caff27b..ddd873a1e9d 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagBackendApiFactoryTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagBackendApiFactoryTest.java @@ -1,5 +1,7 @@ package com.datadog.featureflag; +import static com.datadog.featureflag.FeatureFlagEventType.EXPOSURE; +import static com.datadog.featureflag.FeatureFlagEventType.FLAG_EVALUATION; import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.CONFIGURATION_SOURCE_AGENTLESS; import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.CONFIGURATION_SOURCE_REMOTE_CONFIG; import static org.junit.jupiter.api.Assertions.assertInstanceOf; @@ -22,14 +24,13 @@ class FeatureFlagBackendApiFactoryTest { void remoteConfigUsesOnlyLocalEvpProxy() { final Config config = config(CONFIGURATION_SOURCE_REMOTE_CONFIG, "api-key"); final BackendApiFactory backendApiFactory = mock(BackendApiFactory.class); - final BackendApi localApi = mock(BackendApi.class); - when(backendApiFactory.createEvpProxyApi(Intake.EVENT_PLATFORM, false)).thenReturn(localApi); + final BackendApi proxyApi = mock(BackendApi.class); + when(backendApiFactory.createEvpProxyApi(Intake.EVENT_PLATFORM, false)).thenReturn(proxyApi); final BackendApi selected = - new FeatureFlagBackendApiFactory(config, backendApiFactory, "flag evaluation", false) - .create(); + new FeatureFlagBackendApiFactory(config, backendApiFactory, FLAG_EVALUATION).create(); - assertSame(localApi, selected); + assertSame(proxyApi, selected); verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM, false); } @@ -39,7 +40,7 @@ void remoteConfigDisablesDeliveryWhenLocalEvpProxyIsUnavailable() { final BackendApiFactory backendApiFactory = mock(BackendApiFactory.class); final BackendApi selected = - new FeatureFlagBackendApiFactory(config, backendApiFactory, "exposure", true).create(); + new FeatureFlagBackendApiFactory(config, backendApiFactory, EXPOSURE).create(); assertNull(selected); verify(backendApiFactory).createEvpProxyApi(Intake.EVENT_PLATFORM, true); @@ -56,8 +57,7 @@ void agentlessPrefersLocalEvpProxyWithDirectFallback() { .thenReturn(mock(BackendApi.class)); final BackendApi selected = - new FeatureFlagBackendApiFactory(config, backendApiFactory, "flag evaluation", false) - .create(); + new FeatureFlagBackendApiFactory(config, backendApiFactory, FLAG_EVALUATION).create(); assertInstanceOf(AgentlessFeatureFlagBackendApi.class, selected); verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM, false); @@ -72,8 +72,7 @@ void agentlessUsesDirectIntakeWhenLocalEvpProxyIsUnavailable() { .thenReturn(directApi); final BackendApi selected = - new FeatureFlagBackendApiFactory(config, backendApiFactory, "flag evaluation", false) - .create(); + new FeatureFlagBackendApiFactory(config, backendApiFactory, FLAG_EVALUATION).create(); assertSame(directApi, selected); } @@ -82,14 +81,13 @@ void agentlessUsesDirectIntakeWhenLocalEvpProxyIsUnavailable() { void agentlessUsesLocalEvpProxyWhenApiKeyIsUnavailable() { final Config config = config(CONFIGURATION_SOURCE_AGENTLESS, null); final BackendApiFactory backendApiFactory = mock(BackendApiFactory.class); - final BackendApi localApi = mock(BackendApi.class); - when(backendApiFactory.createEvpProxyApi(Intake.EVENT_PLATFORM, false)).thenReturn(localApi); + final BackendApi proxyApi = mock(BackendApi.class); + when(backendApiFactory.createEvpProxyApi(Intake.EVENT_PLATFORM, false)).thenReturn(proxyApi); final BackendApi selected = - new FeatureFlagBackendApiFactory(config, backendApiFactory, "flag evaluation", false) - .create(); + new FeatureFlagBackendApiFactory(config, backendApiFactory, FLAG_EVALUATION).create(); - assertSame(localApi, selected); + assertSame(proxyApi, selected); verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM, false); } @@ -99,8 +97,7 @@ void agentlessDisablesDeliveryWhenNoRouteIsAvailable() { final BackendApiFactory backendApiFactory = mock(BackendApiFactory.class); final BackendApi selected = - new FeatureFlagBackendApiFactory(config, backendApiFactory, "flag evaluation", false) - .create(); + new FeatureFlagBackendApiFactory(config, backendApiFactory, FLAG_EVALUATION).create(); assertNull(selected); } @@ -111,7 +108,7 @@ void agentlessDisablesDeliveryWhenApiKeyIsEmpty() { final BackendApiFactory backendApiFactory = mock(BackendApiFactory.class); final BackendApi selected = - new FeatureFlagBackendApiFactory(config, backendApiFactory, "exposure", true).create(); + new FeatureFlagBackendApiFactory(config, backendApiFactory, EXPOSURE).create(); assertNull(selected); verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM, true); @@ -121,14 +118,13 @@ void agentlessDisablesDeliveryWhenApiKeyIsEmpty() { void agentlessDoesNotValidateDirectUrlWhileLocalRouteIsAvailable() { final Config config = config(CONFIGURATION_SOURCE_AGENTLESS, "api-key"); final BackendApiFactory backendApiFactory = mock(BackendApiFactory.class); - final BackendApi localApi = mock(BackendApi.class); - when(backendApiFactory.createEvpProxyApi(Intake.EVENT_PLATFORM, false)).thenReturn(localApi); + final BackendApi proxyApi = mock(BackendApi.class); + when(backendApiFactory.createEvpProxyApi(Intake.EVENT_PLATFORM, false)).thenReturn(proxyApi); when(backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM, false)) .thenThrow(new IllegalArgumentException("invalid URL")); final BackendApi selected = - new FeatureFlagBackendApiFactory(config, backendApiFactory, "flag evaluation", false) - .create(); + new FeatureFlagBackendApiFactory(config, backendApiFactory, FLAG_EVALUATION).create(); assertInstanceOf(AgentlessFeatureFlagBackendApi.class, selected); verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM, false); @@ -142,8 +138,7 @@ void agentlessDisablesDeliveryWhenDirectUrlIsInvalidAndLocalRouteIsUnavailable() .thenThrow(new IllegalArgumentException("invalid URL")); final BackendApi selected = - new FeatureFlagBackendApiFactory(config, backendApiFactory, "flag evaluation", false) - .create(); + new FeatureFlagBackendApiFactory(config, backendApiFactory, FLAG_EVALUATION).create(); assertNull(selected); } diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationWriterImplTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationWriterImplTest.java index a395162a8b8..5d366c8b689 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationWriterImplTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationWriterImplTest.java @@ -688,7 +688,8 @@ void agentlessWritesFlagEvaluationsDirectlyWhenLocalProxyIsUnavailable() throws when(backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM, false)) .thenReturn(directApi); final FeatureFlagBackendApiFactory featureFlagBackendApiFactory = - new FeatureFlagBackendApiFactory(config, backendApiFactory, "flag evaluation", false); + new FeatureFlagBackendApiFactory( + config, backendApiFactory, FeatureFlagEventType.FLAG_EVALUATION); final PollingConditions poll = new PollingConditions(TIMEOUT_SECONDS); try (FlagEvaluationWriterImpl writer =