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
11 changes: 6 additions & 5 deletions tests/parametric/capabilities.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
13 changes: 7 additions & 6 deletions tests/parametric/test_dynamic_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
)
Expand Down
91 changes: 58 additions & 33 deletions tests/test_the_test/test_remote_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
{
Expand All @@ -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}
Expand Down
43 changes: 23 additions & 20 deletions utils/_remote_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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(
Expand Down
Loading