From dab22a77df054fc3c1b8df200937c14581739094 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Tue, 16 Jun 2026 18:49:49 -0400 Subject: [PATCH 1/2] feat(parametric/java): enable FFE span-enrichment scenario for Java Enable tests/parametric/test_ffe/test_span_enrichment.py for the Java tracer (DataDog/dd-trace-java#11658), which adds FFE APM feature-flag span enrichment behind DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED. - FeatureFlagEvaluatorController: re-activate the caller-supplied root span (span_id) around the OpenFeature evaluation so the ffe_* tags land on the test's root span. span_id arrives as a decimal string and is resolved via the OpenTracing registry (DDSpanId.from); unknown/unparsable ids skip activation and never throw. - manifests/java.yml: declare the enrichment suite at v1.64.0 (the release that ships the feature). --- manifests/java.yml | 2 +- .../FeatureFlagEvaluatorController.java | 79 ++++++++++++++----- 2 files changed, 62 insertions(+), 19 deletions(-) diff --git a/manifests/java.yml b/manifests/java.yml index 9dbaf7c8dba..6f35831ba33 100644 --- a/manifests/java.yml +++ b/manifests/java.yml @@ -3708,7 +3708,7 @@ manifest: tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV1_ServiceTargets::test_not_match_service_target: irrelevant (APMAPI-1003) tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV2: v1.31.0 tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: v1.56.0 - tests/parametric/test_ffe/test_span_enrichment.py: missing_feature + tests/parametric/test_ffe/test_span_enrichment.py: v1.64.0 tests/parametric/test_headers_b3.py::Test_Headers_B3::test_headers_b3_migrated_extract_invalid: # Modified by easy win activation script - declaration: missing_feature (Need to remove b3=b3multi alias) component_version: <1.58.2+06122213c8 diff --git a/utils/build/docker/java/parametric/src/main/java/com/datadoghq/trace/controller/FeatureFlagEvaluatorController.java b/utils/build/docker/java/parametric/src/main/java/com/datadoghq/trace/controller/FeatureFlagEvaluatorController.java index 4fef048ef59..08534c063c4 100644 --- a/utils/build/docker/java/parametric/src/main/java/com/datadoghq/trace/controller/FeatureFlagEvaluatorController.java +++ b/utils/build/docker/java/parametric/src/main/java/com/datadoghq/trace/controller/FeatureFlagEvaluatorController.java @@ -2,7 +2,9 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import com.datadoghq.trace.opentracing.controller.OpenTracingController; import com.fasterxml.jackson.annotation.JsonAlias; +import datadog.trace.api.DDSpanId; import datadog.trace.api.openfeature.Provider; import dev.openfeature.sdk.Client; import dev.openfeature.sdk.EvaluationContext; @@ -13,6 +15,9 @@ import dev.openfeature.sdk.ProviderState; import dev.openfeature.sdk.Structure; import dev.openfeature.sdk.Value; +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.util.GlobalTracer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -78,26 +83,19 @@ public ResponseEntity> evaluate(@RequestBody final EvaluateR Object value; String reason; final EvaluationContext context = context(request); + // Re-activate the caller-supplied root span around the eval so the ffe_* tags (Phase 2) land + // on the test's span. The client sends span_id as a decimal STRING (see + // _test_client_parametric.py:814-815); the OT registry is keyed by DDSpanId.from(...). + // An unknown/missing/unparsable span_id leaves target null -> skip activation, never throw (T-01-DOS). + final Span target = resolveSpan(request.getSpanId()); try { - value = switch (request.getVariationType()) { - case "BOOLEAN" -> - client.getBooleanValue(request.getFlag(), (Boolean) request.getDefaultValue(), context); - case "STRING" -> client.getStringValue(request.getFlag(), (String) request.getDefaultValue(), context); - case "INTEGER" -> { - final Number integerEval = (Number) request.getDefaultValue(); - yield client.getIntegerValue(request.getFlag(), integerEval.intValue(), context); + if (target != null) { + try (Scope scope = GlobalTracer.get().scopeManager().activate(target)) { + value = evaluate(request, context); } - case "NUMERIC" -> { - final Number doubleEval = (Number) request.getDefaultValue(); - yield client.getDoubleValue(request.getFlag(), doubleEval.doubleValue(), context); - } - case "JSON" -> { - final Value objectValue = client.getObjectValue(request.getFlag(), Value.objectToValue(request.getDefaultValue()), context); - yield context.convertValue(objectValue); - } - default -> request.getDefaultValue(); - }; - + } else { + value = evaluate(request, context); + } reason = "DEFAULT"; } catch (Throwable e) { LOGGER.error("Error on resolution", e); @@ -110,6 +108,40 @@ public ResponseEntity> evaluate(@RequestBody final EvaluateR return ResponseEntity.ok(result); } + /** Look up a span by the (string) span_id the test client sends; null when missing/unparsable. */ + private static Span resolveSpan(final String spanId) { + if (spanId == null || spanId.isEmpty()) { + return null; + } + try { + return OpenTracingController.getSpan(DDSpanId.from(spanId)); + } catch (Throwable e) { + LOGGER.warn("Could not resolve span for span_id {}", spanId, e); + return null; + } + } + + private Object evaluate(final EvaluateRequest request, final EvaluationContext context) { + return switch (request.getVariationType()) { + case "BOOLEAN" -> + client.getBooleanValue(request.getFlag(), (Boolean) request.getDefaultValue(), context); + case "STRING" -> client.getStringValue(request.getFlag(), (String) request.getDefaultValue(), context); + case "INTEGER" -> { + final Number integerEval = (Number) request.getDefaultValue(); + yield client.getIntegerValue(request.getFlag(), integerEval.intValue(), context); + } + case "NUMERIC" -> { + final Number doubleEval = (Number) request.getDefaultValue(); + yield client.getDoubleValue(request.getFlag(), doubleEval.doubleValue(), context); + } + case "JSON" -> { + final Value objectValue = client.getObjectValue(request.getFlag(), Value.objectToValue(request.getDefaultValue()), context); + yield context.convertValue(objectValue); + } + default -> request.getDefaultValue(); + }; + } + private static EvaluationContext context(final EvaluateRequest request) { final MutableContext context = new MutableContext(); context.setTargetingKey(request.getTargetingKey()); @@ -141,12 +173,23 @@ public static class EvaluateRequest { private Object defaultValue; @JsonAlias("targeting_key") private String targetingKey; + // The test client sends span_id as a STRING (see _test_client_parametric.py:814-815). + @JsonAlias("span_id") + private String spanId; private Map attributes; public Map getAttributes() { return attributes; } + public String getSpanId() { + return spanId; + } + + public void setSpanId(String spanId) { + this.spanId = spanId; + } + public void setAttributes(Map attributes) { this.attributes = attributes; } From b63d037def3a0a91874b3234b08e669f07f55049 Mon Sep 17 00:00:00 2001 From: typotter Date: Tue, 23 Jun 2026 13:23:14 -0600 Subject: [PATCH 2/2] feat(parametric/java): activate FFE span enrichment tests for Java - Set manifest to v1.63.0-SNAPSHOT (all 18 tests passing) - Bump parametric JVM heap from 128M to 256M to avoid OOM under 16-worker parallelism - Fix zombie container crash in _clean_containers when Docker returns 404 on inspect --- manifests/java.yml | 2 +- utils/_context/_scenarios/_docker_fixtures.py | 17 +++++++++++++++-- utils/build/docker/java/parametric/run.sh | 2 +- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/manifests/java.yml b/manifests/java.yml index 6f35831ba33..247ac058565 100644 --- a/manifests/java.yml +++ b/manifests/java.yml @@ -3708,7 +3708,7 @@ manifest: tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV1_ServiceTargets::test_not_match_service_target: irrelevant (APMAPI-1003) tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV2: v1.31.0 tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: v1.56.0 - tests/parametric/test_ffe/test_span_enrichment.py: v1.64.0 + tests/parametric/test_ffe/test_span_enrichment.py: v1.63.0-SNAPSHOT tests/parametric/test_headers_b3.py::Test_Headers_B3::test_headers_b3_migrated_extract_invalid: # Modified by easy win activation script - declaration: missing_feature (Need to remove b3=b3multi alias) component_version: <1.58.2+06122213c8 diff --git a/utils/_context/_scenarios/_docker_fixtures.py b/utils/_context/_scenarios/_docker_fixtures.py index e1b8389fc6d..3fc03148324 100644 --- a/utils/_context/_scenarios/_docker_fixtures.py +++ b/utils/_context/_scenarios/_docker_fixtures.py @@ -6,7 +6,7 @@ import pytest from utils.docker_fixtures._test_agent import TestAgentFactory, TestAgentAPI -from docker.errors import DockerException +from docker.errors import DockerException, NotFound from utils._context.docker import get_docker_client from utils._logger import logger from .core import Scenario, ScenarioGroup, scenario_groups as groups @@ -41,7 +41,20 @@ def _clean(self): def _clean_containers(self): """Some containers may still exists from previous unfinished sessions""" - for container in get_docker_client().containers.list(all=True): + try: + containers = get_docker_client().containers.list(all=True) + except NotFound: + # Docker SDK raises NotFound when a container disappears between list() and inspect(). + # Fall back to the raw API which returns only IDs and names without a second inspect call. + raw = get_docker_client().api.containers(all=True) + containers = [] + for r in raw: + try: + containers.append(get_docker_client().containers.get(r["Id"])) + except NotFound: + pass + + for container in containers: if "test-client" in container.name or "test-agent" in container.name or "test-library" in container.name: logger.info(f"Removing {container}") diff --git a/utils/build/docker/java/parametric/run.sh b/utils/build/docker/java/parametric/run.sh index a8b3382356d..7e29726ac7d 100755 --- a/utils/build/docker/java/parametric/run.sh +++ b/utils/build/docker/java/parametric/run.sh @@ -34,7 +34,7 @@ OPTIMIZATION_OPTIONS=(-XX:TieredStopAtLevel=1) # Start client application # shellcheck disable=SC2086 -java -Xmx128M -javaagent:"${DD_JAVA_AGENT}" \ +java -Xmx256M -javaagent:"${DD_JAVA_AGENT}" \ $ENABLE_OTEL_TRACING_API \ "${ENABLE_CRASH_TRACKING[@]}" \ "${DISABLED_FEATURES[@]}" \