Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
571938b
refactor(evp): centralize EVP proxy routing
leoromanovsky Jul 2, 2026
4949ed7
refactor(feature-flagging): share EVP publishing
leoromanovsky Jul 2, 2026
4aa0647
feat(openfeature): add flagevaluation contract
leoromanovsky Jul 2, 2026
ac126fb
refactor(openfeature): name metrics hook explicitly
leoromanovsky Jul 2, 2026
7a43658
feat(openfeature): add flagevaluation logging hook
leoromanovsky Jul 2, 2026
4ec77ca
test(openfeature): cover flagevaluation logging hook
leoromanovsky Jul 2, 2026
6fa7ade
fix(openfeature): snapshot flagevaluation context
leoromanovsky Jul 2, 2026
986a9af
feat(openfeature): register flagevaluation hook
leoromanovsky Jul 2, 2026
f1c37ab
feat(feature-flagging): canonicalize flagevaluation context
leoromanovsky Jul 2, 2026
115a407
feat(feature-flagging): aggregate flagevaluation rows
leoromanovsky Jul 2, 2026
cc3b21c
test(feature-flagging): cover flagevaluation aggregation
leoromanovsky Jul 2, 2026
31d7cb5
feat(feature-flagging): encode flagevaluation payloads
leoromanovsky Jul 2, 2026
e43f93a
test(feature-flagging): cover flagevaluation payload encoding
leoromanovsky Jul 2, 2026
17d7785
feat(telemetry): support tagged core metric counts
leoromanovsky Jul 2, 2026
5f88cad
feat(feature-flagging): run flagevaluation writer lifecycle
leoromanovsky Jul 2, 2026
597e97d
feat(feature-flagging): post flagevaluation payloads
leoromanovsky Jul 2, 2026
58df936
test(feature-flagging): add flagevaluation test support
leoromanovsky Jul 2, 2026
acc6e21
test(feature-flagging): cover flagevaluation writer
leoromanovsky Jul 2, 2026
96df04f
feat(feature-flagging): wire flagevaluation writer lifecycle
leoromanovsky Jul 2, 2026
edb264e
test(feature-flagging): cover flagevaluation writer lifecycle
leoromanovsky Jul 2, 2026
90ed6cb
perf(feature-flagging): benchmark flagevaluation hot path
leoromanovsky Jul 2, 2026
c73cafd
chore: apply spotless formatting
leoromanovsky Jul 2, 2026
5b1456b
test(feature-flagging): cover flag evaluation jacoco paths
leoromanovsky Jul 2, 2026
6e1f81e
fix(feature-flagging): gate flag evaluation enqueue during shutdown
leoromanovsky Jul 2, 2026
78d8297
test(feature-flagging): cover flag eval event
leoromanovsky Jul 2, 2026
6acbb5e
fix(feature-flagging): make aggregator count atomic
leoromanovsky Jul 3, 2026
6b7aa42
Merge branch 'master' into leo.romanovsky/ffl-2446-evp-flagevaluation…
leoromanovsky Jul 3, 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 @@ -24,6 +24,11 @@ public BackendApiFactory(Config config, SharedCommunicationObjects sharedCommuni
}

public @Nullable BackendApi createBackendApi(Intake intake) {
return createBackendApi(intake, null, true);
}

public @Nullable BackendApi createBackendApi(
Intake intake, @Nullable String preferredEvpProxyEndpoint, boolean responseCompression) {
HttpRetryPolicy.Factory retryPolicyFactory = new HttpRetryPolicy.Factory(5, 100, 2.0, true);

if (intake.isAgentlessEnabled(config)) {
Expand All @@ -46,23 +51,40 @@ public BackendApiFactory(Config config, SharedCommunicationObjects sharedCommuni
DDAgentFeaturesDiscovery featuresDiscovery =
sharedCommunicationObjects.featuresDiscovery(config);
featuresDiscovery.discoverIfOutdated();
if (featuresDiscovery.supportsEvpProxy()) {
String traceId = config.getIdGenerationStrategy().generateTraceId().toString();
String evpProxyEndpoint = featuresDiscovery.getEvpProxyEndpoint();
HttpUrl evpProxyUrl = sharedCommunicationObjects.agentUrl.resolve(evpProxyEndpoint);
String subdomain = intake.getUrlPrefix();
return new EvpProxyApi(
traceId,
evpProxyUrl,
subdomain,
retryPolicyFactory,
sharedCommunicationObjects.agentHttpClient,
true);
String evpProxyEndpoint;
if (preferredEvpProxyEndpoint != null) {
if (!featuresDiscovery.supportsEvpProxyEndpoint(preferredEvpProxyEndpoint)) {
log.warn(
"Cannot create backend API client for {} since agent does not support requested EVP"
+ " proxy endpoint {}",
intake,
preferredEvpProxyEndpoint);
return null;
}
evpProxyEndpoint = preferredEvpProxyEndpoint;
} else if (featuresDiscovery.supportsEvpProxy()) {
evpProxyEndpoint = featuresDiscovery.getEvpProxyEndpoint();
} else {
log.warn(
"Cannot create backend API client since agentless mode is disabled, "
+ "and agent does not support EVP proxy");
return null;
}

log.warn(
"Cannot create backend API client since agentless mode is disabled, "
+ "and agent does not support EVP proxy");
return null;
String traceId = config.getIdGenerationStrategy().generateTraceId().toString();
log.debug(
"Creating EVP proxy client for {} using endpoint {} with responseCompression={}",
intake,
evpProxyEndpoint,
responseCompression);
HttpUrl evpProxyUrl = sharedCommunicationObjects.agentUrl.resolve(evpProxyEndpoint);
String subdomain = intake.getUrlPrefix();
return new EvpProxyApi(
traceId,
evpProxyUrl,
subdomain,
retryPolicyFactory,
sharedCommunicationObjects.agentHttpClient,
responseCompression);
}
}
15 changes: 15 additions & 0 deletions communication/src/main/java/datadog/communication/EvpProxy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package datadog.communication;

/** Shared EVP proxy constants. */
public final class EvpProxy {

public static final String SUBDOMAIN_HEADER = "X-Datadog-EVP-Subdomain";

/**
* Default SDK-side target for uncompressed EVP request bodies. Writers may split batches at or
* below this size to keep Agent proxy requests comfortably bounded.
*/
public static final int PAYLOAD_SIZE_LIMIT_BYTES = 5 * 1024 * 1024;
Comment on lines +8 to +12

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The declared limit in the agent is 10MB, but I'm leaving it at 5 here to not change tracer behavior too much.


private EvpProxy() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ public class EvpProxyApi implements BackendApi {
private static final Logger log = LoggerFactory.getLogger(EvpProxyApi.class);

private static final String API_VERSION = "v2";
private static final String X_DATADOG_EVP_SUBDOMAIN_HEADER = "X-Datadog-EVP-Subdomain";
private static final String X_DATADOG_TRACE_ID_HEADER = "x-datadog-trace-id";
private static final String X_DATADOG_PARENT_ID_HEADER = "x-datadog-parent-id";
private static final String ACCEPT_ENCODING_HEADER = "Accept-Encoding";
Expand Down Expand Up @@ -62,7 +61,7 @@ public <T> T post(
Request.Builder requestBuilder =
new Request.Builder()
.url(url)
.addHeader(X_DATADOG_EVP_SUBDOMAIN_HEADER, subdomain)
.addHeader(EvpProxy.SUBDOMAIN_HEADER, subdomain)
.addHeader(X_DATADOG_TRACE_ID_HEADER, traceId)
.addHeader(X_DATADOG_PARENT_ID_HEADER, traceId);

Expand All @@ -79,6 +78,11 @@ public <T> T post(
}

final Request request = requestBuilder.post(requestBody).build();
log.debug(
"Posting EVP request to {} with responseCompression={} requestCompression={}",
url,
responseCompression,
requestCompression);

try (okhttp3.Response response =
OkHttpUtils.sendWithRetries(httpClient, retryPolicyFactory, request)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ private static class State {
String debuggerSnapshotEndpoint;
String debuggerDiagnosticsEndpoint;
String evpProxyEndpoint;
Set<String> evpProxyEndpoints = emptySet();
String version;
String telemetryProxyEndpoint;
Set<String> peerTags = emptySet();
Expand Down Expand Up @@ -140,7 +141,7 @@ protected long getFeaturesDiscoveryMinDelayMillis() {
private synchronized void discoverIfOutdated(final long maxElapsedMs) {
final long now = System.currentTimeMillis();
final long elapsed = now - discoveryState.lastTimeDiscovered;
if (elapsed > maxElapsedMs) {
if (elapsed >= maxElapsedMs) {
final State newState = new State();
doDiscovery(newState);
newState.lastTimeDiscovered = now;
Expand Down Expand Up @@ -288,6 +289,7 @@ private boolean processInfoResponse(State newState, String response) {
break;
}
}
newState.evpProxyEndpoints = unmodifiableSet(endpoints);

for (String endpoint : telemetryProxyEndpoints) {
if (containsEndpoint(endpoints, endpoint)) {
Expand Down Expand Up @@ -422,6 +424,10 @@ public String getEvpProxyEndpoint() {
return discoveryState.evpProxyEndpoint;
}

public boolean supportsEvpProxyEndpoint(String endpoint) {
return containsEndpoint(discoveryState.evpProxyEndpoints, endpoint);
}

public HttpUrl buildUrl(String endpoint) {
return agentBaseUrl.resolve(endpoint);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import static datadog.communication.ddagent.DDAgentFeaturesDiscovery.V05_ENDPOIN
import static datadog.communication.ddagent.DDAgentFeaturesDiscovery.V06_METRICS_ENDPOINT
import static datadog.communication.ddagent.DDAgentFeaturesDiscovery.V07_CONFIG_ENDPOINT
import static datadog.communication.ddagent.DDAgentFeaturesDiscovery.V1_ENDPOINT
import static datadog.communication.ddagent.DDAgentFeaturesDiscovery.V2_EVP_PROXY_ENDPOINT
import static datadog.communication.ddagent.DDAgentFeaturesDiscovery.V4_EVP_PROXY_ENDPOINT
import static datadog.communication.http.OkHttpUtils.DATADOG_CONTAINER_ID
import static datadog.communication.http.OkHttpUtils.DATADOG_CONTAINER_TAGS_HASH
import static datadog.trace.api.ProtocolVersion.V0_4
Expand Down Expand Up @@ -75,6 +77,8 @@ class DDAgentFeaturesDiscoveryTest extends DDSpecification {
features.supportsEvpProxy()
features.supportsContentEncodingHeadersWithEvpProxy()
features.getEvpProxyEndpoint() == "evp_proxy/v4/"
features.supportsEvpProxyEndpoint(V2_EVP_PROXY_ENDPOINT)
features.supportsEvpProxyEndpoint(V4_EVP_PROXY_ENDPOINT)
features.getVersion() == "0.99.0"
!features.supportsLongRunning()
!features.supportsTelemetryProxy()
Expand Down Expand Up @@ -489,6 +493,8 @@ class DDAgentFeaturesDiscoveryTest extends DDSpecification {
1 * client.newCall(_) >> { Request request -> infoResponse(request, INFO_WITH_OLD_EVP_PROXY) }
features.supportsEvpProxy()
features.getEvpProxyEndpoint() == "evp_proxy/v2/" // v3 is advertised, but the tracer should ignore it
features.supportsEvpProxyEndpoint(V2_EVP_PROXY_ENDPOINT)
!features.supportsEvpProxyEndpoint(V4_EVP_PROXY_ENDPOINT)
!features.supportsContentEncodingHeadersWithEvpProxy()
features.supportsDebugger()
features.getDebuggerSnapshotEndpoint() == "debugger/v1/diagnostics"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package datadog.communication;

import static datadog.communication.ddagent.DDAgentFeaturesDiscovery.V2_EVP_PROXY_ENDPOINT;
import static datadog.communication.ddagent.DDAgentFeaturesDiscovery.V4_EVP_PROXY_ENDPOINT;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;

import datadog.communication.ddagent.DDAgentFeaturesDiscovery;
import datadog.communication.ddagent.SharedCommunicationObjects;
import datadog.metrics.api.Monitoring;
import datadog.trace.api.Config;
import datadog.trace.api.ProtocolVersion;
import datadog.trace.api.intake.Intake;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import okhttp3.HttpUrl;
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.Test;

class BackendApiFactoryTest {

private static final MediaType JSON = MediaType.parse("application/json");

@Test
void preferredEvpProxyEndpointMustBeAdvertisedByAgent() {
final FakeFeaturesDiscovery discovery =
new FakeFeaturesDiscovery(
V4_EVP_PROXY_ENDPOINT, Collections.singleton(V4_EVP_PROXY_ENDPOINT));
final BackendApiFactory factory =
new BackendApiFactory(Config.get(), sharedCommunicationObjects(discovery, null));

assertNull(factory.createBackendApi(Intake.EVENT_PLATFORM, V2_EVP_PROXY_ENDPOINT, false));
}

@Test
void preferredEvpProxyEndpointUsesRequestedRouteWhenAdvertised() throws Exception {
final MockWebServer agent = new MockWebServer();
agent.enqueue(new MockResponse().setResponseCode(200).setBody("{}"));
agent.start();
try {
final FakeFeaturesDiscovery discovery =
new FakeFeaturesDiscovery(
V4_EVP_PROXY_ENDPOINT,
new HashSet<>(Arrays.asList(V4_EVP_PROXY_ENDPOINT, V2_EVP_PROXY_ENDPOINT)));
final BackendApiFactory factory =
new BackendApiFactory(
Config.get(), sharedCommunicationObjects(discovery, agent.url("/")));
final BackendApi api =
factory.createBackendApi(Intake.EVENT_PLATFORM, V2_EVP_PROXY_ENDPOINT, false);

assertNotNull(api);
api.post(
"flagevaluation",
RequestBody.create(JSON, "{}".getBytes(StandardCharsets.UTF_8)),
stream -> null,
null,
false);

final RecordedRequest request = agent.takeRequest();
assertEquals("/evp_proxy/v2/api/v2/flagevaluation", request.getPath());
} finally {
agent.shutdown();
}
}

@Test
void preferredEvpProxyEndpointDoesNotRequireLegacyDefaultRoute() throws Exception {
final String futureEndpoint = "evp_proxy/v9/";
final MockWebServer agent = new MockWebServer();
agent.enqueue(new MockResponse().setResponseCode(200).setBody("{}"));
agent.start();
try {
final FakeFeaturesDiscovery discovery =
new FakeFeaturesDiscovery(null, Collections.singleton(futureEndpoint));
final BackendApiFactory factory =
new BackendApiFactory(
Config.get(), sharedCommunicationObjects(discovery, agent.url("/")));
final BackendApi api = factory.createBackendApi(Intake.EVENT_PLATFORM, futureEndpoint, false);

assertNotNull(api);
api.post(
"flagevaluation",
RequestBody.create(JSON, "{}".getBytes(StandardCharsets.UTF_8)),
stream -> null,
null,
false);

final RecordedRequest request = agent.takeRequest();
assertEquals("/evp_proxy/v9/api/v2/flagevaluation", request.getPath());
} finally {
agent.shutdown();
}
}

@Test
void defaultEvpProxyEndpointSupportsDisabledResponseCompression() throws Exception {
final MockWebServer agent = new MockWebServer();
agent.enqueue(new MockResponse().setResponseCode(200).setBody("{}"));
agent.start();
try {
final FakeFeaturesDiscovery discovery =
new FakeFeaturesDiscovery(
V4_EVP_PROXY_ENDPOINT,
new HashSet<>(Arrays.asList(V4_EVP_PROXY_ENDPOINT, V2_EVP_PROXY_ENDPOINT)));
final BackendApiFactory factory =
new BackendApiFactory(
Config.get(), sharedCommunicationObjects(discovery, agent.url("/")));
final BackendApi api = factory.createBackendApi(Intake.EVENT_PLATFORM, null, false);

assertNotNull(api);
api.post(
"flagevaluation",
RequestBody.create(JSON, "{}".getBytes(StandardCharsets.UTF_8)),
stream -> null,
null,
false);

final RecordedRequest request = agent.takeRequest();
assertEquals("/evp_proxy/v4/api/v2/flagevaluation", request.getPath());
} finally {
agent.shutdown();
}
}

private static SharedCommunicationObjects sharedCommunicationObjects(
final DDAgentFeaturesDiscovery discovery, final HttpUrl agentUrl) {
final TestSharedCommunicationObjects sco = new TestSharedCommunicationObjects(discovery);
sco.agentUrl = agentUrl != null ? agentUrl : HttpUrl.get("http://localhost:8126/");
sco.agentHttpClient = new OkHttpClient();
return sco;
}

private static final class TestSharedCommunicationObjects extends SharedCommunicationObjects {
private final DDAgentFeaturesDiscovery discovery;

private TestSharedCommunicationObjects(final DDAgentFeaturesDiscovery discovery) {
this.discovery = discovery;
}

@Override
public DDAgentFeaturesDiscovery featuresDiscovery(final Config config) {
return discovery;
}
}

private static final class FakeFeaturesDiscovery extends DDAgentFeaturesDiscovery {
private final String evpProxyEndpoint;
private final Set<String> evpProxyEndpoints;

private FakeFeaturesDiscovery(
final String evpProxyEndpoint, final Set<String> evpProxyEndpoints) {
super(
new OkHttpClient(),
Monitoring.DISABLED,
HttpUrl.get("http://localhost:8126/"),
ProtocolVersion.V0_5,
true,
false);
this.evpProxyEndpoint = evpProxyEndpoint;
this.evpProxyEndpoints = evpProxyEndpoints;
}

@Override
public void discoverIfOutdated() {}

@Override
public String getEvpProxyEndpoint() {
return evpProxyEndpoint;
}

@Override
public boolean supportsEvpProxyEndpoint(final String endpoint) {
return evpProxyEndpoints.contains(endpoint) || evpProxyEndpoints.contains("/" + endpoint);
}

@Override
public boolean supportsEvpProxy() {
return evpProxyEndpoint != null;
}
}
}
Loading