Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@
**/datastreams/ @DataDog/data-streams-monitoring
**/DataStreams* @DataDog/data-streams-monitoring

# @DataDog/feature-flagging-and-experimentation-sdk
/products/feature-flagging/ @DataDog/feature-flagging-and-experimentation-sdk

# @DataDog/profiling-java
/dd-java-agent/agent-profiling/ @DataDog/profiling-java
/dd-java-agent/agent-crashtracking/ @DataDog/profiling-java
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import datadog.trace.api.config.CrashTrackingConfig;
import datadog.trace.api.config.CwsConfig;
import datadog.trace.api.config.DebuggerConfig;
import datadog.trace.api.config.FeatureFlaggingConfig;
import datadog.trace.api.config.GeneralConfig;
import datadog.trace.api.config.IastConfig;
import datadog.trace.api.config.JmxFetchConfig;
Expand Down Expand Up @@ -125,7 +126,8 @@ private enum AgentFeature {
DATA_JOBS(GeneralConfig.DATA_JOBS_ENABLED, false),
AGENTLESS_LOG_SUBMISSION(GeneralConfig.AGENTLESS_LOG_SUBMISSION_ENABLED, false),
LLMOBS(LlmObsConfig.LLMOBS_ENABLED, false),
LLMOBS_AGENTLESS(LlmObsConfig.LLMOBS_AGENTLESS_ENABLED, false);
LLMOBS_AGENTLESS(LlmObsConfig.LLMOBS_AGENTLESS_ENABLED, false),
FEATURE_FLAGGING(FeatureFlaggingConfig.FLAGGING_PROVIDER_ENABLED, false);

private final String configKey;
private final String systemProp;
Expand Down Expand Up @@ -184,6 +186,7 @@ public boolean isEnabledByDefault() {
private static boolean codeOriginEnabled = false;
private static boolean distributedDebuggerEnabled = false;
private static boolean agentlessLogSubmissionEnabled = false;
private static boolean featureFlaggingEnabled = false;

private static void safelySetContextClassLoader(ClassLoader classLoader) {
try {
Expand Down Expand Up @@ -268,6 +271,7 @@ public static void start(
codeOriginEnabled = isFeatureEnabled(AgentFeature.CODE_ORIGIN);
agentlessLogSubmissionEnabled = isFeatureEnabled(AgentFeature.AGENTLESS_LOG_SUBMISSION);
llmObsEnabled = isFeatureEnabled(AgentFeature.LLMOBS);
featureFlaggingEnabled = isFeatureEnabled(AgentFeature.FEATURE_FLAGGING);

// setup writers when llmobs is enabled to accomodate apm and llmobs
if (llmObsEnabled) {
Expand Down Expand Up @@ -662,6 +666,7 @@ public void execute() {
maybeStartDebugger(instrumentation, scoClass, sco);
maybeStartRemoteConfig(scoClass, sco);
maybeStartAiGuard();
maybeStartFeatureFlagging(scoClass, sco);

if (telemetryEnabled) {
startTelemetry(instrumentation, scoClass, sco);
Expand Down Expand Up @@ -1083,6 +1088,23 @@ private static void maybeStartLLMObs(Instrumentation inst, Class<?> scoClass, Ob
}
}

private static void maybeStartFeatureFlagging(final Class<?> scoClass, final Object sco) {
if (featureFlaggingEnabled) {
StaticEventLogger.begin("Feature Flagging");

try {
final Class<?> ffSysClass =
AGENT_CLASSLOADER.loadClass("com.datadog.featureflag.FeatureFlaggingSystem");
final Method ffSysMethod = ffSysClass.getMethod("start", scoClass);
ffSysMethod.invoke(null, sco);
} catch (final Throwable e) {
log.warn("Not starting Feature Flagging subsystem", e);
}

StaticEventLogger.end("Feature Flagging");
}
}

private static void maybeInstallLogsIntake(Class<?> scoClass, Object sco) {
if (agentlessLogSubmissionEnabled) {
StaticEventLogger.begin("Logs Intake");
Expand Down
2 changes: 2 additions & 0 deletions dd-java-agent/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ includeSubprojShadowJar(project(':dd-java-agent:agent-ci-visibility'), 'ci-visib
includeSubprojShadowJar(project(':dd-java-agent:agent-llmobs'), 'llm-obs', includedJarFileTree)
includeSubprojShadowJar(project(':dd-java-agent:agent-logs-intake'), 'logs-intake', includedJarFileTree)
includeSubprojShadowJar(project(':dd-java-agent:cws-tls'), 'cws-tls', includedJarFileTree)
includeSubprojShadowJar(project(':products:feature-flagging:agent'), 'feature-flagging', includedJarFileTree)

def sharedShadowJar = tasks.register('sharedShadowJar', ShadowJar) {
it.configurations = [project.configurations.sharedShadowInclude]
Expand Down Expand Up @@ -318,6 +319,7 @@ dependencies {
shadowInclude project(path: ':dd-java-agent:agent-bootstrap')
shadowInclude project(path: ':dd-java-agent:agent-debugger:debugger-bootstrap')
shadowInclude project(path: ':dd-java-agent:agent-otel:otel-bootstrap', configuration: 'shadow')
shadowInclude project(path: ':products:feature-flagging:bootstrap')

// Includes for the shared internal shadow jar
sharedShadowInclude deps.shared
Expand Down
1 change: 1 addition & 0 deletions dd-trace-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ val excludedClassesCoverage by extra(
"datadog.trace.api.civisibility.noop.NoOpDDTestSession",
"datadog.trace.api.civisibility.noop.NoOpDDTestSuite",
"datadog.trace.api.config.AIGuardConfig",
"datadog.trace.api.config.FeatureFlaggingConfig",
"datadog.trace.api.config.ProfilingConfig",
"datadog.trace.api.interceptor.MutableSpan",
"datadog.trace.api.profiling.Profiling",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package datadog.trace.api.config;

public class FeatureFlaggingConfig {

public static final String FLAGGING_PROVIDER_ENABLED = "experimental.flagging.provider.enabled";
}
1 change: 1 addition & 0 deletions gradle/dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ final class CachedData {
exclude(project(':components:environment'))
exclude(project(':components:json'))
exclude(project(':components:yaml'))
exclude(project(':products:feature-flagging:bootstrap'))
exclude(project(':remote-config:remote-config-api'))
exclude(project(':remote-config:remote-config-core'))
exclude(project(':telemetry'))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@ public enum Intake {
"ci-intake",
"v2",
Config::isCiVisibilityAgentlessEnabled,
Config::getCiVisibilityIntakeAgentlessUrl);
Config::getCiVisibilityIntakeAgentlessUrl),
EVENT_PLATFORM("event-platform-intake", "v2");

public final String urlPrefix;
public final String version;
public final Function<Config, Boolean> agentlessModeEnabled;
public final Function<Config, String> customUrl;

Intake(String urlPrefix, String version) {
this(urlPrefix, version, config -> false, config -> null);
}

Intake(
String urlPrefix,
String version,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ public enum AgentThread {

LOGS_INTAKE("dd-logs-intake"),

LLMOBS_EVALS_PROCESSOR("dd-llmobs-evals-processor");
LLMOBS_EVALS_PROCESSOR("dd-llmobs-evals-processor"),

FEATURE_FLAG_EXPOSURE_PROCESSOR("dd-ffe-exposure-processor");

public final String threadName;

Expand Down
1 change: 1 addition & 0 deletions metadata/supported-configurations.json
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@
"DD_EXCEPTION_REPLAY_MAX_FRAMES_TO_CAPTURE": ["A"],
"DD_EXPERIMENTAL_API_SECURITY_ENABLED": ["A"],
"DD_EXPERIMENTAL_DEFER_INTEGRATIONS_UNTIL": ["A"],
"DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED": ["A"],
"DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED": ["A"],
"DD_FORCE_CLEAR_TEXT_HTTP_FOR_INTAKE_CLIENT": ["A"],
"DD_GIT_BRANCH": ["A"],
Expand Down
30 changes: 30 additions & 0 deletions products/feature-flagging/agent/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.gradle.kotlin.dsl.project

plugins {
`java-library`
id("com.gradleup.shadow")
}

apply(from = "$rootDir/gradle/java.gradle")
apply(from = "$rootDir/gradle/version.gradle")

description = "Feature flagging agent system"

dependencies {
api(libs.slf4j)
api(project(":products:feature-flagging:lib"))
api(project(":internal-api"))

testImplementation(project(":utils:test-utils"))
testRuntimeOnly(project(":dd-trace-core"))
}

tasks.named<ShadowJar>("shadowJar") {
dependencies {
val deps = project.extra["deps"] as Map<*, *>
val excludeShared = deps["excludeShared"] as groovy.lang.Closure<*>
excludeShared.delegate = this
excludeShared.call()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.datadog.featureflag;

import datadog.communication.ddagent.SharedCommunicationObjects;
import datadog.trace.api.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class FeatureFlaggingSystem {

private static final Logger LOGGER = LoggerFactory.getLogger(FeatureFlaggingSystem.class);

private static volatile RemoteConfigService CONFIG_SERVICE;
private static volatile ExposureWriter EXPOSURE_WRITER;

private FeatureFlaggingSystem() {}

public static void start(final SharedCommunicationObjects sco) {
LOGGER.debug("Feature Flagging system starting");
final Config config = Config.get();

if (!config.isRemoteConfigEnabled()) {
throw new IllegalStateException("Feature Flagging system started without RC");
}
CONFIG_SERVICE = new RemoteConfigServiceImpl(sco, config);
CONFIG_SERVICE.init();

EXPOSURE_WRITER = new ExposureWriterImpl(sco, config);
EXPOSURE_WRITER.init();

LOGGER.debug("Feature Flagging system started");
}

public static void stop() {
if (EXPOSURE_WRITER != null) {
EXPOSURE_WRITER.close();
EXPOSURE_WRITER = null;
}
if (CONFIG_SERVICE != null) {
CONFIG_SERVICE.close();
CONFIG_SERVICE = null;
}
LOGGER.debug("Feature Flagging system stopped");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.datadog.featureflag

import datadog.communication.ddagent.DDAgentFeaturesDiscovery
import datadog.communication.ddagent.SharedCommunicationObjects
import datadog.remoteconfig.Capabilities
import datadog.remoteconfig.ConfigurationDeserializer
import datadog.remoteconfig.ConfigurationPoller
import datadog.remoteconfig.Product
import datadog.trace.api.Config
import datadog.trace.test.util.DDSpecification
import okhttp3.HttpUrl

class FeatureFlaggingSystemTest extends DDSpecification {

void 'test feature flag system initialization'() {
setup:
final poller = Mock(ConfigurationPoller)
final discovery = Stub(DDAgentFeaturesDiscovery) {
discoverIfOutdated() >> {}
supportsEvpProxy() >> { return true }
}
final sco = Stub(SharedCommunicationObjects) {
configurationPoller(_ as Config) >> poller
featuresDiscovery(_ as Config) >> discovery
}
sco.featuresDiscovery = discovery
sco.agentUrl = HttpUrl.get('http://localhost')

when:
FeatureFlaggingSystem.start(sco)

then:
1 * poller.addCapabilities(Capabilities.CAPABILITY_FFE_FLAG_CONFIGURATION_RULES)
1 * poller.addListener(Product.FFE_FLAGS, _ as ConfigurationDeserializer, _)
1 * poller.start()

when:
FeatureFlaggingSystem.stop()

then:
1 * poller.removeCapabilities(Capabilities.CAPABILITY_FFE_FLAG_CONFIGURATION_RULES)
1 * poller.removeListeners(Product.FFE_FLAGS)
1 * poller.stop()
}

void 'test that remote config is required'() {
setup:
injectSysConfig('remote_configuration.enabled', 'false')
final sco = Mock(SharedCommunicationObjects)

when:
FeatureFlaggingSystem.start(sco)

then:
thrown(IllegalStateException)

cleanup:
FeatureFlaggingSystem.stop()
}
}
76 changes: 76 additions & 0 deletions products/feature-flagging/api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import de.thetaphi.forbiddenapis.gradle.CheckForbiddenApis
import groovy.lang.Closure

plugins {
`java-library`
idea
`maven-publish`
}

apply(from = "$rootDir/gradle/java.gradle")
apply(from = "$rootDir/gradle/publish.gradle")

val minJavaVersionForTests by extra(JavaVersion.VERSION_11)

description = "Implementation of the OpenFeature Provider interface."

// Set both JAR and Maven artifact name
val openFeatureArtifactId = "dd-openfeature"
base {
archivesName.set(openFeatureArtifactId)
}

publishing {
publications.withType<MavenPublication>().configureEach {
artifactId = openFeatureArtifactId
}
}

idea {
module {
jdkName = "11"
}
}

java {
toolchain {
languageVersion = JavaLanguageVersion.of(11)
}
}

dependencies {
api("dev.openfeature:sdk:1.18.2")

compileOnly(project(":products:feature-flagging:bootstrap"))

testImplementation(project(":products:feature-flagging:bootstrap"))
testImplementation(libs.bundles.junit5)
testImplementation(libs.bundles.mockito)
testImplementation(libs.moshi)
testImplementation("org.awaitility:awaitility:4.3.0")
}

fun AbstractCompile.configureCompiler(
javaVersionInteger: Int,
compatibilityVersion: JavaVersion? = null,
unsetReleaseFlagReason: String? = null
) {
(project.extra["configureCompiler"] as Closure<*>).call(
this,
javaVersionInteger,
compatibilityVersion,
unsetReleaseFlagReason
)
}

tasks.withType<JavaCompile>().configureEach {
configureCompiler(11, JavaVersion.VERSION_11)
}

tasks.withType<Javadoc>().configureEach {
javadocTool = javaToolchains.javadocToolFor(java.toolchain)
}

tasks.named<CheckForbiddenApis>("forbiddenApisMain") {
failOnMissingClasses = false
}
Loading