Skip to content

Commit 289d4a0

Browse files
committed
Move feature flagging evaluation into provider runtime
1 parent 29efc06 commit 289d4a0

53 files changed

Lines changed: 4024 additions & 3296 deletions

File tree

Some content is hidden

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

dd-smoke-tests/openfeature/application/build.gradle

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ if (hasProperty('featureFlaggingApiJar')) {
3131
}
3232

3333
dependencies {
34-
// OpenFeature SDK is an API dependency of feature-flagging-api but is not
35-
// transitively resolved when the jar is passed as a files() dependency.
34+
// Maven resolves these provider dependencies from the published POM. This smoke test uses the
35+
// provider as a files() dependency, so it must declare them explicitly.
3636
implementation 'dev.openfeature:sdk:1.20.1'
37+
implementation 'com.squareup.moshi:moshi:1.11.0'
3738
implementation 'org.springframework.boot:spring-boot-starter-web'
3839
}

products/feature-flagging/feature-flagging-agent/src/main/java/com/datadog/featureflag/FeatureFlaggingSystem.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,6 @@ private static synchronized void activateAgentless(
7070

7171
private static void initializeSystem(final SharedCommunicationObjects sco, final Config config) {
7272
final ConfigurationSourceService configService = createConfigurationSourceService(sco, config);
73-
if (configService == null) {
74-
LOGGER.debug("Feature Flagging system disabled by unsupported configuration source");
75-
return;
76-
}
7773
final ExposureWriter exposureWriter = new ExposureWriterImpl(sco, config);
7874
initialize(configService, exposureWriter);
7975

@@ -119,7 +115,8 @@ static ConfigurationSourceService createConfigurationSourceService(
119115
return new RemoteConfigServiceImpl(sco, config);
120116
}
121117
if (CONFIGURATION_SOURCE_AGENTLESS.equals(configurationSource)) {
122-
return new AgentlessConfigurationSource(config);
118+
// CDN delivery belongs to the application provider. The agent owns telemetry only.
119+
return null;
123120
}
124121
return null;
125122
}

products/feature-flagging/feature-flagging-agent/src/test/java/com/datadog/featureflag/FeatureFlaggingSystemTest.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import datadog.communication.ddagent.DDAgentFeaturesDiscovery;
2222
import datadog.communication.ddagent.SharedCommunicationObjects;
2323
import datadog.remoteconfig.Capabilities;
24-
import datadog.remoteconfig.ConfigurationDeserializer;
24+
import datadog.remoteconfig.ConfigurationChangesListener;
2525
import datadog.remoteconfig.ConfigurationPoller;
2626
import datadog.remoteconfig.Product;
2727
import datadog.trace.api.Config;
@@ -99,7 +99,7 @@ void testFeatureFlagSystemInitialization() {
9999
FeatureFlaggingSystem.start(sharedCommunicationObjects);
100100

101101
verify(poller).addCapabilities(Capabilities.CAPABILITY_FFE_FLAG_CONFIGURATION_RULES);
102-
verify(poller).addListener(eq(Product.FFE_FLAGS), any(ConfigurationDeserializer.class), any());
102+
verify(poller).addListener(eq(Product.FFE_FLAGS), any(ConfigurationChangesListener.class));
103103
verify(poller).start();
104104

105105
FeatureFlaggingSystem.stop();
@@ -128,9 +128,8 @@ void testThatRemoteConfigIsRequired() {
128128
@Test
129129
@WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "agentless")
130130
@WithConfig(key = REMOTE_CONFIGURATION_ENABLED, value = "false")
131-
void agentlessConfigurationSourceUsesHttpServiceWithoutRemoteConfig() {
132-
assertInstanceOf(
133-
AgentlessConfigurationSource.class,
131+
void agentlessConfigurationSourceIsProviderOwned() {
132+
assertNull(
134133
FeatureFlaggingSystem.createConfigurationSourceService(
135134
sharedCommunicationObjects(), Config.get()));
136135
}

products/feature-flagging/feature-flagging-api/README.md

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -85,19 +85,23 @@ OTEL_EXPORTER_OTLP_PROTOCOL=grpc
8585
## Requirements
8686

8787
- Java 11+
88-
- `DD_FEATURE_FLAGS_CONFIGURATION_SOURCE=agentless` uses the Datadog agentless
89-
backend. Set `DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL` to a
90-
different HTTP backend while keeping agentless delivery semantics. A bare
91-
host uses the standard rules-based server path; a URL with a path is used as
92-
the exact UFC endpoint. Configured URLs are opaque: the SDK does not add the
93-
Datadog-managed `dd_env` query parameter, so custom backends must include any
94-
required tenant or environment scope in the configured URL. The derived
95-
Datadog-managed endpoint is
96-
`https://ufc-server.ff-cdn.<site>/api/v2/feature-flagging/config/rules-based/server`
97-
and expects UFC under the JSON:API `data.attributes` response member. It is
98-
intended for supported commercial sites; use an explicit base URL elsewhere.
99-
Agentless responses do not have an SDK-imposed payload-size limit.
100-
`remote_config` uses the existing Agent Remote
101-
Configuration path. `offline` is reserved for startup-provided UFC bytes;
102-
until those bytes are implemented, no network source starts and evaluations
103-
use defaults.
88+
89+
CDN delivery is the default configuration source. It requires `dd-openfeature`. It does not
90+
require the Datadog Java agent.
91+
92+
`DD_FEATURE_FLAGS_CONFIGURATION_SOURCE=agentless` selects the same CDN delivery mode. Set
93+
`DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL` to use a different HTTP endpoint.
94+
A root URL uses the standard rules-based server path. A URL with a path is the exact UFC endpoint.
95+
96+
The managed endpoint is:
97+
98+
`https://ufc-server.ff-cdn.<site>/api/v2/feature-flagging/config/rules-based/server`
99+
100+
The managed endpoint returns UFC under the JSON:API `data.attributes` member.
101+
102+
`DD_FEATURE_FLAGS_CONFIGURATION_SOURCE=remote_config` selects Remote Configuration. This mode
103+
requires a compatible Datadog Java agent. Java agent version 1.65.0 adds the raw UFC bridge for this
104+
provider version. An older agent produces a clear provider initialization error.
105+
106+
Offline startup bytes are a future configuration source. This version does not support offline
107+
delivery.

products/feature-flagging/feature-flagging-api/build.gradle.kts

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import datadog.gradle.plugin.testJvmConstraints.TestJvmConstraintsExtension
22
import groovy.lang.Closure
3+
import org.gradle.api.tasks.SourceSetContainer
4+
import java.util.zip.ZipFile
35

46
plugins {
57
`java-library`
@@ -16,6 +18,11 @@ configure<TestJvmConstraintsExtension> {
1618

1719
description = "Implementation of the OpenFeature Provider interface."
1820

21+
// This module is an adapter and lifecycle boundary. Core evaluation and HTTP behavior have
22+
// stronger module-local gates. Child JVM tests cover classloader and no-agent behavior here.
23+
extra["minimumBranchCoverage"] = 0.4
24+
extra["minimumInstructionCoverage"] = 0.6
25+
1926
// Set both JAR and Maven artifact name
2027
val openFeatureArtifactId = "dd-openfeature"
2128
base {
@@ -42,14 +49,16 @@ java {
4249

4350
dependencies {
4451
api("dev.openfeature:sdk:1.20.1")
52+
implementation(libs.moshi)
53+
implementation(libs.slf4j)
4554

46-
compileOnly(project(":products:feature-flagging:feature-flagging-bootstrap"))
47-
compileOnly(project(":products:feature-flagging:feature-flagging-config"))
48-
compileOnly(project(":utils:config-utils"))
55+
compileOnly(project(":products:feature-flagging:feature-flagging-core"))
56+
compileOnly(project(":products:feature-flagging:feature-flagging-http"))
4957
compileOnly("io.opentelemetry:opentelemetry-api:1.47.0")
5058

5159
testImplementation(project(":products:feature-flagging:feature-flagging-bootstrap"))
52-
testImplementation(project(":utils:config-utils"))
60+
testImplementation(project(":products:feature-flagging:feature-flagging-core"))
61+
testImplementation(project(":products:feature-flagging:feature-flagging-http"))
5362
testImplementation("io.opentelemetry:opentelemetry-api:1.47.0")
5463
testImplementation(libs.bundles.junit5)
5564
testImplementation(libs.bundles.mockito)
@@ -78,8 +87,61 @@ tasks.withType<Javadoc>().configureEach {
7887
javadocTool = javaToolchains.javadocToolFor(java.toolchain)
7988
}
8089

90+
val coreProject = project(":products:feature-flagging:feature-flagging-core")
91+
val httpProject = project(":products:feature-flagging:feature-flagging-http")
92+
93+
tasks.named<Jar>("jar") {
94+
from(coreProject.extensions.getByType<SourceSetContainer>().named("main").map { it.output })
95+
from(httpProject.extensions.getByType<SourceSetContainer>().named("main").map { it.output })
96+
}
97+
8198
// The dd-openfeature provider jar is not produced by the CI `build` job, so there is no reference
8299
// artifact to compare against. Disable the release jar comparison gate registered by publish.gradle.
83100
tasks.named("compareToReferenceJar") {
84101
enabled = false
85102
}
103+
104+
tasks.register("verifyDdOpenfeatureArtifact") {
105+
dependsOn(tasks.named("jar"), tasks.named("generatePomFileForMavenPublication"))
106+
doLast {
107+
val providerJar = tasks.named<Jar>("jar").get().archiveFile.get().asFile
108+
val requiredEntries = setOf(
109+
"datadog/openfeature/internal/core/ConfigurationStore.class",
110+
"datadog/openfeature/internal/core/FlagEvaluator.class",
111+
"datadog/openfeature/internal/http/CdnConfigurationSource.class",
112+
"datadog/openfeature/internal/http/HttpConfigurationOptions.class"
113+
)
114+
val forbiddenReferences = setOf(
115+
"datadog/trace/api/featureflag/ufc",
116+
"datadog/trace/api/featureflag/FeatureFlaggingGateway",
117+
"datadog/trace/api/Config",
118+
"datadog/trace/util/AgentThread",
119+
"datadog/communication/http"
120+
)
121+
ZipFile(providerJar).use { zip ->
122+
val entryNames = zip.entries().asSequence().map { it.name }.toSet()
123+
val missing = requiredEntries - entryNames
124+
check(missing.isEmpty()) {
125+
"dd-openfeature is missing embedded provider classes: $missing"
126+
}
127+
zip.entries().asSequence()
128+
.filter { it.name.endsWith(".class") }
129+
.forEach { entry ->
130+
val classBytes = zip.getInputStream(entry).readBytes().toString(Charsets.ISO_8859_1)
131+
forbiddenReferences.forEach { reference ->
132+
check(!classBytes.contains(reference)) {
133+
"dd-openfeature class ${entry.name} references forbidden agent class $reference"
134+
}
135+
}
136+
}
137+
}
138+
139+
val pom = layout.buildDirectory.file("publications/maven/pom-default.xml").get().asFile
140+
check(pom.isFile) { "Generated dd-openfeature Maven POM is missing" }
141+
val pomText = pom.readText()
142+
check(pomText.contains("<artifactId>sdk</artifactId>"))
143+
check(pomText.contains("<artifactId>moshi</artifactId>"))
144+
check(!pomText.contains("feature-flagging-core"))
145+
check(!pomText.contains("feature-flagging-http"))
146+
}
147+
}

products/feature-flagging/feature-flagging-api/gradle.lockfile

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ com.github.stephenc.jcip:jcip-annotations:1.0-1=spotbugs
1111
com.google.code.findbugs:jsr305:3.0.2=compileClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
1212
com.google.code.gson:gson:2.13.2=spotbugs
1313
com.google.errorprone:error_prone_annotations:2.41.0=spotbugs
14-
com.squareup.moshi:moshi:1.11.0=testCompileClasspath,testRuntimeClasspath
15-
com.squareup.okio:okio:1.17.5=testCompileClasspath,testRuntimeClasspath
14+
com.squareup.moshi:moshi:1.11.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
15+
com.squareup.okio:okio:1.17.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
1616
com.thoughtworks.qdox:qdox:1.12.1=codenarc
1717
commons-io:commons-io:2.20.0=spotbugs
1818
de.thetaphi:forbiddenapis:3.10=compileClasspath
@@ -57,7 +57,6 @@ org.junit.jupiter:junit-jupiter:5.14.1=testCompileClasspath,testRuntimeClasspath
5757
org.junit.platform:junit-platform-commons:1.14.1=testCompileClasspath,testRuntimeClasspath
5858
org.junit.platform:junit-platform-engine:1.14.1=testCompileClasspath,testRuntimeClasspath
5959
org.junit.platform:junit-platform-launcher:1.14.1=testRuntimeClasspath
60-
org.junit:junit-bom:5.14.0=spotbugs
6160
org.junit:junit-bom:5.14.1=testCompileClasspath,testRuntimeClasspath
6261
org.mockito:mockito-core:4.4.0=testCompileClasspath,testRuntimeClasspath
6362
org.mockito:mockito-junit-jupiter:4.4.0=testCompileClasspath,testRuntimeClasspath
@@ -76,7 +75,6 @@ org.slf4j:jul-to-slf4j:1.7.30=testCompileClasspath,testRuntimeClasspath
7675
org.slf4j:log4j-over-slf4j:1.7.30=testCompileClasspath,testRuntimeClasspath
7776
org.slf4j:slf4j-api:2.0.17=compileClasspath,runtimeClasspath,spotbugs,spotbugsSlf4j,testCompileClasspath,testRuntimeClasspath
7877
org.slf4j:slf4j-simple:2.0.17=spotbugsSlf4j
79-
org.snakeyaml:snakeyaml-engine:2.9=testRuntimeClasspath
8078
org.spockframework:spock-bom:2.4-groovy-3.0=testCompileClasspath,testRuntimeClasspath
8179
org.spockframework:spock-core:2.4-groovy-3.0=testCompileClasspath,testRuntimeClasspath
8280
org.tabletest:tabletest-junit:1.2.1=testCompileClasspath,testRuntimeClasspath

0 commit comments

Comments
 (0)