Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 10 additions & 1 deletion .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ build:
script:
- if [ $CI_PIPELINE_SOURCE == "schedule" ] ; then ./gradlew resolveAndLockAll --write-locks $GRADLE_ARGS; fi
- ./gradlew --version
- ./gradlew clean :dd-java-agent:shadowJar :dd-java-agent:check :dd-trace-api:jar :dd-trace-ot:shadowJar -PskipTests -x spotlessCheck $GRADLE_ARGS
- ./gradlew clean :dd-java-agent:shadowJar :dd-java-agent:check :dd-trace-api:jar :dd-trace-ot:shadowJar :products:feature-flagging:feature-flagging-api:jar -PskipTests -x spotlessCheck $GRADLE_ARGS
- echo UPSTREAM_TRACER_VERSION=$(java -jar workspace/dd-java-agent/build/libs/*.jar) >> upstream.env
- echo "BUILD_JOB_NAME=$CI_JOB_NAME" >> build.env
- echo "BUILD_JOB_ID=$CI_JOB_ID" >> build.env
Expand All @@ -360,6 +360,7 @@ build:
- 'workspace/dd-java-agent/build/libs/*.jar'
- 'workspace/dd-trace-api/build/libs/*.jar'
- 'workspace/dd-trace-ot/build/libs/*.jar'
- 'workspace/products/feature-flagging/feature-flagging-api/build/libs/*.jar'
- 'upstream.env'
- '.gradle/daemon/*/*.out.log'
reports:
Expand Down Expand Up @@ -425,9 +426,11 @@ publish-artifacts-to-s3:
- aws s3 cp workspace/dd-java-agent/build/libs/dd-java-agent-${VERSION}.jar s3://dd-trace-java-builds/${CI_COMMIT_REF_NAME}/dd-java-agent.jar
- aws s3 cp workspace/dd-trace-api/build/libs/dd-trace-api-${VERSION}.jar s3://dd-trace-java-builds/${CI_COMMIT_REF_NAME}/dd-trace-api.jar
- aws s3 cp workspace/dd-trace-ot/build/libs/dd-trace-ot-${VERSION}.jar s3://dd-trace-java-builds/${CI_COMMIT_REF_NAME}/dd-trace-ot.jar
- aws s3 cp workspace/products/feature-flagging/feature-flagging-api/build/libs/dd-openfeature-${VERSION}.jar s3://dd-trace-java-builds/${CI_COMMIT_REF_NAME}/dd-openfeature.jar
- aws s3 cp workspace/dd-java-agent/build/libs/dd-java-agent-${VERSION}.jar s3://dd-trace-java-builds/${CI_PIPELINE_ID}/dd-java-agent.jar
- aws s3 cp workspace/dd-trace-api/build/libs/dd-trace-api-${VERSION}.jar s3://dd-trace-java-builds/${CI_PIPELINE_ID}/dd-trace-api.jar
- aws s3 cp workspace/dd-trace-ot/build/libs/dd-trace-ot-${VERSION}.jar s3://dd-trace-java-builds/${CI_PIPELINE_ID}/dd-trace-ot.jar
- aws s3 cp workspace/products/feature-flagging/feature-flagging-api/build/libs/dd-openfeature-${VERSION}.jar s3://dd-trace-java-builds/${CI_PIPELINE_ID}/dd-openfeature.jar
- |
cat << EOF > links.json
{
Expand All @@ -437,6 +440,12 @@ publish-artifacts-to-s3:
"label": "Public Link to dd-java-agent.jar",
"url": "https://s3.us-east-1.amazonaws.com/dd-trace-java-builds/${CI_PIPELINE_ID}/dd-java-agent.jar"
}
},
{
"external_link": {
"label": "Public Link to dd-openfeature.jar",
"url": "https://s3.us-east-1.amazonaws.com/dd-trace-java-builds/${CI_PIPELINE_ID}/dd-openfeature.jar"
}
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ void testFeatureFlagSystemInitialization() {

FeatureFlaggingSystem.start(sharedCommunicationObjects);
FeatureFlaggingSystem.start(sharedCommunicationObjects);
FeatureFlaggingGateway.activate();
FeatureFlaggingGateway.activate();

verify(poller).addCapabilities(Capabilities.CAPABILITY_FFE_FLAG_CONFIGURATION_RULES);
verify(poller).addListener(eq(Product.FFE_FLAGS), any(ConfigurationDeserializer.class), any());
Expand Down
37 changes: 37 additions & 0 deletions products/feature-flagging/feature-flagging-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ Published as `com.datadoghq:dd-openfeature` on Maven Central.

The OpenFeature SDK (`dev.openfeature:sdk`) is included as a transitive dependency.

`dd-openfeature` 1.66.0 and later requires `dd-java-agent` 1.66.0 or later because it uses
the process-wide Feature Flagging configuration snapshot API.

### Evaluation metrics (optional)

To enable evaluation metrics (`feature_flag.evaluations` counter), enable the Datadog Java agent's
Expand Down Expand Up @@ -55,6 +58,40 @@ boolean enabled = client.getBooleanValue("my-feature", false,
new MutableContext("user-123"));
```

### Inspecting evaluations without telemetry
Comment thread
leoromanovsky marked this conversation as resolved.

Use OpenFeature domains when one client should perform normal live evaluations and another should
only inspect the result. Register a separate `Provider` for each domain and disable telemetry on the
Comment thread
leoromanovsky marked this conversation as resolved.
provider assigned to the inspection domain:

```java
OpenFeatureAPI api = OpenFeatureAPI.getInstance();

api.setProviderAndWait("live", new Provider());
api.setProviderAndWait(
"peek",
new Provider(new Provider.Options().telemetryEnabled(false)));

Client checkoutClient = api.getClient("live");
Client analyticsClient = api.getClient("peek");

EvaluationContext checkoutContext = new MutableContext("session-abc");
EvaluationContext analyticsContext = new MutableContext("user-123");

// Evaluates normally and emits the configured Datadog telemetry.
boolean checkoutEnabled = checkoutClient.getBooleanValue(
"my-feature", false, checkoutContext);

// Evaluates against the shared current configuration without emitting Datadog telemetry.
boolean analyticsEnabled = analyticsClient.getBooleanValue(
"my-feature", false, analyticsContext);
```

`telemetryEnabled(false)` suppresses exposures, EVP flag-evaluation events, OpenTelemetry
evaluation metrics, and APM span enrichment for that provider. It does not disable evaluation or
change the configuration used to resolve flags. All provider domains read the same process-wide
configuration snapshot and share one configuration request path.

## Evaluation metrics

When `DD_METRICS_OTEL_ENABLED=true` and the OpenTelemetry API is on the classpath, the provider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static java.util.Arrays.asList;

import datadog.trace.api.featureflag.FeatureFlaggingGateway;
import datadog.trace.api.featureflag.FeatureFlaggingGateway.ConfigSnapshot;
import datadog.trace.api.featureflag.exposure.ExposureEvent;
import datadog.trace.api.featureflag.exposure.Subject;
import datadog.trace.api.featureflag.ufc.v1.Allocation;
Expand Down Expand Up @@ -43,7 +44,6 @@
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

Expand Down Expand Up @@ -111,11 +111,17 @@ class DDEvaluator implements Evaluator, FeatureFlaggingGateway.ConfigListener {
private static final boolean SPAN_ENRICHMENT_ENABLED = SpanEnrichmentGate.isEnabled();

private final Runnable configCallback;
private final AtomicReference<ServerConfiguration> configuration = new AtomicReference<>();
private final boolean telemetryEnabled;
Comment thread
leoromanovsky marked this conversation as resolved.
private final CountDownLatch initializationLatch = new CountDownLatch(1);
private long lastConfigVersion;

public DDEvaluator(final Runnable configCallback) {
DDEvaluator(final Runnable configCallback) {
this(configCallback, true);
}

public DDEvaluator(final Runnable configCallback, final boolean telemetryEnabled) {
this.configCallback = configCallback;
this.telemetryEnabled = telemetryEnabled;
}

@Override
Expand All @@ -128,7 +134,7 @@ public boolean initialize(

@Override
public boolean hasConfiguration() {
return configuration.get() != null;
return FeatureFlaggingGateway.getConfigSnapshot().getConfig() != null;
Comment thread
leoromanovsky marked this conversation as resolved.
}

@Override
Expand All @@ -137,8 +143,15 @@ public void shutdown() {
}

@Override
public void accept(final ServerConfiguration config) {
configuration.set(config);
public synchronized void accept(final ServerConfiguration ignored) {
// Listener callbacks are notifications only. Always read the process-wide snapshot so a stale
// register-and-replay callback cannot restore an older configuration in this evaluator.
final ConfigSnapshot snapshot = FeatureFlaggingGateway.getConfigSnapshot();
if (snapshot.getVersion() <= lastConfigVersion) {
return;
}
lastConfigVersion = snapshot.getVersion();
final ServerConfiguration config = snapshot.getConfig();
if (config != null) {
initializationLatch.countDown();
configCallback.run();
Expand All @@ -156,7 +169,8 @@ public <T> ProviderEvaluation<T> evaluate(
// Snapshot the config once and thread observeFullEvaluationData through every
// ProviderEvaluation returned, so the hook's consent decision is pinned to this evaluation's
// config and cannot drift on a concurrent Remote Config swap.
final ServerConfiguration config = configuration.get();
final ConfigSnapshot snapshot = FeatureFlaggingGateway.getConfigSnapshot();
final ServerConfiguration config = snapshot.getConfig();
// Boolean.TRUE.equals covers both null (privacy-preserving default) and Boolean.FALSE without
// an NPE — the field is boxed so a malformed UFC message doesn't abort the whole parse.
final boolean observeFullEvaluationData =
Expand Down Expand Up @@ -495,7 +509,7 @@ private static String getMD5Hash(final String input) {
}
}

private static <T> ProviderEvaluation<T> resolveVariant(
private <T> ProviderEvaluation<T> resolveVariant(
Comment thread
leoromanovsky marked this conversation as resolved.
final Class<T> target,
final String key,
final T defaultValue,
Expand Down Expand Up @@ -552,11 +566,12 @@ private static <T> ProviderEvaluation<T> resolveVariant(
.addLong("__dd_eval_timestamp_ms", evalTimestampMs)
.addBoolean(METADATA_OBSERVE_FULL_EVALUATION_DATA, observeFullEvaluationData);
// Surface the UFC split's serial id and the allocation's doLog flag for APM span enrichment —
// only when span enrichment is on, so a provider without enrichment pays nothing extra.
// only when telemetry and span enrichment are on, so a provider without enrichment pays
// nothing extra.
// __dd_split_serial_id is omitted when the split carries no serial id; __dd_do_log is always
// present (when enrichment is on) so the span-enrichment hook can decide whether to record the
// subject.
if (SPAN_ENRICHMENT_ENABLED) {
if (telemetryEnabled && SPAN_ENRICHMENT_ENABLED) {
if (split.serialId != null) {
metadataBuilder.addInteger(METADATA_SPLIT_SERIAL_ID, split.serialId);
}
Expand All @@ -575,7 +590,7 @@ private static <T> ProviderEvaluation<T> resolveVariant(
.flagMetadata(metadataBuilder.build())
.build();
final boolean doLog = allocation.doLog != null && allocation.doLog;
if (doLog) {
if (telemetryEnabled && doLog) {
Comment thread
leoromanovsky marked this conversation as resolved.
dispatchExposure(key, result, context);
}
return result;
Expand Down
Loading