Skip to content

Commit 05671ce

Browse files
leoromanovskypavlokhrebtosarahchen6PerfectSlayerdevflow.devflow-routing-intake
authored
feat(feature-flagging): FFE APM feature-flag span enrichment (experimental, gated) (#11658)
feat(02-02): surface UFC split serialId in eval metadata + add span-enrichment gate - Add nullable Integer serialId to UFC Split model + constructor (Moshi reflectively deserializes the serialId JSON field; no custom adapter needed) - DDEvaluator surfaces __dd_split_serial_id (when present) and __dd_do_log in OpenFeature eval metadata for APM span enrichment (JAVA-01) - Add FeatureFlaggingConfig.SPAN_ENRICHMENT_ENABLED dot-form constant mapping to DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED (distinct, off by default) - Register the env var in metadata/supported-configurations.json (version A, boolean) - Update DDEvaluatorTest Split call sites for the new constructor arg feat(02-02): ULeb128 codec + accumulator + capture hook + write interceptor (gate-gated) - ULeb128Encoder: ULEB128 delta-varint + base64 codec ported verbatim from the frozen Node reference (dd-trace-js#8343); golden vector {100,108,128,130} -> ZAgUAg==, SHA-256 hex targeting-key hashing; JDK stdlib only (java.util.Base64, MessageDigest) - SpanEnrichmentAccumulator: per-trace state keyed by trace id in a shared ConcurrentHashMap; frozen limits (200/10/20/5/64), UTF-8-safe truncation, object default -> JSON (Pattern F tag shapes: ffe_flags_enc bare base64, ffe_subjects_enc / ffe_runtime_defaults JSON objects) - SpanEnrichmentHook: OpenFeature finally hook (capture) resolving the active local root via AgentTracer and applying the Node branch (serialId -> addSerialId/addSubject; missing variant -> addDefault); error-isolated - SpanEnrichmentInterceptor: TraceInterceptor (write) flushing ffe_* onto the local root onTraceComplete with unique priority 4, then clearing state; disable() for provider-close cleanup - Provider: gate-gated construction via ConfigHelper.env (DG-005 — nothing built/ registered when off), hook added to getProviderHooks, interceptor registered via GlobalTracer, shutdown() disables interceptor + drains state - feature-flagging-api: add compileOnly/testImplementation :internal-api for the tracer/interceptor types (runtime-provided by the agent) test(02-02): JUnit 5 L0 suite for span enrichment (7 required cases + golden round-trip) - Codec golden-vector round-trip {100,108,128,130} -> ZAgUAg== (encode + decode + empty + structural dedupe) - no-span, finished-root (accumulate+dedupe+flush), runtime-default missing-variant (ffe_runtime_defaults JSON object), per-subject cap (10 subj / 20 exp/subj) + doLog gating, max-200 serial ids, object-default JSON + 64-char truncation - gate-off negative control: no hook/interceptor constructed, not in getProviderHooks, no accumulator state (DG-005) - gate-on construction + provider-close cleanup (shutdown disables interceptor + drains state); error isolation; interceptor priority uniqueness - JUnit 5 only (no Groovy); injectable RootSpanResolver + Provider gate override avoid static mocking (project mockito uses the subclass mock maker) fix(02-02): correct span-enrichment lifecycle (partial-flush, leak, reconfig) Gap-closure for the three BLOCKER lifecycle defects in 02-REVIEW-java.md. The frozen contract (codec/limits/gate/tag shapes, golden vector ZAgUAg==) is unchanged — this fixes only how per-trace state is scoped, bounded, and flushed. CR-01 (partial-flush data loss + tag misattribution): CoreTracer.write -> interceptCompleteTrace runs onTraceComplete on EVERY flush, and PendingTrace.write(isPartial) excludes the still-open local root from a partial flush (getRootSpan() is null until the final write). The interceptor previously resolved the root by reference (reachable even when absent from the fragment) and unconditionally removed the accumulator on the first partial flush, dropping all pre-flush flags and tagging a not-yet-finished root. SpanEnrichmentInterceptor now flushes + removes ONLY when the local root is actually present in the flushed collection (the final write); a partial flush leaves the accumulator intact. Also adds the missing getTraceId() null guard (WR-01) and never falls back to tagging a non-root span (WR-02). CR-02 (unbounded state leak): State lived in a static ConcurrentHashMap whose only removal paths were trace-complete and shutdown, so dropped / Noop / never-finishing traces leaked forever (#4844 leak class). State now lives in a per-provider SpanEnrichmentStates store, hard-bounded at MAX_TRACES with FIFO eviction of the oldest in-flight entry, so a never-completing trace cannot grow the map unboundedly. CR-03 (reconfiguration corruption): Provider ignored addTraceInterceptor's return value, so a second gate-on provider (rejected on duplicate priority 4) still wired a hook into shared static state and its shutdown() cleared the first provider's live state. Provider now honors the registration result (wires the hook/interceptor only when registration succeeds) and each provider owns its own state store, so one provider's shutdown can never clear another's. Removes the global static map (review IN-03 root cause). Adds an injectable TraceInterceptorRegistrar test seam (mirrors the existing gate-override seam). Runtime-default rendering (String() parity) and hasData() are intentionally unchanged. L0 suite migrated to the instance-owned store; module 146 tests green. test(02-02): regression tests for span-enrichment lifecycle defects Adds SpanEnrichmentLifecycleRegressionTest exercising the real tracer lifecycle paths the original L0 suite bypassed (single root, no children, one provider). Each test FAILS against the pre-fix code and PASSES after the fix: CR-01 partialFlushExcludingRootPreservesPreFlushFlags / partialFlushNeverWrites- TagsOnAChildSpan: a partial flush carrying only child spans (root excluded, as dd-trace-core does) must not drain state or tag a child; the full pre+post-flush serial-id set {100,108,128,130} -> 'ZAgUAg==' must land on the root at final completion. Fail-before: tags written on the partial flush (NeverWantedButInvoked). CR-02 neverCompletingTracesDoNotLeakUnbounded / boundedStoreEvictsOldestFirst: 3x MAX_TRACES distinct never-flushed traces keep the store bounded; eviction is FIFO. Fail-before: store grew to 3x the cap (expected <=cap but was larger). CR-03 secondProviderRejectedRegistrationDoesNotClearFirstProviderState / eachProviderOwnsADistinctStateStore / hookAndInterceptorOfOneProviderShareThe- SameStore: a second provider whose registration is rejected wires no hook/interceptor and its shutdown does not clear the first provider's in-flight state. Fail-before: second provider kept a non-null interceptor (expected null). Fail-before verified by temporarily reintroducing each defect: 5/7 failed (the remaining 2 are structural-invariant checks that hold by construction). chore(feature-flagging): strip internal review labels from span-enrichment comments Remove leaked workflow labels (e.g. JAVA-01) from production comments so the upstream PR carries only behavioral context. fix(feature-flagging): address span-enrichment code-review findings - Reconfiguration safety: replace the per-provider trace interceptor with a single process-wide delegating interceptor (SpanEnrichmentInterceptor.INSTANCE) registered once and rebound per active provider. A closing provider unbinds only if still active, so provider close/reopen no longer permanently disables enrichment via a duplicate-priority rejection, and a displaced provider's late shutdown cannot clobber the active provider's in-flight state. - Runtime defaults: recursively unwrap OpenFeature Value (structure/list/scalar) to its native form before JSON serialization so ffe_runtime_defaults matches the frozen Node contract instead of emitting Value.toString(). - 128-bit trace ids: key per-trace state by the full trace id hex string rather than DDTraceId.toLong(), so two traces sharing low-order 64 bits no longer merge enrichment state. - Gate-off inertness: precompute the immutable provider-hook list once in the constructor so getProviderHooks() (called per evaluation) allocates nothing. - Add regression tests: reconfiguration, displaced-provider late shutdown, 128-bit low-bit collision, Value structure/list/scalar serialization, and gate-off zero-allocation. Merge master Adjustments + clean up; add FeatureFlaggingConfig.SPAN_ENRICHMENT_ENABLED; add Startup INFO log; clean tech debt Rework FFE span-enrichment state store to weak-keyed-by-span map Merge branch 'master' of github.com:DataDog/dd-trace-java into leo.romanovsky/ffe-apm-span-enrichment # Conflicts: # products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/DDEvaluatorTest.java Enhancements after techdebt skill Refactor: Move span-enrichment write tier out of the published dd-openfeature API Use JsonWriter instead of hand-written solution Fix unicode char Address review nits: gate enrichment metadata, name constants, reuse digest - Gate the __dd_split_serial_id / __dd_do_log evaluation metadata on the span-enrichment flag so an enabled provider with enrichment off attaches nothing extra per evaluation (DDEvaluator). - Extract the metadata keys to named constants (METADATA_SPLIT_SERIAL_ID / METADATA_DO_LOG) as the single source of truth; SpanEnrichmentHook references them. - Rename the config constant to EXPERIMENTAL_SPAN_ENRICHMENT_ENABLED to make its experimental status obvious at call sites. - Reuse a ThreadLocal<MessageDigest> for subject hashing instead of allocating a SHA-256 instance per capture (ULeb128Encoder). feat(ffe): Introduce config module to decouple feature flagging from tracing API (#11888) Fix 3 bugs: SpanEnrichmentWriter is now a process-wide singleton; addTraceInterceptor() false return: ensureInterceptorRegistered() now latches only on a true return; registration ordering: moved ensureInterceptorRegistered() below the root == null check Fix nits: add debug logging; fix comments Merge branch 'master' into leo.romanovsky/ffe-apm-span-enrichment Add gate on registration; adjust - add typed metadata Merge branch 'master' of github.com:DataDog/dd-trace-java into leo.romanovsky/ffe-apm-span-enrichment Merge branch 'leo.romanovsky/ffe-apm-span-enrichment' of github.com:DataDog/dd-trace-java into leo.romanovsky/ffe-apm-span-enrichment Merge branch 'master' into leo.romanovsky/ffe-apm-span-enrichment Raise feature-flagging test coverage to pass the JDK 8 jacoco gate Merge branch 'leo.romanovsky/ffe-apm-span-enrichment' of github.com:DataDog/dd-trace-java into leo.romanovsky/ffe-apm-span-enrichment Merge branch 'master' into leo.romanovsky/ffe-apm-span-enrichment Fix failing tests Merge branch 'master' of github.com:DataDog/dd-trace-java into leo.romanovsky/ffe-apm-span-enrichment Merge branch 'leo.romanovsky/ffe-apm-span-enrichment' of github.com:DataDog/dd-trace-java into leo.romanovsky/ffe-apm-span-enrichment Co-authored-by: pavlokhrebto <pavlo.khrebto@datadoghq.com> Co-authored-by: sarahchen6 <sarah.chen@datadoghq.com> Co-authored-by: PerfectSlayer <PerfectSlayer@users.noreply.github.com> Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
1 parent 871f5b6 commit 05671ce

30 files changed

Lines changed: 2228 additions & 14 deletions

File tree

dd-java-agent/agent-bootstrap/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ dependencies {
2323
api project(':dd-java-agent:agent-debugger:debugger-bootstrap')
2424
api project(':components:environment')
2525
api project(':components:json')
26+
api project(':products:feature-flagging:feature-flagging-config')
2627
api project(':products:metrics:metrics-agent')
2728
api libs.instrument.java
2829
api libs.slf4j

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
import datadog.trace.api.config.CrashTrackingConfig;
3535
import datadog.trace.api.config.CwsConfig;
3636
import datadog.trace.api.config.DebuggerConfig;
37-
import datadog.trace.api.config.FeatureFlaggingConfig;
3837
import datadog.trace.api.config.GeneralConfig;
3938
import datadog.trace.api.config.IastConfig;
4039
import datadog.trace.api.config.JmxFetchConfig;
@@ -44,6 +43,7 @@
4443
import datadog.trace.api.config.TraceInstrumentationConfig;
4544
import datadog.trace.api.config.TracerConfig;
4645
import datadog.trace.api.config.UsmConfig;
46+
import datadog.trace.api.featureflag.config.FeatureFlaggingConfig;
4747
import datadog.trace.api.gateway.RequestContextSlot;
4848
import datadog.trace.api.gateway.SubscriptionService;
4949
import datadog.trace.api.git.EmbeddedGitInfoBuilder;

dd-trace-api/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ extra["excludedClassesCoverage"] = listOf(
3838
"datadog.trace.api.civisibility.noop.NoOpDDTestSession",
3939
"datadog.trace.api.civisibility.noop.NoOpDDTestSuite",
4040
"datadog.trace.api.config.AIGuardConfig",
41-
"datadog.trace.api.config.FeatureFlaggingConfig",
4241
"datadog.trace.api.config.ProfilingConfig",
4342
"datadog.trace.api.interceptor.MutableSpan",
4443
"datadog.trace.api.profiling.Profiling",

dd-trace-api/src/main/java/datadog/trace/api/config/FeatureFlaggingConfig.java

Lines changed: 0 additions & 6 deletions
This file was deleted.

metadata/supported-configurations.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1505,6 +1505,14 @@
15051505
"aliases": []
15061506
}
15071507
],
1508+
"DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED": [
1509+
{
1510+
"version": "A",
1511+
"type": "boolean",
1512+
"default": "false",
1513+
"aliases": []
1514+
}
1515+
],
15081516
"DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED": [
15091517
{
15101518
"version": "B",

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public class FeatureFlaggingSystem {
1111

1212
private static volatile RemoteConfigService CONFIG_SERVICE;
1313
private static volatile ExposureWriter EXPOSURE_WRITER;
14+
private static volatile SpanEnrichmentWriter SPAN_ENRICHMENT_WRITER;
1415

1516
private FeatureFlaggingSystem() {}
1617

@@ -27,10 +28,22 @@ public static void start(final SharedCommunicationObjects sco) {
2728
EXPOSURE_WRITER = new ExposureWriterImpl(sco, config);
2829
EXPOSURE_WRITER.init();
2930

31+
// APM span enrichment: agent-side listener for flag-evaluation seam events. Uses the process-
32+
// wide singleton so a subsystem restart reuses the one already-registered trace interceptor
33+
// (which the tracer cannot remove) instead of registering a second, rejected one. Cheap: it
34+
// only accumulates once the provider's gate-on capture hook dispatches events, and registers
35+
// its interceptor lazily on the first such event.
36+
SPAN_ENRICHMENT_WRITER = SpanEnrichmentWriter.getInstance();
37+
SPAN_ENRICHMENT_WRITER.init();
38+
3039
LOGGER.debug("Feature Flagging system started");
3140
}
3241

3342
public static void stop() {
43+
if (SPAN_ENRICHMENT_WRITER != null) {
44+
SPAN_ENRICHMENT_WRITER.close();
45+
SPAN_ENRICHMENT_WRITER = null;
46+
}
3447
if (EXPOSURE_WRITER != null) {
3548
EXPOSURE_WRITER.close();
3649
EXPOSURE_WRITER = null;

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,12 @@ dependencies {
4444
api("dev.openfeature:sdk:1.20.1")
4545

4646
compileOnly(project(":products:feature-flagging:feature-flagging-bootstrap"))
47+
compileOnly(project(":products:feature-flagging:feature-flagging-config"))
48+
compileOnly(project(":utils:config-utils"))
4749
compileOnly("io.opentelemetry:opentelemetry-api:1.47.0")
4850

4951
testImplementation(project(":products:feature-flagging:feature-flagging-bootstrap"))
52+
testImplementation(project(":utils:config-utils"))
5053
testImplementation("io.opentelemetry:opentelemetry-api:1.47.0")
5154
testImplementation(libs.bundles.junit5)
5255
testImplementation(libs.bundles.mockito)

products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ class DDEvaluator implements Evaluator, FeatureFlaggingGateway.ConfigListener {
4646
private static final Set<Class<?>> SUPPORTED_RESOLUTION_TYPES =
4747
new HashSet<>(asList(String.class, Boolean.class, Integer.class, Double.class, Value.class));
4848

49+
// Evaluation-metadata keys consumed by the span-enrichment capture hook (see
50+
// SpanEnrichmentHook). Emitted only when the span-enrichment gate is on.
51+
static final String METADATA_SPLIT_SERIAL_ID = "__dd_split_serial_id";
52+
static final String METADATA_DO_LOG = "__dd_do_log";
53+
54+
// Read once: when off, the __dd_* span-enrichment metadata is not attached to evaluations, so an
55+
// enabled provider pays nothing extra unless span enrichment is also enabled. The gate does not
56+
// change at runtime, and this class is loaded lazily (well after startup) so config is ready.
57+
private static final boolean SPAN_ENRICHMENT_ENABLED = SpanEnrichmentGate.isEnabled();
58+
4959
private final Runnable configCallback;
5060
private final AtomicReference<ServerConfiguration> configuration = new AtomicReference<>();
5161
private final CountDownLatch initializationLatch = new CountDownLatch(1);
@@ -392,6 +402,17 @@ private static <T> ProviderEvaluation<T> resolveVariant(
392402
.addString("flagKey", flag.key)
393403
.addString("variationType", flag.variationType.name())
394404
.addString("allocationKey", allocation.key);
405+
// Surface the UFC split's serial id and the allocation's doLog flag for APM span enrichment —
406+
// only when span enrichment is on, so a provider without enrichment pays nothing extra.
407+
// __dd_split_serial_id is omitted when the split carries no serial id; __dd_do_log is always
408+
// present (when enrichment is on) so the span-enrichment hook can decide whether to record the
409+
// subject.
410+
if (SPAN_ENRICHMENT_ENABLED) {
411+
if (split.serialId != null) {
412+
metadataBuilder.addInteger(METADATA_SPLIT_SERIAL_ID, split.serialId);
413+
}
414+
metadataBuilder.addBoolean(METADATA_DO_LOG, allocation.doLog != null && allocation.doLog);
415+
}
395416
final ProviderEvaluation<T> result =
396417
ProviderEvaluation.<T>builder()
397418
.value(mappedValue)

products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import dev.openfeature.sdk.exceptions.OpenFeatureError;
1717
import dev.openfeature.sdk.exceptions.ProviderNotReadyError;
1818
import java.lang.reflect.Constructor;
19+
import java.util.ArrayList;
1920
import java.util.Collections;
2021
import java.util.List;
2122
import java.util.concurrent.TimeUnit;
@@ -28,13 +29,19 @@ public class Provider extends EventProvider implements Metadata {
2829
private static final Logger log = LoggerFactory.getLogger(Provider.class);
2930
static final String METADATA = "datadog-openfeature-provider";
3031
private static final String EVALUATOR_IMPL = "datadog.trace.api.openfeature.DDEvaluator";
32+
3133
private static final Options DEFAULT_OPTIONS = new Options().initTimeout(30, SECONDS);
3234
private volatile Evaluator evaluator;
3335
private final Options options;
3436
private final AtomicReference<InitializationState> initializationState =
3537
new AtomicReference<>(InitializationState.NOT_STARTED);
3638
private final FlagEvalMetrics flagEvalMetrics;
3739
private final FlagEvalHook flagEvalHook;
40+
// Span enrichment: null unless the gate is on, so the feature has no idle overhead when off.
41+
private final SpanEnrichmentHook spanEnrichmentHook;
42+
// Precomputed hook list returned by getProviderHooks() on every evaluation. Immutable and built
43+
// once so gate-off evaluation allocates nothing on this hot path.
44+
private final List<Hook> providerHooks;
3845

3946
public Provider() {
4047
this(DEFAULT_OPTIONS, null);
@@ -45,6 +52,17 @@ public Provider(final Options options) {
4552
}
4653

4754
Provider(final Options options, final Evaluator evaluator) {
55+
this(options, evaluator, null);
56+
}
57+
58+
/**
59+
* @param spanEnrichmentEnabledOverride when non-null, forces the span-enrichment gate (test
60+
* seam); when null, the gate is read via {@link SpanEnrichmentGate}.
61+
*/
62+
Provider(
63+
final Options options,
64+
final Evaluator evaluator,
65+
final Boolean spanEnrichmentEnabledOverride) {
4866
this.options = options;
4967
this.evaluator = evaluator;
5068
FlagEvalMetrics metrics = null;
@@ -58,6 +76,34 @@ public Provider(final Options options) {
5876
}
5977
this.flagEvalMetrics = metrics;
6078
this.flagEvalHook = hook;
79+
80+
// Span enrichment is wired ONLY when the gate is on — off means no capture hook and no idle
81+
// per-evaluation overhead.
82+
final boolean spanEnrichmentEnabled =
83+
spanEnrichmentEnabledOverride != null
84+
? spanEnrichmentEnabledOverride
85+
: SpanEnrichmentGate.isEnabled();
86+
this.spanEnrichmentHook = spanEnrichmentEnabled ? new SpanEnrichmentHook() : null;
87+
88+
// Precompute the immutable hook list once so getProviderHooks() (called on every evaluation)
89+
// allocates nothing, including when the gate is off.
90+
final List<Hook> hooks = new ArrayList<>(2);
91+
if (flagEvalHook != null) {
92+
hooks.add(flagEvalHook);
93+
}
94+
if (spanEnrichmentHook != null) {
95+
hooks.add(spanEnrichmentHook);
96+
}
97+
this.providerHooks =
98+
hooks.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(hooks);
99+
100+
// Announce the span-enrichment state at startup (matches the reference implementation).
101+
// "enabled" only when the gate is on (the capture hook was constructed), otherwise "disabled".
102+
if (spanEnrichmentHook != null) {
103+
log.info("{} span enrichment enabled", METADATA);
104+
} else {
105+
log.info("{} span enrichment disabled", METADATA);
106+
}
61107
}
62108

63109
@Override
@@ -167,22 +213,27 @@ private Evaluator buildEvaluator() throws Exception {
167213

168214
@Override
169215
public List<Hook> getProviderHooks() {
170-
if (flagEvalHook == null) {
171-
return Collections.emptyList();
172-
}
173-
return Collections.singletonList(flagEvalHook);
216+
return providerHooks;
174217
}
175218

176219
@Override
177220
public void shutdown() {
178221
if (flagEvalMetrics != null) {
179222
flagEvalMetrics.shutdown();
180223
}
224+
// Span enrichment needs no provider-close cleanup here: the capture hook holds no tracer state.
225+
// The agent-side write tier owns the interceptor and per-trace state and is torn down with the
226+
// feature-flagging subsystem, not per provider.
181227
if (evaluator != null) {
182228
evaluator.shutdown();
183229
}
184230
}
185231

232+
// Visible for tests: expose whether span enrichment is wired (gate-on) without leaking the impl.
233+
SpanEnrichmentHook spanEnrichmentHook() {
234+
return spanEnrichmentHook;
235+
}
236+
186237
@Override
187238
public Metadata getMetadata() {
188239
return this;
@@ -223,7 +274,7 @@ public ProviderEvaluation<Value> getObjectEvaluation(
223274
return evaluator.evaluate(Value.class, key, defaultValue, ctx);
224275
}
225276

226-
@SuppressForbidden // Class#forName(String) used to lazy load internal-api dependencies
277+
@SuppressForbidden // Class#forName(String) used to lazy-load the evaluator implementation
227278
protected Class<?> loadEvaluatorClass() throws ClassNotFoundException {
228279
return Class.forName(EVALUATOR_IMPL);
229280
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package datadog.trace.api.openfeature;
2+
3+
import datadog.trace.api.featureflag.config.FeatureFlaggingConfig;
4+
import datadog.trace.bootstrap.config.provider.ConfigProvider;
5+
6+
/**
7+
* Single source for reading the experimental span-enrichment gate, with full {@link ConfigProvider}
8+
* precedence (system property > stable config > env var {@code
9+
* DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED}). OFF by default; distinct from the
10+
* provider-enabled gate. Shared so {@link Provider} (per construction) and {@link DDEvaluator}
11+
* (once at class load) read it the same way.
12+
*/
13+
final class SpanEnrichmentGate {
14+
15+
private SpanEnrichmentGate() {}
16+
17+
static boolean isEnabled() {
18+
try {
19+
return ConfigProvider.getInstance()
20+
.getBoolean(FeatureFlaggingConfig.EXPERIMENTAL_SPAN_ENRICHMENT_ENABLED, false);
21+
} catch (final Throwable t) {
22+
return false; // never let config reading break construction
23+
}
24+
}
25+
}

0 commit comments

Comments
 (0)