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
95eb84b
test(02-05): add failing tests for FlagEvaluationWriterImpl two-tier …
leoromanovsky Jun 12, 2026
724bd9f
feat(02-05): implement FlagEvaluationWriter interface + FlagEvaluatio…
leoromanovsky Jun 12, 2026
304f8d5
test(02-05): add failing tests for FlagEvalEVPHook finallyAfter behavior
leoromanovsky Jun 12, 2026
abbec48
feat(02-05): add FlagEvalEVPHook + gateway wiring + killswitch
leoromanovsky Jun 12, 2026
fcc884e
style(02-05): apply google-java-format via spotlessApply
leoromanovsky Jun 12, 2026
78bbaba
chore(openfeature): remove internal review annotations from EVP flage…
leoromanovsky Jun 12, 2026
be97d8d
feat(openfeature): add EVP flagevaluation error object, deterministic…
leoromanovsky Jun 13, 2026
c8c95a5
chore(openfeature): remove internal planning annotations
leoromanovsky Jun 13, 2026
c459981
fix(openfeature): suppress single-thread SpotBugs MT findings on flag…
leoromanovsky Jun 15, 2026
8ef8ea4
fix(openfeature): align EVP flagevaluation aggregation with worker sc…
leoromanovsky Jun 16, 2026
8823846
Merge remote-tracking branch 'origin/master' into workspace/flag-eval…
leoromanovsky Jun 16, 2026
96a00db
Merge remote-tracking branch 'origin/master' into workspace/flag-eval…
leoromanovsky Jun 19, 2026
8bd298b
Align flagevaluation EVP contract
leoromanovsky Jun 19, 2026
afe4d56
Fix flagevaluation writer test formatting
leoromanovsky Jun 20, 2026
7f0cf1f
fix(openfeature): satisfy flagevaluation spotless
leoromanovsky Jun 20, 2026
cc3c75e
Remove flagevaluation schema validator fixture
leoromanovsky Jun 22, 2026
9cc14b3
Share feature flag EVP publishing
leoromanovsky Jun 22, 2026
fd4dcf6
Merge remote-tracking branch 'origin/master' into workspace/flag-eval…
leoromanovsky Jun 22, 2026
74f1407
Merge remote-tracking branch 'origin/master' into workspace/flag-eval…
leoromanovsky Jun 22, 2026
3d4244f
fix(openfeature): align flagevaluation EVP transport
leoromanovsky Jun 22, 2026
64ede83
fix(openfeature): bound flagevaluation evp payloads
leoromanovsky Jun 23, 2026
033a471
fix(openfeature): emit flagevaluation telemetry counters
leoromanovsky Jun 23, 2026
c211de3
refactor(openfeature): share EVP limits and clarify hooks
leoromanovsky Jun 23, 2026
64f5951
refactor(openfeature): clarify flag eval logging hook
leoromanovsky Jun 23, 2026
6e699cf
style(feature-flagging): fix spotless formatting
leoromanovsky Jun 23, 2026
15a04d7
style(openfeature): format flag eval logging test
leoromanovsky Jun 23, 2026
4a93c61
test(evpproxy): use shared EVP header constant
leoromanovsky Jun 23, 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 @@ -49,6 +54,15 @@ public BackendApiFactory(Config config, SharedCommunicationObjects sharedCommuni
if (featuresDiscovery.supportsEvpProxy()) {
String traceId = config.getIdGenerationStrategy().generateTraceId().toString();
String evpProxyEndpoint = featuresDiscovery.getEvpProxyEndpoint();
if (preferredEvpProxyEndpoint != null
&& featuresDiscovery.supportsEvpProxyEndpoint(preferredEvpProxyEndpoint)) {
evpProxyEndpoint = preferredEvpProxyEndpoint;
Comment on lines 56 to +59

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not fall back to v4 for v2-only flagevaluation

When the flag-evaluation writer requests V2_EVP_PROXY_ENDPOINT, Agents that advertise only /evp_proxy/v4/ leave evpProxyEndpoint at the discovered v4 value because this condition simply skips the preference. The writer then posts to /evp_proxy/v4/api/v2/flagevaluation, despite this signal being documented in the new writer as using /evp_proxy/v2/api/v2/flagevaluation; those environments will send the new telemetry to the wrong Agent route instead of disabling it when v2 is unavailable.

Useful? React with 👍 / 👎.

}
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(
Expand All @@ -57,7 +71,7 @@ public BackendApiFactory(Config config, SharedCommunicationObjects sharedCommuni
subdomain,
retryPolicyFactory,
sharedCommunicationObjects.agentHttpClient,
true);
responseCompression);
}

log.warn(
Expand Down
12 changes: 12 additions & 0 deletions communication/src/main/java/datadog/communication/EvpProxy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package datadog.communication;

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

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

/** EVP uncompressed request-body limit in bytes. */
public static final int PAYLOAD_SIZE_LIMIT_BYTES = 5 * 1024 * 1024;

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 @@ -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 @@ -426,6 +428,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 @@ -3,4 +3,12 @@
public class FeatureFlaggingConfig {

public static final String FLAGGING_PROVIDER_ENABLED = "experimental.flagging.provider.enabled";

/**
* Killswitch for the EVP {@code flagevaluation} emission path. Default: enabled. Disabling it
* turns off EVP flag-evaluation counts while leaving the OTel {@code feature_flag.evaluations}
* metric path untouched. Maps to {@code DD_FLAGGING_EVALUATION_COUNTS_ENABLED}.
*/
public static final String FLAGGING_EVALUATION_COUNTS_ENABLED =
"flagging.evaluation.counts.enabled";
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static datadog.trace.common.writer.DDIntakeWriter.DEFAULT_INTAKE_TIMEOUT;
import static datadog.trace.common.writer.DDIntakeWriter.DEFAULT_INTAKE_VERSION;

import datadog.communication.EvpProxy;
import datadog.communication.http.HttpRetryPolicy;
import datadog.communication.http.OkHttpUtils;
import datadog.trace.api.civisibility.InstrumentationBridge;
Expand All @@ -26,7 +27,6 @@ public class DDEvpProxyApi extends RemoteApi {

private static final Logger log = LoggerFactory.getLogger(DDEvpProxyApi.class);

private static final String DD_EVP_SUBDOMAIN_HEADER = "X-Datadog-EVP-Subdomain";
private static final String CONTENT_ENCODING_HEADER = "Content-Encoding";
private static final String GZIP_CONTENT_TYPE = "gzip";

Expand Down Expand Up @@ -131,7 +131,7 @@ public Response sendSerializedTraces(Payload payload) {
Request.Builder builder =
new Request.Builder()
.url(proxiedApiUrl)
.addHeader(DD_EVP_SUBDOMAIN_HEADER, subdomain)
.addHeader(EvpProxy.SUBDOMAIN_HEADER, subdomain)
.tag(OkHttpUtils.CustomListener.class, telemetryListener);

if (isCompressionEnabled()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static datadog.communication.http.OkHttpUtils.gzippedMsgpackRequestBodyOf;

import datadog.communication.EvpProxy;
import datadog.communication.serialization.GrowableBuffer;
import datadog.communication.serialization.Writable;
import datadog.communication.serialization.msgpack.MsgPackWriter;
Expand Down Expand Up @@ -99,7 +100,7 @@ public class LLMObsSpanMapper implements RemoteMapper {
private int spansWritten;

public LLMObsSpanMapper() {
this(5 << 20);
this(EvpProxy.PAYLOAD_SIZE_LIMIT_BYTES);
}

private LLMObsSpanMapper(int size) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package datadog.trace.common.writer.ddintake

import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import datadog.communication.EvpProxy
import datadog.communication.serialization.ByteBufferConsumer
import datadog.communication.serialization.FlushingBuffer
import datadog.communication.serialization.msgpack.MsgPackWriter
Expand Down Expand Up @@ -64,7 +65,7 @@ class DDEvpProxyApiTest extends DDCoreSpecification {
clientResponse.status().present
clientResponse.status().asInt == 200
agentEvpProxy.getLastRequest().path == path
agentEvpProxy.getLastRequest().getHeader(DDEvpProxyApi.DD_EVP_SUBDOMAIN_HEADER) == intakeSubdomain
agentEvpProxy.getLastRequest().getHeader(EvpProxy.SUBDOMAIN_HEADER) == intakeSubdomain

cleanup:
agentEvpProxy.close()
Expand Down Expand Up @@ -100,7 +101,7 @@ class DDEvpProxyApiTest extends DDCoreSpecification {
clientResponse.status().present
clientResponse.status().asInt == 200
agentEvpProxy.getLastRequest().path == path
agentEvpProxy.getLastRequest().getHeader(DDEvpProxyApi.DD_EVP_SUBDOMAIN_HEADER) == intakeSubdomain
agentEvpProxy.getLastRequest().getHeader(EvpProxy.SUBDOMAIN_HEADER) == intakeSubdomain

cleanup:
agentEvpProxy.close()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ private CoreMetricCollector() {
this.metricsQueue = new ArrayBlockingQueue<>(RAW_QUEUE_SIZE);
}

public void count(String metricName, long value, String tag) {
if (value <= 0) {
return;
}
this.metricsQueue.offer(
new CoreMetric(METRIC_NAMESPACE, true, metricName, "count", value, tag));
}

@Override
public void prepareMetrics() {
// Collect span metrics
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ public enum AgentThread {

LLMOBS_EVALS_PROCESSOR("dd-llmobs-evals-processor"),

FEATURE_FLAG_EXPOSURE_PROCESSOR("dd-ffe-exposure-processor");
FEATURE_FLAG_EXPOSURE_PROCESSOR("dd-ffe-exposure-processor"),

FEATURE_FLAG_EVALUATION_PROCESSOR("dd-ffe-evaluation-processor");

public final String threadName;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,24 @@ class CoreMetricCollectorTest extends DDSpecification {
collector.prepareMetrics()
collector.drain().size() == limit
}

def "direct count core metric"() {
setup:
def collector = CoreMetricCollector.getInstance()
collector.drain()

when:
collector.count('flagevaluation.rows.dropped', 3, 'reason:queue_overflow')
def metrics = collector.drain()

then:
metrics.size() == 1

def metric = metrics[0]
metric.type == 'count'
metric.value == 3
metric.namespace == 'tracers'
metric.metricName == 'flagevaluation.rows.dropped'
metric.tags == ['reason:queue_overflow']
}
}
8 changes: 8 additions & 0 deletions metadata/supported-configurations.json
Original file line number Diff line number Diff line change
Expand Up @@ -1489,6 +1489,14 @@
"aliases": []
}
],
"DD_FLAGGING_EVALUATION_COUNTS_ENABLED": [
{
"version": "A",
"type": "boolean",
"default": "true",
"aliases": []
}
],
"DD_FORCE_CLEAR_TEXT_HTTP_FOR_INTAKE_CLIENT": [
{
"version": "A",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import datadog.communication.ddagent.SharedCommunicationObjects;
import datadog.trace.api.Config;
import datadog.trace.api.config.FeatureFlaggingConfig;
import datadog.trace.api.featureflag.flagevaluation.FlagEvaluationWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -11,6 +13,7 @@ public class FeatureFlaggingSystem {

private static volatile RemoteConfigService CONFIG_SERVICE;
private static volatile ExposureWriter EXPOSURE_WRITER;
private static volatile FlagEvaluationWriter FLAG_EVAL_WRITER;

private FeatureFlaggingSystem() {}

Expand All @@ -27,10 +30,31 @@ public static void start(final SharedCommunicationObjects sco) {
EXPOSURE_WRITER = new ExposureWriterImpl(sco, config);
EXPOSURE_WRITER.init();

// EVP flagevaluation writer — gated by the killswitch
// DD_FLAGGING_EVALUATION_COUNTS_ENABLED (default: on), read through the tracer config system.
final boolean evalCountsEnabled =
config
.configProvider()
.getBoolean(FeatureFlaggingConfig.FLAGGING_EVALUATION_COUNTS_ENABLED, true);
if (evalCountsEnabled) {
final FlagEvaluationWriterImpl evalWriter = new FlagEvaluationWriterImpl(sco, config);
evalWriter.start(); // registers with FeatureFlaggingGateway
FLAG_EVAL_WRITER = evalWriter;
LOGGER.debug("Flag evaluation EVP writer started");
} else {
LOGGER.debug(
"Flag evaluation EVP writer disabled ({}=false)",
FeatureFlaggingConfig.FLAGGING_EVALUATION_COUNTS_ENABLED);
}

LOGGER.debug("Feature Flagging system started");
}

public static void stop() {
if (FLAG_EVAL_WRITER != null) {
FLAG_EVAL_WRITER.close(); // also deregisters from gateway
FLAG_EVAL_WRITER = null;
}
if (EXPOSURE_WRITER != null) {
EXPOSURE_WRITER.close();
EXPOSURE_WRITER = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -387,11 +387,15 @@ private static <T> ProviderEvaluation<T> resolveVariant(
+ e.getMessage());
}

// Stamp eval-time at the resolution point so first/last_evaluation reflect evaluation time,
// not hook-fire time. Passed to the hook via provider metadata "dd.eval.timestamp_ms".
final long evalTimestampMs = System.currentTimeMillis();
final ImmutableMetadata.ImmutableMetadataBuilder metadataBuilder =
ImmutableMetadata.builder()
.addString("flagKey", flag.key)
.addString("variationType", flag.variationType.name())
.addString("allocationKey", allocation.key);
.addString("allocationKey", allocation.key)
.addLong("dd.eval.timestamp_ms", evalTimestampMs);
final ProviderEvaluation<T> result =
ProviderEvaluation.<T>builder()
.value(mappedValue)
Expand Down
Loading