diff --git a/tests/parametric/capabilities.yml b/tests/parametric/capabilities.yml index 2218c856d7c..12567bbecd4 100644 --- a/tests/parametric/capabilities.yml +++ b/tests/parametric/capabilities.yml @@ -69,9 +69,10 @@ capabilities: - ASM_ACTIVATION - ASM_AUTO_USER_INSTRUM_MODE - # SDK_CONFIGURATION supersedes the per-setting APM_TRACING capabilities in the 5.x - # line from 5.128.0 and in the 6.x line from 6.17.0. - '<5.128.0 || >=6.0.0-0 <6.17.0': + # Node.js consumes sdk_config starting in 5.128.0 on the 5.x release line and in + # 6.17.0 on the 6.x release line. Starting in 6.20.0, it also advertises the + # per-setting APM_TRACING capabilities for backend and UI compatibility. + '<5.128.0 || >=6.0.0-0 <6.17.0 || >=6.20.0 <7.0.0-0 || >=7.0.0': - APM_TRACING_CUSTOM_TAGS - APM_TRACING_ENABLED - APM_TRACING_HTTP_HEADER_TAGS @@ -88,11 +89,11 @@ capabilities: '>=5.83.0': - APM_TRACING_MULTICONFIG - '>=5.83.0 <5.128.0 || >=6.0.0-0 <6.17.0': + '>=5.83.0 <5.128.0 || >=6.0.0-0 <6.17.0 || >=6.20.0 <7.0.0-0 || >=7.0.0': - APM_TRACING_ENABLE_CODE_ORIGIN - APM_TRACING_ENABLE_DYNAMIC_INSTRUMENTATION - '>=5.84.0 <5.128.0 || >=6.0.0-0 <6.17.0': + '>=5.84.0 <5.128.0 || >=6.0.0-0 <6.17.0 || >=6.20.0 <7.0.0-0 || >=7.0.0': - APM_TRACING_ENABLE_LIVE_DEBUGGING python: diff --git a/tests/parametric/test_dynamic_configuration.py b/tests/parametric/test_dynamic_configuration.py index 215d81a21e2..0df04ad988b 100644 --- a/tests/parametric/test_dynamic_configuration.py +++ b/tests/parametric/test_dynamic_configuration.py @@ -187,22 +187,23 @@ def uses_sdk_configuration(test_agent: TestAgentAPI) -> bool: that does not poll remote config (tracing disabled by DD_TRACE_ENABLED, for instance) looks like; the shared helper turns that into the `lib_config` default. """ - return remote_config.resolve_sdk_configuration_support(lambda: test_agent.wait_for_rc_capabilities(_RC_WAIT_LOOPS)) + return remote_config.resolve_sdk_configuration_support( + lambda: test_agent.wait_for_rc_capabilities(_RC_WAIT_LOOPS), library_name=context.library.name + ) def assert_rc_capability(test_agent: TestAgentAPI, capability: Capabilities, wait_loops: int = 100) -> None: """Assert that the tracer advertises the capability to remotely configure one setting. - A tracer that has moved to the unified SDK_CONFIGURATION contract advertises that single bit - for every remotely configurable setting instead of the per-setting ones, so it stands in for - any of them. The SDK_CONFIGURATION bit on its own does not, since libdatadog gives that bit a - different meaning; only a tracer that has really dropped the per-setting bits qualifies. + A tracer that has moved to the unified SDK_CONFIGURATION contract may advertise that bit in + place of a per-setting capability. Since libdatadog gives the same bit a different meaning, + the shared resolver also uses the library identity when legacy capabilities remain present. """ seen_capabilities = test_agent.wait_for_rc_capabilities(wait_loops) if capability in seen_capabilities: return - assert remote_config.resolve_sdk_configuration_contract(seen_capabilities), ( + assert remote_config.resolve_sdk_configuration_contract(seen_capabilities, library_name=context.library.name), ( f"RemoteConfig capability missing: neither {capability.name} nor the SDK_CONFIGURATION " f"contract that replaces it; seen: {seen_capabilities}" ) diff --git a/tests/test_the_test/test_remote_config.py b/tests/test_the_test/test_remote_config.py index b68bc157780..878e2a70d79 100644 --- a/tests/test_the_test/test_remote_config.py +++ b/tests/test_the_test/test_remote_config.py @@ -192,13 +192,8 @@ def test_build_apm_tracing_command_legacy_by_default(): @scenarios.test_the_test -def test_resolve_sdk_configuration_contract(): - """The SDK_CONFIGURATION bit alone does not mean the library reads sdk_config. - - Bit 49 is SDK_CONFIGURATION in the remote config source of truth, but libdatadog gives the - same bit to ASM_RAW_RESPONSE_BODY, so the per-setting capabilities have to be gone too. - """ - # dd-trace-js with the SDK_CONFIGURATION support: the per-setting capabilities are dropped +def test_sdk_configuration_alone_selects_sdk_config(): + """SDK_CONFIGURATION without per-setting APM_TRACING capabilities selects sdk_config.""" assert rc.resolve_sdk_configuration_contract( { Capabilities.ASM_ACTIVATION, @@ -207,36 +202,67 @@ def test_resolve_sdk_configuration_contract(): } ) - # dd-trace-php: bit 49 is ASM_RAW_RESPONSE_BODY there, and lib_config is still what it reads - assert ( - rc.resolve_sdk_configuration_contract( - { - Capabilities.APM_TRACING_CUSTOM_TAGS, - Capabilities.APM_TRACING_ENABLED, - Capabilities.APM_TRACING_HTTP_HEADER_TAGS, - Capabilities.APM_TRACING_LOGS_INJECTION, - Capabilities.APM_TRACING_SAMPLE_RATE, - Capabilities.APM_TRACING_SAMPLE_RULES, - Capabilities.APM_TRACING_MULTICONFIG, - Capabilities.SDK_CONFIGURATION, - } - ) - is False - ) - # dd-trace-java: no SDK_CONFIGURATION at all - assert ( - rc.resolve_sdk_configuration_contract({Capabilities.APM_TRACING_SAMPLE_RATE, Capabilities.APM_TRACING_ENABLED}) - is False - ) +@scenarios.test_the_test +def test_sdk_configuration_with_apm_tracing_capabilities_selects_sdk_config(): + """Node.js uses sdk_config while continuing to advertise per-setting capabilities.""" + capabilities = { + Capabilities.APM_TRACING_CUSTOM_TAGS, + Capabilities.APM_TRACING_ENABLED, + Capabilities.APM_TRACING_HTTP_HEADER_TAGS, + Capabilities.APM_TRACING_LOGS_INJECTION, + Capabilities.APM_TRACING_SAMPLE_RATE, + Capabilities.APM_TRACING_SAMPLE_RULES, + Capabilities.APM_TRACING_MULTICONFIG, + Capabilities.APM_TRACING_ENABLE_CODE_ORIGIN, + Capabilities.APM_TRACING_ENABLE_DYNAMIC_INSTRUMENTATION, + Capabilities.APM_TRACING_ENABLE_LIVE_DEBUGGING, + Capabilities.SDK_CONFIGURATION, + } + + assert rc.resolve_sdk_configuration_contract(capabilities, library_name="nodejs") is True + + +@scenarios.test_the_test +def test_php_bit_49_collision_with_apm_tracing_capabilities_selects_lib_config(): + """PHP uses bit 49 for ASM_RAW_RESPONSE_BODY and continues to consume lib_config.""" + capabilities = { + Capabilities.APM_TRACING_CUSTOM_TAGS, + Capabilities.APM_TRACING_ENABLED, + Capabilities.APM_TRACING_HTTP_HEADER_TAGS, + Capabilities.APM_TRACING_LOGS_INJECTION, + Capabilities.APM_TRACING_SAMPLE_RATE, + Capabilities.APM_TRACING_SAMPLE_RULES, + Capabilities.APM_TRACING_MULTICONFIG, + Capabilities.SDK_CONFIGURATION, + } + + assert rc.resolve_sdk_configuration_contract(capabilities, library_name="php") is False + + +@scenarios.test_the_test +def test_apm_tracing_capabilities_without_sdk_configuration_select_lib_config(): + """Per-setting APM_TRACING capabilities without SDK_CONFIGURATION select lib_config.""" + capabilities = { + Capabilities.APM_TRACING_CUSTOM_TAGS, + Capabilities.APM_TRACING_ENABLED, + Capabilities.APM_TRACING_HTTP_HEADER_TAGS, + Capabilities.APM_TRACING_LOGS_INJECTION, + Capabilities.APM_TRACING_SAMPLE_RATE, + Capabilities.APM_TRACING_SAMPLE_RULES, + Capabilities.APM_TRACING_MULTICONFIG, + } + + assert rc.resolve_sdk_configuration_contract(capabilities) is False + + +@scenarios.test_the_test +def test_resolve_sdk_configuration_contract_waits_for_apm_capabilities(): + """Capability registration is inconclusive until an APM_TRACING capability is present.""" - # Only ASM capabilities registered so far: nothing to conclude, ask again later assert rc.resolve_sdk_configuration_contract({Capabilities.ASM_ACTIVATION}) is None assert rc.resolve_sdk_configuration_contract(set()) is None - # A libdatadog library mid-startup: bit 49 is registered with the other AppSec capabilities, - # before any APM_TRACING one. The absence of the per-setting bits is not evidence of the - # unified contract yet, so this must stay inconclusive rather than be cached as sdk_config. assert ( rc.resolve_sdk_configuration_contract( { @@ -248,7 +274,6 @@ def test_resolve_sdk_configuration_contract(): is None ) - # APM_TRACING registered but no SDK_CONFIGURATION: conclusively lib_config assert ( rc.resolve_sdk_configuration_contract( {Capabilities.APM_TRACING_MULTICONFIG, Capabilities.APM_TRACING_ENABLE_CODE_ORIGIN} diff --git a/utils/_remote_config.py b/utils/_remote_config.py index 77309523b80..7dc11cefca0 100644 --- a/utils/_remote_config.py +++ b/utils/_remote_config.py @@ -645,8 +645,9 @@ def to_sdk_config_payload(config: dict[str, Any]) -> dict[str, Any]: capability for capability in Capabilities if capability.name.startswith("APM_TRACING_") ) -# The per-setting APM_TRACING capabilities that SDK_CONFIGURATION replaces. A library on the new -# contract advertises the single SDK_CONFIGURATION bit instead of all of these. +# The per-setting capabilities replaced by the unified SDK_CONFIGURATION contract. Their presence +# alongside bit 49 is ambiguous: Node.js intentionally advertises both families, while libdatadog +# uses bit 49 for ASM_RAW_RESPONSE_BODY and still consumes the legacy lib_config payload. LEGACY_APM_TRACING_CAPABILITIES = frozenset( { Capabilities.APM_TRACING_CUSTOM_TAGS, @@ -659,30 +660,29 @@ def to_sdk_config_payload(config: dict[str, Any]) -> dict[str, Any]: ) -def resolve_sdk_configuration_contract(capabilities: set[Capabilities]) -> bool | None: +def resolve_sdk_configuration_contract( + capabilities: set[Capabilities], *, library_name: str | None = None +) -> bool | None: """Decide which APM_TRACING payload shape a set of advertised capabilities asks for. Returns True for `sdk_config`, False for `lib_config`, and None when the capabilities seen so far cannot tell, so the caller should look again later. - The SDK_CONFIGURATION bit alone is not enough to decide. Bit 49 is SDK_CONFIGURATION in the - remote config source of truth (dd-source `remote-config/shared/libs/rc/capabilities.go`), but - libdatadog hands the same bit to `ASM_RAW_RESPONSE_BODY`, so a libdatadog-based library such as - dd-trace-php advertises it while still reading `lib_config`. - - Dropping the per-setting capabilities is the whole point of the unified bit, so their absence - is what distinguishes the two. Absence only counts once the library has actually registered its - APM_TRACING remote config, though: capabilities are added as products start, and AppSec ones - come first, so an early poll from a libdatadog library shows bit 49 with no APM_TRACING bit yet - and would otherwise be mistaken for the unified contract. + Waiting for an APM capability prevents an unrelated product that registers first from deciding + which APM_TRACING payload shape to use. When bit 49 appears with legacy per-setting bits, only + Node.js treats it as SDK_CONFIGURATION; libdatadog libraries use the same bit for + ASM_RAW_RESPONSE_BODY and still consume lib_config. """ if not capabilities & APM_TRACING_CAPABILITIES: return None - if capabilities & LEGACY_APM_TRACING_CAPABILITIES: + if Capabilities.SDK_CONFIGURATION not in capabilities: return False - return Capabilities.SDK_CONFIGURATION in capabilities + if capabilities & LEGACY_APM_TRACING_CAPABILITIES: + return library_name == "nodejs" + + return True # Memoized once the capabilities are conclusive, both to skip re-scanning the whole /v0.7/config @@ -691,11 +691,14 @@ def resolve_sdk_configuration_contract(capabilities: set[Capabilities]) -> bool _sdk_configuration_support: dict[str, bool] = {} -def resolve_sdk_configuration_support(get_capabilities: Callable[[], set[Capabilities]]) -> bool: +def resolve_sdk_configuration_support( + get_capabilities: Callable[[], set[Capabilities]], *, library_name: str | None = None +) -> bool: """Whether the library reads its APM_TRACING settings from `sdk_config` instead of `lib_config`. - `get_capabilities` returns the capabilities the library currently advertises. How to obtain - them, and how long to wait for them, differs between the end-to-end interface and the + `get_capabilities` returns the capabilities the library currently advertises. `library_name` + resolves bit 49 when it appears alongside legacy per-setting capabilities. How to obtain the + capabilities, and how long to wait for them, differs between the end-to-end interface and the parametric test agent, so that part stays with the caller; everything after it is shared. Falls back to `lib_config` whenever the answer is not yet knowable, which is the safe @@ -711,7 +714,7 @@ def resolve_sdk_configuration_support(get_capabilities: Callable[[], set[Capabil logger.error(f"Could not read the RC capabilities ({e}), assuming no SDK_CONFIGURATION support") return False - supported = resolve_sdk_configuration_contract(capabilities) + supported = resolve_sdk_configuration_contract(capabilities, library_name=library_name) if supported is None: logger.info("No APM_TRACING capability advertised yet, sending lib_config for now") return False @@ -732,7 +735,7 @@ def library_supports_sdk_configuration() -> bool: logger.warning("No remote config request seen, assuming the library does not support SDK_CONFIGURATION") return False - return resolve_sdk_configuration_support(library.get_rc_capabilities) + return resolve_sdk_configuration_support(library.get_rc_capabilities, library_name=context.library.name) def build_apm_tracing_command(