Skip to content
Draft
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
4 changes: 2 additions & 2 deletions ddtrace/internal/openfeature/_remoteconfiguration.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,14 @@ def __call__(self, payloads: t.Sequence[Payload]) -> None:
_featureflag_rc_callback = FeatureFlagCallback()


def enable_featureflags_rc() -> None:
def enable_featureflags_rc(start_poller: bool = True) -> None:
log.debug("[%s][P: %s] Register FFE Remote Config Callback", os.getpid(), os.getppid())
remoteconfig_poller.register_callback(
FFE_FLAGS_PRODUCT,
_featureflag_rc_callback,
capabilities=[FFECapabilities.FFE_FLAG_CONFIGURATION_RULES],
)
remoteconfig_poller.enable_product(FFE_FLAGS_PRODUCT)
remoteconfig_poller.enable_product(FFE_FLAGS_PRODUCT, start_poller=start_poller)


def disable_featureflags_rc() -> None:
Expand Down
13 changes: 10 additions & 3 deletions ddtrace/internal/remoteconfig/products/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,16 @@ def _register_rc_products() -> None:

# Register for both AGENT_CONFIG and AGENT_TASK products (they share the same callback)
remoteconfig_poller.register_callback("AGENT_CONFIG", flare_callback)
remoteconfig_poller.enable_product("AGENT_CONFIG")
remoteconfig_poller.enable_product("AGENT_CONFIG", start_poller=False)
remoteconfig_poller.register_callback("AGENT_TASK", flare_callback)
remoteconfig_poller.enable_product("AGENT_TASK")
remoteconfig_poller.enable_product("AGENT_TASK", start_poller=False)

from ddtrace.internal.settings.openfeature import config as ffe_config

if ffe_config.experimental_flagging_provider_enabled:
from ddtrace.internal.openfeature._remoteconfiguration import enable_featureflags_rc

enable_featureflags_rc(start_poller=False)


def post_preload():
Expand All @@ -45,8 +52,8 @@ def enabled():
def start():
from ddtrace.internal.remoteconfig.worker import remoteconfig_poller

remoteconfig_poller.enable()
_register_rc_products()
remoteconfig_poller.enable()


def restart(join=False):
Expand Down
9 changes: 4 additions & 5 deletions ddtrace/internal/remoteconfig/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,6 @@ def register_callback(
capabilities: list of capabilities to register for this product
"""
try:
# Enable if this is the first product being registered
if not self._client._product_callbacks:
self.enable()

self._client.register_callback(product, callback)

# Check for potential conflicts in capabilities
Expand All @@ -189,7 +185,7 @@ def register_callback(
except Exception:
log.debug("error starting the RCM client", exc_info=True)

def enable_product(self, product: str) -> None:
def enable_product(self, product: str, start_poller: bool = True) -> None:
"""Enable a product to be included in client payloads.

When a product is enabled, it will be added to the 'products' list
Expand All @@ -198,8 +194,11 @@ def enable_product(self, product: str) -> None:

Args:
product: Product name to enable
start_poller: Whether to start the Remote Config poller after enabling the product
"""
self._client.enable_product(product)
if start_poller:
self.enable()

def disable_product(self, product: str) -> None:
"""Disable a product, removing it from client payloads.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
fixes:
- |
openfeature: Fixes an issue where the OpenFeature provider could miss feature flag rules in the
tracer's first Remote Configuration request, causing applications to use default flag values
until a later poll received the configuration.
57 changes: 57 additions & 0 deletions tests/internal/remoteconfig/test_remoteconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ def get_mock_encoded_msg_with_signed_errors(msg, path, signed_errors):
}


def _decode_capabilities(payload):
return int.from_bytes(base64.b64decode(payload["client"]["capabilities"]), "big")


def test_remote_config_register_auto_enable(remote_config_worker):
# ASM_FEATURES product is enabled by default, but LIVE_DEBUGGER isn't
class MockCallback(RCCallback):
Expand All @@ -157,6 +161,59 @@ def __call__(self, payloads):
remoteconfig_poller.disable()


def test_remote_config_enable_product_starts_after_product_is_in_payload(remote_config_worker):
class MockCallback(RCCallback):
def __call__(self, payloads):
pass

captured_payloads = []

def capture_initial_payload():
captured_payloads.append(remoteconfig_poller._client._build_payload({}))

with override_global_config(dict(_remote_config_enabled=True)):
with (
patch.object(remoteconfig_poller, "start", side_effect=capture_initial_payload),
patch.object(remoteconfig_poller._client, "start_subscriber"),
):
remoteconfig_poller.register_callback("LIVE_DEBUGGER", MockCallback())

assert captured_payloads == []

remoteconfig_poller.enable_product("LIVE_DEBUGGER")

assert "LIVE_DEBUGGER" in captured_payloads[0]["client"]["products"]


@pytest.mark.parametrize("ffe_enabled", [False, True])
def test_remote_config_start_registers_initial_payload_products(remote_config_worker, ffe_enabled):
from ddtrace.internal.openfeature._remoteconfiguration import FFE_FLAGS_PRODUCT
from ddtrace.internal.openfeature._remoteconfiguration import FFECapabilities
from ddtrace.internal.remoteconfig.products import client as remote_config_product

captured_payloads = []

def capture_initial_payload():
captured_payloads.append(remoteconfig_poller._client._build_payload({}))

with override_global_config(dict(_remote_config_enabled=True, experimental_flagging_provider_enabled=ffe_enabled)):
with (
patch.object(remoteconfig_poller, "start", side_effect=capture_initial_payload),
patch.object(remoteconfig_poller._client, "start_subscriber"),
):
remote_config_product.start()

assert captured_payloads
products = set(captured_payloads[0]["client"]["products"])
assert {"AGENT_CONFIG", "AGENT_TASK"} <= products

if ffe_enabled:
assert FFE_FLAGS_PRODUCT in products
assert _decode_capabilities(captured_payloads[0]) & FFECapabilities.FFE_FLAG_CONFIGURATION_RULES
else:
assert FFE_FLAGS_PRODUCT not in products


def test_remote_config_register_validate_rc_disabled(remote_config_worker):
remoteconfig_poller.disable()

Expand Down
Loading