Skip to content

Commit fe88e77

Browse files
Initial implementation of the OpenFeature SDK
1 parent 640a4bd commit fe88e77

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+3360
-2
lines changed

.github/CODEOWNERS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,6 @@ dd-trace-core/src/test/groovy/datadog/trace/llmobs/ @DataDog/ml-observability
139139
/internal-api/src/test/groovy/datadog/trace/api/rum/ @DataDog/rum
140140
/telemetry/src/main/java/datadog/telemetry/rum/ @DataDog/rum
141141
/telemetry/src/test/groovy/datadog/telemetry/rum/ @DataDog/rum
142+
143+
# @DataDog/feature-flagging-and-experimentation-sdk
144+
/products/openfeature/ @DataDog/feature-flagging-and-experimentation-sdk

internal-api/src/main/java/datadog/trace/api/intake/Intake.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,18 @@ public enum Intake {
1515
"ci-intake",
1616
"v2",
1717
Config::isCiVisibilityAgentlessEnabled,
18-
Config::getCiVisibilityIntakeAgentlessUrl);
18+
Config::getCiVisibilityIntakeAgentlessUrl),
19+
EVENT_PLATFORM("event-platform-intake", "v2");
1920

2021
public final String urlPrefix;
2122
public final String version;
2223
public final Function<Config, Boolean> agentlessModeEnabled;
2324
public final Function<Config, String> customUrl;
2425

26+
Intake(String urlPrefix, String version) {
27+
this(urlPrefix, version, config -> false, config -> null);
28+
}
29+
2530
Intake(
2631
String urlPrefix,
2732
String version,

internal-api/src/main/java/datadog/trace/util/AgentThreadFactory.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ public enum AgentThread {
5959

6060
LOGS_INTAKE("dd-logs-intake"),
6161

62-
LLMOBS_EVALS_PROCESSOR("dd-llmobs-evals-processor");
62+
LLMOBS_EVALS_PROCESSOR("dd-llmobs-evals-processor"),
63+
64+
FEATURE_FLAG_EXPOSURE_PROCESSOR("dd-ffe-exposure-processor");
6365

6466
public final String threadName;
6567

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import de.thetaphi.forbiddenapis.gradle.CheckForbiddenApis
2+
import groovy.lang.Closure
3+
4+
plugins {
5+
`java-library`
6+
idea
7+
}
8+
9+
apply(from = "$rootDir/gradle/java.gradle")
10+
apply(from = "$rootDir/gradle/publish.gradle")
11+
12+
val minJavaVersionForTests by extra(JavaVersion.VERSION_11)
13+
14+
description = "dd-openfeature"
15+
16+
idea {
17+
module {
18+
jdkName = "11"
19+
}
20+
}
21+
22+
java {
23+
toolchain {
24+
languageVersion = JavaLanguageVersion.of(11)
25+
}
26+
}
27+
28+
val excludedClassesCoverage by extra(
29+
listOf(
30+
// Feature lags POJOs
31+
"datadog.trace.api.openfeature.exposure.ExposureCache.Key",
32+
"datadog.trace.api.openfeature.exposure.ExposureCache.Value",
33+
"datadog.trace.api.openfeature.exposure.dto.Allocation",
34+
"datadog.trace.api.openfeature.exposure.dto.ExposureEvent",
35+
"datadog.trace.api.openfeature.exposure.dto.ExposuresRequest",
36+
"datadog.trace.api.openfeature.exposure.dto.Flag",
37+
"datadog.trace.api.openfeature.exposure.dto.Subject",
38+
"datadog.trace.api.openfeature.exposure.dto.Variant",
39+
"datadog.trace.api.openfeature.config.ufc.v1.Allocation",
40+
"datadog.trace.api.openfeature.config.ufc.v1.ConditionConfiguration",
41+
"datadog.trace.api.openfeature.config.ufc.v1.ConditionOperator",
42+
"datadog.trace.api.openfeature.config.ufc.v1.Environment",
43+
"datadog.trace.api.openfeature.config.ufc.v1.Flag",
44+
"datadog.trace.api.openfeature.config.ufc.v1.Rule",
45+
"datadog.trace.api.openfeature.config.ufc.v1.ServerConfiguration",
46+
"datadog.trace.api.openfeature.config.ufc.v1.Shard",
47+
"datadog.trace.api.openfeature.config.ufc.v1.ShardRange",
48+
"datadog.trace.api.openfeature.config.ufc.v1.Split",
49+
"datadog.trace.api.openfeature.config.ufc.v1.ValueType",
50+
"datadog.trace.api.openfeature.config.ufc.v1.Variant",
51+
)
52+
)
53+
54+
dependencies {
55+
api("dev.openfeature:sdk:1.18.2")
56+
api(libs.slf4j)
57+
api(libs.moshi)
58+
api(libs.okhttp)
59+
api(libs.jctools)
60+
61+
compileOnly(project(":remote-config:remote-config-api"))
62+
compileOnly(project(":communication"))
63+
compileOnly(project(":internal-api"))
64+
65+
testImplementation(project(":utils:test-utils"))
66+
testImplementation(project(":dd-java-agent:testing"))
67+
testImplementation(project(":remote-config:remote-config-api"))
68+
testImplementation(project(":communication"))
69+
testImplementation(project(":internal-api"))
70+
}
71+
72+
fun AbstractCompile.configureCompiler(
73+
javaVersionInteger: Int,
74+
compatibilityVersion: JavaVersion? = null,
75+
unsetReleaseFlagReason: String? = null
76+
) {
77+
(project.extra["configureCompiler"] as Closure<*>).call(
78+
this,
79+
javaVersionInteger,
80+
compatibilityVersion,
81+
unsetReleaseFlagReason
82+
)
83+
}
84+
85+
tasks.withType<JavaCompile>().configureEach {
86+
configureCompiler(11, JavaVersion.VERSION_11)
87+
}
88+
89+
tasks.withType<Javadoc>().configureEach {
90+
javadocTool = javaToolchains.javadocToolFor(java.toolchain)
91+
}
92+
93+
tasks.named<CheckForbiddenApis>("forbiddenApisMain") {
94+
failOnMissingClasses = false
95+
}
96+
97+
tasks.named<Jar>("jar") {
98+
archiveBaseName.set("dd-openfeature")
99+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package datadog.trace.api.openfeature;
2+
3+
import datadog.communication.ddagent.SharedCommunicationObjects;
4+
import datadog.trace.api.Config;
5+
import datadog.trace.api.openfeature.config.RemoteConfigService;
6+
import datadog.trace.api.openfeature.config.RemoteConfigServiceImpl;
7+
import datadog.trace.api.openfeature.config.ServerConfigurationListener;
8+
import datadog.trace.api.openfeature.config.ufc.v1.ServerConfiguration;
9+
import datadog.trace.api.openfeature.evaluator.FeatureFlagEvaluator;
10+
import datadog.trace.api.openfeature.evaluator.FeatureFlagEvaluatorImpl;
11+
import datadog.trace.api.openfeature.exposure.ExposureWriter;
12+
import datadog.trace.api.openfeature.exposure.ExposureWriterImpl;
13+
import dev.openfeature.sdk.EventProvider;
14+
import dev.openfeature.sdk.ProviderEvent;
15+
import dev.openfeature.sdk.ProviderEventDetails;
16+
import java.util.concurrent.CountDownLatch;
17+
import java.util.concurrent.TimeUnit;
18+
import java.util.concurrent.atomic.AtomicBoolean;
19+
20+
public class DDProviderInitializer implements Initializer, ServerConfigurationListener {
21+
22+
private final AtomicBoolean initialized = new AtomicBoolean(false);
23+
private final CountDownLatch configurationLatch = new CountDownLatch(1);
24+
private EventProvider eventProvider;
25+
private RemoteConfigServiceImpl remoteConfigService;
26+
private ExposureWriter exposureWriter;
27+
private FeatureFlagEvaluator evaluator;
28+
29+
@Override
30+
public boolean init(final EventProvider provider, final long timeout, final TimeUnit timeUnit)
31+
throws Exception {
32+
final Config config = Config.get();
33+
final SharedCommunicationObjects sco = new SharedCommunicationObjects();
34+
eventProvider = provider;
35+
exposureWriter = new ExposureWriterImpl(sco, config);
36+
evaluator = new FeatureFlagEvaluatorImpl(exposureWriter);
37+
remoteConfigService = new RemoteConfigServiceImpl(sco, config, this);
38+
return configurationLatch.await(timeout, timeUnit);
39+
}
40+
41+
@Override
42+
public void close() {
43+
remoteConfigService.close();
44+
exposureWriter.close();
45+
}
46+
47+
@Override
48+
public FeatureFlagEvaluator evaluator() {
49+
return evaluator;
50+
}
51+
52+
@Override
53+
public ExposureWriter exposureWriter() {
54+
return exposureWriter;
55+
}
56+
57+
@Override
58+
public RemoteConfigService remoteConfigService() {
59+
return remoteConfigService;
60+
}
61+
62+
@Override
63+
public void onConfiguration(final ServerConfiguration configuration) {
64+
evaluator.onConfiguration(configuration);
65+
if (!initialized.getAndSet(true)) {
66+
eventProvider.emit(ProviderEvent.PROVIDER_READY, ProviderEventDetails.builder().build());
67+
} else {
68+
eventProvider.emit(
69+
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build());
70+
}
71+
configurationLatch.countDown();
72+
}
73+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package datadog.trace.api.openfeature;
2+
3+
import datadog.trace.api.openfeature.config.RemoteConfigService;
4+
import datadog.trace.api.openfeature.evaluator.FeatureFlagEvaluator;
5+
import datadog.trace.api.openfeature.exposure.ExposureWriter;
6+
import de.thetaphi.forbiddenapis.SuppressForbidden;
7+
import dev.openfeature.sdk.EventProvider;
8+
import java.lang.reflect.Constructor;
9+
import java.util.concurrent.TimeUnit;
10+
11+
public interface Initializer {
12+
13+
boolean init(EventProvider provider, long timeout, TimeUnit timeUnit) throws Exception;
14+
15+
void close();
16+
17+
RemoteConfigService remoteConfigService();
18+
19+
FeatureFlagEvaluator evaluator();
20+
21+
ExposureWriter exposureWriter();
22+
23+
/**
24+
* Uses reflection to load the initializer so we are in control of the loading of internal Datadog
25+
* classes
26+
*/
27+
static Initializer withReflection(final String initializerClass) {
28+
return new Initializer() {
29+
30+
private Initializer delegate;
31+
32+
@Override
33+
public boolean init(final EventProvider provider, final long timeout, final TimeUnit timeUnit)
34+
throws Exception {
35+
delegate = loadInitializer();
36+
return delegate.init(provider, timeout, timeUnit);
37+
}
38+
39+
@Override
40+
public void close() {
41+
delegate.close();
42+
}
43+
44+
@Override
45+
public FeatureFlagEvaluator evaluator() {
46+
return delegate.evaluator();
47+
}
48+
49+
@Override
50+
public ExposureWriter exposureWriter() {
51+
return delegate.exposureWriter();
52+
}
53+
54+
@Override
55+
public RemoteConfigService remoteConfigService() {
56+
return delegate.remoteConfigService();
57+
}
58+
59+
@SuppressForbidden // Class#forName(String) used to lazy load Datadog internal dependencies
60+
private Initializer loadInitializer() throws Exception {
61+
final Class<?> clazz = Class.forName(initializerClass);
62+
final Constructor<?> constructor = clazz.getConstructor();
63+
return (Initializer) constructor.newInstance();
64+
}
65+
};
66+
}
67+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package datadog.trace.api.openfeature;
2+
3+
import static java.util.concurrent.TimeUnit.SECONDS;
4+
5+
import dev.openfeature.sdk.EvaluationContext;
6+
import dev.openfeature.sdk.EventProvider;
7+
import dev.openfeature.sdk.Metadata;
8+
import dev.openfeature.sdk.ProviderEvaluation;
9+
import dev.openfeature.sdk.Value;
10+
import dev.openfeature.sdk.exceptions.FatalError;
11+
import dev.openfeature.sdk.exceptions.OpenFeatureError;
12+
import dev.openfeature.sdk.exceptions.ProviderNotReadyError;
13+
import java.util.concurrent.TimeUnit;
14+
15+
public class Provider extends EventProvider implements Metadata {
16+
17+
static final String METADATA = "datadog-openfeature-provider";
18+
static final Options DEFAULT_OPTIONS = new Options().initTimeout(30, SECONDS);
19+
static final String INITIALIZER_CLASS = "datadog.trace.api.openfeature.DDProviderInitializer";
20+
21+
private final Options options;
22+
private final Initializer initializer;
23+
24+
public Provider() {
25+
this(DEFAULT_OPTIONS);
26+
}
27+
28+
public Provider(final Options options) {
29+
this(options, Initializer.withReflection(INITIALIZER_CLASS));
30+
}
31+
32+
Provider(final Options options, final Initializer initializer) {
33+
this.options = options;
34+
this.initializer = initializer;
35+
}
36+
37+
@Override
38+
public void initialize(final EvaluationContext context) throws Exception {
39+
try {
40+
if (!initializer.init(this, options.getTimeout(), options.getUnit())) {
41+
throw new ProviderNotReadyError(
42+
"Provider timed-out while waiting for initial configuration");
43+
}
44+
} catch (final OpenFeatureError e) {
45+
throw e;
46+
} catch (final Throwable e) {
47+
throw new FatalError(
48+
"Failed to initialize provider, is the tracer enabled with -javaagent?", e);
49+
}
50+
}
51+
52+
@Override
53+
public void shutdown() {
54+
initializer.close();
55+
}
56+
57+
@Override
58+
public Metadata getMetadata() {
59+
return this;
60+
}
61+
62+
@Override
63+
public String getName() {
64+
return METADATA;
65+
}
66+
67+
@Override
68+
public ProviderEvaluation<Boolean> getBooleanEvaluation(
69+
final String key, final Boolean defaultValue, final EvaluationContext ctx) {
70+
return initializer.evaluator().evaluate(Boolean.class, key, defaultValue, ctx);
71+
}
72+
73+
@Override
74+
public ProviderEvaluation<String> getStringEvaluation(
75+
final String key, final String defaultValue, final EvaluationContext ctx) {
76+
return initializer.evaluator().evaluate(String.class, key, defaultValue, ctx);
77+
}
78+
79+
@Override
80+
public ProviderEvaluation<Integer> getIntegerEvaluation(
81+
final String key, final Integer defaultValue, final EvaluationContext ctx) {
82+
return initializer.evaluator().evaluate(Integer.class, key, defaultValue, ctx);
83+
}
84+
85+
@Override
86+
public ProviderEvaluation<Double> getDoubleEvaluation(
87+
final String key, final Double defaultValue, final EvaluationContext ctx) {
88+
return initializer.evaluator().evaluate(Double.class, key, defaultValue, ctx);
89+
}
90+
91+
@Override
92+
public ProviderEvaluation<Value> getObjectEvaluation(
93+
final String key, final Value defaultValue, final EvaluationContext ctx) {
94+
return initializer.evaluator().evaluate(Value.class, key, defaultValue, ctx);
95+
}
96+
97+
public static class Options {
98+
99+
private long timeout;
100+
private TimeUnit unit;
101+
102+
public Options initTimeout(final long timeout, final TimeUnit unit) {
103+
this.timeout = timeout;
104+
this.unit = unit;
105+
return this;
106+
}
107+
108+
public long getTimeout() {
109+
return timeout;
110+
}
111+
112+
public TimeUnit getUnit() {
113+
return unit;
114+
}
115+
}
116+
}

0 commit comments

Comments
 (0)