diff --git a/ddtrace/internal/settings/openfeature.py b/ddtrace/internal/settings/openfeature.py index 78a653263f5..2037b7ca785 100644 --- a/ddtrace/internal/settings/openfeature.py +++ b/ddtrace/internal/settings/openfeature.py @@ -2,11 +2,43 @@ OpenFeature configuration settings. """ +from typing import Callable from typing import Optional +from ddtrace.internal.logger import get_logger from ddtrace.internal.settings._core import DDConfig +log = get_logger(__name__) + + +# AIDEV-NOTE: numeric settings here parse leniently on purpose. This class is instantiated at +# module scope (see the bottom of this file), so letting envier raise on an unparsable value +# turns it into an ImportError for ddtrace.openfeature and takes the whole application down at +# startup rather than degrading one setting. dd-trace-java substitutes the default in the same +# situation; match that. +def _lenient_int(env_name: str, default: int) -> Callable[[str], int]: + def parse(raw: str) -> int: + try: + return int(raw) + except ValueError: + log.warning("Invalid value for %s: %r is not an integer; using the default", env_name, raw) + return default + + return parse + + +def _lenient_float(env_name: str, default: float) -> Callable[[str], float]: + def parse(raw: str) -> float: + try: + return float(raw) + except ValueError: + log.warning("Invalid value for %s: %r is not a number; using the default", env_name, raw) + return default + + return parse + + class OpenFeatureConfig(DDConfig): """ Configuration for OpenFeature provider and exposure reporting. @@ -48,6 +80,7 @@ class OpenFeatureConfig(DDConfig): float, "DD_FFE_INTAKE_HEARTBEAT_INTERVAL", default=1.0, + parser=_lenient_float("DD_FFE_INTAKE_HEARTBEAT_INTERVAL", 1.0), ) # Provider initialization timeout in milliseconds. Controls how long initialize() @@ -61,6 +94,7 @@ class OpenFeatureConfig(DDConfig): int, "DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS", default=10000, + parser=_lenient_int("DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS", 10000), ) # Stable Feature Flagging kill switch. When False, the provider is disabled @@ -96,6 +130,7 @@ class OpenFeatureConfig(DDConfig): int, "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS", default=30, + parser=_lenient_int("DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS", 30), ) # Agentless UFC per-request timeout in seconds. @@ -103,6 +138,7 @@ class OpenFeatureConfig(DDConfig): int, "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS", default=5, + parser=_lenient_int("DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS", 5), ) _openfeature_config_keys = [ diff --git a/releasenotes/notes/openfeature-invalid-numeric-config-3f1c8a24b6d90e75.yaml b/releasenotes/notes/openfeature-invalid-numeric-config-3f1c8a24b6d90e75.yaml new file mode 100644 index 00000000000..13913827e35 --- /dev/null +++ b/releasenotes/notes/openfeature-invalid-numeric-config-3f1c8a24b6d90e75.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + openfeature: This fix resolves an issue where an invalid value for a numeric Feature + Flagging environment variable raised an exception while importing ``ddtrace.openfeature``, + preventing the application from starting. Such values are now logged and the documented + default is used instead. diff --git a/tests/openfeature/test_source_selection.py b/tests/openfeature/test_source_selection.py index dd743b2efae..04a07b34a10 100644 --- a/tests/openfeature/test_source_selection.py +++ b/tests/openfeature/test_source_selection.py @@ -130,3 +130,40 @@ def test_create_invalid_endpoint_returns_none(bad_url): cfg = _config(DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL=bad_url) with override_global_config({"_dd_api_key": "secret"}): assert create_agentless_source(cfg, lambda _: None) is None + + +# --------------------------------------------------------------------------- +# Numeric settings degrade instead of breaking the import +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + ("env_name", "attribute", "expected"), + [ + ( + "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS", + "configuration_source_agentless_poll_interval_seconds", + 30, + ), + ( + "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS", + "configuration_source_agentless_request_timeout_seconds", + 5, + ), + ( + "DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS", + "initialization_timeout_ms", + 10000, + ), + ], +) +def test_unparsable_integer_setting_falls_back_to_default(monkeypatch, env_name, attribute, expected): + # OpenFeatureConfig is built at module scope, so raising here would surface as an + # ImportError for ddtrace.openfeature and take the application down at startup. + monkeypatch.setenv(env_name, "0.2") + assert getattr(OpenFeatureConfig(), attribute) == expected + + +def test_unparsable_float_setting_falls_back_to_default(monkeypatch): + monkeypatch.setenv("DD_FFE_INTAKE_HEARTBEAT_INTERVAL", "not-a-number") + assert OpenFeatureConfig().ffe_intake_heartbeat_interval == 1.0