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
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,16 @@ class OpenFeatureProviderSmokeTest extends AbstractServerSmokeTest {
command.addAll(['-jar', springBootShadowJar, "--server.port=${httpPort}".toString()])
final builder = new ProcessBuilder(command).directory(new File(buildDirectory))
builder.environment().put('DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED', 'true')
builder.environment().put('DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED', 'true')
Comment thread
sameerank marked this conversation as resolved.
builder.environment().put('DD_FEATURE_FLAGS_CONFIGURATION_SOURCE', 'remote_config')
return builder
}

@Override
Closure decodedTracesCallback() {
return {}
}

@Override
Closure decodedEvpProxyMessageCallback() {
return { String path, RequestApi request ->
Expand All @@ -71,6 +77,34 @@ class OpenFeatureProviderSmokeTest extends AbstractServerSmokeTest {
hasCapability(capabilities, Capabilities.CAPABILITY_FFE_FLAG_CONFIGURATION_RULES)
}

void 'test open feature evaluation enriches the request span'() {
setup:
setRemoteConfig("datadog/2/FFE_FLAGS/1/config", rcPayload)
final request = new Request.Builder()
.url("http://localhost:${httpPort}/openfeature/evaluate")
.post(RequestBody.create(MediaType.parse('application/json'), JsonOutput.toJson([
flag: 'flag-that-does-not-exist',
variationType: 'STRING',
defaultValue: 'fallback',
targetingKey: 'span-enrichment-smoke-test'
])))
.build()

when:
final response = client.newCall(request).execute()

then:
response.code() == 200
new JsonSlurper().parse(response.body().byteStream()).value == 'fallback'
waitForSpan(defaultPoll) { span ->
span.parentId == 0 &&
span.meta['ffe_runtime_defaults'] == '{"flag-that-does-not-exist":"fallback"}'
}

cleanup:
response.close()
}

void 'test open feature exposures'() {
setup:
setRemoteConfig("datadog/2/FFE_FLAGS/1/config", rcPayload)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import datadog.trace.api.featureflag.SpanEnrichmentEvent;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -33,25 +32,13 @@
*
* <p>All work is wrapped in try/catch — enrichment must NEVER break flag evaluation.
*/
@SuppressFBWarnings(
value = "SING_SINGLETON_HAS_NONPRIVATE_CONSTRUCTOR",
justification =
"The production path constructs the sole instance through the private no-arg constructor "
+ "behind INSTANCE/getInstance(); the package-private constructors are deliberate "
+ "test-only seams for injecting the root-span resolver and interceptor registrar. The "
+ "singleton itself is required (PR #11658 review) so the single, unremovable trace "
+ "interceptor is registered exactly once and survives subsystem start/stop.")
public final class SpanEnrichmentWriter
implements FeatureFlaggingGateway.SpanEnrichmentListener, AutoCloseable {

private static final Logger log = LoggerFactory.getLogger(SpanEnrichmentWriter.class);

// The one instance used by the agent. Persisting it across FeatureFlaggingSystem start/stop keeps
// the single registered interceptor (and its state) alive, so a restart never re-registers.
private static final SpanEnrichmentWriter INSTANCE = new SpanEnrichmentWriter();

public static SpanEnrichmentWriter getInstance() {
return INSTANCE;
return SingletonHolder.INSTANCE;
}

/**
Expand Down Expand Up @@ -85,6 +72,12 @@ static AgentSpan resolveLocalRoot(final AgentSpan active) {
private static final InterceptorRegistrar DEFAULT_REGISTRAR =
interceptor -> GlobalTracer.get().addTraceInterceptor(interceptor);

private static final class SingletonHolder {
// Persisting this instance across FeatureFlaggingSystem start/stop keeps the single registered
// interceptor (and its state) alive, so a restart never re-registers.
private static final SpanEnrichmentWriter INSTANCE = new SpanEnrichmentWriter();
}

private final RootSpanResolver rootSpanResolver;
private final InterceptorRegistrar registrar;
private final SpanEnrichmentStates states;
Expand Down Expand Up @@ -193,4 +186,12 @@ SpanEnrichmentStates states() {
SpanEnrichmentInterceptor interceptor() {
return interceptor;
}

RootSpanResolver rootSpanResolver() {
return rootSpanResolver;
}

InterceptorRegistrar registrar() {
return registrar;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ void agentSingletonIsStableAcrossRestarts() {
assertSame(first.states(), second.states(), "same state store across restarts");
}

@Test
void agentSingletonUsesProductionDependencies() {
final SpanEnrichmentWriter writer = SpanEnrichmentWriter.getInstance();
assertNotNull(writer.rootSpanResolver());
assertNotNull(writer.registrar());
}

@Test
void resolveLocalRootLogic() {
assertNull(SpanEnrichmentWriter.resolveLocalRoot(null));
Expand Down