Skip to content

Commit f66a140

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

Some content is hidden

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

47 files changed

+3394
-3
lines changed

.github/CODEOWNERS

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,9 @@ 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+
/dd-java-agent/agent-feature-flagging/ @DataDog/feature-flagging-and-experimentation-sdk
145+
/internal-api/src/main/java/datadog/trace/api/featureflag @DataDog/feature-flagging-and-experimentation-sdk
146+
/internal-api/src/test/groovy/datadog/trace/api/featureflag @DataDog/feature-flagging-and-experimentation-sdk
147+
/products/openfeature/ @DataDog/feature-flagging-and-experimentation-sdk

dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import datadog.trace.api.config.CrashTrackingConfig;
3333
import datadog.trace.api.config.CwsConfig;
3434
import datadog.trace.api.config.DebuggerConfig;
35+
import datadog.trace.api.config.FeatureFlaggingConfig;
3536
import datadog.trace.api.config.GeneralConfig;
3637
import datadog.trace.api.config.IastConfig;
3738
import datadog.trace.api.config.JmxFetchConfig;
@@ -125,7 +126,8 @@ private enum AgentFeature {
125126
DATA_JOBS(GeneralConfig.DATA_JOBS_ENABLED, false),
126127
AGENTLESS_LOG_SUBMISSION(GeneralConfig.AGENTLESS_LOG_SUBMISSION_ENABLED, false),
127128
LLMOBS(LlmObsConfig.LLMOBS_ENABLED, false),
128-
LLMOBS_AGENTLESS(LlmObsConfig.LLMOBS_AGENTLESS_ENABLED, false);
129+
LLMOBS_AGENTLESS(LlmObsConfig.LLMOBS_AGENTLESS_ENABLED, false),
130+
FEATURE_FLAGGING(FeatureFlaggingConfig.FLAGGING_PROVIDER_ENABLED, false);
129131

130132
private final String configKey;
131133
private final String systemProp;
@@ -184,6 +186,7 @@ public boolean isEnabledByDefault() {
184186
private static boolean codeOriginEnabled = false;
185187
private static boolean distributedDebuggerEnabled = false;
186188
private static boolean agentlessLogSubmissionEnabled = false;
189+
private static boolean featureFlaggingEnabled = false;
187190

188191
private static void safelySetContextClassLoader(ClassLoader classLoader) {
189192
try {
@@ -268,6 +271,7 @@ public static void start(
268271
codeOriginEnabled = isFeatureEnabled(AgentFeature.CODE_ORIGIN);
269272
agentlessLogSubmissionEnabled = isFeatureEnabled(AgentFeature.AGENTLESS_LOG_SUBMISSION);
270273
llmObsEnabled = isFeatureEnabled(AgentFeature.LLMOBS);
274+
featureFlaggingEnabled = isFeatureEnabled(AgentFeature.FEATURE_FLAGGING);
271275

272276
// setup writers when llmobs is enabled to accomodate apm and llmobs
273277
if (llmObsEnabled) {
@@ -662,6 +666,7 @@ public void execute() {
662666
maybeStartDebugger(instrumentation, scoClass, sco);
663667
maybeStartRemoteConfig(scoClass, sco);
664668
maybeStartAiGuard();
669+
maybeStartFeatureFlagging(scoClass, sco);
665670

666671
if (telemetryEnabled) {
667672
startTelemetry(instrumentation, scoClass, sco);
@@ -1083,6 +1088,23 @@ private static void maybeStartLLMObs(Instrumentation inst, Class<?> scoClass, Ob
10831088
}
10841089
}
10851090

1091+
private static void maybeStartFeatureFlagging(final Class<?> scoClass, final Object sco) {
1092+
if (featureFlaggingEnabled) {
1093+
StaticEventLogger.begin("Feature Flagging");
1094+
1095+
try {
1096+
final Class<?> ffSysClass =
1097+
AGENT_CLASSLOADER.loadClass("com.datadog.featureflag.FeatureFlaggingSystem");
1098+
final Method ffSysMethod = ffSysClass.getMethod("start", scoClass);
1099+
ffSysMethod.invoke(null, sco);
1100+
} catch (final Throwable e) {
1101+
log.warn("Not starting Feature Flagging subsystem", e);
1102+
}
1103+
1104+
StaticEventLogger.end("Feature Flagging");
1105+
}
1106+
}
1107+
10861108
private static void maybeInstallLogsIntake(Class<?> scoClass, Object sco) {
10871109
if (agentlessLogSubmissionEnabled) {
10881110
StaticEventLogger.begin("Logs Intake");

dd-trace-api/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ val excludedClassesCoverage by extra(
3535
"datadog.trace.api.civisibility.noop.NoOpDDTestSession",
3636
"datadog.trace.api.civisibility.noop.NoOpDDTestSuite",
3737
"datadog.trace.api.config.AIGuardConfig",
38+
"datadog.trace.api.config.FeatureFlagConfig",
3839
"datadog.trace.api.config.ProfilingConfig",
3940
"datadog.trace.api.interceptor.MutableSpan",
4041
"datadog.trace.api.profiling.Profiling",
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package datadog.trace.api.config;
2+
3+
public class FeatureFlaggingConfig {
4+
5+
public static final String FLAGGING_PROVIDER_ENABLED = "experimental.flagging.provider.enabled";
6+
}

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

metadata/supported-configurations.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@
184184
"DD_EXCEPTION_REPLAY_MAX_FRAMES_TO_CAPTURE": ["A"],
185185
"DD_EXPERIMENTAL_API_SECURITY_ENABLED": ["A"],
186186
"DD_EXPERIMENTAL_DEFER_INTEGRATIONS_UNTIL": ["A"],
187+
"DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED": ["A"],
187188
"DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED": ["A"],
188189
"DD_FORCE_CLEAR_TEXT_HTTP_FOR_INTAKE_CLIENT": ["A"],
189190
"DD_GIT_BRANCH": ["A"],
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+
}

0 commit comments

Comments
 (0)