Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -73,9 +74,10 @@ public <T> 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 =
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,26 @@
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 proxyApi;
private final Supplier<BackendApi> directApiSupplier;
private final String eventType;
private volatile BackendApi activeApi;
private volatile boolean directApiCreationAttempted;

AgentlessExposureBackendApi(
final BackendApi localApi, final Supplier<BackendApi> directApiSupplier) {
this.localApi = localApi;
AgentlessFeatureFlagBackendApi(
final BackendApi proxyApi,
final Supplier<BackendApi> directApiSupplier,
final String eventType) {
this.proxyApi = proxyApi;
this.directApiSupplier = directApiSupplier;
this.activeApi = localApi;
this.eventType = eventType;
this.activeApi = proxyApi;
}

@Override
Expand All @@ -43,7 +48,7 @@ public <T> 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;
}

Expand All @@ -58,13 +63,13 @@ public <T> 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) {
Expand All @@ -74,7 +79,8 @@ private BackendApi getOrCreateDirectApi() {
final BackendApi directApi = directApiSupplier.get();
if (directApi != null) {
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;
}
directApiCreationAttempted = true;
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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, FeatureFlagEventType.EXPOSURE),
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 =
Expand Down Expand Up @@ -104,7 +109,7 @@ private static class ExposureSerializingHandler implements Runnable {
private final Runnable errorCallback;

ExposureSerializingHandler(
final ExposureBackendApiFactory backendApiFactory,
final FeatureFlagBackendApiFactory backendApiFactory,
final MessagePassingBlockingQueue<ExposureEvent> queue,
final long flushInterval,
final TimeUnit timeUnit,
Expand Down

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

side comment: Github UI didn't recognize ExposureBackendApiFactory->FeatureFlagBackendApiFactory as a ~rename, but did for AgentlessExposureBackendApi->AgentlessFeatureFlagBackendApi......booo

Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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 FeatureFlagEventType eventType;

FeatureFlagBackendApiFactory(
final Config config,
final SharedCommunicationObjects sharedCommunicationObjects,
final FeatureFlagEventType eventType) {
this(config, new BackendApiFactory(config, sharedCommunicationObjects), eventType);
}

FeatureFlagBackendApiFactory(
final Config config,
final BackendApiFactory backendApiFactory,
final FeatureFlagEventType eventType) {
this.config = config;
this.backendApiFactory = backendApiFactory;
this.eventType = eventType;
}

@Nullable
BackendApi create() {
final BackendApi proxyApi =
backendApiFactory.createEvpProxyApi(
Intake.EVENT_PLATFORM, eventType.responseCompressionEnabled());
if (!CONFIGURATION_SOURCE_AGENTLESS.equals(config.getFeatureFlaggingConfigurationSource())) {
if (proxyApi == null) {
LOGGER.warn(
"Feature Flagging {} delivery is disabled because the local Agent does not support the EVP proxy",
eventType.logName());
}
return proxyApi;
}

if (proxyApi != null) {
if (hasDirectCredentials()) {
return new AgentlessFeatureFlagBackendApi(
proxyApi, this::createDirectApi, eventType.logName());
}
return proxyApi;
}

final BackendApi directApi = createDirectApi();
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.logName());
return null;
}

private boolean hasDirectCredentials() {
final String apiKey = config.getApiKey();
return apiKey != null && !apiKey.isEmpty();
}

@Nullable
private BackendApi createDirectApi() {
if (!hasDirectCredentials()) {
return null;
}
try {
return backendApiFactory.createDirectIntakeApi(
Intake.EVENT_PLATFORM, eventType.responseCompressionEnabled());
} catch (final IllegalArgumentException exception) {
LOGGER.debug(
"Cannot configure direct Feature Flagging {} delivery", eventType.logName(), exception);
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading
Loading