Skip to content
Draft
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
12 changes: 8 additions & 4 deletions products/feature-flagging/feature-flagging-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,12 @@ OTEL_EXPORTER_OTLP_PROTOCOL=grpc
## Requirements

- Java 11+
- `DD_FEATURE_FLAGS_CONFIGURATION_SOURCE=agentless` uses the Datadog agentless
backend. Set `DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL` to a
- `DD_FEATURE_FLAGS_CONFIGURATION_SOURCE=agentless` enables the provider's bundled standalone
runtime when no Datadog Java agent is present. The provider polls UFC configuration and sends
Feature Flagging events directly to Datadog; set `DD_API_KEY` for direct event delivery. When the
Datadog Java agent is present, the provider prefers the agent-owned runtime and transport instead
of starting a second poller.
- Set `DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL` to a
different HTTP backend while keeping agentless delivery semantics. A bare
host uses the standard rules-based server path; a URL with a path is used as
the exact UFC endpoint. Configured URLs are opaque: the SDK does not add the
Expand All @@ -97,7 +101,7 @@ OTEL_EXPORTER_OTLP_PROTOCOL=grpc
and expects UFC under the JSON:API `data.attributes` response member. It is
intended for supported commercial sites; use an explicit base URL elsewhere.
Agentless responses do not have an SDK-imposed payload-size limit.
`remote_config` uses the existing Agent Remote
Configuration path. `offline` is reserved for startup-provided UFC bytes;
- `remote_config` uses the existing Agent Remote Configuration path and therefore requires the
Datadog Java agent. `offline` is reserved for startup-provided UFC bytes;
until those bytes are implemented, no network source starts and evaluations
use defaults.
72 changes: 69 additions & 3 deletions products/feature-flagging/feature-flagging-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import groovy.lang.Closure
plugins {
`java-library`
idea
id("com.gradleup.shadow")
id("dd-trace-java.module.distributable.api")
id("me.champeau.jmh")
}
Expand Down Expand Up @@ -39,12 +40,23 @@ java {
}

dependencies {
modules {
module("com.squareup.okio:okio") {
replacedBy("com.datadoghq.okio:okio")
}
}

api("dev.openfeature:sdk:1.20.1")

compileOnly(project(":products:feature-flagging:feature-flagging-bootstrap"))
compileOnly(project(":products:feature-flagging:feature-flagging-config"))
compileOnly(project(":utils:config-utils"))
implementation(project(":products:feature-flagging:feature-flagging-bootstrap"))
implementation(project(":products:feature-flagging:feature-flagging-config"))
implementation(project(":products:feature-flagging:feature-flagging-lib"))
implementation(project(":utils:config-utils"))
compileOnly("io.opentelemetry:opentelemetry-api:1.47.0")
// OpenFeature SDK classes retain @lombok.Generated in their bytecode. Supplying the annotation
// on the analysis classpath keeps SpotBugs from treating that optional SDK build detail as a
// missing class; Lombok is neither bundled nor published as a dependency.
compileOnly("org.projectlombok:lombok:1.18.38")

testImplementation(project(":products:feature-flagging:feature-flagging-bootstrap"))
testImplementation(project(":utils:config-utils"))
Expand All @@ -61,6 +73,60 @@ dependencies {
jmhImplementation(project(":utils:config-utils"))
}

tasks.jar {
destinationDirectory = layout.buildDirectory.dir("libs-unbundled")
archiveClassifier = "unbundled"
}

tasks.named<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJar") {
archiveClassifier = ""

dependencies {
exclude(dependency("dev.openfeature:sdk:.*"))
exclude(dependency("io.opentelemetry:.*:.*"))
exclude(dependency("org.slf4j:.*:.*"))
// These are optional agent capabilities reachable from the shared Config/communication
// modules but not from standalone HTTP polling or direct EVP delivery.
exclude(dependency("cafe.cryptography:.*:.*"))
exclude(dependency("com.datadoghq:java-dogstatsd-client:.*"))
exclude(dependency("com.datadoghq:sketches-java:.*"))
exclude(dependency("com.github.jnr:.*:.*"))
exclude(dependency("org.ow2.asm:.*:.*"))
}

relocate("com.datadog.featureflag.", "datadog.openfeature.internal.featureflag.")
relocate("com.squareup.", "datadog.openfeature.internal.com.squareup.")
relocate("okhttp3.", "datadog.openfeature.internal.okhttp3.")
relocate("okio.", "datadog.openfeature.internal.okio.")
relocate("org.jctools.", "datadog.openfeature.internal.org.jctools.")
relocate("datadog.", "datadog.openfeature.internal.datadog.") {
exclude("datadog.trace.api.featureflag.*")
exclude("datadog.trace.api.openfeature.*")
}

// Keep the Feature Flagging implementation because DDEvaluator loads its standalone entrypoint
// reflectively. Minimize the rest of the agent dependency graph to the classes that runtime
// actually reaches instead of publishing unrelated agent products in dd-openfeature.
minimize {
exclude(project(":products:feature-flagging:feature-flagging-lib"))
}

duplicatesStrategy = DuplicatesStrategy.FAIL
exclude("**/META-INF/maven/**/pom.xml")
exclude("com/squareup/moshi/_MoshiKotlin*")
exclude("META-INF/proguard/")
exclude("META-INF/*.kotlin_module")
}

tasks.test {
dependsOn(tasks.named("shadowJar"))
doFirst {
val shadowJar =
tasks.named<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJar").get()
systemProperty("datadog.test.dd-openfeature.jar", shadowJar.archiveFile.get().asFile.absolutePath)
}
}

jmh {
jmhVersion = libs.versions.jmh.get()
duplicateClassesStrategy = DuplicatesStrategy.EXCLUDE
Expand Down
32 changes: 27 additions & 5 deletions products/feature-flagging/feature-flagging-api/gradle.lockfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,31 @@
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
# To regenerate this file, run: ./gradlew :products:feature-flagging:feature-flagging-api:dependencies --write-locks
cafe.cryptography:curve25519-elisabeth:0.1.0=jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
cafe.cryptography:ed25519-elisabeth:0.1.0=jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
ch.qos.logback:logback-classic:1.2.13=jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
ch.qos.logback:logback-core:1.2.13=jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.datadoghq.okhttp3:okhttp:3.12.15=compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.datadoghq.okio:okio:1.17.6=compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.datadoghq:dd-javac-plugin-client:0.2.2=compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.datadoghq:java-dogstatsd-client:4.4.5=jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.datadoghq:sketches-java:0.8.3=jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.github.javaparser:javaparser-core:3.25.6=codenarc
com.github.jnr:jffi:1.3.15=jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.github.jnr:jnr-a64asm:1.0.0=jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.github.jnr:jnr-constants:0.10.4=jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.github.jnr:jnr-enxio:0.32.20=jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.github.jnr:jnr-ffi:2.2.19=jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.github.jnr:jnr-posix:3.1.22=jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.github.jnr:jnr-unixsocket:0.38.25=jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.github.jnr:jnr-x86asm:1.0.2=jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.github.spotbugs:spotbugs-annotations:4.10.3=compileClasspath,jmhCompileClasspath,spotbugs
com.github.spotbugs:spotbugs:4.10.3=spotbugs
com.github.stephenc.jcip:jcip-annotations:1.0-1=spotbugs
com.google.code.findbugs:jsr305:3.0.2=compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
com.google.code.gson:gson:2.14.0=spotbugs
com.google.errorprone:error_prone_annotations:2.48.0=spotbugs
com.squareup.moshi:moshi:1.11.0=jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.squareup.okio:okio:1.17.5=jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.squareup.moshi:moshi:1.11.0=compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.thoughtworks.qdox:qdox:1.12.1=codenarc
commons-io:commons-io:2.21.0=spotbugs
de.thetaphi:forbiddenapis:3.10=compileClasspath,jmhCompileClasspath
Expand Down Expand Up @@ -52,6 +66,8 @@ org.jacoco:org.jacoco.agent:0.8.15=jacocoAgent,jacocoAnt
org.jacoco:org.jacoco.ant:0.8.15=jacocoAnt
org.jacoco:org.jacoco.core:0.8.15=jacocoAnt
org.jacoco:org.jacoco.report:0.8.15=jacocoAnt
org.jctools:jctools-core-jdk11:4.0.6=compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jctools:jctools-core:4.0.6=compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:5.14.1=jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:5.14.1=jmhRuntimeClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-params:5.14.1=jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
Expand All @@ -70,21 +86,27 @@ org.openjdk.jmh:jmh-generator-bytecode:1.37=jmh,jmhCompileClasspath,jmhRuntimeCl
org.openjdk.jmh:jmh-generator-reflection:1.37=jmh,jmhCompileClasspath,jmhRuntimeClasspath
org.opentest4j:opentest4j:1.3.0=jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-analysis:9.10.1=spotbugs
org.ow2.asm:asm-analysis:9.7.1=jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.ow2.asm:asm-commons:9.10.1=jacocoAnt,spotbugs
org.ow2.asm:asm-commons:9.7.1=jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.ow2.asm:asm-tree:9.10.1=jacocoAnt,spotbugs
org.ow2.asm:asm-tree:9.7.1=jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.ow2.asm:asm-util:9.10.1=spotbugs
org.ow2.asm:asm:9.0=jmh,jmhCompileClasspath,jmhRuntimeClasspath
org.ow2.asm:asm-util:9.7.1=jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.ow2.asm:asm:9.0=jmh,jmhCompileClasspath
org.ow2.asm:asm:9.10.1=jacocoAnt,spotbugs
org.ow2.asm:asm:9.7.1=jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.projectlombok:lombok:1.18.38=compileClasspath,jmhCompileClasspath
org.slf4j:jcl-over-slf4j:1.7.30=jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.slf4j:jul-to-slf4j:1.7.30=jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.slf4j:log4j-over-slf4j:1.7.30=jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.slf4j:slf4j-api:2.0.17=compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,spotbugsSlf4j,testCompileClasspath,testRuntimeClasspath
org.slf4j:slf4j-api:2.0.18=spotbugs
org.slf4j:slf4j-simple:2.0.17=spotbugsSlf4j
org.snakeyaml:snakeyaml-engine:2.9=jmhRuntimeClasspath,testRuntimeClasspath
org.snakeyaml:snakeyaml-engine:2.9=jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.spockframework:spock-bom:2.4-groovy-3.0=jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.spockframework:spock-core:2.4-groovy-3.0=jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.tabletest:tabletest-junit:1.2.2=jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.tabletest:tabletest-parser:1.2.1=jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.xmlresolver:xmlresolver:5.3.3=spotbugs
empty=annotationProcessor,jmhAnnotationProcessor,spotbugsPlugins,testAnnotationProcessor
empty=annotationProcessor,jmhAnnotationProcessor,shadow,spotbugsPlugins,testAnnotationProcessor
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import dev.openfeature.sdk.Reason;
import dev.openfeature.sdk.Structure;
import dev.openfeature.sdk.Value;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
Expand All @@ -49,6 +51,9 @@

class DDEvaluator implements Evaluator, FeatureFlaggingGateway.ConfigListener {

private static final String STANDALONE_RUNTIME_CLASS =
"com.datadog.featureflag.StandaloneFeatureFlaggingSystem";

private static final Set<Class<?>> SUPPORTED_RESOLUTION_TYPES =
new HashSet<>(asList(String.class, Boolean.class, Integer.class, Double.class, Value.class));

Expand Down Expand Up @@ -113,6 +118,7 @@ class DDEvaluator implements Evaluator, FeatureFlaggingGateway.ConfigListener {
private final Runnable configCallback;
private final AtomicReference<ServerConfiguration> configuration = new AtomicReference<>();
private final CountDownLatch initializationLatch = new CountDownLatch(1);
private volatile boolean standaloneRuntimeStarted;

public DDEvaluator(final Runnable configCallback) {
this.configCallback = configCallback;
Expand All @@ -121,8 +127,13 @@ public DDEvaluator(final Runnable configCallback) {
@Override
public boolean initialize(
final long timeout, final TimeUnit unit, final EvaluationContext context) throws Exception {
FeatureFlaggingGateway.activate();
FeatureFlaggingGateway.addConfigListener(this);
// Give an installed Java agent first refusal. Its activation listener claims AGENT
// synchronously, which keeps transport, lifecycle, and span enrichment in the agent. With no
// listener (the true standalone case), activation is a no-op and the bundled runtime claims
// STANDALONE below.
FeatureFlaggingGateway.activate();
standaloneRuntimeStarted = startStandaloneRuntime();
return initializationLatch.await(timeout, unit) || hasConfiguration();
}

Expand All @@ -134,6 +145,49 @@ public boolean hasConfiguration() {
@Override
public void shutdown() {
FeatureFlaggingGateway.removeConfigListener(this);
if (standaloneRuntimeStarted) {
stopStandaloneRuntime();
standaloneRuntimeStarted = false;
}
}

private static boolean startStandaloneRuntime() throws Exception {
final Class<?> runtime;
try {
runtime = DDEvaluator.class.getClassLoader().loadClass(STANDALONE_RUNTIME_CLASS);
} catch (final ClassNotFoundException ignored) {
return false;
}
return invokeRuntime(runtime, "start");
}

private static void stopStandaloneRuntime() {
try {
final Class<?> runtime =
DDEvaluator.class.getClassLoader().loadClass(STANDALONE_RUNTIME_CLASS);
invokeRuntime(runtime, "stop");
} catch (final ReflectiveOperationException ignored) {
// The standalone runtime is best-effort during shutdown. Initialization already proved the
// class and method were available, so this only protects mixed-version classpaths.
}
}

static boolean invokeRuntime(final Class<?> runtime, final String methodName)
throws ReflectiveOperationException {
final Method method = runtime.getMethod(methodName);
try {
final Object result = method.invoke(null);
return !(result instanceof Boolean) || (Boolean) result;
} catch (final InvocationTargetException exception) {
final Throwable cause = exception.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
if (cause instanceof Error) {
throw (Error) cause;
}
throw exception;
}
}

@Override
Expand Down Expand Up @@ -591,7 +645,7 @@ private static Object resolveAttribute(final String name, final EvaluationContex
return context.convertValue(resolved);
}

private static boolean isTypeCompatible(final Class<?> target, final ValueType variationType) {
static boolean isTypeCompatible(final Class<?> target, final ValueType variationType) {
if (variationType == null) {
return true; // No type info — allow any
}
Expand Down
Loading